diff --git a/paths.js b/paths.js index c783855..774409d 100644 --- a/paths.js +++ b/paths.js @@ -72,48 +72,96 @@ export function turningFunctionToPoints(turning, samples = 14) { return pts; } +// A path plus the motion that is its own: where it sits, how it +// spins, how it drifts, how fast it traces itself in. Every rate is +// an integer number of cycles so the field loops seamlessly at t = 1. export function makePaths(seed, count, stops = 10) { const rand = mulberry32(seed); const paths = []; for (let i = 0; i < count; i++) { - paths.push(turningFunctionToPoints(createTurningFunction(rand, stops))); + const pts = turningFunctionToPoints(createTurningFunction(rand, stops)); + let cx = 0, cy = 0; + for (let k = 0; k < pts.length; k += 2) { cx += pts[k]; cy += pts[k + 1]; } + cx /= pts.length / 2; + cy /= pts.length / 2; + const sign = rand() > 0.5 ? 1 : -1; + paths.push({ + pts, + cx, + cy, + // own turns per cycle: a third sit still, the rest turn one + // or two full turns either way over the cycle + spin: rand() < 0.33 ? 0 : sign * (1 + Math.floor(rand() * 2)), + // slow orbit around its own place, radius in frame units + driftR: 0.01 + rand() * 0.03, + driftK: 1 + Math.floor(rand() * 3), + driftPhase: rand() * Math.PI * 2, + // its own wave along the line + waveF: 4 + rand() * 6, + waveK: 1 + Math.floor(rand() * 2), + waveA: 0.004 + rand() * 0.008, + // tracing tempo: how many times it redraws itself per cycle + tempo: 1 + Math.floor(rand() * 2), + phase: rand(), + }); } return paths; } -// t in [0, 1) is the animation phase. Each path traces itself in over -// one cycle, offset by its index so the field is always mid-draw, and -// fades as it completes; the whole field drifts slowly and a soft -// wave runs through every line. `colors` is [accent, dim] - the -// palette comes from the page's own custom properties either way. +// t in [0, 1) is the animation phase. Each path traces itself in at +// its own tempo and phase, fades as it completes, turns at its own +// rate about its own center and drifts on its own small orbit; a wave +// with its own frequency runs along it. The tip is interpolated +// between samples so a trace glides instead of stepping. `colors` is +// [accent, dim] - the palette comes from the page's custom properties. +const smooth = (u) => u * u * (3 - 2 * u); + export function drawPaths(ctx, width, height, paths, t, colors) { ctx.clearRect(0, 0, width, height); const s = Math.min(width, height) * 0.9; ctx.save(); ctx.translate(width / 2, height / 2); - ctx.rotate(t * Math.PI * 2 * 0.08); ctx.lineWidth = 1; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; - const n = paths.length; - const wave = t * Math.PI * 2; - for (let i = 0; i < n; i++) { - const pts = paths[i]; - const phase = (t + i / n) % 1; - // ease: quick to appear, long to complete - const progress = Math.min(1, phase * 1.35); - const count = Math.max(2, Math.floor((pts.length / 2) * progress)); + const TAU = Math.PI * 2; + for (let i = 0; i < paths.length; i++) { + const p = paths[i]; + const phase = (t * p.tempo + p.phase) % 1; + // ease: quick to appear, long to complete, then fade + const progress = smooth(Math.min(1, phase * 1.35)); const alpha = phase < 0.85 ? 0.75 : 0.75 * (1 - (phase - 0.85) / 0.15); + const total = p.pts.length / 2; + const tip = 1 + (total - 1) * progress; // fractional sample index + const whole = Math.floor(tip); + const frac = tip - whole; + + const angle = p.spin * TAU * t; + const dx = Math.cos(p.driftPhase + p.driftK * TAU * t) * p.driftR; + const dy = Math.sin(p.driftPhase + p.driftK * TAU * t) * p.driftR; + const wave = p.waveK * TAU * t; + + ctx.save(); + ctx.translate((p.cx + dx) * s, (p.cy + dy) * s); + ctx.rotate(angle); ctx.strokeStyle = i % 3 === 0 ? colors[0] : colors[1]; ctx.globalAlpha = alpha * (i % 3 === 0 ? 1 : 0.55); ctx.beginPath(); - for (let k = 0; k < count; k++) { - const x = pts[k * 2], y = pts[k * 2 + 1]; - const wy = y + Math.sin(x * 7 + wave + i) * 0.008; - if (k === 0) ctx.moveTo(x * s, wy * s); - else ctx.lineTo(x * s, wy * s); + const at = (k) => { + const x = p.pts[k * 2] - p.cx, y = p.pts[k * 2 + 1] - p.cy; + return [x, y + Math.sin(x * p.waveF + wave) * p.waveA]; + }; + for (let k = 0; k <= Math.min(whole, total - 1); k++) { + const [x, y] = at(k); + if (k === 0) ctx.moveTo(x * s, y * s); + else ctx.lineTo(x * s, y * s); + } + if (whole < total - 1 && frac > 0) { + const [ax, ay] = at(whole), [bx, by] = at(whole + 1); + ctx.lineTo((ax + (bx - ax) * frac) * s, (ay + (by - ay) * frac) * s); } ctx.stroke(); + ctx.restore(); } ctx.restore(); }