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.
- package/LICENSE +16 -0
- package/README.md +37 -2
- package/anims/basics/anticipation.js +56 -0
- package/anims/basics/card-flip.js +39 -0
- package/anims/basics/easing-race.js +57 -0
- package/anims/basics/fade-scale.js +44 -0
- package/anims/basics/stagger-grid.js +43 -0
- package/anims/chrome/black-hole.js +81 -0
- package/anims/chrome/cloth-reveal.js +156 -0
- package/anims/chrome/dna-helix.js +52 -0
- package/anims/chrome/dust-bust.js +109 -0
- package/anims/chrome/extruded-type.js +64 -0
- package/anims/chrome/fractal-tree.js +75 -0
- package/anims/chrome/gem.js +111 -0
- package/anims/chrome/geode.js +102 -0
- package/anims/chrome/glass-lens.js +73 -0
- package/anims/chrome/glass-prism.js +95 -0
- package/anims/chrome/gyroid.js +69 -0
- package/anims/chrome/holo-card.js +52 -0
- package/anims/chrome/infinite-tunnel.js +81 -0
- package/anims/chrome/inflated-type.js +141 -0
- package/anims/chrome/isometric-city.js +97 -0
- package/anims/chrome/jelly-cube.js +329 -0
- package/anims/chrome/klein-bottle.js +80 -0
- package/anims/chrome/liquid-metal.js +94 -0
- package/anims/chrome/low-poly-planet.js +163 -0
- package/anims/chrome/mandelbulb.js +79 -0
- package/anims/chrome/melting-logo.js +126 -0
- package/anims/chrome/menger-sponge.js +79 -0
- package/anims/chrome/neon-geodesic.js +149 -0
- package/anims/chrome/paper-fold.js +135 -0
- package/anims/chrome/path-spheres.js +112 -0
- package/anims/chrome/point-globe.js +81 -0
- package/anims/chrome/portal.js +105 -0
- package/anims/chrome/rubiks-cube.js +74 -0
- package/anims/chrome/sdf-morph.js +129 -0
- package/anims/chrome/shatter.js +120 -0
- package/anims/chrome/soap-bubble.js +86 -0
- package/anims/chrome/terrain-flyover.js +78 -0
- package/anims/chrome/tesseract.js +148 -0
- package/anims/chrome/torus-knot.js +89 -0
- package/anims/chrome/volumetric-clouds.js +70 -0
- package/anims/flow/bio-shore.js +140 -0
- package/anims/flow/calligraphy.js +116 -0
- package/anims/flow/chladni.js +152 -0
- package/anims/flow/cloud-chamber.js +101 -0
- package/anims/flow/confetti.js +46 -0
- package/anims/flow/curl-flow.js +52 -0
- package/anims/flow/dandelion.js +80 -0
- package/anims/flow/dla-crystal.js +57 -0
- package/anims/flow/drone-show.js +357 -0
- package/anims/flow/embers.js +63 -0
- package/anims/flow/firefly-sync.js +107 -0
- package/anims/flow/galaxy.js +69 -0
- package/anims/flow/galton-board.js +155 -0
- package/anims/flow/gpu-particles.js +73 -0
- package/anims/flow/harmonograph.js +61 -0
- package/anims/flow/holi.js +153 -0
- package/anims/flow/iron-filings.js +112 -0
- package/anims/flow/lightning.js +72 -0
- package/anims/flow/murmuration.js +105 -0
- package/anims/flow/orbitals.js +195 -0
- package/anims/flow/particle-life.js +87 -0
- package/anims/flow/physarum.js +171 -0
- package/anims/flow/pin-art.js +181 -0
- package/anims/flow/rainbow.js +127 -0
- package/anims/flow/roche-limit.js +161 -0
- package/anims/flow/sand-mandala.js +121 -0
- package/anims/flow/scramble-crossing.js +152 -0
- package/anims/flow/snowfall.js +84 -0
- package/anims/flow/sparks.js +71 -0
- package/anims/flow/spider-web.js +123 -0
- package/anims/flow/starry-night.js +121 -0
- package/anims/flow/states-of-matter.js +113 -0
- package/anims/flow/strange-attractor.js +54 -0
- package/anims/flow/wheat-field.js +186 -0
- package/anims/flow/wind-tunnel.js +227 -0
- package/anims/fluid/bubbles.js +152 -0
- package/anims/fluid/caustics.js +45 -0
- package/anims/fluid/dripping-text.js +53 -0
- package/anims/fluid/ferrofluid.js +56 -0
- package/anims/fluid/fire-smoke.js +83 -0
- package/anims/fluid/honey-drip.js +167 -0
- package/anims/fluid/ink-drop.js +50 -0
- package/anims/fluid/ink-fluid.js +125 -0
- package/anims/fluid/lava-lamp.js +56 -0
- package/anims/fluid/liquid-distort.js +64 -0
- package/anims/fluid/liquid-glass-fill.js +53 -0
- package/anims/fluid/marbling.js +53 -0
- package/anims/fluid/mercury.js +177 -0
- package/anims/fluid/ocean-waves.js +50 -0
- package/anims/fluid/paint-swirl.js +153 -0
- package/anims/fluid/raindrops-glass.js +60 -0
- package/anims/fluid/ripples.js +57 -0
- package/anims/fluid/sph-water.js +75 -0
- package/anims/fluid/underwater.js +55 -0
- package/anims/fluid/water-surface-3d.js +98 -0
- package/anims/hover/cursor-trail.js +37 -0
- package/anims/hover/magnetic.js +41 -0
- package/anims/hover/ripple.js +31 -0
- package/anims/hover/spotlight.js +31 -0
- package/anims/hover/tilt.js +36 -0
- package/anims/micro/arc-spinner.js +22 -0
- package/anims/micro/like-burst.js +42 -0
- package/anims/micro/skeleton.js +36 -0
- package/anims/micro/submit-success.js +45 -0
- package/anims/micro/toggle.js +35 -0
- package/anims/morph/gooey.js +36 -0
- package/anims/morph/icon-morph.js +45 -0
- package/anims/morph/line-draw.js +34 -0
- package/anims/morph/liquid-button.js +53 -0
- package/anims/morph/shape-morph.js +61 -0
- package/anims/registry-more.js +327 -0
- package/anims/registry.js +294 -0
- package/anims/scroll/parallax.js +50 -0
- package/anims/scroll/pinned-track.js +40 -0
- package/anims/scroll/scrub-reveal.js +34 -0
- package/anims/scroll/velocity-skew.js +38 -0
- package/anims/scroll/zoom-through.js +41 -0
- package/anims/shaders/aurora.js +47 -0
- package/anims/shaders/dither.js +69 -0
- package/anims/shaders/domain-warp.js +52 -0
- package/anims/shaders/guilloche.js +42 -0
- package/anims/shaders/kaleidoscope.js +64 -0
- package/anims/shaders/reaction-diffusion.js +85 -0
- package/anims/shaders/rgb-glitch.js +56 -0
- package/anims/shaders/truchet.js +57 -0
- package/anims/shaders/voronoi.js +50 -0
- package/anims/springs/ball-collisions.js +47 -0
- package/anims/springs/pendulum-wave.js +35 -0
- package/anims/springs/rubber-band.js +50 -0
- package/anims/springs/squash-bounce.js +50 -0
- package/anims/springs/verlet-rope.js +46 -0
- package/anims/text/letter-rise.js +40 -0
- package/anims/text/line-mask.js +26 -0
- package/anims/text/scramble.js +32 -0
- package/anims/text/typewriter.js +35 -0
- package/anims/text/vf-wave.js +26 -0
- package/anims/transitions/circle-reveal.js +42 -0
- package/anims/transitions/push-slide.js +40 -0
- package/anims/transitions/shared-expand.js +43 -0
- package/anims/transitions/staircase-wipe.js +41 -0
- package/anims/transitions/view-transition.js +43 -0
- package/bin.mjs +155 -0
- package/js/code.js +122 -0
- package/package.json +10 -4
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Ink fluid: a Navier–Stokes "stable fluids" solver on the GPU. Each frame: curl, vorticity, divergence,
|
|
2
|
+
// a pressure solve to keep it incompressible, then advect velocity and dye. Rendered as ink soaking into paper.
|
|
3
|
+
const PARAMS = { curl: 30, dissipation: 0.97, splatRadius: 0.25 };
|
|
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;touch-action:none';
|
|
8
|
+
stage.append(canvas);
|
|
9
|
+
const gl = canvas.getContext('webgl2', { alpha: false, antialias: false, depth: false });
|
|
10
|
+
if (!gl || !gl.getExtension('EXT_color_buffer_float')) { stage.textContent = 'This needs WebGL2 with float render targets.'; return () => canvas.remove(); }
|
|
11
|
+
|
|
12
|
+
const VS = `#version 300 es
|
|
13
|
+
in vec2 p; out vec2 uv, vL, vR, vT, vB; uniform vec2 texel;
|
|
14
|
+
void main() { uv = p * .5 + .5; vL = uv - vec2(texel.x, 0); vR = uv + vec2(texel.x, 0); vT = uv + vec2(0, texel.y); vB = uv - vec2(0, texel.y); gl_Position = vec4(p, 0, 1); }`;
|
|
15
|
+
const head = `#version 300 es
|
|
16
|
+
precision highp float; precision highp sampler2D; in vec2 uv, vL, vR, vT, vB; out vec4 o;`;
|
|
17
|
+
const FS = {
|
|
18
|
+
splat: `uniform sampler2D tgt; uniform float aspect, radius; uniform vec3 color; uniform vec2 point;
|
|
19
|
+
void main() { vec2 d = uv - point; d.x *= aspect; o = vec4(texture(tgt, uv).xyz + exp(-dot(d, d) / radius) * color, 1); }`,
|
|
20
|
+
advect: `uniform sampler2D vel, src; uniform vec2 simTexel; uniform float dt, keep;
|
|
21
|
+
void main() { vec2 c = uv - dt * texture(vel, uv).xy * simTexel; o = keep * texture(src, c); }`,
|
|
22
|
+
divergence: `uniform sampler2D vel;
|
|
23
|
+
void main() { float L = texture(vel, vL).x, R = texture(vel, vR).x, T = texture(vel, vT).y, B = texture(vel, vB).y;
|
|
24
|
+
vec2 C = texture(vel, uv).xy; if (vL.x < 0.) L = -C.x; if (vR.x > 1.) R = -C.x; if (vT.y > 1.) T = -C.y; if (vB.y < 0.) B = -C.y;
|
|
25
|
+
o = vec4(.5 * (R - L + T - B), 0, 0, 1); }`,
|
|
26
|
+
curl: `uniform sampler2D vel;
|
|
27
|
+
void main() { o = vec4(.5 * (texture(vel, vR).y - texture(vel, vL).y - texture(vel, vT).x + texture(vel, vB).x), 0, 0, 1); }`,
|
|
28
|
+
vorticity: `uniform sampler2D vel, crl; uniform float strength, dt;
|
|
29
|
+
void main() { float L = texture(crl, vL).x, R = texture(crl, vR).x, T = texture(crl, vT).x, B = texture(crl, vB).x, C = texture(crl, uv).x;
|
|
30
|
+
vec2 f = .5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); f /= length(f) + 1e-4; f *= strength * C; f.y *= -1.;
|
|
31
|
+
o = vec4(texture(vel, uv).xy + f * dt, 0, 1); }`,
|
|
32
|
+
pressure: `uniform sampler2D prs, div;
|
|
33
|
+
void main() { o = vec4((texture(prs, vL).x + texture(prs, vR).x + texture(prs, vB).x + texture(prs, vT).x - texture(div, uv).x) * .25, 0, 0, 1); }`,
|
|
34
|
+
gradient: `uniform sampler2D prs, vel;
|
|
35
|
+
void main() { vec2 v = texture(vel, uv).xy - .5 * vec2(texture(prs, vR).x - texture(prs, vL).x, texture(prs, vT).x - texture(prs, vB).x); o = vec4(v, 0, 1); }`,
|
|
36
|
+
scale: `uniform sampler2D src; uniform float k; void main() { o = k * texture(src, uv); }`,
|
|
37
|
+
display: `uniform sampler2D dye;
|
|
38
|
+
void main() { vec3 d = texture(dye, uv).rgb; vec3 paper = vec3(.965, .972, .985);
|
|
39
|
+
o = vec4(paper * exp(-d * 1.6), 1); }`,
|
|
40
|
+
};
|
|
41
|
+
const compile = (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; };
|
|
42
|
+
const vs = compile(gl.VERTEX_SHADER, VS);
|
|
43
|
+
const progs = {};
|
|
44
|
+
for (const k in FS) {
|
|
45
|
+
const p = gl.createProgram(); gl.attachShader(p, vs); gl.attachShader(p, compile(gl.FRAGMENT_SHADER, head + FS[k])); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p);
|
|
46
|
+
const u = {}; const n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS); for (let i = 0; i < n; i++) { const name = gl.getActiveUniform(p, i).name; u[name] = gl.getUniformLocation(p, name); }
|
|
47
|
+
progs[k] = { p, u };
|
|
48
|
+
}
|
|
49
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
50
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
51
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
52
|
+
|
|
53
|
+
const fbo = (w, h) => {
|
|
54
|
+
const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
55
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
56
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
57
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, w, h, 0, gl.RGBA, gl.HALF_FLOAT, null);
|
|
58
|
+
const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
59
|
+
gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT);
|
|
60
|
+
return { tex, fb, w, h };
|
|
61
|
+
};
|
|
62
|
+
const pair = (w, h) => { let a = fbo(w, h), b = fbo(w, h); return { get read() { return a; }, get write() { return b; }, swap() { [a, b] = [b, a]; } }; };
|
|
63
|
+
|
|
64
|
+
let W = 0, H = 0, vel, dye, div, crl, prs;
|
|
65
|
+
const resize = () => {
|
|
66
|
+
const d = Math.min(devicePixelRatio, 1.5); W = Math.max(2, stage.clientWidth * d | 0); H = Math.max(2, stage.clientHeight * d | 0);
|
|
67
|
+
canvas.width = W; canvas.height = H;
|
|
68
|
+
const sim = 128, dyeRes = 512, ar = W / H;
|
|
69
|
+
const sw = ar > 1 ? Math.round(sim * ar) : sim, sh = ar > 1 ? sim : Math.round(sim / ar);
|
|
70
|
+
const dw = ar > 1 ? Math.round(dyeRes * ar) : dyeRes, dh = ar > 1 ? dyeRes : Math.round(dyeRes / ar);
|
|
71
|
+
vel = pair(sw, sh); prs = pair(sw, sh); div = fbo(sw, sh); crl = fbo(sw, sh); dye = pair(dw, dh);
|
|
72
|
+
};
|
|
73
|
+
resize();
|
|
74
|
+
const ro = new ResizeObserver(resize); ro.observe(stage);
|
|
75
|
+
|
|
76
|
+
const bindTex = (unit, t) => { gl.activeTexture(gl.TEXTURE0 + unit); gl.bindTexture(gl.TEXTURE_2D, t.tex); return unit; };
|
|
77
|
+
const run = (prog, target, set) => {
|
|
78
|
+
gl.useProgram(prog.p); const w = target ? target.w : W, h = target ? target.h : H;
|
|
79
|
+
gl.uniform2f(prog.u.texel, 1 / w, 1 / h);
|
|
80
|
+
set(prog.u);
|
|
81
|
+
gl.viewport(0, 0, w, h); gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.fb : null);
|
|
82
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
83
|
+
};
|
|
84
|
+
const splat = (x, y, dx, dy, color) => {
|
|
85
|
+
const r = PARAMS.splatRadius / 100 * (W > H ? W / H : 1);
|
|
86
|
+
run(progs.splat, vel.write, u => { gl.uniform1i(u.tgt, bindTex(0, vel.read)); gl.uniform1f(u.aspect, W / H); gl.uniform2f(u.point, x, y); gl.uniform3f(u.color, dx, dy, 0); gl.uniform1f(u.radius, r); }); vel.swap();
|
|
87
|
+
run(progs.splat, dye.write, u => { gl.uniform1i(u.tgt, bindTex(0, dye.read)); gl.uniform1f(u.aspect, W / H); gl.uniform2f(u.point, x, y); gl.uniform3f(u.color, ...color); gl.uniform1f(u.radius, r); }); dye.swap();
|
|
88
|
+
};
|
|
89
|
+
// ink colours as absorption: what each ink removes from white paper
|
|
90
|
+
const inks = [[0.9, 0.45, 0.05], [0.05, 0.55, 0.9], [0.1, 0.85, 0.65], [0.85, 0.15, 0.6]];
|
|
91
|
+
const randomSplat = () => { const a = Math.random() * Math.PI * 2, f = 900 + Math.random() * 900; splat(0.2 + Math.random() * 0.6, 0.2 + Math.random() * 0.6, Math.cos(a) * f, Math.sin(a) * f, inks[Math.floor(Math.random() * inks.length)].map(c => c * 0.9)); };
|
|
92
|
+
|
|
93
|
+
let lastPtr = null, ptrInk = inks[1];
|
|
94
|
+
const onMove = e => {
|
|
95
|
+
const b = canvas.getBoundingClientRect(), x = (e.clientX - b.left) / b.width, y = 1 - (e.clientY - b.top) / b.height;
|
|
96
|
+
if (lastPtr) splat(x, y, (x - lastPtr[0]) * 6000, (y - lastPtr[1]) * 6000, ptrInk.map(c => c * 0.35));
|
|
97
|
+
lastPtr = [x, y];
|
|
98
|
+
};
|
|
99
|
+
const onLeave = () => { lastPtr = null; ptrInk = inks[Math.floor(Math.random() * inks.length)]; };
|
|
100
|
+
canvas.addEventListener('pointermove', onMove); canvas.addEventListener('pointerleave', onLeave);
|
|
101
|
+
|
|
102
|
+
let last = performance.now(), acc = 0, raf;
|
|
103
|
+
for (let i = 0; i < 4; i++) randomSplat();
|
|
104
|
+
function frame(now) {
|
|
105
|
+
const dt = Math.min(0.016, (now - last) / 1000); last = now;
|
|
106
|
+
acc += dt; if (acc > 0.45) { acc = 0; randomSplat(); }
|
|
107
|
+
const simTexel = [1 / vel.read.w, 1 / vel.read.h];
|
|
108
|
+
run(progs.curl, crl, u => gl.uniform1i(u.vel, bindTex(0, vel.read)));
|
|
109
|
+
run(progs.vorticity, vel.write, u => { gl.uniform1i(u.vel, bindTex(0, vel.read)); gl.uniform1i(u.crl, bindTex(1, crl)); gl.uniform1f(u.strength, PARAMS.curl); gl.uniform1f(u.dt, dt); }); vel.swap();
|
|
110
|
+
run(progs.divergence, div, u => gl.uniform1i(u.vel, bindTex(0, vel.read)));
|
|
111
|
+
run(progs.scale, prs.write, u => { gl.uniform1i(u.src, bindTex(0, prs.read)); gl.uniform1f(u.k, 0.8); }); prs.swap();
|
|
112
|
+
for (let i = 0; i < 20; i++) { run(progs.pressure, prs.write, u => { gl.uniform1i(u.prs, bindTex(0, prs.read)); gl.uniform1i(u.div, bindTex(1, div)); }); prs.swap(); }
|
|
113
|
+
run(progs.gradient, vel.write, u => { gl.uniform1i(u.prs, bindTex(0, prs.read)); gl.uniform1i(u.vel, bindTex(1, vel.read)); }); vel.swap();
|
|
114
|
+
run(progs.advect, vel.write, u => { gl.uniform1i(u.vel, bindTex(0, vel.read)); gl.uniform1i(u.src, bindTex(0, vel.read)); gl.uniform2f(u.simTexel, ...simTexel); gl.uniform1f(u.dt, dt); gl.uniform1f(u.keep, 0.99); }); vel.swap();
|
|
115
|
+
run(progs.advect, dye.write, u => { gl.uniform1i(u.vel, bindTex(0, vel.read)); gl.uniform1i(u.src, bindTex(1, dye.read)); gl.uniform2f(u.simTexel, ...simTexel); gl.uniform1f(u.dt, dt); gl.uniform1f(u.keep, Math.pow(PARAMS.dissipation, dt * 60 * 0.25)); }); dye.swap();
|
|
116
|
+
run(progs.display, null, u => gl.uniform1i(u.dye, bindTex(0, dye.read)));
|
|
117
|
+
raf = requestAnimationFrame(frame);
|
|
118
|
+
}
|
|
119
|
+
raf = requestAnimationFrame(frame);
|
|
120
|
+
return () => {
|
|
121
|
+
cancelAnimationFrame(raf); ro.disconnect();
|
|
122
|
+
canvas.removeEventListener('pointermove', onMove); canvas.removeEventListener('pointerleave', onLeave);
|
|
123
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove();
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Lava lamp: metaballs. Every blob adds a field that falls off with distance; where the sum passes a threshold
|
|
2
|
+
// the pixel is lava. Because fields add up, nearby blobs bulge toward each other and merge smoothly.
|
|
3
|
+
const PARAMS = { blobs: 8, speed: 0.5 };
|
|
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 MAX = 14;
|
|
12
|
+
const vs = `#version 300 es
|
|
13
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
14
|
+
const fs = `#version 300 es
|
|
15
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
16
|
+
uniform vec3 B[${MAX}]; uniform int N; uniform vec2 R;
|
|
17
|
+
void main() {
|
|
18
|
+
vec2 q = uv * vec2(R.x / R.y, 1.);
|
|
19
|
+
float f = 0.;
|
|
20
|
+
for (int i = 0; i < ${MAX}; i++) { if (i >= N) break; vec2 d = q - B[i].xy; f += B[i].z * B[i].z / (dot(d, d) + 1e-4); }
|
|
21
|
+
vec3 bg = mix(vec3(.04, .09, .2), vec3(.09, .18, .36), uv.y);
|
|
22
|
+
float edge = smoothstep(.95, 1.05, f);
|
|
23
|
+
// height rises from 0 at the edge toward 1 inside; its screen-space slope gives a smooth normal
|
|
24
|
+
float hgt = clamp(1. - 1. / max(f, 1e-3), 0., 1.);
|
|
25
|
+
vec3 n = normalize(vec3(-dFdx(hgt) * R.y * .35, -dFdy(hgt) * R.y * .35, 1.));
|
|
26
|
+
float light = clamp(dot(n, normalize(vec3(-.45, .55, .7))), 0., 1.);
|
|
27
|
+
vec3 lava = mix(vec3(.95, .25, .2), vec3(1., .72, .25), clamp(hgt * .9 + uv.y * .25, 0., 1.));
|
|
28
|
+
lava *= .55 + .6 * light; lava += pow(light, 18.) * .35;
|
|
29
|
+
float glow = smoothstep(.25, 1., f) * (1. - edge) * .35;
|
|
30
|
+
o = vec4(mix(bg + glow * vec3(1., .4, .2), lava, edge), 1);
|
|
31
|
+
}`;
|
|
32
|
+
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; };
|
|
33
|
+
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);
|
|
34
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
35
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
36
|
+
const uB = gl.getUniformLocation(prog, 'B'), uN = gl.getUniformLocation(prog, 'N'), uR = gl.getUniformLocation(prog, 'R');
|
|
37
|
+
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); };
|
|
38
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
39
|
+
const n = Math.min(MAX, Math.round(PARAMS.blobs));
|
|
40
|
+
const blobs = Array.from({ length: n }, (_, i) => ({ x: 0.15 + 0.7 * ((i * 0.618) % 1), r: 0.07 + 0.05 * ((i * 0.37) % 1), f: 0.15 + 0.2 * ((i * 0.53) % 1), ph: i * 1.7 }));
|
|
41
|
+
const data = new Float32Array(MAX * 3);
|
|
42
|
+
let raf;
|
|
43
|
+
function frame(now) {
|
|
44
|
+
const t = now / 1000 * PARAMS.speed, ar = canvas.width / canvas.height;
|
|
45
|
+
blobs.forEach((b, i) => {
|
|
46
|
+
data[i * 3] = (b.x + 0.06 * Math.sin(t * 0.7 + b.ph)) * ar;
|
|
47
|
+
data[i * 3 + 1] = 0.5 - 0.42 * Math.cos(t * b.f * 2 + b.ph); // slow rise and fall
|
|
48
|
+
data[i * 3 + 2] = b.r * (1 + 0.15 * Math.sin(t * 1.3 + b.ph));
|
|
49
|
+
});
|
|
50
|
+
gl.uniform3fv(uB, data); gl.uniform1i(uN, n); gl.uniform2f(uR, canvas.width, canvas.height);
|
|
51
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
52
|
+
raf = requestAnimationFrame(frame);
|
|
53
|
+
}
|
|
54
|
+
raf = requestAnimationFrame(frame);
|
|
55
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
56
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Liquid image distortion: the picture is a texture, and every pixel samples it from a spot shifted by
|
|
2
|
+
// flowing noise. Near the pointer the shift grows, like a finger dragged through water.
|
|
3
|
+
const PARAMS = { strength: 0.04, speed: 0.4 };
|
|
4
|
+
|
|
5
|
+
export default function (stage) {
|
|
6
|
+
const canvas = document.createElement('canvas');
|
|
7
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
8
|
+
stage.append(canvas);
|
|
9
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
10
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
11
|
+
|
|
12
|
+
// The "image": painted once on a 2D canvas, then uploaded as a texture.
|
|
13
|
+
const pic = document.createElement('canvas'); pic.width = 1024; pic.height = 768;
|
|
14
|
+
const p = pic.getContext('2d');
|
|
15
|
+
const bands = ['#0b3d91', '#1c7ed6', '#4dabf7', '#a5d8ff', '#e7f5ff', '#ffd43b', '#ff922b'];
|
|
16
|
+
bands.forEach((c, i) => { p.fillStyle = c; p.fillRect(0, i * 768 / bands.length, 1024, 768 / bands.length + 1); });
|
|
17
|
+
p.fillStyle = '#ffffff'; p.font = '900 230px "Mona Sans", system-ui, sans-serif'; p.textAlign = 'center'; p.textBaseline = 'middle';
|
|
18
|
+
p.fillText('LIQUID', 512, 384);
|
|
19
|
+
|
|
20
|
+
const vs = `#version 300 es
|
|
21
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
22
|
+
const fs = `#version 300 es
|
|
23
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
24
|
+
uniform sampler2D img; uniform float T, S; uniform vec2 M, R;
|
|
25
|
+
vec2 hash(vec2 p) { p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3))); return -1. + 2. * fract(sin(p) * 43758.5453); }
|
|
26
|
+
float noise(vec2 p) { vec2 i = floor(p), f = fract(p), u = f * f * (3. - 2. * f);
|
|
27
|
+
return mix(mix(dot(hash(i), f), dot(hash(i + vec2(1, 0)), f - vec2(1, 0)), u.x), mix(dot(hash(i + vec2(0, 1)), f - vec2(0, 1)), dot(hash(i + vec2(1, 1)), f - vec2(1, 1)), u.x), u.y); }
|
|
28
|
+
void main() {
|
|
29
|
+
vec2 q = uv * vec2(R.x / R.y, 1.);
|
|
30
|
+
vec2 n = vec2(noise(q * 2.2 + vec2(T, T * .7)), noise(q * 2.2 + vec2(-T * .8, T) + 7.3));
|
|
31
|
+
vec2 m = M * vec2(R.x / R.y, 1.); float near = exp(-dot(q - m, q - m) * 9.);
|
|
32
|
+
vec2 swirl = vec2(-(q - m).y, (q - m).x) * near * 1.6;
|
|
33
|
+
vec2 st = uv + n * S + swirl * S;
|
|
34
|
+
// cover-fit the 4:3 picture into the frame
|
|
35
|
+
vec2 ar = vec2(R.x / R.y / (4. / 3.), 1.); if (ar.x < 1.) ar = vec2(1., (4. / 3.) / (R.x / R.y));
|
|
36
|
+
vec2 tc = (st - .5) * (ar.x > 1. ? vec2(1., 1. / ar.x) : vec2(1. / ar.y, 1.)) + .5;
|
|
37
|
+
o = vec4(texture(img, vec2(tc.x, 1. - tc.y)).rgb, 1);
|
|
38
|
+
}`;
|
|
39
|
+
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; };
|
|
40
|
+
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);
|
|
41
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
42
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
43
|
+
const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
44
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, pic);
|
|
45
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
|
|
46
|
+
const U = n => gl.getUniformLocation(prog, n);
|
|
47
|
+
const uT = U('T'), uS = U('S'), uM = U('M'), uR = U('R');
|
|
48
|
+
|
|
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 mx = 0.5, my = 0.5, tx = 0.5, ty = 0.5, lastMove = -1e9, raf;
|
|
52
|
+
const onMove = e => { const b = canvas.getBoundingClientRect(); tx = (e.clientX - b.left) / b.width; ty = 1 - (e.clientY - b.top) / b.height; lastMove = performance.now(); };
|
|
53
|
+
canvas.addEventListener('pointermove', onMove);
|
|
54
|
+
function frame(now) {
|
|
55
|
+
const t = now / 1000;
|
|
56
|
+
if (now - lastMove > 1500) { tx = 0.5 + Math.cos(t * 0.7) * 0.3; ty = 0.5 + Math.sin(t * 1.1) * 0.25; }
|
|
57
|
+
mx += (tx - mx) * 0.08; my += (ty - my) * 0.08;
|
|
58
|
+
gl.uniform1f(uT, t * PARAMS.speed); gl.uniform1f(uS, PARAMS.strength); gl.uniform2f(uM, mx, my); gl.uniform2f(uR, canvas.width, canvas.height);
|
|
59
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
60
|
+
raf = requestAnimationFrame(frame);
|
|
61
|
+
}
|
|
62
|
+
raf = requestAnimationFrame(frame);
|
|
63
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
64
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Liquid in a glass: the surface is a row of springs (a 1D wave equation). Tilting the glass pushes the
|
|
2
|
+
// surface and the wave sloshes back and forth; bubbles rise and the fill level breathes up and down.
|
|
3
|
+
const PARAMS = { slosh: 1, fill: 0.6 };
|
|
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;
|
|
11
|
+
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); };
|
|
12
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
13
|
+
const N = 48, hgt = new Float32Array(N), vel = new Float32Array(N);
|
|
14
|
+
let bubbles = [], tiltV = 0, prevTilt = 0, last = performance.now(), raf;
|
|
15
|
+
function frame(now) {
|
|
16
|
+
const dt = Math.min(0.033, (now - last) / 1000); last = now; const t = now / 1000;
|
|
17
|
+
const tilt = Math.sin(t * 1.3) * 0.22 * PARAMS.slosh * (Math.sin(t * 0.31) > 0 ? 1 : 0.3);
|
|
18
|
+
const tiltAcc = (tilt - prevTilt) / dt - tiltV; tiltV = (tilt - prevTilt) / dt; prevTilt = tilt;
|
|
19
|
+
// springs: each column pulled to rest and to its neighbours; tilting acceleration shoves the water sideways
|
|
20
|
+
for (let s = 0; s < 4; s++) {
|
|
21
|
+
const h = dt / 4;
|
|
22
|
+
for (let i = 0; i < N; i++) { const l = hgt[Math.max(0, i - 1)], r = hgt[Math.min(N - 1, i + 1)]; vel[i] += ((l + r - 2 * hgt[i]) * 900 - hgt[i] * 6 - vel[i] * 1.6 + (i / (N - 1) - 0.5) * tiltAcc * 14) * h; }
|
|
23
|
+
for (let i = 0; i < N; i++) hgt[i] += vel[i] * h;
|
|
24
|
+
}
|
|
25
|
+
const S = Math.min(W, H), gw = S * 0.42, gh = S * 0.62, cx = W / 2, base = H * 0.85;
|
|
26
|
+
const level = PARAMS.fill * (0.85 + 0.15 * Math.sin(t * 0.4));
|
|
27
|
+
ctx.fillStyle = '#e9eef3'; ctx.fillRect(0, 0, W, H);
|
|
28
|
+
ctx.save(); ctx.translate(cx, base); ctx.rotate(tilt);
|
|
29
|
+
// glass: slightly tapered tumbler
|
|
30
|
+
const glass = new Path2D(); glass.moveTo(-gw / 2, -gh); glass.lineTo(gw / 2, -gh); glass.lineTo(gw * 0.42, 0); glass.lineTo(-gw * 0.42, 0); glass.closePath();
|
|
31
|
+
ctx.fillStyle = 'rgba(210, 225, 238, .35)'; ctx.fill(glass);
|
|
32
|
+
ctx.save(); ctx.clip(glass);
|
|
33
|
+
const top = -gh * level;
|
|
34
|
+
ctx.beginPath(); ctx.moveTo(-gw, 4);
|
|
35
|
+
for (let i = 0; i < N; i++) { const x = -gw / 2 + (gw * i) / (N - 1), y = top + hgt[i] - Math.sin(-tilt) * x * 0.95; ctx.lineTo(x, y); } // the surface stays level as the glass tilts
|
|
36
|
+
ctx.lineTo(gw, 4); ctx.closePath();
|
|
37
|
+
const juice = ctx.createLinearGradient(0, top, 0, 0); juice.addColorStop(0, '#ffb347'); juice.addColorStop(1, '#f2711c');
|
|
38
|
+
ctx.fillStyle = juice; ctx.fill();
|
|
39
|
+
ctx.strokeStyle = 'rgba(255,240,200,.9)'; ctx.lineWidth = 2; ctx.stroke();
|
|
40
|
+
if (Math.random() < dt * 18) bubbles.push({ x: (Math.random() - 0.5) * gw * 0.7, y: -4, r: 1 + Math.random() * 3, v: 30 + Math.random() * 40 });
|
|
41
|
+
ctx.fillStyle = 'rgba(255, 245, 220, .7)';
|
|
42
|
+
for (const b of bubbles) { b.y -= b.v * dt; b.x += Math.sin(b.y * 0.1) * 0.2; ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, 7); ctx.fill(); }
|
|
43
|
+
bubbles = bubbles.filter(b => b.y > top + 6);
|
|
44
|
+
ctx.restore();
|
|
45
|
+
ctx.strokeStyle = 'rgba(120, 140, 160, .6)'; ctx.lineWidth = 3; ctx.stroke(glass);
|
|
46
|
+
ctx.fillStyle = 'rgba(255,255,255,.55)'; ctx.fillRect(-gw * 0.36, -gh * 0.92, gw * 0.05, gh * 0.8); // highlight
|
|
47
|
+
ctx.restore();
|
|
48
|
+
ctx.fillStyle = 'rgba(40, 50, 60, .1)'; ctx.beginPath(); ctx.ellipse(cx, base + 6, gw * 0.5, 8, 0, 0, 7); ctx.fill();
|
|
49
|
+
raf = requestAnimationFrame(frame);
|
|
50
|
+
}
|
|
51
|
+
raf = requestAnimationFrame(frame);
|
|
52
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Paper marbling: bands of floating paint, combed and swirled. Each pixel traces backward through a comb pass
|
|
2
|
+
// (sideways waves), a swirl around the stylus and a slow drift, then reads its colour from the original stripes.
|
|
3
|
+
const PARAMS = { comb: 1, swirl: 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, M; uniform float T, C, S;
|
|
15
|
+
vec3 paint(float v) {
|
|
16
|
+
vec3 cols[5] = vec3[5](vec3(.95, .9, .8), vec3(.62, .16, .14), vec3(.06, .33, .4), vec3(.86, .6, .2), vec3(.12, .1, .1));
|
|
17
|
+
float f = fract(v) * 5.; int i = int(f); float k = smoothstep(.82, 1., fract(f));
|
|
18
|
+
return mix(cols[i], cols[(i + 1) % 5], k);
|
|
19
|
+
}
|
|
20
|
+
void main() {
|
|
21
|
+
vec2 a = vec2(R.x / R.y, 1.), q = uv * a;
|
|
22
|
+
// undo the stylus swirl: rotate around the pointer, more strongly close to it
|
|
23
|
+
vec2 d = q - M * a; float r = length(d), ang = S * 2.6 * exp(-r * r * 9.) * sin(T * .5);
|
|
24
|
+
q = M * a + mat2(cos(ang), -sin(ang), sin(ang), cos(ang)) * d;
|
|
25
|
+
// undo the comb: alternating sideways waves
|
|
26
|
+
q.x += C * .06 * sin(q.y * 18. + T * .4) + C * .03 * sin(q.y * 41. - T * .3);
|
|
27
|
+
q.y += C * .05 * sin(q.x * 9. + T * .25);
|
|
28
|
+
// the original floated bands, with a little drift
|
|
29
|
+
float v = q.x * 2.2 + .25 * sin(q.y * 3. + T * .1) + T * .015;
|
|
30
|
+
vec3 c = paint(v);
|
|
31
|
+
float grain = fract(sin(dot(uv * R, vec2(12.9, 78.2))) * 43758.5);
|
|
32
|
+
o = vec4(c * (.96 + .04 * grain), 1);
|
|
33
|
+
}`;
|
|
34
|
+
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; };
|
|
35
|
+
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);
|
|
36
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
37
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
38
|
+
const u = n => gl.getUniformLocation(prog, n), uR = u('R'), uM = u('M'), uT = u('T'), uC = u('C'), uS = u('S');
|
|
39
|
+
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); };
|
|
40
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
41
|
+
let mx = 0.5, my = 0.5, tx = mx, ty = my, lastMove = -1e9, raf;
|
|
42
|
+
const onMove = e => { const b = canvas.getBoundingClientRect(); tx = (e.clientX - b.left) / b.width; ty = 1 - (e.clientY - b.top) / b.height; lastMove = performance.now(); };
|
|
43
|
+
canvas.addEventListener('pointermove', onMove);
|
|
44
|
+
function frame(now) {
|
|
45
|
+
const t = now / 1000;
|
|
46
|
+
if (now - lastMove > 1500) { tx = 0.5 + Math.cos(t * 0.35) * 0.25; ty = 0.5 + Math.sin(t * 0.5) * 0.25; }
|
|
47
|
+
mx += (tx - mx) * 0.05; my += (ty - my) * 0.05;
|
|
48
|
+
gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform2f(uM, mx, my); gl.uniform1f(uT, t); gl.uniform1f(uC, PARAMS.comb); gl.uniform1f(uS, PARAMS.swirl);
|
|
49
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3); raf = requestAnimationFrame(frame);
|
|
50
|
+
}
|
|
51
|
+
raf = requestAnimationFrame(frame);
|
|
52
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
53
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Mercury: beads of liquid metal on a tray. JavaScript moves the beads (they pull together and merge, and a
|
|
2
|
+
// fast stroke splits them); the shader turns them into one metaball surface, builds a height from it and
|
|
3
|
+
// reflects a studio (pale floor at the rim, dark room, bright softboxes on top) off its normal.
|
|
4
|
+
const PARAMS = { beads: 14, cohesion: 1, tilt: 1 };
|
|
5
|
+
|
|
6
|
+
export default function (stage) {
|
|
7
|
+
const canvas = document.createElement('canvas');
|
|
8
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none';
|
|
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 MAX = 24;
|
|
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 int N; uniform vec4 B[${MAX}]; uniform vec2 V[${MAX}];
|
|
18
|
+
// metaball field; w returns the field-weighted radius so big puddles can be flatter than small beads
|
|
19
|
+
float field(vec2 p, out float w) {
|
|
20
|
+
float f = 0., wr = 0.;
|
|
21
|
+
for (int i = 0; i < ${MAX}; i++) {
|
|
22
|
+
if (i >= N) break;
|
|
23
|
+
vec2 d = p - B[i].xy, v = V[i]; float s = length(v);
|
|
24
|
+
if (s > 1e-3) { vec2 vn = v / s; d -= vn * dot(d, vn) * (1. - 1. / (1. + s * .9)); } // moving beads stretch along their path
|
|
25
|
+
float r = B[i].z, k = r * r / (dot(d, d) + 1e-6);
|
|
26
|
+
f += k; wr += k * r;
|
|
27
|
+
}
|
|
28
|
+
w = wr / max(f, 1e-6); return f;
|
|
29
|
+
}
|
|
30
|
+
float height(vec2 p) { float w; float f = field(p, w); return f > 1. ? min(w, .065) * sqrt(1. - 1. / f) : 0.; }
|
|
31
|
+
vec3 env(vec3 d) {
|
|
32
|
+
float up = d.z;
|
|
33
|
+
vec3 floorC = vec3(.84, .83, .8), room = vec3(.06, .065, .075), ceil = vec3(.62, .65, .69);
|
|
34
|
+
vec3 c = up < 0. ? mix(room, floorC, smoothstep(0., -.35, up)) : mix(room, ceil, smoothstep(.03, .32, up));
|
|
35
|
+
vec2 q = d.xy / max(d.z, .06); // where the ray meets the ceiling
|
|
36
|
+
float key = smoothstep(.75, .4, length((q - vec2(-.4, .45)) * vec2(1., 1.5))), fill = smoothstep(.5, .2, length((q - vec2(.6, -.1)) * vec2(1.4, 1.)));
|
|
37
|
+
float strip = smoothstep(.08, .035, abs(q.y + .62 + .12 * q.x)) * smoothstep(1.6, 1.2, abs(q.x - .25));
|
|
38
|
+
c += step(0., up) * (vec3(1.) * key * 1.6 + vec3(.9, .95, 1.) * fill * .7 + vec3(1., .96, .9) * strip * 1.2);
|
|
39
|
+
float az = atan(d.y, d.x), hz = exp(-pow((up - .16) / .07, 2.)); // a window glow just above the horizon
|
|
40
|
+
c += hz * vec3(.45, .55, .7) * smoothstep(.3, .95, sin(az * 2. + .6)) * .8;
|
|
41
|
+
return c;
|
|
42
|
+
}
|
|
43
|
+
float hash(vec2 p) { return fract(sin(dot(p, vec2(41.3, 289.1))) * 43758.5); }
|
|
44
|
+
void main() {
|
|
45
|
+
vec2 A = vec2(R.x / R.y, 1.), p = (uv - .5) * A;
|
|
46
|
+
float w, f = field(p, w);
|
|
47
|
+
// the tray: warm pale paper with grain and a soft vignette
|
|
48
|
+
vec3 bg = vec3(.86, .85, .82) * (.93 + .07 * smoothstep(.95, .2, length(uv - .5)));
|
|
49
|
+
bg += (hash(floor(uv * R)) - .5) * .025;
|
|
50
|
+
float ws, fs2 = field(p - vec2(-.018, .03), ws); // shadow: the field again, nudged away from the key light
|
|
51
|
+
bg *= 1. - smoothstep(.22, 1.15, fs2) * .38;
|
|
52
|
+
bg *= 1. - smoothstep(.78, 1., f) * .16; // contact darkening hugging each bead
|
|
53
|
+
float e = 1. / R.y;
|
|
54
|
+
vec3 n = normalize(vec3(-(height(p + vec2(e, 0)) - height(p - vec2(e, 0))) / (2. * e), -(height(p + vec2(0, e)) - height(p - vec2(0, e))) / (2. * e), 1.));
|
|
55
|
+
vec3 vd = normalize(vec3(p * .35, -1.));
|
|
56
|
+
vec3 rd = reflect(vd, n);
|
|
57
|
+
float fr = .78 + .22 * pow(1. - max(0., dot(-vd, n)), 4.); // metal: reflective everywhere, more at grazing angles
|
|
58
|
+
vec3 metal = env(rd) * fr * vec3(.9, .92, .95);
|
|
59
|
+
float aa = fwidth(f) * 1.2;
|
|
60
|
+
float m = smoothstep(1. - aa, 1. + aa, f);
|
|
61
|
+
vec3 c = mix(bg, metal, m);
|
|
62
|
+
o = vec4(pow(c, vec3(.95)), 1);
|
|
63
|
+
}`;
|
|
64
|
+
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; };
|
|
65
|
+
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);
|
|
66
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
67
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
68
|
+
const u = n => gl.getUniformLocation(prog, n), uR = u('R'), uN = u('N'), uB = u('B'), uV = u('V');
|
|
69
|
+
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); };
|
|
70
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
71
|
+
|
|
72
|
+
// beads live in shader units: the frame is 1 tall and aspect wide, centred on 0
|
|
73
|
+
const aspect = () => canvas.width / Math.max(1, canvas.height);
|
|
74
|
+
const beads = [];
|
|
75
|
+
const bead = (x, y, r, vx = 0, vy = 0) => ({ x, y, r, vx, vy, age: 0, jig: 0.6 });
|
|
76
|
+
for (let i = 0; i < Math.round(PARAMS.beads); i++) {
|
|
77
|
+
const a = aspect();
|
|
78
|
+
beads.push(bead((Math.random() - 0.5) * (a - 0.2), (Math.random() - 0.5) * 0.8, 0.022 + Math.random() * 0.042));
|
|
79
|
+
}
|
|
80
|
+
// pointer as a stylus; when idle, a virtual finger swipes through every few seconds
|
|
81
|
+
let px = 99, py = 99, pvx = 0, pvy = 0, lastMove = -1e9, lastT = 0;
|
|
82
|
+
const onMove = e => {
|
|
83
|
+
const b = canvas.getBoundingClientRect(), a = b.width / b.height, now = performance.now();
|
|
84
|
+
const x = ((e.clientX - b.left) / b.width - 0.5) * a, y = 0.5 - (e.clientY - b.top) / b.height;
|
|
85
|
+
const dt = Math.max(0.008, (now - lastT) / 1000);
|
|
86
|
+
if (now - lastMove < 100) { pvx = pvx * 0.5 + (x - px) / dt * 0.5; pvy = pvy * 0.5 + (y - py) / dt * 0.5; }
|
|
87
|
+
px = x; py = y; lastMove = lastT = now;
|
|
88
|
+
};
|
|
89
|
+
const onLeave = () => { px = py = 99; pvx = pvy = 0; };
|
|
90
|
+
canvas.addEventListener('pointermove', onMove); canvas.addEventListener('pointerleave', onLeave);
|
|
91
|
+
const rnd = n => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };
|
|
92
|
+
const t0 = performance.now(), sweep = { k: -1, ang: 0, off: 0 };
|
|
93
|
+
let raf, last = t0;
|
|
94
|
+
const Bf = new Float32Array(MAX * 4), Vf = new Float32Array(MAX * 2);
|
|
95
|
+
|
|
96
|
+
function step(dt, t) {
|
|
97
|
+
const a = aspect(), hw = a / 2, hh = 0.5;
|
|
98
|
+
let sx = px, sy = py, svx = pvx, svy = pvy;
|
|
99
|
+
if (performance.now() - lastMove > 1500) {
|
|
100
|
+
const ph = (t - 0.6) / 3.6, k = Math.floor(ph), f = ph - k, dur = 1.1 / 3.6;
|
|
101
|
+
if (ph >= 0 && f < dur) {
|
|
102
|
+
if (k !== sweep.k) { // each swipe is aimed through the biggest puddle
|
|
103
|
+
const big = beads.reduce((m, b) => b.r > m.r ? b : m, beads[0]);
|
|
104
|
+
sweep.k = k; sweep.ang = rnd(k) * Math.PI * 2; sweep.off = -Math.sin(sweep.ang) * big.x + Math.cos(sweep.ang) * big.y;
|
|
105
|
+
}
|
|
106
|
+
const ang = sweep.ang, off = sweep.off, s = f / dur;
|
|
107
|
+
const dx = Math.cos(ang), dy = Math.sin(ang), L = a * 0.65;
|
|
108
|
+
sx = -dy * off + dx * (s - 0.5) * 2 * L; sy = dx * off + dy * (s - 0.5) * 2 * L;
|
|
109
|
+
svx = dx * 2 * L / (dur * 3.6); svy = dy * 2 * L / (dur * 3.6);
|
|
110
|
+
} else { sx = sy = 99; svx = svy = 0; }
|
|
111
|
+
}
|
|
112
|
+
const sp = Math.hypot(svx, svy), gx = Math.sin(t * 0.37 + 1) * 0.16 * PARAMS.tilt, gy = Math.cos(t * 0.29) * 0.13 * PARAMS.tilt;
|
|
113
|
+
for (const b of beads) {
|
|
114
|
+
b.age += dt; b.jig *= Math.exp(-dt * 3.5);
|
|
115
|
+
let ax = gx, ay = gy;
|
|
116
|
+
const dx = b.x - sx, dy = b.y - sy, d = Math.hypot(dx, dy), reach = b.r + 0.055;
|
|
117
|
+
if (d < reach && d > 1e-5) { // the stylus shoves and drags the metal
|
|
118
|
+
const k = 1 - d / reach; ax += dx / d * k * 9 + svx * k * 2.5; ay += dy / d * k * 9 + svy * k * 2.5;
|
|
119
|
+
}
|
|
120
|
+
b.ax = ax; b.ay = ay;
|
|
121
|
+
}
|
|
122
|
+
// cohesion: beads that are close pull together, the pull shared by size
|
|
123
|
+
for (let i = 0; i < beads.length; i++) for (let j = i + 1; j < beads.length; j++) {
|
|
124
|
+
const A = beads[i], B = beads[j]; if (A.age < 0.35 || B.age < 0.35) continue;
|
|
125
|
+
// surfaces touch at about 1.35x the summed radii (where the metaballs bridge): touching beads snap together
|
|
126
|
+
const dx = B.x - A.x, dy = B.y - A.y, d = Math.hypot(dx, dy) + 1e-6, gap = d - (A.r + B.r) * 1.35;
|
|
127
|
+
if (gap > 0.09) continue;
|
|
128
|
+
const k = gap < 0 ? 6 : PARAMS.cohesion * 0.9 * (1 - gap / 0.09), ma = A.r * A.r, mb = B.r * B.r;
|
|
129
|
+
A.ax += dx / d * k * mb / (ma + mb); A.ay += dy / d * k * mb / (ma + mb);
|
|
130
|
+
B.ax -= dx / d * k * ma / (ma + mb); B.ay -= dy / d * k * ma / (ma + mb);
|
|
131
|
+
}
|
|
132
|
+
for (const b of beads) {
|
|
133
|
+
const damp = 1.4 + 0.012 / b.r; // tiny beads stick to the tray more than big ones
|
|
134
|
+
b.vx = (b.vx + b.ax * dt) * Math.exp(-damp * dt); b.vy = (b.vy + b.ay * dt) * Math.exp(-damp * dt);
|
|
135
|
+
b.x += b.vx * dt; b.y += b.vy * dt;
|
|
136
|
+
const m = b.r * 1.1 + 0.015;
|
|
137
|
+
if (b.x < -hw + m) { b.x = -hw + m; b.vx *= -0.35; } if (b.x > hw - m) { b.x = hw - m; b.vx *= -0.35; }
|
|
138
|
+
if (b.y < -hh + m) { b.y = -hh + m; b.vy *= -0.35; } if (b.y > hh - m) { b.y = hh - m; b.vy *= -0.35; }
|
|
139
|
+
}
|
|
140
|
+
// merge: centres close enough become one bead with the same area and momentum
|
|
141
|
+
for (let i = 0; i < beads.length; i++) for (let j = beads.length - 1; j > i; j--) {
|
|
142
|
+
const A = beads[i], B = beads[j], d = Math.hypot(B.x - A.x, B.y - A.y);
|
|
143
|
+
if (d < (A.r + B.r) * 0.42 && A.age > 0.35 && B.age > 0.35) {
|
|
144
|
+
const ma = A.r * A.r, mb = B.r * B.r, m = ma + mb;
|
|
145
|
+
A.x = (A.x * ma + B.x * mb) / m; A.y = (A.y * ma + B.y * mb) / m;
|
|
146
|
+
A.vx = (A.vx * ma + B.vx * mb) / m; A.vy = (A.vy * ma + B.vy * mb) / m;
|
|
147
|
+
A.r = Math.sqrt(m); A.jig = 1; beads.splice(j, 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// split: a fast stroke through a bead tears it in two, flung apart across the stroke
|
|
151
|
+
if (sp > 0.7) for (let i = beads.length - 1; i >= 0; i--) {
|
|
152
|
+
const b = beads[i];
|
|
153
|
+
if (beads.length >= MAX || b.r < 0.03 || b.age < 0.25 || Math.hypot(b.x - sx, b.y - sy) > b.r * 0.9 + 0.012) continue;
|
|
154
|
+
const f = 0.35 + Math.random() * 0.3, r1 = b.r * Math.sqrt(f), r2 = b.r * Math.sqrt(1 - f);
|
|
155
|
+
const nx = -svy / sp, ny = svx / sp, kick = 0.35 + Math.random() * 0.25;
|
|
156
|
+
beads.splice(i, 1,
|
|
157
|
+
bead(b.x + nx * r1 * 0.9, b.y + ny * r1 * 0.9, r1, b.vx + nx * kick + svx * 0.15, b.vy + ny * kick + svy * 0.15),
|
|
158
|
+
bead(b.x - nx * r2 * 0.9, b.y - ny * r2 * 0.9, r2, b.vx - nx * kick + svx * 0.15, b.vy - ny * kick + svy * 0.15));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function frame(now) {
|
|
163
|
+
const dt = Math.min(0.033, (now - last) / 1000); last = now; const t = (now - t0) / 1000;
|
|
164
|
+
step(dt / 2, t); step(dt / 2, t);
|
|
165
|
+
if (now - lastMove > 120) { pvx *= 0.8; pvy *= 0.8; }
|
|
166
|
+
beads.forEach((b, i) => {
|
|
167
|
+
const r = b.r * (1 + 0.07 * b.jig * Math.sin(b.age * 26));
|
|
168
|
+
Bf.set([b.x, b.y, r, 0], i * 4); Vf.set([b.vx, b.vy], i * 2);
|
|
169
|
+
});
|
|
170
|
+
gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform1i(uN, beads.length);
|
|
171
|
+
gl.uniform4fv(uB, Bf); gl.uniform2fv(uV, Vf);
|
|
172
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
173
|
+
raf = requestAnimationFrame(frame);
|
|
174
|
+
}
|
|
175
|
+
raf = requestAnimationFrame(frame);
|
|
176
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); canvas.removeEventListener('pointerleave', onLeave); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
177
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Ocean waves: Gerstner waves. Water points move in circles, not just up and down, which sharpens crests and
|
|
2
|
+
// flattens troughs. Several layers at different speeds and depths give parallax.
|
|
3
|
+
const PARAMS = { height: 1, choppiness: 0.6 };
|
|
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;
|
|
11
|
+
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); };
|
|
12
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
13
|
+
|
|
14
|
+
// each layer: base line, colour, speed and three component waves [amplitude, wavelength, speed factor]
|
|
15
|
+
const layers = [
|
|
16
|
+
{ y: 0.46, c: '#7fb3e6', s: 0.4, w: [[0.012, 0.35, 1], [0.006, 0.17, 1.4], [0.004, 0.09, 2]] },
|
|
17
|
+
{ y: 0.56, c: '#4f8fd1', s: 0.6, w: [[0.018, 0.42, 1], [0.009, 0.2, 1.3], [0.005, 0.11, 1.9]] },
|
|
18
|
+
{ y: 0.68, c: '#2b6cb8', s: 0.8, w: [[0.026, 0.5, 1], [0.012, 0.24, 1.2], [0.006, 0.13, 1.7]] },
|
|
19
|
+
{ y: 0.82, c: '#174d8f', s: 1, w: [[0.036, 0.62, 1], [0.016, 0.3, 1.25], [0.008, 0.15, 1.8]], foam: true },
|
|
20
|
+
];
|
|
21
|
+
let raf;
|
|
22
|
+
function frame(now) {
|
|
23
|
+
const t = now / 1000;
|
|
24
|
+
ctx.fillStyle = '#dbeafa'; ctx.fillRect(0, 0, W, H);
|
|
25
|
+
ctx.fillStyle = '#ffd8a8'; ctx.beginPath(); ctx.arc(W * 0.72, H * 0.3, Math.min(W, H) * 0.11, 0, Math.PI * 2); ctx.fill();
|
|
26
|
+
for (const L of layers) {
|
|
27
|
+
const pts = [];
|
|
28
|
+
for (let i = -4; i <= 124; i++) {
|
|
29
|
+
let x = (i / 120) * W, y = L.y * H;
|
|
30
|
+
for (const [a, wl, sp] of L.w) {
|
|
31
|
+
const A = a * H * PARAMS.height, k = (2 * Math.PI) / (wl * W), om = Math.sqrt(9.8 * k * H * 0.25) * sp * L.s * 0.35;
|
|
32
|
+
const ph = k * (i / 120) * W - om * t;
|
|
33
|
+
x -= PARAMS.choppiness * A * Math.sin(ph); // horizontal circle component: the Gerstner part
|
|
34
|
+
y -= A * Math.cos(ph);
|
|
35
|
+
}
|
|
36
|
+
pts.push([x, y]);
|
|
37
|
+
}
|
|
38
|
+
ctx.beginPath(); ctx.moveTo(pts[0][0], H);
|
|
39
|
+
for (const [x, y] of pts) ctx.lineTo(x, y);
|
|
40
|
+
ctx.lineTo(pts.at(-1)[0], H); ctx.closePath(); ctx.fillStyle = L.c; ctx.fill();
|
|
41
|
+
if (L.foam) {
|
|
42
|
+
ctx.strokeStyle = 'rgba(255,255,255,.85)'; ctx.lineWidth = 2; ctx.beginPath();
|
|
43
|
+
pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.stroke();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
raf = requestAnimationFrame(frame);
|
|
47
|
+
}
|
|
48
|
+
raf = requestAnimationFrame(frame);
|
|
49
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
50
|
+
}
|