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,109 @@
1
+ // Dust to the wind: a marble bust made of a hundred and fifty thousand points, each sitting on the surface with its own normal so
2
+ // it lights like stone. A dissolving front sweeps across it from one shoulder; every point the front passes flares
3
+ // like an ember, breaks loose and is carried off by a swirling wind, cooling to grey dust. Then the dust is called
4
+ // back along curving paths and the bust stands whole again. The surface is sampled from simple shapes (an egg of a
5
+ // head, a nose, a brow, ears, a bun of hair, a neck, shoulders), keeping only the points no other shape covers.
6
+ const PARAMS = { points: 150000, wind: 1, speed: 1 };
7
+
8
+ export default function (stage) {
9
+ const canvas = document.createElement('canvas');
10
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
11
+ stage.append(canvas);
12
+ const gl = canvas.getContext('webgl2', { antialias: true });
13
+ if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
14
+ const N = Math.max(10000, Math.round(PARAMS.points));
15
+ // the bust's parts: ellipsoids [cx, cy, cz, rx, ry, rz], each sampled on its surface
16
+ const PARTS = [[0, 1.0, 0, 0.46, 0.66, 0.52, 4], [0, 0.97, 0.5, 0.07, 0.17, 0.11, 0.25], [0, 1.2, 0.33, 0.34, 0.05, 0.13, 0.15], [0.45, 1.02, -0.02, 0.07, 0.16, 0.11, 0.22], [-0.45, 1.02, -0.02, 0.07, 0.16, 0.11, 0.22],
17
+ [0, 1.52, -0.36, 0.3, 0.27, 0.27, 0.8], [0, 0.3, -0.02, 0.25, 0.45, 0.24, 1.1], [0, -0.42, -0.05, 1.15, 0.5, 0.48, 3.2]];
18
+ const inside = (x, y, z, skip) => PARTS.some((p, i) => i !== skip && ((x - p[0]) / p[3]) ** 2 + ((y - p[1]) / p[4]) ** 2 + ((z - p[2]) / p[5]) ** 2 < 0.985);
19
+ const home = new Float32Array(N * 3), nrm = new Float32Array(N * 3);
20
+ const total = PARTS.reduce((a, p) => a + p[6], 0);
21
+ for (let k = 0; k < N;) {
22
+ let r = Math.random() * total, pi = 0; while (r > PARTS[pi][6]) { r -= PARTS[pi][6]; pi++; }
23
+ const p = PARTS[pi], u = Math.random() * 2 - 1, a = Math.random() * 6.283, s = Math.sqrt(1 - u * u);
24
+ const ux = Math.cos(a) * s, uy = u, uz = Math.sin(a) * s, x = p[0] + ux * p[3], y = p[1] + uy * p[4], z = p[2] + uz * p[5];
25
+ if (inside(x, y, z, pi) || (pi === 7 && y < -0.62)) continue; // hidden inside another part, or below the cut of the chest
26
+ let nx = ux / p[3], ny = uy / p[4], nz = uz / p[5]; const l = Math.hypot(nx, ny, nz);
27
+ home.set([x, y, z], k * 3); nrm.set([nx / l, ny / l, nz / l], k * 3); k++;
28
+ }
29
+ const pos = new Float32Array(home), vel = new Float32Array(N * 3), free = new Float32Array(N), seed = Float32Array.from({ length: N }, () => Math.random());
30
+
31
+ 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; };
32
+ const prog = (vs, fs) => {
33
+ const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs)); gl.linkProgram(p);
34
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
35
+ const u = {}; for (let i = 0, n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS); i < n; i++) { const nm = gl.getActiveUniform(p, i).name; u[nm] = gl.getUniformLocation(p, nm); }
36
+ return { p, u };
37
+ };
38
+ const ptP = prog(`#version 300 es
39
+ layout(location = 0) in vec3 P; layout(location = 1) in vec3 Nn; layout(location = 2) in float Fr;
40
+ uniform mat4 M; uniform float PS; out vec3 col;
41
+ void main() {
42
+ gl_Position = M * vec4(P, 1);
43
+ vec3 L = normalize(vec3(-.55, .6, .6)), rim = normalize(vec3(.8, .3, -.6));
44
+ float dif = max(dot(Nn, L), 0.), back = pow(max(dot(Nn, rim), 0.), 3.);
45
+ vec3 marble = vec3(.93, .9, .85) * (.16 + .84 * dif) + vec3(.5, .62, .8) * back * .5;
46
+ vec3 ember = vec3(1., .55, .2) * 1.6, dust = vec3(.42, .4, .38);
47
+ col = Fr <= 0. ? marble : mix(ember, dust, smoothstep(0., .6, Fr)) * (1. - smoothstep(1.6, 3., Fr) * .8);
48
+ gl_PointSize = PS * (Fr > 0. ? 1. - .5 * smoothstep(0., 2., Fr) : 1.);
49
+ }`, `#version 300 es
50
+ precision mediump float; in vec3 col; out vec4 o; void main() { vec2 q = gl_PointCoord * 2. - 1.; if (dot(q, q) > 1.) discard; o = vec4(pow(col, vec3(1. / 2.2)), 1); }`);
51
+ const bgP = prog(`#version 300 es
52
+ layout(location = 0) in vec2 p; out vec2 v; void main() { v = p; gl_Position = vec4(p, .9999, 1); }`, `#version 300 es
53
+ precision highp float; in vec2 v; out vec4 o; void main() { vec3 c = mix(vec3(.11, .1, .1), vec3(.025, .024, .026), length(v * vec2(.65, 1.)) * .9); o = vec4(c, 1); }`);
54
+ const vao = gl.createVertexArray(); gl.bindVertexArray(vao);
55
+ const pb = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, pb); gl.bufferData(gl.ARRAY_BUFFER, pos.byteLength, gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
56
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, nrm, gl.STATIC_DRAW); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
57
+ const fb = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, fb); gl.bufferData(gl.ARRAY_BUFFER, free.byteLength, gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 0, 0);
58
+ const qv = gl.createVertexArray(); gl.bindVertexArray(qv); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); 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);
59
+ gl.bindVertexArray(null);
60
+
61
+ let W = 1, H = 1;
62
+ const resize = () => { const d = Math.min(devicePixelRatio, 2); W = canvas.width = Math.max(1, (stage.clientWidth * d) | 0); H = canvas.height = Math.max(1, (stage.clientHeight * d) | 0); };
63
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
64
+ const matrix = t => {
65
+ const az = 0.45 + 0.25 * Math.sin(t * 0.15), el = 0.12, dist = 5.2, E = [Math.sin(az) * Math.cos(el) * dist, 0.75 + Math.sin(el) * dist, Math.cos(az) * Math.cos(el) * dist], T = [0, 0.62, 0];
66
+ const f = T.map((v, i) => v - E[i]), fl = Math.hypot(...f); for (let i = 0; i < 3; i++) f[i] /= fl;
67
+ const rl = Math.hypot(f[2], f[0]), r = [-f[2] / rl, 0, f[0] / rl], 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]];
68
+ const V = [r[0], u[0], -f[0], 0, r[1], u[1], -f[1], 0, r[2], u[2], -f[2], 0, -(r[0] * E[0] + r[1] * E[1] + r[2] * E[2]), -(u[0] * E[0] + u[1] * E[1] + u[2] * E[2]), f[0] * E[0] + f[1] * E[1] + f[2] * E[2], 1];
69
+ const F = 1 / Math.tan(0.36), A = W / H, n = 0.1, fa = 60, nf = 1 / (n - fa), P = [F / A, 0, 0, 0, 0, F, 0, 0, 0, 0, (fa + n) * nf, -1, 0, 0, 2 * fa * n * nf, 0], out = new Float32Array(16);
70
+ 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] * V[c * 4 + k]; out[c * 4 + rr] = s; }
71
+ return out;
72
+ };
73
+ let raf, last = performance.now(), t = 0, phase = 0;
74
+ function frame(now) {
75
+ const dt = Math.min(0.04, (now - last) / 1000) * PARAMS.speed; last = now; t += dt;
76
+ // a cycle: whole (2.5 s), the front sweeps across (4.5 s), dust drifts (2 s), called back (3 s)
77
+ const c = t % 12;
78
+ const front = c < 2.5 ? -3 : c < 7 ? -1.6 + ((c - 2.5) / 4.5) * 3.6 : 3; // position of the dissolving plane along (1, .5, .3)
79
+ const back = c > 9 ? Math.min(1, (c - 9) / 2.6) : 0;
80
+ for (let i = 0; i < N; i++) {
81
+ const i3 = i * 3, hx = home[i3], hy = home[i3 + 1], hz = home[i3 + 2];
82
+ if (back > 0) { // called home: ease back along a curve
83
+ const e = back * back * (3 - 2 * back), sw = Math.sin(e * Math.PI) * 0.6 * (seed[i] - 0.5);
84
+ pos[i3] += (hx + sw - pos[i3]) * Math.min(1, e * 0.25 + 0.02); pos[i3 + 1] += (hy - pos[i3 + 1]) * Math.min(1, e * 0.25 + 0.02); pos[i3 + 2] += (hz - pos[i3 + 2]) * Math.min(1, e * 0.25 + 0.02);
85
+ free[i] = Math.max(0, free[i] - dt * 2); if (back >= 1) { pos[i3] = hx; pos[i3 + 1] = hy; pos[i3 + 2] = hz; free[i] = 0; vel[i3] = vel[i3 + 1] = vel[i3 + 2] = 0; }
86
+ continue;
87
+ }
88
+ if (free[i] <= 0) {
89
+ const s = hx * 0.82 + hy * 0.45 + hz * 0.3 + (seed[i] - 0.5) * 0.25 + Math.sin(hy * 9 + hx * 7) * 0.06;
90
+ if (s < front) { free[i] = 0.001; vel[i3] = 0.2; vel[i3 + 1] = 0.25; vel[i3 + 2] = 0; }
91
+ continue;
92
+ }
93
+ // the wind: a swirling field that carries the dust off to the left and up
94
+ const x = pos[i3], y = pos[i3 + 1], z = pos[i3 + 2], wnd = PARAMS.wind;
95
+ const ax = (-1.2 + Math.sin(y * 2.3 + t * 0.9) * 0.9 + Math.cos(z * 1.7 - t) * 0.5) * wnd, ay = (0.45 + Math.sin(x * 1.9 + t * 0.7) * 0.6) * wnd, az = Math.sin(x * 1.3 + y * 1.1 + t) * 0.8 * wnd;
96
+ vel[i3] += (ax - vel[i3]) * dt * 1.6; vel[i3 + 1] += (ay - vel[i3 + 1]) * dt * 1.6; vel[i3 + 2] += (az - vel[i3 + 2]) * dt * 1.6;
97
+ pos[i3] += vel[i3] * dt; pos[i3 + 1] += vel[i3 + 1] * dt; pos[i3 + 2] += vel[i3 + 2] * dt;
98
+ free[i] += dt;
99
+ }
100
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, W, H); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
101
+ gl.enable(gl.DEPTH_TEST); gl.useProgram(bgP.p); gl.bindVertexArray(qv); gl.drawArrays(gl.TRIANGLES, 0, 3);
102
+ gl.useProgram(ptP.p); gl.uniformMatrix4fv(ptP.u.M, false, matrix(t)); gl.uniform1f(ptP.u.PS, Math.max(1.6, H / 300) * Math.sqrt(150000 / N));
103
+ gl.bindVertexArray(vao); gl.bindBuffer(gl.ARRAY_BUFFER, pb); gl.bufferSubData(gl.ARRAY_BUFFER, 0, pos); gl.bindBuffer(gl.ARRAY_BUFFER, fb); gl.bufferSubData(gl.ARRAY_BUFFER, 0, free);
104
+ gl.drawArrays(gl.POINTS, 0, N); gl.bindVertexArray(null); gl.disable(gl.DEPTH_TEST);
105
+ raf = requestAnimationFrame(frame);
106
+ }
107
+ raf = requestAnimationFrame(frame);
108
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
109
+ }
@@ -0,0 +1,64 @@
1
+ // Extruded type: a word drawn once into a mask, then stacked back to front as dozens of layers. Each layer is
2
+ // placed by the same 3D rotation (an exact orthographic projection is just a 2D affine transform), so the
3
+ // stack reads as solid letters with depth. The face gets a moving chrome sweep; the sides darken with depth.
4
+ const PARAMS = { depth: 1, layers: 36, tilt: 1 };
5
+
6
+ export default function (stage) {
7
+ const canvas = document.createElement('canvas');
8
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
9
+ stage.append(canvas);
10
+ const ctx = canvas.getContext('2d');
11
+ let W = 0, H = 0, mask, side, sideLit, face, mw = 0, mh = 0;
12
+ const WORD = 'DEPTH';
13
+ function build() {
14
+ const fs = Math.min(W * 0.19, H * 0.36);
15
+ mask = document.createElement('canvas'); const m = mask.getContext('2d');
16
+ m.font = `900 ${fs}px "Mona Sans", system-ui, sans-serif`;
17
+ mw = Math.ceil(m.measureText(WORD).width + fs * 0.2); mh = Math.ceil(fs * 1.1);
18
+ mask.width = mw; mask.height = mh;
19
+ m.font = `900 ${fs}px "Mona Sans", system-ui, sans-serif`; m.fontStretch = 'expanded';
20
+ m.textAlign = 'center'; m.textBaseline = 'middle'; m.fillStyle = '#fff'; m.fillText(WORD, mw / 2, mh * 0.53);
21
+ const tint = c => { const k = document.createElement('canvas'); k.width = mw; k.height = mh; const g = k.getContext('2d'); g.drawImage(mask, 0, 0); g.globalCompositeOperation = 'source-in'; g.fillStyle = c; g.fillRect(0, 0, mw, mh); return k; };
22
+ side = tint('#2a1458'); sideLit = tint('#8b5cf6');
23
+ face = document.createElement('canvas'); face.width = mw; face.height = mh;
24
+ }
25
+ const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth; H = stage.clientHeight; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); build(); };
26
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
27
+ let px = 0, py = 0, lastMove = -1e9;
28
+ const onMove = e => { const b = canvas.getBoundingClientRect(); px = (e.clientX - b.left) / b.width * 2 - 1; py = (e.clientY - b.top) / b.height * 2 - 1; lastMove = performance.now(); };
29
+ canvas.addEventListener('pointermove', onMove);
30
+ let ry = 0, rx = 0, raf; const t0 = performance.now();
31
+ function frame(now) {
32
+ const t = (now - t0) / 1000, d = Math.min(devicePixelRatio, 2);
33
+ const idle = now - lastMove > 1500;
34
+ const ty = (idle ? Math.sin(t * 0.7) * 0.55 : px * 0.7) * PARAMS.tilt, tx = (idle ? Math.sin(t * 0.45 + 1) * 0.28 : py * 0.4) * PARAMS.tilt;
35
+ ry += (ty - ry) * 0.08; rx += (tx - rx) * 0.08;
36
+ const bg = ctx.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, '#f3efe8'); bg.addColorStop(1, '#ddd5c8');
37
+ ctx.setTransform(d, 0, 0, d, 0, 0); ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
38
+ // rotation (Y then X); orthographic projection of the text plane = this 2x2 matrix plus a per-layer offset
39
+ const cy = Math.cos(ry), sy = Math.sin(ry), cx = Math.cos(rx), sx = Math.sin(rx);
40
+ const a = cy, b = sx * sy, c = 0, dd = cx, ox = sy, oy = -cy * sx; // where the x and y axes land on screen, and how far one unit of depth shifts
41
+ const depth = Math.min(W, H) * 0.16 * PARAMS.depth, L = Math.max(4, Math.round(PARAMS.layers));
42
+ // soft shadow on the "floor"
43
+ ctx.save(); ctx.translate(W / 2 + ry * 20, H * 0.8); ctx.scale(1, 0.18); const sg = ctx.createRadialGradient(0, 0, 0, 0, 0, mw * 0.5);
44
+ sg.addColorStop(0, 'rgba(60,40,90,.35)'); sg.addColorStop(1, 'rgba(60,40,90,0)'); ctx.fillStyle = sg; ctx.fillRect(-mw, -mw, mw * 2, mw * 2); ctx.restore();
45
+ const light = 0.5 + 0.5 * Math.sin(ry + 0.6); // sides facing the light read brighter
46
+ for (let i = L; i >= 1; i--) {
47
+ const z = depth * i / L, persp = 1 - z * 0.0009 * (1 + sx);
48
+ ctx.setTransform(d * a * persp, d * b * persp, d * c, d * dd * persp, d * (W / 2 + ox * z), d * (H / 2 - 8 + oy * z));
49
+ ctx.globalAlpha = 1; ctx.drawImage(side, -mw / 2, -mh / 2);
50
+ ctx.globalAlpha = (1 - i / L) * 0.55 + light * 0.35; ctx.drawImage(sideLit, -mw / 2, -mh / 2);
51
+ }
52
+ // the face: chrome bands sweeping across, clipped to the letters
53
+ const f = face.getContext('2d'); f.globalCompositeOperation = 'source-over'; f.clearRect(0, 0, mw, mh); f.drawImage(mask, 0, 0); f.globalCompositeOperation = 'source-in';
54
+ const sweep = (t * 0.25 + ry * 0.4) % 1, g = f.createLinearGradient(0, 0, mw, mh * 0.6);
55
+ [[0, '#d9d2ff'], [0.25, '#6d4fd6'], [0.45, '#ffffff'], [0.55, '#b9a8ff'], [0.8, '#4b2fa8'], [1, '#e8e2ff']].forEach(([o, col]) => g.addColorStop((o + sweep) % 1, col));
56
+ f.fillStyle = g; f.fillRect(0, 0, mw, mh);
57
+ ctx.globalAlpha = 1; ctx.setTransform(d * a, d * b, d * c, d * dd, d * W / 2, d * (H / 2 - 8));
58
+ ctx.drawImage(face, -mw / 2, -mh / 2);
59
+ ctx.globalAlpha = 1;
60
+ raf = requestAnimationFrame(frame);
61
+ }
62
+ raf = requestAnimationFrame(frame);
63
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); canvas.remove(); };
64
+ }
@@ -0,0 +1,75 @@
1
+ // 3D fractal tree: a trunk that splits into three branches, each turned about the parent's axis by the golden angle
2
+ // and tilted outward, each shorter and thinner, eight levels deep. The whole tree grows in: a branch only starts once
3
+ // its parent is full length, with an ease-out so each one shoots then settles. Wind sways every branch by an amount
4
+ // that grows toward the tips, blossoms open at the ends, petals drift down, and the camera circles the tree.
5
+ const PARAMS = { depth: 7, wind: 1 };
6
+
7
+ export default function (stage) {
8
+ const canvas = document.createElement('canvas');
9
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
10
+ stage.append(canvas);
11
+ const ctx = canvas.getContext('2d');
12
+ let W = 0, H = 0;
13
+ const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); };
14
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
15
+ // build the branch list once: each with a level, a parent, a direction offset and a length
16
+ const GA = Math.PI * (3 - Math.sqrt(5)), D = Math.round(PARAMS.depth), branches = [];
17
+ let seed = 7; const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
18
+ (function grow(parent, level, yaw, pitch, len) {
19
+ const b = { parent, level, yaw, pitch, len, i: branches.length }; branches.push(b);
20
+ if (level >= D) return;
21
+ for (let k = 0; k < 3; k++) grow(b, level + 1, yaw + k * 2.1 + GA * level + (rnd() - 0.5) * 0.6, 0.45 + rnd() * 0.25, len * (0.68 + rnd() * 0.1));
22
+ })(null, 0, 0, 0, 1);
23
+ const petals = Array.from({ length: 60 }, () => ({ x: (rnd() - 0.5) * 3, y: rnd() * 3, z: (rnd() - 0.5) * 3, ph: rnd() * 6.28 }));
24
+ const ease = x => 1 - (1 - Math.min(1, Math.max(0, x))) ** 3;
25
+ let raf; const t0 = performance.now();
26
+ function frame(now) {
27
+ const t = (now - t0) / 1000, S = Math.min(W, H), cycle = 16, g = (t % cycle) * 1.6; // grow, hold, regrow
28
+ const bg = ctx.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, '#1b1530'); bg.addColorStop(1, '#3a2340');
29
+ ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
30
+ ctx.fillStyle = 'rgba(0,0,0,.25)'; ctx.beginPath(); ctx.ellipse(W / 2, H * 0.93, S * 0.24, S * 0.03, 0, 0, 7); ctx.fill();
31
+ const cam = t * 0.15, cc = Math.cos(cam), cs = Math.sin(cam);
32
+ const proj = p => { const x = p[0] * cc - p[2] * cs, z = p[0] * cs + p[2] * cc, k = 4 / (4 + z); return [W / 2 + x * S * 0.17 * k, H * 0.93 - p[1] * S * 0.17 * k, z, k]; };
33
+ // positions: walk the list (parents come first), building each branch's frame from its parent's
34
+ const P = new Array(branches.length), segs = [];
35
+ for (const b of branches) {
36
+ const grown = ease(g - b.level * 0.55); // each level waits for the one before
37
+ if (grown <= 0) continue;
38
+ const sway = Math.sin(t * 1.3 + b.level * 0.7 + b.yaw) * 0.04 * b.level * PARAMS.wind;
39
+ const base = b.parent ? P[b.parent.i].end : [0, 0, 0];
40
+ const pd = b.parent ? P[b.parent.i].dir : [0, 1, 0];
41
+ if (!base) continue;
42
+ // tilt away from the parent's direction by pitch, around it by yaw
43
+ const up = pd, side = Math.abs(up[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0];
44
+ let a = [up[1] * side[2] - up[2] * side[1], up[2] * side[0] - up[0] * side[2], up[0] * side[1] - up[1] * side[0]]; const al = Math.hypot(...a); a = a.map(v => v / al);
45
+ const c2 = [up[1] * a[2] - up[2] * a[1], up[2] * a[0] - up[0] * a[2], up[0] * a[1] - up[1] * a[0]];
46
+ const yw = b.yaw + sway, pt = b.level ? b.pitch + sway * 0.5 : sway * 0.3;
47
+ const dir = up.map((u, k) => u * Math.cos(pt) + (a[k] * Math.cos(yw) + c2[k] * Math.sin(yw)) * Math.sin(pt));
48
+ const L = b.len * grown, end = base.map((v, k) => v + dir[k] * L);
49
+ P[b.i] = { end: grown >= 1 ? end : end, dir };
50
+ segs.push({ a: base, b: end, w: (D + 1 - b.level) ** 1.5 * 0.9, level: b.level, tip: b.level === D, grown });
51
+ }
52
+ const pr = segs.map(s => ({ s, A: proj(s.a), B: proj(s.b) }));
53
+ pr.sort((x, y) => (y.A[2] + y.B[2]) - (x.A[2] + x.B[2])); // far first
54
+ ctx.lineCap = 'round';
55
+ for (const { s, A, B } of pr) {
56
+ const shade = 60 + s.level * 9 - (A[2] + B[2]) * 12;
57
+ ctx.strokeStyle = `rgb(${shade + 30},${shade},${shade - 10})`; ctx.lineWidth = Math.max(0.6, s.w * A[3] * S / 500);
58
+ ctx.beginPath(); ctx.moveTo(A[0], A[1]); ctx.lineTo(B[0], B[1]); ctx.stroke();
59
+ if (s.tip && s.grown >= 1) { // blossoms open at the tips
60
+ const open = Math.min(1, (g - (D + 1) * 0.55) * 0.8);
61
+ if (open > 0) { ctx.fillStyle = `rgba(255,${170 + (s.level * 13) % 40},${200},${0.75 * open})`; ctx.beginPath(); ctx.arc(B[0], B[1], S * 0.0055 * B[3] * (0.6 + open), 0, 7); ctx.fill(); }
62
+ }
63
+ }
64
+ // petals falling
65
+ ctx.fillStyle = 'rgba(255,190,215,.8)';
66
+ for (const p of petals) {
67
+ p.y -= 0.004; p.x += Math.sin(t + p.ph) * 0.004 * PARAMS.wind; if (p.y < 0) p.y = 3;
68
+ if (g < (D + 2) * 0.55) continue;
69
+ const [x, y, , k] = proj([p.x, p.y, p.z]); ctx.beginPath(); ctx.ellipse(x, y, 2.2 * k, 1.3 * k, t + p.ph, 0, 7); ctx.fill();
70
+ }
71
+ raf = requestAnimationFrame(frame);
72
+ }
73
+ raf = requestAnimationFrame(frame);
74
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
75
+ }
@@ -0,0 +1,111 @@
1
+ // Diamond: a round brilliant cut built from 49 flat facets (table, crown, girdle, pavilion), traced exactly as the
2
+ // intersection of 49 half-spaces. Light enters, then bounces inside: at each facet it either escapes or, past the
3
+ // critical angle, is totally reflected back in, which is what makes a diamond bright. Red, green and blue are traced
4
+ // separately with slightly different refractive indices (dispersion), so the escaping light splits into fire. Strip
5
+ // lights in a dark studio give it something to sparkle with as it turns.
6
+ const PARAMS = { ior: 2.42, fire: 1 };
7
+
8
+ export default function (stage) {
9
+ const canvas = document.createElement('canvas');
10
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
11
+ stage.append(canvas);
12
+ const gl = canvas.getContext('webgl2', { antialias: false });
13
+ if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
14
+ // the cut: each facet a plane n·p = d with n pointing out
15
+ const planes = [], deg = Math.PI / 180;
16
+ const add = (n, d) => { const l = Math.hypot(...n); planes.push(n[0] / l, n[1] / l, n[2] / l, d / l); };
17
+ add([0, 1, 0], 0.4); // table
18
+ const through = (n, p) => n[0] * p[0] + n[1] * p[1] + n[2] * p[2];
19
+ for (let k = 0; k < 8; k++) { // crown mains and upper girdle facets
20
+ const f = k * Math.PI / 4, g = f + Math.PI / 8;
21
+ let n = [Math.sin(34.5 * deg) * Math.cos(f), Math.cos(34.5 * deg), Math.sin(34.5 * deg) * Math.sin(f)]; add(n, through(n, [Math.cos(f), 0.04, Math.sin(f)]));
22
+ n = [Math.sin(46 * deg) * Math.cos(g), Math.cos(46 * deg), Math.sin(46 * deg) * Math.sin(g)]; add(n, through(n, [Math.cos(g), 0.03, Math.sin(g)]));
23
+ }
24
+ for (let k = 0; k < 16; k++) { const f = k * Math.PI / 8; add([Math.cos(f), 0, Math.sin(f)], 1); } // girdle
25
+ for (let k = 0; k < 8; k++) { // pavilion mains to the culet, lower girdle facets
26
+ const f = k * Math.PI / 4 + Math.PI / 8, g = k * Math.PI / 4;
27
+ let n = [Math.sin(40.8 * deg) * Math.cos(f), -Math.cos(40.8 * deg), Math.sin(40.8 * deg) * Math.sin(f)]; add(n, through(n, [0, -0.86, 0]));
28
+ n = [Math.sin(43 * deg) * Math.cos(g), -Math.cos(43 * deg), Math.sin(43 * deg) * Math.sin(g)]; add(n, through(n, [Math.cos(g), -0.03, Math.sin(g)]));
29
+ }
30
+ const NP = planes.length / 4;
31
+ const vs = `#version 300 es
32
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
33
+ const fs = `#version 300 es
34
+ precision highp float; in vec2 uv; out vec4 o;
35
+ uniform vec2 R; uniform float T, IOR, FIRE; uniform mat3 ROT; uniform vec4 PL[${NP}];
36
+ vec3 env(vec3 d) { // a dark studio with strip lights and a ring light
37
+ vec3 c = vec3(.012, .014, .02) + vec3(.05, .05, .06) * smoothstep(-.2, 1., d.y);
38
+ float az = atan(d.z, d.x);
39
+ for (int i = 0; i < 9; i++) { float a = float(i) * .698 + .3; c += vec3(4.5) * smoothstep(.06, .0, abs(sin((az - a) * .5))) * smoothstep(.0, .2, d.y) * smoothstep(.75, .45, d.y); }
40
+ c += vec3(4., 3.6, 3.2) * smoothstep(.04, .0, abs(d.y - .9)); // ring light overhead
41
+ c += vec3(.6, .7, 1.) * smoothstep(.15, .0, abs(d.y + .2)) * .4;
42
+ return c;
43
+ }
44
+ // entering the convex cut from outside: x = t, and the facet hit in yzw
45
+ vec4 enter(vec3 ro, vec3 rd) {
46
+ float tn = -1e9, tf = 1e9; vec3 nn = vec3(0);
47
+ for (int i = 0; i < ${NP}; i++) {
48
+ vec3 n = PL[i].xyz; float den = dot(n, rd), dist = PL[i].w - dot(n, ro);
49
+ if (abs(den) < 1e-6) { if (dist < 0.) return vec4(-1); continue; }
50
+ float t = dist / den;
51
+ if (den < 0.) { if (t > tn) { tn = t; nn = n; } } else tf = min(tf, t);
52
+ }
53
+ return tn < tf && tn > 0. ? vec4(tn, nn) : vec4(-1);
54
+ }
55
+ // leaving from inside: the first facet ahead
56
+ vec4 leave(vec3 p, vec3 rd) {
57
+ float tf = 1e9; vec3 nn = vec3(0, 1, 0);
58
+ for (int i = 0; i < ${NP}; i++) { vec3 n = PL[i].xyz; float den = dot(n, rd); if (den > 1e-6) { float t = (PL[i].w - dot(n, p)) / den; if (t < tf) { tf = t; nn = n; } } }
59
+ return vec4(tf, nn);
60
+ }
61
+ // one colour channel through the stone
62
+ float trace(vec3 ro, vec3 rd, float ior, int ch) {
63
+ vec4 h = enter(ro, rd); if (h.x < 0.) return -1.;
64
+ vec3 n = h.yzw, p = ro + rd * h.x;
65
+ float c = dot(-rd, n), F0 = pow((ior - 1.) / (ior + 1.), 2.), F = F0 + (1. - F0) * pow(1. - c, 5.);
66
+ float acc = F * env(reflect(rd, n))[ch], thr = 1. - F;
67
+ vec3 d = refract(rd, n, 1. / ior);
68
+ for (int b = 0; b < 7; b++) {
69
+ vec4 l = leave(p + d * 1e-4, d); p = p + d * (l.x + 1e-4); vec3 m = l.yzw;
70
+ vec3 out_ = refract(d, -m, ior);
71
+ if (dot(out_, out_) < .01) { d = reflect(d, -m); continue; } // total internal reflection: stays inside
72
+ float ci = dot(d, m), Fi = F0 + (1. - F0) * pow(1. - ci, 5.);
73
+ acc += thr * (1. - Fi) * env(out_)[ch]; thr *= Fi; d = reflect(d, -m);
74
+ }
75
+ return acc;
76
+ }
77
+ vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
78
+ void main() {
79
+ vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
80
+ vec3 eye = vec3(0, 1.6, -3.4), f = normalize(vec3(0, -.05, 0) - eye), r = normalize(cross(vec3(0, 1, 0), f)), u = cross(f, r);
81
+ vec3 rd0 = normalize(f * 2.1 + r * q.x + u * q.y);
82
+ vec3 ro = ROT * eye, rd = ROT * rd0; // turn the world, not the stone
83
+ vec3 bg = mix(vec3(.02, .022, .03), vec3(.07, .07, .09), uv.y);
84
+ float sp = .025 * FIRE;
85
+ float rr = trace(ro, rd, IOR - sp, 0);
86
+ vec3 col;
87
+ if (rr < 0.) col = bg + vec3(.04) * smoothstep(.9, .0, length(q - vec2(0, -.55)) * vec2(1, 3).x);
88
+ else col = vec3(rr, trace(ro, rd, IOR, 1), trace(ro, rd, IOR + sp, 2));
89
+ col = aces(col * 1.5);
90
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
91
+ }`;
92
+ 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; };
93
+ 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);
94
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
95
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
96
+ const U = {}; ['R', 'T', 'IOR', 'FIRE', 'ROT', 'PL'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
97
+ gl.uniform4fv(U.PL, new Float32Array(planes));
98
+ const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, Math.max(1, Math.sqrt(380000 / (w * h)))); canvas.width = w * d | 0; canvas.height = h * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
99
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
100
+ let raf; const t0 = performance.now();
101
+ function frame(now) {
102
+ const t = (now - t0) / 1000, a = t * 0.35, b = Math.sin(t * 0.4) * 0.18; // turn, with a gentle rock
103
+ const ca = Math.cos(a), sa = Math.sin(a), cb = Math.cos(b), sb = Math.sin(b);
104
+ gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, t); gl.uniform1f(U.IOR, PARAMS.ior); gl.uniform1f(U.FIRE, PARAMS.fire);
105
+ gl.uniformMatrix3fv(U.ROT, false, new Float32Array([ca, sb * sa, -cb * sa, 0, cb, sb, sa, -sb * ca, cb * ca]));
106
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
107
+ raf = requestAnimationFrame(frame);
108
+ }
109
+ raf = requestAnimationFrame(frame);
110
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
111
+ }
@@ -0,0 +1,102 @@
1
+ // Geode: a rough grey stone that cracks in half and opens like a book, and inside it is lined with amethyst. All of
2
+ // it is one distance function, raymarched. The stone is a sphere roughened with noise; each half keeps only one side
3
+ // of the cut and has a hollow carved out of it. The hollow's wall is pushed inward into points by a Voronoi pattern
4
+ // on the sphere of directions: every cell becomes a faceted crystal pointing at the centre, paler at its tip. On the
5
+ // cut face you see the layers in section: the rind, the stone, bands of agate, then the crystals.
6
+ const PARAMS = { crystals: 1, sparkle: 1, speed: 1 };
7
+
8
+ export default function (stage) {
9
+ const canvas = document.createElement('canvas');
10
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
11
+ stage.append(canvas);
12
+ const gl = canvas.getContext('webgl2', { antialias: false });
13
+ if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
14
+ const FS = `#version 300 es
15
+ precision highp float; in vec2 uv; out vec4 o; uniform vec2 R; uniform float T, OPEN, CR, SP;
16
+ mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
17
+ vec3 h3(vec3 p) { p = fract(p * vec3(.1031, .103, .0973)); p += dot(p, p.yxz + 33.33); return fract((p.xxy + p.yxx) * p.zyx); }
18
+ float n3(vec3 p) { vec3 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f);
19
+ return mix(mix(mix(h3(i).x, h3(i + vec3(1, 0, 0)).x, f.x), mix(h3(i + vec3(0, 1, 0)).x, h3(i + vec3(1, 1, 0)).x, f.x), f.y), mix(mix(h3(i + vec3(0, 0, 1)).x, h3(i + vec3(1, 0, 1)).x, f.x), mix(h3(i + vec3(0, 1, 1)).x, h3(i + vec3(1, 1, 1)).x, f.x), f.y), f.z); }
20
+ float fbm(vec3 p) { return n3(p) * .55 + n3(p * 2.1) * .28 + n3(p * 4.3) * .17; }
21
+ // crystals: distance to the nearest Voronoi point among directions; near a point the wall stands tallest
22
+ vec2 vor(vec3 n) {
23
+ vec3 p = n * 5.5 * CR, i = floor(p), f = fract(p); float d1 = 9., d2 = 9.;
24
+ for (int z = -1; z <= 1; z++) for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) {
25
+ vec3 g = vec3(x, y, z), q = g + h3(i + g) * .8 + .1 - f; float d = max(max(abs(q.x), abs(q.y)), abs(q.z)) * .7 + length(q) * .3; // faceted
26
+ if (d < d1) { d2 = d1; d1 = d; } else if (d < d2) d2 = d;
27
+ }
28
+ return vec2(d1, d2 - d1);
29
+ }
30
+ float rock(vec3 p) { return length(p) - 1. - .07 * fbm(p * 2.6) - .025 * fbm(p * 9.); }
31
+ float cavity(vec3 p) { vec2 v = vor(normalize(p)); return .7 - .26 * smoothstep(.75, .0, v.x) - length(p); } // > 0 inside the stone around the hollow
32
+ // one half: the stone, minus the hollow, minus everything on the far side of the cut
33
+ float half_(vec3 p, out float mat) {
34
+ float r = rock(p), c = cavity(p), cut = p.x;
35
+ float d = max(max(r, c), cut);
36
+ mat = cut > max(r, c) - .002 ? 2. : (c > r ? 1. : 0.); // 0 outer stone, 1 crystal, 2 the cut face
37
+ return d;
38
+ }
39
+ vec3 local(vec3 p, float s) { // each half swings open about a hinge at the back
40
+ p.x *= s; p -= vec3(-.06, 0, -1.) * vec3(1., 1., 1.); p.xz *= rot(-OPEN * 1.25); p += vec3(-.06, 0, -1.);
41
+ return p;
42
+ }
43
+ float map(vec3 p, out float mat, out float side) {
44
+ float ma, mb; vec3 pa = local(p, 1.), pb = local(p, -1.);
45
+ float da = half_(pa, ma), db = half_(pb, mb);
46
+ side = da < db ? 1. : -1.; mat = da < db ? ma : mb; return min(da, db) * .7;
47
+ }
48
+ float map(vec3 p) { float m, s; return map(p, m, s); }
49
+ vec3 nrm(vec3 p) { vec2 e = vec2(.0015, 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))); }
50
+ void main() {
51
+ vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
52
+ vec3 ro = vec3(0, .55, 4.2), rd = normalize(vec3(q, -2.2)); rd.yz *= rot(-.1);
53
+ ro.xz *= rot(.35 * sin(T * .17)); rd.xz *= rot(.35 * sin(T * .17));
54
+ // a dark velvet display case under a spotlight
55
+ vec3 col = mix(vec3(.05, .03, .06), vec3(.012, .01, .016), length(q * vec2(.6, .9)));
56
+ float t = 0., mat = 0., side = 1.; bool hit = false;
57
+ for (int i = 0; i < 120; i++) { float d = map(ro + rd * t, mat, side); if (d < .0012) { hit = true; break; } t += d; if (t > 9.) break; }
58
+ if (hit) {
59
+ vec3 p = ro + rd * t, n = nrm(p), L = normalize(vec3(-.5, .9, .6)), V = -rd, H = normalize(L + V);
60
+ vec3 lp = local(p, side); float r = length(lp);
61
+ float dif = max(dot(n, L), 0.), amb = .18 + .12 * n.y;
62
+ vec3 base;
63
+ if (mat < .5) base = mix(vec3(.36, .33, .3), vec3(.5, .47, .42), fbm(lp * 6.)) * (.8 + .2 * n3(lp * 30.)); // the stone's skin
64
+ else if (mat < 1.5) {
65
+ vec2 v = vor(normalize(lp)); float tip = smoothstep(.62, .48, r);
66
+ base = mix(vec3(.24, .06, .36), vec3(.82, .62, .95), tip) * (.75 + .5 * smoothstep(0., .08, v.y)); // amethyst: deep at the root, pale at the tip
67
+ } else {
68
+ // the cut face: rind, stone, agate bands, then the crystal layer
69
+ float band = sin(r * 140. + fbm(lp * 7.) * 6.);
70
+ base = r > .95 ? vec3(.42, .38, .34) : r > .8 ? vec3(.58, .55, .5) * (.85 + .15 * n3(lp * 25.)) : r > .72 ? mix(vec3(.78, .8, .86), vec3(.55, .6, .72), .5 + .5 * band) : vec3(.5, .3, .62);
71
+ }
72
+ col = base * (amb + dif * .95);
73
+ float sp = pow(max(dot(n, H), 0.), mat > .5 && mat < 1.5 ? 90. : 18.) * (mat > .5 && mat < 1.5 ? 2.2 * SP : .25);
74
+ if (mat > .5 && mat < 1.5) sp *= .4 + 1.6 * step(.82, h3(floor(lp * 60.)).x); // facets glint one by one
75
+ col += vec3(1., .96, 1.) * sp;
76
+ col *= 1. - .5 * smoothstep(.0, .6, -dot(n, L)) * step(mat, .5);
77
+ }
78
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
79
+ }`;
80
+ const VS = `#version 300 es
81
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
82
+ 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; };
83
+ 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);
84
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
85
+ gl.useProgram(p); const U = n => gl.getUniformLocation(p, n);
86
+ gl.bindVertexArray(gl.createVertexArray()); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
87
+ 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);
88
+ let W = 1, H = 1;
89
+ const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, 2, Math.max(0.5, Math.sqrt(290000 / (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); };
90
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
91
+ let raf, last = performance.now(), t = 0;
92
+ function frame(now) {
93
+ const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt * PARAMS.speed;
94
+ // closed, then it splits and swings open, holds, and closes again
95
+ const c = t % 11, open = c < 1.5 ? 0 : c < 3.6 ? (1 - Math.cos(Math.PI * (c - 1.5) / 2.1)) / 2 : c < 9.2 ? 1 : c < 10.6 ? (1 + Math.cos(Math.PI * (c - 9.2) / 1.4)) / 2 : 0;
96
+ gl.uniform2f(U('R'), W, H); gl.uniform1f(U('T'), t); gl.uniform1f(U('OPEN'), open); gl.uniform1f(U('CR'), PARAMS.crystals); gl.uniform1f(U('SP'), PARAMS.sparkle);
97
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
98
+ raf = requestAnimationFrame(frame);
99
+ }
100
+ raf = requestAnimationFrame(frame);
101
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
102
+ }
@@ -0,0 +1,73 @@
1
+ // Glass lens refraction: inside the lens each pixel samples the scene behind it through a domed surface,
2
+ // pulled toward the centre (magnification). Red, green and blue bend by slightly different amounts: dispersion.
3
+ const PARAMS = { ior: 1.3, dispersion: 0.02 };
4
+
5
+ export default function (stage) {
6
+ const canvas = document.createElement('canvas');
7
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
8
+ stage.append(canvas);
9
+ const gl = canvas.getContext('webgl2', { antialias: false });
10
+ if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
11
+
12
+ // The scene behind the glass, painted once.
13
+ const pic = document.createElement('canvas'); pic.width = 1200; pic.height = 900;
14
+ const g = pic.getContext('2d');
15
+ g.fillStyle = '#efe7dc'; g.fillRect(0, 0, 1200, 900);
16
+ g.fillStyle = '#d9c8b4'; for (let x = 0; x < 1200; x += 60) for (let y = 0; y < 900; y += 60) { g.beginPath(); g.arc(x + 30, y + 30, 6, 0, 7); g.fill(); }
17
+ g.fillStyle = '#8a6a4f'; g.font = '900 210px "Mona Sans", system-ui, sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle';
18
+ g.fillText('REFRACT', 600, 330);
19
+ g.fillStyle = '#e8590c'; g.fillText('LIGHT', 600, 580);
20
+ g.strokeStyle = '#15171a'; g.lineWidth = 6; for (let i = 0; i < 8; i++) { g.beginPath(); g.moveTo(80 + i * 150, 760); g.lineTo(160 + i * 150, 840); g.stroke(); }
21
+
22
+ const vs = `#version 300 es
23
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
24
+ const fs = `#version 300 es
25
+ precision highp float; in vec2 uv; out vec4 o;
26
+ uniform sampler2D img; uniform vec2 R, M; uniform float IOR, DISP;
27
+ vec3 scene(vec2 st) {
28
+ vec2 a = vec2(R.x / R.y, 1.), cover = a.x > 4. / 3. ? vec2(1., (4. / 3.) / a.x) : vec2(a.x / (4. / 3.), 1.);
29
+ vec2 tc = (st - .5) * cover + .5; return texture(img, vec2(tc.x, 1. - tc.y)).rgb;
30
+ }
31
+ void main() {
32
+ vec2 a = vec2(R.x / R.y, 1.), q = uv * a, m = M * a;
33
+ float r = .3, d = length(q - m) / r;
34
+ vec3 col = scene(uv);
35
+ float shadow = smoothstep(1.25, .95, length(q - m - vec2(.02, -.03)) / r) * .18;
36
+ col *= 1. - shadow;
37
+ if (d < 1.) {
38
+ float h = sqrt(1. - d * d); // dome height
39
+ float mag = (IOR - 1.) * 1.1;
40
+ vec2 dir = (q - m);
41
+ vec3 c;
42
+ c.r = scene((m + dir * (1. - mag * (1. + DISP * 6.) * h)) / a).r;
43
+ c.g = scene((m + dir * (1. - mag * h)) / a).g;
44
+ c.b = scene((m + dir * (1. - mag * (1. - DISP * 6.) * h)) / a).b;
45
+ float rim = smoothstep(.82, 1., d);
46
+ float hi = pow(max(0., dot(normalize(vec3(dir / r, h)), normalize(vec3(-.5, .6, .8)))), 40.);
47
+ col = mix(c * 1.04, vec3(1.), rim * .35) + hi * .6;
48
+ }
49
+ o = vec4(col, 1);
50
+ }`;
51
+ 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; };
52
+ 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);
53
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
54
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
55
+ const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, pic);
56
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 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);
57
+ const u = n => gl.getUniformLocation(prog, n), uR = u('R'), uM = u('M'), uI = u('IOR'), uD = u('DISP');
58
+ const resize = () => { const d = Math.min(devicePixelRatio, 1.5); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
59
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
60
+ let mx = 0.5, my = 0.5, tx = 0.5, ty = 0.5, lastMove = -1e9, raf;
61
+ const onMove = e => { const b = canvas.getBoundingClientRect(); tx = (e.clientX - b.left) / b.width; ty = 1 - (e.clientY - b.top) / b.height; lastMove = performance.now(); };
62
+ canvas.addEventListener('pointermove', onMove);
63
+ function frame(now) {
64
+ const t = now / 1000;
65
+ if (now - lastMove > 1500) { tx = 0.5 + Math.cos(t * 0.6) * 0.28; ty = 0.5 + Math.sin(t * 0.9) * 0.2; }
66
+ mx += (tx - mx) * 0.1; my += (ty - my) * 0.1;
67
+ gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform2f(uM, mx, my); gl.uniform1f(uI, PARAMS.ior); gl.uniform1f(uD, PARAMS.dispersion);
68
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
69
+ raf = requestAnimationFrame(frame);
70
+ }
71
+ raf = requestAnimationFrame(frame);
72
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
73
+ }