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,329 @@
1
+ // Jelly cube: a 4x4x4 lattice of masses held together by springs (edges, face and body diagonals, plus
2
+ // skip-one springs that resist bending) and a gentle pull back toward its own rigid shape. The lattice bends a
3
+ // smooth rounded cube through a Bezier volume. It is shaded as real jelly: light is absorbed by how much jelly
4
+ // it passes through (thicker means deeper colour), the floor behind bends through it, and its shadow glows.
5
+ // Grab and throw it, or tap to make it hop. Left alone, it hops on its own.
6
+ const PARAMS = { softness: 1, hop: 1, hue: 350 };
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;touch-action:none;cursor:grab';
11
+ stage.append(canvas);
12
+ const gl = canvas.getContext('webgl2', { antialias: true });
13
+ if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
14
+
15
+ // ── the lattice: 64 nodes, rest shape a unit cube standing on the floor
16
+ const N = 64, X = new Float32Array(N * 3), Vl = new Float32Array(N * 3), Fo = new Float32Array(N * 3), Qr = new Float32Array(N * 3);
17
+ const id = (i, j, k) => i + 4 * j + 16 * k, surf = [];
18
+ for (let k = 0; k < 4; k++) for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) {
19
+ const n = id(i, j, k); Qr[n * 3] = i / 3 - 0.5; Qr[n * 3 + 1] = j / 3 - 0.5; Qr[n * 3 + 2] = k / 3 - 0.5;
20
+ if (i % 3 === 0 || j % 3 === 0 || k % 3 === 0) surf.push(n);
21
+ }
22
+ const sA = [], sB = [], sL = [];
23
+ for (let a = 0; a < N; a++) for (let b = a + 1; b < N; b++) {
24
+ const d = [0, 1, 2].map(c => Math.round((Qr[b * 3 + c] - Qr[a * 3 + c]) * 3)), m = Math.max(...d.map(Math.abs)), z = d.filter(v => v === 0).length;
25
+ if (m <= 1 || (m === 2 && z === 2)) { sA.push(a); sB.push(b); sL.push(Math.hypot(...d) / 3); } // neighbours + diagonals, and skip-one bend springs
26
+ }
27
+ const S = sA.length;
28
+ // quaternion helpers for the rigid part
29
+ const qmul = (a, b) => [a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1], a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0], a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3], a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]];
30
+ const qmat = q => { const [x, y, z, w] = q; return [1 - 2 * (y * y + z * z), 2 * (x * y + z * w), 2 * (x * z - y * w), 2 * (x * y - z * w), 1 - 2 * (x * x + z * z), 2 * (y * z + x * w), 2 * (x * z + y * w), 2 * (y * z - x * w), 1 - 2 * (x * x + y * y)]; };
31
+ const qaxis = (ax, an) => { const l = Math.hypot(...ax) || 1, s = Math.sin(an / 2); return [ax[0] / l * s, ax[1] / l * s, ax[2] / l * s, Math.cos(an / 2)]; };
32
+ let rq = [0, 0, 0, 1];
33
+ const place = (q, c) => { const R = qmat(q); for (let n = 0; n < N; n++) { const x = Qr[n * 3], y = Qr[n * 3 + 1], z = Qr[n * 3 + 2]; for (let r = 0; r < 3; r++) { X[n * 3 + r] = R[r] * x + R[3 + r] * y + R[6 + r] * z + c[r]; Vl[n * 3 + r] = 0; } } };
34
+ place(qaxis([1, 0, 0.6], 0.55), [0.15, 1.35, 0]); rq = qaxis([1, 0, 0.6], 0.55);
35
+
36
+ // ── the visible surface: a rounded box, each vertex fixed inside the lattice (tricubic Bezier weights)
37
+ const G = 16, RB = 0.24, verts = [], keys = new Map(), tris = [];
38
+ const vkey = p => p.map(v => Math.round(v * 1e4)).join(',');
39
+ const addV = p => { const k = vkey(p); if (keys.has(k)) return keys.get(k); keys.set(k, verts.length / 3); verts.push(...p); return verts.length / 3 - 1; };
40
+ const round = p => { const c = p.map(v => Math.max(-(1 - RB), Math.min(1 - RB, v))), d = p.map((v, i) => v - c[i]), l = Math.hypot(...d) || 1; return c.map((v, i) => (v + d[i] / l * RB) * 0.5 * 0.985 + 0.5); };
41
+ for (let ax = 0; ax < 3; ax++) for (const sgn of [-1, 1]) {
42
+ const grid = [];
43
+ for (let b = 0; b <= G; b++) for (let a = 0; a <= G; a++) { const p = [0, 0, 0]; p[ax] = sgn; p[(ax + 1) % 3] = a / G * 2 - 1; p[(ax + 2) % 3] = b / G * 2 - 1; grid.push(addV(round(p))); }
44
+ for (let b = 0; b < G; b++) for (let a = 0; a < G; a++) { const i0 = grid[b * (G + 1) + a], i1 = grid[b * (G + 1) + a + 1], i2 = grid[(b + 1) * (G + 1) + a], i3 = grid[(b + 1) * (G + 1) + a + 1]; tris.push([i0, i1, i3], [i0, i3, i2]); }
45
+ }
46
+ const NV = verts.length / 3, idx = new Uint16Array(tris.length * 3);
47
+ tris.forEach((t, i) => { // wind every triangle so its normal points out of the box
48
+ const P = t.map(v => [verts[v * 3] - 0.5, verts[v * 3 + 1] - 0.5, verts[v * 3 + 2] - 0.5]);
49
+ const e1 = P[1].map((v, c) => v - P[0][c]), e2 = P[2].map((v, c) => v - P[0][c]), cr = [e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]];
50
+ const out = cr[0] * (P[0][0] + P[1][0] + P[2][0]) + cr[1] * (P[0][1] + P[1][1] + P[2][1]) + cr[2] * (P[0][2] + P[1][2] + P[2][2]) > 0;
51
+ idx.set(out ? t : [t[0], t[2], t[1]], i * 3);
52
+ });
53
+ const bern = u => [(1 - u) ** 3, 3 * u * (1 - u) ** 2, 3 * u * u * (1 - u), u ** 3];
54
+ const W = new Float32Array(NV * 64);
55
+ for (let v = 0; v < NV; v++) { const bu = bern(verts[v * 3]), bv = bern(verts[v * 3 + 1]), bw = bern(verts[v * 3 + 2]); for (let k = 0; k < 4; k++) for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) W[v * 64 + id(i, j, k)] = bu[i] * bv[j] * bw[k]; }
56
+ const POS = new Float32Array(NV * 3), NOR = new Float32Array(NV * 3);
57
+ const BUB = [[.34, .64, .4, .052], [.66, .34, .58, .036], [.5, .78, .72, .028], [.72, .62, .26, .042]];
58
+ const BW = BUB.map(([u, v, w]) => { const bu = bern(u), bv = bern(v), bw = bern(w), o = new Float32Array(64); for (let k = 0; k < 4; k++) for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) o[id(i, j, k)] = bu[i] * bv[j] * bw[k]; return o; });
59
+ const BF = new Float32Array(16);
60
+
61
+ // ── GL programs
62
+ const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x); if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(x)); return x; };
63
+ const link = (v, f) => { const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, v)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, f)); gl.bindAttribLocation(p, 0, 'pos'); gl.bindAttribLocation(p, 1, 'nor'); gl.linkProgram(p); if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p)); return p; };
64
+ const quadVS = `#version 300 es
65
+ in vec2 pos; out vec2 uv; void main() { uv = pos * .5 + .5; gl_Position = vec4(pos, 0, 1); }`;
66
+ const meshVS = `#version 300 es
67
+ in vec3 pos, nor; uniform mat4 PV; out vec3 vW, vN; void main() { vW = pos; vN = nor; gl_Position = PV * vec4(pos, 1); }`;
68
+ const ENV = `
69
+ uniform vec3 L;
70
+ vec3 env(vec3 d) {
71
+ vec3 c = mix(vec3(.30, .27, .27), vec3(.78, .75, .73), smoothstep(-.25, .5, d.y));
72
+ if (d.y > 0.) {
73
+ vec2 q = d.xz / d.y - vec2(-.95, .75); // big softbox, up and to the left
74
+ vec2 b = abs(q) - vec2(.7, .45); float r = length(max(b, 0.)) + min(max(b.x, b.y), 0.) - .12;
75
+ c += vec3(3.2, 3.1, 3.) * smoothstep(.06, -.04, r);
76
+ }
77
+ float az = atan(d.x, d.z);
78
+ c += vec3(1.5, 1.55, 1.7) * smoothstep(.07, .02, abs(az - 1.35)) * smoothstep(-.1, .15, d.y) * smoothstep(.8, .45, d.y); // tall strip on the right
79
+ return c;
80
+ }`;
81
+ const bgProg = link(quadVS, `#version 300 es
82
+ precision highp float; in vec2 uv; out vec4 o;
83
+ uniform vec3 E, CF, CR, CU, TINT, SIG; uniform vec2 R; uniform float TH, SH_B, SH_K, HIT; uniform vec2 H[32]; uniform int HN; uniform vec3 CEN;
84
+ float sdPoly(vec2 p) {
85
+ float d = 1e9, s = 1.;
86
+ for (int i = 0; i < 32; i++) {
87
+ if (i >= HN) break;
88
+ int j = i == 0 ? HN - 1 : i - 1;
89
+ vec2 e = H[j] - H[i], w = p - H[i], b = w - e * clamp(dot(w, e) / dot(e, e), 0., 1.);
90
+ d = min(d, dot(b, b));
91
+ bvec3 c = bvec3(p.y >= H[i].y, p.y < H[j].y, e.x * w.y > e.y * w.x);
92
+ if (all(c) || all(not(c))) s = -s;
93
+ }
94
+ return s * sqrt(d);
95
+ }
96
+ void main() {
97
+ vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.) * TH;
98
+ vec3 rd = normalize(CF + CR * q.x + CU * q.y);
99
+ vec3 back = mix(vec3(.80, .77, .74), vec3(.58, .55, .56), smoothstep(-.1, .45, rd.y));
100
+ vec3 col = back;
101
+ if (rd.y < 0.) {
102
+ float t = -E.y / rd.y; vec3 p = E + rd * t;
103
+ vec3 fl = vec3(.86, .83, .79) * (.78 + .3 * exp(-dot(p.xz, p.xz) * .07)); // a pool of light on a pale floor
104
+ float sd = sdPoly(p.xz);
105
+ float inside = (1. - smoothstep(-SH_B, SH_B, sd)) * SH_K; // shadow cast along the light
106
+ float edge = exp(-pow(sd / (SH_B + .05), 2.)) * SH_K; // light bent away at the rim leaves a darker edge
107
+ float ao = exp(-dot(p.xz - CEN.xz, p.xz - CEN.xz) * 3.2) * .5 * HIT; // contact darkening right under it
108
+ vec3 thru = exp(-SIG * .55) * .8 + .08; // light that passed through jelly is coloured
109
+ fl *= mix(vec3(1), thru, inside) * (1. - .4 * edge) * (1. - ao);
110
+ fl += TINT * TINT * smoothstep(.0, .3, -sd) * SH_K * .45; // and gathers into a glowing core
111
+ col = mix(fl, back, smoothstep(5., 16., t));
112
+ }
113
+ col *= 1. - .25 * pow(length(uv - .5) * 1.25, 2.);
114
+ o = vec4(col, 1);
115
+ }`);
116
+ const backProg = link(meshVS, `#version 300 es
117
+ precision highp float; in vec3 vW, vN; out vec4 o; uniform vec3 E;
118
+ void main() { float x = clamp(length(vW - E) / 12., 0., .9999); float h = floor(x * 255.) / 255.; o = vec4(h, (x - h) * 255., 0, 1); }`);
119
+ const copyProg = link(quadVS, `#version 300 es
120
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D BG; void main() { o = texture(BG, uv); }`);
121
+ const jellyProg = link(meshVS, `#version 300 es
122
+ precision highp float; in vec3 vW, vN; out vec4 o;
123
+ uniform sampler2D BG, BK; uniform vec2 R; uniform vec3 E, SIG, TINT; uniform mat4 VM; uniform vec4 B[4];
124
+ ${ENV}
125
+ void main() {
126
+ vec3 n = normalize(vN), v = normalize(E - vW);
127
+ float cv = max(dot(n, v), 0.), F = .04 + .96 * pow(1. - cv, 5.);
128
+ vec2 s = gl_FragCoord.xy / R;
129
+ vec4 bk = texture(BK, s);
130
+ float th = clamp((bk.x + bk.y / 255.) * 12. - length(vW - E), 0., 2.); // how much jelly this ray crosses
131
+ vec3 nv = mat3(VM) * n;
132
+ vec2 off = -nv.xy * (.05 + .15 * th) * vec2(R.y / R.x, 1.); // the floor behind bends through it
133
+ vec3 behind = pow(texture(BG, clamp(s + off, .001, .999)).rgb, vec3(2.2));
134
+ vec3 trans = behind * exp(-SIG * th * 1.25); // Beer's law: thicker, deeper colour
135
+ float wrap = dot(n, L) * .5 + .5;
136
+ vec3 scat = TINT * (1. - exp(-th * 2.)) * (.05 + .2 * wrap) + TINT * pow(1. - cv, 2.) * .22; // light scattered inside, brightest at the rims
137
+ vec3 rd = refract(-v, n, .75);
138
+ for (int i = 0; i < 4; i++) { // a few trapped air bubbles, seen through the bent ray
139
+ vec3 oc = vW - B[i].xyz; float b = dot(oc, rd), c = dot(oc, oc) - B[i].w * B[i].w, h = b * b - c;
140
+ if (h > 0.) { float tt = -b - sqrt(h); if (tt > 0. && tt < th) { vec3 nb = normalize(oc + rd * tt); float cs = abs(dot(nb, rd)), rim = smoothstep(.55, .0, cs);
141
+ vec3 clear = behind * exp(-SIG * max(th - B[i].w * 2., 0.) * 1.25); // less jelly in the way: clearer middle
142
+ trans = mix(clear, trans * .55, rim) + pow(max(dot(reflect(rd, nb), L), 0.), 60.) * .9 + smoothstep(.25, .0, cs) * .06; } }
143
+ }
144
+ vec3 r = reflect(-v, n);
145
+ vec3 refl = env(r) * (r.y < 0. ? .55 : 1.);
146
+ vec3 col = mix(trans + scat, refl, F);
147
+ vec3 h = normalize(L + v); float nh = max(dot(n, h), 0.);
148
+ col += pow(nh, 220.) * 2.2 + pow(nh, 24.) * .06;
149
+ col = col / (1. + col * .18);
150
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
151
+ }`);
152
+
153
+ const quad = gl.createVertexArray(); gl.bindVertexArray(quad);
154
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
155
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
156
+ const mesh = gl.createVertexArray(); gl.bindVertexArray(mesh);
157
+ const posBuf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, posBuf); gl.bufferData(gl.ARRAY_BUFFER, POS.byteLength, gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
158
+ const norBuf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, norBuf); gl.bufferData(gl.ARRAY_BUFFER, NOR.byteLength, gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
159
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, idx, gl.STATIC_DRAW);
160
+ const U = (p, n) => gl.getUniformLocation(p, n);
161
+
162
+ // two offscreen targets: the floor (sampled through the jelly) and the jelly's back-face distance
163
+ const mkTarget = depth => { const tex = gl.createTexture(), fb = gl.createFramebuffer(), rb = depth ? gl.createRenderbuffer() : null; return { tex, fb, rb }; };
164
+ const tBG = mkTarget(false), tBK = mkTarget(true);
165
+ const alloc = (t, w, h) => {
166
+ gl.bindTexture(gl.TEXTURE_2D, t.tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
167
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, t.rb ? gl.NEAREST : gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, t.rb ? gl.NEAREST : gl.LINEAR);
168
+ 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);
169
+ gl.bindFramebuffer(gl.FRAMEBUFFER, t.fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t.tex, 0);
170
+ if (t.rb) { gl.bindRenderbuffer(gl.RENDERBUFFER, t.rb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, w, h); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, t.rb); }
171
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
172
+ };
173
+ let cw = 0, ch = 0, Wc = 1, Hc = 1;
174
+ const resize = () => { const d = Math.min(devicePixelRatio, 2); Wc = stage.clientWidth || 1; Hc = stage.clientHeight || 1; cw = canvas.width = Wc * d | 0; ch = canvas.height = Hc * d | 0; alloc(tBG, cw, ch); alloc(tBK, cw, ch); };
175
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
176
+
177
+ // ── camera (look-at, column-major)
178
+ const FOV = 0.62, TH = Math.tan(FOV / 2), Ln = [-0.55, 1, 0.42], Ll = Math.hypot(...Ln), Ld = Ln.map(v => v / Ll);
179
+ const norm = a => { const l = Math.hypot(...a) || 1; return a.map(v => v / l); }, crs = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
180
+ let cam = null;
181
+ const camera = () => {
182
+ const asp = cw / ch, fit = Math.max(1, 1.25 / asp), eye = [0, 1.75 * fit, 4.7 * fit], ta = [0, 0.7, 0];
183
+ const f = norm(ta.map((v, i) => v - eye[i])), r = norm(crs(f, [0, 1, 0])), u = crs(r, f);
184
+ const VM = new Float32Array([r[0], u[0], -f[0], 0, r[1], u[1], -f[1], 0, r[2], u[2], -f[2], 0, -(r[0] * eye[0] + r[1] * eye[1] + r[2] * eye[2]), -(u[0] * eye[0] + u[1] * eye[1] + u[2] * eye[2]), f[0] * eye[0] + f[1] * eye[1] + f[2] * eye[2], 1]);
185
+ const t = 1 / TH, nr = 0.1, fr = 40, P = new Float32Array([t / asp, 0, 0, 0, 0, t, 0, 0, 0, 0, (fr + nr) / (nr - fr), -1, 0, 0, 2 * fr * nr / (nr - fr), 0]);
186
+ const PV = new Float32Array(16); for (let c = 0; c < 4; c++) for (let rr = 0; rr < 4; rr++) { let s = 0; for (let k = 0; k < 4; k++) s += P[k * 4 + rr] * VM[c * 4 + k]; PV[c * 4 + rr] = s; }
187
+ cam = { eye, f, r, u, VM, PV, asp };
188
+ };
189
+ const ray = (px, py) => { const x = (px / Wc * 2 - 1) * TH * cam.asp, y = (1 - py / Hc * 2) * TH; return norm(cam.f.map((v, i) => v + cam.r[i] * x + cam.u[i] * y)); };
190
+ const toScreen = (p) => { const P = cam.PV, w = P[3] * p[0] + P[7] * p[1] + P[11] * p[2] + P[15]; return [((P[0] * p[0] + P[4] * p[1] + P[8] * p[2] + P[12]) / w * 0.5 + 0.5) * Wc, (0.5 - (P[1] * p[0] + P[5] * p[1] + P[9] * p[2] + P[13]) / w * 0.5) * Hc]; };
191
+
192
+ // ── pointer: grab a node and drag (the jelly stretches, then flies when let go); a quick tap makes it hop
193
+ let grab = null, lastInput = -1e9;
194
+ const nodeAt = (x, y) => { let best = -1, bd = Math.max(46, Hc * 0.16) ** 2; for (const n of surf) { const s = toScreen([X[n * 3], X[n * 3 + 1], X[n * 3 + 2]]), d = (s[0] - x) ** 2 + (s[1] - y) ** 2; if (d < bd) { bd = d; best = n; } } return best; };
195
+ const local = e => { const b = canvas.getBoundingClientRect(); return [e.clientX - b.left, e.clientY - b.top]; };
196
+ const onDown = e => {
197
+ if (!cam) return; const [x, y] = local(e), n = nodeAt(x, y); lastInput = performance.now(); if (n < 0) return;
198
+ const p = [X[n * 3], X[n * 3 + 1], X[n * 3 + 2]], depth = p.reduce((s, v, i) => s + (v - cam.eye[i]) * cam.f[i], 0);
199
+ grab = { n, depth, target: p, t0: performance.now(), x, y, moved: 0 }; canvas.style.cursor = 'grabbing'; canvas.setPointerCapture?.(e.pointerId);
200
+ };
201
+ const onMove = e => {
202
+ if (!grab) return; const [x, y] = local(e), d = ray(x, y), k = grab.depth / d.reduce((s, v, i) => s + v * cam.f[i], 0);
203
+ grab.target = cam.eye.map((v, i) => v + d[i] * k); grab.target[1] = Math.max(0.02, grab.target[1]);
204
+ grab.moved = Math.max(grab.moved, Math.hypot(x - grab.x, y - grab.y)); lastInput = performance.now();
205
+ };
206
+ const onUp = () => {
207
+ if (grab && grab.moved < 6 && performance.now() - grab.t0 < 300) { // a tap: pop up and away from where it was touched
208
+ const g = grab.n; for (let n = 0; n < N; n++) { const dx = X[n * 3] - X[g * 3], dz = X[n * 3 + 2] - X[g * 3 + 2], w = Math.exp(-(dx * dx + dz * dz) * 3); Vl[n * 3] += dx * 2.2 * w; Vl[n * 3 + 1] += 4.2 * PARAMS.hop * (0.6 + 0.4 * w); Vl[n * 3 + 2] += dz * 2.2 * w; }
209
+ }
210
+ grab = null; canvas.style.cursor = 'grab'; lastInput = performance.now();
211
+ };
212
+ canvas.addEventListener('pointerdown', onDown); canvas.addEventListener('pointermove', onMove); canvas.addEventListener('pointerup', onUp); canvas.addEventListener('pointercancel', onUp);
213
+
214
+ // ── physics
215
+ const GRAV = 14, cen = [0, 0, 0];
216
+ let nextHop = 1.75, hopN = 0;
217
+ const hop = () => { // idle autopilot: a hop with a spin, always aimed back toward the middle
218
+ const a = hopN++ * 2.39996 + 0.7, ax = [Math.cos(a), 0.25 * Math.sin(a * 1.7), Math.sin(a)], w = (4.2 + 2.2 * Math.abs(Math.sin(a * 3.1))) * Math.min(1.4, PARAMS.hop), up = 4.1 * PARAMS.hop;
219
+ const axn = norm(ax), vx = -cen[0] * 1.6 + Math.cos(a * 2.3) * 0.35, vz = -cen[2] * 1.6 + Math.sin(a * 1.9) * 0.25;
220
+ for (let n = 0; n < N; n++) { const rx = X[n * 3] - cen[0], ry = X[n * 3 + 1] - cen[1], rz = X[n * 3 + 2] - cen[2]; Vl[n * 3] += vx + w * (axn[1] * rz - axn[2] * ry); Vl[n * 3 + 1] += up + w * (axn[2] * rx - axn[0] * rz); Vl[n * 3 + 2] += vz + w * (axn[0] * ry - axn[1] * rx); }
221
+ };
222
+ function step(h) {
223
+ const soft = Math.max(0.3, PARAMS.softness), KS = 1100 / soft, CS = 2, KG = 60 / soft;
224
+ cen[0] = cen[1] = cen[2] = 0; for (let n = 0; n < N; n++) for (let c = 0; c < 3; c++) cen[c] += X[n * 3 + c] / N;
225
+ // best-fit rotation of the rest shape (polar decomposition by quaternion iteration, warm-started)
226
+ const A = new Float64Array(9);
227
+ for (let n = 0; n < N; n++) for (let c = 0; c < 3; c++) { const q = Qr[n * 3 + c]; for (let r = 0; r < 3; r++) A[c * 3 + r] += (X[n * 3 + r] - cen[r]) * q; }
228
+ for (let it = 0; it < 4; it++) {
229
+ const R = qmat(rq), om = [0, 0, 0]; let dn = 0;
230
+ for (let c = 0; c < 3; c++) { const rc = [R[c * 3], R[c * 3 + 1], R[c * 3 + 2]], ac = [A[c * 3], A[c * 3 + 1], A[c * 3 + 2]], x = crs(rc, ac); om[0] += x[0]; om[1] += x[1]; om[2] += x[2]; dn += rc[0] * ac[0] + rc[1] * ac[1] + rc[2] * ac[2]; }
231
+ const o = om.map(v => v / (Math.abs(dn) + 1e-9)), w = Math.hypot(...o); if (w < 1e-9) break;
232
+ rq = qmul(qaxis(o, w), rq); const l = Math.hypot(...rq); rq = rq.map(v => v / l);
233
+ }
234
+ const R = qmat(rq);
235
+ for (let n = 0; n < N; n++) {
236
+ const x = Qr[n * 3], y = Qr[n * 3 + 1], z = Qr[n * 3 + 2];
237
+ for (let r = 0; r < 3; r++) Fo[n * 3 + r] = KG * (R[r] * x + R[3 + r] * y + R[6 + r] * z + cen[r] - X[n * 3 + r]);
238
+ Fo[n * 3 + 1] -= GRAV;
239
+ }
240
+ for (let s = 0; s < S; s++) {
241
+ const a = sA[s] * 3, b = sB[s] * 3, dx = X[b] - X[a], dy = X[b + 1] - X[a + 1], dz = X[b + 2] - X[a + 2], L = Math.hypot(dx, dy, dz) || 1e-6;
242
+ const ux = dx / L, uy = dy / L, uz = dz / L, rv = (Vl[b] - Vl[a]) * ux + (Vl[b + 1] - Vl[a + 1]) * uy + (Vl[b + 2] - Vl[a + 2]) * uz;
243
+ const f = KS * (L - sL[s]) + CS * rv;
244
+ Fo[a] += f * ux; Fo[a + 1] += f * uy; Fo[a + 2] += f * uz; Fo[b] -= f * ux; Fo[b + 1] -= f * uy; Fo[b + 2] -= f * uz;
245
+ }
246
+ if (grab) { // pull the grabbed node and its neighbours toward the pointer
247
+ const g = grab.n * 3, ex = grab.target[0] - X[g], ey = grab.target[1] - X[g + 1], ez = grab.target[2] - X[g + 2];
248
+ for (let n = 0; n < N; n++) { const d2 = (Qr[n * 3] - Qr[g]) ** 2 + (Qr[n * 3 + 1] - Qr[g + 1]) ** 2 + (Qr[n * 3 + 2] - Qr[g + 2]) ** 2, w = Math.exp(-d2 * 9) * 950; if (w < 3) continue; Fo[n * 3] += ex * w - Vl[n * 3] * w * 0.04; Fo[n * 3 + 1] += ey * w - Vl[n * 3 + 1] * w * 0.04; Fo[n * 3 + 2] += ez * w - Vl[n * 3 + 2] * w * 0.04; }
249
+ }
250
+ for (let i = 0; i < N * 3; i++) { Vl[i] = (Vl[i] + Fo[i] * h) * (1 - 0.15 * h); X[i] += Vl[i] * h; }
251
+ for (let n = 0; n < N; n++) {
252
+ const i = n * 3;
253
+ if (X[i + 1] < 0) { // floor: no bounce per node (the springs give the bounce), friction from the impact
254
+ X[i + 1] = 0; const vn = Math.max(0, -Vl[i + 1]); Vl[i + 1] = Math.max(0, Vl[i + 1]);
255
+ const vt = Math.hypot(Vl[i], Vl[i + 2]); if (vt > 0) { const k = Math.max(0, vt - (0.55 * vn + 0.004)) / vt; Vl[i] *= k; Vl[i + 2] *= k; }
256
+ }
257
+ for (const c of [0, 2]) { const lim = c === 0 ? 1.7 : 1.2; if (X[i + c] > lim) { X[i + c] = lim; Vl[i + c] = -Math.abs(Vl[i + c]) * 0.3; } else if (X[i + c] < -lim) { X[i + c] = -lim; Vl[i + c] = Math.abs(Vl[i + c]) * 0.3; } }
258
+ }
259
+ }
260
+
261
+ // convex hull of the lattice's shadow on the floor (projected along the light)
262
+ const hull = pts => {
263
+ pts.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
264
+ const c = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]), lo = [], up = [];
265
+ for (const p of pts) { while (lo.length >= 2 && c(lo[lo.length - 2], lo[lo.length - 1], p) <= 0) lo.pop(); lo.push(p); }
266
+ for (let i = pts.length - 1; i >= 0; i--) { const p = pts[i]; while (up.length >= 2 && c(up[up.length - 2], up[up.length - 1], p) <= 0) up.pop(); up.push(p); }
267
+ return lo.slice(0, -1).concat(up.slice(0, -1));
268
+ };
269
+ const tint = () => { const h = ((PARAMS.hue % 360) + 360) % 360 / 60, s = 0.9, l = 0.5, C = (1 - Math.abs(2 * l - 1)) * s, x = C * (1 - Math.abs(h % 2 - 1)), m = l - C / 2;
270
+ const [r, g, b] = h < 1 ? [C, x, 0] : h < 2 ? [x, C, 0] : h < 3 ? [0, C, x] : h < 4 ? [0, x, C] : h < 5 ? [x, 0, C] : [C, 0, x]; return [r + m, g + m, b + m]; };
271
+
272
+ let raf, last = performance.now(); const t0 = last;
273
+ function frame(now) {
274
+ const dt = Math.min(1 / 30, (now - last) / 1000), t = (now - t0) / 1000; last = now;
275
+ if (!grab && now - lastInput > 2500 && t > nextHop) { hop(); nextHop = t + 2.5; }
276
+ else if (grab || now - lastInput <= 2500) nextHop = Math.max(nextHop, t + 1.2);
277
+ const n = Math.max(1, Math.ceil(dt * 720)); for (let i = 0; i < n; i++) step(dt / n);
278
+ camera();
279
+ // deform the rounded box through the lattice, then smooth normals
280
+ for (let v = 0; v < NV; v++) { let x = 0, y = 0, z = 0; const o = v * 64; for (let k = 0; k < 64; k++) { const w = W[o + k]; x += w * X[k * 3]; y += w * X[k * 3 + 1]; z += w * X[k * 3 + 2]; } POS[v * 3] = x; POS[v * 3 + 1] = y; POS[v * 3 + 2] = z; }
281
+ NOR.fill(0);
282
+ for (let i = 0; i < idx.length; i += 3) {
283
+ const a = idx[i] * 3, b = idx[i + 1] * 3, c = idx[i + 2] * 3, e1x = POS[b] - POS[a], e1y = POS[b + 1] - POS[a + 1], e1z = POS[b + 2] - POS[a + 2], e2x = POS[c] - POS[a], e2y = POS[c + 1] - POS[a + 1], e2z = POS[c + 2] - POS[a + 2];
284
+ const nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
285
+ for (const q of [a, b, c]) { NOR[q] += nx; NOR[q + 1] += ny; NOR[q + 2] += nz; }
286
+ }
287
+ BW.forEach((w, b) => { let x = 0, y = 0, z = 0; for (let k = 0; k < 64; k++) { x += w[k] * X[k * 3]; y += w[k] * X[k * 3 + 1]; z += w[k] * X[k * 3 + 2]; } BF.set([x, y, z, BUB[b][3]], b * 4); });
288
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf); gl.bufferSubData(gl.ARRAY_BUFFER, 0, POS);
289
+ gl.bindBuffer(gl.ARRAY_BUFFER, norBuf); gl.bufferSubData(gl.ARRAY_BUFFER, 0, NOR);
290
+ // shadow footprint, its softness (grows with height) and how close the jelly is to the floor
291
+ const sp = surf.map(k => { const y = X[k * 3 + 1]; return [X[k * 3] - Ld[0] / Ld[1] * y, X[k * 3 + 2] - Ld[2] / Ld[1] * y]; });
292
+ const H = hull(sp).slice(0, 32), Hf = new Float32Array(64); H.forEach((p, i) => { Hf[i * 2] = p[0]; Hf[i * 2 + 1] = p[1]; });
293
+ let minY = 1e9; for (let k = 0; k < N; k++) minY = Math.min(minY, X[k * 3 + 1]);
294
+ const lift = Math.max(0, cen[1] - 0.5), TI = tint(), SIG = TI.map(c => (1 - c) * 2.4 + 0.1);
295
+
296
+ gl.bindFramebuffer(gl.FRAMEBUFFER, tBG.fb); gl.viewport(0, 0, cw, ch); gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE);
297
+ gl.useProgram(bgProg); gl.bindVertexArray(quad);
298
+ gl.uniform3fv(U(bgProg, 'E'), cam.eye); gl.uniform3fv(U(bgProg, 'CF'), cam.f); gl.uniform3fv(U(bgProg, 'CR'), cam.r); gl.uniform3fv(U(bgProg, 'CU'), cam.u);
299
+ gl.uniform2f(U(bgProg, 'R'), cw, ch); gl.uniform1f(U(bgProg, 'TH'), TH); gl.uniform3fv(U(bgProg, 'TINT'), TI); gl.uniform3fv(U(bgProg, 'SIG'), SIG); gl.uniform3fv(U(bgProg, 'CEN'), cen);
300
+ gl.uniform2fv(U(bgProg, 'H'), Hf); gl.uniform1i(U(bgProg, 'HN'), H.length);
301
+ gl.uniform1f(U(bgProg, 'SH_B'), 0.05 + lift * 0.28); gl.uniform1f(U(bgProg, 'SH_K'), 0.85 / (1 + lift * 1.6)); gl.uniform1f(U(bgProg, 'HIT'), Math.exp(-minY * 5));
302
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
303
+
304
+ gl.bindFramebuffer(gl.FRAMEBUFFER, tBK.fb); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
305
+ gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); gl.cullFace(gl.FRONT);
306
+ gl.useProgram(backProg); gl.bindVertexArray(mesh);
307
+ gl.uniformMatrix4fv(U(backProg, 'PV'), false, cam.PV); gl.uniform3fv(U(backProg, 'E'), cam.eye);
308
+ gl.drawElements(gl.TRIANGLES, idx.length, gl.UNSIGNED_SHORT, 0);
309
+
310
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, cw, ch); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
311
+ gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE);
312
+ gl.useProgram(copyProg); gl.bindVertexArray(quad); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tBG.tex); gl.uniform1i(U(copyProg, 'BG'), 0);
313
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
314
+ gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK);
315
+ gl.useProgram(jellyProg); gl.bindVertexArray(mesh);
316
+ gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, tBK.tex); gl.uniform1i(U(jellyProg, 'BG'), 0); gl.uniform1i(U(jellyProg, 'BK'), 1);
317
+ gl.uniformMatrix4fv(U(jellyProg, 'PV'), false, cam.PV); gl.uniformMatrix4fv(U(jellyProg, 'VM'), false, cam.VM);
318
+ gl.uniform3fv(U(jellyProg, 'E'), cam.eye); gl.uniform2f(U(jellyProg, 'R'), cw, ch); gl.uniform3fv(U(jellyProg, 'L'), Ld);
319
+ gl.uniform3fv(U(jellyProg, 'SIG'), SIG); gl.uniform3fv(U(jellyProg, 'TINT'), TI); gl.uniform4fv(U(jellyProg, 'B'), BF);
320
+ gl.drawElements(gl.TRIANGLES, idx.length, gl.UNSIGNED_SHORT, 0);
321
+ raf = requestAnimationFrame(frame);
322
+ }
323
+ raf = requestAnimationFrame(frame);
324
+ return () => {
325
+ cancelAnimationFrame(raf); ro.disconnect();
326
+ canvas.removeEventListener('pointerdown', onDown); canvas.removeEventListener('pointermove', onMove); canvas.removeEventListener('pointerup', onUp); canvas.removeEventListener('pointercancel', onUp);
327
+ gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove();
328
+ };
329
+ }
@@ -0,0 +1,80 @@
1
+ // Klein bottle: a surface with no inside or outside. The neck bends round and passes through the wall to join the
2
+ // base from within, so a path along it ends up on the "other" side. It is built from its parametric equations as a
3
+ // triangle mesh (normals from the derivative directions), drawn with depth testing and lit on both faces, since every
4
+ // face is seen from both sides. Glowing grid lines follow the parameter lines, which makes the twist readable.
5
+ const PARAMS = { spin: 1, grid: 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 gl = canvas.getContext('webgl2', { antialias: true });
12
+ if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
13
+ // the classic "bottle" parametrisation, u in [0, π), v in [0, 2π)
14
+ const K = (u, v) => {
15
+ const cu = Math.cos(u), su = Math.sin(u), cv = Math.cos(v), sv = Math.sin(v);
16
+ const x = -2 / 15 * cu * (3 * cv - 30 * su + 90 * cu ** 4 * su - 60 * cu ** 6 * su + 5 * cu * cv * su);
17
+ const y = -1 / 15 * su * (3 * cv - 3 * cu ** 2 * cv - 48 * cu ** 4 * cv + 48 * cu ** 6 * cv - 60 * su + 5 * cu * cv * su - 5 * cu ** 3 * cv * su - 80 * cu ** 5 * cv * su + 80 * cu ** 7 * cv * su);
18
+ const z = 2 / 15 * (3 + 5 * cu * su) * sv;
19
+ return [x, y, z];
20
+ };
21
+ const NU = 160, NV = 48, pos = [], nor = [], tex = [], idx = [];
22
+ for (let i = 0; i <= NU; i++) for (let j = 0; j <= NV; j++) {
23
+ const u = i / NU * Math.PI, v = j / NV * Math.PI * 2, e = 1e-4, p = K(u, v), pu = K(u + e, v), pv = K(u, v + e);
24
+ const a = pu.map((c, k) => c - p[k]), b = pv.map((c, k) => c - p[k]);
25
+ let n = [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; const l = Math.hypot(...n) || 1; n = n.map(c => c / l);
26
+ pos.push(...p); nor.push(...n); tex.push(i / NU, j / NV);
27
+ }
28
+ // centre it and scale it to a unit-ish size
29
+ const lo = [1e9, 1e9, 1e9], hi = [-1e9, -1e9, -1e9];
30
+ for (let i = 0; i < pos.length; i++) { lo[i % 3] = Math.min(lo[i % 3], pos[i]); hi[i % 3] = Math.max(hi[i % 3], pos[i]); }
31
+ const mid = lo.map((v, k) => (v + hi[k]) / 2), span = Math.max(...hi.map((v, k) => v - lo[k]));
32
+ for (let i = 0; i < pos.length; i++) pos[i] = (pos[i] - mid[i % 3]) / span * 4.6;
33
+ for (let i = 0; i < NU; i++) for (let j = 0; j < NV; j++) { const a = i * (NV + 1) + j, b = a + NV + 1; idx.push(a, b, a + 1, b, b + 1, a + 1); }
34
+ const vs = `#version 300 es
35
+ in vec3 P, N; in vec2 UV; uniform mat4 M, V; out vec3 n, w; out vec2 uv;
36
+ void main() { vec4 wp = M * vec4(P, 1); w = wp.xyz; n = mat3(M) * N; uv = UV; gl_Position = V * wp; }`;
37
+ const fs = `#version 300 es
38
+ precision highp float; in vec3 n, w; in vec2 uv; uniform vec3 E; uniform float G, T; out vec4 o;
39
+ void main() {
40
+ vec3 N = normalize(n) * (gl_FrontFacing ? 1. : -1.), vd = normalize(E - w);
41
+ if (dot(N, vd) < 0.) N = -N; // lit from whichever side we see
42
+ vec3 L = normalize(vec3(.5, .9, -.4));
43
+ float dif = max(dot(N, L), 0.), rim = pow(1. - max(dot(N, vd), 0.), 3.);
44
+ vec3 base = .5 + .5 * cos(6.28 * (uv.x * .9 + uv.y * .15 + vec3(0, .33, .67)) + T * .3); // hue flows along the bottle
45
+ // a see-through hologram: faint body, bright rims and grid lines, all added up, so the neck shows passing through the wall
46
+ vec3 col = base * (.05 + .12 * dif) + base * rim * .55 + pow(max(dot(reflect(-L, N), vd), 0.), 40.) * .25;
47
+ vec2 g = abs(fract(uv * vec2(32., 16.)) - .5) / fwidth(uv * vec2(32., 16.));
48
+ float line = 1. - min(min(g.x, g.y), 1.);
49
+ col += mix(base, vec3(1), .5) * line * .32 * G;
50
+ o = vec4(col, 1);
51
+ }`;
52
+ const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x); if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(x)); return x; };
53
+ const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, fs));
54
+ gl.bindAttribLocation(prog, 0, 'P'); gl.bindAttribLocation(prog, 1, 'N'); gl.bindAttribLocation(prog, 2, 'UV'); gl.linkProgram(prog); gl.useProgram(prog);
55
+ const buf = (data, loc, size) => { gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0); };
56
+ buf(pos, 0, 3); buf(nor, 1, 3); buf(tex, 2, 2);
57
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(idx), gl.STATIC_DRAW);
58
+ gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE); // additive: no sorting needed, every layer shows
59
+ const U = {}; ['M', 'V', 'E', 'G', 'T'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
60
+ const resize = () => { const d = Math.min(devicePixelRatio, 2); canvas.width = (stage.clientWidth || 1) * d | 0; canvas.height = (stage.clientHeight || 1) * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
61
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
62
+ const mul = (a, b) => { const o = new Float32Array(16); for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) { let s = 0; for (let k = 0; k < 4; k++) s += a[k * 4 + j] * b[i * 4 + k]; o[i * 4 + j] = s; } return o; };
63
+ let raf, last = performance.now(), ang = 0;
64
+ function frame(now) {
65
+ const dt = Math.min(0.05, (now - last) / 1000); last = now; ang += dt * 0.4 * PARAMS.spin;
66
+ const t = now / 1000, asp = canvas.width / canvas.height, f = 1 / Math.tan(0.5), near = 0.1, far = 50;
67
+ const P = new Float32Array([f / asp, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) / (near - far), -1, 0, 0, 2 * far * near / (near - far), 0]);
68
+ const dist = 5.2 * Math.max(1, 1.1 / asp), E = [0, 0.4, dist];
69
+ const Vw = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -E[0], -E[1], -E[2], 1]);
70
+ const ca = Math.cos(ang), sa = Math.sin(ang), tb = 0.35 + Math.sin(t * 0.2) * 0.2, cb = Math.cos(tb), sb = Math.sin(tb);
71
+ const Ry = new Float32Array([ca, 0, -sa, 0, 0, 1, 0, 0, sa, 0, ca, 0, 0, 0, 0, 1]), Rx = new Float32Array([1, 0, 0, 0, 0, cb, sb, 0, 0, -sb, cb, 0, 0, 0, 0, 1]);
72
+ gl.clearColor(0.015, 0.018, 0.035, 1); gl.clear(gl.COLOR_BUFFER_BIT);
73
+ gl.uniformMatrix4fv(U.M, false, mul(Rx, Ry)); gl.uniformMatrix4fv(U.V, false, mul(P, Vw));
74
+ gl.uniform3fv(U.E, E); gl.uniform1f(U.G, PARAMS.grid); gl.uniform1f(U.T, t);
75
+ gl.drawElements(gl.TRIANGLES, idx.length, gl.UNSIGNED_INT, 0);
76
+ raf = requestAnimationFrame(frame);
77
+ }
78
+ raf = requestAnimationFrame(frame);
79
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
80
+ }
@@ -0,0 +1,94 @@
1
+ // Liquid metal: a pool of molten chrome. A blob pulls itself up out of the surface (a smooth union, so a neck
2
+ // forms and thins), hangs there wobbling, then drops back and sends rings across the pool. Every pixel is
3
+ // raymarched and reflected twice, so the blob mirrors the pool and the pool mirrors the blob.
4
+ const PARAMS = { cycle: 5.5, ripple: 1, smooth: 0.45 };
5
+
6
+ export default function (stage) {
7
+ const canvas = document.createElement('canvas');
8
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
9
+ stage.append(canvas);
10
+ const gl = canvas.getContext('webgl2', { antialias: false });
11
+ if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
12
+ const vs = `#version 300 es
13
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
14
+ const fs = `#version 300 es
15
+ precision highp float; in vec2 uv; out vec4 o;
16
+ uniform vec2 R; uniform float T, BY, BR, TI, TR, RA, K;
17
+ float smin(float a, float b, float k) { float h = clamp(.5 + .5 * (b - a) / k, 0., 1.); return mix(b, a, h) - k * h * (1. - h); }
18
+ float rings(vec2 q, float t0, float amp) { // a ring wave from the centre, starting at t0
19
+ float age = T - t0, r = length(q), front = age * 1.7;
20
+ if (age < 0.) return 0.;
21
+ return amp * exp(-age * 1.1) * sin(r * 11. - age * 13.) * smoothstep(front, front - .9, r) * exp(-r * .5);
22
+ }
23
+ float surf(vec3 p) {
24
+ float h = .035 * sin(p.x * 1.9 + T * .7) * sin(p.z * 1.6 - T * .55) + .02 * sin(p.x * 4.3 - p.z * 3.1 + T * 1.3);
25
+ h += rings(p.xz, TI, .1 * RA) + rings(p.xz, TR, .05 * RA);
26
+ return (p.y - h) * .6;
27
+ }
28
+ float blob(vec3 p) {
29
+ vec3 q = p - vec3(0, BY, 0);
30
+ return length(q * vec3(1, 1. + .08 * sin(T * 5.), 1)) - BR - .045 * sin(q.x * 7. + T * 3.) * sin(q.y * 6. + T * 2.3) * sin(q.z * 7. - T * 2.);
31
+ }
32
+ float map(vec3 p) { return smin(surf(p), blob(p) * .75, K); } // the wobble bends the field, so step a little shorter
33
+ vec3 nrm(vec3 p) { vec2 e = vec2(.002, -.002); return normalize(e.xyy * map(p + e.xyy) + e.yyx * map(p + e.yyx) + e.yxy * map(p + e.yxy) + e.xxx * map(p + e.xxx)); }
34
+ float march(vec3 ro, vec3 rd, float far) { float t = 0.; for (int i = 0; i < 110; i++) { float d = map(ro + rd * t); if (d < .0012 * (1. + t)) return t; t += d; if (t > far) break; } return -1.; }
35
+ vec3 env(vec3 d) {
36
+ vec3 c = mix(vec3(.02, .022, .03), vec3(.13, .14, .17), smoothstep(-.1, .7, d.y));
37
+ vec2 q = d.xz / max(d.y, .05);
38
+ c += vec3(1.) * 2.6 * smoothstep(.06, .0, abs(q.x - .1) - .55) * smoothstep(.06, .0, abs(q.y + .2) - .16) * step(0., d.y); // overhead softbox
39
+ float az = atan(d.x, d.z);
40
+ c += vec3(1., .55, .3) * 1.6 * smoothstep(.1, .02, abs(az - 1.9)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y); // warm strip
41
+ c += vec3(.35, .6, 1.) * 1.4 * smoothstep(.1, .02, abs(az + 1.6)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y); // cool strip
42
+ c += vec3(.9, .9, 1.) * .5 * exp(-pow(d.y * 18., 2.)); // horizon line
43
+ c += vec3(1., .97, .92) * 1.3 * smoothstep(.07, .0, abs(d.y - .34) - .05); // a ring light all round the studio
44
+ return c;
45
+ }
46
+ vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
47
+ void main() {
48
+ vec2 q = (uv - .5) * vec2(R.x / R.y, 1.);
49
+ float a = T * .12;
50
+ vec3 ro = vec3(sin(a) * 4.3, 2.7, -cos(a) * 4.3), ta = vec3(0, .25, 0);
51
+ vec3 f = normalize(ta - ro), r = normalize(cross(vec3(0, 1, 0), f)), u = cross(f, r);
52
+ vec3 rd = normalize(f * 1.6 + r * q.x + u * q.y);
53
+ vec3 col = env(rd) * .35;
54
+ float t = march(ro, rd, 30.);
55
+ if (t > 0.) {
56
+ vec3 p = ro + rd * t, n = nrm(p), rf = reflect(rd, n);
57
+ float fr = .75 + .25 * pow(1. - max(0., dot(n, -rd)), 5.);
58
+ vec3 refl = env(rf);
59
+ float t2 = march(p + n * .02, rf, 12.); // second bounce: the metal mirrors itself
60
+ if (t2 > 0.) { vec3 p2 = p + n * .02 + rf * t2, r2 = reflect(rf, nrm(p2)); refl = env(vec3(r2.x, abs(r2.y), r2.z)) * (r2.y < 0. ? .6 : .85); } // rays heading down see the pool's mirror of the room
61
+ else if (rf.y < 0.) refl = env(vec3(rf.x, -rf.y, rf.z)) * .6;
62
+ col = refl * fr * vec3(.92, .95, 1.);
63
+ col = mix(col, env(rd) * .35, smoothstep(9., 22., t)); // the pool fades into the dark studio
64
+ }
65
+ col = aces(col);
66
+ col *= 1. - .3 * pow(length(uv - .5) * 1.3, 2.);
67
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
68
+ }`;
69
+ const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x); if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(x)); return x; };
70
+ const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, fs)); gl.bindAttribLocation(prog, 0, 'p'); gl.linkProgram(prog); gl.useProgram(prog);
71
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
72
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
73
+ const U = {}; ['R', 'T', 'BY', 'BR', 'TI', 'TR', 'RA', 'K'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
74
+ const resize = () => { const d = Math.min(devicePixelRatio, 1); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
75
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
76
+ const ease = x => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
77
+ let raf; const t0 = performance.now();
78
+ function frame(now) {
79
+ const t = (now - t0) / 1000, C = PARAMS.cycle, k = Math.floor(t / C), c = (t % C) / C;
80
+ // the cycle: rise (0–30%), hang and wobble (30–62%), fall (62–72%), rings spread (72–100%)
81
+ let by;
82
+ if (c < 0.3) by = -0.55 + 1.45 * ease(c / 0.3);
83
+ else if (c < 0.62) by = 0.9 + 0.06 * Math.sin((c - 0.3) * C * 4);
84
+ else if (c < 0.72) { const f = (c - 0.62) / 0.1; by = 0.9 - 1.6 * f * f; }
85
+ else by = -0.7;
86
+ const impact = k * C + C * (0.62 + 0.1 * Math.sqrt(0.9 / 1.6)); // when the falling blob meets the surface
87
+ gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, t);
88
+ gl.uniform1f(U.BY, by); gl.uniform1f(U.BR, 0.48); gl.uniform1f(U.TI, t >= impact ? impact : impact - C);
89
+ gl.uniform1f(U.TR, k * C + 0.15 * C); gl.uniform1f(U.RA, PARAMS.ripple); gl.uniform1f(U.K, PARAMS.smooth);
90
+ gl.drawArrays(gl.TRIANGLES, 0, 3); raf = requestAnimationFrame(frame);
91
+ }
92
+ raf = requestAnimationFrame(frame);
93
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
94
+ }