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.
Files changed (146) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +37 -2
  3. package/anims/basics/anticipation.js +56 -0
  4. package/anims/basics/card-flip.js +39 -0
  5. package/anims/basics/easing-race.js +57 -0
  6. package/anims/basics/fade-scale.js +44 -0
  7. package/anims/basics/stagger-grid.js +43 -0
  8. package/anims/chrome/black-hole.js +81 -0
  9. package/anims/chrome/cloth-reveal.js +156 -0
  10. package/anims/chrome/dna-helix.js +52 -0
  11. package/anims/chrome/dust-bust.js +109 -0
  12. package/anims/chrome/extruded-type.js +64 -0
  13. package/anims/chrome/fractal-tree.js +75 -0
  14. package/anims/chrome/gem.js +111 -0
  15. package/anims/chrome/geode.js +102 -0
  16. package/anims/chrome/glass-lens.js +73 -0
  17. package/anims/chrome/glass-prism.js +95 -0
  18. package/anims/chrome/gyroid.js +69 -0
  19. package/anims/chrome/holo-card.js +52 -0
  20. package/anims/chrome/infinite-tunnel.js +81 -0
  21. package/anims/chrome/inflated-type.js +141 -0
  22. package/anims/chrome/isometric-city.js +97 -0
  23. package/anims/chrome/jelly-cube.js +329 -0
  24. package/anims/chrome/klein-bottle.js +80 -0
  25. package/anims/chrome/liquid-metal.js +94 -0
  26. package/anims/chrome/low-poly-planet.js +163 -0
  27. package/anims/chrome/mandelbulb.js +79 -0
  28. package/anims/chrome/melting-logo.js +126 -0
  29. package/anims/chrome/menger-sponge.js +79 -0
  30. package/anims/chrome/neon-geodesic.js +149 -0
  31. package/anims/chrome/paper-fold.js +135 -0
  32. package/anims/chrome/path-spheres.js +112 -0
  33. package/anims/chrome/point-globe.js +81 -0
  34. package/anims/chrome/portal.js +105 -0
  35. package/anims/chrome/rubiks-cube.js +74 -0
  36. package/anims/chrome/sdf-morph.js +129 -0
  37. package/anims/chrome/shatter.js +120 -0
  38. package/anims/chrome/soap-bubble.js +86 -0
  39. package/anims/chrome/terrain-flyover.js +78 -0
  40. package/anims/chrome/tesseract.js +148 -0
  41. package/anims/chrome/torus-knot.js +89 -0
  42. package/anims/chrome/volumetric-clouds.js +70 -0
  43. package/anims/flow/bio-shore.js +140 -0
  44. package/anims/flow/calligraphy.js +116 -0
  45. package/anims/flow/chladni.js +152 -0
  46. package/anims/flow/cloud-chamber.js +101 -0
  47. package/anims/flow/confetti.js +46 -0
  48. package/anims/flow/curl-flow.js +52 -0
  49. package/anims/flow/dandelion.js +80 -0
  50. package/anims/flow/dla-crystal.js +57 -0
  51. package/anims/flow/drone-show.js +357 -0
  52. package/anims/flow/embers.js +63 -0
  53. package/anims/flow/firefly-sync.js +107 -0
  54. package/anims/flow/galaxy.js +69 -0
  55. package/anims/flow/galton-board.js +155 -0
  56. package/anims/flow/gpu-particles.js +73 -0
  57. package/anims/flow/harmonograph.js +61 -0
  58. package/anims/flow/holi.js +153 -0
  59. package/anims/flow/iron-filings.js +112 -0
  60. package/anims/flow/lightning.js +72 -0
  61. package/anims/flow/murmuration.js +105 -0
  62. package/anims/flow/orbitals.js +195 -0
  63. package/anims/flow/particle-life.js +87 -0
  64. package/anims/flow/physarum.js +171 -0
  65. package/anims/flow/pin-art.js +181 -0
  66. package/anims/flow/rainbow.js +127 -0
  67. package/anims/flow/roche-limit.js +161 -0
  68. package/anims/flow/sand-mandala.js +121 -0
  69. package/anims/flow/scramble-crossing.js +152 -0
  70. package/anims/flow/snowfall.js +84 -0
  71. package/anims/flow/sparks.js +71 -0
  72. package/anims/flow/spider-web.js +123 -0
  73. package/anims/flow/starry-night.js +121 -0
  74. package/anims/flow/states-of-matter.js +113 -0
  75. package/anims/flow/strange-attractor.js +54 -0
  76. package/anims/flow/wheat-field.js +186 -0
  77. package/anims/flow/wind-tunnel.js +227 -0
  78. package/anims/fluid/bubbles.js +152 -0
  79. package/anims/fluid/caustics.js +45 -0
  80. package/anims/fluid/dripping-text.js +53 -0
  81. package/anims/fluid/ferrofluid.js +56 -0
  82. package/anims/fluid/fire-smoke.js +83 -0
  83. package/anims/fluid/honey-drip.js +167 -0
  84. package/anims/fluid/ink-drop.js +50 -0
  85. package/anims/fluid/ink-fluid.js +125 -0
  86. package/anims/fluid/lava-lamp.js +56 -0
  87. package/anims/fluid/liquid-distort.js +64 -0
  88. package/anims/fluid/liquid-glass-fill.js +53 -0
  89. package/anims/fluid/marbling.js +53 -0
  90. package/anims/fluid/mercury.js +177 -0
  91. package/anims/fluid/ocean-waves.js +50 -0
  92. package/anims/fluid/paint-swirl.js +153 -0
  93. package/anims/fluid/raindrops-glass.js +60 -0
  94. package/anims/fluid/ripples.js +57 -0
  95. package/anims/fluid/sph-water.js +75 -0
  96. package/anims/fluid/underwater.js +55 -0
  97. package/anims/fluid/water-surface-3d.js +98 -0
  98. package/anims/hover/cursor-trail.js +37 -0
  99. package/anims/hover/magnetic.js +41 -0
  100. package/anims/hover/ripple.js +31 -0
  101. package/anims/hover/spotlight.js +31 -0
  102. package/anims/hover/tilt.js +36 -0
  103. package/anims/micro/arc-spinner.js +22 -0
  104. package/anims/micro/like-burst.js +42 -0
  105. package/anims/micro/skeleton.js +36 -0
  106. package/anims/micro/submit-success.js +45 -0
  107. package/anims/micro/toggle.js +35 -0
  108. package/anims/morph/gooey.js +36 -0
  109. package/anims/morph/icon-morph.js +45 -0
  110. package/anims/morph/line-draw.js +34 -0
  111. package/anims/morph/liquid-button.js +53 -0
  112. package/anims/morph/shape-morph.js +61 -0
  113. package/anims/registry-more.js +327 -0
  114. package/anims/registry.js +294 -0
  115. package/anims/scroll/parallax.js +50 -0
  116. package/anims/scroll/pinned-track.js +40 -0
  117. package/anims/scroll/scrub-reveal.js +34 -0
  118. package/anims/scroll/velocity-skew.js +38 -0
  119. package/anims/scroll/zoom-through.js +41 -0
  120. package/anims/shaders/aurora.js +47 -0
  121. package/anims/shaders/dither.js +69 -0
  122. package/anims/shaders/domain-warp.js +52 -0
  123. package/anims/shaders/guilloche.js +42 -0
  124. package/anims/shaders/kaleidoscope.js +64 -0
  125. package/anims/shaders/reaction-diffusion.js +85 -0
  126. package/anims/shaders/rgb-glitch.js +56 -0
  127. package/anims/shaders/truchet.js +57 -0
  128. package/anims/shaders/voronoi.js +50 -0
  129. package/anims/springs/ball-collisions.js +47 -0
  130. package/anims/springs/pendulum-wave.js +35 -0
  131. package/anims/springs/rubber-band.js +50 -0
  132. package/anims/springs/squash-bounce.js +50 -0
  133. package/anims/springs/verlet-rope.js +46 -0
  134. package/anims/text/letter-rise.js +40 -0
  135. package/anims/text/line-mask.js +26 -0
  136. package/anims/text/scramble.js +32 -0
  137. package/anims/text/typewriter.js +35 -0
  138. package/anims/text/vf-wave.js +26 -0
  139. package/anims/transitions/circle-reveal.js +42 -0
  140. package/anims/transitions/push-slide.js +40 -0
  141. package/anims/transitions/shared-expand.js +43 -0
  142. package/anims/transitions/staircase-wipe.js +41 -0
  143. package/anims/transitions/view-transition.js +43 -0
  144. package/bin.mjs +155 -0
  145. package/js/code.js +122 -0
  146. 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
+ }