motion-library 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -0
- package/README.md +37 -2
- package/anims/basics/anticipation.js +56 -0
- package/anims/basics/card-flip.js +39 -0
- package/anims/basics/easing-race.js +57 -0
- package/anims/basics/fade-scale.js +44 -0
- package/anims/basics/stagger-grid.js +43 -0
- package/anims/chrome/black-hole.js +81 -0
- package/anims/chrome/cloth-reveal.js +156 -0
- package/anims/chrome/dna-helix.js +52 -0
- package/anims/chrome/dust-bust.js +109 -0
- package/anims/chrome/extruded-type.js +64 -0
- package/anims/chrome/fractal-tree.js +75 -0
- package/anims/chrome/gem.js +111 -0
- package/anims/chrome/geode.js +102 -0
- package/anims/chrome/glass-lens.js +73 -0
- package/anims/chrome/glass-prism.js +95 -0
- package/anims/chrome/gyroid.js +69 -0
- package/anims/chrome/holo-card.js +52 -0
- package/anims/chrome/infinite-tunnel.js +81 -0
- package/anims/chrome/inflated-type.js +141 -0
- package/anims/chrome/isometric-city.js +97 -0
- package/anims/chrome/jelly-cube.js +329 -0
- package/anims/chrome/klein-bottle.js +80 -0
- package/anims/chrome/liquid-metal.js +94 -0
- package/anims/chrome/low-poly-planet.js +163 -0
- package/anims/chrome/mandelbulb.js +79 -0
- package/anims/chrome/melting-logo.js +126 -0
- package/anims/chrome/menger-sponge.js +79 -0
- package/anims/chrome/neon-geodesic.js +149 -0
- package/anims/chrome/paper-fold.js +135 -0
- package/anims/chrome/path-spheres.js +112 -0
- package/anims/chrome/point-globe.js +81 -0
- package/anims/chrome/portal.js +105 -0
- package/anims/chrome/rubiks-cube.js +74 -0
- package/anims/chrome/sdf-morph.js +129 -0
- package/anims/chrome/shatter.js +120 -0
- package/anims/chrome/soap-bubble.js +86 -0
- package/anims/chrome/terrain-flyover.js +78 -0
- package/anims/chrome/tesseract.js +148 -0
- package/anims/chrome/torus-knot.js +89 -0
- package/anims/chrome/volumetric-clouds.js +70 -0
- package/anims/flow/bio-shore.js +140 -0
- package/anims/flow/calligraphy.js +116 -0
- package/anims/flow/chladni.js +152 -0
- package/anims/flow/cloud-chamber.js +101 -0
- package/anims/flow/confetti.js +46 -0
- package/anims/flow/curl-flow.js +52 -0
- package/anims/flow/dandelion.js +80 -0
- package/anims/flow/dla-crystal.js +57 -0
- package/anims/flow/drone-show.js +357 -0
- package/anims/flow/embers.js +63 -0
- package/anims/flow/firefly-sync.js +107 -0
- package/anims/flow/galaxy.js +69 -0
- package/anims/flow/galton-board.js +155 -0
- package/anims/flow/gpu-particles.js +73 -0
- package/anims/flow/harmonograph.js +61 -0
- package/anims/flow/holi.js +153 -0
- package/anims/flow/iron-filings.js +112 -0
- package/anims/flow/lightning.js +72 -0
- package/anims/flow/murmuration.js +105 -0
- package/anims/flow/orbitals.js +195 -0
- package/anims/flow/particle-life.js +87 -0
- package/anims/flow/physarum.js +171 -0
- package/anims/flow/pin-art.js +181 -0
- package/anims/flow/rainbow.js +127 -0
- package/anims/flow/roche-limit.js +161 -0
- package/anims/flow/sand-mandala.js +121 -0
- package/anims/flow/scramble-crossing.js +152 -0
- package/anims/flow/snowfall.js +84 -0
- package/anims/flow/sparks.js +71 -0
- package/anims/flow/spider-web.js +123 -0
- package/anims/flow/starry-night.js +121 -0
- package/anims/flow/states-of-matter.js +113 -0
- package/anims/flow/strange-attractor.js +54 -0
- package/anims/flow/wheat-field.js +186 -0
- package/anims/flow/wind-tunnel.js +227 -0
- package/anims/fluid/bubbles.js +152 -0
- package/anims/fluid/caustics.js +45 -0
- package/anims/fluid/dripping-text.js +53 -0
- package/anims/fluid/ferrofluid.js +56 -0
- package/anims/fluid/fire-smoke.js +83 -0
- package/anims/fluid/honey-drip.js +167 -0
- package/anims/fluid/ink-drop.js +50 -0
- package/anims/fluid/ink-fluid.js +125 -0
- package/anims/fluid/lava-lamp.js +56 -0
- package/anims/fluid/liquid-distort.js +64 -0
- package/anims/fluid/liquid-glass-fill.js +53 -0
- package/anims/fluid/marbling.js +53 -0
- package/anims/fluid/mercury.js +177 -0
- package/anims/fluid/ocean-waves.js +50 -0
- package/anims/fluid/paint-swirl.js +153 -0
- package/anims/fluid/raindrops-glass.js +60 -0
- package/anims/fluid/ripples.js +57 -0
- package/anims/fluid/sph-water.js +75 -0
- package/anims/fluid/underwater.js +55 -0
- package/anims/fluid/water-surface-3d.js +98 -0
- package/anims/hover/cursor-trail.js +37 -0
- package/anims/hover/magnetic.js +41 -0
- package/anims/hover/ripple.js +31 -0
- package/anims/hover/spotlight.js +31 -0
- package/anims/hover/tilt.js +36 -0
- package/anims/micro/arc-spinner.js +22 -0
- package/anims/micro/like-burst.js +42 -0
- package/anims/micro/skeleton.js +36 -0
- package/anims/micro/submit-success.js +45 -0
- package/anims/micro/toggle.js +35 -0
- package/anims/morph/gooey.js +36 -0
- package/anims/morph/icon-morph.js +45 -0
- package/anims/morph/line-draw.js +34 -0
- package/anims/morph/liquid-button.js +53 -0
- package/anims/morph/shape-morph.js +61 -0
- package/anims/registry-more.js +327 -0
- package/anims/registry.js +294 -0
- package/anims/scroll/parallax.js +50 -0
- package/anims/scroll/pinned-track.js +40 -0
- package/anims/scroll/scrub-reveal.js +34 -0
- package/anims/scroll/velocity-skew.js +38 -0
- package/anims/scroll/zoom-through.js +41 -0
- package/anims/shaders/aurora.js +47 -0
- package/anims/shaders/dither.js +69 -0
- package/anims/shaders/domain-warp.js +52 -0
- package/anims/shaders/guilloche.js +42 -0
- package/anims/shaders/kaleidoscope.js +64 -0
- package/anims/shaders/reaction-diffusion.js +85 -0
- package/anims/shaders/rgb-glitch.js +56 -0
- package/anims/shaders/truchet.js +57 -0
- package/anims/shaders/voronoi.js +50 -0
- package/anims/springs/ball-collisions.js +47 -0
- package/anims/springs/pendulum-wave.js +35 -0
- package/anims/springs/rubber-band.js +50 -0
- package/anims/springs/squash-bounce.js +50 -0
- package/anims/springs/verlet-rope.js +46 -0
- package/anims/text/letter-rise.js +40 -0
- package/anims/text/line-mask.js +26 -0
- package/anims/text/scramble.js +32 -0
- package/anims/text/typewriter.js +35 -0
- package/anims/text/vf-wave.js +26 -0
- package/anims/transitions/circle-reveal.js +42 -0
- package/anims/transitions/push-slide.js +40 -0
- package/anims/transitions/shared-expand.js +43 -0
- package/anims/transitions/staircase-wipe.js +41 -0
- package/anims/transitions/view-transition.js +43 -0
- package/bin.mjs +155 -0
- package/js/code.js +122 -0
- package/package.json +10 -4
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Cloud chamber: radiation made visible. The air is supersaturated with alcohol vapour, so every charged particle
|
|
2
|
+
// that passes leaves a trail of droplets, lit from the side. A thoriated welding rod sends out alpha particles:
|
|
3
|
+
// short, fat, dead-straight tracks. Beta electrons are thin and wander, and in the magnet under the chamber they
|
|
4
|
+
// curl into tightening spirals as they lose energy. Every few seconds a cosmic-ray muon cuts clean across the whole
|
|
5
|
+
// chamber, sometimes knocking loose a little electron of its own (a delta ray). Trails fade, swell and drift.
|
|
6
|
+
const PARAMS = { activity: 1, field: 1, mist: 1 };
|
|
7
|
+
|
|
8
|
+
export default function (stage) {
|
|
9
|
+
const canvas = document.createElement('canvas');
|
|
10
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block;background:#020304';
|
|
11
|
+
stage.append(canvas);
|
|
12
|
+
const ctx = canvas.getContext('2d');
|
|
13
|
+
let W = 1, H = 1, S = 1;
|
|
14
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1; S = Math.min(W, H); canvas.width = Math.max(1, (W * d) | 0); canvas.height = Math.max(1, (H * d) | 0); ctx.setTransform(d, 0, 0, d, 0, 0); };
|
|
15
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
16
|
+
// one soft droplet, drawn once and stamped thousands of times
|
|
17
|
+
const dot = document.createElement('canvas'); dot.width = dot.height = 32;
|
|
18
|
+
{ const g = dot.getContext('2d'), r = g.createRadialGradient(16, 16, 0, 16, 16, 16); r.addColorStop(0, 'rgba(235,242,255,1)'); r.addColorStop(0.35, 'rgba(200,215,240,.55)'); r.addColorStop(1, 'rgba(160,180,220,0)'); g.fillStyle = r; g.fillRect(0, 0, 32, 32); }
|
|
19
|
+
// the mist: a little noise canvas that drifts, scaled up soft
|
|
20
|
+
const FW = 128, FH = 80, fog = document.createElement('canvas'); fog.width = FW; fog.height = FH; const fg = fog.getContext('2d'), fimg = fg.createImageData(FW, FH);
|
|
21
|
+
const hash = (x, y) => { const s = Math.sin(x * 127.1 + y * 311.7) * 43758.5453; return s - Math.floor(s); };
|
|
22
|
+
const vn = (x, y) => { const xi = Math.floor(x), yi = Math.floor(y), u = x - xi, v = y - yi, a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1), d = hash(xi + 1, yi + 1), su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); return a + (b - a) * su + (c - a) * sv + (a - b - c + d) * su * sv; };
|
|
23
|
+
|
|
24
|
+
const tracks = [], rand = (a, b) => a + Math.random() * (b - a);
|
|
25
|
+
const source = () => ({ x: W * 0.17, y: H * 0.7 });
|
|
26
|
+
// a track is a list of droplets, each with its own spread so the trail diffuses as it ages
|
|
27
|
+
function addTrack(kind, x, y, ang, len) {
|
|
28
|
+
const drops = [], B = 0.9 * PARAMS.field;
|
|
29
|
+
let px = x, py = y, a = ang, s = 0;
|
|
30
|
+
const step = kind === 'alpha' ? 1.1 : kind === 'muon' ? 2.2 : 1.6, sign = Math.random() < 0.9 ? 1 : -1; // the odd positron curls the other way
|
|
31
|
+
while (s < len) {
|
|
32
|
+
const left = 1 - s / len;
|
|
33
|
+
if (kind === 'beta' || kind === 'delta') {
|
|
34
|
+
a += sign * B * step / (S * 0.32 * Math.pow(Math.max(0.05, left), 0.8)); // tighter as it slows: a spiral
|
|
35
|
+
a += (Math.random() - 0.5) * (0.05 + 0.5 * (1 - left) ** 2); // and multiple scattering
|
|
36
|
+
} else if (kind === 'alpha') { if (left < 0.12) a += (Math.random() - 0.5) * 0.12; }
|
|
37
|
+
px += Math.cos(a) * step; py += Math.sin(a) * step; s += step;
|
|
38
|
+
if (px < -20 || px > W + 20 || py < -20 || py > H + 20) break;
|
|
39
|
+
const n = kind === 'alpha' ? 1 : kind === 'muon' ? (Math.random() < 0.6 ? 1 : 0) : (Math.random() < 0.75 ? 1 : 0);
|
|
40
|
+
for (let k = 0; k < n; k++) drops.push({ x: px + rand(-0.6, 0.6), y: py + rand(-0.6, 0.6), dx: rand(-1, 1), dy: rand(-1, 1), r: kind === 'alpha' ? rand(1.1, 1.9) * (1 + (1 - left) * 0.6) : rand(0.6, 1.1) });
|
|
41
|
+
if (kind === 'muon' && Math.random() < 0.0016) addTrack('delta', px, py, a + rand(-1.6, 1.6), rand(0.04, 0.1) * S);
|
|
42
|
+
}
|
|
43
|
+
tracks.push({ kind, drops, age: 0, life: kind === 'alpha' ? 2.8 : kind === 'muon' ? 3 : 2.5, bright: kind === 'alpha' ? 0.2 : kind === 'muon' ? 0.5 : 0.62 });
|
|
44
|
+
}
|
|
45
|
+
const emit = kind => {
|
|
46
|
+
if (kind === 'alpha') { const s = source(); addTrack('alpha', s.x + rand(-2, 2), s.y + rand(-2, 2), rand(-1.9, 0.7), rand(0.16, 0.28) * S); }
|
|
47
|
+
else if (kind === 'beta') { const fromRod = Math.random() < 0.5, s = source(); addTrack('beta', fromRod ? s.x : rand(0, W), fromRod ? s.y : rand(0, H), rand(0, 6.283), rand(0.2, 0.75) * S); }
|
|
48
|
+
else { // a muon: in at one edge, straight out the other
|
|
49
|
+
const a = rand(0, 6.283), cx = rand(W * 0.2, W * 0.8), cy = rand(H * 0.2, H * 0.8), R = Math.hypot(W, H);
|
|
50
|
+
addTrack('muon', cx - Math.cos(a) * R / 2, cy - Math.sin(a) * R / 2, a, R);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const rates = { alpha: 2.2, beta: 2.2, muon: 0.45 };
|
|
54
|
+
const acc = { alpha: 0.8, beta: 0.3, muon: 0.85 };
|
|
55
|
+
// a little backlog so the chamber is never empty when it first appears
|
|
56
|
+
for (let i = 0; i < 3; i++) emit('alpha'); emit('beta'); emit('muon');
|
|
57
|
+
tracks.forEach((t, i) => { t.age = 0.15 + i * 0.25; });
|
|
58
|
+
|
|
59
|
+
let raf, last = performance.now(), time = 0;
|
|
60
|
+
function frame(now) {
|
|
61
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; time += dt;
|
|
62
|
+
for (const k in rates) { acc[k] += dt * rates[k] * PARAMS.activity; while (acc[k] > 1) { acc[k] -= 1 + Math.random() * 0.6; emit(k); } }
|
|
63
|
+
// the chamber floor, lit by a lamp off the left edge
|
|
64
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
65
|
+
const bg = ctx.createLinearGradient(0, 0, W, 0); bg.addColorStop(0, '#0b0f15'); bg.addColorStop(0.35, '#040507'); bg.addColorStop(1, '#020203');
|
|
66
|
+
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
|
|
67
|
+
// mist drifting across the light
|
|
68
|
+
for (let y = 0; y < FH; y++) for (let x = 0; x < FW; x++) {
|
|
69
|
+
const n = vn(x * 0.07 + time * 0.25, y * 0.085 - time * 0.08) * 0.6 + vn(x * 0.19 - time * 0.4, y * 0.19 + time * 0.1) * 0.4, i = (y * FW + x) * 4;
|
|
70
|
+
const v = Math.max(0, n - 0.3) * 55 * PARAMS.mist * (1.2 - x / FW);
|
|
71
|
+
fimg.data[i] = 170; fimg.data[i + 1] = 185; fimg.data[i + 2] = 215; fimg.data[i + 3] = v;
|
|
72
|
+
}
|
|
73
|
+
fg.putImageData(fimg, 0, 0); ctx.imageSmoothingEnabled = true; ctx.drawImage(fog, 0, 0, W, H);
|
|
74
|
+
// the source: a grey rod reaching in from the corner
|
|
75
|
+
const s = source();
|
|
76
|
+
ctx.save(); ctx.translate(s.x, s.y); ctx.rotate(Math.PI * 0.82);
|
|
77
|
+
const rg = ctx.createLinearGradient(0, -4, 0, 4); rg.addColorStop(0, '#9aa0a8'); rg.addColorStop(0.5, '#e4e7ea'); rg.addColorStop(1, '#4a4e55');
|
|
78
|
+
ctx.fillStyle = rg; ctx.fillRect(0, -3.2, S * 0.5, 6.4); ctx.beginPath(); ctx.arc(0, 0, 3.2, Math.PI / 2, -Math.PI / 2); ctx.fill(); ctx.restore();
|
|
79
|
+
// the tracks, and a fine drizzle of droplets falling out of the sensitive layer
|
|
80
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
81
|
+
for (let i = 0; i < 140; i++) {
|
|
82
|
+
const h = (i * 0.6180339 + Math.floor(time * 0.7 + i * 0.37) * 0.17) % 1, x = h * W, y = ((i * 0.7548776) % 1) * H + ((time * 0.7 + i * 0.37) % 1) * 8;
|
|
83
|
+
ctx.globalAlpha = 0.18 * Math.sin(((time * 0.7 + i * 0.37) % 1) * Math.PI); ctx.drawImage(dot, x - 1.5, y - 1.5, 3, 3);
|
|
84
|
+
}
|
|
85
|
+
for (let i = tracks.length - 1; i >= 0; i--) {
|
|
86
|
+
const t = tracks[i]; t.age += dt;
|
|
87
|
+
if (t.age > t.life) { tracks.splice(i, 1); continue; }
|
|
88
|
+
const a = t.age, k = Math.min(1, a / 0.07) * Math.exp(-a / (t.life * 0.45)) * t.bright, swell = 1 + a * 0.9, spread = a * a * 4;
|
|
89
|
+
const driftX = Math.sin(time * 0.3) * a * 3, driftY = a * a * 5; // convection, and droplets starting to fall
|
|
90
|
+
for (const d of t.drops) {
|
|
91
|
+
const x = d.x + d.dx * spread + driftX, y = d.y + d.dy * spread + driftY, r = d.r * swell * 2;
|
|
92
|
+
ctx.globalAlpha = Math.min(1, k * (0.75 + 0.5 * (1 - x / W))); // nearer the lamp, brighter
|
|
93
|
+
ctx.drawImage(dot, x - r, y - r, r * 2, r * 2);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over';
|
|
97
|
+
raf = requestAnimationFrame(frame);
|
|
98
|
+
}
|
|
99
|
+
raf = requestAnimationFrame(frame);
|
|
100
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
101
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// 3D paper confetti: each piece tumbles around its own axis (drawn as a squash of its width by cos(angle)),
|
|
2
|
+
// so pieces flash their front and back colours. Drag slows the fall; a sideways flutter keeps them drifting.
|
|
3
|
+
const PARAMS = { pieces: 160, gravity: 1 };
|
|
4
|
+
|
|
5
|
+
export default function (stage) {
|
|
6
|
+
const canvas = document.createElement('canvas');
|
|
7
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
8
|
+
stage.append(canvas);
|
|
9
|
+
const ctx = canvas.getContext('2d');
|
|
10
|
+
let W = 0, H = 0;
|
|
11
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth; H = stage.clientHeight; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); };
|
|
12
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
13
|
+
const colours = [['#ff4d6d', '#c9184a'], ['#ffd43b', '#f08c00'], ['#4dabf7', '#1864ab'], ['#69db7c', '#2b8a3e'], ['#ffffff', '#ced4da'], ['#ff922b', '#d9480f']];
|
|
14
|
+
let pieces = [];
|
|
15
|
+
function burst(x, y, n) {
|
|
16
|
+
const S = Math.min(W, H);
|
|
17
|
+
for (let i = 0; i < n; i++) {
|
|
18
|
+
const a = -Math.PI / 2 + (Math.random() - 0.5) * 1.6, sp = S * (1.4 + Math.random() * 1.6);
|
|
19
|
+
const c = colours[Math.floor(Math.random() * colours.length)];
|
|
20
|
+
pieces.push({ x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, w: 5 + Math.random() * 6, h: 8 + Math.random() * 8, spin: Math.random() * 6.28, vs: 8 + Math.random() * 14, tilt: Math.random() * 6.28, vt: (Math.random() - 0.5) * 6, front: c[0], back: c[1], wob: Math.random() * 6.28, round: Math.random() < 0.25 });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
let next = 0, last = performance.now(), raf;
|
|
24
|
+
function frame(now) {
|
|
25
|
+
const dt = Math.min(0.033, (now - last) / 1000); last = now;
|
|
26
|
+
if (now > next) { const n = Math.round(PARAMS.pieces / 2); burst(W * 0.25, H, n); burst(W * 0.75, H, n); next = now + 2600; }
|
|
27
|
+
const g = Math.min(W, H) * 1.6 * PARAMS.gravity;
|
|
28
|
+
ctx.fillStyle = '#1b1530'; ctx.fillRect(0, 0, W, H);
|
|
29
|
+
for (const p of pieces) {
|
|
30
|
+
p.vy += g * dt;
|
|
31
|
+
p.vx *= Math.exp(-2.2 * dt); p.vy *= Math.exp(-2.2 * dt); // paper is mostly drag
|
|
32
|
+
p.wob += dt * 5; p.x += (p.vx + Math.sin(p.wob) * 40) * dt; p.y += p.vy * dt;
|
|
33
|
+
p.spin += p.vs * dt; p.tilt += p.vt * dt;
|
|
34
|
+
const c = Math.cos(p.spin);
|
|
35
|
+
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.tilt); ctx.scale(1, Math.abs(c) < 0.08 ? 0.08 : c);
|
|
36
|
+
ctx.fillStyle = c > 0 ? p.front : p.back;
|
|
37
|
+
if (p.round) { ctx.beginPath(); ctx.arc(0, 0, p.w * 0.6, 0, 7); ctx.fill(); }
|
|
38
|
+
else ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
|
39
|
+
ctx.restore();
|
|
40
|
+
}
|
|
41
|
+
pieces = pieces.filter(p => p.y < H + 30);
|
|
42
|
+
raf = requestAnimationFrame(frame);
|
|
43
|
+
}
|
|
44
|
+
raf = requestAnimationFrame(frame);
|
|
45
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Curl-noise flow field: particles follow the curl of a smooth stream function, which has no sources or sinks,
|
|
2
|
+
// so they swirl without ever bunching up. A faint fill each frame leaves the trails.
|
|
3
|
+
const PARAMS = { count: 4000, speed: 1, scale: 0.0025 };
|
|
4
|
+
|
|
5
|
+
export default function (stage) {
|
|
6
|
+
const canvas = document.createElement('canvas');
|
|
7
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
8
|
+
stage.append(canvas);
|
|
9
|
+
const ctx = canvas.getContext('2d');
|
|
10
|
+
let W = 0, H = 0;
|
|
11
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth; H = stage.clientHeight; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); ctx.fillStyle = '#07161c'; ctx.fillRect(0, 0, W, H); };
|
|
12
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
13
|
+
|
|
14
|
+
// stream function ψ = Σ a·sin(kx·x + p)·cos(ky·y + q); velocity = (∂ψ/∂y, −∂ψ/∂x)
|
|
15
|
+
const waves = [[1, 1.0, 1.3, 0.0, 0.7], [0.6, 2.1, 1.7, 1.3, 2.2], [0.35, 3.7, 4.1, 2.6, 0.4]];
|
|
16
|
+
const field = (x, y, t) => {
|
|
17
|
+
let vx = 0, vy = 0; const s = PARAMS.scale;
|
|
18
|
+
for (const [a, kx, ky, p, q] of waves) {
|
|
19
|
+
const X = kx * x * s + p + t * 0.15 * kx, Y = ky * y * s + q - t * 0.1 * ky;
|
|
20
|
+
vx += -a * ky * Math.sin(X) * Math.sin(Y); // ∂ψ/∂y
|
|
21
|
+
vy += -a * kx * Math.cos(X) * Math.cos(Y); // −∂ψ/∂x
|
|
22
|
+
}
|
|
23
|
+
return [vx, vy];
|
|
24
|
+
};
|
|
25
|
+
const n = Math.round(PARAMS.count);
|
|
26
|
+
const P = new Float32Array(n * 3);
|
|
27
|
+
const spawn = i => { P[i * 3] = Math.random() * W; P[i * 3 + 1] = Math.random() * H; P[i * 3 + 2] = 60 + Math.random() * 200; };
|
|
28
|
+
for (let i = 0; i < n; i++) spawn(i);
|
|
29
|
+
const palette = ['#3bc9db', '#99e9f2', '#0a87a6', '#ffffff'];
|
|
30
|
+
let raf;
|
|
31
|
+
function frame(now) {
|
|
32
|
+
const t = now / 1000;
|
|
33
|
+
ctx.fillStyle = 'rgba(7, 22, 28, 0.09)'; ctx.fillRect(0, 0, W, H);
|
|
34
|
+
ctx.lineWidth = 1;
|
|
35
|
+
for (let c = 0; c < palette.length; c++) {
|
|
36
|
+
ctx.strokeStyle = palette[c]; ctx.globalAlpha = c === 3 ? 0.35 : 0.55; ctx.beginPath();
|
|
37
|
+
for (let i = c; i < n; i += palette.length) {
|
|
38
|
+
const x = P[i * 3], y = P[i * 3 + 1];
|
|
39
|
+
const [vx, vy] = field(x, y, t);
|
|
40
|
+
const nx = x + vx * PARAMS.speed * 0.9, ny = y + vy * PARAMS.speed * 0.9;
|
|
41
|
+
ctx.moveTo(x, y); ctx.lineTo(nx, ny);
|
|
42
|
+
P[i * 3] = nx; P[i * 3 + 1] = ny;
|
|
43
|
+
if (--P[i * 3 + 2] < 0 || nx < 0 || nx > W || ny < 0 || ny > H) spawn(i);
|
|
44
|
+
}
|
|
45
|
+
ctx.stroke();
|
|
46
|
+
}
|
|
47
|
+
ctx.globalAlpha = 1;
|
|
48
|
+
raf = requestAnimationFrame(frame);
|
|
49
|
+
}
|
|
50
|
+
raf = requestAnimationFrame(frame);
|
|
51
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
52
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Dandelion clock: a sphere of seeds (spread evenly by the golden angle) turning slowly in 3D. Gusts tear seeds loose
|
|
2
|
+
// from the windward side; each one then floats on its parachute, tumbling slowly, carried by the gust and sinking a
|
|
3
|
+
// little. A seed is a thin stalk with a fan of fine filaments, drawn in perspective. When the head is bare it regrows.
|
|
4
|
+
const PARAMS = { seeds: 150, wind: 1 };
|
|
5
|
+
|
|
6
|
+
export default function (stage) {
|
|
7
|
+
const canvas = document.createElement('canvas');
|
|
8
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
9
|
+
stage.append(canvas);
|
|
10
|
+
const ctx = canvas.getContext('2d');
|
|
11
|
+
let W = 0, H = 0;
|
|
12
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); };
|
|
13
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
14
|
+
|
|
15
|
+
const N = Math.round(PARAMS.seeds), GA = Math.PI * (3 - Math.sqrt(5));
|
|
16
|
+
let seeds = [];
|
|
17
|
+
const grow = () => {
|
|
18
|
+
seeds = Array.from({ length: N }, (_, i) => {
|
|
19
|
+
const y = 1 - (i + 0.5) / N * 2, r = Math.sqrt(1 - y * y), a = i * GA; // even points on a sphere
|
|
20
|
+
return { d: [Math.cos(a) * r, y, Math.sin(a) * r], free: false, x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, spin: 0, tilt: 0, fade: 1 };
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
grow();
|
|
24
|
+
let raf, last = performance.now(), regrow = 0;
|
|
25
|
+
function frame(now) {
|
|
26
|
+
const dt = Math.min(0.033, (now - last) / 1000); last = now;
|
|
27
|
+
const t = now / 1000, S = Math.min(W, H), head = { x: W * 0.36, y: H * 0.45 }, R = S * 0.19;
|
|
28
|
+
const gust = Math.max(0, Math.sin(t * 0.55) * 0.7 + Math.sin(t * 1.3) * 0.3) * PARAMS.wind; // gusts come and go
|
|
29
|
+
const bg = ctx.createLinearGradient(0, 0, W, H); bg.addColorStop(0, '#2f4d3f'); bg.addColorStop(1, '#14231d');
|
|
30
|
+
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
|
|
31
|
+
// soft bokeh behind
|
|
32
|
+
for (let i = 0; i < 9; i++) { const x = (i * 0.13 + 0.05) % 1 * W, y = ((i * 0.37) % 1) * H, r = S * (0.05 + (i % 3) * 0.03); const g = ctx.createRadialGradient(x, y, 0, x, y, r); g.addColorStop(0, 'rgba(210,235,170,.10)'); g.addColorStop(1, 'rgba(210,235,170,0)'); ctx.fillStyle = g; ctx.fillRect(x - r, y - r, r * 2, r * 2); }
|
|
33
|
+
// stem
|
|
34
|
+
ctx.strokeStyle = '#6f8f5a'; ctx.lineWidth = S * 0.012; ctx.lineCap = 'round';
|
|
35
|
+
ctx.beginPath(); ctx.moveTo(head.x, head.y); ctx.quadraticCurveTo(head.x - S * 0.04, head.y + S * 0.4, head.x + S * 0.02, H + 10); ctx.stroke();
|
|
36
|
+
ctx.fillStyle = '#7d6b3c'; ctx.beginPath(); ctx.arc(head.x, head.y, S * 0.025, 0, 7); ctx.fill();
|
|
37
|
+
const rot = t * 0.25, cr = Math.cos(rot), sr = Math.sin(rot), F = S * 2.2;
|
|
38
|
+
const proj = (x, y, z) => { const k = F / (F + z * R); return [head.x + x * R * k, head.y - y * R * k, k]; };
|
|
39
|
+
const list = [];
|
|
40
|
+
let attached = 0;
|
|
41
|
+
for (const s of seeds) {
|
|
42
|
+
if (!s.free) {
|
|
43
|
+
attached++;
|
|
44
|
+
const dx = s.d[0] * cr - s.d[2] * sr, dz = s.d[0] * sr + s.d[2] * cr;
|
|
45
|
+
// the wind takes seeds from the side it blows from, a few at a time
|
|
46
|
+
if (gust > 0.4 && dx < -0.2 && Math.random() < gust * dt * 0.9) {
|
|
47
|
+
Object.assign(s, { free: true, x: dx, y: s.d[1], z: dz, vx: 0.4 + gust * 1.2, vy: 0.2 + Math.random() * 0.4, vz: (Math.random() - 0.5) * 0.6, spin: (Math.random() - 0.5) * 2, tilt: Math.atan2(s.d[1], dx) });
|
|
48
|
+
} else { list.push({ s, x: dx, y: s.d[1], z: dz, a: Math.atan2(s.d[1], dx), att: true }); continue; }
|
|
49
|
+
}
|
|
50
|
+
// floating: drag pulls it toward the wind's speed, it sinks slowly, and it tumbles a little
|
|
51
|
+
s.vx += ((0.5 + gust * 1.6) - s.vx) * dt * 0.8; s.vy += (-0.12 - s.vy) * dt * 0.6 + Math.sin(t * 2 + s.spin * 9) * dt * 0.3; s.vz *= 1 - dt * 0.5;
|
|
52
|
+
s.x += s.vx * dt; s.y += s.vy * dt; s.z += s.vz * dt; s.tilt += s.spin * dt * 0.6;
|
|
53
|
+
if (head.x + s.x * R > W + 60 || head.y - s.y * R > H + 60) { s.fade = 0; continue; }
|
|
54
|
+
list.push({ s, x: s.x, y: s.y, z: s.z, a: s.tilt + Math.sin(t + s.spin * 5) * 0.4, att: false });
|
|
55
|
+
}
|
|
56
|
+
list.sort((p, q) => q.z - p.z); // far first
|
|
57
|
+
for (const it of list) {
|
|
58
|
+
const [x, y, k] = proj(it.x, it.y, it.z), len = R * 0.55 * k, ca = Math.cos(it.a), sa = Math.sin(it.a);
|
|
59
|
+
// attached seeds point outward from the head; free ones hang under their parachute
|
|
60
|
+
const ux = it.att ? ca : Math.sin(it.a * 0.3) * 0.5, uy = it.att ? -sa : -1;
|
|
61
|
+
const bx = it.att ? head.x + (x - head.x) * 0.25 : x - ux * len * 0.6, by = it.att ? head.y + (y - head.y) * 0.25 : y - uy * len * 0.6;
|
|
62
|
+
const tx = it.att ? x : x + ux * len * 0.4, ty = it.att ? y : y + uy * len * 0.4;
|
|
63
|
+
const a = Math.min(1, 0.45 + k * 0.55);
|
|
64
|
+
ctx.strokeStyle = `rgba(240,236,220,${a * 0.7})`; ctx.lineWidth = Math.max(0.5, k * 0.8);
|
|
65
|
+
ctx.beginPath(); ctx.moveTo(bx, by); ctx.lineTo(tx, ty); ctx.stroke();
|
|
66
|
+
ctx.fillStyle = `rgba(120,100,60,${a})`; ctx.beginPath(); ctx.arc(bx, by, 1.2 * k, 0, 7); ctx.fill();
|
|
67
|
+
// the pappus: a fan of filaments, foreshortened by how it faces us
|
|
68
|
+
ctx.strokeStyle = `rgba(255,255,250,${a * 0.55})`; ctx.lineWidth = 0.5;
|
|
69
|
+
const pr = len * 0.42, dirA = Math.atan2(ty - by, tx - bx);
|
|
70
|
+
ctx.beginPath();
|
|
71
|
+
for (let j = 0; j < 14; j++) { const q = dirA + (j / 13 - 0.5) * 2.6; ctx.moveTo(tx, ty); ctx.lineTo(tx + Math.cos(q) * pr, ty + Math.sin(q) * pr * (0.55 + 0.45 * Math.abs(Math.cos(t * 0.3 + j)))); }
|
|
72
|
+
ctx.stroke();
|
|
73
|
+
}
|
|
74
|
+
seeds = seeds.filter(s => s.fade > 0);
|
|
75
|
+
if (!attached && !seeds.length) { regrow += dt; if (regrow > 1.2) { grow(); regrow = 0; } }
|
|
76
|
+
raf = requestAnimationFrame(frame);
|
|
77
|
+
}
|
|
78
|
+
raf = requestAnimationFrame(frame);
|
|
79
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Diffusion-limited aggregation: particles wander at random until they touch the crystal, then stick where they hit.
|
|
2
|
+
// Tips stick out further into the wandering particles than the hollows between them, so they catch more and grow
|
|
3
|
+
// faster: that feedback is what grows the branching, coral-like fractal. Walkers start on a circle just outside the
|
|
4
|
+
// crystal and are dropped if they stray too far. Colour follows the order things stuck, so you can read its growth.
|
|
5
|
+
const PARAMS = { speed: 1, stickiness: 1 };
|
|
6
|
+
|
|
7
|
+
export default function (stage) {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block;image-rendering:auto';
|
|
10
|
+
stage.append(canvas);
|
|
11
|
+
const ctx = canvas.getContext('2d');
|
|
12
|
+
let W = 0, H = 0, GW = 0, GH = 0, grid, img, ictx, idata, cx, cy, rmax, count;
|
|
13
|
+
const CELL = 2;
|
|
14
|
+
const reset = () => {
|
|
15
|
+
GW = Math.max(16, Math.ceil(W / CELL)); GH = Math.max(16, Math.ceil(H / CELL));
|
|
16
|
+
grid = new Uint8Array(GW * GH); img = document.createElement('canvas'); img.width = GW; img.height = GH;
|
|
17
|
+
ictx = img.getContext('2d'); idata = ictx.createImageData(GW, GH);
|
|
18
|
+
for (let i = 0; i < GW * GH; i++) { idata.data[i * 4] = 8; idata.data[i * 4 + 1] = 10; idata.data[i * 4 + 2] = 20; idata.data[i * 4 + 3] = 255; }
|
|
19
|
+
cx = GW >> 1; cy = GH >> 1; rmax = 2; count = 0; stick(cx, cy);
|
|
20
|
+
};
|
|
21
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); reset(); };
|
|
22
|
+
// colour along the growth: deep blue at the seed through cyan to a pale ice at the tips
|
|
23
|
+
const hue = k => { const s = Math.min(1, k); return [40 + 160 * s ** 1.5, 90 + 150 * s, 200 + 55 * s]; };
|
|
24
|
+
function stick(x, y) {
|
|
25
|
+
grid[y * GW + x] = 1; count++;
|
|
26
|
+
const r = Math.hypot(x - cx, y - cy); rmax = Math.max(rmax, r);
|
|
27
|
+
const [R, G, B] = hue(r / (Math.min(GW, GH) * 0.48)), k = (y * GW + x) * 4;
|
|
28
|
+
idata.data[k] = R; idata.data[k + 1] = G; idata.data[k + 2] = B;
|
|
29
|
+
}
|
|
30
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
31
|
+
const touching = (x, y) => grid[y * GW + x - 1] | grid[y * GW + x + 1] | grid[(y - 1) * GW + x] | grid[(y + 1) * GW + x];
|
|
32
|
+
let raf, done = 0;
|
|
33
|
+
function frame() {
|
|
34
|
+
if (done > 0) { done--; if (!done) reset(); }
|
|
35
|
+
else {
|
|
36
|
+
const limit = Math.min(GW, GH) / 2 - 3;
|
|
37
|
+
// a budget of random-walk steps per frame; each walker is released near the crystal so few steps are wasted
|
|
38
|
+
let budget = 160000 * PARAMS.speed, x = 0, y = 0, alive = false;
|
|
39
|
+
while (budget > 0) {
|
|
40
|
+
if (!alive) { const a = Math.random() * 6.28, r = rmax + 6; x = Math.round(cx + Math.cos(a) * r); y = Math.round(cy + Math.sin(a) * r); alive = true; }
|
|
41
|
+
const s = Math.random() * 4 | 0; x += s === 0 ? 1 : s === 1 ? -1 : 0; y += s === 2 ? 1 : s === 3 ? -1 : 0; budget--;
|
|
42
|
+
const d = Math.hypot(x - cx, y - cy);
|
|
43
|
+
if (d > rmax + 18 || x < 1 || y < 1 || x >= GW - 1 || y >= GH - 1) { alive = false; continue; } // strayed: drop it
|
|
44
|
+
if (touching(x, y) && Math.random() < 0.35 + 0.65 * Math.min(1, PARAMS.stickiness)) { stick(x, y); alive = false; if (rmax > limit) { done = 120; break; } }
|
|
45
|
+
}
|
|
46
|
+
ictx.putImageData(idata, 0, 0);
|
|
47
|
+
}
|
|
48
|
+
ctx.fillStyle = '#080a14'; ctx.fillRect(0, 0, W, H);
|
|
49
|
+
ctx.imageSmoothingEnabled = false; ctx.drawImage(img, 0, 0, GW * CELL, GH * CELL);
|
|
50
|
+
// a soft bloom: the same image, blurred and added
|
|
51
|
+
ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = 0.35; ctx.filter = 'blur(4px)'; ctx.drawImage(img, 0, 0, GW * CELL, GH * CELL);
|
|
52
|
+
ctx.filter = 'none'; ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over';
|
|
53
|
+
raf = requestAnimationFrame(frame);
|
|
54
|
+
}
|
|
55
|
+
raf = requestAnimationFrame(frame);
|
|
56
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
57
|
+
}
|