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,153 @@
1
+ // Paint swirl: colour bands carried around several vortices. Each frame the paint is advected: every pixel
2
+ // looks back along the velocity field (the sum of the vortices) and takes the colour that was there. Two pour
3
+ // spouts push fresh paint outward in rings, and the pointer stirs a vortex of its own. Sampling is clamped
4
+ // Catmull-Rom, and each pixel is nudged toward the nearest paint colour, so the bands stay crisp instead of mud.
5
+ const PARAMS = { vortices: 4, strength: 1, pour: 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;touch-action:none';
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 advect = `#version 300 es
16
+ precision highp float; in vec2 uv; out vec4 o;
17
+ uniform sampler2D S; uniform vec2 A; uniform float DT, INIT; uniform int N;
18
+ uniform vec4 V[8]; // vortex: x, y, strength, core radius
19
+ uniform vec4 P[2]; // pour: x, y, radius, flow
20
+ uniform vec3 PC[2]; // pour colours
21
+ vec3 pal(float k) {
22
+ k = mod(k, 6.);
23
+ return k < 1. ? vec3(.07, .15, .25) : k < 2. ? vec3(.12, .48, .55) : k < 3. ? vec3(.96, .93, .85) : k < 4. ? vec3(.89, .34, .18) : k < 5. ? vec3(.95, .65, .25) : vec3(.91, .7, .65);
24
+ }
25
+ // the canvas is a seamless torus: each vortex and spout also acts through its 8 neighbouring copies, so there are no walls
26
+ vec2 vel(vec2 p) {
27
+ vec2 v = vec2(0);
28
+ for (int iy = -1; iy <= 1; iy++) for (int ix = -1; ix <= 1; ix++) {
29
+ vec2 sh = vec2(float(ix) * A.x, float(iy));
30
+ for (int i = 0; i < 8; i++) {
31
+ if (i >= N) break;
32
+ vec2 d = p - V[i].xy + sh; float r2 = dot(d, d) + 1e-6, c = V[i].w;
33
+ v += V[i].z * vec2(-d.y, d.x) / r2 * (1. - exp(-r2 / (c * c))); // Lamb–Oseen vortex: solid core, 1/r outside
34
+ }
35
+ for (int i = 0; i < 2; i++) { vec2 d = p - P[i].xy + sh; float r2 = dot(d, d) + 1e-6; v += P[i].w * d / r2 * (1. - exp(-r2 / (P[i].z * P[i].z))); } // spouts push paint outward
36
+ }
37
+ return v;
38
+ }
39
+ vec3 cr(vec2 q) { // clamped Catmull-Rom: sharp, no ringing
40
+ vec2 sz = vec2(textureSize(S, 0)), p = q * sz - .5, f = fract(p); ivec2 b = ivec2(floor(p));
41
+ vec4 wx = vec4(f.x * (-.5 + f.x * (1. - .5 * f.x)), 1. + f.x * f.x * (-2.5 + 1.5 * f.x), f.x * (.5 + f.x * (2. - 1.5 * f.x)), f.x * f.x * (-.5 + .5 * f.x));
42
+ vec4 wy = vec4(f.y * (-.5 + f.y * (1. - .5 * f.y)), 1. + f.y * f.y * (-2.5 + 1.5 * f.y), f.y * (.5 + f.y * (2. - 1.5 * f.y)), f.y * f.y * (-.5 + .5 * f.y));
43
+ vec3 c = vec3(0), mn = vec3(9), mx = vec3(-9); ivec2 n = ivec2(sz);
44
+ for (int y = 0; y < 4; y++) for (int x = 0; x < 4; x++) {
45
+ vec3 s = texelFetch(S, ((b + ivec2(x - 1, y - 1)) % n + n) % n, 0).rgb;
46
+ c += s * wx[x] * wy[y];
47
+ if (x > 0 && x < 3 && y > 0 && y < 3) { mn = min(mn, s); mx = max(mx, s); }
48
+ }
49
+ return clamp(c, mn, mx);
50
+ }
51
+ void main() {
52
+ vec2 p = uv * A;
53
+ if (INIT > .5) { float k = p.y * 9. + sin(uv.x * 6.2832 * 2.) * .35 + sin(uv.x * 6.2832 * 5. + 2.) * .08; o = vec4(pal(floor(k)), 1); return; }
54
+ vec2 v1 = vel(p), v2 = vel(p - v1 * DT * .5); // midpoint back-trace
55
+ vec3 c = cr(fract((p - v2 * DT) / A));
56
+ // acrylic paints do not mix: nudge every pixel toward the nearest palette colour, which re-sharpens the bands
57
+ vec3 near = pal(0.); float best = 9.;
58
+ for (int k = 0; k < 6; k++) { vec3 q = pal(float(k) + .5); float d = dot(c - q, c - q); if (d < best) { best = d; near = q; } }
59
+ c = mix(c, near, .05);
60
+ for (int i = 0; i < 2; i++) { vec2 dd = p - P[i].xy; dd -= A * round(dd / A); float d = length(dd); c = mix(c, PC[i], smoothstep(P[i].z * .8, P[i].z * .5, d) * step(.0001, P[i].w)); }
61
+ o = vec4(c, 1);
62
+ }`;
63
+ const show = `#version 300 es
64
+ precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S; uniform vec2 R;
65
+ void main() {
66
+ vec2 e = 1.5 / R;
67
+ vec3 c = texture(S, uv).rgb;
68
+ float l = dot(c, vec3(.3, .55, .15));
69
+ float lx = dot(texture(S, uv + vec2(e.x, 0)).rgb - texture(S, uv - vec2(e.x, 0)).rgb, vec3(.3, .55, .15));
70
+ float ly = dot(texture(S, uv + vec2(0, e.y)).rgb - texture(S, uv - vec2(0, e.y)).rgb, vec3(.3, .55, .15));
71
+ vec3 n = normalize(vec3(-lx * 2.2, -ly * 2.2, 1.)); // the colour edges read as a slight paint ridge
72
+ vec3 L = normalize(vec3(-.5, .6, .8));
73
+ float spec = pow(max(0., dot(reflect(-L, n), vec3(0, 0, 1))), 36.) * .35;
74
+ c = c * (.9 + .1 * dot(n, L)) + spec;
75
+ c *= 1. - .18 * pow(length(uv - .5) * 1.3, 2.);
76
+ o = vec4(c, 1);
77
+ }`;
78
+ 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; };
79
+ const program = src => { const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, src)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p); return p; };
80
+ const pA = program(advect), pS = program(show);
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 ua = n => gl.getUniformLocation(pA, n), uS = { S: ua('S'), A: ua('A'), DT: ua('DT'), INIT: ua('INIT'), N: ua('N'), V: ua('V'), P: ua('P'), PC: ua('PC') };
84
+ const uShowR = gl.getUniformLocation(pS, 'R'), uShowS = gl.getUniformLocation(pS, 'S');
85
+
86
+ let sw = 0, shh = 0, tex = [], fbo = [], cur = 0, needInit = true;
87
+ function makeTargets() {
88
+ tex.forEach(t => gl.deleteTexture(t)); fbo.forEach(f => gl.deleteFramebuffer(f));
89
+ tex = []; fbo = [];
90
+ for (let i = 0; i < 2; i++) {
91
+ const t = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t);
92
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, sw, shh, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
93
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
94
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
95
+ const f = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, f); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0);
96
+ tex.push(t); fbo.push(f);
97
+ }
98
+ needInit = true;
99
+ }
100
+ const resize = () => {
101
+ const d = Math.min(devicePixelRatio, 1.5), w = stage.clientWidth, h = stage.clientHeight;
102
+ canvas.width = w * d | 0; canvas.height = h * d | 0;
103
+ const k = Math.min(1, 900 / Math.max(w, 1)); // the paint lives at CSS-pixel resolution (max 900 wide)
104
+ const nw = Math.max(2, w * k | 0), nh = Math.max(2, h * k | 0);
105
+ if (nw !== sw || nh !== shh) { sw = nw; shh = nh; makeTargets(); }
106
+ };
107
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
108
+
109
+ // the pointer stirs: a vortex that follows it, turning the way the pointer is circling
110
+ let mx = 0, my = 0, mvx = 0, mvy = 0, spin = 0, stir = 0, lastMove = -1e9;
111
+ const onMove = e => {
112
+ const b = canvas.getBoundingClientRect(), a = b.width / b.height, x = (e.clientX - b.left) / b.width * a, y = 1 - (e.clientY - b.top) / b.height;
113
+ const vx = x - mx, vy = y - my, cross = mvx * vy - mvy * vx;
114
+ spin = spin * 0.85 + Math.sign(cross || 1) * 0.15; mvx = vx; mvy = vy; mx = x; my = y;
115
+ stir = Math.min(1, stir + Math.hypot(vx, vy) * 6); lastMove = performance.now();
116
+ };
117
+ canvas.addEventListener('pointermove', onMove);
118
+ const PALETTE = [[.07, .15, .25], [.12, .48, .55], [.96, .93, .85], [.89, .34, .18], [.95, .65, .25], [.91, .7, .65]];
119
+ const Vf = new Float32Array(32), Pf = new Float32Array(8), PCf = new Float32Array(6);
120
+ const t0 = performance.now();
121
+ let raf, last = t0;
122
+ function frame(now) {
123
+ const dt = Math.min(0.033, (now - last) / 1000); last = now; const t = (now - t0) / 1000;
124
+ const a = sw / shh;
125
+ // base vortices drift on slow orbits with alternating spin; every ~25 s they reverse and the bands partly unwind
126
+ const n = Math.max(1, Math.min(6, Math.round(PARAMS.vortices)));
127
+ for (let i = 0; i < n; i++) {
128
+ const cx = a * (0.5 + 0.32 * Math.cos(i * 2.399 + 0.6)), cy = 0.5 + 0.28 * Math.sin(i * 2.399 + 0.6);
129
+ Vf.set([cx + 0.08 * Math.cos(t * 0.21 + i), cy + 0.08 * Math.sin(t * 0.17 + i * 2), (i % 2 ? -1 : 1) * 0.024 * PARAMS.strength * Math.cos(t * 0.125) * (0.8 + 0.2 * Math.sin(t * 0.4 + i)), 0.06 + 0.02 * (i % 3)], i * 4);
130
+ }
131
+ let N = n;
132
+ if (now - lastMove > 120) stir *= Math.exp(-dt * 2.5);
133
+ if (stir > 0.01) { Vf.set([mx, my, 0.03 * stir * Math.sign(spin || 1), 0.05], N * 4); N++; }
134
+ // two pour spouts, each pouring for a while, then resting; the colour steps every half second, making rings
135
+ for (let i = 0; i < 2; i++) {
136
+ const cyc = (t + i * 2.6) % 5.2, on = cyc < 3 ? 1 : 0;
137
+ Pf.set([a * (0.3 + 0.4 * i) + 0.1 * Math.sin(t * 0.3 + i * 3), 0.5 + 0.18 * Math.cos(t * 0.23 + i * 2), 0.035, on * 0.005 * PARAMS.pour], i * 4);
138
+ PCf.set(PALETTE[(Math.floor((t + i * 1.3) / 0.5) * (i ? 5 : 1)) % 6], i * 3);
139
+ }
140
+ gl.useProgram(pA); gl.viewport(0, 0, sw, shh);
141
+ gl.uniform2f(uS.A, a, 1); gl.uniform1f(uS.DT, dt); gl.uniform1f(uS.INIT, needInit ? 1 : 0); gl.uniform1i(uS.N, N);
142
+ gl.uniform4fv(uS.V, Vf); gl.uniform4fv(uS.P, Pf); gl.uniform3fv(uS.PC, PCf); gl.uniform1i(uS.S, 0);
143
+ gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex[cur]);
144
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[1 - cur]); gl.drawArrays(gl.TRIANGLES, 0, 3);
145
+ cur = 1 - cur; needInit = false;
146
+ gl.useProgram(pS); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, canvas.width, canvas.height);
147
+ gl.uniform2f(uShowR, sw, shh); gl.uniform1i(uShowS, 0); gl.bindTexture(gl.TEXTURE_2D, tex[cur]);
148
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
149
+ raf = requestAnimationFrame(frame);
150
+ }
151
+ raf = requestAnimationFrame(frame);
152
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
153
+ }
@@ -0,0 +1,60 @@
1
+ // Rain on glass: the city behind the window is blurred, but every drop is a tiny lens that shows it sharp,
2
+ // upside down and magnified. Heavy drops slide, wobble and leave a trail of small droplets behind them.
3
+ const PARAMS = { rain: 1, size: 1 };
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 ctx = canvas.getContext('2d');
10
+ let W = 0, H = 0, sharp, blurred;
11
+ function paintCity() {
12
+ sharp = document.createElement('canvas'); sharp.width = W; sharp.height = H;
13
+ const g = sharp.getContext('2d');
14
+ const sky = g.createLinearGradient(0, 0, 0, H); sky.addColorStop(0, '#1b2238'); sky.addColorStop(1, '#3a2a3a'); g.fillStyle = sky; g.fillRect(0, 0, W, H);
15
+ for (let i = 0; i < 26; i++) { const x = Math.random() * W, w = 20 + Math.random() * 60, h = H * (0.25 + Math.random() * 0.5); g.fillStyle = '#121626'; g.fillRect(x, H - h, w, h);
16
+ for (let y = H - h + 8; y < H - 6; y += 10) for (let xx = x + 5; xx < x + w - 5; xx += 9) if (Math.random() < 0.35) { g.fillStyle = ['#ffd27a', '#ffb35c', '#9fd3ff', '#ff8aa0'][Math.random() * 4 | 0]; g.fillRect(xx, y, 4, 5); } }
17
+ for (let i = 0; i < 18; i++) { const x = Math.random() * W, y = H * (0.7 + Math.random() * 0.3), c = ['#ff5c5c', '#ffd27a', '#ffffff'][Math.random() * 3 | 0]; const rg = g.createRadialGradient(x, y, 0, x, y, 30); rg.addColorStop(0, c); rg.addColorStop(1, 'rgba(0,0,0,0)'); g.fillStyle = rg; g.fillRect(x - 30, y - 30, 60, 60); }
18
+ blurred = document.createElement('canvas'); blurred.width = W; blurred.height = H;
19
+ const b = blurred.getContext('2d'); b.filter = 'blur(10px) brightness(.85)'; b.drawImage(sharp, -20, -20, W + 40, H + 40);
20
+ }
21
+ 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); paintCity(); };
22
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
23
+
24
+ const S = () => Math.min(W, H) * 0.012 * PARAMS.size;
25
+ let drops = [], beads = [];
26
+ const newDrop = (y = Math.random() * H) => ({ x: Math.random() * W, y, r: S() * (0.6 + Math.random() * 1.8), vy: 0, wob: Math.random() * 6.28, lastTrail: 0 });
27
+ for (let i = 0; i < 40; i++) drops.push(newDrop());
28
+ for (let i = 0; i < 260; i++) beads.push({ x: Math.random() * W, y: Math.random() * H, r: S() * (0.2 + Math.random() * 0.5) });
29
+ function lens(x, y, r) {
30
+ // a drop flips and magnifies what is behind it: draw the sharp city through a clipped, inverted window
31
+ ctx.save(); ctx.beginPath(); ctx.ellipse(x, y, r, r * 1.08, 0, 0, Math.PI * 2); ctx.clip();
32
+ ctx.translate(x, y); ctx.scale(-0.35, -0.35); ctx.translate(-x, -y);
33
+ ctx.drawImage(sharp, x - W / 2 + (x - W / 2) * 0.3, y - H / 2 + (y - H / 2) * 0.3, W, H);
34
+ ctx.restore();
35
+ ctx.strokeStyle = 'rgba(0,0,0,.35)'; ctx.lineWidth = Math.max(1, r * 0.12); ctx.beginPath(); ctx.ellipse(x, y + r * 0.04, r, r * 1.08, 0, 0, Math.PI * 2); ctx.stroke();
36
+ ctx.fillStyle = 'rgba(255,255,255,.55)'; ctx.beginPath(); ctx.ellipse(x - r * 0.3, y - r * 0.38, r * 0.22, r * 0.14, -0.6, 0, Math.PI * 2); ctx.fill();
37
+ }
38
+ let last = performance.now(), raf;
39
+ function frame(now) {
40
+ const dt = Math.min(0.033, (now - last) / 1000); last = now;
41
+ if (Math.random() < dt * 14 * PARAMS.rain) beads.push({ x: Math.random() * W, y: Math.random() * H, r: S() * (0.2 + Math.random() * 0.5) });
42
+ if (Math.random() < dt * 3 * PARAMS.rain) drops.push(newDrop(-10));
43
+ if (beads.length > 420) beads.splice(0, beads.length - 420);
44
+ ctx.drawImage(blurred, 0, 0, W, H);
45
+ for (const b of beads) lens(b.x, b.y, b.r);
46
+ for (const d of drops) {
47
+ // big drops overcome surface tension and slide, in fits and starts
48
+ const heavy = d.r > S() * 1.4;
49
+ if (heavy) { d.vy += (Math.random() < 0.05 ? 0 : 900 * dt) * (d.r / S()) * 0.4; d.vy *= 0.92; d.y += d.vy * dt; d.wob += dt * 6; d.x += Math.sin(d.wob) * 0.3; }
50
+ if (heavy && d.y - d.lastTrail > d.r * 1.2) { beads.push({ x: d.x + (Math.random() - 0.5) * d.r * 0.4, y: d.y - d.r, r: d.r * (0.2 + Math.random() * 0.25) }); d.lastTrail = d.y; d.r *= 0.985; }
51
+ // absorb beads in the path
52
+ for (let i = beads.length - 1; i >= 0; i--) { const b = beads[i]; if (Math.abs(b.x - d.x) < d.r && b.y > d.y - d.r && b.y < d.y + d.r) { d.r = Math.min(S() * 3.5, Math.hypot(d.r, b.r)); beads.splice(i, 1); } }
53
+ lens(d.x, d.y, d.r);
54
+ }
55
+ drops = drops.filter(d => d.y < H + 30);
56
+ raf = requestAnimationFrame(frame);
57
+ }
58
+ raf = requestAnimationFrame(frame);
59
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
60
+ }
@@ -0,0 +1,57 @@
1
+ // Water ripples: two height buffers and the wave equation. Each cell becomes the average of its neighbours
2
+ // minus its own previous height; the surface slope bends the tiles underneath and catches the light.
3
+ const PARAMS = { damping: 0.985, dropRate: 1 };
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;image-rendering:auto';
8
+ stage.append(canvas);
9
+ const ctx = canvas.getContext('2d');
10
+ const sim = document.createElement('canvas'), sctx = sim.getContext('2d');
11
+ let W = 0, H = 0, w = 0, h = 0, a, b, img, tiles;
12
+ const CELL = 2;
13
+ const resize = () => {
14
+ 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);
15
+ w = Math.max(8, Math.ceil(W / CELL)); h = Math.max(8, Math.ceil(H / CELL)); sim.width = w; sim.height = h;
16
+ a = new Float32Array(w * h); b = new Float32Array(w * h); img = sctx.createImageData(w, h);
17
+ // the pool floor: blue tiles with lighter grout, precomputed once
18
+ tiles = new Uint8ClampedArray(w * h * 3);
19
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
20
+ const grout = (x % 16 < 1) || (y % 16 < 1), i = (y * w + x) * 3, v = ((x >> 4) + (y >> 4)) % 2 ? 8 : 0;
21
+ tiles[i] = grout ? 96 : 24 + v; tiles[i + 1] = grout ? 176 : 122 + v; tiles[i + 2] = grout ? 205 : 170 + v;
22
+ }
23
+ };
24
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
25
+
26
+ const drop = (x, y, r = 2, s = 260) => {
27
+ for (let j = -r; j <= r; j++) for (let i = -r; i <= r; i++) {
28
+ const X = (x | 0) + i, Y = (y | 0) + j; if (X > 0 && X < w - 1 && Y > 0 && Y < h - 1 && i * i + j * j <= r * r) a[Y * w + X] += s;
29
+ }
30
+ };
31
+ let lastMove = -1e9;
32
+ const onMove = e => { const r = canvas.getBoundingClientRect(); drop((e.clientX - r.left) / CELL, (e.clientY - r.top) / CELL, 1, 90); lastMove = performance.now(); };
33
+ canvas.addEventListener('pointermove', onMove);
34
+ let acc = 0, last = performance.now(), raf;
35
+ function frame(now) {
36
+ const dt = Math.min(0.033, (now - last) / 1000); last = now;
37
+ acc += dt * PARAMS.dropRate * (now - lastMove > 1500 ? 1.5 : 0.4);
38
+ while (acc > 1) { acc -= 1; drop(2 + Math.random() * (w - 4), 2 + Math.random() * (h - 4), 2 + (Math.random() * 2 | 0), 420); }
39
+ for (let y = 1; y < h - 1; y++) for (let x = 1; x < w - 1; x++) {
40
+ const i = y * w + x;
41
+ b[i] = ((a[i - 1] + a[i + 1] + a[i - w] + a[i + w]) / 2 - b[i]) * PARAMS.damping;
42
+ }
43
+ [a, b] = [b, a];
44
+ const px = img.data;
45
+ for (let y = 1; y < h - 1; y++) for (let x = 1; x < w - 1; x++) {
46
+ const i = y * w + x, dx = a[i - 1] - a[i + 1], dy = a[i - w] - a[i + w];
47
+ const sx = Math.min(w - 1, Math.max(0, x + Math.round(dx * 0.03))), sy = Math.min(h - 1, Math.max(0, y + Math.round(dy * 0.03)));
48
+ const t = (sy * w + sx) * 3, light = (dx + dy) * 0.22, o = i * 4;
49
+ px[o] = tiles[t] + light; px[o + 1] = tiles[t + 1] + light; px[o + 2] = tiles[t + 2] + light; px[o + 3] = 255;
50
+ }
51
+ sctx.putImageData(img, 0, 0);
52
+ ctx.imageSmoothingEnabled = true; ctx.drawImage(sim, 0, 0, W, H);
53
+ raf = requestAnimationFrame(frame);
54
+ }
55
+ raf = requestAnimationFrame(frame);
56
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); canvas.remove(); };
57
+ }
@@ -0,0 +1,75 @@
1
+ // SPH water: smoothed-particle hydrodynamics. Each particle's density comes from its neighbours; pressure pushes
2
+ // crowded particles apart and viscosity evens out their velocities. Gravity swings to slosh the tank.
3
+ // Rendered by splatting soft blobs and thresholding them into one liquid surface.
4
+ const PARAMS = { particles: 600, slosh: 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
+ const buf = document.createElement('canvas'), bctx = buf.getContext('2d', { willReadFrequently: true });
12
+ let W = 0, H = 0, bw = 0, bh = 0;
13
+ 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); bw = Math.ceil(W / 2); bh = Math.ceil(H / 2); buf.width = bw; buf.height = bh; };
14
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
15
+
16
+ const N = Math.round(PARAMS.particles), h = 16, h2 = h * h, mass = 1, k = 8000, mu = 2.2;
17
+ let rho0 = 0; // measured from the starting packing, so the water keeps its volume
18
+ const POLY6 = 4 / (Math.PI * h2 ** 4), SPIKY = -10 / (Math.PI * h ** 5), VISC = 40 / (Math.PI * h ** 5);
19
+ const P = Array.from({ length: N }, (_, i) => ({ x: W * 0.15 + (i % 30) * h * 0.55, y: H * 0.2 + Math.floor(i / 30) * h * 0.55, vx: 0, vy: 0, fx: 0, fy: 0, rho: 0, p: 0 }));
20
+ const grid = new Map(), key = (x, y) => (x << 16) | (y & 0xffff);
21
+ let raf, last = performance.now();
22
+ function step(dt, gx, gy) {
23
+ grid.clear();
24
+ for (const a of P) { const kk = key(a.x / h | 0, a.y / h | 0); let c = grid.get(kk); if (!c) grid.set(kk, c = []); c.push(a); }
25
+ const near = a => { const out = [], cx = a.x / h | 0, cy = a.y / h | 0; for (let j = -1; j <= 1; j++) for (let i = -1; i <= 1; i++) { const c = grid.get(key(cx + i, cy + j)); if (c) out.push(...c); } return out; };
26
+ const lists = P.map(near);
27
+ P.forEach((a, n) => { a.rho = 0; for (const b of lists[n]) { const r2 = (a.x - b.x) ** 2 + (a.y - b.y) ** 2; if (r2 < h2) a.rho += mass * POLY6 * (h2 - r2) ** 3; } });
28
+ if (!rho0) rho0 = P.reduce((sum, a) => sum + a.rho, 0) / P.length;
29
+ for (const a of P) a.p = k * (a.rho - rho0) / rho0;
30
+ P.forEach((a, n) => {
31
+ let fx = 0, fy = 0;
32
+ for (const b of lists[n]) {
33
+ if (a === b) continue;
34
+ const dx = b.x - a.x, dy = b.y - a.y, r = Math.hypot(dx, dy);
35
+ if (r >= h || r === 0) continue;
36
+ const press = -mass * (a.p + b.p) / (2 * b.rho) * SPIKY * (h - r) ** 3 / r; // pressure pushes apart
37
+ fx -= press * dx; fy -= press * dy;
38
+ const vis = mu * mass / b.rho * VISC * (h - r); // viscosity evens velocities
39
+ fx += vis * (b.vx - a.vx); fy += vis * (b.vy - a.vy);
40
+ }
41
+ a.fx = fx + gx * a.rho; a.fy = fy + gy * a.rho;
42
+ });
43
+ for (const a of P) {
44
+ a.vx += dt * a.fx / a.rho; a.vy += dt * a.fy / a.rho;
45
+ const sp = Math.hypot(a.vx, a.vy); if (sp > 700) { a.vx *= 700 / sp; a.vy *= 700 / sp; } // keep the stiff solver stable
46
+ a.x += dt * a.vx; a.y += dt * a.vy;
47
+ const m = 4, damp = -0.4;
48
+ if (a.x < m) { a.x = m; a.vx *= damp; } if (a.x > W - m) { a.x = W - m; a.vx *= damp; }
49
+ if (a.y < m) { a.y = m; a.vy *= damp; } if (a.y > H - m) { a.y = H - m; a.vy *= damp; }
50
+ }
51
+ }
52
+ function frame(now) {
53
+ const real = Math.min(0.033, (now - last) / 1000); last = now; const t = now / 1000;
54
+ const ang = Math.sin(t * 0.7) * 0.6 * PARAMS.slosh, g = 1600;
55
+ for (let s = 0; s < 8; s++) step(Math.min(real, 0.02) / 8, Math.sin(ang) * g, Math.cos(ang) * g);
56
+ // splat soft blobs at half resolution, then threshold alpha into a liquid surface
57
+ bctx.clearRect(0, 0, bw, bh);
58
+ for (const a of P) { const x = a.x / 2, y = a.y / 2, r = h * 0.8; const g2 = bctx.createRadialGradient(x, y, 0, x, y, r); g2.addColorStop(0, 'rgba(0,0,0,1)'); g2.addColorStop(1, 'rgba(0,0,0,0)'); bctx.fillStyle = g2; bctx.fillRect(x - r, y - r, r * 2, r * 2); }
59
+ const img = bctx.getImageData(0, 0, bw, bh), d = img.data;
60
+ for (let y = 0; y < bh; y++) for (let x = 0; x < bw; x++) {
61
+ const i = (y * bw + x) * 4, a = d[i + 3];
62
+ if (a > 150) { const edge = a < 200 ? 1 : 0, depth = y / bh; d[i] = edge ? 190 : 30 + 20 * depth; d[i + 1] = edge ? 230 : 110 - 40 * depth; d[i + 2] = edge ? 255 : 200 - 50 * depth; d[i + 3] = 255; }
63
+ else { d[i] = 236; d[i + 1] = 241; d[i + 2] = 246; d[i + 3] = 255; }
64
+ }
65
+ bctx.putImageData(img, 0, 0);
66
+ ctx.fillStyle = '#dfe5ea'; ctx.fillRect(0, 0, W, H); // clear behind the tilted tank so its corners never smear
67
+ ctx.save(); ctx.translate(W / 2, H / 2); ctx.rotate(-ang * 0.35); ctx.scale(0.86, 0.86); ctx.translate(-W / 2, -H / 2);
68
+ ctx.imageSmoothingEnabled = true; ctx.drawImage(buf, 0, 0, W, H);
69
+ ctx.strokeStyle = '#9aa6b4'; ctx.lineWidth = 3; ctx.strokeRect(1.5, 1.5, W - 3, H - 3);
70
+ ctx.restore();
71
+ raf = requestAnimationFrame(frame);
72
+ }
73
+ raf = requestAnimationFrame(frame);
74
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
75
+ }
@@ -0,0 +1,55 @@
1
+ // Underwater: god rays slanting down from the surface, caustic light rippling on the sand, depth fog, kelp
2
+ // swaying in the current and bubbles rising, all in one fragment shader.
3
+ const PARAMS = { rays: 1, bubbles: 1 };
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
+ const vs = `#version 300 es
12
+ in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
13
+ const fs = `#version 300 es
14
+ precision highp float; in vec2 uv; out vec4 o; uniform vec2 R; uniform float T, RY, BU;
15
+ float h(float n) { return fract(sin(n) * 43758.5453); }
16
+ float h2(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
17
+ float noise(vec2 p) { vec2 i = floor(p), f = fract(p), u = f * f * (3. - 2. * f); return mix(mix(h2(i), h2(i + vec2(1, 0)), u.x), mix(h2(i + vec2(0, 1)), h2(i + vec2(1, 1)), u.x), u.y); }
18
+ float caustic(vec2 p, float t) { float c = 0.; for (int i = 0; i < 3; i++) { p += vec2(sin(p.y * 1.7 + t), cos(p.x * 1.3 - t * .8)) * .4; c += abs(sin(p.x * 3.) * sin(p.y * 3.)); } return pow(1. - c / 3., 6.); }
19
+ void main() {
20
+ vec2 a = vec2(R.x / R.y, 1.), q = uv * a;
21
+ vec3 deep = vec3(.01, .08, .16), shallow = vec3(.05, .42, .55);
22
+ vec3 col = mix(deep, shallow, pow(uv.y, 1.4));
23
+ // god rays: bright bands slanting from the surface, flickering with the waves
24
+ float x = q.x + (1. - uv.y) * .35;
25
+ float rays = noise(vec2(x * 7., T * .3)) * noise(vec2(x * 15. + 4., T * .5));
26
+ col += vec3(.45, .75, .8) * pow(rays, 2.) * pow(uv.y, 1.5) * 1.6 * RY;
27
+ // sand with caustics
28
+ float sand = smoothstep(.22, .18, uv.y + .03 * sin(q.x * 3.));
29
+ vec3 sandC = vec3(.55, .5, .38) * (.6 + .4 * noise(q * 40.)) + vec3(.6, .9, .9) * caustic(q * 6., T * .9) * .7;
30
+ col = mix(col, mix(sandC, deep, .35), sand);
31
+ // kelp: a few swaying ribbons
32
+ for (int i = 0; i < 7; i++) { float fi = float(i), kx = (fi + .5) / 7. * a.x + (h(fi) - .5) * .2;
33
+ float sway = sin(T * .8 + fi + uv.y * 4.) * .04 * uv.y; float w = .012 * (1. - uv.y * .6);
34
+ float k = smoothstep(w, w * .3, abs(q.x - kx - sway)) * step(uv.y, .45 + h(fi + 3.) * .35);
35
+ col = mix(col, vec3(.04, .2, .12) * (.6 + .4 * uv.y), k * .9); }
36
+ // bubbles rising in columns
37
+ for (int i = 0; i < 18; i++) { float fi = float(i); if (fi > 18. * BU) break;
38
+ float bx = h(fi * 3.1) * a.x, sp = .08 + h(fi) * .12, by = fract(T * sp + h(fi * 7.)) * 1.2 - .1;
39
+ vec2 c = vec2(bx + sin(T * 2. + fi) * .01, by); float r = .006 + h(fi * 9.) * .01, d = length(q - c);
40
+ col += vec3(.7, .95, 1.) * (smoothstep(r, r * .7, d) - smoothstep(r * .7, r * .3, d) * .7) * .8; }
41
+ col = mix(col, deep, (1. - uv.y) * .25); // depth fog
42
+ o = vec4(col, 1);
43
+ }`;
44
+ 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; };
45
+ 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);
46
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
47
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
48
+ const u = n => gl.getUniformLocation(prog, n), uR = u('R'), uT = u('T'), uRY = u('RY'), uBU = u('BU');
49
+ 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); };
50
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
51
+ let raf;
52
+ function frame(now) { gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform1f(uT, now / 1000); gl.uniform1f(uRY, PARAMS.rays); gl.uniform1f(uBU, PARAMS.bubbles); gl.drawArrays(gl.TRIANGLES, 0, 3); raf = requestAnimationFrame(frame); }
53
+ raf = requestAnimationFrame(frame);
54
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
55
+ }
@@ -0,0 +1,98 @@
1
+ // Ocean at sunset: a raymarched heightfield. The surface is a stack of sharp-crested waves, each one dragging
2
+ // the next along its slope so crests bunch up like real swell. Every pixel reflects the sky with Fresnel,
3
+ // lets the low sun glow through thin crests, and catches the sun's glint on its facets.
4
+ const PARAMS = { waves: 1, sun: 0.5, speed: 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, D, SY;
16
+ vec3 SUN;
17
+ // sharp-crested waves; each one drags the sample point along its slope (that bunches the crests)
18
+ float waves(vec2 p, int it) {
19
+ float w = 1., ws = 0., sum = 0., f = 1., t = T, a = 0.;
20
+ for (int i = 0; i < 40; i++) {
21
+ if (i >= it) break;
22
+ vec2 d = vec2(sin(a), cos(a));
23
+ float x = dot(d, p) * f + t, wave = exp(sin(x) - 1.), dx = wave * cos(x);
24
+ sum += wave * w; ws += w; p -= d * dx * w * .38;
25
+ a += 1232.399963; w = mix(w, 0., .2); f *= 1.18; t *= 1.07;
26
+ }
27
+ return sum / ws;
28
+ }
29
+ float hgt(vec2 p, int it) { return waves(p * .55, it) * D - D; }
30
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
31
+ float noise(vec2 p) { vec2 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f); return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), mix(hash(i + vec2(0, 1)), hash(i + 1.), f.x), f.y); }
32
+ float fbm(vec2 p) { float s = 0., a = .5; for (int i = 0; i < 5; i++) { s += a * noise(p); p = p * 2.03 + 7.1; a *= .5; } return s; }
33
+ vec3 sky(vec3 d) {
34
+ float y = max(d.y, 0.), sd = max(dot(d, SUN), 0.), low = 1. - smoothstep(.02, .3, SY);
35
+ vec3 top = mix(vec3(.1, .2, .5), vec3(.04, .05, .18), low), mid = mix(vec3(.8, .5, .45), vec3(.62, .2, .26), low), hor = mix(vec3(1., .7, .45), vec3(1., .42, .14), low);
36
+ vec3 c = mix(hor, mid, smoothstep(0., .06, y)); c = mix(c, top, smoothstep(.04, .3, y));
37
+ c += vec3(1., .38, .1) * pow(sd, 4.) * .22 + vec3(1., .55, .22) * pow(sd, 18.) * .35 + vec3(1., .78, .5) * pow(sd, 160.) * 1.4;
38
+ c += vec3(1., .9, .7) * smoothstep(.99935, .9997, sd) * 12.; // the sun's disc (HDR, tonemapped later)
39
+ vec2 q = d.xz / (y + .06) * .6 + vec2(T * .01, 0); // streaky clouds lit from below
40
+ float cl = smoothstep(.42, .75, fbm(q * vec2(.6, 2.2))) * smoothstep(.0, .08, y) * (1. - smoothstep(.25, .6, y));
41
+ c = mix(c, mix(vec3(.3, .12, .22), vec3(1., .55, .3), pow(sd, 4.)), cl * .8);
42
+ return c;
43
+ }
44
+ vec3 normalAt(vec2 p, float e) {
45
+ float h = hgt(p, 34);
46
+ return normalize(cross(vec3(e, hgt(p + vec2(e, 0), 34) - h, 0), vec3(0, hgt(p + vec2(0, -e), 34) - h, -e)) * vec3(1, 1, 1));
47
+ }
48
+ vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
49
+ void main() {
50
+ SUN = normalize(vec3(-.18, SY, 1.));
51
+ vec2 q = (uv - .5) * vec2(R.x / R.y, 1.);
52
+ vec3 ro = vec3(0, 2.1 + .12 * sin(T * .4), T * .9);
53
+ float roll = .025 * sin(T * .33);
54
+ vec3 rd = normalize(vec3(q.x * cos(roll) - q.y * sin(roll), q.x * sin(roll) + q.y * cos(roll) - .1, 1.15));
55
+ vec3 col;
56
+ if (rd.y >= 0.) col = sky(rd);
57
+ else {
58
+ // march between the top (y = 0) and bottom (y = -D) planes of the wave slab
59
+ vec3 pos = ro + rd * (ro.y / -rd.y), end = ro + rd * ((ro.y + D) / -rd.y);
60
+ vec3 dir = normalize(end - pos);
61
+ for (int i = 0; i < 56; i++) {
62
+ float h = hgt(pos.xz, 12);
63
+ if (h + .01 > pos.y) break;
64
+ pos += dir * (pos.y - h);
65
+ }
66
+ float dist = length(pos - ro);
67
+ vec3 n = normalAt(pos.xz, .01);
68
+ n = normalize(mix(n, vec3(0, 1, 0), min(1., sqrt(dist * .012) * 1.1))); // calm the far field so it does not shimmer
69
+ float fr = .04 + .96 * pow(1. - max(0., dot(n, -rd)), 5.);
70
+ vec3 rf = reflect(rd, n); rf.y = abs(rf.y);
71
+ vec3 refl = sky(rf);
72
+ float crest = clamp((pos.y + D) / D, 0., 1.);
73
+ vec3 deep = vec3(.01, .035, .06) + vec3(.02, .14, .16) * pow(crest, 3.) * pow(max(0., dot(rd, SUN)), 2.) * 2.5; // low sun glows through thin crests
74
+ vec3 c = mix(deep, refl, fr);
75
+ c += vec3(1., .8, .55) * pow(max(0., dot(rf, SUN)), 900.) * 30. * (1. - smoothstep(.0, .15, SY - .1)); // glint
76
+ col = mix(c, sky(normalize(vec3(rd.x, .001, rd.z))), 1. - exp(-dist * .0028));
77
+ }
78
+ col = aces(col * .72);
79
+ col *= 1. - .25 * pow(length(uv - .5) * 1.25, 2.);
80
+ o = vec4(pow(col, vec3(1. / 2.2)), 1);
81
+ }`;
82
+ 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; };
83
+ 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);
84
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
85
+ gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
86
+ const u = n => gl.getUniformLocation(prog, n), uR = u('R'), uT = u('T'), uD = u('D'), uSY = u('SY');
87
+ // a heavy shader: render at 1x pixels at most and let the browser scale
88
+ const resize = () => { const d = Math.min(devicePixelRatio, 1); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
89
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
90
+ let raf; const t0 = performance.now();
91
+ function frame(now) {
92
+ gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform1f(uT, (now - t0) / 1000 * PARAMS.speed + 40);
93
+ gl.uniform1f(uD, 0.55 * PARAMS.waves); gl.uniform1f(uSY, 0.005 + 0.1 * PARAMS.sun);
94
+ gl.drawArrays(gl.TRIANGLES, 0, 3); raf = requestAnimationFrame(frame);
95
+ }
96
+ raf = requestAnimationFrame(frame);
97
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
98
+ }
@@ -0,0 +1,37 @@
1
+ // Cursor trail: the head follows the pointer, every other point eases toward the one ahead of it.
2
+ const PARAMS = { points: 24, ease: 0.35 };
3
+
4
+ export default function (stage) {
5
+ const canvas = document.createElement('canvas');
6
+ canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block;cursor:none';
7
+ stage.append(canvas);
8
+ const ctx = canvas.getContext('2d');
9
+ let W = 0, H = 0;
10
+ 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); };
11
+ const ro = new ResizeObserver(resize); ro.observe(stage); resize();
12
+
13
+ let px = W / 2, py = H / 2, lastMove = -1e9;
14
+ const onMove = e => { const b = canvas.getBoundingClientRect(); px = e.clientX - b.left; py = e.clientY - b.top; lastMove = performance.now(); };
15
+ canvas.addEventListener('pointermove', onMove);
16
+ const n = Math.round(PARAMS.points);
17
+ const pts = Array.from({ length: n }, () => ({ x: W / 2, y: H / 2 }));
18
+ let raf;
19
+ function frame(now) {
20
+ let x = px, y = py;
21
+ if (now - lastMove > 1500) { const t = now / 1000; x = W / 2 + Math.sin(t * 1.6) * W * 0.34; y = H / 2 + Math.sin(t * 2.3 + 1) * H * 0.3; }
22
+ pts[0].x += (x - pts[0].x) * 0.6; pts[0].y += (y - pts[0].y) * 0.6;
23
+ for (let i = 1; i < n; i++) { pts[i].x += (pts[i - 1].x - pts[i].x) * PARAMS.ease; pts[i].y += (pts[i - 1].y - pts[i].y) * PARAMS.ease; }
24
+ ctx.fillStyle = '#1d1a12'; ctx.fillRect(0, 0, W, H);
25
+ ctx.lineCap = 'round';
26
+ for (let i = n - 1; i > 0; i--) {
27
+ const k = 1 - i / n;
28
+ ctx.strokeStyle = `hsl(${38 - 60 * (1 - k)}, 95%, ${55 + 10 * k}%)`;
29
+ ctx.lineWidth = 2 + 22 * k;
30
+ ctx.beginPath(); ctx.moveTo(pts[i].x, pts[i].y); ctx.lineTo(pts[i - 1].x, pts[i - 1].y); ctx.stroke();
31
+ }
32
+ ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(pts[0].x, pts[0].y, 5, 0, Math.PI * 2); ctx.fill();
33
+ raf = requestAnimationFrame(frame);
34
+ }
35
+ raf = requestAnimationFrame(frame);
36
+ return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); canvas.remove(); };
37
+ }
@@ -0,0 +1,41 @@
1
+ // Magnetic button: within a radius the button leans toward the pointer and its label leans further.
2
+ // When the pointer is idle, a ghost cursor circles so the effect is always visible.
3
+ const PARAMS = { strength: 0.4, radius: 140 };
4
+
5
+ export default function (stage) {
6
+ const root = document.createElement('div');
7
+ root.className = 'mg-root';
8
+ root.innerHTML = `<style>
9
+ .mg-root { position: absolute; inset: 0; display: grid; place-items: center; background: #fff8e6; font-family: "Mona Sans", system-ui, sans-serif; }
10
+ .mg-btn { width: 132px; height: 132px; border-radius: 50%; border: 0; background: #15171a; color: #fff; font: 650 15px "Mona Sans", system-ui, sans-serif; display: grid; place-items: center; cursor: pointer; will-change: transform; }
11
+ .mg-btn span { display: block; will-change: transform; }
12
+ .mg-ghost { position: absolute; left: 0; top: 0; width: 14px; height: 14px; margin: -7px; border-radius: 50%; background: #b07a00; opacity: 0; transition: opacity .3s; pointer-events: none; }
13
+ </style><button class="mg-btn" type="button"><span>Hover me</span></button><div class="mg-ghost"></div>`;
14
+ stage.append(root);
15
+ const btn = root.querySelector('.mg-btn'), label = btn.firstChild, ghost = root.querySelector('.mg-ghost');
16
+ let px = 0, py = 0, lastMove = -1e9;
17
+ const onMove = e => { const b = root.getBoundingClientRect(); px = e.clientX - b.left; py = e.clientY - b.top; lastMove = performance.now(); };
18
+ root.addEventListener('pointermove', onMove);
19
+
20
+ const S = { x: 0, y: 0, vx: 0, vy: 0 }, L = { x: 0, y: 0, vx: 0, vy: 0 };
21
+ const step = (s, tx, ty, k, c, dt) => { s.vx += (-k * (s.x - tx) - c * s.vx) * dt; s.vy += (-k * (s.y - ty) - c * s.vy) * dt; s.x += s.vx * dt; s.y += s.vy * dt; };
22
+ let last = performance.now(), raf;
23
+ function frame(now) {
24
+ const dt = Math.min(0.032, (now - last) / 1000); last = now;
25
+ const idle = now - lastMove > 1500;
26
+ let x = px, y = py;
27
+ if (idle) { const t = now / 1000, w = root.clientWidth, h = root.clientHeight; x = w / 2 + Math.cos(t * 1.1) * w * 0.26; y = h / 2 + Math.sin(t * 1.7) * h * 0.22; }
28
+ ghost.style.opacity = idle ? 0.8 : 0;
29
+ ghost.style.transform = `translate(${x}px, ${y}px)`;
30
+ const b = btn.getBoundingClientRect(), r = root.getBoundingClientRect();
31
+ const cx = b.left - r.left + b.width / 2 - S.x, cy = b.top - r.top + b.height / 2 - S.y;
32
+ const dx = x - cx, dy = y - cy, inside = Math.hypot(dx, dy) < PARAMS.radius;
33
+ const tx = inside ? dx * PARAMS.strength : 0, ty = inside ? dy * PARAMS.strength : 0;
34
+ step(S, tx, ty, 170, 16, dt); step(L, tx * 0.6, ty * 0.6, 220, 18, dt);
35
+ btn.style.transform = `translate(${S.x}px, ${S.y}px)`;
36
+ label.style.transform = `translate(${L.x}px, ${L.y}px)`;
37
+ raf = requestAnimationFrame(frame);
38
+ }
39
+ raf = requestAnimationFrame(frame);
40
+ return () => { cancelAnimationFrame(raf); root.removeEventListener('pointermove', onMove); root.remove(); };
41
+ }
@@ -0,0 +1,31 @@
1
+ // Click ripple: a circle grows from the exact press point to the farthest corner, then fades.
2
+ const PARAMS = { duration: 650 };
3
+
4
+ export default function (stage) {
5
+ const root = document.createElement('div');
6
+ root.className = 'rp-root';
7
+ root.innerHTML = `<style>
8
+ .rp-root { position: absolute; inset: 0; display: grid; place-items: center; background: #15171a; font-family: "Mona Sans", system-ui, sans-serif; }
9
+ .rp-btn { position: relative; overflow: hidden; width: 66%; height: 42%; border: 0; border-radius: 18px; background: #fab005; color: #2b1d00; font: 750 clamp(14px, 6vmin, 34px) "Mona Sans", system-ui, sans-serif; cursor: pointer; transition: transform .16s cubic-bezier(.23,1,.32,1); }
10
+ .rp-btn:active { transform: scale(0.97); }
11
+ .rp-wave { position: absolute; border-radius: 50%; background: #fff; pointer-events: none; }
12
+ </style><button class="rp-btn" type="button">Press anywhere</button>`;
13
+ stage.append(root);
14
+ const btn = root.querySelector('.rp-btn');
15
+ function ripple(x, y) {
16
+ const w = btn.clientWidth, h = btn.clientHeight, r = Math.hypot(Math.max(x, w - x), Math.max(y, h - y));
17
+ const el = document.createElement('span'); el.className = 'rp-wave';
18
+ el.style.cssText = `left:${x - r}px;top:${y - r}px;width:${r * 2}px;height:${r * 2}px`;
19
+ btn.append(el);
20
+ el.animate([{ transform: 'scale(0)', opacity: 0.45 }, { transform: 'scale(1)', opacity: 0.32, offset: 0.7 }, { transform: 'scale(1)', opacity: 0 }],
21
+ { duration: PARAMS.duration, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' }).onfinish = () => el.remove();
22
+ }
23
+ let manual = -1e9;
24
+ const onDown = e => { const b = btn.getBoundingClientRect(); ripple(e.clientX - b.left, e.clientY - b.top); manual = performance.now(); };
25
+ btn.addEventListener('pointerdown', onDown);
26
+ const auto = setInterval(() => {
27
+ if (performance.now() - manual < 2500) return;
28
+ ripple(btn.clientWidth * (0.15 + Math.random() * 0.7), btn.clientHeight * (0.2 + Math.random() * 0.6));
29
+ }, 900);
30
+ return () => { clearInterval(auto); btn.removeEventListener('pointerdown', onDown); root.remove(); };
31
+ }