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,95 @@
1
+ // Glass prism: a raymarched triangular prism in a striped studio. Rays refract in, travel through the glass,
2
+ // bounce off a face if they hit it too steeply (total internal reflection), and refract out. Red, green and
3
+ // blue each use a slightly different index of refraction, so every edge behind the glass splits into a rainbow.
4
+ const PARAMS = { dispersion: 1, ior: 1.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';
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, A, DS, IOR;
16
+ mat3 rot;
17
+ float prism(vec3 p) {
18
+ p = rot * p;
19
+ vec3 q = abs(p);
20
+ return max(q.z - .95, max(q.x * .866025 + p.y * .5, -p.y) - .42) - .02; // equilateral cross-section, softened edges
21
+ }
22
+ vec3 nrm(vec3 p) { vec2 k = vec2(.001, -.001); return normalize(k.xyy * prism(p + k.xyy) + k.yyx * prism(p + k.yyx) + k.yxy * prism(p + k.yxy) + k.xxx * prism(p + k.xxx)); }
23
+ vec3 spectrum(float x) { return clamp(vec3(abs(x * 6. - 3.) - 1., 2. - abs(x * 6. - 2.), 2. - abs(x * 6. - 4.)), 0., 1.); }
24
+ // the studio: a floor with the prism's rainbow cast across it, and a backdrop of bold stripes and a softbox
25
+ vec3 env(vec3 ro, vec3 d) {
26
+ if (d.y < 0.) {
27
+ vec3 p = ro + d * ((-1.05 - ro.y) / d.y);
28
+ vec3 c = vec3(.2, .2, .23) * (1. - smoothstep(4., 14., length(p.xz)) * .6);
29
+ vec2 dir = vec2(cos(A * 1.4 + .6), sin(A * 1.4 + .6)), rel = p.xz;
30
+ float along = dot(rel, dir), across = dot(rel, vec2(-dir.y, dir.x));
31
+ float fan = across / max(along, .01) * 2.2; // a fan of colour spreading away from the prism
32
+ float band = smoothstep(.0, .25, along - .6) * smoothstep(3.6, 2., along) * step(0., fan) * step(fan, 1.);
33
+ c += spectrum(fan) * band * 1.1;
34
+ c *= .6 + .4 * smoothstep(.2, 1.4, length(p.xz - vec2(.1, .25))); // soft contact shadow
35
+ return c;
36
+ }
37
+ float az = atan(d.x, d.z);
38
+ float stripes = smoothstep(.45, .55, abs(fract(az * 3.2 + .25) - .5) * 2.);
39
+ vec3 c = mix(vec3(.03, .03, .045), vec3(.85, .85, .88), stripes) * smoothstep(.95, .1, d.y) * smoothstep(-.02, .12, d.y);
40
+ c += vec3(1.) * 3. * smoothstep(.08, .02, abs(az + .55)) * smoothstep(.2, .3, d.y) * smoothstep(.8, .6, d.y); // tall softbox
41
+ return c + vec3(.08, .08, .1) * d.y;
42
+ }
43
+ vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
44
+ void main() {
45
+ vec2 q = (uv - .5) * vec2(R.x / R.y, 1.);
46
+ float ca = cos(A), sa = sin(A), cb = cos(.42), sb = sin(.42);
47
+ rot = mat3(1, 0, 0, 0, cb, -sb, 0, sb, cb) * mat3(ca, 0, sa, 0, 1, 0, -sa, 0, ca);
48
+ vec3 ro = vec3(0, .9, -5.6), f = normalize(vec3(0, -.15, 0) - ro), r = normalize(cross(vec3(0, 1, 0), f)), u = cross(f, r);
49
+ vec3 rd = normalize(f * 1.75 + r * q.x + u * q.y);
50
+ float t = 0.; bool hit = false;
51
+ for (int i = 0; i < 90; i++) { float d = prism(ro + rd * t); if (d < .0008) { hit = true; break; } t += d; if (t > 12.) break; }
52
+ vec3 col;
53
+ if (!hit) col = env(ro, rd);
54
+ else {
55
+ vec3 p = ro + rd * t, n = nrm(p);
56
+ float fr = .04 + .96 * pow(1. - max(0., dot(n, -rd)), 5.);
57
+ vec3 refr = vec3(0);
58
+ for (int c = 0; c < 3; c++) {
59
+ float ior = IOR + (float(c) - 1.) * .035 * DS;
60
+ vec3 d = refract(rd, n, 1. / ior), pp = p - n * .004;
61
+ for (int b = 0; b < 3; b++) { // up to two bounces inside the glass
62
+ float s = 0.;
63
+ for (int i = 0; i < 40; i++) { float dd = -prism(pp + d * s); if (dd < .0008) break; s += max(dd, .002); }
64
+ pp += d * s; vec3 n2 = nrm(pp);
65
+ vec3 out_ = refract(d, -n2, ior);
66
+ if (dot(out_, out_) > 0. || b == 2) { d = dot(out_, out_) > 0. ? out_ : reflect(d, -n2); pp += n2 * .006; break; }
67
+ d = reflect(d, -n2); pp -= n2 * .004; // total internal reflection
68
+ }
69
+ refr[c] = env(pp, d)[c];
70
+ }
71
+ refr *= vec3(.94, .97, 1.);
72
+ col = mix(refr, env(p, reflect(rd, n)), fr);
73
+ col += pow(1. - abs(dot(n, rd)), 12.) * .4; // bright rim where the faces turn edge-on
74
+ }
75
+ col = aces(col * .95);
76
+ col *= 1. - .25 * pow(length(uv - .5) * 1.3, 2.);
77
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
78
+ }`;
79
+ 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; };
80
+ 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);
81
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
82
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
83
+ const U = {}; ['R', 'T', 'A', 'DS', 'IOR'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
84
+ const resize = () => { const d = Math.min(devicePixelRatio, 1.25); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
85
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
86
+ let raf, ang = 0.6, last = performance.now(); const t0 = last;
87
+ function frame(now) {
88
+ const dt = Math.min(0.05, (now - last) / 1000); last = now; ang += dt * 0.35 * PARAMS.spin;
89
+ gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, (now - t0) / 1000); gl.uniform1f(U.A, ang);
90
+ gl.uniform1f(U.DS, PARAMS.dispersion); gl.uniform1f(U.IOR, PARAMS.ior);
91
+ gl.drawArrays(gl.TRIANGLES, 0, 3); raf = requestAnimationFrame(frame);
92
+ }
93
+ raf = requestAnimationFrame(frame);
94
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
95
+ }
@@ -0,0 +1,69 @@
1
+ // Gyroid fly-through: the gyroid, sin x cos y + sin y cos z + sin z cos x = 0, is a surface that splits space into
2
+ // two interwoven labyrinths. Thickened into a sheet, it makes endless organic walls. A tube is carved along the camera's
3
+ // winding path so the flight never hits a wall. The walls are glossy and lit by a headlight and two coloured fills, the
4
+ // colour shifts with depth, and fog swallows the distance.
5
+ const PARAMS = { speed: 1, scale: 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, Z, K;
18
+ vec2 path(float z) { return vec2(sin(z * .31) * 1.4 + sin(z * .13) * 1.1, cos(z * .23) * 1.2); }
19
+ float gyroid(vec3 p) { return dot(sin(p), cos(p.yzx)); }
20
+ float map(vec3 p) {
21
+ float s = 1.6 * K;
22
+ float g = abs(gyroid(p * s)) / s - .12 / K; // a thickened sheet
23
+ g = max(g, .55 - length(p.xy - path(p.z))); // carve the flight tube
24
+ return g * .7; // the gyroid isn't an exact distance: step carefully
25
+ }
26
+ vec3 nrm(vec3 p) { vec2 e = vec2(.002, -.002); return normalize(e.xyy * map(p + e.xyy) + e.yyx * map(p + e.yyx) + e.yxy * map(p + e.yxy) + e.xxx * map(p + e.xxx)); }
27
+ vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
28
+ void main() {
29
+ vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
30
+ vec3 ro = vec3(path(Z), Z), ta = vec3(path(Z + 1.5), Z + 1.5);
31
+ vec3 f = normalize(ta - ro), r = normalize(cross(f, vec3(sin(Z * .1) * .3, 1, 0))), u = cross(r, f);
32
+ vec3 rd = normalize(f * 1.3 + r * q.x + u * q.y);
33
+ float t = 0.; bool hit = false;
34
+ for (int i = 0; i < 140; i++) { float d = map(ro + rd * t); if (d < .001 * (1. + t)) { hit = true; break; } t += d; if (t > 14.) break; }
35
+ vec3 fog = mix(vec3(.02, .03, .06), vec3(.10, .05, .14), uv.y);
36
+ vec3 col = fog;
37
+ if (hit) {
38
+ vec3 p = ro + rd * t, n = nrm(p);
39
+ float head = max(dot(n, -rd), 0.);
40
+ vec3 base = .5 + .5 * cos(6.28 * (p.z * .045 + vec3(0, .33, .67)) + 1.); // hue drifts along the tunnel
41
+ float ao = clamp(map(p + n * .15) / .15, 0., 1.);
42
+ vec3 l1 = normalize(vec3(1, 1, -.5)), l2 = normalize(vec3(-1, -.4, .3));
43
+ col = base * (.15 + .9 * head * head) * (.4 + .6 * ao);
44
+ col += vec3(1., .5, .25) * max(dot(n, l1), 0.) * .35 * ao + vec3(.2, .5, 1.) * max(dot(n, l2), 0.) * .35 * ao;
45
+ col += pow(max(dot(reflect(rd, n), -rd), 0.), 30.) * .7; // the headlight's glint
46
+ col = mix(col, fog, 1. - exp(-t * t * .025));
47
+ }
48
+ col = aces(col * 1.2);
49
+ col *= 1. - .35 * pow(length(uv - .5) * 1.3, 2.);
50
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
51
+ }`;
52
+ const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x); if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(x)); return x; };
53
+ const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, fs)); gl.bindAttribLocation(prog, 0, 'p'); gl.linkProgram(prog); gl.useProgram(prog);
54
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
55
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
56
+ const U = {}; ['R', 'T', 'Z', 'K'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
57
+ 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); };
58
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
59
+ let raf, last = performance.now(), z = 0;
60
+ function frame(now) {
61
+ const dt = Math.min(0.05, (now - last) / 1000); last = now;
62
+ z += dt * 1.1 * PARAMS.speed;
63
+ gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, now / 1000); gl.uniform1f(U.Z, z); gl.uniform1f(U.K, PARAMS.scale);
64
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
65
+ raf = requestAnimationFrame(frame);
66
+ }
67
+ raf = requestAnimationFrame(frame);
68
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
69
+ }
@@ -0,0 +1,52 @@
1
+ // Holographic card: a card tilts toward the pointer on a spring, in real CSS 3D. A rainbow foil layer slides
2
+ // with the tilt (color-dodge blending, masked to a diagonal pattern), and a glare follows the light. With no
3
+ // pointer it sways on its own. The variables live on the card itself, so only the card restyles.
4
+ const PARAMS = { tilt: 18, foil: 1, stiffness: 170 };
5
+
6
+ export default function (stage) {
7
+ const root = document.createElement('div');
8
+ root.style.cssText = 'position:absolute;inset:0;display:grid;place-items:center;perspective:900px;background:radial-gradient(120% 90% at 50% 20%,#262b3a,#0c0e14);overflow:hidden;touch-action:none';
9
+ root.innerHTML = `
10
+ <style>
11
+ .hc{position:relative;width:min(56%,calc(78cqh*.72));aspect-ratio:5/7;border-radius:4.5%/3.2%;transform-style:preserve-3d;
12
+ transform:rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));box-shadow:0 30px 60px -20px rgba(0,0,0,.7),0 0 0 1px rgba(255,255,255,.08) inset;
13
+ background:linear-gradient(160deg,#1d2337,#121522 60%,#1b1530);overflow:hidden;container-type:size}
14
+ .hc-art{position:absolute;inset:7% 7% 34%;border-radius:3%;background:radial-gradient(circle at 50% 45%,#ffd38a 0 9%,transparent 10%),
15
+ repeating-conic-gradient(from 0deg at 50% 45%,#2c3a6b 0 10deg,#1a2143 10deg 20deg);overflow:hidden;box-shadow:0 0 0 1px rgba(255,255,255,.12)}
16
+ .hc-art::before,.hc-art::after{content:"";position:absolute;left:50%;top:45%;border:2px solid rgba(255,220,160,.8);border-radius:50%;translate:-50% -50%}
17
+ .hc-art::before{width:46%;aspect-ratio:1}.hc-art::after{width:70%;aspect-ratio:1;border-style:dashed;border-color:rgba(160,200,255,.6);rotate:var(--spin,0deg)}
18
+ .hc-title{position:absolute;left:8%;right:8%;bottom:19%;font:800 9cqw/1 "Mona Sans",system-ui,sans-serif;font-stretch:115%;letter-spacing:-.02em;color:#f4f1ff}
19
+ .hc-sub{position:absolute;left:8%;right:8%;bottom:9%;font:500 4.2cqw/1.3 "Mona Sans",system-ui,sans-serif;color:rgba(230,228,255,.6);display:flex;justify-content:space-between}
20
+ .hc-foil{position:absolute;inset:0;mix-blend-mode:color-dodge;opacity:calc(.55*var(--foil,1));
21
+ background:repeating-linear-gradient(115deg,#ff6b9d 0%,#ffd36b 6%,#7dffb3 12%,#6bc8ff 18%,#b07dff 24%,#ff6b9d 30%);background-size:220% 220%;
22
+ background-position:var(--fx,50%) var(--fy,50%);
23
+ -webkit-mask:repeating-linear-gradient(135deg,#000 0 2px,rgba(0,0,0,.35) 2px 5px),radial-gradient(circle at var(--gx,50%) var(--gy,50%),#000,transparent 75%);
24
+ -webkit-mask-composite:source-in;mask-composite:intersect}
25
+ .hc-glare{position:absolute;inset:0;mix-blend-mode:overlay;background:radial-gradient(circle at var(--gx,50%) var(--gy,50%),rgba(255,255,255,.75),rgba(255,255,255,.15) 30%,rgba(0,0,0,.35) 75%)}
26
+ .hc-edge{position:absolute;inset:0;border-radius:inherit;box-shadow:0 0 0 1.5px rgba(255,255,255,.22) inset}
27
+ </style>
28
+ <div class="hc"><div class="hc-art"></div><div class="hc-title">Motion Card</div><div class="hc-sub"><span>No. 027</span><span>Holo foil</span></div><div class="hc-foil"></div><div class="hc-glare"></div><div class="hc-edge"></div></div>`;
29
+ stage.append(root);
30
+ const card = root.querySelector('.hc');
31
+ let tx = 0, ty = 0, rx = 0, ry = 0, vx = 0, vy = 0, lastMove = -1e9;
32
+ const onMove = e => { const b = card.getBoundingClientRect(); tx = Math.max(-1, Math.min(1, ((e.clientX - b.left) / b.width) * 2 - 1)); ty = Math.max(-1, Math.min(1, ((e.clientY - b.top) / b.height) * 2 - 1)); lastMove = performance.now(); };
33
+ const onLeave = () => { lastMove = -1e9; };
34
+ root.addEventListener('pointermove', onMove); root.addEventListener('pointerleave', onLeave);
35
+ let raf, last = performance.now(); const t0 = last;
36
+ function frame(now) {
37
+ const dt = Math.min(0.033, (now - last) / 1000); last = now; const t = (now - t0) / 1000;
38
+ let gx = tx, gy = ty;
39
+ if (now - lastMove > 1200) { gx = Math.sin(t * 0.9) * 0.8; gy = Math.sin(t * 1.3 + 1) * 0.5; } // idle: a slow figure-of-eight
40
+ // spring toward the target tilt (stiffness from PARAMS, damping fixed near critical)
41
+ const k = PARAMS.stiffness, c = 2 * Math.sqrt(k) * 0.75;
42
+ vx += ((gx - rx) * k - vx * c) * dt; vy += ((gy - ry) * k - vy * c) * dt; rx += vx * dt; ry += vy * dt;
43
+ const s = card.style;
44
+ s.setProperty('--ry', (rx * PARAMS.tilt).toFixed(2) + 'deg'); s.setProperty('--rx', (-ry * PARAMS.tilt).toFixed(2) + 'deg');
45
+ s.setProperty('--gx', (50 + rx * 45).toFixed(1) + '%'); s.setProperty('--gy', (50 + ry * 45).toFixed(1) + '%');
46
+ s.setProperty('--fx', (50 + rx * 60).toFixed(1) + '%'); s.setProperty('--fy', (50 + ry * 60).toFixed(1) + '%');
47
+ s.setProperty('--foil', PARAMS.foil); s.setProperty('--spin', (t * 12 % 360).toFixed(1) + 'deg');
48
+ raf = requestAnimationFrame(frame);
49
+ }
50
+ raf = requestAnimationFrame(frame);
51
+ return () => { cancelAnimationFrame(raf); root.removeEventListener('pointermove', onMove); root.removeEventListener('pointerleave', onLeave); root.remove(); };
52
+ }
@@ -0,0 +1,81 @@
1
+ // Infinite tunnel: the camera flies down a winding tube lined with bevelled metal panels. The tube is a
2
+ // distance field around a curved path, the panels are cut from its angle and depth, and light pulses race
3
+ // along the seams. Coloured lights travel with you, so the panels glint as they pass.
4
+ const PARAMS = { speed: 1, twist: 1, glow: 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, Z, TW, G;
16
+ vec2 path(float z) { return vec2(sin(z * .13) * 2.2 + sin(z * .051) * 1.5, cos(z * .09) * 1.6); }
17
+ // panel coordinates on the wall: 14 around, staggered rows along the tube, twisting slowly with depth
18
+ vec3 panel(vec3 p) {
19
+ vec2 q = p.xy - path(p.z);
20
+ float a = atan(q.y, q.x) / 6.2832 + p.z * .012 * TW, v = p.z * .9;
21
+ float row = floor(v), u = a * 14. + mod(row, 2.) * .5;
22
+ vec2 f = vec2(fract(u), fract(v));
23
+ float e = min(min(f.x, 1. - f.x) / 14. * 6.2832 * 1.5, min(f.y, 1. - f.y) / .9); // distance to the panel's edge
24
+ return vec3(e, floor(u), row);
25
+ }
26
+ float map(vec3 p) {
27
+ vec2 q = p.xy - path(p.z);
28
+ float e = panel(p).x;
29
+ return 1.5 - length(q) - smoothstep(0., .07, e) * .045; // panels stand proud of the seams
30
+ }
31
+ vec3 nrm(vec3 p) { vec2 k = vec2(.003, -.003); return normalize(k.xyy * map(p + k.xyy) + k.yyx * map(p + k.yyx) + k.yxy * map(p + k.yxy) + k.xxx * map(p + k.xxx)); }
32
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(41.7, 289.3))) * 43758.5); }
33
+ vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
34
+ void main() {
35
+ vec2 q = (uv - .5) * vec2(R.x / R.y, 1.);
36
+ vec3 ro = vec3(path(Z), Z), ta = vec3(path(Z + 2.5), Z + 2.5);
37
+ vec3 f = normalize(ta - ro), up = vec3(sin(Z * .05) * .3, 1, 0), r = normalize(cross(up, f)), u = cross(f, r);
38
+ vec3 rd = normalize(f * 1.2 + r * q.x + u * q.y);
39
+ float t = 0.; vec3 p = ro;
40
+ for (int i = 0; i < 90; i++) { p = ro + rd * t; float d = map(p); if (d < .002) break; t += d * .9; if (t > 40.) break; }
41
+ vec3 n = nrm(p), pa = panel(p);
42
+ float id = hash(pa.yz);
43
+ vec3 base = mix(vec3(.05, .055, .07), vec3(.12, .12, .14), id);
44
+ vec3 col = base * .2;
45
+ // three lights that ride along ahead of the camera
46
+ for (int i = 0; i < 3; i++) {
47
+ float lz = Z + 2. + float(i) * 5.5;
48
+ vec3 lp = vec3(path(lz) + vec2(sin(T + float(i) * 2.) * .6, cos(T * .8 + float(i)) * .5), lz);
49
+ vec3 lc = i == 0 ? vec3(1., .25, .6) : i == 1 ? vec3(.2, .8, 1.) : vec3(1., .65, .25);
50
+ vec3 L = lp - p; float dl = length(L); L /= dl;
51
+ float att = 1. / (1. + dl * dl * .35);
52
+ col += lc * att * (base * max(0., dot(n, L)) * 3. + pow(max(0., dot(reflect(rd, n), L)), 40.) * (1.5 + id * 2.));
53
+ }
54
+ // seams glow, with pulses racing down them
55
+ float seam = 1. - smoothstep(0., .035, pa.x);
56
+ float pulse = pow(.5 + .5 * sin(p.z * .7 - T * 6. + pa.y * 1.3), 10.);
57
+ vec3 neon = mix(vec3(1., .2, .55), vec3(.2, .75, 1.), .5 + .5 * sin(pa.y * .9 + p.z * .05));
58
+ col += neon * seam * (.25 + 2.2 * pulse) * G;
59
+ col = mix(col, vec3(.01, .008, .02), 1. - exp(-t * .09)); // fog into the distance
60
+ col += neon * .08 * G / (1. + t * .3) * exp(-length(q) * 3.);
61
+ col = aces(col * 1.2);
62
+ col *= 1. - .35 * pow(length(uv - .5) * 1.25, 2.);
63
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
64
+ }`;
65
+ 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; };
66
+ 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);
67
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
68
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
69
+ const U = {}; ['R', 'T', 'Z', 'TW', 'G'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
70
+ const resize = () => { const d = Math.min(devicePixelRatio, 1.25); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
71
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
72
+ let raf, z = 0, last = performance.now(); const t0 = last;
73
+ function frame(now) {
74
+ const dt = Math.min(0.05, (now - last) / 1000); last = now; z += dt * 3.2 * PARAMS.speed; // distance travelled, so the speed slider never jumps
75
+ gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, (now - t0) / 1000); gl.uniform1f(U.Z, z);
76
+ gl.uniform1f(U.TW, PARAMS.twist); gl.uniform1f(U.G, PARAMS.glow);
77
+ gl.drawArrays(gl.TRIANGLES, 0, 3); raf = requestAnimationFrame(frame);
78
+ }
79
+ raf = requestAnimationFrame(frame);
80
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
81
+ }
@@ -0,0 +1,141 @@
1
+ // Inflated type: a word blown up like a foil balloon, then let down again. The letters are drawn once to a canvas,
2
+ // and a distance transform turns that into a field of distances to the nearest edge. Raymarched in 3D, each point
3
+ // is inside the balloon if it is inside a letter and closer to the letter's mid-plane than a thickness that grows
4
+ // with the square root of how deep into the letter it is: thin at the seams, fat in the middle, the way a sealed foil
5
+ // balloon puffs. While it is soft the seams crinkle. The foil is holographic: its colour shifts with the angle.
6
+ const PARAMS = { inflate: 1, wobble: 1, sheen: 1 };
7
+ const WORDS = ['PUFF', 'BLOB', 'AIR'];
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
+ // signed distance to the letters, in pixels (positive outside), by an exact Euclidean distance transform
17
+ function field(word) {
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", "Arial Rounded MT Bold", 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), out = 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++; out[q] = (q - v[k]) ** 2 + tmp[v[k]]; set(q, out[q]); }
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); // depth inside a letter; distance from one outside
36
+ // the balloon's height: a membrane under pressure (solve ∇²h = −p inside the letters, h = 0 at the seams) on a
37
+ // half-size grid. Its square root is round across every stroke, where the plain depth would leave a sharp ridge.
38
+ const hw = TW >> 1, hh = TH >> 1, inside = new Uint8Array(hw * hh); let m = new Float32Array(hw * hh), m2 = new Float32Array(hw * hh);
39
+ for (let y = 0; y < hh; y++) for (let x = 0; x < hw; x++) { const i = (y * 2) * TW + x * 2; inside[y * hw + x] = px[i * 4 + 3] > 127 ? 1 : 0; m[y * hw + x] = inside[y * hw + x] ? Math.sqrt(depth[i]) * 0.5 : 0; }
40
+ for (let it = 0; it < 220; it++) {
41
+ for (let y = 1; y < hh - 1; y++) for (let x = 1; x < hw - 1; x++) { const i = y * hw + x; m2[i] = inside[i] ? (m[i - 1] + m[i + 1] + m[i - hw] + m[i + hw]) * 0.25 + 0.25 : 0; }
42
+ [m, m2] = [m2, m];
43
+ }
44
+ const d = new Float32Array(TW * TH * 2);
45
+ for (let i = 0; i < TW * TH; i++) {
46
+ const row = (i / TW) | 0, col = i % TW, o = ((TH - 1 - row) * TW + col) * 2; // rows flipped: textures start at the bottom
47
+ const fx = Math.min(hw - 1.001, col / 2), fy = Math.min(hh - 1.001, row / 2), x0 = fx | 0, y0 = fy | 0, ax = fx - x0, ay = fy - y0;
48
+ const hv = m[y0 * hw + x0] * (1 - ax) * (1 - ay) + m[y0 * hw + x0 + 1] * ax * (1 - ay) + m[(y0 + 1) * hw + x0] * (1 - ax) * ay + m[(y0 + 1) * hw + x0 + 1] * ax * ay;
49
+ d[o] = Math.sqrt(away[i]) - Math.sqrt(depth[i]); d[o + 1] = Math.sqrt(Math.max(0, hv)) * 2; // distance; height in full-size pixels
50
+ }
51
+ return d;
52
+ }
53
+ const QUAD = `#version 300 es
54
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
55
+ const FS = `#version 300 es
56
+ precision highp float; in vec2 uv; out vec4 o;
57
+ uniform sampler2D F; uniform vec2 R; uniform float T, INF, WOB, SHEEN, MIX;
58
+ const float SX = 5., SY = 5. * ${TH}. / ${TW}., PX = 5. / ${TW}.;
59
+ float h(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
60
+ float n3(vec3 p) { vec3 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f); vec2 q = i.xy + i.z * 37.;
61
+ return mix(mix(mix(h(q), h(q + vec2(1, 0)), f.x), mix(h(q + vec2(0, 1)), h(q + 1.), f.x), f.y), mix(mix(h(q + 37.), h(q + vec2(38, 37)), f.x), mix(h(q + vec2(37, 38)), h(q + 38.), f.x), f.y), f.z); }
62
+ float sd2(vec2 p) { // distance to the letters (outside the canvas: to its box)
63
+ vec2 t = vec2(p.x / SX + .5, p.y / SY + .5);
64
+ vec2 b = abs(p) - vec2(SX, SY) * .5;
65
+ if (max(b.x, b.y) > 0.) return length(max(b, 0.)) + 1.;
66
+ return texture(F, t).r * PX;
67
+ }
68
+ float puff(vec2 p) { return texture(F, vec2(p.x / SX + .5, p.y / SY + .5)).g * PX; }
69
+ mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
70
+ vec3 tf(vec3 p) { p.xz *= rot(.32 * sin(T * .45) * WOB); p.y -= .08 * sin(T * 1.1) * WOB; return p; }
71
+ float map(vec3 p) {
72
+ p = tf(p);
73
+ float d = sd2(p.xy), depth = max(-d, 0.);
74
+ float thick = INF * .55 * puff(p.xy) + .004; // a balloon: the membrane's height, round across each stroke
75
+ float crinkle = (1. - INF) * .035 * n3(p * 9.) * smoothstep(.12, .0, depth); // soft foil crinkles near the seams
76
+ return max(d, abs(p.z) - thick + crinkle) * .6;
77
+ }
78
+ vec3 nrm(vec3 p) { vec2 e = vec2(.004, 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))); }
79
+ vec3 env(vec3 d) { // a bright pastel studio
80
+ vec3 c = mix(vec3(.95, .86, .86), vec3(.78, .82, .98), smoothstep(-.3, .8, d.y));
81
+ c += vec3(1.6) * smoothstep(.75, .95, dot(d, normalize(vec3(-.4, .8, .45))));
82
+ c += vec3(1.2, 1.05, 1.2) * smoothstep(.85, .97, dot(d, normalize(vec3(.8, .3, .5))));
83
+ c *= .75 + .25 * smoothstep(-.6, .2, d.y);
84
+ return c;
85
+ }
86
+ void main() {
87
+ vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
88
+ vec3 ro = vec3(0, .2, 4.6), rd = normalize(vec3(q, -2.35)); rd.yz *= rot(-.05);
89
+ // the backdrop: a pastel sweep with a soft shadow under the word
90
+ vec3 col = mix(vec3(.98, .9, .88), vec3(.86, .87, 1.), uv.y);
91
+ float tf0 = (-1.15 - ro.y) / rd.y;
92
+ if (tf0 > 0.) { vec3 fp = ro + rd * tf0; col *= 1. - .22 * INF * exp(-dot(fp.xz * vec2(.3, 1.6), fp.xz * vec2(.3, 1.6))); }
93
+ float t = 0.; bool hit = false;
94
+ for (int i = 0; i < 110; i++) { float d = map(ro + rd * t); if (d < .0015) { hit = true; break; } t += d; if (t > 12.) break; }
95
+ if (hit) {
96
+ vec3 p = ro + rd * t, n = nrm(p), r = reflect(rd, n);
97
+ float fr = pow(1. - max(dot(n, -rd), 0.), 3.);
98
+ // holographic foil: thin-film colour that shifts with the viewing angle
99
+ vec3 film = .5 + .5 * cos(6.2831 * (fr * 1.4 + p.x * .08 + vec3(0., .33, .67) + MIX * .2));
100
+ vec3 base = mix(vec3(1., .78, .9), vec3(.78, .86, 1.), uv.x);
101
+ vec3 foil = mix(base, film, .45 * SHEEN);
102
+ float dif = max(dot(n, normalize(vec3(-.4, .8, .45))), 0.);
103
+ col = foil * (.3 + .6 * dif) + env(r) * (.35 + .65 * fr) * mix(vec3(1), foil, .5);
104
+ col += vec3(1.) * pow(max(dot(r, normalize(vec3(-.4, .8, .45))), 0.), 40.) * 1.6 + vec3(1.) * pow(max(dot(r, normalize(vec3(.8, .3, .5))), 0.), 30.) * .7;
105
+ }
106
+ o = vec4(pow(col / (1. + col * .18), vec3(.92)), 1);
107
+ }`;
108
+ 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; };
109
+ const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, QUAD)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, FS)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p);
110
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
111
+ gl.useProgram(p);
112
+ const U = n => gl.getUniformLocation(p, n);
113
+ gl.bindVertexArray(gl.createVertexArray());
114
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
115
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
116
+ const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex);
117
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
118
+ 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);
119
+ const fields = WORDS.map(field);
120
+ const load = i => { gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG16F, TW, TH, 0, gl.RG, gl.FLOAT, fields[i]); };
121
+ let word = 0; load(0);
122
+ let W = 1, H = 1;
123
+ const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, 2, Math.max(0.6, Math.sqrt(520000 / (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); };
124
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
125
+ let raf, last = performance.now(), t = 0, cyc = 0;
126
+ function frame(now) {
127
+ const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt; cyc += dt;
128
+ // blow up with a springy overshoot, hold, let down, swap the word
129
+ let inf;
130
+ if (cyc < 2.2) { const k = cyc / 2.2; inf = 1 - Math.exp(-5 * k) * Math.cos(9 * k); }
131
+ else if (cyc < 6.5) inf = 1 + 0.015 * Math.sin(cyc * 3);
132
+ else if (cyc < 8) inf = 1 - (cyc - 6.5) / 1.5 * 0.92;
133
+ else { cyc = 0; word = (word + 1) % WORDS.length; load(word); inf = 0.08; }
134
+ gl.uniform1i(U('F'), 0); gl.uniform2f(U('R'), W, H); gl.uniform1f(U('T'), t); gl.uniform1f(U('INF'), Math.max(0.06, inf * PARAMS.inflate));
135
+ gl.uniform1f(U('WOB'), PARAMS.wobble); gl.uniform1f(U('SHEEN'), PARAMS.sheen); gl.uniform1f(U('MIX'), word);
136
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
137
+ raf = requestAnimationFrame(frame);
138
+ }
139
+ raf = requestAnimationFrame(frame);
140
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
141
+ }
@@ -0,0 +1,97 @@
1
+ // Isometric city: a grid of blocks drawn back to front (painter's order by row + column), each building a box
2
+ // with three visible faces. Buildings rise in on springs, cars drive the road grid, and a day–night cycle
3
+ // dims the sky and lights the windows. Every so often a block is demolished and rebuilt taller.
4
+ const PARAMS = { size: 12, cycle: 16, traffic: 14 };
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;
12
+ 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); };
13
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
14
+
15
+ const N = Math.round(PARAMS.size), road = (i) => i % 4 === 0;
16
+ const tiles = [];
17
+ const palette = [['#e9e4da', '#c9c1b3', '#a99f8f'], ['#d8e2ea', '#aebccb', '#8797aa'], ['#f0d9c4', '#d4b49a', '#b38f74'], ['#dfe8d9', '#b7c7ab', '#93a585']];
18
+ for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) {
19
+ const isRoad = road(i) || road(j), park = !isRoad && Math.random() < 0.1;
20
+ const centre = 1 - Math.hypot(i - N / 2, j - N / 2) / (N * 0.75);
21
+ tiles.push({ i, j, isRoad, park, target: isRoad || park ? 0 : 0.6 + Math.random() * 2.4 * Math.max(0.2, centre) + (Math.random() < 0.08 ? 2.5 : 0), h: 0, v: 0, delay: (i + j) * 0.05 + Math.random() * 0.2, col: palette[Math.random() * 4 | 0], seed: Math.random() * 1000 });
22
+ }
23
+ const cars = Array.from({ length: Math.round(PARAMS.traffic) }, () => { const alongI = Math.random() < 0.5, lane = Math.floor(Math.random() * (N / 4)) * 4; return { alongI, lane, s: Math.random() * N, v: (0.9 + Math.random() * 0.8) * (Math.random() < 0.5 ? 1 : -1), col: ['#e5484d', '#f5d90a', '#3e63dd', '#ffffff', '#30a46c'][Math.random() * 5 | 0] }; });
24
+ const hash = (a, b) => { const s = Math.sin(a * 127.1 + b * 311.7) * 43758.5; return s - Math.floor(s); };
25
+
26
+ let raf, last = performance.now(), nextRebuild = 5; const t0 = last;
27
+ function frame(now) {
28
+ const dt = Math.min(0.05, (now - last) / 1000); last = now; const t = (now - t0) / 1000;
29
+ const tw = Math.min(W / (N + 1), H * 1.6 / (N + 1)), th = tw / 2, unit = th * 1.1;
30
+ const ox = W / 2, oy = H / 2 - (N * th) / 2 + th * 1.2;
31
+ const P = (x, y, z) => [ox + (x - y) * tw / 2, oy + (x + y) * th / 2 - z * unit];
32
+ // day and night: 0 = noon, 0.5 = midnight
33
+ const ph = (t / PARAMS.cycle) % 1, night = Math.max(0, Math.min(1, (Math.cos(ph * 6.283) * -1 + 0.2) * 1.4));
34
+ const sky = ctx.createLinearGradient(0, 0, 0, H);
35
+ sky.addColorStop(0, `rgb(${lerp3([168, 210, 240], [12, 16, 38], night)})`); sky.addColorStop(1, `rgb(${lerp3([236, 228, 214], [34, 30, 58], night)})`);
36
+ ctx.fillStyle = sky; ctx.fillRect(0, 0, W, H);
37
+ // springs: each building rises toward its target height after its delay; occasionally one is rebuilt
38
+ if (t > nextRebuild) {
39
+ const pick = tiles.filter(x => x.target > 0), k = pick[Math.random() * pick.length | 0];
40
+ if (k) { k.target = 0; k.rebuild = t + 1.2; }
41
+ nextRebuild = t + 3 + Math.random() * 3;
42
+ }
43
+ for (const tl of tiles) {
44
+ if (tl.rebuild && t > tl.rebuild) { tl.target = 1 + Math.random() * 3.5; tl.rebuild = 0; tl.col = palette[Math.random() * 4 | 0]; tl.seed = Math.random() * 1000; }
45
+ if (t < tl.delay) continue;
46
+ tl.v += ((tl.target - tl.h) * 120 - tl.v * 14) * dt; tl.h += tl.v * dt; if (tl.h < 0) { tl.h = 0; tl.v = 0; }
47
+ }
48
+ const carsOn = new Map();
49
+ for (const c of cars) {
50
+ c.s = (c.s + c.v * dt * 1.2 + N) % N;
51
+ const i = c.alongI ? Math.floor(c.s) : c.lane, j = c.alongI ? c.lane : Math.floor(c.s), key = j * N + i;
52
+ if (!carsOn.has(key)) carsOn.set(key, []); carsOn.get(key).push(c);
53
+ }
54
+ const quad = (pts, fill) => { ctx.fillStyle = fill; ctx.beginPath(); pts.forEach(([x, y], k) => k ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.closePath(); ctx.fill(); };
55
+ const shade = (hex, k) => { const n = parseInt(hex.slice(1), 16); return `rgb(${[(n >> 16) & 255, (n >> 8) & 255, n & 255].map(v => Math.round(v * k)).join(',')})`; };
56
+ const dim = 1 - night * 0.62;
57
+ // painter's order: rows and columns summed, back to front
58
+ const order = [...tiles].sort((a, b) => (a.i + a.j) - (b.i + b.j));
59
+ for (const tl of order) {
60
+ const { i, j } = tl;
61
+ quad([P(i, j, 0), P(i + 1, j, 0), P(i + 1, j + 1, 0), P(i, j + 1, 0)], tl.isRoad ? shade('#5b5f68', dim) : tl.park ? shade('#7fb069', dim) : shade('#cfc8bb', dim));
62
+ if (tl.isRoad && ((road(i) && !road(j)) || (road(j) && !road(i)))) { // lane dashes
63
+ ctx.strokeStyle = `rgba(255,240,200,${0.5 * dim + 0.2})`; ctx.lineWidth = 1; ctx.beginPath();
64
+ const a = road(i) ? P(i + 0.5, j + 0.2, 0) : P(i + 0.2, j + 0.5, 0), b = road(i) ? P(i + 0.5, j + 0.8, 0) : P(i + 0.8, j + 0.5, 0);
65
+ ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
66
+ }
67
+ if (tl.park) { const c = P(i + 0.5, j + 0.5, 0); ctx.fillStyle = shade('#3f7f3a', dim); ctx.beginPath(); ctx.ellipse(c[0], c[1] - unit * 0.35, tw * 0.16, tw * 0.2, 0, 0, 7); ctx.fill(); }
68
+ for (const c of carsOn.get(j * N + i) || []) {
69
+ const f = c.s % 1, x = c.alongI ? i + f : i + 0.5 + (c.v > 0 ? 0.18 : -0.18), y = c.alongI ? j + 0.5 + (c.v > 0 ? -0.18 : 0.18) : j + f;
70
+ const l = 0.22, w = 0.13, hx = c.alongI ? l : w, hy = c.alongI ? w : l, z = 0.18;
71
+ quad([P(x - hx, y - hy, z), P(x + hx, y - hy, z), P(x + hx, y + hy, z), P(x - hx, y + hy, z)], shade(c.col, 0.6 + 0.4 * dim));
72
+ quad([P(x + hx, y - hy, 0), P(x + hx, y + hy, 0), P(x + hx, y + hy, z), P(x + hx, y - hy, z)], shade(c.col, 0.45 * dim + 0.15));
73
+ quad([P(x - hx, y + hy, 0), P(x + hx, y + hy, 0), P(x + hx, y + hy, z), P(x - hx, y + hy, z)], shade(c.col, 0.6 * dim + 0.15));
74
+ if (night > 0.3) { const hl = P(x + (c.alongI ? Math.sign(c.v) * (l + 0.1) : 0), y + (c.alongI ? 0 : Math.sign(c.v) * (l + 0.1)), 0.05); ctx.fillStyle = `rgba(255,236,170,${night * 0.6})`; ctx.beginPath(); ctx.arc(hl[0], hl[1], tw * 0.08, 0, 7); ctx.fill(); }
75
+ }
76
+ if (tl.h > 0.02) {
77
+ const h = tl.h, m = 0.1, x0 = i + m, x1 = i + 1 - m, y0 = j + m, y1 = j + 1 - m;
78
+ quad([P(x1, y0, 0), P(x1, y1, 0), P(x1, y1, h), P(x1, y0, h)], shade(tl.col[1], dim)); // right face
79
+ quad([P(x0, y1, 0), P(x1, y1, 0), P(x1, y1, h), P(x0, y1, h)], shade(tl.col[2], dim)); // left face
80
+ quad([P(x0, y0, h), P(x1, y0, h), P(x1, y1, h), P(x0, y1, h)], shade(tl.col[0], dim)); // roof
81
+ // windows: lit at night by a per-window hash, so the pattern holds still instead of flickering
82
+ const floors = Math.floor(h / 0.32);
83
+ for (let f = 0; f < floors; f++) for (let k = 0; k < 3; k++) {
84
+ const z = 0.12 + f * 0.32, on = hash(tl.seed + f, k) < 0.55 && night > hash(k, tl.seed + f) * 0.8;
85
+ const col = on ? `rgba(255,214,130,${0.6 + 0.4 * night})` : `rgba(40,55,80,${0.35 + 0.25 * night})`;
86
+ const u0 = 0.18 + k * 0.24, u1 = u0 + 0.14;
87
+ quad([P(x1, y0 + (y1 - y0) * u0, z), P(x1, y0 + (y1 - y0) * u1, z), P(x1, y0 + (y1 - y0) * u1, z + 0.17), P(x1, y0 + (y1 - y0) * u0, z + 0.17)], col);
88
+ quad([P(x0 + (x1 - x0) * u0, y1, z), P(x0 + (x1 - x0) * u1, y1, z), P(x0 + (x1 - x0) * u1, y1, z + 0.17), P(x0 + (x1 - x0) * u0, y1, z + 0.17)], col);
89
+ }
90
+ }
91
+ }
92
+ raf = requestAnimationFrame(frame);
93
+ }
94
+ const lerp3 = (a, b, k) => a.map((v, i) => Math.round(v + (b[i] - v) * k)).join(',');
95
+ raf = requestAnimationFrame(frame);
96
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
97
+ }