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