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,329 @@
|
|
|
1
|
+
// Jelly cube: a 4x4x4 lattice of masses held together by springs (edges, face and body diagonals, plus
|
|
2
|
+
// skip-one springs that resist bending) and a gentle pull back toward its own rigid shape. The lattice bends a
|
|
3
|
+
// smooth rounded cube through a Bezier volume. It is shaded as real jelly: light is absorbed by how much jelly
|
|
4
|
+
// it passes through (thicker means deeper colour), the floor behind bends through it, and its shadow glows.
|
|
5
|
+
// Grab and throw it, or tap to make it hop. Left alone, it hops on its own.
|
|
6
|
+
const PARAMS = { softness: 1, hop: 1, hue: 350 };
|
|
7
|
+
|
|
8
|
+
export default function (stage) {
|
|
9
|
+
const canvas = document.createElement('canvas');
|
|
10
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:grab';
|
|
11
|
+
stage.append(canvas);
|
|
12
|
+
const gl = canvas.getContext('webgl2', { antialias: true });
|
|
13
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
14
|
+
|
|
15
|
+
// ── the lattice: 64 nodes, rest shape a unit cube standing on the floor
|
|
16
|
+
const N = 64, X = new Float32Array(N * 3), Vl = new Float32Array(N * 3), Fo = new Float32Array(N * 3), Qr = new Float32Array(N * 3);
|
|
17
|
+
const id = (i, j, k) => i + 4 * j + 16 * k, surf = [];
|
|
18
|
+
for (let k = 0; k < 4; k++) for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) {
|
|
19
|
+
const n = id(i, j, k); Qr[n * 3] = i / 3 - 0.5; Qr[n * 3 + 1] = j / 3 - 0.5; Qr[n * 3 + 2] = k / 3 - 0.5;
|
|
20
|
+
if (i % 3 === 0 || j % 3 === 0 || k % 3 === 0) surf.push(n);
|
|
21
|
+
}
|
|
22
|
+
const sA = [], sB = [], sL = [];
|
|
23
|
+
for (let a = 0; a < N; a++) for (let b = a + 1; b < N; b++) {
|
|
24
|
+
const d = [0, 1, 2].map(c => Math.round((Qr[b * 3 + c] - Qr[a * 3 + c]) * 3)), m = Math.max(...d.map(Math.abs)), z = d.filter(v => v === 0).length;
|
|
25
|
+
if (m <= 1 || (m === 2 && z === 2)) { sA.push(a); sB.push(b); sL.push(Math.hypot(...d) / 3); } // neighbours + diagonals, and skip-one bend springs
|
|
26
|
+
}
|
|
27
|
+
const S = sA.length;
|
|
28
|
+
// quaternion helpers for the rigid part
|
|
29
|
+
const qmul = (a, b) => [a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1], a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0], a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3], a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]];
|
|
30
|
+
const qmat = q => { const [x, y, z, w] = q; return [1 - 2 * (y * y + z * z), 2 * (x * y + z * w), 2 * (x * z - y * w), 2 * (x * y - z * w), 1 - 2 * (x * x + z * z), 2 * (y * z + x * w), 2 * (x * z + y * w), 2 * (y * z - x * w), 1 - 2 * (x * x + y * y)]; };
|
|
31
|
+
const qaxis = (ax, an) => { const l = Math.hypot(...ax) || 1, s = Math.sin(an / 2); return [ax[0] / l * s, ax[1] / l * s, ax[2] / l * s, Math.cos(an / 2)]; };
|
|
32
|
+
let rq = [0, 0, 0, 1];
|
|
33
|
+
const place = (q, c) => { const R = qmat(q); for (let n = 0; n < N; n++) { const x = Qr[n * 3], y = Qr[n * 3 + 1], z = Qr[n * 3 + 2]; for (let r = 0; r < 3; r++) { X[n * 3 + r] = R[r] * x + R[3 + r] * y + R[6 + r] * z + c[r]; Vl[n * 3 + r] = 0; } } };
|
|
34
|
+
place(qaxis([1, 0, 0.6], 0.55), [0.15, 1.35, 0]); rq = qaxis([1, 0, 0.6], 0.55);
|
|
35
|
+
|
|
36
|
+
// ── the visible surface: a rounded box, each vertex fixed inside the lattice (tricubic Bezier weights)
|
|
37
|
+
const G = 16, RB = 0.24, verts = [], keys = new Map(), tris = [];
|
|
38
|
+
const vkey = p => p.map(v => Math.round(v * 1e4)).join(',');
|
|
39
|
+
const addV = p => { const k = vkey(p); if (keys.has(k)) return keys.get(k); keys.set(k, verts.length / 3); verts.push(...p); return verts.length / 3 - 1; };
|
|
40
|
+
const round = p => { const c = p.map(v => Math.max(-(1 - RB), Math.min(1 - RB, v))), d = p.map((v, i) => v - c[i]), l = Math.hypot(...d) || 1; return c.map((v, i) => (v + d[i] / l * RB) * 0.5 * 0.985 + 0.5); };
|
|
41
|
+
for (let ax = 0; ax < 3; ax++) for (const sgn of [-1, 1]) {
|
|
42
|
+
const grid = [];
|
|
43
|
+
for (let b = 0; b <= G; b++) for (let a = 0; a <= G; a++) { const p = [0, 0, 0]; p[ax] = sgn; p[(ax + 1) % 3] = a / G * 2 - 1; p[(ax + 2) % 3] = b / G * 2 - 1; grid.push(addV(round(p))); }
|
|
44
|
+
for (let b = 0; b < G; b++) for (let a = 0; a < G; a++) { const i0 = grid[b * (G + 1) + a], i1 = grid[b * (G + 1) + a + 1], i2 = grid[(b + 1) * (G + 1) + a], i3 = grid[(b + 1) * (G + 1) + a + 1]; tris.push([i0, i1, i3], [i0, i3, i2]); }
|
|
45
|
+
}
|
|
46
|
+
const NV = verts.length / 3, idx = new Uint16Array(tris.length * 3);
|
|
47
|
+
tris.forEach((t, i) => { // wind every triangle so its normal points out of the box
|
|
48
|
+
const P = t.map(v => [verts[v * 3] - 0.5, verts[v * 3 + 1] - 0.5, verts[v * 3 + 2] - 0.5]);
|
|
49
|
+
const e1 = P[1].map((v, c) => v - P[0][c]), e2 = P[2].map((v, c) => v - P[0][c]), cr = [e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]];
|
|
50
|
+
const out = cr[0] * (P[0][0] + P[1][0] + P[2][0]) + cr[1] * (P[0][1] + P[1][1] + P[2][1]) + cr[2] * (P[0][2] + P[1][2] + P[2][2]) > 0;
|
|
51
|
+
idx.set(out ? t : [t[0], t[2], t[1]], i * 3);
|
|
52
|
+
});
|
|
53
|
+
const bern = u => [(1 - u) ** 3, 3 * u * (1 - u) ** 2, 3 * u * u * (1 - u), u ** 3];
|
|
54
|
+
const W = new Float32Array(NV * 64);
|
|
55
|
+
for (let v = 0; v < NV; v++) { const bu = bern(verts[v * 3]), bv = bern(verts[v * 3 + 1]), bw = bern(verts[v * 3 + 2]); for (let k = 0; k < 4; k++) for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) W[v * 64 + id(i, j, k)] = bu[i] * bv[j] * bw[k]; }
|
|
56
|
+
const POS = new Float32Array(NV * 3), NOR = new Float32Array(NV * 3);
|
|
57
|
+
const BUB = [[.34, .64, .4, .052], [.66, .34, .58, .036], [.5, .78, .72, .028], [.72, .62, .26, .042]];
|
|
58
|
+
const BW = BUB.map(([u, v, w]) => { const bu = bern(u), bv = bern(v), bw = bern(w), o = new Float32Array(64); for (let k = 0; k < 4; k++) for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) o[id(i, j, k)] = bu[i] * bv[j] * bw[k]; return o; });
|
|
59
|
+
const BF = new Float32Array(16);
|
|
60
|
+
|
|
61
|
+
// ── GL programs
|
|
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 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, 'pos'); gl.bindAttribLocation(p, 1, 'nor'); gl.linkProgram(p); if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p)); return p; };
|
|
64
|
+
const quadVS = `#version 300 es
|
|
65
|
+
in vec2 pos; out vec2 uv; void main() { uv = pos * .5 + .5; gl_Position = vec4(pos, 0, 1); }`;
|
|
66
|
+
const meshVS = `#version 300 es
|
|
67
|
+
in vec3 pos, nor; uniform mat4 PV; out vec3 vW, vN; void main() { vW = pos; vN = nor; gl_Position = PV * vec4(pos, 1); }`;
|
|
68
|
+
const ENV = `
|
|
69
|
+
uniform vec3 L;
|
|
70
|
+
vec3 env(vec3 d) {
|
|
71
|
+
vec3 c = mix(vec3(.30, .27, .27), vec3(.78, .75, .73), smoothstep(-.25, .5, d.y));
|
|
72
|
+
if (d.y > 0.) {
|
|
73
|
+
vec2 q = d.xz / d.y - vec2(-.95, .75); // big softbox, up and to the left
|
|
74
|
+
vec2 b = abs(q) - vec2(.7, .45); float r = length(max(b, 0.)) + min(max(b.x, b.y), 0.) - .12;
|
|
75
|
+
c += vec3(3.2, 3.1, 3.) * smoothstep(.06, -.04, r);
|
|
76
|
+
}
|
|
77
|
+
float az = atan(d.x, d.z);
|
|
78
|
+
c += vec3(1.5, 1.55, 1.7) * smoothstep(.07, .02, abs(az - 1.35)) * smoothstep(-.1, .15, d.y) * smoothstep(.8, .45, d.y); // tall strip on the right
|
|
79
|
+
return c;
|
|
80
|
+
}`;
|
|
81
|
+
const bgProg = link(quadVS, `#version 300 es
|
|
82
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
83
|
+
uniform vec3 E, CF, CR, CU, TINT, SIG; uniform vec2 R; uniform float TH, SH_B, SH_K, HIT; uniform vec2 H[32]; uniform int HN; uniform vec3 CEN;
|
|
84
|
+
float sdPoly(vec2 p) {
|
|
85
|
+
float d = 1e9, s = 1.;
|
|
86
|
+
for (int i = 0; i < 32; i++) {
|
|
87
|
+
if (i >= HN) break;
|
|
88
|
+
int j = i == 0 ? HN - 1 : i - 1;
|
|
89
|
+
vec2 e = H[j] - H[i], w = p - H[i], b = w - e * clamp(dot(w, e) / dot(e, e), 0., 1.);
|
|
90
|
+
d = min(d, dot(b, b));
|
|
91
|
+
bvec3 c = bvec3(p.y >= H[i].y, p.y < H[j].y, e.x * w.y > e.y * w.x);
|
|
92
|
+
if (all(c) || all(not(c))) s = -s;
|
|
93
|
+
}
|
|
94
|
+
return s * sqrt(d);
|
|
95
|
+
}
|
|
96
|
+
void main() {
|
|
97
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.) * TH;
|
|
98
|
+
vec3 rd = normalize(CF + CR * q.x + CU * q.y);
|
|
99
|
+
vec3 back = mix(vec3(.80, .77, .74), vec3(.58, .55, .56), smoothstep(-.1, .45, rd.y));
|
|
100
|
+
vec3 col = back;
|
|
101
|
+
if (rd.y < 0.) {
|
|
102
|
+
float t = -E.y / rd.y; vec3 p = E + rd * t;
|
|
103
|
+
vec3 fl = vec3(.86, .83, .79) * (.78 + .3 * exp(-dot(p.xz, p.xz) * .07)); // a pool of light on a pale floor
|
|
104
|
+
float sd = sdPoly(p.xz);
|
|
105
|
+
float inside = (1. - smoothstep(-SH_B, SH_B, sd)) * SH_K; // shadow cast along the light
|
|
106
|
+
float edge = exp(-pow(sd / (SH_B + .05), 2.)) * SH_K; // light bent away at the rim leaves a darker edge
|
|
107
|
+
float ao = exp(-dot(p.xz - CEN.xz, p.xz - CEN.xz) * 3.2) * .5 * HIT; // contact darkening right under it
|
|
108
|
+
vec3 thru = exp(-SIG * .55) * .8 + .08; // light that passed through jelly is coloured
|
|
109
|
+
fl *= mix(vec3(1), thru, inside) * (1. - .4 * edge) * (1. - ao);
|
|
110
|
+
fl += TINT * TINT * smoothstep(.0, .3, -sd) * SH_K * .45; // and gathers into a glowing core
|
|
111
|
+
col = mix(fl, back, smoothstep(5., 16., t));
|
|
112
|
+
}
|
|
113
|
+
col *= 1. - .25 * pow(length(uv - .5) * 1.25, 2.);
|
|
114
|
+
o = vec4(col, 1);
|
|
115
|
+
}`);
|
|
116
|
+
const backProg = link(meshVS, `#version 300 es
|
|
117
|
+
precision highp float; in vec3 vW, vN; out vec4 o; uniform vec3 E;
|
|
118
|
+
void main() { float x = clamp(length(vW - E) / 12., 0., .9999); float h = floor(x * 255.) / 255.; o = vec4(h, (x - h) * 255., 0, 1); }`);
|
|
119
|
+
const copyProg = link(quadVS, `#version 300 es
|
|
120
|
+
precision highp float; in vec2 uv; out vec4 o; uniform sampler2D BG; void main() { o = texture(BG, uv); }`);
|
|
121
|
+
const jellyProg = link(meshVS, `#version 300 es
|
|
122
|
+
precision highp float; in vec3 vW, vN; out vec4 o;
|
|
123
|
+
uniform sampler2D BG, BK; uniform vec2 R; uniform vec3 E, SIG, TINT; uniform mat4 VM; uniform vec4 B[4];
|
|
124
|
+
${ENV}
|
|
125
|
+
void main() {
|
|
126
|
+
vec3 n = normalize(vN), v = normalize(E - vW);
|
|
127
|
+
float cv = max(dot(n, v), 0.), F = .04 + .96 * pow(1. - cv, 5.);
|
|
128
|
+
vec2 s = gl_FragCoord.xy / R;
|
|
129
|
+
vec4 bk = texture(BK, s);
|
|
130
|
+
float th = clamp((bk.x + bk.y / 255.) * 12. - length(vW - E), 0., 2.); // how much jelly this ray crosses
|
|
131
|
+
vec3 nv = mat3(VM) * n;
|
|
132
|
+
vec2 off = -nv.xy * (.05 + .15 * th) * vec2(R.y / R.x, 1.); // the floor behind bends through it
|
|
133
|
+
vec3 behind = pow(texture(BG, clamp(s + off, .001, .999)).rgb, vec3(2.2));
|
|
134
|
+
vec3 trans = behind * exp(-SIG * th * 1.25); // Beer's law: thicker, deeper colour
|
|
135
|
+
float wrap = dot(n, L) * .5 + .5;
|
|
136
|
+
vec3 scat = TINT * (1. - exp(-th * 2.)) * (.05 + .2 * wrap) + TINT * pow(1. - cv, 2.) * .22; // light scattered inside, brightest at the rims
|
|
137
|
+
vec3 rd = refract(-v, n, .75);
|
|
138
|
+
for (int i = 0; i < 4; i++) { // a few trapped air bubbles, seen through the bent ray
|
|
139
|
+
vec3 oc = vW - B[i].xyz; float b = dot(oc, rd), c = dot(oc, oc) - B[i].w * B[i].w, h = b * b - c;
|
|
140
|
+
if (h > 0.) { float tt = -b - sqrt(h); if (tt > 0. && tt < th) { vec3 nb = normalize(oc + rd * tt); float cs = abs(dot(nb, rd)), rim = smoothstep(.55, .0, cs);
|
|
141
|
+
vec3 clear = behind * exp(-SIG * max(th - B[i].w * 2., 0.) * 1.25); // less jelly in the way: clearer middle
|
|
142
|
+
trans = mix(clear, trans * .55, rim) + pow(max(dot(reflect(rd, nb), L), 0.), 60.) * .9 + smoothstep(.25, .0, cs) * .06; } }
|
|
143
|
+
}
|
|
144
|
+
vec3 r = reflect(-v, n);
|
|
145
|
+
vec3 refl = env(r) * (r.y < 0. ? .55 : 1.);
|
|
146
|
+
vec3 col = mix(trans + scat, refl, F);
|
|
147
|
+
vec3 h = normalize(L + v); float nh = max(dot(n, h), 0.);
|
|
148
|
+
col += pow(nh, 220.) * 2.2 + pow(nh, 24.) * .06;
|
|
149
|
+
col = col / (1. + col * .18);
|
|
150
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
151
|
+
}`);
|
|
152
|
+
|
|
153
|
+
const quad = gl.createVertexArray(); gl.bindVertexArray(quad);
|
|
154
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
155
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
156
|
+
const mesh = gl.createVertexArray(); gl.bindVertexArray(mesh);
|
|
157
|
+
const posBuf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, posBuf); gl.bufferData(gl.ARRAY_BUFFER, POS.byteLength, gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
|
|
158
|
+
const norBuf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, norBuf); gl.bufferData(gl.ARRAY_BUFFER, NOR.byteLength, gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
|
|
159
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, idx, gl.STATIC_DRAW);
|
|
160
|
+
const U = (p, n) => gl.getUniformLocation(p, n);
|
|
161
|
+
|
|
162
|
+
// two offscreen targets: the floor (sampled through the jelly) and the jelly's back-face distance
|
|
163
|
+
const mkTarget = depth => { const tex = gl.createTexture(), fb = gl.createFramebuffer(), rb = depth ? gl.createRenderbuffer() : null; return { tex, fb, rb }; };
|
|
164
|
+
const tBG = mkTarget(false), tBK = mkTarget(true);
|
|
165
|
+
const alloc = (t, w, h) => {
|
|
166
|
+
gl.bindTexture(gl.TEXTURE_2D, t.tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
167
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, t.rb ? gl.NEAREST : gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, t.rb ? gl.NEAREST : gl.LINEAR);
|
|
168
|
+
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);
|
|
169
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, t.fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t.tex, 0);
|
|
170
|
+
if (t.rb) { gl.bindRenderbuffer(gl.RENDERBUFFER, t.rb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, w, h); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, t.rb); }
|
|
171
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
172
|
+
};
|
|
173
|
+
let cw = 0, ch = 0, Wc = 1, Hc = 1;
|
|
174
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); Wc = stage.clientWidth || 1; Hc = stage.clientHeight || 1; cw = canvas.width = Wc * d | 0; ch = canvas.height = Hc * d | 0; alloc(tBG, cw, ch); alloc(tBK, cw, ch); };
|
|
175
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
176
|
+
|
|
177
|
+
// ── camera (look-at, column-major)
|
|
178
|
+
const FOV = 0.62, TH = Math.tan(FOV / 2), Ln = [-0.55, 1, 0.42], Ll = Math.hypot(...Ln), Ld = Ln.map(v => v / Ll);
|
|
179
|
+
const norm = a => { const l = Math.hypot(...a) || 1; return a.map(v => v / l); }, crs = (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]];
|
|
180
|
+
let cam = null;
|
|
181
|
+
const camera = () => {
|
|
182
|
+
const asp = cw / ch, fit = Math.max(1, 1.25 / asp), eye = [0, 1.75 * fit, 4.7 * fit], ta = [0, 0.7, 0];
|
|
183
|
+
const f = norm(ta.map((v, i) => v - eye[i])), r = norm(crs(f, [0, 1, 0])), u = crs(r, f);
|
|
184
|
+
const VM = new Float32Array([r[0], u[0], -f[0], 0, r[1], u[1], -f[1], 0, r[2], u[2], -f[2], 0, -(r[0] * eye[0] + r[1] * eye[1] + r[2] * eye[2]), -(u[0] * eye[0] + u[1] * eye[1] + u[2] * eye[2]), f[0] * eye[0] + f[1] * eye[1] + f[2] * eye[2], 1]);
|
|
185
|
+
const t = 1 / TH, nr = 0.1, fr = 40, P = new Float32Array([t / asp, 0, 0, 0, 0, t, 0, 0, 0, 0, (fr + nr) / (nr - fr), -1, 0, 0, 2 * fr * nr / (nr - fr), 0]);
|
|
186
|
+
const PV = new Float32Array(16); for (let c = 0; c < 4; c++) for (let rr = 0; rr < 4; rr++) { let s = 0; for (let k = 0; k < 4; k++) s += P[k * 4 + rr] * VM[c * 4 + k]; PV[c * 4 + rr] = s; }
|
|
187
|
+
cam = { eye, f, r, u, VM, PV, asp };
|
|
188
|
+
};
|
|
189
|
+
const ray = (px, py) => { const x = (px / Wc * 2 - 1) * TH * cam.asp, y = (1 - py / Hc * 2) * TH; return norm(cam.f.map((v, i) => v + cam.r[i] * x + cam.u[i] * y)); };
|
|
190
|
+
const toScreen = (p) => { const P = cam.PV, w = P[3] * p[0] + P[7] * p[1] + P[11] * p[2] + P[15]; return [((P[0] * p[0] + P[4] * p[1] + P[8] * p[2] + P[12]) / w * 0.5 + 0.5) * Wc, (0.5 - (P[1] * p[0] + P[5] * p[1] + P[9] * p[2] + P[13]) / w * 0.5) * Hc]; };
|
|
191
|
+
|
|
192
|
+
// ── pointer: grab a node and drag (the jelly stretches, then flies when let go); a quick tap makes it hop
|
|
193
|
+
let grab = null, lastInput = -1e9;
|
|
194
|
+
const nodeAt = (x, y) => { let best = -1, bd = Math.max(46, Hc * 0.16) ** 2; for (const n of surf) { const s = toScreen([X[n * 3], X[n * 3 + 1], X[n * 3 + 2]]), d = (s[0] - x) ** 2 + (s[1] - y) ** 2; if (d < bd) { bd = d; best = n; } } return best; };
|
|
195
|
+
const local = e => { const b = canvas.getBoundingClientRect(); return [e.clientX - b.left, e.clientY - b.top]; };
|
|
196
|
+
const onDown = e => {
|
|
197
|
+
if (!cam) return; const [x, y] = local(e), n = nodeAt(x, y); lastInput = performance.now(); if (n < 0) return;
|
|
198
|
+
const p = [X[n * 3], X[n * 3 + 1], X[n * 3 + 2]], depth = p.reduce((s, v, i) => s + (v - cam.eye[i]) * cam.f[i], 0);
|
|
199
|
+
grab = { n, depth, target: p, t0: performance.now(), x, y, moved: 0 }; canvas.style.cursor = 'grabbing'; canvas.setPointerCapture?.(e.pointerId);
|
|
200
|
+
};
|
|
201
|
+
const onMove = e => {
|
|
202
|
+
if (!grab) return; const [x, y] = local(e), d = ray(x, y), k = grab.depth / d.reduce((s, v, i) => s + v * cam.f[i], 0);
|
|
203
|
+
grab.target = cam.eye.map((v, i) => v + d[i] * k); grab.target[1] = Math.max(0.02, grab.target[1]);
|
|
204
|
+
grab.moved = Math.max(grab.moved, Math.hypot(x - grab.x, y - grab.y)); lastInput = performance.now();
|
|
205
|
+
};
|
|
206
|
+
const onUp = () => {
|
|
207
|
+
if (grab && grab.moved < 6 && performance.now() - grab.t0 < 300) { // a tap: pop up and away from where it was touched
|
|
208
|
+
const g = grab.n; for (let n = 0; n < N; n++) { const dx = X[n * 3] - X[g * 3], dz = X[n * 3 + 2] - X[g * 3 + 2], w = Math.exp(-(dx * dx + dz * dz) * 3); Vl[n * 3] += dx * 2.2 * w; Vl[n * 3 + 1] += 4.2 * PARAMS.hop * (0.6 + 0.4 * w); Vl[n * 3 + 2] += dz * 2.2 * w; }
|
|
209
|
+
}
|
|
210
|
+
grab = null; canvas.style.cursor = 'grab'; lastInput = performance.now();
|
|
211
|
+
};
|
|
212
|
+
canvas.addEventListener('pointerdown', onDown); canvas.addEventListener('pointermove', onMove); canvas.addEventListener('pointerup', onUp); canvas.addEventListener('pointercancel', onUp);
|
|
213
|
+
|
|
214
|
+
// ── physics
|
|
215
|
+
const GRAV = 14, cen = [0, 0, 0];
|
|
216
|
+
let nextHop = 1.75, hopN = 0;
|
|
217
|
+
const hop = () => { // idle autopilot: a hop with a spin, always aimed back toward the middle
|
|
218
|
+
const a = hopN++ * 2.39996 + 0.7, ax = [Math.cos(a), 0.25 * Math.sin(a * 1.7), Math.sin(a)], w = (4.2 + 2.2 * Math.abs(Math.sin(a * 3.1))) * Math.min(1.4, PARAMS.hop), up = 4.1 * PARAMS.hop;
|
|
219
|
+
const axn = norm(ax), vx = -cen[0] * 1.6 + Math.cos(a * 2.3) * 0.35, vz = -cen[2] * 1.6 + Math.sin(a * 1.9) * 0.25;
|
|
220
|
+
for (let n = 0; n < N; n++) { const rx = X[n * 3] - cen[0], ry = X[n * 3 + 1] - cen[1], rz = X[n * 3 + 2] - cen[2]; Vl[n * 3] += vx + w * (axn[1] * rz - axn[2] * ry); Vl[n * 3 + 1] += up + w * (axn[2] * rx - axn[0] * rz); Vl[n * 3 + 2] += vz + w * (axn[0] * ry - axn[1] * rx); }
|
|
221
|
+
};
|
|
222
|
+
function step(h) {
|
|
223
|
+
const soft = Math.max(0.3, PARAMS.softness), KS = 1100 / soft, CS = 2, KG = 60 / soft;
|
|
224
|
+
cen[0] = cen[1] = cen[2] = 0; for (let n = 0; n < N; n++) for (let c = 0; c < 3; c++) cen[c] += X[n * 3 + c] / N;
|
|
225
|
+
// best-fit rotation of the rest shape (polar decomposition by quaternion iteration, warm-started)
|
|
226
|
+
const A = new Float64Array(9);
|
|
227
|
+
for (let n = 0; n < N; n++) for (let c = 0; c < 3; c++) { const q = Qr[n * 3 + c]; for (let r = 0; r < 3; r++) A[c * 3 + r] += (X[n * 3 + r] - cen[r]) * q; }
|
|
228
|
+
for (let it = 0; it < 4; it++) {
|
|
229
|
+
const R = qmat(rq), om = [0, 0, 0]; let dn = 0;
|
|
230
|
+
for (let c = 0; c < 3; c++) { const rc = [R[c * 3], R[c * 3 + 1], R[c * 3 + 2]], ac = [A[c * 3], A[c * 3 + 1], A[c * 3 + 2]], x = crs(rc, ac); om[0] += x[0]; om[1] += x[1]; om[2] += x[2]; dn += rc[0] * ac[0] + rc[1] * ac[1] + rc[2] * ac[2]; }
|
|
231
|
+
const o = om.map(v => v / (Math.abs(dn) + 1e-9)), w = Math.hypot(...o); if (w < 1e-9) break;
|
|
232
|
+
rq = qmul(qaxis(o, w), rq); const l = Math.hypot(...rq); rq = rq.map(v => v / l);
|
|
233
|
+
}
|
|
234
|
+
const R = qmat(rq);
|
|
235
|
+
for (let n = 0; n < N; n++) {
|
|
236
|
+
const x = Qr[n * 3], y = Qr[n * 3 + 1], z = Qr[n * 3 + 2];
|
|
237
|
+
for (let r = 0; r < 3; r++) Fo[n * 3 + r] = KG * (R[r] * x + R[3 + r] * y + R[6 + r] * z + cen[r] - X[n * 3 + r]);
|
|
238
|
+
Fo[n * 3 + 1] -= GRAV;
|
|
239
|
+
}
|
|
240
|
+
for (let s = 0; s < S; s++) {
|
|
241
|
+
const a = sA[s] * 3, b = sB[s] * 3, dx = X[b] - X[a], dy = X[b + 1] - X[a + 1], dz = X[b + 2] - X[a + 2], L = Math.hypot(dx, dy, dz) || 1e-6;
|
|
242
|
+
const ux = dx / L, uy = dy / L, uz = dz / L, rv = (Vl[b] - Vl[a]) * ux + (Vl[b + 1] - Vl[a + 1]) * uy + (Vl[b + 2] - Vl[a + 2]) * uz;
|
|
243
|
+
const f = KS * (L - sL[s]) + CS * rv;
|
|
244
|
+
Fo[a] += f * ux; Fo[a + 1] += f * uy; Fo[a + 2] += f * uz; Fo[b] -= f * ux; Fo[b + 1] -= f * uy; Fo[b + 2] -= f * uz;
|
|
245
|
+
}
|
|
246
|
+
if (grab) { // pull the grabbed node and its neighbours toward the pointer
|
|
247
|
+
const g = grab.n * 3, ex = grab.target[0] - X[g], ey = grab.target[1] - X[g + 1], ez = grab.target[2] - X[g + 2];
|
|
248
|
+
for (let n = 0; n < N; n++) { const d2 = (Qr[n * 3] - Qr[g]) ** 2 + (Qr[n * 3 + 1] - Qr[g + 1]) ** 2 + (Qr[n * 3 + 2] - Qr[g + 2]) ** 2, w = Math.exp(-d2 * 9) * 950; if (w < 3) continue; Fo[n * 3] += ex * w - Vl[n * 3] * w * 0.04; Fo[n * 3 + 1] += ey * w - Vl[n * 3 + 1] * w * 0.04; Fo[n * 3 + 2] += ez * w - Vl[n * 3 + 2] * w * 0.04; }
|
|
249
|
+
}
|
|
250
|
+
for (let i = 0; i < N * 3; i++) { Vl[i] = (Vl[i] + Fo[i] * h) * (1 - 0.15 * h); X[i] += Vl[i] * h; }
|
|
251
|
+
for (let n = 0; n < N; n++) {
|
|
252
|
+
const i = n * 3;
|
|
253
|
+
if (X[i + 1] < 0) { // floor: no bounce per node (the springs give the bounce), friction from the impact
|
|
254
|
+
X[i + 1] = 0; const vn = Math.max(0, -Vl[i + 1]); Vl[i + 1] = Math.max(0, Vl[i + 1]);
|
|
255
|
+
const vt = Math.hypot(Vl[i], Vl[i + 2]); if (vt > 0) { const k = Math.max(0, vt - (0.55 * vn + 0.004)) / vt; Vl[i] *= k; Vl[i + 2] *= k; }
|
|
256
|
+
}
|
|
257
|
+
for (const c of [0, 2]) { const lim = c === 0 ? 1.7 : 1.2; if (X[i + c] > lim) { X[i + c] = lim; Vl[i + c] = -Math.abs(Vl[i + c]) * 0.3; } else if (X[i + c] < -lim) { X[i + c] = -lim; Vl[i + c] = Math.abs(Vl[i + c]) * 0.3; } }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// convex hull of the lattice's shadow on the floor (projected along the light)
|
|
262
|
+
const hull = pts => {
|
|
263
|
+
pts.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
264
|
+
const c = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]), lo = [], up = [];
|
|
265
|
+
for (const p of pts) { while (lo.length >= 2 && c(lo[lo.length - 2], lo[lo.length - 1], p) <= 0) lo.pop(); lo.push(p); }
|
|
266
|
+
for (let i = pts.length - 1; i >= 0; i--) { const p = pts[i]; while (up.length >= 2 && c(up[up.length - 2], up[up.length - 1], p) <= 0) up.pop(); up.push(p); }
|
|
267
|
+
return lo.slice(0, -1).concat(up.slice(0, -1));
|
|
268
|
+
};
|
|
269
|
+
const tint = () => { const h = ((PARAMS.hue % 360) + 360) % 360 / 60, s = 0.9, l = 0.5, C = (1 - Math.abs(2 * l - 1)) * s, x = C * (1 - Math.abs(h % 2 - 1)), m = l - C / 2;
|
|
270
|
+
const [r, g, b] = h < 1 ? [C, x, 0] : h < 2 ? [x, C, 0] : h < 3 ? [0, C, x] : h < 4 ? [0, x, C] : h < 5 ? [x, 0, C] : [C, 0, x]; return [r + m, g + m, b + m]; };
|
|
271
|
+
|
|
272
|
+
let raf, last = performance.now(); const t0 = last;
|
|
273
|
+
function frame(now) {
|
|
274
|
+
const dt = Math.min(1 / 30, (now - last) / 1000), t = (now - t0) / 1000; last = now;
|
|
275
|
+
if (!grab && now - lastInput > 2500 && t > nextHop) { hop(); nextHop = t + 2.5; }
|
|
276
|
+
else if (grab || now - lastInput <= 2500) nextHop = Math.max(nextHop, t + 1.2);
|
|
277
|
+
const n = Math.max(1, Math.ceil(dt * 720)); for (let i = 0; i < n; i++) step(dt / n);
|
|
278
|
+
camera();
|
|
279
|
+
// deform the rounded box through the lattice, then smooth normals
|
|
280
|
+
for (let v = 0; v < NV; v++) { let x = 0, y = 0, z = 0; const o = v * 64; for (let k = 0; k < 64; k++) { const w = W[o + k]; x += w * X[k * 3]; y += w * X[k * 3 + 1]; z += w * X[k * 3 + 2]; } POS[v * 3] = x; POS[v * 3 + 1] = y; POS[v * 3 + 2] = z; }
|
|
281
|
+
NOR.fill(0);
|
|
282
|
+
for (let i = 0; i < idx.length; i += 3) {
|
|
283
|
+
const a = idx[i] * 3, b = idx[i + 1] * 3, c = idx[i + 2] * 3, e1x = POS[b] - POS[a], e1y = POS[b + 1] - POS[a + 1], e1z = POS[b + 2] - POS[a + 2], e2x = POS[c] - POS[a], e2y = POS[c + 1] - POS[a + 1], e2z = POS[c + 2] - POS[a + 2];
|
|
284
|
+
const nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
|
|
285
|
+
for (const q of [a, b, c]) { NOR[q] += nx; NOR[q + 1] += ny; NOR[q + 2] += nz; }
|
|
286
|
+
}
|
|
287
|
+
BW.forEach((w, b) => { let x = 0, y = 0, z = 0; for (let k = 0; k < 64; k++) { x += w[k] * X[k * 3]; y += w[k] * X[k * 3 + 1]; z += w[k] * X[k * 3 + 2]; } BF.set([x, y, z, BUB[b][3]], b * 4); });
|
|
288
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, posBuf); gl.bufferSubData(gl.ARRAY_BUFFER, 0, POS);
|
|
289
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, norBuf); gl.bufferSubData(gl.ARRAY_BUFFER, 0, NOR);
|
|
290
|
+
// shadow footprint, its softness (grows with height) and how close the jelly is to the floor
|
|
291
|
+
const sp = surf.map(k => { const y = X[k * 3 + 1]; return [X[k * 3] - Ld[0] / Ld[1] * y, X[k * 3 + 2] - Ld[2] / Ld[1] * y]; });
|
|
292
|
+
const H = hull(sp).slice(0, 32), Hf = new Float32Array(64); H.forEach((p, i) => { Hf[i * 2] = p[0]; Hf[i * 2 + 1] = p[1]; });
|
|
293
|
+
let minY = 1e9; for (let k = 0; k < N; k++) minY = Math.min(minY, X[k * 3 + 1]);
|
|
294
|
+
const lift = Math.max(0, cen[1] - 0.5), TI = tint(), SIG = TI.map(c => (1 - c) * 2.4 + 0.1);
|
|
295
|
+
|
|
296
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, tBG.fb); gl.viewport(0, 0, cw, ch); gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE);
|
|
297
|
+
gl.useProgram(bgProg); gl.bindVertexArray(quad);
|
|
298
|
+
gl.uniform3fv(U(bgProg, 'E'), cam.eye); gl.uniform3fv(U(bgProg, 'CF'), cam.f); gl.uniform3fv(U(bgProg, 'CR'), cam.r); gl.uniform3fv(U(bgProg, 'CU'), cam.u);
|
|
299
|
+
gl.uniform2f(U(bgProg, 'R'), cw, ch); gl.uniform1f(U(bgProg, 'TH'), TH); gl.uniform3fv(U(bgProg, 'TINT'), TI); gl.uniform3fv(U(bgProg, 'SIG'), SIG); gl.uniform3fv(U(bgProg, 'CEN'), cen);
|
|
300
|
+
gl.uniform2fv(U(bgProg, 'H'), Hf); gl.uniform1i(U(bgProg, 'HN'), H.length);
|
|
301
|
+
gl.uniform1f(U(bgProg, 'SH_B'), 0.05 + lift * 0.28); gl.uniform1f(U(bgProg, 'SH_K'), 0.85 / (1 + lift * 1.6)); gl.uniform1f(U(bgProg, 'HIT'), Math.exp(-minY * 5));
|
|
302
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
303
|
+
|
|
304
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, tBK.fb); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
305
|
+
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); gl.cullFace(gl.FRONT);
|
|
306
|
+
gl.useProgram(backProg); gl.bindVertexArray(mesh);
|
|
307
|
+
gl.uniformMatrix4fv(U(backProg, 'PV'), false, cam.PV); gl.uniform3fv(U(backProg, 'E'), cam.eye);
|
|
308
|
+
gl.drawElements(gl.TRIANGLES, idx.length, gl.UNSIGNED_SHORT, 0);
|
|
309
|
+
|
|
310
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, cw, ch); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
311
|
+
gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE);
|
|
312
|
+
gl.useProgram(copyProg); gl.bindVertexArray(quad); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tBG.tex); gl.uniform1i(U(copyProg, 'BG'), 0);
|
|
313
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
314
|
+
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK);
|
|
315
|
+
gl.useProgram(jellyProg); gl.bindVertexArray(mesh);
|
|
316
|
+
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, tBK.tex); gl.uniform1i(U(jellyProg, 'BG'), 0); gl.uniform1i(U(jellyProg, 'BK'), 1);
|
|
317
|
+
gl.uniformMatrix4fv(U(jellyProg, 'PV'), false, cam.PV); gl.uniformMatrix4fv(U(jellyProg, 'VM'), false, cam.VM);
|
|
318
|
+
gl.uniform3fv(U(jellyProg, 'E'), cam.eye); gl.uniform2f(U(jellyProg, 'R'), cw, ch); gl.uniform3fv(U(jellyProg, 'L'), Ld);
|
|
319
|
+
gl.uniform3fv(U(jellyProg, 'SIG'), SIG); gl.uniform3fv(U(jellyProg, 'TINT'), TI); gl.uniform4fv(U(jellyProg, 'B'), BF);
|
|
320
|
+
gl.drawElements(gl.TRIANGLES, idx.length, gl.UNSIGNED_SHORT, 0);
|
|
321
|
+
raf = requestAnimationFrame(frame);
|
|
322
|
+
}
|
|
323
|
+
raf = requestAnimationFrame(frame);
|
|
324
|
+
return () => {
|
|
325
|
+
cancelAnimationFrame(raf); ro.disconnect();
|
|
326
|
+
canvas.removeEventListener('pointerdown', onDown); canvas.removeEventListener('pointermove', onMove); canvas.removeEventListener('pointerup', onUp); canvas.removeEventListener('pointercancel', onUp);
|
|
327
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove();
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Klein bottle: a surface with no inside or outside. The neck bends round and passes through the wall to join the
|
|
2
|
+
// base from within, so a path along it ends up on the "other" side. It is built from its parametric equations as a
|
|
3
|
+
// triangle mesh (normals from the derivative directions), drawn with depth testing and lit on both faces, since every
|
|
4
|
+
// face is seen from both sides. Glowing grid lines follow the parameter lines, which makes the twist readable.
|
|
5
|
+
const PARAMS = { spin: 1, grid: 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: true });
|
|
12
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
13
|
+
// the classic "bottle" parametrisation, u in [0, π), v in [0, 2π)
|
|
14
|
+
const K = (u, v) => {
|
|
15
|
+
const cu = Math.cos(u), su = Math.sin(u), cv = Math.cos(v), sv = Math.sin(v);
|
|
16
|
+
const x = -2 / 15 * cu * (3 * cv - 30 * su + 90 * cu ** 4 * su - 60 * cu ** 6 * su + 5 * cu * cv * su);
|
|
17
|
+
const y = -1 / 15 * su * (3 * cv - 3 * cu ** 2 * cv - 48 * cu ** 4 * cv + 48 * cu ** 6 * cv - 60 * su + 5 * cu * cv * su - 5 * cu ** 3 * cv * su - 80 * cu ** 5 * cv * su + 80 * cu ** 7 * cv * su);
|
|
18
|
+
const z = 2 / 15 * (3 + 5 * cu * su) * sv;
|
|
19
|
+
return [x, y, z];
|
|
20
|
+
};
|
|
21
|
+
const NU = 160, NV = 48, pos = [], nor = [], tex = [], idx = [];
|
|
22
|
+
for (let i = 0; i <= NU; i++) for (let j = 0; j <= NV; j++) {
|
|
23
|
+
const u = i / NU * Math.PI, v = j / NV * Math.PI * 2, e = 1e-4, p = K(u, v), pu = K(u + e, v), pv = K(u, v + e);
|
|
24
|
+
const a = pu.map((c, k) => c - p[k]), b = pv.map((c, k) => c - p[k]);
|
|
25
|
+
let n = [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; const l = Math.hypot(...n) || 1; n = n.map(c => c / l);
|
|
26
|
+
pos.push(...p); nor.push(...n); tex.push(i / NU, j / NV);
|
|
27
|
+
}
|
|
28
|
+
// centre it and scale it to a unit-ish size
|
|
29
|
+
const lo = [1e9, 1e9, 1e9], hi = [-1e9, -1e9, -1e9];
|
|
30
|
+
for (let i = 0; i < pos.length; i++) { lo[i % 3] = Math.min(lo[i % 3], pos[i]); hi[i % 3] = Math.max(hi[i % 3], pos[i]); }
|
|
31
|
+
const mid = lo.map((v, k) => (v + hi[k]) / 2), span = Math.max(...hi.map((v, k) => v - lo[k]));
|
|
32
|
+
for (let i = 0; i < pos.length; i++) pos[i] = (pos[i] - mid[i % 3]) / span * 4.6;
|
|
33
|
+
for (let i = 0; i < NU; i++) for (let j = 0; j < NV; j++) { const a = i * (NV + 1) + j, b = a + NV + 1; idx.push(a, b, a + 1, b, b + 1, a + 1); }
|
|
34
|
+
const vs = `#version 300 es
|
|
35
|
+
in vec3 P, N; in vec2 UV; uniform mat4 M, V; out vec3 n, w; out vec2 uv;
|
|
36
|
+
void main() { vec4 wp = M * vec4(P, 1); w = wp.xyz; n = mat3(M) * N; uv = UV; gl_Position = V * wp; }`;
|
|
37
|
+
const fs = `#version 300 es
|
|
38
|
+
precision highp float; in vec3 n, w; in vec2 uv; uniform vec3 E; uniform float G, T; out vec4 o;
|
|
39
|
+
void main() {
|
|
40
|
+
vec3 N = normalize(n) * (gl_FrontFacing ? 1. : -1.), vd = normalize(E - w);
|
|
41
|
+
if (dot(N, vd) < 0.) N = -N; // lit from whichever side we see
|
|
42
|
+
vec3 L = normalize(vec3(.5, .9, -.4));
|
|
43
|
+
float dif = max(dot(N, L), 0.), rim = pow(1. - max(dot(N, vd), 0.), 3.);
|
|
44
|
+
vec3 base = .5 + .5 * cos(6.28 * (uv.x * .9 + uv.y * .15 + vec3(0, .33, .67)) + T * .3); // hue flows along the bottle
|
|
45
|
+
// a see-through hologram: faint body, bright rims and grid lines, all added up, so the neck shows passing through the wall
|
|
46
|
+
vec3 col = base * (.05 + .12 * dif) + base * rim * .55 + pow(max(dot(reflect(-L, N), vd), 0.), 40.) * .25;
|
|
47
|
+
vec2 g = abs(fract(uv * vec2(32., 16.)) - .5) / fwidth(uv * vec2(32., 16.));
|
|
48
|
+
float line = 1. - min(min(g.x, g.y), 1.);
|
|
49
|
+
col += mix(base, vec3(1), .5) * line * .32 * G;
|
|
50
|
+
o = vec4(col, 1);
|
|
51
|
+
}`;
|
|
52
|
+
const sh = (t, s) => { const x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x); if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(x)); return x; };
|
|
53
|
+
const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, fs));
|
|
54
|
+
gl.bindAttribLocation(prog, 0, 'P'); gl.bindAttribLocation(prog, 1, 'N'); gl.bindAttribLocation(prog, 2, 'UV'); gl.linkProgram(prog); gl.useProgram(prog);
|
|
55
|
+
const buf = (data, loc, size) => { gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0); };
|
|
56
|
+
buf(pos, 0, 3); buf(nor, 1, 3); buf(tex, 2, 2);
|
|
57
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(idx), gl.STATIC_DRAW);
|
|
58
|
+
gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE); // additive: no sorting needed, every layer shows
|
|
59
|
+
const U = {}; ['M', 'V', 'E', 'G', 'T'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
60
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); canvas.width = (stage.clientWidth || 1) * d | 0; canvas.height = (stage.clientHeight || 1) * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
61
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
62
|
+
const mul = (a, b) => { const o = new Float32Array(16); for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) { let s = 0; for (let k = 0; k < 4; k++) s += a[k * 4 + j] * b[i * 4 + k]; o[i * 4 + j] = s; } return o; };
|
|
63
|
+
let raf, last = performance.now(), ang = 0;
|
|
64
|
+
function frame(now) {
|
|
65
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; ang += dt * 0.4 * PARAMS.spin;
|
|
66
|
+
const t = now / 1000, asp = canvas.width / canvas.height, f = 1 / Math.tan(0.5), near = 0.1, far = 50;
|
|
67
|
+
const P = new Float32Array([f / asp, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) / (near - far), -1, 0, 0, 2 * far * near / (near - far), 0]);
|
|
68
|
+
const dist = 5.2 * Math.max(1, 1.1 / asp), E = [0, 0.4, dist];
|
|
69
|
+
const Vw = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -E[0], -E[1], -E[2], 1]);
|
|
70
|
+
const ca = Math.cos(ang), sa = Math.sin(ang), tb = 0.35 + Math.sin(t * 0.2) * 0.2, cb = Math.cos(tb), sb = Math.sin(tb);
|
|
71
|
+
const Ry = new Float32Array([ca, 0, -sa, 0, 0, 1, 0, 0, sa, 0, ca, 0, 0, 0, 0, 1]), Rx = new Float32Array([1, 0, 0, 0, 0, cb, sb, 0, 0, -sb, cb, 0, 0, 0, 0, 1]);
|
|
72
|
+
gl.clearColor(0.015, 0.018, 0.035, 1); gl.clear(gl.COLOR_BUFFER_BIT);
|
|
73
|
+
gl.uniformMatrix4fv(U.M, false, mul(Rx, Ry)); gl.uniformMatrix4fv(U.V, false, mul(P, Vw));
|
|
74
|
+
gl.uniform3fv(U.E, E); gl.uniform1f(U.G, PARAMS.grid); gl.uniform1f(U.T, t);
|
|
75
|
+
gl.drawElements(gl.TRIANGLES, idx.length, gl.UNSIGNED_INT, 0);
|
|
76
|
+
raf = requestAnimationFrame(frame);
|
|
77
|
+
}
|
|
78
|
+
raf = requestAnimationFrame(frame);
|
|
79
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
80
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Liquid metal: a pool of molten chrome. A blob pulls itself up out of the surface (a smooth union, so a neck
|
|
2
|
+
// forms and thins), hangs there wobbling, then drops back and sends rings across the pool. Every pixel is
|
|
3
|
+
// raymarched and reflected twice, so the blob mirrors the pool and the pool mirrors the blob.
|
|
4
|
+
const PARAMS = { cycle: 5.5, ripple: 1, smooth: 0.45 };
|
|
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;
|
|
16
|
+
uniform vec2 R; uniform float T, BY, BR, TI, TR, RA, K;
|
|
17
|
+
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); }
|
|
18
|
+
float rings(vec2 q, float t0, float amp) { // a ring wave from the centre, starting at t0
|
|
19
|
+
float age = T - t0, r = length(q), front = age * 1.7;
|
|
20
|
+
if (age < 0.) return 0.;
|
|
21
|
+
return amp * exp(-age * 1.1) * sin(r * 11. - age * 13.) * smoothstep(front, front - .9, r) * exp(-r * .5);
|
|
22
|
+
}
|
|
23
|
+
float surf(vec3 p) {
|
|
24
|
+
float h = .035 * sin(p.x * 1.9 + T * .7) * sin(p.z * 1.6 - T * .55) + .02 * sin(p.x * 4.3 - p.z * 3.1 + T * 1.3);
|
|
25
|
+
h += rings(p.xz, TI, .1 * RA) + rings(p.xz, TR, .05 * RA);
|
|
26
|
+
return (p.y - h) * .6;
|
|
27
|
+
}
|
|
28
|
+
float blob(vec3 p) {
|
|
29
|
+
vec3 q = p - vec3(0, BY, 0);
|
|
30
|
+
return length(q * vec3(1, 1. + .08 * sin(T * 5.), 1)) - BR - .045 * sin(q.x * 7. + T * 3.) * sin(q.y * 6. + T * 2.3) * sin(q.z * 7. - T * 2.);
|
|
31
|
+
}
|
|
32
|
+
float map(vec3 p) { return smin(surf(p), blob(p) * .75, K); } // the wobble bends the field, so step a little shorter
|
|
33
|
+
vec3 nrm(vec3 p) { vec2 e = vec2(.002, -.002); return normalize(e.xyy * map(p + e.xyy) + e.yyx * map(p + e.yyx) + e.yxy * map(p + e.yxy) + e.xxx * map(p + e.xxx)); }
|
|
34
|
+
float march(vec3 ro, vec3 rd, float far) { float t = 0.; for (int i = 0; i < 110; i++) { float d = map(ro + rd * t); if (d < .0012 * (1. + t)) return t; t += d; if (t > far) break; } return -1.; }
|
|
35
|
+
vec3 env(vec3 d) {
|
|
36
|
+
vec3 c = mix(vec3(.02, .022, .03), vec3(.13, .14, .17), smoothstep(-.1, .7, d.y));
|
|
37
|
+
vec2 q = d.xz / max(d.y, .05);
|
|
38
|
+
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); // overhead softbox
|
|
39
|
+
float az = atan(d.x, d.z);
|
|
40
|
+
c += vec3(1., .55, .3) * 1.6 * smoothstep(.1, .02, abs(az - 1.9)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y); // warm strip
|
|
41
|
+
c += vec3(.35, .6, 1.) * 1.4 * smoothstep(.1, .02, abs(az + 1.6)) * smoothstep(-.05, .2, d.y) * smoothstep(.75, .3, d.y); // cool strip
|
|
42
|
+
c += vec3(.9, .9, 1.) * .5 * exp(-pow(d.y * 18., 2.)); // horizon line
|
|
43
|
+
c += vec3(1., .97, .92) * 1.3 * smoothstep(.07, .0, abs(d.y - .34) - .05); // a ring light all round the studio
|
|
44
|
+
return c;
|
|
45
|
+
}
|
|
46
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
47
|
+
void main() {
|
|
48
|
+
vec2 q = (uv - .5) * vec2(R.x / R.y, 1.);
|
|
49
|
+
float a = T * .12;
|
|
50
|
+
vec3 ro = vec3(sin(a) * 4.3, 2.7, -cos(a) * 4.3), ta = vec3(0, .25, 0);
|
|
51
|
+
vec3 f = normalize(ta - ro), r = normalize(cross(vec3(0, 1, 0), f)), u = cross(f, r);
|
|
52
|
+
vec3 rd = normalize(f * 1.6 + r * q.x + u * q.y);
|
|
53
|
+
vec3 col = env(rd) * .35;
|
|
54
|
+
float t = march(ro, rd, 30.);
|
|
55
|
+
if (t > 0.) {
|
|
56
|
+
vec3 p = ro + rd * t, n = nrm(p), rf = reflect(rd, n);
|
|
57
|
+
float fr = .75 + .25 * pow(1. - max(0., dot(n, -rd)), 5.);
|
|
58
|
+
vec3 refl = env(rf);
|
|
59
|
+
float t2 = march(p + n * .02, rf, 12.); // second bounce: the metal mirrors itself
|
|
60
|
+
if (t2 > 0.) { vec3 p2 = p + n * .02 + rf * t2, r2 = reflect(rf, nrm(p2)); refl = env(vec3(r2.x, abs(r2.y), r2.z)) * (r2.y < 0. ? .6 : .85); } // rays heading down see the pool's mirror of the room
|
|
61
|
+
else if (rf.y < 0.) refl = env(vec3(rf.x, -rf.y, rf.z)) * .6;
|
|
62
|
+
col = refl * fr * vec3(.92, .95, 1.);
|
|
63
|
+
col = mix(col, env(rd) * .35, smoothstep(9., 22., t)); // the pool fades into the dark studio
|
|
64
|
+
}
|
|
65
|
+
col = aces(col);
|
|
66
|
+
col *= 1. - .3 * pow(length(uv - .5) * 1.3, 2.);
|
|
67
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
68
|
+
}`;
|
|
69
|
+
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; };
|
|
70
|
+
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);
|
|
71
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
72
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
73
|
+
const U = {}; ['R', 'T', 'BY', 'BR', 'TI', 'TR', 'RA', 'K'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
74
|
+
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); };
|
|
75
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
76
|
+
const ease = x => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
|
77
|
+
let raf; const t0 = performance.now();
|
|
78
|
+
function frame(now) {
|
|
79
|
+
const t = (now - t0) / 1000, C = PARAMS.cycle, k = Math.floor(t / C), c = (t % C) / C;
|
|
80
|
+
// the cycle: rise (0–30%), hang and wobble (30–62%), fall (62–72%), rings spread (72–100%)
|
|
81
|
+
let by;
|
|
82
|
+
if (c < 0.3) by = -0.55 + 1.45 * ease(c / 0.3);
|
|
83
|
+
else if (c < 0.62) by = 0.9 + 0.06 * Math.sin((c - 0.3) * C * 4);
|
|
84
|
+
else if (c < 0.72) { const f = (c - 0.62) / 0.1; by = 0.9 - 1.6 * f * f; }
|
|
85
|
+
else by = -0.7;
|
|
86
|
+
const impact = k * C + C * (0.62 + 0.1 * Math.sqrt(0.9 / 1.6)); // when the falling blob meets the surface
|
|
87
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, t);
|
|
88
|
+
gl.uniform1f(U.BY, by); gl.uniform1f(U.BR, 0.48); gl.uniform1f(U.TI, t >= impact ? impact : impact - C);
|
|
89
|
+
gl.uniform1f(U.TR, k * C + 0.15 * C); gl.uniform1f(U.RA, PARAMS.ripple); gl.uniform1f(U.K, PARAMS.smooth);
|
|
90
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3); raf = requestAnimationFrame(frame);
|
|
91
|
+
}
|
|
92
|
+
raf = requestAnimationFrame(frame);
|
|
93
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
94
|
+
}
|