Hero: every path moves on its own, and the tip glides
The trace advanced a whole sample at a time (floor of points x progress) - visible steps; the tip now interpolates between samples. The single field-wide rotate is gone: each path turns at its own seeded rate and direction about its own center, drifts on its own small orbit, carries its own wave and traces itself at its own tempo. Every rate is an integer number of cycles, so the loop stays seamless. Same generator feeds the paint worklet and the canvas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ecd77fae35
commit
588e35fd44
@@ -72,48 +72,96 @@ export function turningFunctionToPoints(turning, samples = 14) {
|
|||||||
return pts;
|
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) {
|
export function makePaths(seed, count, stops = 10) {
|
||||||
const rand = mulberry32(seed);
|
const rand = mulberry32(seed);
|
||||||
const paths = [];
|
const paths = [];
|
||||||
for (let i = 0; i < count; i++) {
|
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;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
// t in [0, 1) is the animation phase. Each path traces itself in over
|
// t in [0, 1) is the animation phase. Each path traces itself in at
|
||||||
// one cycle, offset by its index so the field is always mid-draw, and
|
// its own tempo and phase, fades as it completes, turns at its own
|
||||||
// fades as it completes; the whole field drifts slowly and a soft
|
// rate about its own center and drifts on its own small orbit; a wave
|
||||||
// wave runs through every line. `colors` is [accent, dim] - the
|
// with its own frequency runs along it. The tip is interpolated
|
||||||
// palette comes from the page's own custom properties either way.
|
// 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) {
|
export function drawPaths(ctx, width, height, paths, t, colors) {
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
const s = Math.min(width, height) * 0.9;
|
const s = Math.min(width, height) * 0.9;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(width / 2, height / 2);
|
ctx.translate(width / 2, height / 2);
|
||||||
ctx.rotate(t * Math.PI * 2 * 0.08);
|
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = 'round';
|
||||||
const n = paths.length;
|
const TAU = Math.PI * 2;
|
||||||
const wave = t * Math.PI * 2;
|
for (let i = 0; i < paths.length; i++) {
|
||||||
for (let i = 0; i < n; i++) {
|
const p = paths[i];
|
||||||
const pts = paths[i];
|
const phase = (t * p.tempo + p.phase) % 1;
|
||||||
const phase = (t + i / n) % 1;
|
// ease: quick to appear, long to complete, then fade
|
||||||
// ease: quick to appear, long to complete
|
const progress = smooth(Math.min(1, phase * 1.35));
|
||||||
const progress = Math.min(1, phase * 1.35);
|
|
||||||
const count = Math.max(2, Math.floor((pts.length / 2) * progress));
|
|
||||||
const alpha = phase < 0.85 ? 0.75 : 0.75 * (1 - (phase - 0.85) / 0.15);
|
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.strokeStyle = i % 3 === 0 ? colors[0] : colors[1];
|
||||||
ctx.globalAlpha = alpha * (i % 3 === 0 ? 1 : 0.55);
|
ctx.globalAlpha = alpha * (i % 3 === 0 ? 1 : 0.55);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (let k = 0; k < count; k++) {
|
const at = (k) => {
|
||||||
const x = pts[k * 2], y = pts[k * 2 + 1];
|
const x = p.pts[k * 2] - p.cx, y = p.pts[k * 2 + 1] - p.cy;
|
||||||
const wy = y + Math.sin(x * 7 + wave + i) * 0.008;
|
return [x, y + Math.sin(x * p.waveF + wave) * p.waveA];
|
||||||
if (k === 0) ctx.moveTo(x * s, wy * s);
|
};
|
||||||
else ctx.lineTo(x * s, wy * s);
|
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.stroke();
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user