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,163 @@
1
+ // Low-poly planet: an icosahedron split three times into 1,280 faces, each vertex pushed out by 3D noise.
2
+ // Faces are flat-shaded and coloured by height (sea, sand, grass, forest, rock, snow), pine trees sprout on
3
+ // the green faces, and puffy clouds orbit faster than the ground. Drag to spin it.
4
+ const PARAMS = { terrain: 1, sea: 0.5, spin: 1 };
5
+
6
+ export default function (stage) {
7
+ const canvas = document.createElement('canvas');
8
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:grab';
9
+ stage.append(canvas);
10
+ const gl = canvas.getContext('webgl2', { antialias: true });
11
+ if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
12
+
13
+ // ── geometry
14
+ const perm = Uint8Array.from({ length: 256 }, () => Math.random() * 256);
15
+ const hsh = (x, y, z) => perm[(perm[(perm[x & 255] + y) & 255] + z) & 255] / 255;
16
+ const sm = t => t * t * (3 - 2 * t), lerp = (a, b, t) => a + (b - a) * t;
17
+ const noise = (x, y, z) => {
18
+ const X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z), u = sm(x - X), v = sm(y - Y), w = sm(z - Z);
19
+ return lerp(lerp(lerp(hsh(X, Y, Z), hsh(X + 1, Y, Z), u), lerp(hsh(X, Y + 1, Z), hsh(X + 1, Y + 1, Z), u), v),
20
+ lerp(lerp(hsh(X, Y, Z + 1), hsh(X + 1, Y, Z + 1), u), lerp(hsh(X, Y + 1, Z + 1), hsh(X + 1, Y + 1, Z + 1), u), v), w);
21
+ };
22
+ const fbm = (x, y, z) => { let s = 0, a = 0.5, f = 1; for (let i = 0; i < 4; i++) { s += a * noise(x * f + 11, y * f + 7, z * f + 3); a *= 0.5; f *= 2.1; } return s; };
23
+ function icosphere(level) {
24
+ const t = (1 + Math.sqrt(5)) / 2;
25
+ let V = [[-1, t, 0], [1, t, 0], [-1, -t, 0], [1, -t, 0], [0, -1, t], [0, 1, t], [0, -1, -t], [0, 1, -t], [t, 0, -1], [t, 0, 1], [-t, 0, -1], [-t, 0, 1]].map(v => { const l = Math.hypot(...v); return v.map(c => c / l); });
26
+ let F = [[0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1]];
27
+ for (let l = 0; l < level; l++) {
28
+ const cache = new Map(), mid = (a, b) => { const k = a < b ? a + ',' + b : b + ',' + a; if (cache.has(k)) return cache.get(k); const m = V[a].map((c, i) => (c + V[b][i]) / 2), len = Math.hypot(...m); V.push(m.map(c => c / len)); cache.set(k, V.length - 1); return V.length - 1; };
29
+ F = F.flatMap(([a, b, c]) => { const ab = mid(a, b), bc = mid(b, c), ca = mid(c, a); return [[a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]]; });
30
+ }
31
+ return { V, F };
32
+ }
33
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], cross = (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]];
34
+ const norm = a => { const l = Math.hypot(...a) || 1; return a.map(c => c / l); };
35
+ const hex = h => [parseInt(h.slice(1, 3), 16) / 255, parseInt(h.slice(3, 5), 16) / 255, parseInt(h.slice(5, 7), 16) / 255];
36
+ function pushTri(out, a, b, c, col, shine, ctr = [0, 0, 0]) {
37
+ let n = norm(cross(sub(b, a), sub(c, a)));
38
+ const m = [0, 1, 2].map(i => (a[i] + b[i] + c[i]) / 3 - ctr[i]);
39
+ if (n[0] * m[0] + n[1] * m[1] + n[2] * m[2] < 0) n = n.map(x => -x); // face outward from the shape's centre, whatever the winding
40
+ for (const v of [a, b, c]) out.push(v[0], v[1], v[2], n[0], n[1], n[2], col[0], col[1], col[2], shine);
41
+ }
42
+ function buildPlanet() {
43
+ const { V, F } = icosphere(3), sea = 0.38 + PARAMS.sea * 0.14, amp = 0.32 * PARAMS.terrain;
44
+ const H = V.map(v => fbm(v[0] * 1.7, v[1] * 1.7, v[2] * 1.7));
45
+ const P = V.map((v, i) => { const r = 1 + Math.max(0, H[i] - sea) * amp; return v.map(c => c * r); });
46
+ const out = [], greens = [];
47
+ F.forEach(([a, b, c]) => {
48
+ const h = (H[a] + H[b] + H[c]) / 3 - sea, j = (Math.random() - 0.5) * 0.06;
49
+ let col, shine = 0;
50
+ if (h < 0) { col = h < -0.06 ? hex('#1c5a86') : hex('#2b86b0'); shine = 1; }
51
+ else if (h < 0.025) col = hex('#e6cf8c');
52
+ else if (h < 0.11) { col = hex('#6cb04f'); greens.push([a, b, c]); }
53
+ else if (h < 0.17) { col = hex('#3f8a3c'); greens.push([a, b, c]); }
54
+ else if (h < 0.24) col = hex('#8b7e70');
55
+ else col = hex('#f1f3f6');
56
+ pushTri(out, P[a], P[b], P[c], col.map(x => Math.min(1, Math.max(0, x + j))), shine);
57
+ });
58
+ // pine trees: a five-sided cone standing on the face's centre, pointing away from the core
59
+ greens.sort(() => Math.random() - 0.5).slice(0, 140).forEach(([a, b, c]) => {
60
+ const cen = [0, 1, 2].map(i => (P[a][i] + P[b][i] + P[c][i]) / 3), up = norm(cen), side = norm(cross(up, [0.3, 1, 0.1])), side2 = cross(up, side);
61
+ const hgt = 0.05 + Math.random() * 0.04, rad = 0.018 + Math.random() * 0.01, tip = cen.map((x, i) => x + up[i] * hgt);
62
+ const col = hex(Math.random() < 0.5 ? '#2f6e33' : '#3b7f37');
63
+ for (let k = 0; k < 5; k++) {
64
+ const a0 = k / 5 * 6.283, a1 = (k + 1) / 5 * 6.283;
65
+ const p0 = cen.map((x, i) => x + (side[i] * Math.cos(a0) + side2[i] * Math.sin(a0)) * rad - up[i] * 0.004);
66
+ const p1 = cen.map((x, i) => x + (side[i] * Math.cos(a1) + side2[i] * Math.sin(a1)) * rad - up[i] * 0.004);
67
+ pushTri(out, p0, p1, tip, col, 0, cen);
68
+ }
69
+ });
70
+ return new Float32Array(out);
71
+ }
72
+ function buildClouds() {
73
+ const { V, F } = icosphere(0), out = [];
74
+ for (let k = 0; k < 9; k++) {
75
+ const th = Math.random() * 6.283, ph = Math.acos(Math.random() * 1.6 - 0.8), c = [Math.sin(ph) * Math.cos(th), Math.cos(ph), Math.sin(ph) * Math.sin(th)].map(x => x * 1.34);
76
+ const side = norm(cross(c, [0, 1, 0]));
77
+ for (let m = 0; m < 3 + (k % 3); m++) {
78
+ const off = (m - 1.5) * 0.07, s = 0.05 + Math.random() * 0.045, ctr = c.map((x, i) => x + side[i] * off + (Math.random() - 0.5) * 0.03);
79
+ F.forEach(([a, b, d]) => pushTri(out, ...[a, b, d].map(i => V[i].map((x, j) => ctr[j] + x * s * (j === 1 ? 0.75 : 1))), [0.97, 0.98, 1], 0, ctr));
80
+ }
81
+ }
82
+ return new Float32Array(out);
83
+ }
84
+
85
+ // ── GL
86
+ 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; };
87
+ 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, 'p'); gl.linkProgram(p); return p; };
88
+ const meshProg = link(`#version 300 es
89
+ in vec3 pos, nor, col; in float shine; uniform mat4 PV, M; uniform float T;
90
+ out vec3 vN, vC, vW; out float vS;
91
+ void main() { vec4 w = M * vec4(pos, 1); vW = w.xyz; vN = mat3(M) * nor; vC = col; vS = shine;
92
+ if (shine > .5) vN = normalize(vN + .08 * vec3(sin(T * 2. + pos.x * 30.), cos(T * 1.7 + pos.y * 30.), sin(T * 1.9 + pos.z * 30.))); // the sea glints
93
+ gl_Position = PV * w; }`, `#version 300 es
94
+ precision highp float; in vec3 vN, vC, vW; in float vS; out vec4 o;
95
+ void main() {
96
+ vec3 n = normalize(vN), L = normalize(vec3(-.6, .45, .65)), V = normalize(vec3(0, 0, 3.6) - vW);
97
+ float d = max(0., dot(n, L)), rim = pow(1. - max(0., dot(n, V)), 3.);
98
+ vec3 c = vC * (.16 + .95 * d) + vec3(.35, .55, 1.) * rim * .35;
99
+ c += vS * pow(max(0., dot(reflect(-L, n), V)), 40.) * .8;
100
+ o = vec4(pow(c, vec3(.92)), 1);
101
+ }`);
102
+ const bgProg = link(`#version 300 es
103
+ in vec2 p; out vec2 uv; void main() { uv = p; gl_Position = vec4(p, .999, 1); }`, `#version 300 es
104
+ precision highp float; in vec2 uv; out vec4 o; uniform vec2 R; uniform float RS;
105
+ float h(vec2 p) { return fract(sin(dot(p, vec2(12.99, 78.23))) * 43758.55); }
106
+ void main() {
107
+ vec2 q = uv * vec2(R.x / R.y, 1.);
108
+ vec3 c = mix(vec3(.02, .025, .06), vec3(.05, .04, .1), uv.y * .5 + .5);
109
+ vec2 g = floor(gl_FragCoord.xy / 3.); float s = h(g); c += step(.996, s) * (.4 + .6 * h(g + 3.)) * vec3(.9, .95, 1.);
110
+ float r = length(q);
111
+ c += vec3(.25, .5, 1.) * exp(-max(0., r - RS) * 9.) * .55 * smoothstep(RS * .9, RS, r); // atmosphere halo
112
+ o = vec4(c, 1);
113
+ }`);
114
+ const quad = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, quad); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
115
+ const bgVao = gl.createVertexArray(); gl.bindVertexArray(bgVao); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
116
+ function meshVao(data) {
117
+ const vao = gl.createVertexArray(); gl.bindVertexArray(vao);
118
+ const b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
119
+ [['pos', 3, 0], ['nor', 3, 12], ['col', 3, 24], ['shine', 1, 36]].forEach(([n, s, o]) => { const l = gl.getAttribLocation(meshProg, n); gl.enableVertexAttribArray(l); gl.vertexAttribPointer(l, s, gl.FLOAT, false, 40, o); });
120
+ return { vao, count: data.length / 10, buf: b };
121
+ }
122
+ const planet = meshVao(buildPlanet()), clouds = meshVao(buildClouds());
123
+ const uPV = gl.getUniformLocation(meshProg, 'PV'), uM = gl.getUniformLocation(meshProg, 'M'), uT = gl.getUniformLocation(meshProg, 'T');
124
+ const uBR = gl.getUniformLocation(bgProg, 'R'), uRS = gl.getUniformLocation(bgProg, 'RS');
125
+
126
+ // ── tiny matrix kit (column-major)
127
+ const mul = (a, b) => { const o = new Float32Array(16); 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 += a[k * 4 + r] * b[c * 4 + k]; o[c * 4 + r] = s; } return o; };
128
+ const persp = (fov, asp, n, f) => { const t = 1 / Math.tan(fov / 2); return new Float32Array([t / asp, 0, 0, 0, 0, t, 0, 0, 0, 0, (f + n) / (n - f), -1, 0, 0, 2 * f * n / (n - f), 0]); };
129
+ const rotY = a => new Float32Array([Math.cos(a), 0, -Math.sin(a), 0, 0, 1, 0, 0, Math.sin(a), 0, Math.cos(a), 0, 0, 0, 0, 1]);
130
+ const rotX = a => new Float32Array([1, 0, 0, 0, 0, Math.cos(a), Math.sin(a), 0, 0, -Math.sin(a), Math.cos(a), 0, 0, 0, 0, 1]);
131
+ const trans = z => new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, z, 1]);
132
+
133
+ const resize = () => { const d = Math.min(devicePixelRatio, 2); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; };
134
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
135
+ let ang = 0.8, vel = 0, dragX = null;
136
+ const onDown = e => { dragX = e.clientX; canvas.style.cursor = 'grabbing'; canvas.setPointerCapture?.(e.pointerId); };
137
+ const onMove = e => { if (dragX == null) return; vel = (e.clientX - dragX) * 0.012; ang += vel; dragX = e.clientX; };
138
+ const onUp = () => { dragX = null; canvas.style.cursor = 'grab'; };
139
+ canvas.addEventListener('pointerdown', onDown); canvas.addEventListener('pointermove', onMove); canvas.addEventListener('pointerup', onUp); canvas.addEventListener('pointercancel', onUp);
140
+ let raf, last = performance.now(); const t0 = last;
141
+ function frame(now) {
142
+ const dt = Math.min(0.05, (now - last) / 1000); last = now; const t = (now - t0) / 1000;
143
+ if (dragX == null) { ang += vel + dt * 0.25 * PARAMS.spin; vel *= 0.94; }
144
+ const asp = canvas.width / canvas.height, fov = 0.75, dist = 3.6;
145
+ gl.viewport(0, 0, canvas.width, canvas.height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
146
+ gl.disable(gl.DEPTH_TEST); gl.useProgram(bgProg); gl.bindVertexArray(bgVao);
147
+ gl.uniform2f(uBR, canvas.width, canvas.height); gl.uniform1f(uRS, 1.12 / Math.tan(fov / 2) / Math.sqrt(dist * dist - 1));
148
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
149
+ gl.enable(gl.DEPTH_TEST); gl.useProgram(meshProg); gl.uniform1f(uT, t);
150
+ gl.uniformMatrix4fv(uPV, false, mul(persp(fov, asp, 0.1, 20), trans(-dist)));
151
+ gl.uniformMatrix4fv(uM, false, mul(rotX(0.35), rotY(ang)));
152
+ gl.bindVertexArray(planet.vao); gl.drawArrays(gl.TRIANGLES, 0, planet.count);
153
+ gl.uniformMatrix4fv(uM, false, mul(rotX(0.35), rotY(ang * 1.35 + t * 0.06)));
154
+ gl.bindVertexArray(clouds.vao); gl.drawArrays(gl.TRIANGLES, 0, clouds.count);
155
+ raf = requestAnimationFrame(frame);
156
+ }
157
+ raf = requestAnimationFrame(frame);
158
+ return () => {
159
+ cancelAnimationFrame(raf); ro.disconnect();
160
+ canvas.removeEventListener('pointerdown', onDown); canvas.removeEventListener('pointermove', onMove); canvas.removeEventListener('pointerup', onUp); canvas.removeEventListener('pointercancel', onUp);
161
+ gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove();
162
+ };
163
+ }
@@ -0,0 +1,79 @@
1
+ // Mandelbulb: the 3D cousin of the Mandelbrot set. Each point is raised to a power in spherical coordinates
2
+ // (radius to the n, both angles times n) and added back, over and over; the distance estimate from that orbit
3
+ // lets a ray step safely toward the surface. The power drifts slowly, so the bulb breathes and rearranges.
4
+ const PARAMS = { power: 8, drift: 1, detail: 1 };
5
+
6
+ export default function (stage) {
7
+ const canvas = document.createElement('canvas');
8
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
9
+ stage.append(canvas);
10
+ const 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; uniform vec2 R; uniform float T, PW, EPS;
16
+ vec4 trap;
17
+ float de(vec3 p) {
18
+ vec3 z = p; float dr = 1., r = 0.; trap = vec4(abs(z), dot(z, z));
19
+ for (int i = 0; i < 9; i++) {
20
+ r = length(z); if (r > 2.) break;
21
+ float th = acos(clamp(z.y / r, -1., 1.)) * PW, ph = atan(z.z, z.x) * PW;
22
+ dr = pow(r, PW - 1.) * PW * dr + 1.;
23
+ z = pow(r, PW) * vec3(sin(th) * cos(ph), cos(th), sin(th) * sin(ph)) + p;
24
+ trap = min(trap, vec4(abs(z), dot(z, z))); // orbit trap: how close the orbit came to the axes and origin
25
+ }
26
+ return .5 * log(r) * r / dr;
27
+ }
28
+ vec3 nrm(vec3 p, float e) { vec2 k = vec2(e, -e); return normalize(k.xyy * de(p + k.xyy) + k.yyx * de(p + k.yyx) + k.yxy * de(p + k.yxy) + k.xxx * de(p + k.xxx)); }
29
+ vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
30
+ void main() {
31
+ vec2 q = (uv - .5) * vec2(R.x / R.y, 1.);
32
+ float a = T * .15;
33
+ vec3 ro = vec3(sin(a) * 4.4, 1.1 * sin(T * .07) + .6, cos(a) * 4.4), f = normalize(-ro), r = normalize(cross(vec3(0, 1, 0), f)), u = cross(f, r);
34
+ vec3 rd = normalize(f * 1.55 + r * q.x + u * q.y);
35
+ vec3 bg = mix(vec3(.02, .02, .035), vec3(.09, .06, .1), uv.y) + vec3(.12, .07, .04) * exp(-length(q) * 2.2);
36
+ vec3 col = bg;
37
+ // only march inside the bounding sphere
38
+ float b = dot(ro, rd), c = dot(ro, ro) - 1.44, h = b * b - c;
39
+ float glow = 0.;
40
+ if (h > 0.) {
41
+ float t = max(0., -b - sqrt(h)), tEnd = -b + sqrt(h), steps = 0.; bool hit = false;
42
+ for (int i = 0; i < 140; i++) {
43
+ float d = de(ro + rd * t);
44
+ glow += exp(-d * 30.) * .012;
45
+ if (d < EPS * t) { hit = true; break; }
46
+ t += d; steps += 1.; if (t > tEnd) break;
47
+ }
48
+ if (hit) {
49
+ vec3 p = ro + rd * t, n = nrm(p, EPS * t * .5);
50
+ float ao = clamp(1. - steps / 90., 0., 1.); // more steps to arrive = deeper in a crevice
51
+ vec3 base = mix(vec3(.95, .55, .22), vec3(.2, .55, .65), clamp(trap.y * 1.3, 0., 1.));
52
+ base = mix(base, vec3(.95, .9, .8), clamp(1. - trap.w * 1.5, 0., 1.) * .6);
53
+ vec3 L = normalize(vec3(.6, .8, -.4));
54
+ float dif = max(0., dot(n, L)), sp = pow(max(0., dot(reflect(rd, n), L)), 24.), rim = pow(1. - max(0., dot(n, -rd)), 3.);
55
+ col = base * (.12 + dif * 1.1) * (.35 + .65 * ao) + sp * .5 * ao + rim * vec3(.5, .35, .7) * .4 * ao;
56
+ }
57
+ }
58
+ col += vec3(.9, .5, .3) * glow * .35; // halo from near misses
59
+ col = aces(col * 1.1);
60
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
61
+ }`;
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 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);
64
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
65
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
66
+ const uR = gl.getUniformLocation(prog, 'R'), uT = gl.getUniformLocation(prog, 'T'), uP = gl.getUniformLocation(prog, 'PW'), uE = gl.getUniformLocation(prog, 'EPS');
67
+ 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); };
68
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
69
+ let raf; const t0 = performance.now();
70
+ function frame(now) {
71
+ const t = (now - t0) / 1000;
72
+ gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform1f(uT, t);
73
+ gl.uniform1f(uP, PARAMS.power + Math.sin(t * 0.25) * 1.5 * PARAMS.drift);
74
+ gl.uniform1f(uE, 0.0016 / PARAMS.detail);
75
+ gl.drawArrays(gl.TRIANGLES, 0, 3); raf = requestAnimationFrame(frame);
76
+ }
77
+ raf = requestAnimationFrame(frame);
78
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
79
+ }
@@ -0,0 +1,126 @@
1
+ // Melting chrome logo: solid chrome letters slump into a puddle, then pull themselves back up, like liquid metal
2
+ // with a memory. The letters are drawn once to a canvas and turned into a 2D distance field (an exact distance
3
+ // transform); in 3D that field is extruded with rounded edges. Melting bends space rather than the letters: each
4
+ // point reads the field from higher up, more so near the floor and down the drip lines, so the shapes sag and run.
5
+ // A puddle grows under them and merges with a smooth minimum. Reflections come from a studio of softboxes.
6
+ const PARAMS = { drips: 1, speed: 1, gloss: 1 };
7
+ const WORD = 'MELT';
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) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
15
+ const TW = 512, TH = 192;
16
+ // the word's signed distance in pixels (positive outside), by an exact Euclidean distance transform
17
+ const field = (() => {
18
+ const c = document.createElement('canvas'); c.width = TW; c.height = TH; const g = c.getContext('2d');
19
+ g.fillStyle = '#fff'; g.font = `900 ${Math.round(TH * 0.8)}px "Arial Black", system-ui, sans-serif`; g.textAlign = 'center'; g.textBaseline = 'middle';
20
+ g.fillText(WORD, TW / 2, TH * 0.54, TW * 0.9);
21
+ const px = g.getImageData(0, 0, TW, TH).data, INF = 1e10;
22
+ const edt = inside => {
23
+ const f = new Float64Array(TW * TH); for (let i = 0; i < TW * TH; i++) f[i] = (px[i * 4 + 3] > 127) === inside ? 0 : INF;
24
+ const n = Math.max(TW, TH), v = new Int32Array(n), z = new Float64Array(n + 1), tmp = new Float64Array(n);
25
+ const pass = (get, set, len) => {
26
+ for (let q = 0; q < len; q++) tmp[q] = get(q);
27
+ let k = 0; v[0] = 0; z[0] = -INF; z[1] = INF;
28
+ for (let q = 1; q < len; q++) { let s; while ((s = ((tmp[q] + q * q) - (tmp[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k])) <= z[k]) k--; k++; v[k] = q; z[k] = s; z[k + 1] = INF; }
29
+ k = 0; for (let q = 0; q < len; q++) { while (z[k + 1] < q) k++; set(q, (q - v[k]) ** 2 + tmp[v[k]]); }
30
+ };
31
+ for (let x = 0; x < TW; x++) pass(y => f[y * TW + x], (y, val) => { f[y * TW + x] = val; }, TH);
32
+ for (let y = 0; y < TH; y++) pass(x => f[y * TW + x], (x, val) => { f[y * TW + x] = val; }, TW);
33
+ return f;
34
+ };
35
+ const depth = edt(false), away = edt(true), d = new Float32Array(TW * TH);
36
+ for (let i = 0; i < TW * TH; i++) { const row = (i / TW) | 0, col = i % TW; d[(TH - 1 - row) * TW + col] = Math.sqrt(away[i]) - Math.sqrt(depth[i]); }
37
+ return d;
38
+ })();
39
+ const FS = `#version 300 es
40
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D F; uniform vec2 R; uniform float T, M, DR, GL;
41
+ const float SX = 4.6, SY = 4.6 * ${TH}. / ${TW}., PX = 4.6 / ${TW}., FLOOR = -.95;
42
+ mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
43
+ float h1(float x) { return fract(sin(x * 127.1) * 43758.5453); }
44
+ float n1(float x) { float i = floor(x), f = fract(x); f = f * f * (3. - 2. * f); return mix(h1(i), h1(i + 1.), f); }
45
+ float sd2(vec2 p) { vec2 b = abs(p) - vec2(SX, SY) * .5; if (max(b.x, b.y) > 0.) return length(max(b, 0.)) + .5; return texture(F, vec2(p.x / SX + .5, p.y / SY + .5)).r * PX; }
46
+ 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); }
47
+ float smax(float a, float b, float k) { return -smin(-a, -b, k); }
48
+ float map(vec3 p) {
49
+ p.xz *= rot(.35 * sin(T * .3));
50
+ // the letters, extruded with rounded edges
51
+ float th = .22, r = .07, d2 = sd2(vec2(p.x, p.y + .03)), dz = abs(p.z) - th;
52
+ float letters = length(max(vec2(d2 + r, dz + r), 0.)) - r + min(max(d2, dz), 0.);
53
+ // the melt front sinks through them (wavy, with a molten rounded rim), and what is above it is gone
54
+ float front = mix(1.1, FLOOR - .15, M) + .07 * sin(p.x * 3.3 + T * 1.3) * M;
55
+ float body = smax(letters, p.y - front, .14);
56
+ // drips hang from the front where there is metal, and stretch as it goes
57
+ float drips = 1e3;
58
+ for (int i = 0; i < 7; i++) {
59
+ float x = -1.9 + float(i) * .63 + .2 * sin(float(i) * 7.1), len = M * (1. - M) * 4. * (.35 + .5 * h1(float(i) * 3.3)) * DR;
60
+ float here = step(sd2(vec2(x, front + .05)), .02);
61
+ vec3 a = vec3(x, front, 0.), b = vec3(x, front - len, 0.), pa = p - a, ba = b - a;
62
+ float k = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-4), 0., 1.);
63
+ drips = min(drips, length(pa - ba * k) - (.07 - .03 * k) * here + (1. - here) * 9.);
64
+ }
65
+ float puddle = length(vec3(p.x * .38, (p.y - FLOOR) * 5., p.z * .85)) / 5. - .02 - .14 * M;
66
+ puddle = max(puddle, FLOOR - p.y);
67
+ return min(smin(smin(body, drips, .12), puddle, .2), p.y - FLOOR + 10.) * .7;
68
+ }
69
+ vec3 nrm(vec3 p) { vec2 e = vec2(.002, 0); return normalize(vec3(map(p + e.xyy) - map(p - e.xyy), map(p + e.yxy) - map(p - e.yxy), map(p + e.yyx) - map(p - e.yyx))); }
70
+ vec3 env(vec3 d) {
71
+ vec3 c = mix(vec3(.02, .022, .03), vec3(.13, .14, .17), smoothstep(-.1, .7, d.y));
72
+ vec2 q = d.xz / max(d.y, .05);
73
+ 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);
74
+ float az = atan(d.x, d.z);
75
+ c += vec3(1., .55, .3) * 1.6 * smoothstep(.1, .02, abs(az - 1.9)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y);
76
+ c += vec3(.35, .6, 1.) * 1.4 * smoothstep(.1, .02, abs(az + 1.6)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y);
77
+ c += vec3(1., .97, .92) * 1.3 * smoothstep(.07, .0, abs(d.y - .34) - .05);
78
+ c += vec3(1.25, 1.22, 1.18) * smoothstep(.35, .85, d.z) * smoothstep(-.35, .45, d.y); // a big bright wall behind the camera, for the front faces
79
+ return c;
80
+ }
81
+ vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
82
+ void main() {
83
+ vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
84
+ vec3 ro = vec3(0, .55, 5.4), rd = normalize(vec3(q, -2.3)); rd.yz *= rot(-.12);
85
+ vec3 col = vec3(0); float t = 0.; bool hit = false;
86
+ for (int i = 0; i < 130; i++) { float d = map(ro + rd * t); if (d < .0012) { hit = true; break; } t += d; if (t > 14.) break; }
87
+ float tf = (FLOOR - ro.y) / rd.y;
88
+ if (hit) {
89
+ vec3 p = ro + rd * t, n = nrm(p), r = reflect(rd, n);
90
+ float fr = .6 + .4 * pow(1. - max(dot(n, -rd), 0.), 5.);
91
+ col = env(r) * vec3(.9, .92, .96) * fr * GL + vec3(.02);
92
+ } else if (tf > 0.) {
93
+ vec3 fp = ro + rd * tf, r = reflect(rd, vec3(0, 1, 0)); // a dark glossy floor reflecting the studio
94
+ col = env(r) * .035 + vec3(.012, .013, .016);
95
+ col *= 1. - .6 * exp(-dot(fp.xz * vec2(.35, 1.4), fp.xz * vec2(.35, 1.4))); // contact shadow
96
+ } else col = mix(vec3(.025, .027, .034), vec3(.008, .009, .012), uv.y);
97
+ o = vec4(pow(aces(col), vec3(1. / 2.2)), 1);
98
+ }`;
99
+ const VS = `#version 300 es
100
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
101
+ 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; };
102
+ 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);
103
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
104
+ gl.useProgram(p); const U = n => gl.getUniformLocation(p, n);
105
+ gl.bindVertexArray(gl.createVertexArray()); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
106
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
107
+ const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.R16F, TW, TH, 0, gl.RED, gl.FLOAT, field);
108
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
109
+ 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);
110
+ let W = 1, H = 1;
111
+ const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, 2, Math.max(0.5, Math.sqrt(330000 / (w * h)))); W = canvas.width = Math.max(1, (w * d) | 0); H = canvas.height = Math.max(1, (h * d) | 0); gl.viewport(0, 0, W, H); };
112
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
113
+ let raf, last = performance.now(), t = 0;
114
+ function frame(now) {
115
+ const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt * PARAMS.speed;
116
+ // solid, melting (slow at first, then running), a puddle, then it pulls itself back up with a little overshoot
117
+ const c = t % 11; let m;
118
+ if (c < 2.5) m = 0; else if (c < 6) { const k = (c - 2.5) / 3.5; m = k * k * (3 - 2 * k); } else if (c < 7.6) m = 1;
119
+ else if (c < 9.6) { const k = (c - 7.6) / 2; m = Math.max(-0.08, 1 - (1 - Math.exp(-6 * k) * Math.cos(10 * k)) * 1.0); } else m = 0;
120
+ gl.uniform1i(U('F'), 0); gl.uniform2f(U('R'), W, H); gl.uniform1f(U('T'), t); gl.uniform1f(U('M'), Math.max(0, m)); gl.uniform1f(U('DR'), PARAMS.drips); gl.uniform1f(U('GL'), PARAMS.gloss);
121
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
122
+ raf = requestAnimationFrame(frame);
123
+ }
124
+ raf = requestAnimationFrame(frame);
125
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
126
+ }
@@ -0,0 +1,79 @@
1
+ // Menger sponge: a cube with its centre cross carved out, then the same carved out of each of the 20 smaller cubes
2
+ // left, and so on. As a distance function that is one box plus a loop: fold space into repeating cells (mod), and at
3
+ // each level subtract a cross three times thinner. The raymarcher counts the levels to shade the deep holes darker,
4
+ // the brass catches a studio light and a soft shadow is marched toward the lamp. The camera drifts in toward a face.
5
+ const PARAMS = { levels: 4, speed: 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: false });
12
+ if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
13
+ const vs = `#version 300 es
14
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
15
+ const fs = `#version 300 es
16
+ precision highp float; in vec2 uv; out vec4 o;
17
+ uniform vec2 R; uniform float T; uniform int L; uniform mat3 ROT; uniform vec3 CE, CF, CR, CU;
18
+ float sdBox(vec3 p, vec3 b) { vec3 q = abs(p) - b; return length(max(q, 0.)) + min(max(q.x, max(q.y, q.z)), 0.); }
19
+ // x: distance, y: how deep in the holes (for occlusion)
20
+ vec2 map(vec3 p) {
21
+ p = ROT * p;
22
+ float d = sdBox(p, vec3(1)), s = 1., occ = 1.;
23
+ for (int m = 0; m < 6; m++) {
24
+ if (m >= L) break;
25
+ vec3 a = mod(p * s, 2.) - 1.; s *= 3.;
26
+ vec3 r = abs(1. - 3. * abs(a));
27
+ float da = max(r.x, r.y), db = max(r.y, r.z), dc = max(r.z, r.x);
28
+ float c = (min(da, min(db, dc)) - 1.) / s; // the cross at this level
29
+ if (c > d) { d = c; occ = min(occ, .25 * da * db * dc); }
30
+ }
31
+ return vec2(d, occ);
32
+ }
33
+ vec3 nrm(vec3 p) { vec2 e = vec2(.0007, -.0007); return normalize(e.xyy * map(p + e.xyy).x + e.yyx * map(p + e.yyx).x + e.yxy * map(p + e.yxy).x + e.xxx * map(p + e.xxx).x); }
34
+ float shadow(vec3 ro, vec3 rd) { float r = 1., t = .01; for (int i = 0; i < 48; i++) { float h = map(ro + rd * t).x; r = min(r, 10. * h / t); t += clamp(h, .005, .1); if (r < .001 || t > 4.) break; } return clamp(r, 0., 1.); }
35
+ vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
36
+ void main() {
37
+ vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
38
+ vec3 ro = CE, rd = normalize(CF * 1.8 + CR * q.x + CU * q.y);
39
+ vec3 bg = mix(vec3(.05, .055, .07), vec3(.16, .14, .13), uv.y * .6 + .2);
40
+ vec3 col = bg; float t = 0.; vec2 h; bool hit = false;
41
+ for (int i = 0; i < 160; i++) { h = map(ro + rd * t); if (h.x < .0004 * (1. + t)) { hit = true; break; } t += h.x; if (t > 12.) break; }
42
+ if (hit) {
43
+ vec3 p = ro + rd * t, n = nrm(p), L1 = normalize(vec3(.6, .8, .4));
44
+ float dif = max(dot(n, L1), 0.) * shadow(p + n * .002, L1), sky = .5 + .5 * n.y, back = max(dot(n, -L1 * vec3(1, 0, 1)), 0.);
45
+ float occ = clamp(h.y * 1.4, 0., 1.);
46
+ vec3 brass = vec3(.86, .66, .38);
47
+ vec3 r = reflect(rd, n);
48
+ float spec = pow(max(dot(r, L1), 0.), 40.) * dif;
49
+ col = brass * (dif * vec3(1.25, 1.1, .95) + sky * vec3(.12, .14, .2) * occ + back * vec3(.08, .07, .1)) * (.3 + .7 * occ) + spec * vec3(1.2, 1., .8);
50
+ col += brass * .5 * mix(vec3(.05), vec3(.5, .45, .4), smoothstep(-.2, .6, r.y)) * occ * pow(1. - max(dot(n, -rd), 0.), 2.); // a little sheen at grazing angles
51
+ col = mix(col, bg, smoothstep(4., 11., t));
52
+ }
53
+ col = aces(col);
54
+ col *= 1. - .3 * pow(length(uv - .5) * 1.25, 2.);
55
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
56
+ }`;
57
+ 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; };
58
+ 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);
59
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
60
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
61
+ const U = {}; ['R', 'T', 'L', 'ROT', 'CE', 'CF', 'CR', 'CU'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
62
+ const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, Math.max(1, Math.sqrt(340000 / (w * h)))); canvas.width = w * d | 0; canvas.height = h * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
63
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
64
+ const nrm = a => { const l = Math.hypot(...a); return a.map(v => v / l); };
65
+ let raf; const t0 = performance.now();
66
+ function frame(now) {
67
+ const t = (now - t0) / 1000 * PARAMS.speed;
68
+ const ya = t * 0.18 + 0.6, pa = 0.5 + Math.sin(t * 0.13) * 0.15, cy = Math.cos(ya), sy = Math.sin(ya), cp = Math.cos(pa), sp = Math.sin(pa);
69
+ const rot = new Float32Array([cy, sp * sy, -cp * sy, 0, cp, sp, sy, -sp * cy, cp * cy]);
70
+ const asp = canvas.width / canvas.height, dist = (4.3 - 0.9 * (0.5 + 0.5 * Math.sin(t * 0.21))) * Math.max(1, 1.15 / asp); // drift in and out
71
+ const eye = [0.25, 0.35, dist], f = nrm(eye.map(v => -v)), r = nrm([-f[2], 0, f[0]]), u = [r[1] * f[2] - r[2] * f[1], r[2] * f[0] - r[0] * f[2], r[0] * f[1] - r[1] * f[0]];
72
+ gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, t); gl.uniform1i(U.L, Math.round(PARAMS.levels));
73
+ gl.uniformMatrix3fv(U.ROT, false, rot); gl.uniform3fv(U.CE, eye); gl.uniform3fv(U.CF, f); gl.uniform3fv(U.CR, r); gl.uniform3fv(U.CU, u);
74
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
75
+ raf = requestAnimationFrame(frame);
76
+ }
77
+ raf = requestAnimationFrame(frame);
78
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
79
+ }
@@ -0,0 +1,149 @@
1
+ // Neon geodesic: an icosahedron with every face split into nine (frequency 3), pushed out onto a sphere:
2
+ // 92 nodes, 270 struts. Each strut is stroked as neon tube light; the glow is the same drawing shrunk and
3
+ // blown back up (a cheap bloom). Pulses run along the struts, preferring to go straight, and light the
4
+ // panels they pass. A bright ring sweeps pole to pole. The far side is dimmer, and a glossy floor mirrors it.
5
+ const PARAMS = { pulses: 14, spin: 1, glow: 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
+ const mk = () => { const c = document.createElement('canvas'); return [c, c.getContext('2d')]; };
13
+ const [neon, nx] = mk(), [sm1, s1x] = mk(), [sm2, s2x] = mk(), [refl, rx] = mk();
14
+ let W = 0, H = 0, D = 1;
15
+ const resize = () => {
16
+ D = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1;
17
+ canvas.width = neon.width = refl.width = W * D | 0; canvas.height = neon.height = refl.height = H * D | 0;
18
+ sm1.width = Math.max(1, W * D / 4 | 0); sm1.height = Math.max(1, H * D / 4 | 0); sm2.width = Math.max(1, W * D / 12 | 0); sm2.height = Math.max(1, H * D / 12 | 0);
19
+ };
20
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
21
+
22
+ // ── geometry: frequency-3 icosphere
23
+ const g = (1 + Math.sqrt(5)) / 2;
24
+ const ico = [[-1, g, 0], [1, g, 0], [-1, -g, 0], [1, -g, 0], [0, -1, g], [0, 1, g], [0, -1, -g], [0, 1, -g], [g, 0, -1], [g, 0, 1], [-g, 0, -1], [-g, 0, 1]];
25
+ const icoF = [[0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1]];
26
+ const V = [], vmap = new Map(), E = [], emap = new Map(), F = [], pent = new Set();
27
+ const nrm = a => { const l = Math.hypot(...a); return a.map(v => v / l); };
28
+ const vid = p => { const q = nrm(p), k = q.map(v => Math.round(v * 1e4)).join(','); if (!vmap.has(k)) { vmap.set(k, V.length); V.push(q); } return vmap.get(k); };
29
+ const eid = (a, b) => { const k = a < b ? a + '_' + b : b + '_' + a; if (!emap.has(k)) { emap.set(k, E.length); E.push({ a: Math.min(a, b), b: Math.max(a, b), faces: [] }); } return emap.get(k); };
30
+ const Fq = 3;
31
+ for (const [A, B, C] of icoF) {
32
+ const P = (i, j) => vid([0, 1, 2].map(c => (ico[A][c] * (Fq - i - j) + ico[B][c] * i + ico[C][c] * j) / Fq));
33
+ pent.add(P(0, 0)); pent.add(P(Fq, 0)); pent.add(P(0, Fq));
34
+ for (let j = 0; j < Fq; j++) for (let i = 0; i < Fq - j; i++) {
35
+ const tri = [[P(i, j), P(i + 1, j), P(i, j + 1)]];
36
+ if (i + j < Fq - 1) tri.push([P(i + 1, j), P(i + 1, j + 1), P(i, j + 1)]);
37
+ for (const t of tri) { const f = F.length; F.push({ v: t, e: 0 }); for (let k = 0; k < 3; k++) E[eid(t[k], t[(k + 1) % 3])].faces.push(f); }
38
+ }
39
+ }
40
+ const adj = V.map(() => []); E.forEach((e, i) => { adj[e.a].push([e.b, i]); adj[e.b].push([e.a, i]); });
41
+ const nodeHeat = new Float32Array(V.length);
42
+
43
+ // ── pulses: walk the struts, preferring to carry on straight, and light the panels they pass
44
+ const pulses = [];
45
+ const spawn = () => { const a = Math.random() * V.length | 0, [b] = adj[a][Math.random() * adj[a].length | 0]; return { a, b, p: Math.random(), speed: 1.1 + Math.random() * 0.9, trail: [], hue: Math.random() < 0.35 ? 1 : 0 }; };
46
+ const advance = (P, dt) => {
47
+ const A = V[P.a], B = V[P.b], len = Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]);
48
+ P.p += dt * P.speed * 0.42 / len;
49
+ while (P.p >= 1) {
50
+ P.p -= 1; nodeHeat[P.b] = 1;
51
+ const dir = nrm(V[P.b].map((v, c) => v - V[P.a][c]));
52
+ let best = null, bs = -1e9;
53
+ for (const [n] of adj[P.b]) { if (n === P.a) continue; const d = nrm(V[n].map((v, c) => v - V[P.b][c])), s = d[0] * dir[0] + d[1] * dir[1] + d[2] * dir[2] + Math.random() * 0.9; if (s > bs) { bs = s; best = n; } }
54
+ P.a = P.b; P.b = best;
55
+ for (const f of E[emap.get(P.a < P.b ? P.a + '_' + P.b : P.b + '_' + P.a)].faces) F[f].e = 1;
56
+ }
57
+ const A2 = V[P.a], B2 = V[P.b]; P.pos = nrm(A2.map((v, c) => v + (B2[c] - v) * P.p));
58
+ P.trail.push(P.pos); if (P.trail.length > 16) P.trail.shift();
59
+ };
60
+
61
+ let raf, last = performance.now(), ang = 0; const t0 = last;
62
+ function frame(now) {
63
+ const dt = Math.min(0.05, (now - last) / 1000), t = (now - t0) / 1000; last = now;
64
+ ang += dt * 0.32 * PARAMS.spin;
65
+ const n = Math.max(0, Math.round(PARAMS.pulses)); while (pulses.length < n) pulses.push(spawn()); pulses.length = n;
66
+ for (const P of pulses) advance(P, dt);
67
+ const fade = Math.exp(-dt * 2.2); for (const f of F) f.e *= fade; for (let i = 0; i < nodeHeat.length; i++) nodeHeat[i] *= Math.exp(-dt * 3);
68
+
69
+ const R = Math.min(W * 0.3, H * 0.28), cx = W / 2, cy = H * 0.41, floorY = cy + R * 1.3;
70
+ const tilt = 0.36 + Math.sin(t * 0.21) * 0.08, ca = Math.cos(ang), sa = Math.sin(ang), ct = Math.cos(tilt), st = Math.sin(tilt), cam = 3.6;
71
+ const P3 = new Float32Array(V.length * 4);
72
+ const proj = (x, y, z, o) => { const x1 = x * ca + z * sa, z1 = -x * sa + z * ca, y1 = y * ct - z1 * st, z2 = y * st + z1 * ct, k = cam / (cam - z2); o[0] = cx + x1 * R * k; o[1] = cy - y1 * R * k; o[2] = z2; o[3] = k; return o; };
73
+ const tmp = [0, 0, 0, 0];
74
+ V.forEach((v, i) => { proj(v[0], v[1], v[2], tmp); P3.set(tmp, i * 4); });
75
+ const hueShift = Math.sin(t * 0.17) * 18;
76
+ const grad = (c, a) => { const gr = c.createLinearGradient(0, cy - R * 1.1, 0, cy + R * 1.1); gr.addColorStop(0, `hsla(${188 + hueShift},100%,64%,${a})`); gr.addColorStop(0.5, `hsla(${258 + hueShift},100%,68%,${a})`); gr.addColorStop(1, `hsla(${318 + hueShift},100%,64%,${a})`); return gr; };
77
+
78
+ // ── neon layer (in CSS pixels, scaled by D)
79
+ nx.setTransform(1, 0, 0, 1, 0, 0); nx.clearRect(0, 0, neon.width, neon.height); nx.setTransform(D, 0, 0, D, 0, 0);
80
+ nx.globalCompositeOperation = 'lighter'; nx.lineCap = 'round'; nx.lineJoin = 'round';
81
+ // panels lit by passing pulses (near side only)
82
+ for (const f of F) {
83
+ if (f.e < 0.02) continue;
84
+ const [a, b, c] = f.v, z = (P3[a * 4 + 2] + P3[b * 4 + 2] + P3[c * 4 + 2]) / 3; if (z < -0.15) continue;
85
+ nx.fillStyle = grad(nx, f.e * 0.16 * Math.min(1, z + 0.6)); nx.beginPath(); nx.moveTo(P3[a * 4], P3[a * 4 + 1]); nx.lineTo(P3[b * 4], P3[b * 4 + 1]); nx.lineTo(P3[c * 4], P3[c * 4 + 1]); nx.closePath(); nx.fill();
86
+ }
87
+ // struts in four depth bands: the far side thinner and dimmer
88
+ const sweep = ((t * 0.3) % 1.7) * 2 - 1.5, hot = [];
89
+ for (let band = 0; band < 4; band++) {
90
+ nx.beginPath();
91
+ for (const e of E) {
92
+ const za = P3[e.a * 4 + 2], zb = P3[e.b * 4 + 2], z = (za + zb) / 2, bnd = Math.min(3, Math.max(0, Math.floor((z + 1) * 2)));
93
+ if (bnd !== band) continue;
94
+ nx.moveTo(P3[e.a * 4], P3[e.a * 4 + 1]); nx.lineTo(P3[e.b * 4], P3[e.b * 4 + 1]);
95
+ if (Math.abs((V[e.a][1] + V[e.b][1]) / 2 - sweep) < 0.09) hot.push(e);
96
+ }
97
+ nx.strokeStyle = grad(nx, [0.07, 0.16, 0.45, 0.95][band]); nx.lineWidth = [0.7, 0.9, 1.2, 1.5][band]; nx.stroke();
98
+ }
99
+ // the sweeping ring: those struts burn whiter
100
+ nx.beginPath(); for (const e of hot) { const z = (P3[e.a * 4 + 2] + P3[e.b * 4 + 2]) / 2; if (z < -0.3) continue; nx.moveTo(P3[e.a * 4], P3[e.a * 4 + 1]); nx.lineTo(P3[e.b * 4], P3[e.b * 4 + 1]); }
101
+ nx.strokeStyle = 'rgba(225,240,255,.7)'; nx.lineWidth = 1.5; nx.stroke();
102
+ // nodes: the twelve five-way joints are bigger
103
+ for (let i = 0; i < V.length; i++) {
104
+ const x = P3[i * 4], y = P3[i * 4 + 1], z = P3[i * 4 + 2], k = P3[i * 4 + 3], a = 0.25 + 0.75 * Math.max(0, (z + 1) / 2) ** 1.5, hh = nodeHeat[i];
105
+ const r = (pent.has(i) ? 2 : 1.1) * k + hh * 1.6;
106
+ nx.fillStyle = `rgba(${200 + 55 * hh | 0},${230 + 25 * hh | 0},255,${Math.min(1, a * ((pent.has(i) ? 0.6 : 0.3) + hh))})`; nx.beginPath(); nx.arc(x, y, r, 0, 6.2832); nx.fill();
107
+ }
108
+ // pulses: a fading tail and a white-hot head
109
+ for (const P of pulses) {
110
+ const pts = P.trail.map(p => proj(p[0], p[1], p[2], [0, 0, 0, 0])), col = P.hue ? '255,236,190' : '255,178,90';
111
+ for (let i = 1; i < pts.length; i++) {
112
+ const a = pts[i - 1], b = pts[i], f = i / pts.length, depth = Math.max(0.15, (b[2] + 1) / 2);
113
+ nx.strokeStyle = `rgba(${col},${f * 0.9 * depth})`; nx.lineWidth = (0.8 + f * 1.7) * b[3]; nx.beginPath(); nx.moveTo(a[0], a[1]); nx.lineTo(b[0], b[1]); nx.stroke();
114
+ }
115
+ const h = pts[pts.length - 1]; if (!h) continue; const depth = Math.max(0.2, (h[2] + 1) / 2);
116
+ nx.fillStyle = `rgba(255,255,255,${depth})`; nx.beginPath(); nx.arc(h[0], h[1], 2.2 * h[3], 0, 6.2832); nx.fill();
117
+ nx.fillStyle = `rgba(${col},${0.4 * depth})`; nx.beginPath(); nx.arc(h[0], h[1], 5 * h[3], 0, 6.2832); nx.fill();
118
+ }
119
+
120
+ // ── compose: background, floor glow, mirrored reflection, then bloom + crisp neon
121
+ ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalCompositeOperation = 'source-over';
122
+ const bg = ctx.createLinearGradient(0, 0, 0, canvas.height); bg.addColorStop(0, '#07061a'); const fy = Math.min(0.99, floorY / H); bg.addColorStop(fy, '#0f0b2a'); bg.addColorStop(fy + 0.005, '#06051a'); bg.addColorStop(1, '#030210');
123
+ ctx.fillStyle = bg; ctx.fillRect(0, 0, canvas.width, canvas.height);
124
+ const halo = ctx.createRadialGradient(cx * D, cy * D, R * 0.4 * D, cx * D, cy * D, R * 2.1 * D); halo.addColorStop(0, `hsla(${255 + hueShift},90%,45%,.22)`); halo.addColorStop(1, 'hsla(260,90%,40%,0)');
125
+ ctx.fillStyle = halo; ctx.fillRect(0, 0, canvas.width, canvas.height);
126
+ const pool = ctx.createRadialGradient(cx * D, floorY * D, 0, cx * D, floorY * D, R * 1.6 * D); pool.addColorStop(0, `hsla(${300 + hueShift},100%,60%,.22)`); pool.addColorStop(1, 'hsla(300,100%,60%,0)');
127
+ ctx.save(); ctx.translate(cx * D, floorY * D); ctx.scale(1, 0.16); ctx.translate(-cx * D, -floorY * D); ctx.fillStyle = pool; ctx.fillRect(0, (floorY - R * 1.6) * D, canvas.width, R * 3.2 * D); ctx.restore();
128
+ // reflection: the neon layer flipped about the floor line, faded with distance
129
+ rx.setTransform(1, 0, 0, 1, 0, 0); rx.globalCompositeOperation = 'source-over'; rx.clearRect(0, 0, refl.width, refl.height);
130
+ rx.setTransform(1, 0, 0, -1, 0, 2 * floorY * D); rx.drawImage(neon, 0, 0); rx.setTransform(1, 0, 0, 1, 0, 0);
131
+ rx.globalCompositeOperation = 'destination-in';
132
+ const rf = rx.createLinearGradient(0, floorY * D, 0, (floorY + R * 1.4) * D); rf.addColorStop(0, 'rgba(0,0,0,.42)'); rf.addColorStop(1, 'rgba(0,0,0,0)');
133
+ rx.fillStyle = rf; rx.fillRect(0, floorY * D, refl.width, refl.height);
134
+ ctx.globalCompositeOperation = 'lighter';
135
+ ctx.drawImage(refl, 0, 0);
136
+ // bloom: shrink twice, blow back up (bilinear), add
137
+ s1x.clearRect(0, 0, sm1.width, sm1.height); s1x.drawImage(neon, 0, 0, sm1.width, sm1.height); s1x.drawImage(refl, 0, 0, sm1.width, sm1.height);
138
+ s2x.clearRect(0, 0, sm2.width, sm2.height); s2x.drawImage(sm1, 0, 0, sm2.width, sm2.height);
139
+ ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
140
+ const gk = PARAMS.glow;
141
+ ctx.globalAlpha = Math.min(1, 0.85 * gk); ctx.drawImage(sm2, 0, 0, canvas.width, canvas.height);
142
+ ctx.globalAlpha = Math.min(1, 0.5 * gk); ctx.drawImage(sm1, 0, 0, canvas.width, canvas.height);
143
+ ctx.globalAlpha = 1; ctx.drawImage(neon, 0, 0);
144
+ ctx.globalCompositeOperation = 'source-over';
145
+ raf = requestAnimationFrame(frame);
146
+ }
147
+ raf = requestAnimationFrame(frame);
148
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
149
+ }