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,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
|
+
}
|