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,69 @@
|
|
|
1
|
+
// Ordered dithering: a lit 3D scene (a torus knot-ish ring and a sphere over a floor, raymarched) reduced to a couple
|
|
2
|
+
// of ink tones per pixel. Each screen pixel compares its brightness with a fixed threshold from an 8×8 Bayer matrix;
|
|
3
|
+
// the matrix spreads thresholds so evenly that, from a distance, the on/off pattern reads as smooth grey. Pixels are
|
|
4
|
+
// drawn big on purpose, and the two-colour palette is an old paper-and-ink one.
|
|
5
|
+
const PARAMS = { pixel: 2, tones: 2 };
|
|
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;image-rendering:pixelated';
|
|
10
|
+
stage.append(canvas);
|
|
11
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
12
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
13
|
+
const vs = `#version 300 es
|
|
14
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
15
|
+
const fs = `#version 300 es
|
|
16
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
17
|
+
uniform vec2 R; uniform float T, LV;
|
|
18
|
+
mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
|
|
19
|
+
float map(vec3 p) {
|
|
20
|
+
vec3 q = p - vec3(0, 1.1, 0); q.xz *= rot(T * .5); q.xy *= rot(T * .3);
|
|
21
|
+
vec2 t = vec2(length(q.xz) - .8, q.y);
|
|
22
|
+
float a = atan(q.z, q.x); t = rot(a * 1.5 + T) * t; // a twisted ring with a squared section
|
|
23
|
+
float ring = length(max(abs(t) - vec2(.12, .26), 0.)) - .04;
|
|
24
|
+
float ball = length(p - vec3(sin(T * .7) * 1.6, .45 + abs(sin(T * 1.8)) * .5, cos(T * .7) * .8)) - .42;
|
|
25
|
+
return min(min(ring, ball), p.y);
|
|
26
|
+
}
|
|
27
|
+
vec3 nrm(vec3 p) { vec2 e = vec2(.001, -.001); return normalize(e.xyy * map(p + e.xyy) + e.yyx * map(p + e.yyx) + e.yxy * map(p + e.yxy) + e.xxx * map(p + e.xxx)); }
|
|
28
|
+
float shadow(vec3 ro, vec3 rd) { float r = 1., t = .03; for (int i = 0; i < 40; i++) { float h = map(ro + rd * t); r = min(r, 8. * h / t); t += clamp(h, .02, .3); if (r < .002 || t > 6.) break; } return clamp(r, 0., 1.); }
|
|
29
|
+
float bayer(vec2 p) { // the 8×8 Bayer threshold, built bit by bit
|
|
30
|
+
ivec2 i = ivec2(mod(p, 8.)); int v = 0;
|
|
31
|
+
for (int b = 0; b < 3; b++) { int x = (i.x >> b) & 1, y = (i.y >> b) & 1; v |= ((x ^ y) << (2 * (2 - b) + 1)) | (y << (2 * (2 - b))); }
|
|
32
|
+
return (float(v) + .5) / 64.;
|
|
33
|
+
}
|
|
34
|
+
void main() {
|
|
35
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
36
|
+
vec3 ro = vec3(sin(T * .1) * 3.3, 2.1, -cos(T * .1) * 3.3), ta = vec3(0, .85, 0);
|
|
37
|
+
vec3 f = normalize(ta - ro), r = normalize(cross(vec3(0, 1, 0), f)), u = cross(f, r), rd = normalize(f * 1.6 + r * q.x + u * q.y);
|
|
38
|
+
float t = 0., lum = .97 - .22 * uv.y; // a pale backdrop: sparse dots, so the objects stand out
|
|
39
|
+
for (int i = 0; i < 120; i++) { float d = map(ro + rd * t); if (d < .001) break; t += d; if (t > 20.) { t = -1.; break; } }
|
|
40
|
+
if (t > 0.) {
|
|
41
|
+
vec3 p = ro + rd * t, n = nrm(p), L = normalize(vec3(.6, 1, -.4));
|
|
42
|
+
float dif = max(dot(n, L), 0.) * shadow(p + n * .003, L), sp = pow(max(dot(reflect(rd, n), L), 0.), 24.);
|
|
43
|
+
lum = .02 + .75 * dif * dif + .6 * sp; // dark shadow sides against the pale paper
|
|
44
|
+
if (p.y < .002) lum = (.45 + .5 * dif) * (.95 - .15 * step(.5, fract(p.x * .5) + fract(p.z * .5) - .5)); // a faint checker floor
|
|
45
|
+
lum = mix(lum, .97 - .22 * uv.y, smoothstep(5., 12., t));
|
|
46
|
+
}
|
|
47
|
+
lum = pow(clamp(lum, 0., 1.), .9);
|
|
48
|
+
// quantise to LV tones, letting the Bayer threshold decide which way each pixel rounds
|
|
49
|
+
float lv = max(LV - 1., 1.), v = floor(lum * lv + bayer(gl_FragCoord.xy)) / lv;
|
|
50
|
+
vec3 ink = vec3(.11, .1, .16), paper = vec3(.93, .89, .8);
|
|
51
|
+
o = vec4(mix(ink, paper, clamp(v, 0., 1.)), 1);
|
|
52
|
+
}`;
|
|
53
|
+
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; };
|
|
54
|
+
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);
|
|
55
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
56
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
57
|
+
const U = {}; ['R', 'T', 'LV'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
58
|
+
// the canvas is rendered at 1/pixel resolution and scaled up without smoothing: that is the chunky dither grid
|
|
59
|
+
const resize = () => { const px = Math.max(1, PARAMS.pixel); canvas.width = Math.max(1, (stage.clientWidth || 1) / px | 0); canvas.height = Math.max(1, (stage.clientHeight || 1) / px | 0); gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
60
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
61
|
+
let raf; const t0 = performance.now();
|
|
62
|
+
function frame(now) {
|
|
63
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, (now - t0) / 1000); gl.uniform1f(U.LV, Math.round(PARAMS.tones));
|
|
64
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
65
|
+
raf = requestAnimationFrame(frame);
|
|
66
|
+
}
|
|
67
|
+
raf = requestAnimationFrame(frame);
|
|
68
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
69
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Domain warping (after Inigo Quilez): instead of reading noise at p, read it at p pushed by another noise, which was
|
|
2
|
+
// itself read at a pushed point: f(p + g(p + h(p))). Each layer of pushing folds the pattern into marbled, smoky
|
|
3
|
+
// filaments. The intermediate warp vectors are reused for colour, so the dye follows the folds, and everything drifts
|
|
4
|
+
// slowly so the marble keeps flowing.
|
|
5
|
+
const PARAMS = { warp: 1, speed: 1 };
|
|
6
|
+
|
|
7
|
+
export default function (stage) {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
10
|
+
stage.append(canvas);
|
|
11
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
12
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
13
|
+
const vs = `#version 300 es
|
|
14
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
15
|
+
const fs = `#version 300 es
|
|
16
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
17
|
+
uniform vec2 R; uniform float T, K;
|
|
18
|
+
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
19
|
+
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); }
|
|
20
|
+
const mat2 M = mat2(.8, .6, -.6, .8);
|
|
21
|
+
float fbm(vec2 p) { float s = 0., a = .5; for (int i = 0; i < 6; i++) { s += a * noise(p); p = M * p * 2.02; a *= .5; } return s; }
|
|
22
|
+
void main() {
|
|
23
|
+
vec2 p = (uv * 2. - 1.) * vec2(R.x / R.y, 1.) * 1.6;
|
|
24
|
+
float t = T * .06;
|
|
25
|
+
vec2 q = vec2(fbm(p + vec2(0, t)), fbm(p + vec2(5.2, 1.3) - t)); // first push
|
|
26
|
+
vec2 r = vec2(fbm(p + 4. * K * q + vec2(1.7, 9.2) + t * 1.5), fbm(p + 4. * K * q + vec2(8.3, 2.8) - t)); // pushed again
|
|
27
|
+
float f = fbm(p + 4. * K * r); // read at the twice-pushed point
|
|
28
|
+
vec3 col = mix(vec3(.10, .15, .32), vec3(.72, .38, .2), clamp(f * f * 3.2, 0., 1.));
|
|
29
|
+
col = mix(col, vec3(.05, .05, .13), clamp(length(q), 0., 1.) * .7);
|
|
30
|
+
col = mix(col, vec3(.95, .82, .62), clamp(abs(r.x) * r.x * 1.6, 0., 1.) * .8);
|
|
31
|
+
col = mix(col, vec3(.2, .55, .6), smoothstep(.55, .9, r.y) * .5);
|
|
32
|
+
col *= .55 + 1.1 * f;
|
|
33
|
+
col = pow(col, vec3(.95));
|
|
34
|
+
o = vec4(col, 1);
|
|
35
|
+
}`;
|
|
36
|
+
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; };
|
|
37
|
+
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);
|
|
38
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
39
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
40
|
+
const U = {}; ['R', 'T', 'K'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
41
|
+
const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, Math.max(1, Math.sqrt(500000 / (w * h)))); canvas.width = w * d | 0; canvas.height = h * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
42
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
43
|
+
let raf, last = performance.now(), t = 0;
|
|
44
|
+
function frame(now) {
|
|
45
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt * PARAMS.speed;
|
|
46
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, t); gl.uniform1f(U.K, PARAMS.warp);
|
|
47
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
48
|
+
raf = requestAnimationFrame(frame);
|
|
49
|
+
}
|
|
50
|
+
raf = requestAnimationFrame(frame);
|
|
51
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Guilloche spirograph: a hypotrochoid (a pen on a small circle rolling inside a big one), drawn as a ribbon
|
|
2
|
+
// of slightly offset lines, like the engraving on a banknote. Whole-number R and r close the loop.
|
|
3
|
+
const PARAMS = { R: 7, r: 4, d: 5.2, lines: 7 };
|
|
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
|
+
const gcd = (a, b) => b ? gcd(b, a % b) : a;
|
|
15
|
+
const R = Math.round(PARAMS.R), r = Math.max(1, Math.min(R - 1, Math.round(PARAMS.r)));
|
|
16
|
+
const turns = r / gcd(R, r); // full turns until the curve closes
|
|
17
|
+
const N = 2600, lines = Math.round(PARAMS.lines);
|
|
18
|
+
const point = (t, d) => [(R - r) * Math.cos(t) + d * Math.cos(((R - r) / r) * t), (R - r) * Math.sin(t) - d * Math.sin(((R - r) / r) * t)];
|
|
19
|
+
let raf; const t0 = performance.now(), CYCLE = 6500;
|
|
20
|
+
function frame(now) {
|
|
21
|
+
const ph = ((now - t0) % CYCLE) / CYCLE;
|
|
22
|
+
const draw = Math.min(1, ph / 0.7), fade = ph > 0.85 ? 1 - (ph - 0.85) / 0.15 : 1;
|
|
23
|
+
const ease = 1 - Math.pow(1 - draw, 2.2);
|
|
24
|
+
ctx.fillStyle = '#f7f4e6'; ctx.fillRect(0, 0, W, H);
|
|
25
|
+
const scale = (Math.min(W, H) * 0.44) / (R - r + PARAMS.d + lines * 0.12);
|
|
26
|
+
ctx.save(); ctx.translate(W / 2, H / 2); ctx.rotate(now / 9000);
|
|
27
|
+
ctx.globalAlpha = fade; ctx.lineWidth = 1;
|
|
28
|
+
const upto = Math.floor(N * ease);
|
|
29
|
+
for (let l = 0; l < lines; l++) {
|
|
30
|
+
const d = PARAMS.d + (l - (lines - 1) / 2) * 0.12;
|
|
31
|
+
ctx.strokeStyle = l % 2 ? '#7b8c10' : '#3d4a08';
|
|
32
|
+
ctx.beginPath();
|
|
33
|
+
for (let i = 0; i <= upto; i++) { const [x, y] = point((i / N) * Math.PI * 2 * turns, d); i ? ctx.lineTo(x * scale, y * scale) : ctx.moveTo(x * scale, y * scale); }
|
|
34
|
+
ctx.stroke();
|
|
35
|
+
}
|
|
36
|
+
if (draw < 1) { const [x, y] = point((upto / N) * Math.PI * 2 * turns, PARAMS.d); ctx.fillStyle = '#d9480f'; ctx.beginPath(); ctx.arc(x * scale, y * scale, 4, 0, 7); ctx.fill(); }
|
|
37
|
+
ctx.restore();
|
|
38
|
+
raf = requestAnimationFrame(frame);
|
|
39
|
+
}
|
|
40
|
+
raf = requestAnimationFrame(frame);
|
|
41
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Kaleidoscope: fold the plane into N mirrored wedges. Convert each pixel to polar coordinates, take the angle modulo
|
|
2
|
+
// one wedge, and mirror every other wedge, so whatever is drawn in one slice repeats round the circle with perfect
|
|
3
|
+
// symmetry. The slice holds drifting glass chips (soft-edged cells of a moving Voronoi pattern) and a light leak; the
|
|
4
|
+
// whole thing slowly turns and breathes in and out, and the edges of the chips split slightly into colour fringes.
|
|
5
|
+
const PARAMS = { mirrors: 8, zoom: 1 };
|
|
6
|
+
|
|
7
|
+
export default function (stage) {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
10
|
+
stage.append(canvas);
|
|
11
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
12
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
13
|
+
const vs = `#version 300 es
|
|
14
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
15
|
+
const fs = `#version 300 es
|
|
16
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
17
|
+
uniform vec2 R; uniform float T, N, Z;
|
|
18
|
+
vec2 hash2(vec2 p) { p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3))); return fract(sin(p) * 43758.5453); }
|
|
19
|
+
// the glass chips: nearest and second-nearest cell distance, and the cell's id
|
|
20
|
+
vec3 chips(vec2 p) {
|
|
21
|
+
vec2 i = floor(p), f = fract(p); float d1 = 8., d2 = 8.; vec2 best = vec2(0);
|
|
22
|
+
for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) {
|
|
23
|
+
vec2 g = vec2(x, y), o = hash2(i + g); o = .5 + .45 * sin(T * .5 + 6.2831 * o);
|
|
24
|
+
float d = length(g + o - f); if (d < d1) { d2 = d1; d1 = d; best = i + g; } else if (d < d2) d2 = d;
|
|
25
|
+
}
|
|
26
|
+
return vec3(d2 - d1, best);
|
|
27
|
+
}
|
|
28
|
+
vec3 slice(vec2 p) {
|
|
29
|
+
vec3 c = chips(p * 2.2 + vec2(T * .15, 0));
|
|
30
|
+
vec3 tint = .55 + .45 * cos(6.2831 * (hash2(c.yz).x + vec3(0, .33, .67)));
|
|
31
|
+
float edge = smoothstep(.0, .06, c.x);
|
|
32
|
+
vec3 col = tint * edge * (.6 + .4 * hash2(c.yz + 3.).y);
|
|
33
|
+
col += vec3(1., .85, .6) * .25 * exp(-length(p - vec2(.8, .1)) * 2.); // a warm light leak
|
|
34
|
+
return col;
|
|
35
|
+
}
|
|
36
|
+
vec3 fold(vec2 q, float k) {
|
|
37
|
+
float r = length(q), a = atan(q.y, q.x) + T * .05;
|
|
38
|
+
float w = 6.2831 / N; a = mod(a, w); a = min(a, w - a); // one wedge, mirrored at its edges
|
|
39
|
+
vec2 p = vec2(cos(a), sin(a)) * r * k;
|
|
40
|
+
return slice(p);
|
|
41
|
+
}
|
|
42
|
+
void main() {
|
|
43
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
44
|
+
float k = Z * (1.4 + .25 * sin(T * .23)); // breathing zoom
|
|
45
|
+
vec3 col = vec3(fold(q * 1.006, k).r, fold(q, k).g, fold(q * .994, k).b); // slight colour fringing, like cheap mirrors
|
|
46
|
+
col *= smoothstep(1.35, .7, length(q)); // the tube's dark rim
|
|
47
|
+
o = vec4(pow(col, vec3(.95)), 1);
|
|
48
|
+
}`;
|
|
49
|
+
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; };
|
|
50
|
+
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);
|
|
51
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
52
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
53
|
+
const U = {}; ['R', 'T', 'N', 'Z'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
54
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); canvas.width = (stage.clientWidth || 1) * d | 0; canvas.height = (stage.clientHeight || 1) * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
55
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
56
|
+
let raf; const t0 = performance.now();
|
|
57
|
+
function frame(now) {
|
|
58
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, (now - t0) / 1000); gl.uniform1f(U.N, Math.round(PARAMS.mirrors)); gl.uniform1f(U.Z, PARAMS.zoom);
|
|
59
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
60
|
+
raf = requestAnimationFrame(frame);
|
|
61
|
+
}
|
|
62
|
+
raf = requestAnimationFrame(frame);
|
|
63
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
64
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Reaction–diffusion (Gray–Scott): chemical B eats A, both spread at different rates, A is fed in and B dies off.
|
|
2
|
+
// The balance of feed and kill decides the pattern: spots, stripes or coral. Runs on the GPU, 12 steps a frame.
|
|
3
|
+
const PARAMS = { feed: 0.037, kill: 0.06 };
|
|
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 || !gl.getExtension('EXT_color_buffer_float')) { stage.textContent = 'This needs WebGL2 with float render targets.'; 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 simFs = `#version 300 es
|
|
14
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
15
|
+
uniform sampler2D S; uniform vec2 px; uniform float F, K; uniform vec3 seed;
|
|
16
|
+
void main() {
|
|
17
|
+
vec2 c = texture(S, uv).rg;
|
|
18
|
+
// 3x3 Laplacian: centre -1, edges .2, corners .05
|
|
19
|
+
vec2 lap = -c;
|
|
20
|
+
lap += .2 * (texture(S, uv + vec2(px.x, 0)).rg + texture(S, uv - vec2(px.x, 0)).rg + texture(S, uv + vec2(0, px.y)).rg + texture(S, uv - vec2(0, px.y)).rg);
|
|
21
|
+
lap += .05 * (texture(S, uv + px).rg + texture(S, uv - px).rg + texture(S, uv + vec2(px.x, -px.y)).rg + texture(S, uv + vec2(-px.x, px.y)).rg);
|
|
22
|
+
float a = c.r, b = c.g, abb = a * b * b;
|
|
23
|
+
a += 1. * lap.r - abb + F * (1. - a);
|
|
24
|
+
b += .5 * lap.g + abb - (K + F) * b;
|
|
25
|
+
if (seed.z > 0. && distance(uv / px, seed.xy / px) < seed.z) b = .9;
|
|
26
|
+
o = vec4(clamp(a, 0., 1.), clamp(b, 0., 1.), 0, 1);
|
|
27
|
+
}`;
|
|
28
|
+
const showFs = `#version 300 es
|
|
29
|
+
precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S;
|
|
30
|
+
void main() {
|
|
31
|
+
float b = texture(S, uv).g, t = smoothstep(.12, .32, b);
|
|
32
|
+
vec3 col = mix(vec3(.96, .94, .84), vec3(.36, .42, .05), t);
|
|
33
|
+
col = mix(col, vec3(.12, .14, .03), smoothstep(.3, .45, b));
|
|
34
|
+
o = vec4(col, 1);
|
|
35
|
+
}`;
|
|
36
|
+
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; };
|
|
37
|
+
const mk = fs => { const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p); return p; };
|
|
38
|
+
const sim = mk(simFs), show = mk(showFs);
|
|
39
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
40
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
41
|
+
|
|
42
|
+
const aspect = stage.clientWidth / Math.max(1, stage.clientHeight);
|
|
43
|
+
const SH = 220, SW = Math.round(SH * aspect);
|
|
44
|
+
// start: A everywhere, a few drops of B
|
|
45
|
+
const init = new Float32Array(SW * SH * 4);
|
|
46
|
+
for (let i = 0; i < SW * SH; i++) init[i * 4] = 1;
|
|
47
|
+
for (let k = 0; k < 14; k++) {
|
|
48
|
+
const cx = Math.random() * SW, cy = Math.random() * SH, r = 2 + Math.random() * 4;
|
|
49
|
+
for (let y = -r; y <= r; y++) for (let x = -r; x <= r; x++) { const X = (cx + x) | 0, Y = (cy + y) | 0; if (X >= 0 && X < SW && Y >= 0 && Y < SH && x * x + y * y <= r * r) init[(Y * SW + X) * 4 + 1] = 0.9; }
|
|
50
|
+
}
|
|
51
|
+
const target = () => {
|
|
52
|
+
const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
53
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, SW, SH, 0, gl.RGBA, gl.FLOAT, init);
|
|
54
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
55
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
|
56
|
+
const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
57
|
+
return { tex, fb };
|
|
58
|
+
};
|
|
59
|
+
let A = target(), B = target();
|
|
60
|
+
const su = n => gl.getUniformLocation(sim, n), uS = su('S'), uPx = su('px'), uF = su('F'), uK = su('K'), uSeed = su('seed');
|
|
61
|
+
|
|
62
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 1.5); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; };
|
|
63
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
64
|
+
let seed = [0, 0, 0], lastMove = -1e9;
|
|
65
|
+
const onMove = e => { const b = canvas.getBoundingClientRect(); seed = [(e.clientX - b.left) / b.width, 1 - (e.clientY - b.top) / b.height, 3]; lastMove = performance.now(); };
|
|
66
|
+
canvas.addEventListener('pointermove', onMove);
|
|
67
|
+
let raf, acc = 0, last = performance.now();
|
|
68
|
+
function frame(now) {
|
|
69
|
+
acc += (now - last) / 1000; last = now;
|
|
70
|
+
if (now - lastMove > 1500 && acc > 0.6) { acc = 0; seed = [Math.random(), Math.random(), 3]; }
|
|
71
|
+
gl.useProgram(sim); gl.viewport(0, 0, SW, SH);
|
|
72
|
+
gl.uniform2f(uPx, 1 / SW, 1 / SH); gl.uniform1f(uF, PARAMS.feed); gl.uniform1f(uK, PARAMS.kill); gl.uniform1i(uS, 0);
|
|
73
|
+
for (let i = 0; i < 12; i++) {
|
|
74
|
+
gl.uniform3f(uSeed, seed[0], seed[1], i === 0 ? seed[2] : 0);
|
|
75
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, B.fb); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, A.tex);
|
|
76
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3); [A, B] = [B, A];
|
|
77
|
+
}
|
|
78
|
+
seed[2] = 0;
|
|
79
|
+
gl.useProgram(show); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, canvas.width, canvas.height);
|
|
80
|
+
gl.bindTexture(gl.TEXTURE_2D, A.tex); gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
81
|
+
raf = requestAnimationFrame(frame);
|
|
82
|
+
}
|
|
83
|
+
raf = requestAnimationFrame(frame);
|
|
84
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', onMove); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
85
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// RGB-split glitch: the picture is split into red, green and blue copies and added back together.
|
|
2
|
+
// Aligned, they rebuild the original; offset, the colour splits. In bursts, slices jump sideways.
|
|
3
|
+
const PARAMS = { intensity: 1, rate: 0.8 };
|
|
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, d = 1, src, chans;
|
|
11
|
+
const build = () => {
|
|
12
|
+
// the source picture
|
|
13
|
+
src = document.createElement('canvas'); src.width = W * d; src.height = H * d;
|
|
14
|
+
const s = src.getContext('2d'); s.scale(d, d);
|
|
15
|
+
s.fillStyle = '#101205'; s.fillRect(0, 0, W, H);
|
|
16
|
+
s.fillStyle = '#e9f5a0'; s.font = `900 ${Math.min(W * 0.2, H * 0.34)}px "Mona Sans", system-ui, sans-serif`; s.textAlign = 'center'; s.textBaseline = 'middle';
|
|
17
|
+
s.fillText('GLITCH', W / 2, H * 0.46);
|
|
18
|
+
s.fillStyle = '#7b8c10'; s.fillRect(W * 0.18, H * 0.7, W * 0.64, H * 0.05);
|
|
19
|
+
s.fillStyle = '#ffffff'; s.font = `600 ${Math.max(11, H * 0.05)}px "Mona Sans", system-ui, sans-serif`; s.fillText('signal lost · retrying', W / 2, H * 0.82);
|
|
20
|
+
// one copy per channel: multiply by pure red, green or blue
|
|
21
|
+
chans = ['#ff0000', '#00ff00', '#0000ff'].map(c => {
|
|
22
|
+
const k = document.createElement('canvas'); k.width = src.width; k.height = src.height;
|
|
23
|
+
const g = k.getContext('2d'); g.drawImage(src, 0, 0); g.globalCompositeOperation = 'multiply'; g.fillStyle = c; g.fillRect(0, 0, k.width, k.height);
|
|
24
|
+
return k;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const resize = () => { d = Math.min(devicePixelRatio, 2); W = stage.clientWidth; H = stage.clientHeight; canvas.width = W * d; canvas.height = H * d; build(); };
|
|
28
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
29
|
+
|
|
30
|
+
let burst = 0, next = performance.now() + 600, raf;
|
|
31
|
+
function frame(now) {
|
|
32
|
+
if (now > next) { burst = 1; next = now + (400 + Math.random() * 1600) / Math.max(0.05, PARAMS.rate); }
|
|
33
|
+
burst *= 0.9;
|
|
34
|
+
const k = (0.15 + burst) * PARAMS.intensity, jitter = () => (Math.random() - 0.5) * 2;
|
|
35
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
36
|
+
ctx.globalCompositeOperation = 'source-over'; ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
37
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
38
|
+
const off = W * d * 0.012 * k;
|
|
39
|
+
ctx.drawImage(chans[0], -off + jitter() * off * 0.3, 0);
|
|
40
|
+
ctx.drawImage(chans[1], 0, jitter() * off * 0.2);
|
|
41
|
+
ctx.drawImage(chans[2], off + jitter() * off * 0.3, 0);
|
|
42
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
43
|
+
if (burst > 0.2) {
|
|
44
|
+
// displaced horizontal slices
|
|
45
|
+
for (let i = 0; i < 6; i++) {
|
|
46
|
+
const y = Math.random() * canvas.height, h = (0.01 + Math.random() * 0.07) * canvas.height, dx = jitter() * canvas.width * 0.08 * k;
|
|
47
|
+
ctx.drawImage(canvas, 0, y, canvas.width, h, dx, y, canvas.width, h);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
ctx.fillStyle = 'rgba(0,0,0,.18)';
|
|
51
|
+
for (let y = 0; y < canvas.height; y += 4 * d) ctx.fillRect(0, y, canvas.width, d); // scanlines
|
|
52
|
+
raf = requestAnimationFrame(frame);
|
|
53
|
+
}
|
|
54
|
+
raf = requestAnimationFrame(frame);
|
|
55
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
56
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Truchet tiles: every square holds two quarter-circle arcs joining the midpoints of its sides, in one of two
|
|
2
|
+
// orientations chosen at random. Whatever the choice, the arcs always meet their neighbours', so the grid forms
|
|
3
|
+
// unbroken winding paths. Dashes flow along the paths; flipping the flow direction on alternate squares (a
|
|
4
|
+
// checkerboard) keeps it continuous across tiles. Now and then a tile turns a quarter, and the paths reroute.
|
|
5
|
+
const PARAMS = { tiles: 9, flow: 1 };
|
|
6
|
+
|
|
7
|
+
export default function (stage) {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
10
|
+
stage.append(canvas);
|
|
11
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
12
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
13
|
+
const vs = `#version 300 es
|
|
14
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
15
|
+
const fs = `#version 300 es
|
|
16
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
17
|
+
uniform vec2 R; uniform float T, N, FL;
|
|
18
|
+
float hash(vec2 p) { return fract(sin(dot(p, vec2(41.3, 289.1))) * 43758.5453); }
|
|
19
|
+
void main() {
|
|
20
|
+
vec2 p = (uv - .5) * vec2(R.x / R.y, 1.) * N;
|
|
21
|
+
vec2 id = floor(p), f = fract(p) - .5;
|
|
22
|
+
// each tile's orientation: random, re-rolled now and then, with a quarter-turn animation as it changes
|
|
23
|
+
float h = hash(id), epoch = floor(T * .25 + h * 7.), h2 = hash(id + epoch * 1.7);
|
|
24
|
+
float turn = smoothstep(.0, .35, fract(T * .25 + h * 7.)) * step(.8, hash(id + (epoch - 1.) * 1.7));
|
|
25
|
+
float flip = step(.5, h2);
|
|
26
|
+
float a = (flip + turn) * 1.5708; f = mat2(cos(a), -sin(a), sin(a), cos(a)) * f;
|
|
27
|
+
// two arcs, centred on opposite corners
|
|
28
|
+
vec2 c1 = f - vec2(.5), c2 = f + vec2(.5);
|
|
29
|
+
float d1 = abs(length(c1) - .5), d2 = abs(length(c2) - .5), d = min(d1, d2);
|
|
30
|
+
vec2 c = d1 < d2 ? c1 : c2;
|
|
31
|
+
float ang = atan(c.y, c.x) * (d1 < d2 ? 1. : -1.);
|
|
32
|
+
float par = mod(id.x + id.y, 2.) * 2. - 1.; // checkerboard: alternate the flow direction
|
|
33
|
+
float dash = fract(ang / 1.5708 * 2. * par * (flip > .5 ? -1. : 1.) - T * FL * .8);
|
|
34
|
+
float w = .11, line = smoothstep(w, w - 2.5 * N / R.y, d);
|
|
35
|
+
float glow = exp(-d * 18.) * .5;
|
|
36
|
+
vec3 base = .5 + .5 * cos(6.2832 * (vec3(0, .33, .67) + id.x * .03 + id.y * .05 + T * .03));
|
|
37
|
+
vec3 col = vec3(.03, .03, .06) + base * glow * .5;
|
|
38
|
+
col += base * line * (.35 + .9 * smoothstep(.55, .5, abs(dash - .5)) );
|
|
39
|
+
col += vec3(1) * line * smoothstep(.05, .0, abs(dash - .5)) * .4; // a bright head on each dash
|
|
40
|
+
o = vec4(pow(col, vec3(.9)), 1);
|
|
41
|
+
}`;
|
|
42
|
+
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; };
|
|
43
|
+
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);
|
|
44
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
45
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
46
|
+
const U = {}; ['R', 'T', 'N', 'FL'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
47
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); canvas.width = (stage.clientWidth || 1) * d | 0; canvas.height = (stage.clientHeight || 1) * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
48
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
49
|
+
let raf; const t0 = performance.now();
|
|
50
|
+
function frame(now) {
|
|
51
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, (now - t0) / 1000); gl.uniform1f(U.N, PARAMS.tiles); gl.uniform1f(U.FL, PARAMS.flow);
|
|
52
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
53
|
+
raf = requestAnimationFrame(frame);
|
|
54
|
+
}
|
|
55
|
+
raf = requestAnimationFrame(frame);
|
|
56
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
57
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Voronoi cells: every pixel finds the nearest of a set of moving points; that point owns the cell.
|
|
2
|
+
// The gap between the nearest and second-nearest distance draws the edges.
|
|
3
|
+
const PARAMS = { cells: 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 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;
|
|
15
|
+
uniform vec2 R; uniform float T, C;
|
|
16
|
+
vec2 hash2(vec2 p) { p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3))); return fract(sin(p) * 43758.5453); }
|
|
17
|
+
void main() {
|
|
18
|
+
vec2 p = uv * vec2(R.x / R.y, 1.) * C;
|
|
19
|
+
vec2 ip = floor(p), fp = fract(p);
|
|
20
|
+
float d1 = 8., d2 = 8.; vec2 id = vec2(0);
|
|
21
|
+
for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) {
|
|
22
|
+
vec2 g = vec2(x, y), h = hash2(ip + g);
|
|
23
|
+
vec2 pt = g + .5 + .4 * sin(T + 6.2831 * h) - fp; // each seed orbits inside its own cell
|
|
24
|
+
float d = dot(pt, pt);
|
|
25
|
+
if (d < d1) { d2 = d1; d1 = d; id = ip + g; } else if (d < d2) d2 = d;
|
|
26
|
+
}
|
|
27
|
+
float edge = sqrt(d2) - sqrt(d1);
|
|
28
|
+
vec2 h = hash2(id);
|
|
29
|
+
vec3 pal[4] = vec3[4](vec3(.48, .55, .06), vec3(.85, .82, .55), vec3(.97, .95, .86), vec3(.3, .36, .05));
|
|
30
|
+
vec3 col = pal[int(h.x * 4.)];
|
|
31
|
+
col *= .88 + .12 * (1. - sqrt(d1)); // soft dome inside each cell
|
|
32
|
+
col = mix(vec3(.09, .1, .04), col, smoothstep(.02, .07, edge));
|
|
33
|
+
o = vec4(col, 1);
|
|
34
|
+
}`;
|
|
35
|
+
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; };
|
|
36
|
+
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);
|
|
37
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
38
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
39
|
+
const uR = gl.getUniformLocation(prog, 'R'), uT = gl.getUniformLocation(prog, 'T'), uC = gl.getUniformLocation(prog, 'C');
|
|
40
|
+
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); };
|
|
41
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
42
|
+
let raf;
|
|
43
|
+
function frame(now) {
|
|
44
|
+
gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform1f(uT, now / 1000 * PARAMS.speed * 2); gl.uniform1f(uC, PARAMS.cells * 0.55);
|
|
45
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
46
|
+
raf = requestAnimationFrame(frame);
|
|
47
|
+
}
|
|
48
|
+
raf = requestAnimationFrame(frame);
|
|
49
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Ball collisions: overlapping balls are separated, then exchange velocity along the line between their centres.
|
|
2
|
+
const PARAMS = { count: 18, restitution: 0.95 };
|
|
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';
|
|
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
|
+
const colours = ['#d9601a', '#ffa94d', '#ffe8cc', '#fff', '#e8590c'];
|
|
14
|
+
const s = Math.min(W, H);
|
|
15
|
+
const balls = Array.from({ length: Math.round(PARAMS.count) }, (_, i) => {
|
|
16
|
+
const r = s * (0.03 + Math.random() * 0.05);
|
|
17
|
+
return { x: r + Math.random() * (W - 2 * r), y: r + Math.random() * (H - 2 * r), vx: (Math.random() - 0.5) * s * 1.2, vy: (Math.random() - 0.5) * s * 1.2, r, m: r * r, c: colours[i % colours.length] };
|
|
18
|
+
});
|
|
19
|
+
let last = performance.now(), raf;
|
|
20
|
+
function frame(now) {
|
|
21
|
+
const dt = Math.min(0.025, (now - last) / 1000); last = now;
|
|
22
|
+
const e = PARAMS.restitution;
|
|
23
|
+
for (const b of balls) {
|
|
24
|
+
b.x += b.vx * dt; b.y += b.vy * dt;
|
|
25
|
+
if (b.x < b.r) { b.x = b.r; b.vx = Math.abs(b.vx) * e; } if (b.x > W - b.r) { b.x = W - b.r; b.vx = -Math.abs(b.vx) * e; }
|
|
26
|
+
if (b.y < b.r) { b.y = b.r; b.vy = Math.abs(b.vy) * e; } if (b.y > H - b.r) { b.y = H - b.r; b.vy = -Math.abs(b.vy) * e; }
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i < balls.length; i++) for (let j = i + 1; j < balls.length; j++) {
|
|
29
|
+
const a = balls[i], b = balls[j], dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy), min = a.r + b.r;
|
|
30
|
+
if (d === 0 || d >= min) continue;
|
|
31
|
+
const nx = dx / d, ny = dy / d, overlap = (min - d) / 2;
|
|
32
|
+
a.x -= nx * overlap; a.y -= ny * overlap; b.x += nx * overlap; b.y += ny * overlap;
|
|
33
|
+
const rel = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny;
|
|
34
|
+
if (rel > 0) continue; // already separating
|
|
35
|
+
const jImp = -(1 + e) * rel / (1 / a.m + 1 / b.m);
|
|
36
|
+
a.vx -= jImp * nx / a.m; a.vy -= jImp * ny / a.m; b.vx += jImp * nx / b.m; b.vy += jImp * ny / b.m;
|
|
37
|
+
}
|
|
38
|
+
// keep the box lively: top up energy if it drains away
|
|
39
|
+
const ke = balls.reduce((k, b) => k + b.vx * b.vx + b.vy * b.vy, 0) / balls.length;
|
|
40
|
+
if (ke < s * s * 0.08) balls.forEach(b => { b.vx *= 1.01; b.vy *= 1.01; });
|
|
41
|
+
ctx.fillStyle = '#1f130b'; ctx.fillRect(0, 0, W, H);
|
|
42
|
+
for (const b of balls) { ctx.fillStyle = b.c; ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill(); }
|
|
43
|
+
raf = requestAnimationFrame(frame);
|
|
44
|
+
}
|
|
45
|
+
raf = requestAnimationFrame(frame);
|
|
46
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Pendulum wave: pendulum n swings (base + n) times per cycle, so the row drifts out of sync and back.
|
|
2
|
+
const PARAMS = { count: 15, cycle: 30 };
|
|
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';
|
|
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
|
+
const n = Math.round(PARAMS.count), base = 16, A = 0.42;
|
|
14
|
+
let raf; const t0 = performance.now();
|
|
15
|
+
function frame(now) {
|
|
16
|
+
const t = (now - t0) / 1000;
|
|
17
|
+
ctx.fillStyle = '#1f130b'; ctx.fillRect(0, 0, W, H);
|
|
18
|
+
const top = H * 0.08;
|
|
19
|
+
ctx.fillStyle = '#5c3a1e'; ctx.fillRect(W * 0.06, top - 3, W * 0.88, 4);
|
|
20
|
+
for (let i = 0; i < n; i++) {
|
|
21
|
+
const N = base + i; // swings per cycle
|
|
22
|
+
const len = H * 0.8 * Math.pow(base / N, 2) + H * 0.06; // real pendulums: length ∝ 1 / frequency²
|
|
23
|
+
const ang = A * Math.cos(2 * Math.PI * N * t / PARAMS.cycle);
|
|
24
|
+
const ax = W * 0.1 + (W * 0.8) * (i / Math.max(1, n - 1));
|
|
25
|
+
const bx = ax + Math.sin(ang) * len, by = top + Math.cos(ang) * len;
|
|
26
|
+
ctx.strokeStyle = 'rgba(255, 216, 168, .35)'; ctx.lineWidth = 1;
|
|
27
|
+
ctx.beginPath(); ctx.moveTo(ax, top); ctx.lineTo(bx, by); ctx.stroke();
|
|
28
|
+
ctx.fillStyle = `hsl(${18 + i * (30 / n)}, 90%, ${50 + i * (18 / n)}%)`;
|
|
29
|
+
ctx.beginPath(); ctx.arc(bx, by, Math.max(3, W / n * 0.28), 0, Math.PI * 2); ctx.fill();
|
|
30
|
+
}
|
|
31
|
+
raf = requestAnimationFrame(frame);
|
|
32
|
+
}
|
|
33
|
+
raf = requestAnimationFrame(frame);
|
|
34
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
35
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Rubber-band drag: 1:1 inside the track, increasing resistance past the edge, spring back with the release velocity.
|
|
2
|
+
const PARAMS = { constant: 0.55, stiffness: 200 };
|
|
3
|
+
|
|
4
|
+
export default function (stage) {
|
|
5
|
+
const root = document.createElement('div');
|
|
6
|
+
root.className = 'rb-root';
|
|
7
|
+
root.innerHTML = `<style>
|
|
8
|
+
.rb-root { position: absolute; inset: 0; background: #fff4e6; container-type: size; font-family: "Mona Sans", system-ui, sans-serif; touch-action: none; }
|
|
9
|
+
.rb-track { position: absolute; left: 12%; right: 12%; top: 50%; height: 2px; background: #ffc078; }
|
|
10
|
+
.rb-card { position: absolute; left: 50%; top: 50%; width: 30cqmin; height: 30cqmin; margin: -15cqmin; border-radius: 6cqmin; background: #d9601a; color: #fff; display: grid; place-items: center; font-weight: 750; font-size: max(11px, 4.6cqmin); cursor: grab; user-select: none; will-change: transform; }
|
|
11
|
+
.rb-card:active { cursor: grabbing; }
|
|
12
|
+
</style><div class="rb-track"></div><div class="rb-card">Drag me</div>`;
|
|
13
|
+
stage.append(root);
|
|
14
|
+
const card = root.querySelector('.rb-card');
|
|
15
|
+
const bound = () => root.clientWidth * 0.38 - card.offsetWidth / 2; // how far it may go freely
|
|
16
|
+
// Apple's rubber-band: the further past the bound, the less it follows
|
|
17
|
+
const rubber = (over, dim) => (over * dim * PARAMS.constant) / (dim + PARAMS.constant * Math.abs(over));
|
|
18
|
+
const clampRubber = raw => { const b = bound(), dim = root.clientWidth; if (raw > b) return b + rubber(raw - b, dim); if (raw < -b) return -b - rubber(-b - raw, dim); return raw; };
|
|
19
|
+
|
|
20
|
+
let x = 0, v = 0, dragging = false, startX = 0, raw = 0, hist = [], lastUser = -1e9, ghost = null;
|
|
21
|
+
const down = e => { dragging = true; card.setPointerCapture(e.pointerId); startX = e.clientX - raw; hist = []; lastUser = performance.now(); ghost = null; };
|
|
22
|
+
const move = e => { if (!dragging) return; raw = e.clientX - startX; x = clampRubber(raw); hist.push([performance.now(), x]); if (hist.length > 5) hist.shift(); lastUser = performance.now(); };
|
|
23
|
+
const up = () => {
|
|
24
|
+
if (!dragging) return; dragging = false;
|
|
25
|
+
if (hist.length > 1) { const [t0, x0] = hist[0], [t1, x1] = hist.at(-1); v = (x1 - x0) / Math.max(1, t1 - t0) * 1000; }
|
|
26
|
+
raw = 0;
|
|
27
|
+
};
|
|
28
|
+
card.addEventListener('pointerdown', down); card.addEventListener('pointermove', move);
|
|
29
|
+
card.addEventListener('pointerup', up); card.addEventListener('pointercancel', up);
|
|
30
|
+
|
|
31
|
+
let last = performance.now(), raf;
|
|
32
|
+
function frame(now) {
|
|
33
|
+
const dt = Math.min(0.032, (now - last) / 1000); last = now;
|
|
34
|
+
// ghost drag: pull right past the edge, let go, then left
|
|
35
|
+
if (!dragging && now - lastUser > 2000) {
|
|
36
|
+
const t = (now / 1000) % 3.2, dir = Math.floor(now / 3200) % 2 ? -1 : 1;
|
|
37
|
+
if (t < 1.4) { ghost = dir * root.clientWidth * 0.75 * Math.sin((t / 1.4) * Math.PI / 2); x = clampRubber(ghost); v = 0; }
|
|
38
|
+
else if (ghost !== null) { ghost = null; }
|
|
39
|
+
}
|
|
40
|
+
if (!dragging && ghost === null) {
|
|
41
|
+
const target = 0, k = PARAMS.stiffness, c = 2 * Math.sqrt(k) * 0.75;
|
|
42
|
+
for (let i = 0; i < 4; i++) { const h = dt / 4; v += (-k * (x - target) - c * v) * h; x += v * h; }
|
|
43
|
+
}
|
|
44
|
+
const stretch = 1 + Math.min(0.25, Math.abs(x) / root.clientWidth * 0.3);
|
|
45
|
+
card.style.transform = `translateX(${x}px) scaleX(${stretch}) scaleY(${1 / stretch})`;
|
|
46
|
+
raf = requestAnimationFrame(frame);
|
|
47
|
+
}
|
|
48
|
+
raf = requestAnimationFrame(frame);
|
|
49
|
+
return () => { cancelAnimationFrame(raf); root.remove(); };
|
|
50
|
+
}
|