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,163 @@
|
|
|
1
|
+
// Low-poly planet: an icosahedron split three times into 1,280 faces, each vertex pushed out by 3D noise.
|
|
2
|
+
// Faces are flat-shaded and coloured by height (sea, sand, grass, forest, rock, snow), pine trees sprout on
|
|
3
|
+
// the green faces, and puffy clouds orbit faster than the ground. Drag to spin it.
|
|
4
|
+
const PARAMS = { terrain: 1, sea: 0.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;touch-action:none;cursor:grab';
|
|
9
|
+
stage.append(canvas);
|
|
10
|
+
const gl = canvas.getContext('webgl2', { antialias: true });
|
|
11
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
12
|
+
|
|
13
|
+
// ── geometry
|
|
14
|
+
const perm = Uint8Array.from({ length: 256 }, () => Math.random() * 256);
|
|
15
|
+
const hsh = (x, y, z) => perm[(perm[(perm[x & 255] + y) & 255] + z) & 255] / 255;
|
|
16
|
+
const sm = t => t * t * (3 - 2 * t), lerp = (a, b, t) => a + (b - a) * t;
|
|
17
|
+
const noise = (x, y, z) => {
|
|
18
|
+
const X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z), u = sm(x - X), v = sm(y - Y), w = sm(z - Z);
|
|
19
|
+
return lerp(lerp(lerp(hsh(X, Y, Z), hsh(X + 1, Y, Z), u), lerp(hsh(X, Y + 1, Z), hsh(X + 1, Y + 1, Z), u), v),
|
|
20
|
+
lerp(lerp(hsh(X, Y, Z + 1), hsh(X + 1, Y, Z + 1), u), lerp(hsh(X, Y + 1, Z + 1), hsh(X + 1, Y + 1, Z + 1), u), v), w);
|
|
21
|
+
};
|
|
22
|
+
const fbm = (x, y, z) => { let s = 0, a = 0.5, f = 1; for (let i = 0; i < 4; i++) { s += a * noise(x * f + 11, y * f + 7, z * f + 3); a *= 0.5; f *= 2.1; } return s; };
|
|
23
|
+
function icosphere(level) {
|
|
24
|
+
const t = (1 + Math.sqrt(5)) / 2;
|
|
25
|
+
let V = [[-1, t, 0], [1, t, 0], [-1, -t, 0], [1, -t, 0], [0, -1, t], [0, 1, t], [0, -1, -t], [0, 1, -t], [t, 0, -1], [t, 0, 1], [-t, 0, -1], [-t, 0, 1]].map(v => { const l = Math.hypot(...v); return v.map(c => c / l); });
|
|
26
|
+
let F = [[0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1]];
|
|
27
|
+
for (let l = 0; l < level; l++) {
|
|
28
|
+
const cache = new Map(), mid = (a, b) => { const k = a < b ? a + ',' + b : b + ',' + a; if (cache.has(k)) return cache.get(k); const m = V[a].map((c, i) => (c + V[b][i]) / 2), len = Math.hypot(...m); V.push(m.map(c => c / len)); cache.set(k, V.length - 1); return V.length - 1; };
|
|
29
|
+
F = F.flatMap(([a, b, c]) => { const ab = mid(a, b), bc = mid(b, c), ca = mid(c, a); return [[a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]]; });
|
|
30
|
+
}
|
|
31
|
+
return { V, F };
|
|
32
|
+
}
|
|
33
|
+
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]], cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
34
|
+
const norm = a => { const l = Math.hypot(...a) || 1; return a.map(c => c / l); };
|
|
35
|
+
const hex = h => [parseInt(h.slice(1, 3), 16) / 255, parseInt(h.slice(3, 5), 16) / 255, parseInt(h.slice(5, 7), 16) / 255];
|
|
36
|
+
function pushTri(out, a, b, c, col, shine, ctr = [0, 0, 0]) {
|
|
37
|
+
let n = norm(cross(sub(b, a), sub(c, a)));
|
|
38
|
+
const m = [0, 1, 2].map(i => (a[i] + b[i] + c[i]) / 3 - ctr[i]);
|
|
39
|
+
if (n[0] * m[0] + n[1] * m[1] + n[2] * m[2] < 0) n = n.map(x => -x); // face outward from the shape's centre, whatever the winding
|
|
40
|
+
for (const v of [a, b, c]) out.push(v[0], v[1], v[2], n[0], n[1], n[2], col[0], col[1], col[2], shine);
|
|
41
|
+
}
|
|
42
|
+
function buildPlanet() {
|
|
43
|
+
const { V, F } = icosphere(3), sea = 0.38 + PARAMS.sea * 0.14, amp = 0.32 * PARAMS.terrain;
|
|
44
|
+
const H = V.map(v => fbm(v[0] * 1.7, v[1] * 1.7, v[2] * 1.7));
|
|
45
|
+
const P = V.map((v, i) => { const r = 1 + Math.max(0, H[i] - sea) * amp; return v.map(c => c * r); });
|
|
46
|
+
const out = [], greens = [];
|
|
47
|
+
F.forEach(([a, b, c]) => {
|
|
48
|
+
const h = (H[a] + H[b] + H[c]) / 3 - sea, j = (Math.random() - 0.5) * 0.06;
|
|
49
|
+
let col, shine = 0;
|
|
50
|
+
if (h < 0) { col = h < -0.06 ? hex('#1c5a86') : hex('#2b86b0'); shine = 1; }
|
|
51
|
+
else if (h < 0.025) col = hex('#e6cf8c');
|
|
52
|
+
else if (h < 0.11) { col = hex('#6cb04f'); greens.push([a, b, c]); }
|
|
53
|
+
else if (h < 0.17) { col = hex('#3f8a3c'); greens.push([a, b, c]); }
|
|
54
|
+
else if (h < 0.24) col = hex('#8b7e70');
|
|
55
|
+
else col = hex('#f1f3f6');
|
|
56
|
+
pushTri(out, P[a], P[b], P[c], col.map(x => Math.min(1, Math.max(0, x + j))), shine);
|
|
57
|
+
});
|
|
58
|
+
// pine trees: a five-sided cone standing on the face's centre, pointing away from the core
|
|
59
|
+
greens.sort(() => Math.random() - 0.5).slice(0, 140).forEach(([a, b, c]) => {
|
|
60
|
+
const cen = [0, 1, 2].map(i => (P[a][i] + P[b][i] + P[c][i]) / 3), up = norm(cen), side = norm(cross(up, [0.3, 1, 0.1])), side2 = cross(up, side);
|
|
61
|
+
const hgt = 0.05 + Math.random() * 0.04, rad = 0.018 + Math.random() * 0.01, tip = cen.map((x, i) => x + up[i] * hgt);
|
|
62
|
+
const col = hex(Math.random() < 0.5 ? '#2f6e33' : '#3b7f37');
|
|
63
|
+
for (let k = 0; k < 5; k++) {
|
|
64
|
+
const a0 = k / 5 * 6.283, a1 = (k + 1) / 5 * 6.283;
|
|
65
|
+
const p0 = cen.map((x, i) => x + (side[i] * Math.cos(a0) + side2[i] * Math.sin(a0)) * rad - up[i] * 0.004);
|
|
66
|
+
const p1 = cen.map((x, i) => x + (side[i] * Math.cos(a1) + side2[i] * Math.sin(a1)) * rad - up[i] * 0.004);
|
|
67
|
+
pushTri(out, p0, p1, tip, col, 0, cen);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return new Float32Array(out);
|
|
71
|
+
}
|
|
72
|
+
function buildClouds() {
|
|
73
|
+
const { V, F } = icosphere(0), out = [];
|
|
74
|
+
for (let k = 0; k < 9; k++) {
|
|
75
|
+
const th = Math.random() * 6.283, ph = Math.acos(Math.random() * 1.6 - 0.8), c = [Math.sin(ph) * Math.cos(th), Math.cos(ph), Math.sin(ph) * Math.sin(th)].map(x => x * 1.34);
|
|
76
|
+
const side = norm(cross(c, [0, 1, 0]));
|
|
77
|
+
for (let m = 0; m < 3 + (k % 3); m++) {
|
|
78
|
+
const off = (m - 1.5) * 0.07, s = 0.05 + Math.random() * 0.045, ctr = c.map((x, i) => x + side[i] * off + (Math.random() - 0.5) * 0.03);
|
|
79
|
+
F.forEach(([a, b, d]) => pushTri(out, ...[a, b, d].map(i => V[i].map((x, j) => ctr[j] + x * s * (j === 1 ? 0.75 : 1))), [0.97, 0.98, 1], 0, ctr));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return new Float32Array(out);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── GL
|
|
86
|
+
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; };
|
|
87
|
+
const link = (v, f) => { const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, v)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, f)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p); return p; };
|
|
88
|
+
const meshProg = link(`#version 300 es
|
|
89
|
+
in vec3 pos, nor, col; in float shine; uniform mat4 PV, M; uniform float T;
|
|
90
|
+
out vec3 vN, vC, vW; out float vS;
|
|
91
|
+
void main() { vec4 w = M * vec4(pos, 1); vW = w.xyz; vN = mat3(M) * nor; vC = col; vS = shine;
|
|
92
|
+
if (shine > .5) vN = normalize(vN + .08 * vec3(sin(T * 2. + pos.x * 30.), cos(T * 1.7 + pos.y * 30.), sin(T * 1.9 + pos.z * 30.))); // the sea glints
|
|
93
|
+
gl_Position = PV * w; }`, `#version 300 es
|
|
94
|
+
precision highp float; in vec3 vN, vC, vW; in float vS; out vec4 o;
|
|
95
|
+
void main() {
|
|
96
|
+
vec3 n = normalize(vN), L = normalize(vec3(-.6, .45, .65)), V = normalize(vec3(0, 0, 3.6) - vW);
|
|
97
|
+
float d = max(0., dot(n, L)), rim = pow(1. - max(0., dot(n, V)), 3.);
|
|
98
|
+
vec3 c = vC * (.16 + .95 * d) + vec3(.35, .55, 1.) * rim * .35;
|
|
99
|
+
c += vS * pow(max(0., dot(reflect(-L, n), V)), 40.) * .8;
|
|
100
|
+
o = vec4(pow(c, vec3(.92)), 1);
|
|
101
|
+
}`);
|
|
102
|
+
const bgProg = link(`#version 300 es
|
|
103
|
+
in vec2 p; out vec2 uv; void main() { uv = p; gl_Position = vec4(p, .999, 1); }`, `#version 300 es
|
|
104
|
+
precision highp float; in vec2 uv; out vec4 o; uniform vec2 R; uniform float RS;
|
|
105
|
+
float h(vec2 p) { return fract(sin(dot(p, vec2(12.99, 78.23))) * 43758.55); }
|
|
106
|
+
void main() {
|
|
107
|
+
vec2 q = uv * vec2(R.x / R.y, 1.);
|
|
108
|
+
vec3 c = mix(vec3(.02, .025, .06), vec3(.05, .04, .1), uv.y * .5 + .5);
|
|
109
|
+
vec2 g = floor(gl_FragCoord.xy / 3.); float s = h(g); c += step(.996, s) * (.4 + .6 * h(g + 3.)) * vec3(.9, .95, 1.);
|
|
110
|
+
float r = length(q);
|
|
111
|
+
c += vec3(.25, .5, 1.) * exp(-max(0., r - RS) * 9.) * .55 * smoothstep(RS * .9, RS, r); // atmosphere halo
|
|
112
|
+
o = vec4(c, 1);
|
|
113
|
+
}`);
|
|
114
|
+
const quad = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, quad); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
115
|
+
const bgVao = gl.createVertexArray(); gl.bindVertexArray(bgVao); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
116
|
+
function meshVao(data) {
|
|
117
|
+
const vao = gl.createVertexArray(); gl.bindVertexArray(vao);
|
|
118
|
+
const b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
|
|
119
|
+
[['pos', 3, 0], ['nor', 3, 12], ['col', 3, 24], ['shine', 1, 36]].forEach(([n, s, o]) => { const l = gl.getAttribLocation(meshProg, n); gl.enableVertexAttribArray(l); gl.vertexAttribPointer(l, s, gl.FLOAT, false, 40, o); });
|
|
120
|
+
return { vao, count: data.length / 10, buf: b };
|
|
121
|
+
}
|
|
122
|
+
const planet = meshVao(buildPlanet()), clouds = meshVao(buildClouds());
|
|
123
|
+
const uPV = gl.getUniformLocation(meshProg, 'PV'), uM = gl.getUniformLocation(meshProg, 'M'), uT = gl.getUniformLocation(meshProg, 'T');
|
|
124
|
+
const uBR = gl.getUniformLocation(bgProg, 'R'), uRS = gl.getUniformLocation(bgProg, 'RS');
|
|
125
|
+
|
|
126
|
+
// ── tiny matrix kit (column-major)
|
|
127
|
+
const mul = (a, b) => { const o = new Float32Array(16); for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) { let s = 0; for (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k]; o[c * 4 + r] = s; } return o; };
|
|
128
|
+
const persp = (fov, asp, n, f) => { const t = 1 / Math.tan(fov / 2); return new Float32Array([t / asp, 0, 0, 0, 0, t, 0, 0, 0, 0, (f + n) / (n - f), -1, 0, 0, 2 * f * n / (n - f), 0]); };
|
|
129
|
+
const rotY = a => new Float32Array([Math.cos(a), 0, -Math.sin(a), 0, 0, 1, 0, 0, Math.sin(a), 0, Math.cos(a), 0, 0, 0, 0, 1]);
|
|
130
|
+
const rotX = a => new Float32Array([1, 0, 0, 0, 0, Math.cos(a), Math.sin(a), 0, 0, -Math.sin(a), Math.cos(a), 0, 0, 0, 0, 1]);
|
|
131
|
+
const trans = z => new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, z, 1]);
|
|
132
|
+
|
|
133
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; };
|
|
134
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
135
|
+
let ang = 0.8, vel = 0, dragX = null;
|
|
136
|
+
const onDown = e => { dragX = e.clientX; canvas.style.cursor = 'grabbing'; canvas.setPointerCapture?.(e.pointerId); };
|
|
137
|
+
const onMove = e => { if (dragX == null) return; vel = (e.clientX - dragX) * 0.012; ang += vel; dragX = e.clientX; };
|
|
138
|
+
const onUp = () => { dragX = null; canvas.style.cursor = 'grab'; };
|
|
139
|
+
canvas.addEventListener('pointerdown', onDown); canvas.addEventListener('pointermove', onMove); canvas.addEventListener('pointerup', onUp); canvas.addEventListener('pointercancel', onUp);
|
|
140
|
+
let raf, last = performance.now(); const t0 = last;
|
|
141
|
+
function frame(now) {
|
|
142
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; const t = (now - t0) / 1000;
|
|
143
|
+
if (dragX == null) { ang += vel + dt * 0.25 * PARAMS.spin; vel *= 0.94; }
|
|
144
|
+
const asp = canvas.width / canvas.height, fov = 0.75, dist = 3.6;
|
|
145
|
+
gl.viewport(0, 0, canvas.width, canvas.height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
146
|
+
gl.disable(gl.DEPTH_TEST); gl.useProgram(bgProg); gl.bindVertexArray(bgVao);
|
|
147
|
+
gl.uniform2f(uBR, canvas.width, canvas.height); gl.uniform1f(uRS, 1.12 / Math.tan(fov / 2) / Math.sqrt(dist * dist - 1));
|
|
148
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
149
|
+
gl.enable(gl.DEPTH_TEST); gl.useProgram(meshProg); gl.uniform1f(uT, t);
|
|
150
|
+
gl.uniformMatrix4fv(uPV, false, mul(persp(fov, asp, 0.1, 20), trans(-dist)));
|
|
151
|
+
gl.uniformMatrix4fv(uM, false, mul(rotX(0.35), rotY(ang)));
|
|
152
|
+
gl.bindVertexArray(planet.vao); gl.drawArrays(gl.TRIANGLES, 0, planet.count);
|
|
153
|
+
gl.uniformMatrix4fv(uM, false, mul(rotX(0.35), rotY(ang * 1.35 + t * 0.06)));
|
|
154
|
+
gl.bindVertexArray(clouds.vao); gl.drawArrays(gl.TRIANGLES, 0, clouds.count);
|
|
155
|
+
raf = requestAnimationFrame(frame);
|
|
156
|
+
}
|
|
157
|
+
raf = requestAnimationFrame(frame);
|
|
158
|
+
return () => {
|
|
159
|
+
cancelAnimationFrame(raf); ro.disconnect();
|
|
160
|
+
canvas.removeEventListener('pointerdown', onDown); canvas.removeEventListener('pointermove', onMove); canvas.removeEventListener('pointerup', onUp); canvas.removeEventListener('pointercancel', onUp);
|
|
161
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove();
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Mandelbulb: the 3D cousin of the Mandelbrot set. Each point is raised to a power in spherical coordinates
|
|
2
|
+
// (radius to the n, both angles times n) and added back, over and over; the distance estimate from that orbit
|
|
3
|
+
// lets a ray step safely toward the surface. The power drifts slowly, so the bulb breathes and rearranges.
|
|
4
|
+
const PARAMS = { power: 8, drift: 1, detail: 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, PW, EPS;
|
|
16
|
+
vec4 trap;
|
|
17
|
+
float de(vec3 p) {
|
|
18
|
+
vec3 z = p; float dr = 1., r = 0.; trap = vec4(abs(z), dot(z, z));
|
|
19
|
+
for (int i = 0; i < 9; i++) {
|
|
20
|
+
r = length(z); if (r > 2.) break;
|
|
21
|
+
float th = acos(clamp(z.y / r, -1., 1.)) * PW, ph = atan(z.z, z.x) * PW;
|
|
22
|
+
dr = pow(r, PW - 1.) * PW * dr + 1.;
|
|
23
|
+
z = pow(r, PW) * vec3(sin(th) * cos(ph), cos(th), sin(th) * sin(ph)) + p;
|
|
24
|
+
trap = min(trap, vec4(abs(z), dot(z, z))); // orbit trap: how close the orbit came to the axes and origin
|
|
25
|
+
}
|
|
26
|
+
return .5 * log(r) * r / dr;
|
|
27
|
+
}
|
|
28
|
+
vec3 nrm(vec3 p, float e) { vec2 k = vec2(e, -e); return normalize(k.xyy * de(p + k.xyy) + k.yyx * de(p + k.yyx) + k.yxy * de(p + k.yxy) + k.xxx * de(p + k.xxx)); }
|
|
29
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
30
|
+
void main() {
|
|
31
|
+
vec2 q = (uv - .5) * vec2(R.x / R.y, 1.);
|
|
32
|
+
float a = T * .15;
|
|
33
|
+
vec3 ro = vec3(sin(a) * 4.4, 1.1 * sin(T * .07) + .6, cos(a) * 4.4), f = normalize(-ro), r = normalize(cross(vec3(0, 1, 0), f)), u = cross(f, r);
|
|
34
|
+
vec3 rd = normalize(f * 1.55 + r * q.x + u * q.y);
|
|
35
|
+
vec3 bg = mix(vec3(.02, .02, .035), vec3(.09, .06, .1), uv.y) + vec3(.12, .07, .04) * exp(-length(q) * 2.2);
|
|
36
|
+
vec3 col = bg;
|
|
37
|
+
// only march inside the bounding sphere
|
|
38
|
+
float b = dot(ro, rd), c = dot(ro, ro) - 1.44, h = b * b - c;
|
|
39
|
+
float glow = 0.;
|
|
40
|
+
if (h > 0.) {
|
|
41
|
+
float t = max(0., -b - sqrt(h)), tEnd = -b + sqrt(h), steps = 0.; bool hit = false;
|
|
42
|
+
for (int i = 0; i < 140; i++) {
|
|
43
|
+
float d = de(ro + rd * t);
|
|
44
|
+
glow += exp(-d * 30.) * .012;
|
|
45
|
+
if (d < EPS * t) { hit = true; break; }
|
|
46
|
+
t += d; steps += 1.; if (t > tEnd) break;
|
|
47
|
+
}
|
|
48
|
+
if (hit) {
|
|
49
|
+
vec3 p = ro + rd * t, n = nrm(p, EPS * t * .5);
|
|
50
|
+
float ao = clamp(1. - steps / 90., 0., 1.); // more steps to arrive = deeper in a crevice
|
|
51
|
+
vec3 base = mix(vec3(.95, .55, .22), vec3(.2, .55, .65), clamp(trap.y * 1.3, 0., 1.));
|
|
52
|
+
base = mix(base, vec3(.95, .9, .8), clamp(1. - trap.w * 1.5, 0., 1.) * .6);
|
|
53
|
+
vec3 L = normalize(vec3(.6, .8, -.4));
|
|
54
|
+
float dif = max(0., dot(n, L)), sp = pow(max(0., dot(reflect(rd, n), L)), 24.), rim = pow(1. - max(0., dot(n, -rd)), 3.);
|
|
55
|
+
col = base * (.12 + dif * 1.1) * (.35 + .65 * ao) + sp * .5 * ao + rim * vec3(.5, .35, .7) * .4 * ao;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
col += vec3(.9, .5, .3) * glow * .35; // halo from near misses
|
|
59
|
+
col = aces(col * 1.1);
|
|
60
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
61
|
+
}`;
|
|
62
|
+
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; };
|
|
63
|
+
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);
|
|
64
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
65
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
66
|
+
const uR = gl.getUniformLocation(prog, 'R'), uT = gl.getUniformLocation(prog, 'T'), uP = gl.getUniformLocation(prog, 'PW'), uE = gl.getUniformLocation(prog, 'EPS');
|
|
67
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 1); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
68
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
69
|
+
let raf; const t0 = performance.now();
|
|
70
|
+
function frame(now) {
|
|
71
|
+
const t = (now - t0) / 1000;
|
|
72
|
+
gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform1f(uT, t);
|
|
73
|
+
gl.uniform1f(uP, PARAMS.power + Math.sin(t * 0.25) * 1.5 * PARAMS.drift);
|
|
74
|
+
gl.uniform1f(uE, 0.0016 / PARAMS.detail);
|
|
75
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3); raf = requestAnimationFrame(frame);
|
|
76
|
+
}
|
|
77
|
+
raf = requestAnimationFrame(frame);
|
|
78
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
79
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Melting chrome logo: solid chrome letters slump into a puddle, then pull themselves back up, like liquid metal
|
|
2
|
+
// with a memory. The letters are drawn once to a canvas and turned into a 2D distance field (an exact distance
|
|
3
|
+
// transform); in 3D that field is extruded with rounded edges. Melting bends space rather than the letters: each
|
|
4
|
+
// point reads the field from higher up, more so near the floor and down the drip lines, so the shapes sag and run.
|
|
5
|
+
// A puddle grows under them and merges with a smooth minimum. Reflections come from a studio of softboxes.
|
|
6
|
+
const PARAMS = { drips: 1, speed: 1, gloss: 1 };
|
|
7
|
+
const WORD = 'MELT';
|
|
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
|
+
// the word's signed distance in pixels (positive outside), by an exact Euclidean distance transform
|
|
17
|
+
const field = (() => {
|
|
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", 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);
|
|
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++; set(q, (q - v[k]) ** 2 + tmp[v[k]]); }
|
|
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), d = new Float32Array(TW * TH);
|
|
36
|
+
for (let i = 0; i < TW * TH; i++) { const row = (i / TW) | 0, col = i % TW; d[(TH - 1 - row) * TW + col] = Math.sqrt(away[i]) - Math.sqrt(depth[i]); }
|
|
37
|
+
return d;
|
|
38
|
+
})();
|
|
39
|
+
const FS = `#version 300 es
|
|
40
|
+
precision highp float; in vec2 uv; out vec4 o; uniform sampler2D F; uniform vec2 R; uniform float T, M, DR, GL;
|
|
41
|
+
const float SX = 4.6, SY = 4.6 * ${TH}. / ${TW}., PX = 4.6 / ${TW}., FLOOR = -.95;
|
|
42
|
+
mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
|
|
43
|
+
float h1(float x) { return fract(sin(x * 127.1) * 43758.5453); }
|
|
44
|
+
float n1(float x) { float i = floor(x), f = fract(x); f = f * f * (3. - 2. * f); return mix(h1(i), h1(i + 1.), f); }
|
|
45
|
+
float sd2(vec2 p) { vec2 b = abs(p) - vec2(SX, SY) * .5; if (max(b.x, b.y) > 0.) return length(max(b, 0.)) + .5; return texture(F, vec2(p.x / SX + .5, p.y / SY + .5)).r * PX; }
|
|
46
|
+
float smin(float a, float b, float k) { float h = clamp(.5 + .5 * (b - a) / k, 0., 1.); return mix(b, a, h) - k * h * (1. - h); }
|
|
47
|
+
float smax(float a, float b, float k) { return -smin(-a, -b, k); }
|
|
48
|
+
float map(vec3 p) {
|
|
49
|
+
p.xz *= rot(.35 * sin(T * .3));
|
|
50
|
+
// the letters, extruded with rounded edges
|
|
51
|
+
float th = .22, r = .07, d2 = sd2(vec2(p.x, p.y + .03)), dz = abs(p.z) - th;
|
|
52
|
+
float letters = length(max(vec2(d2 + r, dz + r), 0.)) - r + min(max(d2, dz), 0.);
|
|
53
|
+
// the melt front sinks through them (wavy, with a molten rounded rim), and what is above it is gone
|
|
54
|
+
float front = mix(1.1, FLOOR - .15, M) + .07 * sin(p.x * 3.3 + T * 1.3) * M;
|
|
55
|
+
float body = smax(letters, p.y - front, .14);
|
|
56
|
+
// drips hang from the front where there is metal, and stretch as it goes
|
|
57
|
+
float drips = 1e3;
|
|
58
|
+
for (int i = 0; i < 7; i++) {
|
|
59
|
+
float x = -1.9 + float(i) * .63 + .2 * sin(float(i) * 7.1), len = M * (1. - M) * 4. * (.35 + .5 * h1(float(i) * 3.3)) * DR;
|
|
60
|
+
float here = step(sd2(vec2(x, front + .05)), .02);
|
|
61
|
+
vec3 a = vec3(x, front, 0.), b = vec3(x, front - len, 0.), pa = p - a, ba = b - a;
|
|
62
|
+
float k = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-4), 0., 1.);
|
|
63
|
+
drips = min(drips, length(pa - ba * k) - (.07 - .03 * k) * here + (1. - here) * 9.);
|
|
64
|
+
}
|
|
65
|
+
float puddle = length(vec3(p.x * .38, (p.y - FLOOR) * 5., p.z * .85)) / 5. - .02 - .14 * M;
|
|
66
|
+
puddle = max(puddle, FLOOR - p.y);
|
|
67
|
+
return min(smin(smin(body, drips, .12), puddle, .2), p.y - FLOOR + 10.) * .7;
|
|
68
|
+
}
|
|
69
|
+
vec3 nrm(vec3 p) { vec2 e = vec2(.002, 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))); }
|
|
70
|
+
vec3 env(vec3 d) {
|
|
71
|
+
vec3 c = mix(vec3(.02, .022, .03), vec3(.13, .14, .17), smoothstep(-.1, .7, d.y));
|
|
72
|
+
vec2 q = d.xz / max(d.y, .05);
|
|
73
|
+
c += vec3(1.) * 2.6 * smoothstep(.06, .0, abs(q.x - .1) - .55) * smoothstep(.06, .0, abs(q.y + .2) - .16) * step(0., d.y);
|
|
74
|
+
float az = atan(d.x, d.z);
|
|
75
|
+
c += vec3(1., .55, .3) * 1.6 * smoothstep(.1, .02, abs(az - 1.9)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y);
|
|
76
|
+
c += vec3(.35, .6, 1.) * 1.4 * smoothstep(.1, .02, abs(az + 1.6)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y);
|
|
77
|
+
c += vec3(1., .97, .92) * 1.3 * smoothstep(.07, .0, abs(d.y - .34) - .05);
|
|
78
|
+
c += vec3(1.25, 1.22, 1.18) * smoothstep(.35, .85, d.z) * smoothstep(-.35, .45, d.y); // a big bright wall behind the camera, for the front faces
|
|
79
|
+
return c;
|
|
80
|
+
}
|
|
81
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
82
|
+
void main() {
|
|
83
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
84
|
+
vec3 ro = vec3(0, .55, 5.4), rd = normalize(vec3(q, -2.3)); rd.yz *= rot(-.12);
|
|
85
|
+
vec3 col = vec3(0); float t = 0.; bool hit = false;
|
|
86
|
+
for (int i = 0; i < 130; i++) { float d = map(ro + rd * t); if (d < .0012) { hit = true; break; } t += d; if (t > 14.) break; }
|
|
87
|
+
float tf = (FLOOR - ro.y) / rd.y;
|
|
88
|
+
if (hit) {
|
|
89
|
+
vec3 p = ro + rd * t, n = nrm(p), r = reflect(rd, n);
|
|
90
|
+
float fr = .6 + .4 * pow(1. - max(dot(n, -rd), 0.), 5.);
|
|
91
|
+
col = env(r) * vec3(.9, .92, .96) * fr * GL + vec3(.02);
|
|
92
|
+
} else if (tf > 0.) {
|
|
93
|
+
vec3 fp = ro + rd * tf, r = reflect(rd, vec3(0, 1, 0)); // a dark glossy floor reflecting the studio
|
|
94
|
+
col = env(r) * .035 + vec3(.012, .013, .016);
|
|
95
|
+
col *= 1. - .6 * exp(-dot(fp.xz * vec2(.35, 1.4), fp.xz * vec2(.35, 1.4))); // contact shadow
|
|
96
|
+
} else col = mix(vec3(.025, .027, .034), vec3(.008, .009, .012), uv.y);
|
|
97
|
+
o = vec4(pow(aces(col), vec3(1. / 2.2)), 1);
|
|
98
|
+
}`;
|
|
99
|
+
const VS = `#version 300 es
|
|
100
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
101
|
+
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; };
|
|
102
|
+
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);
|
|
103
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
104
|
+
gl.useProgram(p); const U = n => gl.getUniformLocation(p, n);
|
|
105
|
+
gl.bindVertexArray(gl.createVertexArray()); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
106
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
107
|
+
const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.R16F, TW, TH, 0, gl.RED, gl.FLOAT, field);
|
|
108
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
109
|
+
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);
|
|
110
|
+
let W = 1, H = 1;
|
|
111
|
+
const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, 2, Math.max(0.5, Math.sqrt(330000 / (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); };
|
|
112
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
113
|
+
let raf, last = performance.now(), t = 0;
|
|
114
|
+
function frame(now) {
|
|
115
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt * PARAMS.speed;
|
|
116
|
+
// solid, melting (slow at first, then running), a puddle, then it pulls itself back up with a little overshoot
|
|
117
|
+
const c = t % 11; let m;
|
|
118
|
+
if (c < 2.5) m = 0; else if (c < 6) { const k = (c - 2.5) / 3.5; m = k * k * (3 - 2 * k); } else if (c < 7.6) m = 1;
|
|
119
|
+
else if (c < 9.6) { const k = (c - 7.6) / 2; m = Math.max(-0.08, 1 - (1 - Math.exp(-6 * k) * Math.cos(10 * k)) * 1.0); } else m = 0;
|
|
120
|
+
gl.uniform1i(U('F'), 0); gl.uniform2f(U('R'), W, H); gl.uniform1f(U('T'), t); gl.uniform1f(U('M'), Math.max(0, m)); gl.uniform1f(U('DR'), PARAMS.drips); gl.uniform1f(U('GL'), PARAMS.gloss);
|
|
121
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
122
|
+
raf = requestAnimationFrame(frame);
|
|
123
|
+
}
|
|
124
|
+
raf = requestAnimationFrame(frame);
|
|
125
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
126
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Menger sponge: a cube with its centre cross carved out, then the same carved out of each of the 20 smaller cubes
|
|
2
|
+
// left, and so on. As a distance function that is one box plus a loop: fold space into repeating cells (mod), and at
|
|
3
|
+
// each level subtract a cross three times thinner. The raymarcher counts the levels to shade the deep holes darker,
|
|
4
|
+
// the brass catches a studio light and a soft shadow is marched toward the lamp. The camera drifts in toward a face.
|
|
5
|
+
const PARAMS = { levels: 4, 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; uniform int L; uniform mat3 ROT; uniform vec3 CE, CF, CR, CU;
|
|
18
|
+
float sdBox(vec3 p, vec3 b) { vec3 q = abs(p) - b; return length(max(q, 0.)) + min(max(q.x, max(q.y, q.z)), 0.); }
|
|
19
|
+
// x: distance, y: how deep in the holes (for occlusion)
|
|
20
|
+
vec2 map(vec3 p) {
|
|
21
|
+
p = ROT * p;
|
|
22
|
+
float d = sdBox(p, vec3(1)), s = 1., occ = 1.;
|
|
23
|
+
for (int m = 0; m < 6; m++) {
|
|
24
|
+
if (m >= L) break;
|
|
25
|
+
vec3 a = mod(p * s, 2.) - 1.; s *= 3.;
|
|
26
|
+
vec3 r = abs(1. - 3. * abs(a));
|
|
27
|
+
float da = max(r.x, r.y), db = max(r.y, r.z), dc = max(r.z, r.x);
|
|
28
|
+
float c = (min(da, min(db, dc)) - 1.) / s; // the cross at this level
|
|
29
|
+
if (c > d) { d = c; occ = min(occ, .25 * da * db * dc); }
|
|
30
|
+
}
|
|
31
|
+
return vec2(d, occ);
|
|
32
|
+
}
|
|
33
|
+
vec3 nrm(vec3 p) { vec2 e = vec2(.0007, -.0007); return normalize(e.xyy * map(p + e.xyy).x + e.yyx * map(p + e.yyx).x + e.yxy * map(p + e.yxy).x + e.xxx * map(p + e.xxx).x); }
|
|
34
|
+
float shadow(vec3 ro, vec3 rd) { float r = 1., t = .01; for (int i = 0; i < 48; i++) { float h = map(ro + rd * t).x; r = min(r, 10. * h / t); t += clamp(h, .005, .1); if (r < .001 || t > 4.) break; } return clamp(r, 0., 1.); }
|
|
35
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
36
|
+
void main() {
|
|
37
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
38
|
+
vec3 ro = CE, rd = normalize(CF * 1.8 + CR * q.x + CU * q.y);
|
|
39
|
+
vec3 bg = mix(vec3(.05, .055, .07), vec3(.16, .14, .13), uv.y * .6 + .2);
|
|
40
|
+
vec3 col = bg; float t = 0.; vec2 h; bool hit = false;
|
|
41
|
+
for (int i = 0; i < 160; i++) { h = map(ro + rd * t); if (h.x < .0004 * (1. + t)) { hit = true; break; } t += h.x; if (t > 12.) break; }
|
|
42
|
+
if (hit) {
|
|
43
|
+
vec3 p = ro + rd * t, n = nrm(p), L1 = normalize(vec3(.6, .8, .4));
|
|
44
|
+
float dif = max(dot(n, L1), 0.) * shadow(p + n * .002, L1), sky = .5 + .5 * n.y, back = max(dot(n, -L1 * vec3(1, 0, 1)), 0.);
|
|
45
|
+
float occ = clamp(h.y * 1.4, 0., 1.);
|
|
46
|
+
vec3 brass = vec3(.86, .66, .38);
|
|
47
|
+
vec3 r = reflect(rd, n);
|
|
48
|
+
float spec = pow(max(dot(r, L1), 0.), 40.) * dif;
|
|
49
|
+
col = brass * (dif * vec3(1.25, 1.1, .95) + sky * vec3(.12, .14, .2) * occ + back * vec3(.08, .07, .1)) * (.3 + .7 * occ) + spec * vec3(1.2, 1., .8);
|
|
50
|
+
col += brass * .5 * mix(vec3(.05), vec3(.5, .45, .4), smoothstep(-.2, .6, r.y)) * occ * pow(1. - max(dot(n, -rd), 0.), 2.); // a little sheen at grazing angles
|
|
51
|
+
col = mix(col, bg, smoothstep(4., 11., t));
|
|
52
|
+
}
|
|
53
|
+
col = aces(col);
|
|
54
|
+
col *= 1. - .3 * pow(length(uv - .5) * 1.25, 2.);
|
|
55
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
56
|
+
}`;
|
|
57
|
+
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; };
|
|
58
|
+
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);
|
|
59
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
60
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
61
|
+
const U = {}; ['R', 'T', 'L', 'ROT', 'CE', 'CF', 'CR', 'CU'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
62
|
+
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); };
|
|
63
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
64
|
+
const nrm = a => { const l = Math.hypot(...a); return a.map(v => v / l); };
|
|
65
|
+
let raf; const t0 = performance.now();
|
|
66
|
+
function frame(now) {
|
|
67
|
+
const t = (now - t0) / 1000 * PARAMS.speed;
|
|
68
|
+
const ya = t * 0.18 + 0.6, pa = 0.5 + Math.sin(t * 0.13) * 0.15, cy = Math.cos(ya), sy = Math.sin(ya), cp = Math.cos(pa), sp = Math.sin(pa);
|
|
69
|
+
const rot = new Float32Array([cy, sp * sy, -cp * sy, 0, cp, sp, sy, -sp * cy, cp * cy]);
|
|
70
|
+
const asp = canvas.width / canvas.height, dist = (4.3 - 0.9 * (0.5 + 0.5 * Math.sin(t * 0.21))) * Math.max(1, 1.15 / asp); // drift in and out
|
|
71
|
+
const eye = [0.25, 0.35, dist], f = nrm(eye.map(v => -v)), r = nrm([-f[2], 0, f[0]]), u = [r[1] * f[2] - r[2] * f[1], r[2] * f[0] - r[0] * f[2], r[0] * f[1] - r[1] * f[0]];
|
|
72
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, t); gl.uniform1i(U.L, Math.round(PARAMS.levels));
|
|
73
|
+
gl.uniformMatrix3fv(U.ROT, false, rot); gl.uniform3fv(U.CE, eye); gl.uniform3fv(U.CF, f); gl.uniform3fv(U.CR, r); gl.uniform3fv(U.CU, u);
|
|
74
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
75
|
+
raf = requestAnimationFrame(frame);
|
|
76
|
+
}
|
|
77
|
+
raf = requestAnimationFrame(frame);
|
|
78
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
79
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Neon geodesic: an icosahedron with every face split into nine (frequency 3), pushed out onto a sphere:
|
|
2
|
+
// 92 nodes, 270 struts. Each strut is stroked as neon tube light; the glow is the same drawing shrunk and
|
|
3
|
+
// blown back up (a cheap bloom). Pulses run along the struts, preferring to go straight, and light the
|
|
4
|
+
// panels they pass. A bright ring sweeps pole to pole. The far side is dimmer, and a glossy floor mirrors it.
|
|
5
|
+
const PARAMS = { pulses: 14, spin: 1, glow: 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 ctx = canvas.getContext('2d');
|
|
12
|
+
const mk = () => { const c = document.createElement('canvas'); return [c, c.getContext('2d')]; };
|
|
13
|
+
const [neon, nx] = mk(), [sm1, s1x] = mk(), [sm2, s2x] = mk(), [refl, rx] = mk();
|
|
14
|
+
let W = 0, H = 0, D = 1;
|
|
15
|
+
const resize = () => {
|
|
16
|
+
D = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1;
|
|
17
|
+
canvas.width = neon.width = refl.width = W * D | 0; canvas.height = neon.height = refl.height = H * D | 0;
|
|
18
|
+
sm1.width = Math.max(1, W * D / 4 | 0); sm1.height = Math.max(1, H * D / 4 | 0); sm2.width = Math.max(1, W * D / 12 | 0); sm2.height = Math.max(1, H * D / 12 | 0);
|
|
19
|
+
};
|
|
20
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
21
|
+
|
|
22
|
+
// ── geometry: frequency-3 icosphere
|
|
23
|
+
const g = (1 + Math.sqrt(5)) / 2;
|
|
24
|
+
const ico = [[-1, g, 0], [1, g, 0], [-1, -g, 0], [1, -g, 0], [0, -1, g], [0, 1, g], [0, -1, -g], [0, 1, -g], [g, 0, -1], [g, 0, 1], [-g, 0, -1], [-g, 0, 1]];
|
|
25
|
+
const icoF = [[0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1]];
|
|
26
|
+
const V = [], vmap = new Map(), E = [], emap = new Map(), F = [], pent = new Set();
|
|
27
|
+
const nrm = a => { const l = Math.hypot(...a); return a.map(v => v / l); };
|
|
28
|
+
const vid = p => { const q = nrm(p), k = q.map(v => Math.round(v * 1e4)).join(','); if (!vmap.has(k)) { vmap.set(k, V.length); V.push(q); } return vmap.get(k); };
|
|
29
|
+
const eid = (a, b) => { const k = a < b ? a + '_' + b : b + '_' + a; if (!emap.has(k)) { emap.set(k, E.length); E.push({ a: Math.min(a, b), b: Math.max(a, b), faces: [] }); } return emap.get(k); };
|
|
30
|
+
const Fq = 3;
|
|
31
|
+
for (const [A, B, C] of icoF) {
|
|
32
|
+
const P = (i, j) => vid([0, 1, 2].map(c => (ico[A][c] * (Fq - i - j) + ico[B][c] * i + ico[C][c] * j) / Fq));
|
|
33
|
+
pent.add(P(0, 0)); pent.add(P(Fq, 0)); pent.add(P(0, Fq));
|
|
34
|
+
for (let j = 0; j < Fq; j++) for (let i = 0; i < Fq - j; i++) {
|
|
35
|
+
const tri = [[P(i, j), P(i + 1, j), P(i, j + 1)]];
|
|
36
|
+
if (i + j < Fq - 1) tri.push([P(i + 1, j), P(i + 1, j + 1), P(i, j + 1)]);
|
|
37
|
+
for (const t of tri) { const f = F.length; F.push({ v: t, e: 0 }); for (let k = 0; k < 3; k++) E[eid(t[k], t[(k + 1) % 3])].faces.push(f); }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const adj = V.map(() => []); E.forEach((e, i) => { adj[e.a].push([e.b, i]); adj[e.b].push([e.a, i]); });
|
|
41
|
+
const nodeHeat = new Float32Array(V.length);
|
|
42
|
+
|
|
43
|
+
// ── pulses: walk the struts, preferring to carry on straight, and light the panels they pass
|
|
44
|
+
const pulses = [];
|
|
45
|
+
const spawn = () => { const a = Math.random() * V.length | 0, [b] = adj[a][Math.random() * adj[a].length | 0]; return { a, b, p: Math.random(), speed: 1.1 + Math.random() * 0.9, trail: [], hue: Math.random() < 0.35 ? 1 : 0 }; };
|
|
46
|
+
const advance = (P, dt) => {
|
|
47
|
+
const A = V[P.a], B = V[P.b], len = Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]);
|
|
48
|
+
P.p += dt * P.speed * 0.42 / len;
|
|
49
|
+
while (P.p >= 1) {
|
|
50
|
+
P.p -= 1; nodeHeat[P.b] = 1;
|
|
51
|
+
const dir = nrm(V[P.b].map((v, c) => v - V[P.a][c]));
|
|
52
|
+
let best = null, bs = -1e9;
|
|
53
|
+
for (const [n] of adj[P.b]) { if (n === P.a) continue; const d = nrm(V[n].map((v, c) => v - V[P.b][c])), s = d[0] * dir[0] + d[1] * dir[1] + d[2] * dir[2] + Math.random() * 0.9; if (s > bs) { bs = s; best = n; } }
|
|
54
|
+
P.a = P.b; P.b = best;
|
|
55
|
+
for (const f of E[emap.get(P.a < P.b ? P.a + '_' + P.b : P.b + '_' + P.a)].faces) F[f].e = 1;
|
|
56
|
+
}
|
|
57
|
+
const A2 = V[P.a], B2 = V[P.b]; P.pos = nrm(A2.map((v, c) => v + (B2[c] - v) * P.p));
|
|
58
|
+
P.trail.push(P.pos); if (P.trail.length > 16) P.trail.shift();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let raf, last = performance.now(), ang = 0; const t0 = last;
|
|
62
|
+
function frame(now) {
|
|
63
|
+
const dt = Math.min(0.05, (now - last) / 1000), t = (now - t0) / 1000; last = now;
|
|
64
|
+
ang += dt * 0.32 * PARAMS.spin;
|
|
65
|
+
const n = Math.max(0, Math.round(PARAMS.pulses)); while (pulses.length < n) pulses.push(spawn()); pulses.length = n;
|
|
66
|
+
for (const P of pulses) advance(P, dt);
|
|
67
|
+
const fade = Math.exp(-dt * 2.2); for (const f of F) f.e *= fade; for (let i = 0; i < nodeHeat.length; i++) nodeHeat[i] *= Math.exp(-dt * 3);
|
|
68
|
+
|
|
69
|
+
const R = Math.min(W * 0.3, H * 0.28), cx = W / 2, cy = H * 0.41, floorY = cy + R * 1.3;
|
|
70
|
+
const tilt = 0.36 + Math.sin(t * 0.21) * 0.08, ca = Math.cos(ang), sa = Math.sin(ang), ct = Math.cos(tilt), st = Math.sin(tilt), cam = 3.6;
|
|
71
|
+
const P3 = new Float32Array(V.length * 4);
|
|
72
|
+
const proj = (x, y, z, o) => { const x1 = x * ca + z * sa, z1 = -x * sa + z * ca, y1 = y * ct - z1 * st, z2 = y * st + z1 * ct, k = cam / (cam - z2); o[0] = cx + x1 * R * k; o[1] = cy - y1 * R * k; o[2] = z2; o[3] = k; return o; };
|
|
73
|
+
const tmp = [0, 0, 0, 0];
|
|
74
|
+
V.forEach((v, i) => { proj(v[0], v[1], v[2], tmp); P3.set(tmp, i * 4); });
|
|
75
|
+
const hueShift = Math.sin(t * 0.17) * 18;
|
|
76
|
+
const grad = (c, a) => { const gr = c.createLinearGradient(0, cy - R * 1.1, 0, cy + R * 1.1); gr.addColorStop(0, `hsla(${188 + hueShift},100%,64%,${a})`); gr.addColorStop(0.5, `hsla(${258 + hueShift},100%,68%,${a})`); gr.addColorStop(1, `hsla(${318 + hueShift},100%,64%,${a})`); return gr; };
|
|
77
|
+
|
|
78
|
+
// ── neon layer (in CSS pixels, scaled by D)
|
|
79
|
+
nx.setTransform(1, 0, 0, 1, 0, 0); nx.clearRect(0, 0, neon.width, neon.height); nx.setTransform(D, 0, 0, D, 0, 0);
|
|
80
|
+
nx.globalCompositeOperation = 'lighter'; nx.lineCap = 'round'; nx.lineJoin = 'round';
|
|
81
|
+
// panels lit by passing pulses (near side only)
|
|
82
|
+
for (const f of F) {
|
|
83
|
+
if (f.e < 0.02) continue;
|
|
84
|
+
const [a, b, c] = f.v, z = (P3[a * 4 + 2] + P3[b * 4 + 2] + P3[c * 4 + 2]) / 3; if (z < -0.15) continue;
|
|
85
|
+
nx.fillStyle = grad(nx, f.e * 0.16 * Math.min(1, z + 0.6)); nx.beginPath(); nx.moveTo(P3[a * 4], P3[a * 4 + 1]); nx.lineTo(P3[b * 4], P3[b * 4 + 1]); nx.lineTo(P3[c * 4], P3[c * 4 + 1]); nx.closePath(); nx.fill();
|
|
86
|
+
}
|
|
87
|
+
// struts in four depth bands: the far side thinner and dimmer
|
|
88
|
+
const sweep = ((t * 0.3) % 1.7) * 2 - 1.5, hot = [];
|
|
89
|
+
for (let band = 0; band < 4; band++) {
|
|
90
|
+
nx.beginPath();
|
|
91
|
+
for (const e of E) {
|
|
92
|
+
const za = P3[e.a * 4 + 2], zb = P3[e.b * 4 + 2], z = (za + zb) / 2, bnd = Math.min(3, Math.max(0, Math.floor((z + 1) * 2)));
|
|
93
|
+
if (bnd !== band) continue;
|
|
94
|
+
nx.moveTo(P3[e.a * 4], P3[e.a * 4 + 1]); nx.lineTo(P3[e.b * 4], P3[e.b * 4 + 1]);
|
|
95
|
+
if (Math.abs((V[e.a][1] + V[e.b][1]) / 2 - sweep) < 0.09) hot.push(e);
|
|
96
|
+
}
|
|
97
|
+
nx.strokeStyle = grad(nx, [0.07, 0.16, 0.45, 0.95][band]); nx.lineWidth = [0.7, 0.9, 1.2, 1.5][band]; nx.stroke();
|
|
98
|
+
}
|
|
99
|
+
// the sweeping ring: those struts burn whiter
|
|
100
|
+
nx.beginPath(); for (const e of hot) { const z = (P3[e.a * 4 + 2] + P3[e.b * 4 + 2]) / 2; if (z < -0.3) continue; nx.moveTo(P3[e.a * 4], P3[e.a * 4 + 1]); nx.lineTo(P3[e.b * 4], P3[e.b * 4 + 1]); }
|
|
101
|
+
nx.strokeStyle = 'rgba(225,240,255,.7)'; nx.lineWidth = 1.5; nx.stroke();
|
|
102
|
+
// nodes: the twelve five-way joints are bigger
|
|
103
|
+
for (let i = 0; i < V.length; i++) {
|
|
104
|
+
const x = P3[i * 4], y = P3[i * 4 + 1], z = P3[i * 4 + 2], k = P3[i * 4 + 3], a = 0.25 + 0.75 * Math.max(0, (z + 1) / 2) ** 1.5, hh = nodeHeat[i];
|
|
105
|
+
const r = (pent.has(i) ? 2 : 1.1) * k + hh * 1.6;
|
|
106
|
+
nx.fillStyle = `rgba(${200 + 55 * hh | 0},${230 + 25 * hh | 0},255,${Math.min(1, a * ((pent.has(i) ? 0.6 : 0.3) + hh))})`; nx.beginPath(); nx.arc(x, y, r, 0, 6.2832); nx.fill();
|
|
107
|
+
}
|
|
108
|
+
// pulses: a fading tail and a white-hot head
|
|
109
|
+
for (const P of pulses) {
|
|
110
|
+
const pts = P.trail.map(p => proj(p[0], p[1], p[2], [0, 0, 0, 0])), col = P.hue ? '255,236,190' : '255,178,90';
|
|
111
|
+
for (let i = 1; i < pts.length; i++) {
|
|
112
|
+
const a = pts[i - 1], b = pts[i], f = i / pts.length, depth = Math.max(0.15, (b[2] + 1) / 2);
|
|
113
|
+
nx.strokeStyle = `rgba(${col},${f * 0.9 * depth})`; nx.lineWidth = (0.8 + f * 1.7) * b[3]; nx.beginPath(); nx.moveTo(a[0], a[1]); nx.lineTo(b[0], b[1]); nx.stroke();
|
|
114
|
+
}
|
|
115
|
+
const h = pts[pts.length - 1]; if (!h) continue; const depth = Math.max(0.2, (h[2] + 1) / 2);
|
|
116
|
+
nx.fillStyle = `rgba(255,255,255,${depth})`; nx.beginPath(); nx.arc(h[0], h[1], 2.2 * h[3], 0, 6.2832); nx.fill();
|
|
117
|
+
nx.fillStyle = `rgba(${col},${0.4 * depth})`; nx.beginPath(); nx.arc(h[0], h[1], 5 * h[3], 0, 6.2832); nx.fill();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── compose: background, floor glow, mirrored reflection, then bloom + crisp neon
|
|
121
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalCompositeOperation = 'source-over';
|
|
122
|
+
const bg = ctx.createLinearGradient(0, 0, 0, canvas.height); bg.addColorStop(0, '#07061a'); const fy = Math.min(0.99, floorY / H); bg.addColorStop(fy, '#0f0b2a'); bg.addColorStop(fy + 0.005, '#06051a'); bg.addColorStop(1, '#030210');
|
|
123
|
+
ctx.fillStyle = bg; ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
124
|
+
const halo = ctx.createRadialGradient(cx * D, cy * D, R * 0.4 * D, cx * D, cy * D, R * 2.1 * D); halo.addColorStop(0, `hsla(${255 + hueShift},90%,45%,.22)`); halo.addColorStop(1, 'hsla(260,90%,40%,0)');
|
|
125
|
+
ctx.fillStyle = halo; ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
126
|
+
const pool = ctx.createRadialGradient(cx * D, floorY * D, 0, cx * D, floorY * D, R * 1.6 * D); pool.addColorStop(0, `hsla(${300 + hueShift},100%,60%,.22)`); pool.addColorStop(1, 'hsla(300,100%,60%,0)');
|
|
127
|
+
ctx.save(); ctx.translate(cx * D, floorY * D); ctx.scale(1, 0.16); ctx.translate(-cx * D, -floorY * D); ctx.fillStyle = pool; ctx.fillRect(0, (floorY - R * 1.6) * D, canvas.width, R * 3.2 * D); ctx.restore();
|
|
128
|
+
// reflection: the neon layer flipped about the floor line, faded with distance
|
|
129
|
+
rx.setTransform(1, 0, 0, 1, 0, 0); rx.globalCompositeOperation = 'source-over'; rx.clearRect(0, 0, refl.width, refl.height);
|
|
130
|
+
rx.setTransform(1, 0, 0, -1, 0, 2 * floorY * D); rx.drawImage(neon, 0, 0); rx.setTransform(1, 0, 0, 1, 0, 0);
|
|
131
|
+
rx.globalCompositeOperation = 'destination-in';
|
|
132
|
+
const rf = rx.createLinearGradient(0, floorY * D, 0, (floorY + R * 1.4) * D); rf.addColorStop(0, 'rgba(0,0,0,.42)'); rf.addColorStop(1, 'rgba(0,0,0,0)');
|
|
133
|
+
rx.fillStyle = rf; rx.fillRect(0, floorY * D, refl.width, refl.height);
|
|
134
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
135
|
+
ctx.drawImage(refl, 0, 0);
|
|
136
|
+
// bloom: shrink twice, blow back up (bilinear), add
|
|
137
|
+
s1x.clearRect(0, 0, sm1.width, sm1.height); s1x.drawImage(neon, 0, 0, sm1.width, sm1.height); s1x.drawImage(refl, 0, 0, sm1.width, sm1.height);
|
|
138
|
+
s2x.clearRect(0, 0, sm2.width, sm2.height); s2x.drawImage(sm1, 0, 0, sm2.width, sm2.height);
|
|
139
|
+
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
|
|
140
|
+
const gk = PARAMS.glow;
|
|
141
|
+
ctx.globalAlpha = Math.min(1, 0.85 * gk); ctx.drawImage(sm2, 0, 0, canvas.width, canvas.height);
|
|
142
|
+
ctx.globalAlpha = Math.min(1, 0.5 * gk); ctx.drawImage(sm1, 0, 0, canvas.width, canvas.height);
|
|
143
|
+
ctx.globalAlpha = 1; ctx.drawImage(neon, 0, 0);
|
|
144
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
145
|
+
raf = requestAnimationFrame(frame);
|
|
146
|
+
}
|
|
147
|
+
raf = requestAnimationFrame(frame);
|
|
148
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
149
|
+
}
|