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,105 @@
1
+ // Starling murmuration: a 3D flock where each bird only watches its nearest seven neighbours (as real starlings do),
2
+ // matching their heading, keeping close, and not touching. A soft pull keeps the flock in the sky. Every few seconds a
3
+ // falcon dives through: birds near it flee, and the panic passes from bird to bird, which is what makes the dark waves
4
+ // ripple across the cloud. Birds are projected with perspective, so dense, edge-on parts of the flock read darker.
5
+ const PARAMS = { birds: 1600, falcon: 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';
10
+ stage.append(canvas);
11
+ const ctx = canvas.getContext('2d');
12
+ let W = 0, H = 0;
13
+ 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); };
14
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
15
+
16
+ const N = Math.round(PARAMS.birds), P = new Float32Array(N * 3), V = new Float32Array(N * 3), fear = new Float32Array(N);
17
+ for (let i = 0; i < N; i++) {
18
+ const a = Math.random() * 6.28, r = Math.cbrt(Math.random()) * 1.2;
19
+ P[i * 3] = Math.cos(a) * r * 1.6; P[i * 3 + 1] = (Math.random() - 0.5) * 0.8; P[i * 3 + 2] = Math.sin(a) * r;
20
+ V[i * 3] = 0.6 + Math.random() * 0.1; V[i * 3 + 1] = 0; V[i * 3 + 2] = (Math.random() - 0.5) * 0.2;
21
+ }
22
+ // a hash grid so each bird only looks at birds in its cell and the 26 around it
23
+ const CELL = 0.22, grid = new Map(), key = (x, y, z) => ((x + 512) << 20) | ((y + 512) << 10) | (z + 512);
24
+ const falcon = { x: 9, y: 0, z: 0, vx: 0, vy: 0, vz: 0, on: false, next: 2 };
25
+ const near = new Int32Array(7), nd = new Float32Array(7);
26
+ let raf, last = performance.now();
27
+ function step(dt, t) {
28
+ grid.clear();
29
+ for (let i = 0; i < N; i++) {
30
+ const k = key(Math.floor(P[i * 3] / CELL), Math.floor(P[i * 3 + 1] / CELL), Math.floor(P[i * 3 + 2] / CELL));
31
+ let b = grid.get(k); if (!b) grid.set(k, b = []); b.push(i);
32
+ }
33
+ // the falcon: dives through the flock's middle, then leaves and waits
34
+ if (PARAMS.falcon > 0 && !falcon.on && t > falcon.next) {
35
+ const a = Math.random() * 6.28; Object.assign(falcon, { on: true, x: Math.cos(a) * 3.5, y: 1.6, z: Math.sin(a) * 3.5 });
36
+ const l = Math.hypot(falcon.x, falcon.y, falcon.z); falcon.vx = -falcon.x / l * 2.6; falcon.vy = -falcon.y / l * 2.6; falcon.vz = -falcon.z / l * 2.6;
37
+ }
38
+ if (falcon.on) {
39
+ falcon.x += falcon.vx * dt; falcon.y += falcon.vy * dt; falcon.z += falcon.vz * dt;
40
+ if (Math.hypot(falcon.x, falcon.y, falcon.z) > 4.5 && falcon.x * falcon.vx + falcon.z * falcon.vz > 0) { falcon.on = false; falcon.next = t + 4 + Math.random() * 4 / Math.max(0.2, PARAMS.falcon); }
41
+ }
42
+ for (let i = 0; i < N; i++) {
43
+ const x = P[i * 3], y = P[i * 3 + 1], z = P[i * 3 + 2], cx = Math.floor(x / CELL), cy = Math.floor(y / CELL), cz = Math.floor(z / CELL);
44
+ let n = 0; nd.fill(1e9);
45
+ for (let a = -1; a <= 1; a++) for (let b = -1; b <= 1; b++) for (let c = -1; c <= 1; c++) {
46
+ const bucket = grid.get(key(cx + a, cy + b, cz + c)); if (!bucket) continue;
47
+ for (const j of bucket) {
48
+ if (j === i) continue;
49
+ const d = (P[j * 3] - x) ** 2 + (P[j * 3 + 1] - y) ** 2 + (P[j * 3 + 2] - z) ** 2;
50
+ if (d >= nd[6]) continue;
51
+ let s = 6; while (s > 0 && nd[s - 1] > d) { nd[s] = nd[s - 1]; near[s] = near[s - 1]; s--; } // keep the seven nearest, sorted
52
+ nd[s] = d; near[s] = j; if (n < 7) n++;
53
+ }
54
+ }
55
+ let ax = 0, ay = 0, az = 0, mx = 0, my = 0, mz = 0, px = 0, py = 0, pz = 0, f = 0;
56
+ for (let k = 0; k < n; k++) {
57
+ const j = near[k], dx = P[j * 3] - x, dy = P[j * 3 + 1] - y, dz = P[j * 3 + 2] - z, d = Math.sqrt(nd[k]) + 1e-4;
58
+ mx += V[j * 3]; my += V[j * 3 + 1]; mz += V[j * 3 + 2]; px += dx; py += dy; pz += dz;
59
+ if (d < 0.05) { ax -= dx / d * (0.05 - d) * 40; ay -= dy / d * (0.05 - d) * 40; az -= dz / d * (0.05 - d) * 40; }
60
+ f = Math.max(f, fear[j] * 0.93); // panic spreads to the nearest neighbours
61
+ }
62
+ if (n) { ax += (mx / n - V[i * 3]) * 1.8 + px / n * 2; ay += (my / n - V[i * 3 + 1]) * 1.8 + py / n * 2; az += (mz / n - V[i * 3 + 2]) * 1.8 + pz / n * 2; }
63
+ // stay in this patch of sky: a soft roost point that wanders, and a firm wall past a radius, so the flock wheels
64
+ // round instead of flying off
65
+ const ox = x - Math.sin(t * 0.13) * 0.5, oy = y - Math.sin(t * 0.21) * 0.25, oz = z, r = Math.hypot(ox, oy * 1.6, oz);
66
+ ax -= ox * 0.35; ay -= oy * 0.9; az -= oz * 0.45;
67
+ if (r > 1.1) { const k = (r - 1.1) * 4 / r; ax -= ox * k; ay -= oy * k; az -= oz * k; }
68
+ if (falcon.on) {
69
+ const dx = x - falcon.x, dy = y - falcon.y, dz = z - falcon.z, d = Math.hypot(dx, dy, dz);
70
+ if (d < 0.7) { const k = (0.7 - d) * 12 / (d + 0.05); ax += dx * k; ay += dy * k; az += dz * k; f = 1; }
71
+ }
72
+ fear[i] += (f - fear[i]) * Math.min(1, dt * 8);
73
+ if (fear[i] > 0.05) { const l = Math.hypot(mx, my, mz) || 1; ax += (V[i * 3 + 2] / l) * fear[i] * 3; az -= (V[i * 3] / l) * fear[i] * 3; } // frightened birds bank sideways
74
+ let vx = V[i * 3] + ax * dt, vy = V[i * 3 + 1] + ay * dt, vz = V[i * 3 + 2] + az * dt;
75
+ const sp = Math.hypot(vx, vy, vz), target = 0.75 + fear[i] * 0.6, k = (sp + (target - sp) * Math.min(1, dt * 2)) / (sp || 1);
76
+ V[i * 3] = vx * k; V[i * 3 + 1] = vy * k; V[i * 3 + 2] = vz * k;
77
+ }
78
+ for (let i = 0; i < N * 3; i++) P[i] += V[i] * dt;
79
+ }
80
+ function frame(now) {
81
+ const dt = Math.min(0.033, (now - last) / 1000); last = now;
82
+ const t = now / 1000;
83
+ step(dt, t);
84
+ const sky = ctx.createLinearGradient(0, 0, 0, H);
85
+ sky.addColorStop(0, '#5b6f9a'); sky.addColorStop(0.55, '#d79a8a'); sky.addColorStop(0.85, '#f2c08a'); sky.addColorStop(1, '#f6d7a2');
86
+ ctx.fillStyle = sky; ctx.fillRect(0, 0, W, H);
87
+ ctx.fillStyle = '#2b2530'; ctx.beginPath(); ctx.moveTo(0, H);
88
+ for (let x = 0; x <= W; x += 6) ctx.lineTo(x, H * 0.93 - Math.abs(Math.sin(x * 0.05) * Math.sin(x * 0.013)) * H * 0.05);
89
+ ctx.lineTo(W, H); ctx.fill();
90
+ // camera slowly circles the flock
91
+ const ang = t * 0.05, ca = Math.cos(ang), sa = Math.sin(ang), F = Math.min(W, H) * 1.05, cx = W / 2, cy = H * 0.44, dist = 3.6;
92
+ ctx.fillStyle = 'rgba(25,20,30,.85)';
93
+ const draw = (x, y, z, s) => {
94
+ const rx = x * ca - z * sa, rz = x * sa + z * ca + dist;
95
+ if (rz < 0.3) return;
96
+ const k = F / rz, sz = Math.max(0.6, s * k * 0.012);
97
+ ctx.fillRect(cx + rx * k - sz, cy - y * k - sz * 0.5, sz * 2, sz); // a bird far away is just a dash
98
+ };
99
+ for (let i = 0; i < N; i++) draw(P[i * 3], P[i * 3 + 1], P[i * 3 + 2], 1);
100
+ if (falcon.on) { ctx.fillStyle = 'rgba(20,15,20,1)'; draw(falcon.x, falcon.y, falcon.z, 2.6); }
101
+ raf = requestAnimationFrame(frame);
102
+ }
103
+ raf = requestAnimationFrame(frame);
104
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
105
+ }
@@ -0,0 +1,195 @@
1
+ // Hydrogen orbitals: a quarter of a million particles, each a guess at where the electron is. Every frame each one
2
+ // proposes a small random step and keeps it with probability |ψ(new)|² / |ψ(old)|² (the Metropolis rule), so the
3
+ // cloud's density always follows the exact wavefunction: the real solution of the Schrödinger equation, built from
4
+ // Laguerre polynomials and spherical harmonics. Colour is the phase of ψ, which turns with time. Between orbitals the
5
+ // atom is in a superposition of both, and the cloud flows from one shape into the next.
6
+ const PARAMS = { grid: 512, speed: 1, glow: 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';
11
+ const label = document.createElement('div');
12
+ label.style.cssText = 'position:absolute;left:14px;bottom:12px;font:500 12px/1.3 ui-monospace,SFMono-Regular,Menlo,monospace;color:rgba(214,226,255,.62);letter-spacing:.04em;pointer-events:none;white-space:pre';
13
+ stage.append(canvas, label);
14
+ const gl = canvas.getContext('webgl2', { antialias: false });
15
+ if (!gl || !gl.getExtension('EXT_color_buffer_float')) { stage.textContent = 'This needs WebGL2 with float render targets.'; return () => canvas.remove(); }
16
+ const G = Math.max(64, Math.round(PARAMS.grid)), COUNT = G * G;
17
+
18
+ // n, l, m: the order they appear in; the cloud flows from each into the next
19
+ const STATES = [[2, 1, 1], [3, 2, 0], [3, 2, 2], [4, 3, 1], [3, 2, 1], [4, 3, 3], [4, 3, 0], [1, 0, 0], [2, 1, 0]];
20
+ const HOLD = 5.5, MORPH = 4.5, SUB = 'spdf', name = s => `${s[0]}${SUB[s[1]]}`;
21
+ const reach = s => 2.3 * s[0] * s[0] + 1.2; // how far out the cloud extends, in Bohr radii
22
+
23
+ const ORB = `
24
+ float fact(int n) { float f = 1.; for (int i = 2; i <= n; i++) f *= float(i); return f; }
25
+ float lag(int k, float a, float x) { // generalised Laguerre polynomial L_k^a(x)
26
+ float L0 = 1., L1 = 1. + a - x; if (k == 0) return L0;
27
+ for (int i = 1; i < k; i++) { float L2 = ((2. * float(i) + 1. + a - x) * L1 - (float(i) + a) * L0) / float(i + 1); L0 = L1; L1 = L2; }
28
+ return L1;
29
+ }
30
+ float leg(int l, int m, float x) { // associated Legendre polynomial P_l^m(x)
31
+ float s = sqrt(max(0., 1. - x * x)), pmm = 1., f = 1.;
32
+ for (int i = 0; i < m; i++) { pmm *= -f * s; f += 2.; }
33
+ if (l == m) return pmm;
34
+ float pm1 = x * float(2 * m + 1) * pmm; if (l == m + 1) return pm1;
35
+ float pll = 0.;
36
+ for (int ll = m + 2; ll <= l; ll++) { pll = (x * float(2 * ll - 1) * pm1 - float(ll + m - 1) * pmm) / float(ll - m); pmm = pm1; pm1 = pll; }
37
+ return pll;
38
+ }
39
+ vec2 psi(vec3 p, ivec3 q) { // the hydrogen eigenstate ψ_nlm, complex
40
+ int n = q.x, l = q.y, m = q.z, am = abs(m);
41
+ float r = length(p) + 1e-6, rho = 2. * r / float(n), rl = 1.;
42
+ for (int i = 0; i < l; i++) rl *= rho;
43
+ float R = sqrt(pow(2. / float(n), 3.) * fact(n - l - 1) / (2. * float(n) * fact(n + l))) * exp(-.5 * rho) * rl * lag(n - l - 1, float(2 * l + 1), rho);
44
+ float Y = sqrt(float(2 * l + 1) / 12.566371 * fact(l - am) / fact(l + am)) * leg(l, am, p.z / r);
45
+ float ph = float(m) * atan(p.y, p.x);
46
+ return R * Y * vec2(cos(ph), sin(ph));
47
+ }
48
+ uniform ivec3 QA, QB; uniform float MIX, T;
49
+ vec2 cmul(vec2 a, vec2 b) { return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }
50
+ vec2 wave(vec3 p) { // the superposition, each part turning at its own energy
51
+ float ea = -.5 / float(QA.x * QA.x), eb = -.5 / float(QB.x * QB.x), k = 3.14159;
52
+ return cmul(psi(p, QA) * cos(MIX * 1.5708), vec2(cos(ea * T * k), -sin(ea * T * k)))
53
+ + cmul(psi(p, QB) * sin(MIX * 1.5708), vec2(cos(eb * T * k), -sin(eb * T * k)));
54
+ }`;
55
+ const QUAD = `#version 300 es
56
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
57
+ const simFS = `#version 300 es
58
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S; uniform float F, SC;
59
+ ${ORB}
60
+ vec3 h3(vec3 p) { p = fract(p * vec3(.1031, .103, .0973)); p += dot(p, p.yxz + 33.33); return fract((p.xxy + p.yxx) * p.zyx); }
61
+ void main() {
62
+ vec3 p = texture(S, uv).xyz, r = h3(vec3(gl_FragCoord.xy, F)), r2 = h3(r * 91.7 + F * .13), q;
63
+ if (r2.x < .05) { // now and then a long jump anywhere, so lobes behind nodes still fill
64
+ vec3 d = normalize(h3(r2 * 37.1 + 5.) - .5 + 1e-4); q = d * pow(r2.y, .333) * SC * 1.3;
65
+ } else q = p + (r - .5) * SC * .1;
66
+ vec2 a = wave(p), b = wave(q);
67
+ float accept = dot(b, b) / max(dot(a, a), 1e-37);
68
+ o = vec4(r2.z < accept ? q : p, 1);
69
+ }`;
70
+ const drawVS = `#version 300 es
71
+ precision highp float; uniform sampler2D S; uniform int G; uniform mat4 M; uniform float PS, D, SC; out vec3 c;
72
+ ${ORB}
73
+ void main() {
74
+ vec3 p = texelFetch(S, ivec2(gl_VertexID % G, gl_VertexID / G), 0).xyz;
75
+ vec2 w = wave(p); float h = atan(w.y, w.x) / 6.2831853;
76
+ c = .5 + .5 * cos(6.2831853 * (h + vec3(0., .33, .67))); // phase → hue
77
+ c = mix(vec3(.6, .7, 1.), c, .85);
78
+ gl_Position = M * vec4(p, 1); gl_PointSize = PS;
79
+ c *= 1.25 - .5 * smoothstep(-1., 1., (gl_Position.w - D) / SC); // the far side of the cloud is dimmer: depth
80
+ }`;
81
+ const drawFS = `#version 300 es
82
+ precision mediump float; in vec3 c; out vec4 o; uniform float K;
83
+ void main() { vec2 q = gl_PointCoord * 2. - 1.; o = vec4(c * K * max(0., 1. - dot(q, q)), 1); }`;
84
+ const downFS = `#version 300 es
85
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S; uniform vec2 H;
86
+ void main() { o = (texture(S, uv) * 4. + texture(S, uv - H) + texture(S, uv + H) + texture(S, uv + vec2(H.x, -H.y)) + texture(S, uv - vec2(H.x, -H.y))) / 8.; }`;
87
+ const upFS = `#version 300 es
88
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S; uniform vec2 H;
89
+ void main() { o = (texture(S, uv + vec2(-2. * H.x, 0)) + texture(S, uv + vec2(2. * H.x, 0)) + texture(S, uv + vec2(0, 2. * H.y)) + texture(S, uv + vec2(0, -2. * H.y))
90
+ + 2. * (texture(S, uv + H) + texture(S, uv - H) + texture(S, uv + vec2(H.x, -H.y)) + texture(S, uv - vec2(H.x, -H.y)))) / 12.; }`;
91
+ const finalFS = `#version 300 es
92
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S, B; uniform float K, T; uniform vec2 R;
93
+ float h2(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
94
+ void main() {
95
+ vec2 d = (uv - .5) * vec2(R.x / R.y, 1.);
96
+ vec3 c = texture(S, uv).rgb; c = pow(c, vec3(1.65)) * 3.; // steeper: lobes stand out, the thin fringe falls away
97
+ c += texture(B, uv).rgb * K + vec3(.004, .006, .016) * (1. - length(d));
98
+ c = (c * (2.51 * c + .03)) / (c * (2.43 * c + .59) + .14);
99
+ o = vec4(pow(clamp(c, 0., 1.), vec3(1. / 2.2)) + (h2(uv * R + fract(T) * 91.) - .5) / 160., 1);
100
+ }`;
101
+
102
+ const sh = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); return s; };
103
+ const prog = (vs, fs) => {
104
+ const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p);
105
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
106
+ const u = {}; for (let i = 0, n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS); i < n; i++) { const nm = gl.getActiveUniform(p, i).name; u[nm] = gl.getUniformLocation(p, nm); }
107
+ return { p, u };
108
+ };
109
+ const simP = prog(QUAD, simFS), drawP = prog(drawVS, drawFS), downP = prog(QUAD, downFS), upP = prog(QUAD, upFS), finalP = prog(QUAD, finalFS);
110
+
111
+ const tex = (w, h, f, t, data = null, filt = gl.LINEAR) => {
112
+ const x = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, x); gl.texImage2D(gl.TEXTURE_2D, 0, f, w, h, 0, gl.RGBA, t, data);
113
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filt); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filt);
114
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
115
+ const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, x, 0);
116
+ return { tex: x, fb, w, h };
117
+ };
118
+ // particles start as a loose ball and condense into the first orbital
119
+ const init = new Float32Array(COUNT * 4), r0 = reach(STATES[0]);
120
+ for (let i = 0; i < COUNT; i++) { const u = Math.random() * 2 - 1, a = Math.random() * 6.283, s = Math.sqrt(1 - u * u), r = Math.cbrt(Math.random()) * r0; init.set([Math.cos(a) * s * r, Math.sin(a) * s * r, u * r, 1], i * 4); }
121
+ let A = tex(G, G, gl.RGBA32F, gl.FLOAT, init, gl.NEAREST), B = tex(G, G, gl.RGBA32F, gl.FLOAT, init, gl.NEAREST);
122
+
123
+ const quad = gl.createVertexArray(); gl.bindVertexArray(quad);
124
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
125
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
126
+ const empty = gl.createVertexArray();
127
+
128
+ let W = 1, H = 1, scene = null, mips = [];
129
+ const resize = () => {
130
+ const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, 2);
131
+ W = canvas.width = Math.max(1, (w * d) | 0); H = canvas.height = Math.max(1, (h * d) | 0);
132
+ for (const r of [scene, ...mips]) if (r) { gl.deleteTexture(r.tex); gl.deleteFramebuffer(r.fb); }
133
+ scene = tex(W, H, gl.RGBA16F, gl.HALF_FLOAT); mips = [];
134
+ let mw = W, mh = H; for (let i = 0; i < 5; i++) { mw = Math.max(1, mw >> 1); mh = Math.max(1, mh >> 1); mips.push(tex(mw, mh, gl.RGBA16F, gl.HALF_FLOAT)); }
135
+ };
136
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
137
+
138
+ const pass = (P, target, src) => {
139
+ gl.useProgram(P.p); gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.fb : null); gl.viewport(0, 0, target ? target.w : W, target ? target.h : H);
140
+ if (src) { gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, src); gl.uniform1i(P.u.S, 0); }
141
+ gl.bindVertexArray(quad);
142
+ };
143
+ const setWave = (P, qa, qb, mix, t) => { gl.uniform3i(P.u.QA, ...qa); gl.uniform3i(P.u.QB, ...qb); gl.uniform1f(P.u.MIX, mix); gl.uniform1f(P.u.T, t); };
144
+ // a camera circling the atom's z axis, looking slightly down on it
145
+ const view = (az, el, dist, aspect) => {
146
+ const ex = Math.cos(el) * Math.cos(az) * dist, ey = Math.cos(el) * Math.sin(az) * dist, ez = Math.sin(el) * dist;
147
+ let fx = -ex, fy = -ey, fz = -ez; const fl = Math.hypot(fx, fy, fz); fx /= fl; fy /= fl; fz /= fl;
148
+ let rx = fy * 1 - fz * 0, ry = fz * 0 - fx * 1, rz = 0; const rl = Math.hypot(rx, ry) || 1; rx /= rl; ry /= rl;
149
+ const ux = ry * fz - rz * fy, uy = rz * fx - rx * fz, uz = rx * fy - ry * fx;
150
+ const f = 1 / Math.tan(0.36), near = dist * 0.05, far = dist * 4, nf = 1 / (near - far);
151
+ const V = [rx, ux, -fx, 0, ry, uy, -fy, 0, rz, uz, -fz, 0, -(rx * ex + ry * ey + rz * ez), -(ux * ex + uy * ey + uz * ez), fx * ex + fy * ey + fz * ez, 1];
152
+ const P = [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, -1, 0, 0, 2 * far * near * nf, 0];
153
+ const M = new Float32Array(16);
154
+ for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) { let s = 0; for (let k = 0; k < 4; k++) s += P[k * 4 + r] * V[c * 4 + k]; M[c * 4 + r] = s; }
155
+ return M;
156
+ };
157
+
158
+ let raf, last = performance.now(), t = 0, frameNo = 0, shown = '';
159
+ const ease = x => x * x * (3 - 2 * x);
160
+ function frame(now) {
161
+ t += Math.min(0.05, (now - last) / 1000) * PARAMS.speed; last = now; frameNo++;
162
+ // where we are in the sequence: holding one state, or flowing to the next
163
+ const period = HOLD + MORPH, k = Math.floor(t / period), local = t - k * period;
164
+ const qa = STATES[k % STATES.length], qb = STATES[(k + 1) % STATES.length];
165
+ const mix = local < HOLD ? 0 : ease((local - HOLD) / MORPH);
166
+ const sc = reach(qa) + (reach(qb) - reach(qa)) * mix;
167
+ const txt = mix === 0 ? `${name(qa)} n=${qa[0]} l=${qa[1]} m=${qa[2]}` : `${name(qa)} + ${name(qb)} superposition`;
168
+ if (txt !== shown) { label.textContent = txt; shown = txt; }
169
+
170
+ gl.disable(gl.BLEND);
171
+ for (let s = 0; s < 2; s++) { // two Metropolis steps per frame
172
+ pass(simP, B, A.tex); gl.uniform1f(simP.u.F, frameNo * 2 + s); gl.uniform1f(simP.u.SC, sc); setWave(simP, qa, qb, mix, t);
173
+ gl.drawArrays(gl.TRIANGLES, 0, 3); [A, B] = [B, A];
174
+ }
175
+ gl.bindFramebuffer(gl.FRAMEBUFFER, scene.fb); gl.viewport(0, 0, W, H); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT);
176
+ gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE);
177
+ gl.useProgram(drawP.p); gl.bindVertexArray(empty); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, A.tex);
178
+ gl.uniform1i(drawP.u.S, 0); gl.uniform1i(drawP.u.G, G); setWave(drawP, qa, qb, mix, t);
179
+ gl.uniformMatrix4fv(drawP.u.M, false, view(t * 0.16, 0.78 + 0.32 * Math.sin(t * 0.13), sc * 3.1, W / H));
180
+ gl.uniform1f(drawP.u.PS, Math.max(1, H / 480)); gl.uniform1f(drawP.u.D, sc * 3.1); gl.uniform1f(drawP.u.SC, sc); gl.uniform1f(drawP.u.K, 0.05 * (262144 / COUNT) * Math.min(1, (H / 480) ** 2)); // the same glow at any size or count
181
+ gl.drawArrays(gl.POINTS, 0, COUNT);
182
+ let src = scene;
183
+ gl.disable(gl.BLEND);
184
+ for (const m of mips) { pass(downP, m, src.tex); gl.uniform2f(downP.u.H, 0.5 / src.w, 0.5 / src.h); gl.drawArrays(gl.TRIANGLES, 0, 3); src = m; }
185
+ gl.enable(gl.BLEND);
186
+ for (let i = mips.length - 1; i > 0; i--) { pass(upP, mips[i - 1], mips[i].tex); gl.uniform2f(upP.u.H, 0.5 / mips[i].w, 0.5 / mips[i].h); gl.drawArrays(gl.TRIANGLES, 0, 3); }
187
+ gl.disable(gl.BLEND);
188
+ pass(finalP, null, scene.tex); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, mips[0].tex); gl.uniform1i(finalP.u.B, 1);
189
+ gl.uniform1f(finalP.u.K, 0.35 * PARAMS.glow); gl.uniform1f(finalP.u.T, t); gl.uniform2f(finalP.u.R, W, H); gl.drawArrays(gl.TRIANGLES, 0, 3);
190
+ gl.activeTexture(gl.TEXTURE0);
191
+ raf = requestAnimationFrame(frame);
192
+ }
193
+ raf = requestAnimationFrame(frame);
194
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); label.remove(); };
195
+ }
@@ -0,0 +1,87 @@
1
+ // Particle life: six species of particles and one table that says how much each species likes or dislikes each
2
+ // other one. Every pair pushes apart when very close, then attracts or repels by its table entry, fading out at
3
+ // a fixed range. The rule is not symmetric (red may chase green while green flees red), and out of that come things
4
+ // that look alive: cells with membranes, worms, spinners, swarms that hunt each other. Every half minute the table
5
+ // slowly drifts to a new random one, and the creatures melt and different ones form. Drag to stir.
6
+ const PARAMS = { particles: 2400, species: 6, range: 0.105 };
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:#05060a;touch-action:none';
11
+ stage.append(canvas);
12
+ const ctx = canvas.getContext('2d');
13
+ let W = 1, H = 1, A = 1;
14
+ const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1; A = 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); ctx.fillStyle = '#05060a'; ctx.fillRect(0, 0, W, H); };
15
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
16
+
17
+ const N = Math.max(300, Math.round(PARAMS.particles)), K = Math.max(2, Math.min(8, Math.round(PARAMS.species)));
18
+ const HUES = ['#ff4d6d', '#ffd166', '#06d6a0', '#4cc9f0', '#b388ff', '#ff9e5e', '#f2f2f2', '#7bdff2'];
19
+ const sprites = HUES.map(c => { const s = document.createElement('canvas'); s.width = s.height = 24; const g = s.getContext('2d'), r = g.createRadialGradient(12, 12, 0, 12, 12, 12); r.addColorStop(0, '#fff'); r.addColorStop(0.22, c); r.addColorStop(0.6, c + '44'); r.addColorStop(1, c + '00'); g.fillStyle = r; g.fillRect(0, 0, 24, 24); return s; });
20
+ // positions live in a wrapping box A wide and 1 tall
21
+ const x = new Float32Array(N), y = new Float32Array(N), vx = new Float32Array(N), vy = new Float32Array(N), sp = new Uint8Array(N);
22
+ for (let i = 0; i < N; i++) { x[i] = Math.random() * A; y[i] = Math.random(); sp[i] = i % K; }
23
+ const rand = () => { const m = new Float32Array(K * K); for (let i = 0; i < K * K; i++) m[i] = Math.random() * 2 - 1; return m; };
24
+ let from = rand(), to = rand(), M = new Float32Array(K * K), epoch = 0;
25
+ const ptr = { x: 0, y: 0, on: false };
26
+ const at = e => { const r = canvas.getBoundingClientRect(); ptr.x = ((e.clientX - r.left) / r.width) * A; ptr.y = (e.clientY - r.top) / r.height; };
27
+ const down = e => { ptr.on = true; at(e); canvas.setPointerCapture(e.pointerId); };
28
+ const move = e => { if (ptr.on) at(e); };
29
+ const up = () => { ptr.on = false; };
30
+ canvas.addEventListener('pointerdown', down); canvas.addEventListener('pointermove', move); canvas.addEventListener('pointerup', up); canvas.addEventListener('pointercancel', up);
31
+
32
+ const BETA = 0.3, cell = new Int32Array(N), heads = new Map();
33
+ function step(dt) {
34
+ const R = PARAMS.range, gx = Math.max(1, Math.floor(A / R)), gy = Math.max(1, Math.floor(1 / R));
35
+ heads.clear();
36
+ for (let i = 0; i < N; i++) { const k = (Math.floor((x[i] / A) * gx) % gx) * gy + (Math.floor(y[i] * gy) % gy); cell[i] = heads.get(k) ?? -1; heads.set(k, i); } // linked lists per cell
37
+ const fr = Math.pow(0.5, dt / 0.04), F = 9 * R;
38
+ for (let i = 0; i < N; i++) {
39
+ let ax = 0, ay = 0;
40
+ const cx = Math.floor((x[i] / A) * gx), cy = Math.floor(y[i] * gy), row = sp[i] * K;
41
+ for (let ox = -1; ox <= 1; ox++) for (let oy = -1; oy <= 1; oy++) {
42
+ let j = heads.get((((cx + ox) % gx + gx) % gx) * gy + (((cy + oy) % gy + gy) % gy)) ?? -1;
43
+ while (j >= 0) {
44
+ if (j !== i) {
45
+ let dx = x[j] - x[i], dy = y[j] - y[i];
46
+ if (dx > A / 2) dx -= A; else if (dx < -A / 2) dx += A; if (dy > 0.5) dy -= 1; else if (dy < -0.5) dy += 1; // the box wraps
47
+ const d = Math.sqrt(dx * dx + dy * dy) / R;
48
+ if (d > 0 && d < 1) {
49
+ const f = d < BETA ? d / BETA - 1 : M[row + sp[j]] * (1 - Math.abs(2 * d - 1 - BETA) / (1 - BETA));
50
+ ax += (dx / (d * R)) * f; ay += (dy / (d * R)) * f;
51
+ }
52
+ }
53
+ j = cell[j];
54
+ }
55
+ }
56
+ if (ptr.on) { const dx = x[i] - ptr.x, dy = y[i] - ptr.y, d2 = dx * dx + dy * dy; if (d2 < 0.02) { ax += (dx / Math.sqrt(d2 + 1e-4)) * 8; ay += (dy / Math.sqrt(d2 + 1e-4)) * 8; } }
57
+ vx[i] = vx[i] * fr + ax * F * dt; vy[i] = vy[i] * fr + ay * F * dt;
58
+ }
59
+ for (let i = 0; i < N; i++) { x[i] = (x[i] + vx[i] * dt + A) % A; y[i] = (y[i] + vy[i] * dt + 1) % 1; }
60
+ }
61
+
62
+ let raf, last = performance.now(), time = 0;
63
+ function frame(now) {
64
+ const dt = Math.min(0.033, (now - last) / 1000); last = now; time += dt;
65
+ // the table drifts to a new random one every 30 s (over 6 s)
66
+ if (time - epoch * 30 > 30) { epoch++; from = M.slice(); to = rand(); }
67
+ const k = Math.min(1, (time - epoch * 30) / 6), e = k * k * (3 - 2 * k);
68
+ for (let i = 0; i < K * K; i++) M[i] = from[i] + (to[i] - from[i]) * e;
69
+ step(dt);
70
+ // short trails: fade the last frame instead of clearing it
71
+ ctx.globalCompositeOperation = 'source-over'; ctx.fillStyle = 'rgba(5,6,10,.22)'; ctx.fillRect(0, 0, W, H);
72
+ ctx.globalCompositeOperation = 'lighter';
73
+ const s = Math.max(6, H / 66);
74
+ for (let i = 0; i < N; i++) ctx.drawImage(sprites[sp[i]], (x[i] / A) * W - s / 2, y[i] * H - s / 2, s, s);
75
+ ctx.globalCompositeOperation = 'source-over';
76
+ raf = requestAnimationFrame(frame);
77
+ }
78
+ // let the first creatures form before anyone looks
79
+ for (let i = 0; i < K * K; i++) M[i] = from[i];
80
+ for (let i = 0; i < 90; i++) step(1 / 60);
81
+ raf = requestAnimationFrame(frame);
82
+ return () => {
83
+ cancelAnimationFrame(raf); ro.disconnect();
84
+ canvas.removeEventListener('pointerdown', down); canvas.removeEventListener('pointermove', move); canvas.removeEventListener('pointerup', up); canvas.removeEventListener('pointercancel', up);
85
+ canvas.remove();
86
+ };
87
+ }
@@ -0,0 +1,171 @@
1
+ // Slime mould maps Tokyo: a quarter of a million agents on the GPU, each one sensing a chemical trail ahead-left,
2
+ // ahead and ahead-right, turning toward the strongest, stepping forward and laying down more trail. That one rule is
3
+ // how the real Physarum polycephalum forages. In 2010 researchers put oat flakes where the cities around Tokyo are
4
+ // and the mould grew a network much like the rail system. Here the flakes leak food into the trail map, light (the
5
+ // bay) is avoided, and the network tightens from a fine mesh into trunk lines between the cities. The trail is
6
+ // rendered as a wet height field, so the veins catch the light.
7
+ const PARAMS = { agents: 384, sensor: 0.39, decay: 0.9 };
8
+
9
+ export default function (stage) {
10
+ const canvas = document.createElement('canvas');
11
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
12
+ stage.append(canvas);
13
+ const gl = canvas.getContext('webgl2', { antialias: false });
14
+ if (!gl || !gl.getExtension('EXT_color_buffer_float')) { stage.textContent = 'This needs WebGL2 with float render targets.'; return () => canvas.remove(); }
15
+ const G = Math.max(64, Math.round(PARAMS.agents)), COUNT = G * G, M = 768; // agents = G², trail map M×M over the dish
16
+
17
+ // oat flakes: Tokyo in the middle and the towns around it, in dish coordinates (-1..1, y up)
18
+ const CITIES = [[0.02, 0.04], [-0.12, -0.12], [-0.2, -0.26], [0.2, 0.06], [0.3, 0.14], [0.02, 0.3], [-0.16, 0.36], [-0.38, 0.18], [-0.46, 0.0],
19
+ [-0.36, -0.2], [-0.6, -0.36], [0.24, 0.42], [0.46, 0.4], [0.56, 0.12], [0.62, -0.1], [0.1, 0.56], [-0.3, 0.56], [-0.58, 0.32], [-0.66, 0.06],
20
+ [0.36, 0.62], [0.72, 0.3], [-0.06, 0.16], [-0.24, 0.12], [0.16, 0.24], [-0.5, -0.5], [0.42, -0.34], [0.26, -0.5], [-0.02, -0.66]];
21
+ // the bay and the open sea: lit in the real experiment, which the mould avoids
22
+ const SEA = `
23
+ float sea(vec2 p) { // > 0 in the water
24
+ float bay = .2 - length((p - vec2(.06, -.36)) * vec2(1.25, .85)) + .03 * sin(p.x * 13.) * sin(p.y * 9.);
25
+ float mouth = min(.07 - abs(p.x - .08 + .12 * (p.y + .5)), -.46 - p.y); // the bay opens south to the ocean
26
+ float ocean = -.66 - p.y + .25 * p.x + .04 * sin(p.x * 11.);
27
+ return max(max(bay, ocean), mouth);
28
+ }`;
29
+ const QUAD = `#version 300 es
30
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
31
+ // the agents: sense, turn, step
32
+ const agentFS = `#version 300 es
33
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D A, TR; uniform float F, SA, SD;
34
+ ${SEA}
35
+ float h(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
36
+ float sense(vec2 p, float a) { return texture(TR, p + vec2(cos(a), sin(a)) * SD / ${M}.).r; }
37
+ void main() {
38
+ vec4 s = texture(A, uv); vec2 p = s.xy; float a = s.z;
39
+ float l = sense(p, a + SA), c = sense(p, a), r = sense(p, a - SA), rnd = h(uv * 917. + F);
40
+ float RA = .78; // turn twice the sensor angle: Jones's network regime
41
+ if (c > l && c > r) {} else if (c < l && c < r) a += (rnd < .5 ? 1. : -1.) * RA; else if (l > r) a += RA; else if (r > l) a -= RA;
42
+ vec2 q = p + vec2(cos(a), sin(a)) * 1.05 / ${M}.;
43
+ vec2 d = q * 2. - 1.;
44
+ if (length(d) > .97 || sea(d) > 0.) { a = h(uv * 31. + F) * 6.2832; q = p; } // hit the dish wall or the light: turn away
45
+ o = vec4(q, a, 1);
46
+ }`;
47
+ // deposit: every agent adds a little chemical where it stands
48
+ const depVS = `#version 300 es
49
+ uniform sampler2D A; uniform int G;
50
+ void main() { vec2 p = texelFetch(A, ivec2(gl_VertexID % G, gl_VertexID / G), 0).xy; gl_Position = vec4(p * 2. - 1., 0, 1); gl_PointSize = 1.; }`;
51
+ const depFS = `#version 300 es
52
+ precision mediump float; out vec4 o; void main() { o = vec4(.18, 0, 0, 0); }`;
53
+ // the trail map: blur, fade, keep the food sources topped up, nothing survives in the light
54
+ const trailFS = `#version 300 es
55
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D TR; uniform float DEC; uniform vec2 C[${CITIES.length}];
56
+ ${SEA}
57
+ void main() {
58
+ vec2 e = vec2(1. / ${M}.); float s = 0.;
59
+ for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) s += texture(TR, uv + vec2(x, y) * e).r;
60
+ float v = s / 9. * DEC;
61
+ vec2 d = uv * 2. - 1.;
62
+ for (int i = 0; i < ${CITIES.length}; i++) { float l = length(d - C[i]); v += 1.6 * smoothstep(.024, .008, l) + .045 * smoothstep(.16, .0, l); } // a strong source and a faint halo agents can find // the flakes feed the network
63
+ if (sea(d) > 0. || length(d) > .98) v = 0.;
64
+ o = vec4(min(v, 40.), 0, 0, 1);
65
+ }`;
66
+ // the picture: agar in a glass dish on a dark table; the trail as a wet yellow height field with highlights
67
+ const showFS = `#version 300 es
68
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D TR; uniform vec2 R; uniform vec2 C[${CITIES.length}]; uniform float T;
69
+ ${SEA}
70
+ float h(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
71
+ float n2(vec2 p) { vec2 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f); return mix(mix(h(i), h(i + vec2(1, 0)), f.x), mix(h(i + vec2(0, 1)), h(i + 1.), f.x), f.y); }
72
+ float slime(vec2 t) { return smoothstep(.15, 2.6, texture(TR, t).r); }
73
+ void main() {
74
+ float s = min(R.x, R.y);
75
+ vec2 p = (uv * R - R * .5) / (s * .46); // dish coordinates
76
+ float rr = length(p);
77
+ vec3 col = vec3(.075, .07, .068) * (1. - .35 * length(uv - .5)) * (.92 + .08 * n2(uv * R / 3.)); // the table
78
+ col *= 1. - .55 * smoothstep(1.16, .98, rr) * smoothstep(.9, 1.02, rr); // the dish's shadow
79
+ if (rr < 1.) {
80
+ vec2 t = p * .5 + .5, e = vec2(1.5 / ${M}.);
81
+ float hgt = slime(t);
82
+ vec2 g = vec2(slime(t + vec2(e.x, 0)) - slime(t - vec2(e.x, 0)), slime(t + vec2(0, e.y)) - slime(t - vec2(0, e.y)));
83
+ vec3 N = normalize(vec3(-g * 2.6, 1.));
84
+ vec3 L = normalize(vec3(-.5, .6, .8)), H = normalize(L + vec3(0, 0, 1));
85
+ // agar: a pale amber gel, slightly darker toward the rim, the lit sea a little cooler
86
+ vec3 agar = mix(vec3(.82, .74, .55), vec3(.66, .58, .42), smoothstep(.4, 1., rr)) * (.95 + .05 * n2(p * 40.));
87
+ float w = sea(p);
88
+ agar = mix(agar, vec3(.74, .76, .72), smoothstep(-.01, .03, w) * .6);
89
+ agar *= 1. - .25 * smoothstep(.04, -.01, abs(w)) * step(-.03, w); // a faint coastline
90
+ vec3 c = agar;
91
+ // oat flakes, under the mould
92
+ for (int i = 0; i < ${CITIES.length}; i++) {
93
+ vec2 q = p - C[i]; float a = atan(q.y, q.x), rad = .022 + .006 * sin(a * 3. + float(i)) + .004 * sin(a * 7. + float(i) * 2.);
94
+ float f = smoothstep(rad, rad - .006, length(q));
95
+ c = mix(c, vec3(.93, .87, .72) * (.85 + .3 * n2(q * 200.)), f);
96
+ }
97
+ // the mould: thin film is pale, thick veins saturate to deep yellow-orange
98
+ float raw = texture(TR, t).r;
99
+ c = mix(c, vec3(.96, .84, .4), smoothstep(.03, .6, raw) * .45); // the thin film between the veins
100
+ vec3 sl = mix(vec3(.98, .86, .3), vec3(.93, .62, .1), smoothstep(.55, 1., hgt));
101
+ sl *= .9 + .2 * n2(p * 90. + vec2(T * .7, -T * .4)); // grainy cytoplasm streaming through the tubes
102
+ c = mix(c, sl * (.6 + .5 * max(dot(N, L), 0.)), smoothstep(.03, .35, hgt));
103
+ c += vec3(1., .97, .85) * pow(max(dot(N, H), 0.), 60.) * .55 * smoothstep(.2, .6, hgt); // wet highlights
104
+ // the glass: a bright rim and a soft reflection across the lid
105
+ c += vec3(.9, .95, 1.) * (smoothstep(.93, .99, rr) * .25 + smoothstep(.2, .0, abs(p.x * .6 + p.y - .9)) * .05);
106
+ col = mix(col, c, smoothstep(1., .985, rr));
107
+ }
108
+ o = vec4(pow(col, vec3(1.05)), 1);
109
+ }`;
110
+
111
+ const sh = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); return s; };
112
+ const prog = (vs, fs) => {
113
+ const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p);
114
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
115
+ const u = {}; for (let i = 0, n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS); i < n; i++) { const nm = gl.getActiveUniform(p, i).name.replace('[0]', ''); u[nm] = gl.getUniformLocation(p, nm); }
116
+ return { p, u };
117
+ };
118
+ const agentP = prog(QUAD, agentFS), depP = prog(depVS, depFS), trailP = prog(QUAD, trailFS), showP = prog(QUAD, showFS);
119
+ const quad = gl.createVertexArray(); gl.bindVertexArray(quad);
120
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
121
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
122
+ const empty = gl.createVertexArray();
123
+ const target = (w, h, ifmt, type, filt, data = null) => {
124
+ const t = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t); gl.texImage2D(gl.TEXTURE_2D, 0, ifmt, w, h, 0, gl.RGBA, type, data);
125
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filt); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filt);
126
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
127
+ const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0);
128
+ return { t, fb };
129
+ };
130
+ // agents start spread over the land in the dish, pointing every which way
131
+ const init = new Float32Array(COUNT * 4);
132
+ for (let i = 0; i < COUNT; i++) {
133
+ let x, y; do { const a = Math.random() * 6.283, r = Math.sqrt(Math.random()) * 0.95; x = Math.cos(a) * r; y = Math.sin(a) * r; } while (y < -0.62 + 0.25 * x || Math.hypot((x - 0.06) * 1.25, (y + 0.36) * 0.85) < 0.2);
134
+ init.set([x * 0.5 + 0.5, y * 0.5 + 0.5, Math.random() * 6.283, 1], i * 4);
135
+ }
136
+ let A = target(G, G, gl.RGBA32F, gl.FLOAT, gl.NEAREST, init), B = target(G, G, gl.RGBA32F, gl.FLOAT, gl.NEAREST, init);
137
+ let TA = target(M, M, gl.RGBA16F, gl.HALF_FLOAT, gl.LINEAR), TB = target(M, M, gl.RGBA16F, gl.HALF_FLOAT, gl.LINEAR);
138
+ const cityArr = new Float32Array(CITIES.flat());
139
+
140
+ let W = 1, H = 1;
141
+ const resize = () => { const d = Math.min(devicePixelRatio, 2); W = canvas.width = Math.max(1, (stage.clientWidth * d) | 0); H = canvas.height = Math.max(1, (stage.clientHeight * d) | 0); };
142
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
143
+
144
+ let raf, frameNo = 0, t0 = performance.now();
145
+ function frame(now) {
146
+ frameNo++;
147
+ gl.bindVertexArray(quad);
148
+ for (let s = 0; s < 2; s++) {
149
+ // move the agents
150
+ gl.disable(gl.BLEND); gl.useProgram(agentP.p); gl.bindFramebuffer(gl.FRAMEBUFFER, B.fb); gl.viewport(0, 0, G, G);
151
+ gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, A.t); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, TA.t);
152
+ gl.uniform1i(agentP.u.A, 0); gl.uniform1i(agentP.u.TR, 1); gl.uniform1f(agentP.u.F, (frameNo * 2 + s) % 1000); gl.uniform1f(agentP.u.SA, PARAMS.sensor); gl.uniform1f(agentP.u.SD, 9);
153
+ gl.bindVertexArray(quad); gl.drawArrays(gl.TRIANGLES, 0, 3); [A, B] = [B, A];
154
+ // they deposit into the trail
155
+ gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE); gl.useProgram(depP.p); gl.bindFramebuffer(gl.FRAMEBUFFER, TA.fb); gl.viewport(0, 0, M, M);
156
+ gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, A.t); gl.uniform1i(depP.u.A, 0); gl.uniform1i(depP.u.G, G);
157
+ gl.bindVertexArray(empty); gl.drawArrays(gl.POINTS, 0, COUNT); gl.disable(gl.BLEND);
158
+ // and the trail spreads and fades
159
+ gl.useProgram(trailP.p); gl.bindFramebuffer(gl.FRAMEBUFFER, TB.fb); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, TA.t);
160
+ gl.uniform1i(trailP.u.TR, 0); gl.uniform1f(trailP.u.DEC, PARAMS.decay); gl.uniform2fv(trailP.u.C, cityArr);
161
+ gl.bindVertexArray(quad); gl.drawArrays(gl.TRIANGLES, 0, 3); [TA, TB] = [TB, TA];
162
+ }
163
+ gl.useProgram(showP.p); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, W, H);
164
+ gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, TA.t); gl.uniform1i(showP.u.TR, 0); gl.uniform2f(showP.u.R, W, H);
165
+ gl.uniform2fv(showP.u.C, cityArr); gl.uniform1f(showP.u.T, (now - t0) / 1000);
166
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
167
+ raf = requestAnimationFrame(frame);
168
+ }
169
+ raf = requestAnimationFrame(frame);
170
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
171
+ }