120 lines
4.8 KiB
JavaScript
120 lines
4.8 KiB
JavaScript
// The generator behind redoal.com's hero - a port of lysbue's
|
|
// "paths" CSS paint worklet (static/worklet/paint/paths.js): a path is
|
|
// a turning function, a list of (angle shift, distance) stops walked
|
|
// from the center and smoothed into quadratic arcs. Shared verbatim by
|
|
// the paint worklet (paths-worklet.js, where the browser has the CSS
|
|
// Paint API) and the canvas fallback (hero.js) so both draw the same
|
|
// field. Everything is seeded: a worklet is instantiated whenever the
|
|
// engine likes, and unseeded randomness would reshuffle the picture on
|
|
// every repaint.
|
|
|
|
export function mulberry32(seed) {
|
|
let a = seed >>> 0;
|
|
return () => {
|
|
a |= 0;
|
|
a = (a + 0x6d2b79f5) | 0;
|
|
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
};
|
|
}
|
|
|
|
function isInBounds(x, y) {
|
|
return x > -0.5 && x < 0.5 && y > -0.5 && y < 0.5;
|
|
}
|
|
|
|
// Flat array of (angle shift, distance) pairs; distance 1 = the
|
|
// frame's short side. The first pair is the start: absolute angle and
|
|
// distance from the center.
|
|
export function createTurningFunction(rand, stopCount) {
|
|
const turning = [rand() * Math.PI * 2, rand() * 0.3];
|
|
let radianSum = turning[0];
|
|
let x = Math.cos(turning[0]) * turning[1];
|
|
let y = Math.sin(turning[0]) * turning[1];
|
|
for (let i = 0; i < stopCount; i++) {
|
|
let shiftAngle = 0, shiftDistance = 0.1, next = radianSum;
|
|
for (let j = 0; j < 80; j++) {
|
|
shiftAngle = Math.pow(1 + rand() * 4, -2) * Math.PI * 2 * (rand() > 0.5 ? -1 : 1);
|
|
shiftDistance = Math.max(rand() * 0.25, 0.05);
|
|
next = radianSum + shiftAngle;
|
|
if (isInBounds(x + Math.cos(next) * shiftDistance, y + Math.sin(next) * shiftDistance)) break;
|
|
}
|
|
radianSum = next;
|
|
x += Math.cos(radianSum) * shiftDistance;
|
|
y += Math.sin(radianSum) * shiftDistance;
|
|
turning.push(shiftAngle, shiftDistance);
|
|
}
|
|
return turning;
|
|
}
|
|
|
|
// Smooth the stops into quadratic arcs (control point = half a step
|
|
// along the previous heading, like the original) and flatten each arc
|
|
// into `samples` points so the curve can be traced progressively.
|
|
export function turningFunctionToPoints(turning, samples = 14) {
|
|
const pts = [];
|
|
let heading = turning[0];
|
|
let x = Math.cos(turning[0]) * turning[1];
|
|
let y = Math.sin(turning[0]) * turning[1];
|
|
pts.push(x, y);
|
|
for (let i = 2; i < turning.length; i += 2) {
|
|
const cx = x + Math.cos(heading) * (turning[i + 1] / 2);
|
|
const cy = y + Math.sin(heading) * (turning[i + 1] / 2);
|
|
heading += turning[i];
|
|
const nx = x + Math.cos(heading) * turning[i + 1];
|
|
const ny = y + Math.sin(heading) * turning[i + 1];
|
|
for (let s = 1; s <= samples; s++) {
|
|
const u = s / samples, v = 1 - u;
|
|
pts.push(v * v * x + 2 * v * u * cx + u * u * nx, v * v * y + 2 * v * u * cy + u * u * ny);
|
|
}
|
|
x = nx;
|
|
y = ny;
|
|
}
|
|
return pts;
|
|
}
|
|
|
|
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)));
|
|
}
|
|
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.
|
|
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 alpha = phase < 0.85 ? 0.75 : 0.75 * (1 - (phase - 0.85) / 0.15);
|
|
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);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
}
|