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,357 @@
|
|
|
1
|
+
// Drone light show: a fleet of drones lifts off a barge and flies formations over a night city: a turning globe,
|
|
2
|
+
// Saturn and its ring, a beating heart, a word. Every change of formation is an assignment problem: each drone needs
|
|
3
|
+
// its own spot, and the show only looks clean if no drone crosses the sky to reach it. Targets are matched by
|
|
4
|
+
// sorting both sets into bands, then thousands of swap tests each keep a swap only if it shortens the total flight.
|
|
5
|
+
// The bay reflects everything: the image is mirrored at the waterline, broken up by the swell and stretched toward you.
|
|
6
|
+
const PARAMS = { drones: 1500, speed: 1, glow: 1 };
|
|
7
|
+
const WORD = 'HELLO';
|
|
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 hdr = !!gl.getExtension('EXT_color_buffer_float');
|
|
16
|
+
const N = Math.max(200, Math.round(PARAMS.drones));
|
|
17
|
+
|
|
18
|
+
// the world is in metres: the camera floats 4 m above the bay; the drones fly 500 m away, the city stands at 760 m
|
|
19
|
+
const CAMH = 4, FOC = 4, HOR = -0.44, CZ = 500, CY = 98, RAD = 46;
|
|
20
|
+
const BASE = ((FOC * -CAMH) / 760 + HOR) * 0.5 + 0.5; // the city's waterline, as a fraction of the height
|
|
21
|
+
|
|
22
|
+
// ── small JS noise for the continents and Saturn's bands
|
|
23
|
+
const hash3 = (x, y, z) => { const h = Math.sin(x * 127.1 + y * 311.7 + z * 74.7) * 43758.5453; return h - Math.floor(h); };
|
|
24
|
+
const vnoise = (x, y, z) => {
|
|
25
|
+
const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z), s = v => v * v * (3 - 2 * v);
|
|
26
|
+
const u = s(x - xi), v = s(y - yi), w = s(z - zi), L = (a, b, t) => a + (b - a) * t;
|
|
27
|
+
const c = (i, j, k) => hash3(xi + i, yi + j, zi + k);
|
|
28
|
+
return L(L(L(c(0, 0, 0), c(1, 0, 0), u), L(c(0, 1, 0), c(1, 1, 0), u), v), L(L(c(0, 0, 1), c(1, 0, 1), u), L(c(0, 1, 1), c(1, 1, 1), u), v), w);
|
|
29
|
+
};
|
|
30
|
+
const fbm = (x, y, z) => vnoise(x, y, z) * 0.55 + vnoise(x * 2.1, y * 2.1, z * 2.1) * 0.3 + vnoise(x * 4.3, y * 4.3, z * 4.3) * 0.15;
|
|
31
|
+
const smooth = (a, b, v) => { const t = Math.min(1, Math.max(0, (v - a) / (b - a))); return t * t * (3 - 2 * t); };
|
|
32
|
+
const lin = c => c.map(v => v * v); // colours are picked by eye, mixed as light
|
|
33
|
+
|
|
34
|
+
// ── formations: each knows a colour per slot and where slot k is at time t (it returns that slot's brightness)
|
|
35
|
+
function makePad() {
|
|
36
|
+
const rows = Math.max(4, Math.round(Math.sqrt(N / 7))), cols = Math.ceil(N / rows), col = new Float32Array(N * 3);
|
|
37
|
+
for (let k = 0; k < N; k++) col.set(lin([0.55, 0.68, 0.85]), k * 3);
|
|
38
|
+
return { col, pos(k, t, o) { o[0] = ((k % cols) - (cols - 1) / 2) * 1.3; o[1] = 0.7; o[2] = CZ + (((k / cols) | 0) - (rows - 1) / 2) * 1.3; return 0.07; } };
|
|
39
|
+
}
|
|
40
|
+
function makeGlobe() {
|
|
41
|
+
const P = new Float32Array(N * 3), col = new Float32Array(N * 3);
|
|
42
|
+
for (let k = 0; k < N; k++) {
|
|
43
|
+
const y = 1 - 2 * (k + 0.5) / N, r = Math.sqrt(1 - y * y), a = k * 2.399963, x = Math.cos(a) * r, z = Math.sin(a) * r;
|
|
44
|
+
P[k * 3] = x; P[k * 3 + 1] = y; P[k * 3 + 2] = z;
|
|
45
|
+
const n = fbm(x * 1.05 + 5.2, y * 1.05 + 1.3, z * 1.05 - 2.7);
|
|
46
|
+
let c;
|
|
47
|
+
if (Math.abs(y) > 0.9) c = [0.85, 0.93, 1];
|
|
48
|
+
else if (n > 0.5) { const m = Math.min(1, (n - 0.5) * 8); c = [0.12 + 0.85 * m * m, 0.9 - 0.18 * m, 0.22]; } // forest, then desert inland
|
|
49
|
+
else { const m = Math.min(1, (0.5 - n) * 9); c = [0.0, 0.8 - 0.55 * m, 1]; } // shallows, then deep ocean
|
|
50
|
+
col.set(lin(c), k * 3);
|
|
51
|
+
}
|
|
52
|
+
return { col, pos(k, t, o) {
|
|
53
|
+
let x = P[k * 3], y = P[k * 3 + 1], z = P[k * 3 + 2], u;
|
|
54
|
+
const a = t * 0.28, ca = Math.cos(a), sa = Math.sin(a); u = x * ca + z * sa; z = -x * sa + z * ca; x = u; // spin
|
|
55
|
+
u = x * 0.917 - y * 0.399; y = x * 0.399 + y * 0.917; x = u; // axial tilt
|
|
56
|
+
u = y * 0.955 + z * 0.296; z = -y * 0.296 + z * 0.955; y = u; // north leans toward us
|
|
57
|
+
o[0] = x * RAD; o[1] = CY + y * RAD; o[2] = CZ + z * RAD;
|
|
58
|
+
return 0.08 + 0.92 * smooth(-0.2, 0.45, -z); // the far side is dimmed, as a show designer would
|
|
59
|
+
} };
|
|
60
|
+
}
|
|
61
|
+
function makeSaturn() {
|
|
62
|
+
const P = new Float32Array(N * 3), col = new Float32Array(N * 3), NP = Math.round(N * 0.42);
|
|
63
|
+
for (let k = 0; k < N; k++) {
|
|
64
|
+
let x, y, z, c;
|
|
65
|
+
if (k < NP) {
|
|
66
|
+
y = 1 - 2 * (k + 0.5) / NP; const r = Math.sqrt(1 - y * y), a = k * 2.399963; x = Math.cos(a) * r * 0.58; z = Math.sin(a) * r * 0.58; y *= 0.53;
|
|
67
|
+
const band = 0.5 + 0.5 * Math.sin(y * 19 + fbm(x * 3, y * 3, z * 3) * 4);
|
|
68
|
+
c = [1 - 0.12 * band, 0.86 - 0.38 * band, 0.6 - 0.42 * band];
|
|
69
|
+
} else {
|
|
70
|
+
let r; do { r = Math.sqrt(0.72 + Math.random() * 1.38); } while (r > 1.2 && r < 1.26); // Cassini division
|
|
71
|
+
const a = Math.random() * Math.PI * 2; x = Math.cos(a) * r; z = Math.sin(a) * r; y = (Math.random() - 0.5) * 0.02;
|
|
72
|
+
const s = r < 0.98 ? 0.5 : r < 1.2 ? 1 : 0.72; c = [0.98 * s, 0.86 * s, 0.62 * s];
|
|
73
|
+
}
|
|
74
|
+
P[k * 3] = x; P[k * 3 + 1] = y; P[k * 3 + 2] = z; col.set(lin(c), k * 3);
|
|
75
|
+
}
|
|
76
|
+
return { col, pos(k, t, o) {
|
|
77
|
+
let x = P[k * 3], y = P[k * 3 + 1], z = P[k * 3 + 2], u;
|
|
78
|
+
u = y * 0.927 - z * 0.375; z = y * 0.375 + z * 0.927; y = u; // tip the ring open
|
|
79
|
+
u = x * 0.98 - y * 0.199; y = x * 0.199 + y * 0.98; x = u;
|
|
80
|
+
const a = 0.45 * Math.sin(t * 0.32), ca = Math.cos(a), sa = Math.sin(a); u = x * ca + z * sa; z = -x * sa + z * ca; x = u;
|
|
81
|
+
o[0] = x * RAD; o[1] = CY + y * RAD; o[2] = CZ + z * RAD;
|
|
82
|
+
if (k < NP) return 0.1 + 0.9 * smooth(-0.15, 0.3, -z);
|
|
83
|
+
return z > 0 && x * x + (y * 1.08) ** 2 < 0.56 * 0.56 ? 0.08 : 1; // ring behind the planet goes dark
|
|
84
|
+
} };
|
|
85
|
+
}
|
|
86
|
+
function makeHeart() {
|
|
87
|
+
const M = 240, poly = [];
|
|
88
|
+
for (let i = 0; i < M; i++) { const s = (i / M) * Math.PI * 2; poly.push([(16 * Math.sin(s) ** 3) / 17, (13 * Math.cos(s) - 5 * Math.cos(2 * s) - 2 * Math.cos(3 * s) - Math.cos(4 * s)) / 17 + 0.12]); }
|
|
89
|
+
const inside = (x, y) => { let c = false; for (let i = 0, j = M - 1; i < M; j = i++) { const [xi, yi] = poly[i], [xj, yj] = poly[j]; if ((yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) c = !c; } return c; };
|
|
90
|
+
const edge = (x, y) => { let m = 9; for (const [px, py] of poly) m = Math.min(m, (px - x) ** 2 + (py - y) ** 2); return Math.sqrt(m); };
|
|
91
|
+
const P = new Float32Array(N * 3), col = new Float32Array(N * 3);
|
|
92
|
+
for (let k = 0; k < N;) {
|
|
93
|
+
const x = Math.random() * 2 - 1, y = Math.random() * 2 - 1.05;
|
|
94
|
+
if (!inside(x, y)) continue;
|
|
95
|
+
const d = Math.min(1, edge(x, y) / 0.42), z = (k % 2 ? 1 : -1) * 0.36 * Math.sqrt(d);
|
|
96
|
+
P[k * 3] = x; P[k * 3 + 1] = y; P[k * 3 + 2] = z;
|
|
97
|
+
const hl = Math.max(0, 1 - Math.hypot(x + 0.42, y - 0.36) / 0.55) * (z < 0 ? 1 : 0.3);
|
|
98
|
+
col.set(lin([1, 0.12 + 0.55 * hl, 0.3 + 0.45 * hl]), k * 3); k++;
|
|
99
|
+
}
|
|
100
|
+
return { col, pos(k, t, o) {
|
|
101
|
+
const tau = (t * 1.15) % 1, beat = 1 + 0.07 * (Math.exp(-(((tau - 0.1) / 0.05) ** 2)) + 0.6 * Math.exp(-(((tau - 0.3) / 0.06) ** 2)));
|
|
102
|
+
let x = P[k * 3] * beat, y = P[k * 3 + 1] * beat, z = P[k * 3 + 2] * beat, u;
|
|
103
|
+
const a = 0.5 * Math.sin(t * 0.55), ca = Math.cos(a), sa = Math.sin(a); u = x * ca + z * sa; z = -x * sa + z * ca; x = u;
|
|
104
|
+
o[0] = x * RAD; o[1] = CY + y * RAD; o[2] = CZ + z * RAD;
|
|
105
|
+
return z < 0.05 ? 1 : 0.4;
|
|
106
|
+
} };
|
|
107
|
+
}
|
|
108
|
+
function makeWord() {
|
|
109
|
+
const c = document.createElement('canvas'); c.width = 1200; c.height = 300;
|
|
110
|
+
const g = c.getContext('2d'); g.font = '900 220px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle';
|
|
111
|
+
g.fillStyle = '#fff'; g.fillText(WORD, 600, 150, 1160);
|
|
112
|
+
const px = g.getImageData(0, 0, 1200, 300).data, spots = [];
|
|
113
|
+
for (let y = 0; y < 300; y += 2) for (let x = 0; x < 1200; x += 2) if (px[(y * 1200 + x) * 4 + 3] > 140) spots.push(x, y);
|
|
114
|
+
let x0 = 1e9, x1 = -1e9; for (let i = 0; i < spots.length; i += 2) { x0 = Math.min(x0, spots[i]); x1 = Math.max(x1, spots[i]); }
|
|
115
|
+
const sc = 2.5 / Math.max(1, x1 - x0), cx = (x0 + x1) / 2, P = new Float32Array(N * 3), col = new Float32Array(N * 3), n = spots.length / 2;
|
|
116
|
+
for (let k = 0; k < N; k++) {
|
|
117
|
+
const s = n ? (Math.random() * n) | 0 : 0, x = ((spots[s * 2] ?? 600) - cx + Math.random() * 2) * sc, y = -((spots[s * 2 + 1] ?? 150) - 150 + Math.random() * 2) * sc;
|
|
118
|
+
P[k * 3] = x; P[k * 3 + 1] = y; P[k * 3 + 2] = 0;
|
|
119
|
+
col.set(lin([1, 0.78 + 0.18 * (x / 2.5 + 0.5), 0.45 + 0.5 * (x / 2.5 + 0.5)]), k * 3);
|
|
120
|
+
}
|
|
121
|
+
return { col, pos(k, t, o) {
|
|
122
|
+
const x = P[k * 3], y = P[k * 3 + 1];
|
|
123
|
+
o[0] = x * RAD; o[1] = CY + y * RAD; o[2] = CZ + Math.sin(x * 2.2 - t * 1.6) * 6;
|
|
124
|
+
return 0.5 + 0.5 * Math.max(0, Math.sin(x * 2.6 - t * 3.4)) ** 3 + 0.12 * Math.sin(t * 9 + k); // a shimmer runs along the letters
|
|
125
|
+
} };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pad = makePad(), forms = { globe: makeGlobe(), saturn: makeSaturn(), heart: makeHeart(), word: makeWord() };
|
|
129
|
+
const SHOW = [
|
|
130
|
+
{ form: pad, tr: 0, hold: 1.2 },
|
|
131
|
+
{ form: forms.globe, tr: 5.6, hold: 7, kind: 'launch' },
|
|
132
|
+
{ form: forms.saturn, tr: 4.6, hold: 7.5 },
|
|
133
|
+
{ form: forms.heart, tr: 4.6, hold: 6.5 },
|
|
134
|
+
{ form: forms.word, tr: 4.6, hold: 6.5 },
|
|
135
|
+
{ form: pad, tr: 6.2, hold: 2.4, kind: 'land' },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
// ── assignment: band-sort both sets, then keep every swap that shortens the total (squared) flight
|
|
139
|
+
const cur = new Float32Array(N * 3), curCol = new Float32Array(N * 3), from = new Float32Array(N * 3), fromCol = new Float32Array(N * 3);
|
|
140
|
+
const delay = new Float32Array(N), Q = new Float32Array(N * 3), tmp = [0, 0, 0];
|
|
141
|
+
let perm = new Int32Array(N);
|
|
142
|
+
function assign(form, tEnd) {
|
|
143
|
+
for (let k = 0; k < N; k++) { form.pos(k, tEnd, tmp); Q[k * 3] = tmp[0]; Q[k * 3 + 1] = tmp[1]; Q[k * 3 + 2] = tmp[2]; }
|
|
144
|
+
let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9;
|
|
145
|
+
for (let i = 0; i < N; i++) { x0 = Math.min(x0, from[i * 3]); x1 = Math.max(x1, from[i * 3]); y0 = Math.min(y0, from[i * 3 + 1]); y1 = Math.max(y1, from[i * 3 + 1]); }
|
|
146
|
+
const flat = y1 - y0 < (x1 - x0) * 0.2; // a flat pad sorts by x first: drones rise straight up
|
|
147
|
+
const a = flat ? 0 : 1, b = flat ? 1 : 0, ap = flat ? 0 : 1, bp = flat ? 2 : 0, sgn = flat ? -1 : 1;
|
|
148
|
+
const ip = Array.from({ length: N }, (_, i) => i).sort((i, j) => from[i * 3 + ap] - from[j * 3 + ap]);
|
|
149
|
+
const iq = Array.from({ length: N }, (_, i) => i).sort((i, j) => Q[i * 3 + a] - Q[j * 3 + a]);
|
|
150
|
+
const band = Math.max(1, Math.round(Math.sqrt(N) * 0.9)), out = new Int32Array(N);
|
|
151
|
+
for (let s = 0; s < N; s += band) {
|
|
152
|
+
const e = Math.min(N, s + band);
|
|
153
|
+
const bp2 = ip.slice(s, e).sort((i, j) => sgn * (from[i * 3 + bp] - from[j * 3 + bp])), bq = iq.slice(s, e).sort((i, j) => Q[i * 3 + b] - Q[j * 3 + b]);
|
|
154
|
+
for (let m = 0; m < bp2.length; m++) { out[bp2[m]] = bq[m]; ip[s + m] = bp2[m]; }
|
|
155
|
+
}
|
|
156
|
+
const d2 = (i, s) => (from[i * 3] - Q[s * 3]) ** 2 + (from[i * 3 + 1] - Q[s * 3 + 1]) ** 2 + (from[i * 3 + 2] - Q[s * 3 + 2]) ** 2;
|
|
157
|
+
for (let it = 0; it < N * 50; it++) {
|
|
158
|
+
const p = (Math.random() * N) | 0, q = Math.random() < 0.8 ? Math.min(N - 1, p + 1 + ((Math.random() * 40) | 0)) : (Math.random() * N) | 0;
|
|
159
|
+
const i = ip[p], j = ip[q]; if (i === j) continue;
|
|
160
|
+
const si = out[i], sj = out[j];
|
|
161
|
+
if (d2(i, sj) + d2(j, si) < d2(i, si) + d2(j, sj)) { out[i] = sj; out[j] = si; }
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
let si = 0, segT0 = 0;
|
|
166
|
+
function begin(seg, t0) {
|
|
167
|
+
from.set(cur); fromCol.set(curCol);
|
|
168
|
+
perm = seg.tr ? assign(seg.form, t0 + seg.tr) : perm;
|
|
169
|
+
let xm = 1; for (let i = 0; i < N; i++) xm = Math.max(xm, Math.abs(from[i * 3]));
|
|
170
|
+
for (let i = 0; i < N; i++) delay[i] = seg.kind === 'launch' ? (Math.abs(from[i * 3]) / xm) * 1.1 + Math.random() * 0.25 : Math.random() * 0.5;
|
|
171
|
+
}
|
|
172
|
+
for (let k = 0; k < N; k++) { pad.pos(k, 0, tmp); cur.set(tmp, k * 3); perm[k] = k; const b = 0.07; curCol[k * 3] = pad.col[k * 3] * b; curCol[k * 3 + 1] = pad.col[k * 3 + 1] * b; curCol[k * 3 + 2] = pad.col[k * 3 + 2] * b; }
|
|
173
|
+
const phase = Float32Array.from({ length: N * 3 }, () => Math.random() * 6.283);
|
|
174
|
+
|
|
175
|
+
function update(t) {
|
|
176
|
+
let seg = SHOW[si];
|
|
177
|
+
while (t - segT0 > seg.tr + seg.hold) { segT0 += seg.tr + seg.hold; si = (si + 1) % SHOW.length; seg = SHOW[si]; begin(seg, segT0); }
|
|
178
|
+
const maxD = seg.kind === 'launch' ? 1.35 : 0.5, span = Math.max(0.5, seg.tr - maxD), f = seg.form;
|
|
179
|
+
for (let i = 0; i < N; i++) {
|
|
180
|
+
const s = perm[i], bright = f.pos(s, t, tmp);
|
|
181
|
+
let e = seg.tr ? Math.min(1, Math.max(0, (t - segT0 - delay[i]) / span)) : 1;
|
|
182
|
+
e = e * e * e * (e * (e * 6 - 15) + 10); // smootherstep: drones accelerate and brake
|
|
183
|
+
const ax = from[i * 3], ay = from[i * 3 + 1], az = from[i * 3 + 2];
|
|
184
|
+
let x, y, z;
|
|
185
|
+
if (seg.kind === 'launch' || seg.kind === 'land') {
|
|
186
|
+
// a curved path: climb first then swing over (launch), or swing over the pad then descend (landing)
|
|
187
|
+
const up = seg.kind === 'launch', cx = up ? ax : tmp[0], cy = up ? tmp[1] : ay, cz = up ? az : tmp[2], u = 1 - e;
|
|
188
|
+
x = u * u * ax + 2 * u * e * cx + e * e * tmp[0]; y = u * u * ay + 2 * u * e * cy + e * e * tmp[1]; z = u * u * az + 2 * u * e * cz + e * e * tmp[2];
|
|
189
|
+
} else { x = ax + (tmp[0] - ax) * e; y = ay + (tmp[1] - ay) * e; z = az + (tmp[2] - az) * e; }
|
|
190
|
+
cur[i * 3] = x; cur[i * 3 + 1] = y; cur[i * 3 + 2] = z;
|
|
191
|
+
const dim = 1 - 0.3 * Math.sin(Math.PI * e);
|
|
192
|
+
for (let c = 0; c < 3; c++) curCol[i * 3 + c] = (fromCol[i * 3 + c] * (1 - e) + f.col[s * 3 + c] * bright * e) * dim;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── GL plumbing
|
|
197
|
+
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; };
|
|
198
|
+
const prog = (vs, fs) => {
|
|
199
|
+
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);
|
|
200
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
201
|
+
const u = {}; for (let i = 0, n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS); i < n; i++) { const nm = gl.getActiveUniform(p, i).name; u[nm] = gl.getUniformLocation(p, nm); }
|
|
202
|
+
return { p, u };
|
|
203
|
+
};
|
|
204
|
+
const QUAD = `#version 300 es
|
|
205
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
206
|
+
const HEAD = `#version 300 es
|
|
207
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
208
|
+
float h1(float n) { return fract(sin(n * 127.1) * 43758.5453); }
|
|
209
|
+
float h2(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
210
|
+
vec3 L(vec3 c) { return c * c; }
|
|
211
|
+
const float FOC = ${FOC.toFixed(1)}, HOR = ${HOR.toFixed(3)}, CAMH = ${CAMH.toFixed(1)};
|
|
212
|
+
`;
|
|
213
|
+
// the sky and the city, drawn per pixel: two rows of blocks with lit windows, towers with blinking beacons
|
|
214
|
+
const skyP = prog(QUAD, HEAD + `
|
|
215
|
+
uniform vec2 R; uniform float T, A, BASE;
|
|
216
|
+
vec3 blocks(float x, float w, float seed, float hmax) {
|
|
217
|
+
float i = floor(x / w), f = fract(x / w), r = h1(i + seed), r2 = h1(i * 1.7 + seed + 3.1);
|
|
218
|
+
float h = hmax * (.14 + .86 * pow(r, 2.3)), tower = step(.9, r2);
|
|
219
|
+
h = mix(h, hmax * (1.3 + .7 * r), tower);
|
|
220
|
+
h *= mix(1., .45, step(r2, .12));
|
|
221
|
+
h -= step(min(f, 1. - f), .14) * step(hmax * .55, h) * h * .1; // stepped crowns
|
|
222
|
+
h += tower * step(abs(f - .5), .03) * hmax * .4; // and a spire
|
|
223
|
+
return vec3(h, i + seed * 7.3, tower);
|
|
224
|
+
}
|
|
225
|
+
void main() {
|
|
226
|
+
vec2 c = uv * 2. - 1.; float hh = uv.y - BASE; vec3 col = vec3(0);
|
|
227
|
+
if (hh > 0.) {
|
|
228
|
+
vec3 sky = mix(vec3(.38, .2, .21), vec3(.08, .075, .16), smoothstep(0., .2, hh));
|
|
229
|
+
col = L(mix(sky, vec3(.02, .028, .065), smoothstep(.14, .72, hh)));
|
|
230
|
+
vec2 sp = vec2(c.x * A, c.y) * 110., si = floor(sp); float s = h2(si);
|
|
231
|
+
if (s > .99) { vec2 sf = fract(sp) - .5 - (vec2(h2(si + 1.7), h2(si + 4.2)) - .5) * .5; col += vec3(.7, .76, 1.) * exp(-dot(sf, sf) * 26.) * (s - .99) * 40. * smoothstep(.1, .45, hh) * (.7 + .3 * sin(T * 2. + s * 300.)); }
|
|
232
|
+
for (int k = 0; k < 2; k++) {
|
|
233
|
+
float Z = k == 0 ? 1100. : 760., W = k == 0 ? 36. : 27., HM = k == 0 ? 100. : 66., SEED = k == 0 ? 11.3 : 3.7;
|
|
234
|
+
float x = c.x * A * Z / FOC, y = (c.y - HOR) * Z / FOC + CAMH, mpp = Z / FOC * 2. / R.y;
|
|
235
|
+
vec3 b = blocks(x, W, SEED, HM);
|
|
236
|
+
if (y < b.x) {
|
|
237
|
+
col = k == 0 ? L(vec3(.085, .066, .11)) : L(vec3(.024, .026, .046));
|
|
238
|
+
vec2 g = vec2(x / 2.8, y / 3.3), gi = floor(g), gf = fract(g), aa = fwidth(g);
|
|
239
|
+
float win = smoothstep(.2 - aa.x, .2 + aa.x, gf.x) * smoothstep(.8 + aa.x, .8 - aa.x, gf.x) * smoothstep(.25 - aa.y, .25 + aa.y, gf.y) * smoothstep(.72 + aa.y, .72 - aa.y, gf.y);
|
|
240
|
+
win = mix(win, .3, clamp(max(aa.x, aa.y) * 2. - .6, 0., 1.));
|
|
241
|
+
float on = step(.6, h2(gi + b.y * 1.31)) * step(.45, h2(vec2(floor(x / 11.), floor(y / 8.)) + b.y));
|
|
242
|
+
vec3 wc = mix(vec3(1., .64, .34), vec3(.62, .8, 1.), step(.78, h2(gi * .37 + b.y)));
|
|
243
|
+
col += L(wc) * win * on * (k == 0 ? .3 : .85);
|
|
244
|
+
}
|
|
245
|
+
if (b.z > .5) { // aviation light on the spire
|
|
246
|
+
float xc = (floor(x / W) + .5) * W, top = blocks(xc, W, SEED, HM).x + 1.;
|
|
247
|
+
float d = length(vec2(x - xc, y - top)) / mpp;
|
|
248
|
+
col += vec3(1., .08, .04) * exp(-d * d * .35) * 3. * step(.55, fract(T * .6 + h1(b.y)));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
o = vec4(col, 1);
|
|
253
|
+
}`);
|
|
254
|
+
// the drones: a sharp core and a faint halo per light
|
|
255
|
+
const droneP = prog(`#version 300 es
|
|
256
|
+
layout(location = 0) in vec3 P; layout(location = 1) in vec3 C;
|
|
257
|
+
uniform float A, S; out vec3 col;
|
|
258
|
+
void main() { gl_Position = vec4(${FOC.toFixed(1)} * P.x / P.z / A, ${FOC.toFixed(1)} * (P.y - ${CAMH.toFixed(1)}) / P.z + ${HOR.toFixed(3)}, 0, 1); gl_PointSize = S * ${CZ.toFixed(1)} / P.z; col = C; }`,
|
|
259
|
+
`#version 300 es
|
|
260
|
+
precision mediump float; in vec3 col; out vec4 o;
|
|
261
|
+
void main() { vec2 q = gl_PointCoord * 2. - 1.; float r2 = dot(q, q); if (r2 > 1.) discard; o = vec4(col * (exp(-r2 * 11.) + .1 * exp(-r2 * 3.)), 1); }`);
|
|
262
|
+
// the bay: the scene mirrored at the waterline, rippled by three swells and stretched toward the viewer
|
|
263
|
+
const waterP = prog(QUAD, HEAD + `
|
|
264
|
+
uniform sampler2D S; uniform vec2 R; uniform float T, A, BASE;
|
|
265
|
+
void main() {
|
|
266
|
+
vec3 c = texture(S, uv).rgb; float d = BASE - uv.y;
|
|
267
|
+
if (d > 0.) {
|
|
268
|
+
float z = 1. / (d + .006);
|
|
269
|
+
float w = sin(z * .9 - T * 1.3 + sin(uv.x * A * 7. + z * .13) * 1.5) * .6 + sin(z * 2.3 + T * 1.9 + uv.x * A * 19.) * .3 + sin(z * 5.1 - T * 2.7 + uv.x * A * 41.) * .15;
|
|
270
|
+
vec2 m = vec2(uv.x + w * d * .05, 2. * BASE - uv.y);
|
|
271
|
+
vec3 r = vec3(0);
|
|
272
|
+
for (int k = 0; k < 8; k++) r += texture(S, m + vec2((float(k) - 3.5) * .0012 * w, float(k) * d * .018)).rgb;
|
|
273
|
+
float fres = mix(.7, .28, smoothstep(0., .22, d));
|
|
274
|
+
c += r / 8. * fres * vec3(.82, .9, 1.) + L(vec3(.012, .018, .036)) * (1. - fres);
|
|
275
|
+
}
|
|
276
|
+
o = vec4(c, 1);
|
|
277
|
+
}`);
|
|
278
|
+
const downP = prog(QUAD, `#version 300 es
|
|
279
|
+
precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S; uniform vec2 H;
|
|
280
|
+
void main() { o = (texture(S, uv) * 4. + texture(S, uv - H) + texture(S, uv + H) + texture(S, uv + vec2(H.x, -H.y)) + texture(S, uv - vec2(H.x, -H.y))) / 8.; }`);
|
|
281
|
+
const upP = prog(QUAD, `#version 300 es
|
|
282
|
+
precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S; uniform vec2 H;
|
|
283
|
+
void main() { o = (texture(S, uv + vec2(-2. * H.x, 0)) + texture(S, uv + vec2(2. * H.x, 0)) + texture(S, uv + vec2(0, 2. * H.y)) + texture(S, uv + vec2(0, -2. * H.y))
|
|
284
|
+
+ 2. * (texture(S, uv + H) + texture(S, uv - H) + texture(S, uv + vec2(H.x, -H.y)) + texture(S, uv - vec2(H.x, -H.y)))) / 12.; }`);
|
|
285
|
+
const finalP = prog(QUAD, `#version 300 es
|
|
286
|
+
precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S, B; uniform float K, T; uniform vec2 R;
|
|
287
|
+
float h2(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
288
|
+
void main() {
|
|
289
|
+
vec3 c = texture(S, uv).rgb + texture(B, uv).rgb * K;
|
|
290
|
+
c = (c * (2.51 * c + .03)) / (c * (2.43 * c + .59) + .14);
|
|
291
|
+
c = pow(clamp(c, 0., 1.), vec3(1. / 2.2));
|
|
292
|
+
c *= mix(1., smoothstep(1.25, .35, length((uv - .5) * vec2(1.1, 1.))), .3);
|
|
293
|
+
o = vec4(c + (h2(uv * R + fract(T) * 91.) - .5) / 128., 1);
|
|
294
|
+
}`);
|
|
295
|
+
|
|
296
|
+
const quad = gl.createVertexArray(); gl.bindVertexArray(quad);
|
|
297
|
+
const qb = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, qb); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
298
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
299
|
+
const dv = gl.createVertexArray(); gl.bindVertexArray(dv);
|
|
300
|
+
const data = new Float32Array(N * 6), db = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, db); gl.bufferData(gl.ARRAY_BUFFER, data.byteLength, gl.DYNAMIC_DRAW);
|
|
301
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
|
|
302
|
+
gl.bindVertexArray(null);
|
|
303
|
+
|
|
304
|
+
const fmt = hdr ? [gl.RGBA16F, gl.HALF_FLOAT] : [gl.RGBA8, gl.UNSIGNED_BYTE];
|
|
305
|
+
const rt = (w, h) => {
|
|
306
|
+
const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, fmt[0], w, h, 0, gl.RGBA, fmt[1], null);
|
|
307
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
308
|
+
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);
|
|
309
|
+
const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
310
|
+
return { tex, fb, w, h };
|
|
311
|
+
};
|
|
312
|
+
let W = 1, H = 1, scene = null, comp = null, mips = [];
|
|
313
|
+
const resize = () => {
|
|
314
|
+
const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, 2, Math.max(1, Math.sqrt(2.4e6 / (w * h))));
|
|
315
|
+
W = canvas.width = Math.max(1, (w * d) | 0); H = canvas.height = Math.max(1, (h * d) | 0);
|
|
316
|
+
for (const r of [scene, comp, ...mips]) if (r) { gl.deleteTexture(r.tex); gl.deleteFramebuffer(r.fb); }
|
|
317
|
+
scene = rt(W, H); comp = rt(W, H); mips = [];
|
|
318
|
+
let mw = W, mh = H; for (let i = 0; i < 6; i++) { mw = Math.max(1, mw >> 1); mh = Math.max(1, mh >> 1); mips.push(rt(mw, mh)); }
|
|
319
|
+
};
|
|
320
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
321
|
+
|
|
322
|
+
const pass = (P, target, tex) => {
|
|
323
|
+
gl.useProgram(P.p); gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.fb : null); gl.viewport(0, 0, target ? target.w : W, target ? target.h : H);
|
|
324
|
+
if (tex) { gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex); gl.uniform1i(P.u.S, 0); }
|
|
325
|
+
gl.bindVertexArray(quad);
|
|
326
|
+
};
|
|
327
|
+
let raf, last = performance.now(), t = 0;
|
|
328
|
+
function frame(now) {
|
|
329
|
+
t += Math.min(0.05, (now - last) / 1000) * PARAMS.speed; last = now;
|
|
330
|
+
update(t);
|
|
331
|
+
for (let i = 0; i < N; i++) {
|
|
332
|
+
const o = i * 6, w = 0.22;
|
|
333
|
+
data[o] = cur[i * 3] + Math.sin(t * 1.3 + phase[i * 3]) * w; data[o + 1] = cur[i * 3 + 1] + Math.sin(t * 1.7 + phase[i * 3 + 1]) * w; data[o + 2] = cur[i * 3 + 2] + Math.sin(t * 1.1 + phase[i * 3 + 2]) * w;
|
|
334
|
+
data[o + 3] = curCol[i * 3] * 3; data[o + 4] = curCol[i * 3 + 1] * 3; data[o + 5] = curCol[i * 3 + 2] * 3;
|
|
335
|
+
}
|
|
336
|
+
const A = W / H;
|
|
337
|
+
gl.disable(gl.BLEND);
|
|
338
|
+
pass(skyP, scene); gl.uniform2f(skyP.u.R, W, H); gl.uniform1f(skyP.u.T, t); gl.uniform1f(skyP.u.A, A); gl.uniform1f(skyP.u.BASE, BASE); gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
339
|
+
gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE);
|
|
340
|
+
gl.useProgram(droneP.p); gl.uniform1f(droneP.u.A, A); gl.uniform1f(droneP.u.S, Math.max(5, H / 105)); gl.bindVertexArray(dv);
|
|
341
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, db); gl.bufferSubData(gl.ARRAY_BUFFER, 0, data); gl.drawArrays(gl.POINTS, 0, N);
|
|
342
|
+
gl.disable(gl.BLEND);
|
|
343
|
+
pass(waterP, comp, scene.tex); gl.uniform2f(waterP.u.R, W, H); gl.uniform1f(waterP.u.T, t); gl.uniform1f(waterP.u.A, A); gl.uniform1f(waterP.u.BASE, BASE); gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
344
|
+
// bloom: halve down the chain, then add each level back up into the one above it
|
|
345
|
+
let src = comp;
|
|
346
|
+
for (const m of mips) { pass(downP, m, src.tex); gl.uniform2f(downP.u.H, 0.5 / src.w, 0.5 / src.h); gl.drawArrays(gl.TRIANGLES, 0, 3); src = m; }
|
|
347
|
+
gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE);
|
|
348
|
+
for (let i = mips.length - 1; i > 0; i--) { pass(upP, mips[i - 1], mips[i].tex); gl.uniform2f(upP.u.H, 0.5 / mips[i].w, 0.5 / mips[i].h); gl.drawArrays(gl.TRIANGLES, 0, 3); }
|
|
349
|
+
gl.disable(gl.BLEND);
|
|
350
|
+
pass(finalP, null, comp.tex); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, mips[0].tex); gl.uniform1i(finalP.u.B, 1);
|
|
351
|
+
gl.uniform1f(finalP.u.K, 0.22 * PARAMS.glow); gl.uniform1f(finalP.u.T, t); gl.uniform2f(finalP.u.R, W, H); gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
352
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
353
|
+
raf = requestAnimationFrame(frame);
|
|
354
|
+
}
|
|
355
|
+
raf = requestAnimationFrame(frame);
|
|
356
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
357
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Bonfire embers: glowing specks lifted by the fire's heat, swirled by turbulence and cooling as they rise. Buoyancy
|
|
2
|
+
// fades as they cool, so the hottest ones climb fastest and the dim ones drift and sink. The turbulence is a curl of
|
|
3
|
+
// layered sine fields, which never converges, so embers curl into eddies instead of clumping. The fire is a stack of
|
|
4
|
+
// flickering soft tongues drawn with additive light.
|
|
5
|
+
const PARAMS = { embers: 260, turbulence: 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
|
+
let W = 0, H = 0;
|
|
13
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1; canvas.width = W * d; canvas.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); };
|
|
14
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
15
|
+
// a stream function made of sines; its curl is a swirling, divergence-free velocity
|
|
16
|
+
const psi = (x, y, t) => Math.sin(x * 3.1 + t * 0.7) * Math.cos(y * 2.3 - t * 0.5) + 0.5 * Math.sin(x * 6.7 - y * 5.3 + t * 1.3) + 0.25 * Math.cos(x * 11 + y * 13 - t * 2);
|
|
17
|
+
const curl = (x, y, t) => { const e = 0.01; return [(psi(x, y + e, t) - psi(x, y - e, t)) / (2 * e), -(psi(x + e, y, t) - psi(x - e, y, t)) / (2 * e)]; };
|
|
18
|
+
const make = () => ({ x: 0.5 + (Math.random() - 0.5) * 0.14, y: 0.86, vx: 0, vy: 0, k: 0.7 + Math.random() * 0.3, cool: 0.12 + Math.random() * 0.2, s: 0.6 + Math.random() * 1.4, ph: Math.random() * 6.28 });
|
|
19
|
+
const embers = Array.from({ length: Math.round(PARAMS.embers) }, () => { const e = make(); e.y = Math.random() * 0.86; e.k = Math.random(); return e; });
|
|
20
|
+
let raf, last = performance.now();
|
|
21
|
+
function frame(now) {
|
|
22
|
+
const dt = Math.min(0.033, (now - last) / 1000); last = now;
|
|
23
|
+
const t = now / 1000, S = Math.min(W, H), fx = W / 2, fy = H * 0.88;
|
|
24
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
25
|
+
const bg = ctx.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, '#07070c'); bg.addColorStop(1, '#1a0f0b');
|
|
26
|
+
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
|
|
27
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
28
|
+
// the glow on everything around the fire, breathing with the flames
|
|
29
|
+
const fl = 0.85 + Math.sin(t * 7) * 0.05 + Math.sin(t * 13.3) * 0.04;
|
|
30
|
+
const glow = ctx.createRadialGradient(fx, fy, 0, fx, fy, S * 0.7 * fl); glow.addColorStop(0, 'rgba(255,120,40,.35)'); glow.addColorStop(1, 'rgba(255,60,10,0)');
|
|
31
|
+
ctx.fillStyle = glow; ctx.fillRect(0, 0, W, H);
|
|
32
|
+
// flame tongues
|
|
33
|
+
for (let i = 0; i < 26; i++) {
|
|
34
|
+
const u = (i / 25 - 0.5), hgt = S * (0.16 + 0.12 * (1 - Math.abs(u) * 2)) * (0.7 + 0.3 * Math.sin(t * (5 + i % 5) + i * 1.7));
|
|
35
|
+
const x = fx + u * S * 0.22 + Math.sin(t * 3 + i) * S * 0.01, y = fy - hgt * 0.5;
|
|
36
|
+
const g = ctx.createRadialGradient(x, y + hgt * 0.25, 0, x, y, hgt * 0.6);
|
|
37
|
+
g.addColorStop(0, 'rgba(255,230,160,.30)'); g.addColorStop(0.4, 'rgba(255,130,40,.16)'); g.addColorStop(1, 'rgba(200,40,10,0)');
|
|
38
|
+
ctx.fillStyle = g; ctx.beginPath(); ctx.ellipse(x, y, hgt * 0.22, hgt * 0.6, Math.sin(t * 2 + i) * 0.15, 0, 7); ctx.fill();
|
|
39
|
+
}
|
|
40
|
+
for (const e of embers) {
|
|
41
|
+
const [cx, cy] = curl(e.x * 2, e.y * 2, t * 0.6);
|
|
42
|
+
const lift = 0.32 * e.k * e.k; // hot air lifts hot embers; cooled ones barely rise
|
|
43
|
+
e.vx += (cx * 0.12 * PARAMS.turbulence - e.vx) * dt * 2;
|
|
44
|
+
e.vy += ((cy * 0.12 * PARAMS.turbulence - lift + 0.03) - e.vy) * dt * 2;
|
|
45
|
+
e.x += e.vx * dt * (H / W); e.y += e.vy * dt;
|
|
46
|
+
e.k -= e.cool * dt;
|
|
47
|
+
if (e.k <= 0 || e.y < -0.05 || e.y > 0.95 || e.x < -0.05 || e.x > 1.05) Object.assign(e, make());
|
|
48
|
+
const x = e.x * W, y = e.y * H, k = Math.max(0, e.k) * (0.8 + 0.2 * Math.sin(t * 20 + e.ph)); // they twinkle
|
|
49
|
+
const r = e.s * (1 + k), g = ctx.createRadialGradient(x, y, 0, x, y, r * 4);
|
|
50
|
+
g.addColorStop(0, `rgba(255,${180 + k * 70 | 0},${80 + k * 120 | 0},${k})`); g.addColorStop(0.3, `rgba(255,${90 + k * 60 | 0},20,${k * 0.5})`); g.addColorStop(1, 'rgba(255,60,0,0)');
|
|
51
|
+
ctx.fillStyle = g; ctx.fillRect(x - r * 4, y - r * 4, r * 8, r * 8);
|
|
52
|
+
}
|
|
53
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
54
|
+
// logs in silhouette
|
|
55
|
+
ctx.fillStyle = '#0c0807';
|
|
56
|
+
ctx.save(); ctx.translate(fx, fy + S * 0.02);
|
|
57
|
+
for (const [ang, len] of [[0.25, 0.34], [-0.3, 0.3], [0.05, 0.28]]) { ctx.save(); ctx.rotate(ang); ctx.beginPath(); ctx.roundRect(-S * len / 2, -S * 0.022, S * len, S * 0.044, S * 0.02); ctx.fill(); ctx.restore(); }
|
|
58
|
+
ctx.restore();
|
|
59
|
+
raf = requestAnimationFrame(frame);
|
|
60
|
+
}
|
|
61
|
+
raf = requestAnimationFrame(frame);
|
|
62
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
63
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Firefly sync: a forest of fireflies flashing at random slowly falls into one rhythm, with nobody in charge. Each
|
|
2
|
+
// one is a clock that flashes when it runs out and resets. Whenever it sees a neighbour flash, it nudges its own
|
|
3
|
+
// clock forward a little, and if that tips it over, it flashes right away (the Mirollo–Strogatz model of
|
|
4
|
+
// pulse-coupled oscillators). Small groups lock together, waves of flashes start to roll through the trees, and in
|
|
5
|
+
// the end the whole forest blinks as one. Fireflies sit between three layers of trees, near ones blurred to bokeh.
|
|
6
|
+
// Wave the pointer to startle the ones nearby and break the spell.
|
|
7
|
+
const PARAMS = { fireflies: 700, coupling: 0.07, spread: 0.05 };
|
|
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;background:#04070a';
|
|
12
|
+
stage.append(canvas);
|
|
13
|
+
const ctx = canvas.getContext('2d');
|
|
14
|
+
let W = 1, H = 1, layers = [];
|
|
15
|
+
const rnd = (a, b) => a + Math.random() * (b - a);
|
|
16
|
+
// three layers of trees, painted once: far ones misty blue, near ones black
|
|
17
|
+
function paintForest() {
|
|
18
|
+
const d = Math.min(devicePixelRatio, 2);
|
|
19
|
+
layers = [0, 1, 2].map(L => {
|
|
20
|
+
const c = document.createElement('canvas'); c.width = Math.max(1, (W * d) | 0); c.height = Math.max(1, (H * d) | 0);
|
|
21
|
+
const g = c.getContext('2d'); g.setTransform(d, 0, 0, d, 0, 0);
|
|
22
|
+
const tone = ['#14262f', '#0a161c', '#020405'][L], n = [16, 9, 4][L];
|
|
23
|
+
g.fillStyle = tone;
|
|
24
|
+
for (let i = 0; i < n; i++) {
|
|
25
|
+
const x = (i + rnd(0.1, 0.9)) * (W / n), w = [W * 0.01, W * 0.022, W * 0.05][L] * rnd(0.7, 1.3), top = -H * 0.1;
|
|
26
|
+
g.beginPath(); g.moveTo(x - w * 0.5, H); g.lineTo(x - w * 0.36, top); g.lineTo(x + w * 0.36, top); g.lineTo(x + w * 0.5, H); g.fill();
|
|
27
|
+
for (let b = 0; b < 3; b++) { // a few branches
|
|
28
|
+
const y = H * rnd(0.15, 0.6), s = Math.random() < 0.5 ? -1 : 1, len = w * rnd(2, 5);
|
|
29
|
+
g.beginPath(); g.moveTo(x, y); g.quadraticCurveTo(x + s * len * 0.6, y - len * 0.3, x + s * len, y - len * 0.5); g.lineWidth = w * 0.18; g.strokeStyle = tone; g.stroke();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (L === 2) { g.fillStyle = '#020304'; for (let x = 0; x < W; x += 3) { const h = H * rnd(0.02, 0.07); g.fillRect(x, H - h, 1.6, h); } } // grass
|
|
33
|
+
return c;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1; canvas.width = Math.max(1, (W * d) | 0); canvas.height = Math.max(1, (H * d) | 0); ctx.setTransform(d, 0, 0, d, 0, 0); paintForest(); };
|
|
37
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
38
|
+
|
|
39
|
+
// a soft yellow-green glow, stamped for every flash
|
|
40
|
+
const glow = document.createElement('canvas'); glow.width = glow.height = 64;
|
|
41
|
+
{ const g = glow.getContext('2d'), r = g.createRadialGradient(32, 32, 0, 32, 32, 32); r.addColorStop(0, 'rgba(250,255,200,1)'); r.addColorStop(0.18, 'rgba(200,255,90,.75)'); r.addColorStop(0.5, 'rgba(120,220,60,.18)'); r.addColorStop(1, 'rgba(80,180,40,0)'); g.fillStyle = r; g.fillRect(0, 0, 64, 64); }
|
|
42
|
+
const disc = document.createElement('canvas'); disc.width = disc.height = 64;
|
|
43
|
+
{ const g = disc.getContext('2d'), r = g.createRadialGradient(32, 32, 20, 32, 32, 32); r.addColorStop(0, 'rgba(200,255,110,.5)'); r.addColorStop(0.85, 'rgba(210,255,120,.62)'); r.addColorStop(1, 'rgba(190,255,100,0)'); g.fillStyle = r; g.beginPath(); g.arc(32, 32, 32, 0, 7); g.fill(); }
|
|
44
|
+
|
|
45
|
+
const N = Math.max(50, Math.round(PARAMS.fireflies));
|
|
46
|
+
// each firefly: position (x, y in 0..1 of the stage, depth 0 far .. 1 near), its clock, its own natural period
|
|
47
|
+
const F = Array.from({ length: N }, () => { const z = Math.random() ** 2.2; return { x: Math.random(), y: rnd(0.25, 0.92), z, layer: z < 0.4 ? 0 : z < 0.8 ? 1 : 2, c: Math.random(), T: 1 + rnd(-1, 1) * PARAMS.spread, since: 9, vx: rnd(-1, 1), vy: rnd(-1, 1) }; });
|
|
48
|
+
const ptr = { x: 0, y: 0, on: false };
|
|
49
|
+
const move = e => { const r = canvas.getBoundingClientRect(); ptr.x = (e.clientX - r.left) / r.width; ptr.y = (e.clientY - r.top) / r.height; ptr.on = true; };
|
|
50
|
+
const leave = () => { ptr.on = false; };
|
|
51
|
+
canvas.addEventListener('pointermove', move); canvas.addEventListener('pointerleave', leave);
|
|
52
|
+
|
|
53
|
+
let raf, last = performance.now(), time = 0, syncedFor = 0;
|
|
54
|
+
const SEE = 0.2; // how far a firefly can see a flash (stage widths)
|
|
55
|
+
function frame(now) {
|
|
56
|
+
const dt = Math.min(0.04, (now - last) / 1000); last = now; time += dt;
|
|
57
|
+
// advance every clock; collect who fires, then let each flash nudge the neighbours who see it (which can cascade)
|
|
58
|
+
const queue = [];
|
|
59
|
+
for (const f of F) {
|
|
60
|
+
f.c += dt / f.T; f.since += dt;
|
|
61
|
+
f.x += f.vx * dt * 0.004; f.y += f.vy * dt * 0.004; f.vx += rnd(-1, 1) * dt * 2; f.vy += rnd(-1, 1) * dt * 2; f.vx *= 0.99; f.vy *= 0.99;
|
|
62
|
+
if (f.x < 0 || f.x > 1) f.vx = -f.vx; if (f.y < 0.2 || f.y > 0.95) f.vy = -f.vy;
|
|
63
|
+
if (ptr.on && Math.hypot(f.x - ptr.x, (f.y - ptr.y) * H / W) < 0.08) f.c = Math.random(); // startled
|
|
64
|
+
if (f.c >= 1) { f.c = 0; f.since = 0; queue.push(f); }
|
|
65
|
+
}
|
|
66
|
+
for (let q = 0; q < queue.length; q++) {
|
|
67
|
+
const s = queue[q];
|
|
68
|
+
for (const f of F) {
|
|
69
|
+
if (f === s || f.since < 0.12 || f.c < 0.18) continue; // a refractory moment just after flashing
|
|
70
|
+
const d = Math.hypot(f.x - s.x, (f.y - s.y) * H / W, (f.z - s.z) * 0.6);
|
|
71
|
+
if (d > SEE) continue;
|
|
72
|
+
f.c += PARAMS.coupling * (1 - d / SEE);
|
|
73
|
+
if (f.c >= 1) { f.c = 0; f.since = 0; queue.push(f); }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// once the forest has blinked as one for a while, a gust scrambles a patch of it and the story starts again
|
|
77
|
+
const lit = F.reduce((a, f) => a + (f.since < 0.15 ? 1 : 0), 0) / N;
|
|
78
|
+
syncedFor = lit > 0.6 ? syncedFor + 1 : syncedFor;
|
|
79
|
+
if (syncedFor > 110) { syncedFor = 0; const cx = Math.random(); for (const f of F) if (Math.abs(f.x - cx) < 0.35) f.c = Math.random(); }
|
|
80
|
+
|
|
81
|
+
// draw: night sky glow, then each tree layer with its fireflies behind the next layer
|
|
82
|
+
const sky = ctx.createLinearGradient(0, 0, 0, H); sky.addColorStop(0, '#1b2f3b'); sky.addColorStop(0.55, '#14252e'); sky.addColorStop(1, '#1a2c28'); // moonlit haze between the trunks
|
|
83
|
+
ctx.globalCompositeOperation = 'source-over'; ctx.fillStyle = sky; ctx.fillRect(0, 0, W, H);
|
|
84
|
+
const mist = ctx.createLinearGradient(0, H * 0.55, 0, H); mist.addColorStop(0, 'rgba(60,90,90,0)'); mist.addColorStop(1, 'rgba(60,90,90,.18)');
|
|
85
|
+
for (let L = 0; L < 3; L++) {
|
|
86
|
+
ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1;
|
|
87
|
+
ctx.drawImage(layers[L], 0, 0, W, H);
|
|
88
|
+
if (L === 0) { ctx.fillStyle = mist; ctx.fillRect(0, 0, W, H); }
|
|
89
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
90
|
+
for (const f of F) {
|
|
91
|
+
if (f.layer !== L) continue;
|
|
92
|
+
const flash = f.since < 0.05 ? f.since / 0.05 : Math.exp(-(f.since - 0.05) / 0.22), ember = 0;
|
|
93
|
+
const b = Math.max(ember, flash); if (b < 0.02) continue;
|
|
94
|
+
const sx = f.x * W, sy = f.y * H, size = (1.3 + f.z * f.z * 6) * Math.min(W, H) / 560;
|
|
95
|
+
const blur = Math.max(0, f.z - 0.72) * 70 * Math.min(W, H) / 560; // depth of field: the nearest go soft
|
|
96
|
+
if (blur > 6 && flash > 0.05) { const r = size + blur; ctx.globalAlpha = Math.min(1, b * 0.9 * (size * 4) / r); ctx.drawImage(disc, sx - r, sy - r, r * 2, r * 2); }
|
|
97
|
+
else { const r = size * 4.5; ctx.globalAlpha = b; ctx.drawImage(glow, sx - r, sy - r, r * 2, r * 2); }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// when many flash at once, the air itself lights up a little
|
|
101
|
+
ctx.globalAlpha = Math.min(0.2, lit * 0.35); ctx.fillStyle = '#4f7a2c'; ctx.fillRect(0, 0, W, H);
|
|
102
|
+
ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over';
|
|
103
|
+
raf = requestAnimationFrame(frame);
|
|
104
|
+
}
|
|
105
|
+
raf = requestAnimationFrame(frame);
|
|
106
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', move); canvas.removeEventListener('pointerleave', leave); canvas.remove(); };
|
|
107
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Spiral galaxy: stars placed on logarithmic spiral arms, scattered into density waves, orbiting with a flat
|
|
2
|
+
// rotation curve (inner stars lap the outer ones). Additive points let the core bloom by itself.
|
|
3
|
+
const PARAMS = { stars: 40000, arms: 3, twist: 2 };
|
|
4
|
+
|
|
5
|
+
export default function (stage) {
|
|
6
|
+
const canvas = document.createElement('canvas');
|
|
7
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
8
|
+
stage.append(canvas);
|
|
9
|
+
const gl = canvas.getContext('webgl2', { antialias: false, premultipliedAlpha: false });
|
|
10
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
11
|
+
|
|
12
|
+
const N = Math.round(PARAMS.stars), arms = Math.round(PARAMS.arms);
|
|
13
|
+
const data = new Float32Array(N * 8); // radius, angle, height, size, r, g, b, speed
|
|
14
|
+
const gauss = () => { let u = 0, v = 0; while (!u) u = Math.random(); while (!v) v = Math.random(); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); };
|
|
15
|
+
for (let i = 0; i < N; i++) {
|
|
16
|
+
const core = i < N * 0.18;
|
|
17
|
+
const r = core ? Math.abs(gauss()) * 0.12 : 0.06 + Math.pow(Math.random(), 1.6) * 0.94;
|
|
18
|
+
const arm = Math.floor(Math.random() * arms);
|
|
19
|
+
const spread = (0.35 + r * 0.9) * 0.32;
|
|
20
|
+
const a = arm * (Math.PI * 2 / arms) + Math.log(r + 0.02) * PARAMS.twist + gauss() * spread * (core ? 6 : 1);
|
|
21
|
+
const z = gauss() * (core ? 0.05 : 0.018) * (1 - r * 0.6);
|
|
22
|
+
const hot = !core && Math.random() < 0.03; // pink star-forming knots along the arms
|
|
23
|
+
let col;
|
|
24
|
+
if (core) col = [1, 0.86 + Math.random() * 0.1, 0.62 + Math.random() * 0.15];
|
|
25
|
+
else if (hot) col = [1, 0.45, 0.7];
|
|
26
|
+
else { const t = Math.random(); col = [0.62 + 0.35 * t, 0.75 + 0.2 * t, 1]; }
|
|
27
|
+
const o = i * 8;
|
|
28
|
+
data[o] = r; data[o + 1] = a; data[o + 2] = z; data[o + 3] = (core ? 1.6 : 1) * (0.6 + Math.random() * 1.6) * (hot ? 2.4 : 1);
|
|
29
|
+
data[o + 4] = col[0]; data[o + 5] = col[1]; data[o + 6] = col[2];
|
|
30
|
+
data[o + 7] = 0.22 / (r + 0.08); // flat rotation curve: angular speed ∝ 1/r
|
|
31
|
+
}
|
|
32
|
+
const vs = `#version 300 es
|
|
33
|
+
in vec4 a; in vec4 b; uniform float T, D; uniform vec2 R; out vec3 c; out float fade;
|
|
34
|
+
void main() {
|
|
35
|
+
float ang = a.y + T * b.w * .25;
|
|
36
|
+
vec3 p = vec3(cos(ang) * a.x, a.z, sin(ang) * a.x);
|
|
37
|
+
float tilt = .95, ct = cos(tilt), st = sin(tilt);
|
|
38
|
+
p = vec3(p.x, p.y * ct - p.z * st, p.y * st + p.z * ct);
|
|
39
|
+
float yaw = T * .03, cy = cos(yaw), sy = sin(yaw);
|
|
40
|
+
p = vec3(p.x * cy - p.z * sy, p.y, p.x * sy + p.z * cy);
|
|
41
|
+
float z = p.z + 2.2;
|
|
42
|
+
gl_Position = vec4(p.x / z * 2.6 * R.y / R.x, p.y / z * 2.6, 0, 1);
|
|
43
|
+
gl_PointSize = a.w * D * 2.2 / z;
|
|
44
|
+
c = b.xyz; fade = clamp(1.6 - z * .45, .25, 1.);
|
|
45
|
+
}`;
|
|
46
|
+
const fs = `#version 300 es
|
|
47
|
+
precision mediump float; in vec3 c; in float fade; out vec4 o;
|
|
48
|
+
void main() { vec2 q = gl_PointCoord - .5; float d = dot(q, q); float k = exp(-d * 18.); o = vec4(c * k * .55 * fade, 1); }`;
|
|
49
|
+
const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x); if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(x)); return x; };
|
|
50
|
+
const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, fs));
|
|
51
|
+
gl.bindAttribLocation(prog, 0, 'a'); gl.bindAttribLocation(prog, 1, 'b'); gl.linkProgram(prog); gl.useProgram(prog);
|
|
52
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
|
|
53
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 32, 0);
|
|
54
|
+
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 32, 16);
|
|
55
|
+
gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE);
|
|
56
|
+
const uT = gl.getUniformLocation(prog, 'T'), uR = gl.getUniformLocation(prog, 'R'), uD = gl.getUniformLocation(prog, 'D');
|
|
57
|
+
let dpr = 1;
|
|
58
|
+
const resize = () => { dpr = Math.min(devicePixelRatio, 2); canvas.width = stage.clientWidth * dpr | 0; canvas.height = stage.clientHeight * dpr | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
59
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
60
|
+
let raf;
|
|
61
|
+
function frame(now) {
|
|
62
|
+
gl.clearColor(0.012, 0.016, 0.035, 1); gl.clear(gl.COLOR_BUFFER_BIT);
|
|
63
|
+
gl.uniform1f(uT, now / 1000); gl.uniform2f(uR, canvas.width, canvas.height); gl.uniform1f(uD, dpr * Math.min(canvas.width, canvas.height) / 500);
|
|
64
|
+
gl.drawArrays(gl.POINTS, 0, N);
|
|
65
|
+
raf = requestAnimationFrame(frame);
|
|
66
|
+
}
|
|
67
|
+
raf = requestAnimationFrame(frame);
|
|
68
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
69
|
+
}
|