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,89 @@
|
|
|
1
|
+
// Torus knot: a tube swept along a (p, q) knot that winds p times around the axis and q times through the hole.
|
|
2
|
+
// The tube's frame is carried along by parallel transport (no sudden flips, unlike Frenet frames). It is shaded
|
|
3
|
+
// as chrome that shifts hue with viewing angle, like a thin film, and stripes flow along it.
|
|
4
|
+
const PARAMS = { p: 2, q: 3, flow: 1 };
|
|
5
|
+
|
|
6
|
+
export default function (stage) {
|
|
7
|
+
const canvas = document.createElement('canvas');
|
|
8
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
9
|
+
stage.append(canvas);
|
|
10
|
+
const gl = canvas.getContext('webgl2', { antialias: true });
|
|
11
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
12
|
+
const P = Math.max(1, Math.round(PARAMS.p)), Q = Math.max(1, Math.round(PARAMS.q)), SEG = 520, RAD = 28, tube = 0.34;
|
|
13
|
+
const curve = t => { const r = 2 + Math.cos(Q * t); return [r * Math.cos(P * t), r * Math.sin(P * t), Math.sin(Q * t) * 1.1].map(v => v * 0.55); };
|
|
14
|
+
const sub = (a, b) => a.map((v, i) => v - b[i]), nrm = a => { const l = Math.hypot(...a) || 1; return a.map(v => v / l); };
|
|
15
|
+
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]], dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
16
|
+
// parallel transport: carry the normal along, removing its component along each new tangent
|
|
17
|
+
const pts = [], T = [], N = [], B = [];
|
|
18
|
+
for (let i = 0; i <= SEG; i++) { const t = i / SEG * Math.PI * 2; pts.push(curve(t)); T.push(nrm(sub(curve(t + 1e-3), curve(t - 1e-3)))); }
|
|
19
|
+
let n = nrm(cross(T[0], [0, 0, 1]));
|
|
20
|
+
for (let i = 0; i <= SEG; i++) { n = nrm(sub(n, T[i].map(v => v * dot(n, T[i])))); N.push(n); B.push(cross(T[i], n)); }
|
|
21
|
+
// close the loop: spread the leftover twist evenly so the seam meets
|
|
22
|
+
const twist = Math.atan2(dot(cross(N[SEG], N[0]), T[0]), dot(N[SEG], N[0]));
|
|
23
|
+
const verts = [], idx = [];
|
|
24
|
+
for (let i = 0; i <= SEG; i++) {
|
|
25
|
+
const tw = twist * i / SEG;
|
|
26
|
+
for (let j = 0; j <= RAD; j++) {
|
|
27
|
+
const a = j / RAD * Math.PI * 2 + tw, c = Math.cos(a), s = Math.sin(a), d = N[i].map((v, k) => v * c + B[i][k] * s);
|
|
28
|
+
verts.push(pts[i][0] + d[0] * tube, pts[i][1] + d[1] * tube, pts[i][2] + d[2] * tube, d[0], d[1], d[2], i / SEG, j / RAD);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (let i = 0; i < SEG; i++) for (let j = 0; j < RAD; j++) { const a = i * (RAD + 1) + j, b = a + RAD + 1; idx.push(a, b, a + 1, b, b + 1, a + 1); }
|
|
32
|
+
|
|
33
|
+
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; };
|
|
34
|
+
const link = (v, f) => { const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, v)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, f)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p); return p; };
|
|
35
|
+
const prog = link(`#version 300 es
|
|
36
|
+
in vec3 pos, nor; in vec2 st; uniform mat4 PV, M; out vec3 vN, vW; out vec2 vS;
|
|
37
|
+
void main() { vec4 w = M * vec4(pos, 1); vW = w.xyz; vN = mat3(M) * nor; vS = st; gl_Position = PV * w; }`, `#version 300 es
|
|
38
|
+
precision highp float; in vec3 vN, vW; in vec2 vS; out vec4 o; uniform float T, F;
|
|
39
|
+
vec3 env(vec3 d) {
|
|
40
|
+
vec3 c = mix(vec3(.05, .05, .07), vec3(.5, .52, .58), smoothstep(-.3, .9, d.y));
|
|
41
|
+
c += vec3(1.) * 2. * smoothstep(.1, .0, abs(d.x + .3) - .25) * smoothstep(.55, .75, d.y);
|
|
42
|
+
c += vec3(1., .6, .35) * 1.2 * smoothstep(.08, .0, abs(d.z - .2) - .06) * smoothstep(-.2, .1, d.y) * smoothstep(.5, .2, d.y);
|
|
43
|
+
return c;
|
|
44
|
+
}
|
|
45
|
+
void main() {
|
|
46
|
+
vec3 n = normalize(vN), v = normalize(vec3(0, 0, 6.5) - vW), r = reflect(-v, n);
|
|
47
|
+
float fr = pow(1. - max(0., dot(n, v)), 2.);
|
|
48
|
+
vec3 film = .5 + .5 * cos(6.2832 * (fr * 1.4 + vec3(0., .33, .67) + vS.x * 2.)); // hue slides with the viewing angle
|
|
49
|
+
vec3 c = env(r) * mix(vec3(.9), film, .55);
|
|
50
|
+
float stripe = smoothstep(.45, .5, abs(fract(vS.x * 24. - T * .35 * F) - .5)); // stripes flowing along the knot
|
|
51
|
+
c *= .82 + .18 * stripe;
|
|
52
|
+
c += pow(max(0., dot(r, normalize(vec3(-.4, .8, .5)))), 60.) * .8;
|
|
53
|
+
o = vec4(pow(c / (1. + c * .35), vec3(1. / 1.8)), 1);
|
|
54
|
+
}`);
|
|
55
|
+
const bg = link(`#version 300 es
|
|
56
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, .999, 1); }`, `#version 300 es
|
|
57
|
+
precision highp float; in vec2 uv; out vec4 o; void main() { vec3 c = mix(vec3(.05, .05, .08), vec3(.16, .14, .2), uv.y); c += vec3(.25, .15, .3) * exp(-length(uv - vec2(.5, .55)) * 3.); o = vec4(c, 1); }`);
|
|
58
|
+
const quadVao = gl.createVertexArray(); gl.bindVertexArray(quadVao);
|
|
59
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
60
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
61
|
+
const vao = gl.createVertexArray(); gl.bindVertexArray(vao);
|
|
62
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
|
|
63
|
+
[['pos', 3, 0], ['nor', 3, 12], ['st', 2, 24]].forEach(([nm, s, o]) => { const l = gl.getAttribLocation(prog, nm); gl.enableVertexAttribArray(l); gl.vertexAttribPointer(l, s, gl.FLOAT, false, 32, o); });
|
|
64
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(idx), gl.STATIC_DRAW);
|
|
65
|
+
const uPV = gl.getUniformLocation(prog, 'PV'), uM = gl.getUniformLocation(prog, 'M'), uT = gl.getUniformLocation(prog, 'T'), uF = gl.getUniformLocation(prog, 'F');
|
|
66
|
+
// tiny matrix kit (column-major)
|
|
67
|
+
const mul = (a, b) => { const o = new Float32Array(16); for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) { let s = 0; for (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k]; o[c * 4 + r] = s; } return o; };
|
|
68
|
+
const persp = (fov, asp, nr, f) => { const t = 1 / Math.tan(fov / 2); return new Float32Array([t / asp, 0, 0, 0, 0, t, 0, 0, 0, 0, (f + nr) / (nr - f), -1, 0, 0, 2 * f * nr / (nr - f), 0]); };
|
|
69
|
+
const rotY = a => new Float32Array([Math.cos(a), 0, -Math.sin(a), 0, 0, 1, 0, 0, Math.sin(a), 0, Math.cos(a), 0, 0, 0, 0, 1]);
|
|
70
|
+
const rotX = a => new Float32Array([1, 0, 0, 0, 0, Math.cos(a), Math.sin(a), 0, 0, -Math.sin(a), Math.cos(a), 0, 0, 0, 0, 1]);
|
|
71
|
+
const trans = z => new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, z, 1]);
|
|
72
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); canvas.width = stage.clientWidth * d | 0; canvas.height = stage.clientHeight * d | 0; };
|
|
73
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
74
|
+
let raf; const t0 = performance.now();
|
|
75
|
+
function frame(now) {
|
|
76
|
+
const t = (now - t0) / 1000, asp = canvas.width / canvas.height;
|
|
77
|
+
gl.viewport(0, 0, canvas.width, canvas.height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
78
|
+
gl.disable(gl.DEPTH_TEST); gl.useProgram(bg); gl.bindVertexArray(quadVao); gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
79
|
+
gl.enable(gl.DEPTH_TEST); gl.useProgram(prog); gl.bindVertexArray(vao);
|
|
80
|
+
const fit = Math.max(1, 1.25 / asp); // pull back on tall stages
|
|
81
|
+
gl.uniformMatrix4fv(uPV, false, mul(persp(0.7, asp, 0.1, 30), trans(-6.5 * fit)));
|
|
82
|
+
gl.uniformMatrix4fv(uM, false, mul(rotX(0.9 + Math.sin(t * 0.3) * 0.25), rotY(t * 0.4)));
|
|
83
|
+
gl.uniform1f(uT, t); gl.uniform1f(uF, PARAMS.flow);
|
|
84
|
+
gl.drawElements(gl.TRIANGLES, idx.length, gl.UNSIGNED_INT, 0);
|
|
85
|
+
raf = requestAnimationFrame(frame);
|
|
86
|
+
}
|
|
87
|
+
raf = requestAnimationFrame(frame);
|
|
88
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
89
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Volumetric clouds: the sky is filled with a density field (layers of 3D noise, shaped into a slab and eroded at the
|
|
2
|
+
// edges). Each pixel's ray walks through it, adding light and losing transparency as it goes (Beer's law). At each
|
|
3
|
+
// step a short second march toward the sun measures how much cloud is in the way, which gives the dark bellies and
|
|
4
|
+
// bright tops; a forward-scattering term makes edges glow when you look toward the sun. The wind slides it all along.
|
|
5
|
+
const PARAMS = { cover: 0.5, wind: 1 };
|
|
6
|
+
|
|
7
|
+
export default function (stage) {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
|
|
10
|
+
stage.append(canvas);
|
|
11
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
12
|
+
if (!gl) { stage.textContent = 'This needs WebGL2.'; return () => canvas.remove(); }
|
|
13
|
+
const vs = `#version 300 es
|
|
14
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
15
|
+
const fs = `#version 300 es
|
|
16
|
+
precision highp float; in vec2 uv; out vec4 o;
|
|
17
|
+
uniform vec2 R; uniform float T, COV, WIND;
|
|
18
|
+
float hash(vec3 p) { p = fract(p * .3183099 + .1); p *= 17.; return fract(p.x * p.y * p.z * (p.x + p.y + p.z)); }
|
|
19
|
+
float noise(vec3 x) { vec3 i = floor(x), f = fract(x); f = f * f * (3. - 2. * f);
|
|
20
|
+
return mix(mix(mix(hash(i), hash(i + vec3(1, 0, 0)), f.x), mix(hash(i + vec3(0, 1, 0)), hash(i + vec3(1, 1, 0)), f.x), f.y),
|
|
21
|
+
mix(mix(hash(i + vec3(0, 0, 1)), hash(i + vec3(1, 0, 1)), f.x), mix(hash(i + vec3(0, 1, 1)), hash(i + 1.), f.x), f.y), f.z); }
|
|
22
|
+
float density(vec3 p) {
|
|
23
|
+
vec3 q = p + vec3(T * .6 * WIND, 0, T * .25 * WIND);
|
|
24
|
+
float f = .5 * noise(q * .3) + .25 * noise(q * .62) + .125 * noise(q * 1.27) + .0625 * noise(q * 2.6) + .03 * noise(q * 5.3);
|
|
25
|
+
float slab = smoothstep(0., 1.2, p.y) * smoothstep(5., 2.8, p.y); // clouds live in a layer
|
|
26
|
+
return clamp((f - (.64 - COV * .2)) * 4.5 * slab, 0., 1.); // above the threshold: cloud; below: clear sky
|
|
27
|
+
}
|
|
28
|
+
vec3 aces(vec3 x) { return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.); }
|
|
29
|
+
void main() {
|
|
30
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
31
|
+
vec3 ro = vec3(0, -1.2, 0), rd = normalize(vec3(q.x, q.y * .75 + .78, 1.4)); // looking up into the layer
|
|
32
|
+
vec3 sd = normalize(vec3(.55, .32, .8));
|
|
33
|
+
float sun = max(dot(rd, sd), 0.);
|
|
34
|
+
vec3 sky = mix(vec3(.42, .6, .86), vec3(.06, .2, .55), clamp(rd.y * 1.2, 0., 1.)) + vec3(1., .85, .6) * (pow(sun, 600.) * 6. + pow(sun, 8.) * .25);
|
|
35
|
+
// march from where the ray enters the layer
|
|
36
|
+
float t0 = max(0., (0. - ro.y) / rd.y), t1 = (5. - ro.y) / max(rd.y, .03);
|
|
37
|
+
float t = t0 + .12 * fract(sin(dot(uv * R, vec2(12.9, 78.2))) * 43758.5), trans = 1.; vec3 light = vec3(0);
|
|
38
|
+
float hg = .75 * (1. + .8 * pow(sun, 6.)) + .5 * pow(sun, 30.); // brighter toward the sun: forward scattering
|
|
39
|
+
for (int i = 0; i < 64; i++) {
|
|
40
|
+
if (t > t1 || trans < .02) break;
|
|
41
|
+
vec3 p = ro + rd * t; float d = density(p);
|
|
42
|
+
if (d > .01) {
|
|
43
|
+
float sh = 0.; for (int j = 1; j <= 5; j++) sh += density(p + sd * float(j) * .35); // cloud toward the sun
|
|
44
|
+
vec3 lit = vec3(1., .95, .88) * exp(-sh * 1.6) * hg * 1.5 + vec3(.22, .3, .45) * (.3 + .7 * smoothstep(.0, 4., p.y));
|
|
45
|
+
float a = 1. - exp(-d * .9 * .25 / .25);
|
|
46
|
+
light += trans * a * lit; trans *= 1. - a;
|
|
47
|
+
}
|
|
48
|
+
t += .12 + t * .02;
|
|
49
|
+
}
|
|
50
|
+
vec3 col = sky * trans + light;
|
|
51
|
+
col = mix(col, sky, 1. - exp(-max(t0, 0.) * .02));
|
|
52
|
+
col = aces(col * 1.05);
|
|
53
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
54
|
+
}`;
|
|
55
|
+
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; };
|
|
56
|
+
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);
|
|
57
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
58
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
59
|
+
const U = {}; ['R', 'T', 'COV', 'WIND'].forEach(n => U[n] = gl.getUniformLocation(prog, n));
|
|
60
|
+
const resize = () => { const w = stage.clientWidth || 1, h = stage.clientHeight || 1, d = Math.min(devicePixelRatio, Math.max(0.6, Math.sqrt(190000 / (w * h)))); canvas.width = w * d | 0; canvas.height = h * d | 0; gl.viewport(0, 0, canvas.width, canvas.height); };
|
|
61
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
62
|
+
let raf; const t0 = performance.now();
|
|
63
|
+
function frame(now) {
|
|
64
|
+
gl.uniform2f(U.R, canvas.width, canvas.height); gl.uniform1f(U.T, (now - t0) / 1000); gl.uniform1f(U.COV, PARAMS.cover); gl.uniform1f(U.WIND, PARAMS.wind);
|
|
65
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
66
|
+
raf = requestAnimationFrame(frame);
|
|
67
|
+
}
|
|
68
|
+
raf = requestAnimationFrame(frame);
|
|
69
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
70
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Bioluminescent shore: a night beach where plankton (Noctiluca) flash blue whenever the water is disturbed. The glow
|
|
2
|
+
// lives in a texture laid over the beach in world space. Every frame it fades, it is carried up and back down the
|
|
3
|
+
// sand with the wash, and it is lit again wherever a wave breaks and wherever the pointer stirs the water. Each pixel
|
|
4
|
+
// casts a ray onto the beach to find where it looks, then reads the waves, the wet sand's sheen and the glow there.
|
|
5
|
+
const PARAMS = { waves: 1, glow: 1, sparkle: 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;touch-action:none';
|
|
10
|
+
stage.append(canvas);
|
|
11
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
12
|
+
if (!gl || !gl.getExtension('EXT_color_buffer_float')) { stage.textContent = 'This needs WebGL2 with float render targets.'; return () => canvas.remove(); }
|
|
13
|
+
const GS = 512; // glow texture: x −24..24 m, z 0..56 m from the camera
|
|
14
|
+
const COMMON = `
|
|
15
|
+
const float X0 = -24., X1 = 24., Z0 = 0., Z1 = 56.;
|
|
16
|
+
uniform float T, WV;
|
|
17
|
+
float h(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
18
|
+
float n2(vec2 p) { vec2 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f); return mix(mix(h(i), h(i + vec2(1, 0)), f.x), mix(h(i + vec2(0, 1)), h(i + 1.), f.x), f.y); }
|
|
19
|
+
float shore(float x) { return 9. + 1.6 * sin(x * .17 + 1.) + .8 * sin(x * .43); } // where the sand meets the sea (z)
|
|
20
|
+
// the wave train: crests run toward the beach; phase 0..1, crest at 0
|
|
21
|
+
float phase(vec2 w) { return fract((w.y - shore(w.x)) / 7. + T * .16 * WV + n2(vec2(w.x * .08, T * .05)) * .35); }
|
|
22
|
+
// how far up the sand the current wash has reached (it surges in after each break, then slides back)
|
|
23
|
+
float wash(float x) { float p = fract(T * .16 * WV + n2(vec2(x * .08, T * .05)) * .35); return shore(x) - 3.2 * sin(3.14159 * clamp(p * 1.6, 0., 1.)) * (.7 + .3 * n2(vec2(x * .2, floor(T * .16 * WV)))); }`;
|
|
24
|
+
const QUAD = `#version 300 es
|
|
25
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
26
|
+
// the glow, in world space: fade, ride the wash, light up at breaking crests and under the pointer
|
|
27
|
+
const glowFS = `#version 300 es
|
|
28
|
+
precision highp float; in vec2 uv; out vec4 o; uniform sampler2D G; uniform vec3 P; uniform float DT;
|
|
29
|
+
${COMMON}
|
|
30
|
+
void main() {
|
|
31
|
+
vec2 w = vec2(mix(X0, X1, uv.x), mix(Z0, Z1, uv.y));
|
|
32
|
+
float s = shore(w.x), ws = wash(w.x);
|
|
33
|
+
// the wash carries light up the sand and back: sample from slightly seaward (or landward) of here
|
|
34
|
+
float drift = w.y < s + 2. && w.y > ws - 1. ? (fract(T * .16 * WV) < .32 ? .9 : -.5) : 0.;
|
|
35
|
+
vec2 src = uv + vec2(0., drift * DT / (Z1 - Z0));
|
|
36
|
+
float g = texture(G, src).r * exp(-DT * 1.1);
|
|
37
|
+
float ph = phase(w), surf = smoothstep(s + 9., s + 2., w.y) * step(s - .3, w.y);
|
|
38
|
+
float crest = exp(-pow((ph - .02) / .018, 2.)) + exp(-pow((ph - 1.02) / .018, 2.)); // the line where the wave is breaking
|
|
39
|
+
g += crest * surf * (.15 + 1.3 * pow(n2(w * vec2(.9, 2.) + T * .7), 2.)) * DT * 9.; // breaking is patchy, so the line flickers along its length
|
|
40
|
+
if (w.y < s && w.y > ws) g += DT * 1.4 * n2(w * 2. + T * 3.) * smoothstep(ws, ws + .6, w.y); // the foam edge of the wash
|
|
41
|
+
float d = length((w - P.xy) * vec2(1., .7));
|
|
42
|
+
g += P.z * exp(-d * d * 3.) * DT * 6.; // a hand dragged through the water
|
|
43
|
+
o = vec4(min(g, 6.), 0, 0, 1);
|
|
44
|
+
}`;
|
|
45
|
+
// the picture
|
|
46
|
+
const showFS = `#version 300 es
|
|
47
|
+
precision highp float; in vec2 uv; out vec4 o; uniform sampler2D G; uniform vec2 R; uniform float CH, PITCH, FOC, K, SP;
|
|
48
|
+
${COMMON}
|
|
49
|
+
vec3 sky(vec3 d) {
|
|
50
|
+
vec3 c = mix(vec3(.007, .013, .026), vec3(.0008, .0016, .005), smoothstep(0., .5, d.y));
|
|
51
|
+
vec2 sp = vec2(atan(d.x, d.z), d.y) * 340., ci = floor(sp);
|
|
52
|
+
vec3 p3 = fract(vec3(ci.xyx) * .1031); p3 += dot(p3, p3.yzx + 33.33); float s = fract((p3.x + p3.y) * p3.z);
|
|
53
|
+
c += vec3(.75, .82, 1.) * step(.994, s) * (s - .994) * 120. * smoothstep(.45, .1, length(fract(sp) - .5));
|
|
54
|
+
float band = exp(-pow((d.y - .25 - d.x * .3) / .09, 2.)) * n2(vec2(d.x * 9., d.y * 14.)); // a faint Milky Way
|
|
55
|
+
return c + vec3(.012, .013, .02) * band;
|
|
56
|
+
}
|
|
57
|
+
void main() {
|
|
58
|
+
vec2 q = (uv * 2. - 1.) * vec2(R.x / R.y, 1.);
|
|
59
|
+
float cp = cos(PITCH), sp = sin(PITCH);
|
|
60
|
+
vec3 d = normalize(vec3(q.x, q.y * cp - FOC * sp, q.y * sp + FOC * cp));
|
|
61
|
+
vec3 col;
|
|
62
|
+
if (d.y > -.0005) col = sky(d);
|
|
63
|
+
else {
|
|
64
|
+
float t = CH / -d.y; vec2 w = vec2(d.x, d.z) * t; float dist = t;
|
|
65
|
+
float s = shore(w.x), ws = wash(w.x), ph = phase(w);
|
|
66
|
+
vec2 gt = vec2((w.x - X0) / (X1 - X0), (w.y - Z0) / (Z1 - Z0));
|
|
67
|
+
float g = texture(G, gt).r, gs = (texture(G, gt + vec2(.004, 0)).r + texture(G, gt - vec2(.004, 0)).r + texture(G, gt + vec2(0, .006)).r + texture(G, gt - vec2(0, .006)).r) * .25;
|
|
68
|
+
if (w.y > ws) { // sea (and the thin sheet of the wash)
|
|
69
|
+
float wave = sin(ph * 6.2832), slope = cos(ph * 6.2832);
|
|
70
|
+
float fres = .03 + .5 * pow(1. - abs(d.y), 6.);
|
|
71
|
+
vec3 refl = sky(reflect(d, normalize(vec3(n2(w * 1.3 + T) - .5, 6. + slope * 2., slope * 1.5))));
|
|
72
|
+
col = vec3(.001, .003, .007) + refl * fres * (w.y < s ? .5 : 1.);
|
|
73
|
+
col *= .8 + .2 * wave;
|
|
74
|
+
} else { // sand: wet and glossy near the water, dry further up
|
|
75
|
+
float wet = smoothstep(ws - 4., ws - .3, w.y);
|
|
76
|
+
col = mix(vec3(.0055, .005, .0045), vec3(.0025, .003, .0045), wet) * (.85 + .3 * n2(w * 6.));
|
|
77
|
+
col += sky(reflect(d, vec3(0, 1, 0))) * wet * .35;
|
|
78
|
+
}
|
|
79
|
+
// the plankton light: a broad glow plus sparks where it is strongest
|
|
80
|
+
float spark = step(1. - .04 * SP, h(floor(w * 7.) + floor(T * 12.))) * smoothstep(.3, .1, length(fract(w * 7.) - .5)) * g;
|
|
81
|
+
col += vec3(.05, .55, 1.) * (g * .5 + gs * .5) * K + vec3(.6, .95, 1.) * spark * .8 * K;
|
|
82
|
+
col = mix(col, vec3(.004, .008, .016), smoothstep(30., 56., dist)); // haze toward the horizon
|
|
83
|
+
}
|
|
84
|
+
col = col / (1. + col);
|
|
85
|
+
o = vec4(pow(col, vec3(1. / 2.2)), 1);
|
|
86
|
+
}`;
|
|
87
|
+
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; };
|
|
88
|
+
const prog = fs => {
|
|
89
|
+
const p = gl.createProgram(); gl.attachShader(p, sh(gl.VERTEX_SHADER, QUAD)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs)); gl.bindAttribLocation(p, 0, 'p'); gl.linkProgram(p);
|
|
90
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
91
|
+
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); }
|
|
92
|
+
return { p, u };
|
|
93
|
+
};
|
|
94
|
+
const glowP = prog(glowFS), showP = prog(showFS);
|
|
95
|
+
const quad = gl.createVertexArray(); gl.bindVertexArray(quad);
|
|
96
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
97
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
98
|
+
const target = () => {
|
|
99
|
+
const t = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, GS, GS, 0, gl.RGBA, gl.HALF_FLOAT, null);
|
|
100
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
101
|
+
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);
|
|
102
|
+
const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0);
|
|
103
|
+
gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); return { t, fb };
|
|
104
|
+
};
|
|
105
|
+
let A = target(), B = target();
|
|
106
|
+
let W = 1, H = 1;
|
|
107
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = canvas.width = Math.max(1, (stage.clientWidth * d) | 0); H = canvas.height = Math.max(1, (stage.clientHeight * d) | 0); };
|
|
108
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
109
|
+
|
|
110
|
+
// camera: standing on the beach, 2.2 m up, looking out to sea and a little down
|
|
111
|
+
const CH = 2.2, PITCH = 0.2, FOC = 2.2;
|
|
112
|
+
const ptr = { x: 0, z: 0, s: 0, on: false };
|
|
113
|
+
const move = e => {
|
|
114
|
+
const r = canvas.getBoundingClientRect(), qx = (((e.clientX - r.left) / r.width) * 2 - 1) * (r.width / r.height), qy = 1 - ((e.clientY - r.top) / r.height) * 2;
|
|
115
|
+
const cp = Math.cos(PITCH), sp = Math.sin(PITCH), dy = qy * cp - FOC * sp, dz = qy * sp + FOC * cp;
|
|
116
|
+
if (dy >= -0.001) { ptr.on = false; return; }
|
|
117
|
+
const t = CH / -dy; ptr.x = qx * t; ptr.z = dz * t; ptr.on = true;
|
|
118
|
+
};
|
|
119
|
+
const leave = () => { ptr.on = false; };
|
|
120
|
+
canvas.addEventListener('pointermove', move); canvas.addEventListener('pointerleave', leave);
|
|
121
|
+
|
|
122
|
+
let raf, last = performance.now(), t = 0;
|
|
123
|
+
function frame(now) {
|
|
124
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt;
|
|
125
|
+
ptr.s += ((ptr.on ? 1 : 0) - ptr.s) * Math.min(1, dt * 8);
|
|
126
|
+
gl.bindVertexArray(quad);
|
|
127
|
+
gl.useProgram(glowP.p); gl.bindFramebuffer(gl.FRAMEBUFFER, B.fb); gl.viewport(0, 0, GS, GS);
|
|
128
|
+
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, A.t); gl.uniform1i(glowP.u.G, 0);
|
|
129
|
+
gl.uniform1f(glowP.u.T, t); gl.uniform1f(glowP.u.WV, PARAMS.waves); gl.uniform1f(glowP.u.DT, dt); gl.uniform3f(glowP.u.P, ptr.x, ptr.z, ptr.s);
|
|
130
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3); [A, B] = [B, A];
|
|
131
|
+
gl.useProgram(showP.p); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, W, H);
|
|
132
|
+
gl.bindTexture(gl.TEXTURE_2D, A.t); gl.uniform1i(showP.u.G, 0); gl.uniform2f(showP.u.R, W, H);
|
|
133
|
+
gl.uniform1f(showP.u.T, t); gl.uniform1f(showP.u.WV, PARAMS.waves); gl.uniform1f(showP.u.CH, CH); gl.uniform1f(showP.u.PITCH, PITCH); gl.uniform1f(showP.u.FOC, FOC);
|
|
134
|
+
gl.uniform1f(showP.u.K, PARAMS.glow); gl.uniform1f(showP.u.SP, PARAMS.sparkle);
|
|
135
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
136
|
+
raf = requestAnimationFrame(frame);
|
|
137
|
+
}
|
|
138
|
+
raf = requestAnimationFrame(frame);
|
|
139
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('pointermove', move); canvas.removeEventListener('pointerleave', leave); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); };
|
|
140
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Calligraphy: a brush writes 永 (eternity), the character students practise because it holds the eight basic
|
|
2
|
+
// strokes. The brush is forty bristles in a teardrop footprint that turns with the stroke and grows with pressure.
|
|
3
|
+
// Each bristle carries its own ink and lays it down as it moves, so as a stroke runs long the outer bristles run dry
|
|
4
|
+
// first and leave the streaked "flying white" (飛白). Where the brush slows, at a turn or the end of a stroke, ink
|
|
5
|
+
// pools darker; a quick flick lifts it to a sharp point. Then the seal goes on, and a fresh sheet.
|
|
6
|
+
const PARAMS = { pressure: 1, ink: 1, speed: 1 };
|
|
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;background:#efe9dc';
|
|
11
|
+
stage.append(canvas);
|
|
12
|
+
const ctx = canvas.getContext('2d');
|
|
13
|
+
let W = 1, H = 1, paper = null, ink = null, ig = null, box = { x: 0, y: 0, s: 1 };
|
|
14
|
+
const resize = () => {
|
|
15
|
+
const d = Math.min(devicePixelRatio, 2); W = stage.clientWidth || 1; H = stage.clientHeight || 1;
|
|
16
|
+
canvas.width = Math.max(1, (W * d) | 0); canvas.height = Math.max(1, (H * d) | 0); ctx.setTransform(d, 0, 0, d, 0, 0);
|
|
17
|
+
// rice paper: warm, a little uneven, with fibres
|
|
18
|
+
paper = document.createElement('canvas'); paper.width = canvas.width; paper.height = canvas.height;
|
|
19
|
+
const g = paper.getContext('2d'); g.setTransform(d, 0, 0, d, 0, 0); g.fillStyle = '#efe9dc'; g.fillRect(0, 0, W, H);
|
|
20
|
+
for (let i = 0; i < W * H / 40; i++) { g.fillStyle = `rgba(${Math.random() < 0.5 ? '160,140,110' : '255,252,245'},${Math.random() * 0.05})`; g.fillRect(Math.random() * W, Math.random() * H, 1 + Math.random() * 2, 1 + Math.random() * 2); }
|
|
21
|
+
g.lineWidth = 0.6; for (let i = 0; i < W * H / 900; i++) { const x = Math.random() * W, y = Math.random() * H, a = Math.random() * 6.28, l = 6 + Math.random() * 18; g.strokeStyle = `rgba(150,130,100,${0.05 + Math.random() * 0.07})`; g.beginPath(); g.moveTo(x, y); g.quadraticCurveTo(x + Math.cos(a + 0.5) * l * 0.5, y + Math.sin(a + 0.5) * l * 0.5, x + Math.cos(a) * l, y + Math.sin(a) * l); g.stroke(); }
|
|
22
|
+
ink = document.createElement('canvas'); ink.width = canvas.width; ink.height = canvas.height; ig = ink.getContext('2d'); ig.setTransform(d, 0, 0, d, 0, 0);
|
|
23
|
+
const s = Math.min(W, H) * 0.78; box = { x: (W - s) / 2 - s * 0.05, y: (H - s) / 2, s };
|
|
24
|
+
restart();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// 永, stroke by stroke: points are [x, y, pressure] in a unit square, y downward
|
|
28
|
+
const STROKES = [
|
|
29
|
+
[[0.46, 0.06, 0.25], [0.5, 0.11, 0.85], [0.54, 0.17, 0.75], [0.53, 0.19, 0.2]], // 點 the dot
|
|
30
|
+
[[0.31, 0.29, 0.5], [0.42, 0.275, 0.55], [0.53, 0.265, 0.6], [0.56, 0.3, 0.95], [0.545, 0.45, 0.7], [0.545, 0.68, 0.72], [0.55, 0.86, 0.85], [0.52, 0.9, 0.7], [0.44, 0.84, 0.05]], // 橫折鉤 across, down, hook
|
|
31
|
+
[[0.17, 0.46, 0.45], [0.3, 0.45, 0.5], [0.4, 0.44, 0.7], [0.36, 0.52, 0.65], [0.27, 0.63, 0.5], [0.12, 0.76, 0.06]], // 橫撇 across, then sweep down-left
|
|
32
|
+
[[0.81, 0.36, 0.7], [0.74, 0.43, 0.6], [0.65, 0.5, 0.35], [0.61, 0.53, 0.08]], // 撇 the short throw
|
|
33
|
+
[[0.6, 0.5, 0.35], [0.68, 0.6, 0.55], [0.77, 0.72, 0.85], [0.86, 0.8, 1], [0.92, 0.82, 0.55], [0.99, 0.83, 0.04]], // 捺 the long press and release
|
|
34
|
+
];
|
|
35
|
+
const cat = (a, b, c, d, t) => 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t * t + (-a + 3 * b - 3 * c + d) * t * t * t);
|
|
36
|
+
// sample each stroke densely, with a little hand wobble so no two sheets are the same
|
|
37
|
+
let paths = [];
|
|
38
|
+
function prepare() {
|
|
39
|
+
paths = STROKES.map(k => {
|
|
40
|
+
const pts = k.map(([x, y, p]) => [x + (Math.random() - 0.5) * 0.012, y + (Math.random() - 0.5) * 0.012, p]);
|
|
41
|
+
const out = [];
|
|
42
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
43
|
+
const a = pts[Math.max(0, i - 1)], b = pts[i], c = pts[i + 1], d = pts[Math.min(pts.length - 1, i + 2)];
|
|
44
|
+
for (let s = 0; s < 24; s++) { const t = s / 24; out.push([cat(a[0], b[0], c[0], d[0], t), cat(a[1], b[1], c[1], d[1], t), Math.max(0.02, cat(a[2], b[2], c[2], d[2], t))]); }
|
|
45
|
+
}
|
|
46
|
+
out.push(pts[pts.length - 1]);
|
|
47
|
+
let L = 0; for (let i = 1; i < out.length; i++) { L += Math.hypot(out[i][0] - out[i - 1][0], out[i][1] - out[i - 1][1]); out[i].push(L); } out[0].push(0);
|
|
48
|
+
return out;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const NB = 40;
|
|
52
|
+
const bristles = Array.from({ length: NB }, (_, i) => {
|
|
53
|
+
const a = Math.random() * 6.283, r = Math.sqrt(Math.random());
|
|
54
|
+
return { ox: Math.cos(a) * r, oy: Math.sin(a) * r, edge: r, ink: 1, px: 0, py: 0, on: false, w: 0.7 + Math.random() * 0.6 };
|
|
55
|
+
});
|
|
56
|
+
let si = 0, sd = 0, state = 'write', stateT = 0, seal = 0;
|
|
57
|
+
function restart() { ig.clearRect(0, 0, W, H); prepare(); si = 0; sd = 0; state = 'write'; stateT = 0; seal = 0; dip(); }
|
|
58
|
+
function dip() { for (const b of bristles) { b.ink = PARAMS.ink * (1.1 - b.edge * 0.35) * (0.85 + Math.random() * 0.3); b.on = false; } }
|
|
59
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
60
|
+
|
|
61
|
+
// where the brush is along the current stroke, at arc length s
|
|
62
|
+
const at = (path, s) => {
|
|
63
|
+
let i = 1; while (i < path.length - 1 && path[i][3] < s) i++;
|
|
64
|
+
const a = path[i - 1], b = path[i], k = Math.min(1, Math.max(0, (s - a[3]) / Math.max(1e-6, b[3] - a[3])));
|
|
65
|
+
return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k, Math.atan2(b[1] - a[1], b[0] - a[0])];
|
|
66
|
+
};
|
|
67
|
+
function brushTo(x, y, p, dir, speed) {
|
|
68
|
+
const R = box.s * 0.068 * p * PARAMS.pressure, X = box.x + x * box.s, Y = box.y + y * box.s;
|
|
69
|
+
const c = Math.cos(dir), s = Math.sin(dir), slow = Math.min(2.2, 0.35 / Math.max(0.05, speed)); // slow brush: more ink soaks in
|
|
70
|
+
for (const b of bristles) {
|
|
71
|
+
// the footprint is a teardrop trailing behind the tip: long along the stroke, as wide as the pressure
|
|
72
|
+
const along = b.ox * R * 0.75 - R * 0.25, across = b.oy * R;
|
|
73
|
+
const bx = X + c * along - s * across, by = Y + s * along + c * across;
|
|
74
|
+
if (b.on && b.ink > 0.02) {
|
|
75
|
+
const dist = Math.hypot(bx - b.px, by - b.py), a = Math.min(0.9, b.ink * 0.5 * (0.6 + 0.4 * slow));
|
|
76
|
+
ig.strokeStyle = `rgba(18,16,17,${a})`; ig.lineWidth = Math.max(0.8, R * 0.2 * b.w);
|
|
77
|
+
ig.beginPath(); ig.moveTo(b.px, b.py); ig.lineTo(bx, by); ig.stroke();
|
|
78
|
+
if (b.ink > 0.4 && Math.random() < 0.15) { ig.fillStyle = `rgba(18,16,17,${0.04 * slow})`; ig.beginPath(); ig.arc(bx, by, R * 0.35, 0, 7); ig.fill(); } // wet ink feathers into the paper
|
|
79
|
+
b.ink -= dist / box.s * (0.45 + b.edge * 1.5) * (0.8 + Math.random() * 0.4); // edge bristles run dry first
|
|
80
|
+
}
|
|
81
|
+
b.px = bx; b.py = by; b.on = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let raf, last = performance.now();
|
|
86
|
+
function frame(now) {
|
|
87
|
+
const dt = Math.min(0.05, (now - last) / 1000) * PARAMS.speed; last = now; stateT += dt;
|
|
88
|
+
if (state === 'write') {
|
|
89
|
+
const path = paths[si], L = path[path.length - 1][3];
|
|
90
|
+
// speed along the stroke: settle in slowly, run through the middle, slow again into the end (except flicks)
|
|
91
|
+
const u = sd / L, flick = path[path.length - 1][2] < 0.1;
|
|
92
|
+
const v = 0.35 * (0.35 + Math.sin(Math.PI * Math.min(1, u * 1.05)) * (flick && u > 0.7 ? 2.6 : 1.4));
|
|
93
|
+
const steps = 6, ds = (v * dt) / steps;
|
|
94
|
+
for (let k = 0; k < steps && sd < L; k++) { sd = Math.min(L, sd + ds); const [x, y, p, dir] = at(path, sd); brushTo(x, y, p, dir, v); }
|
|
95
|
+
if (sd >= L) { si++; sd = 0; stateT = 0; for (const b of bristles) b.on = false; if (si >= paths.length) { state = 'seal'; } else { state = 'lift'; dip(); } } // back to the inkstone between strokes
|
|
96
|
+
} else if (state === 'lift' && stateT > 0.32) { state = 'write'; stateT = 0; }
|
|
97
|
+
else if (state === 'seal') { seal = Math.min(1, stateT / 0.25); if (stateT > 4.2) { state = 'fade'; stateT = 0; } }
|
|
98
|
+
else if (state === 'fade' && stateT > 1) restart();
|
|
99
|
+
|
|
100
|
+
ctx.globalAlpha = 1; ctx.drawImage(paper, 0, 0, W, H);
|
|
101
|
+
ctx.globalAlpha = state === 'fade' ? 1 - stateT : 1; ctx.drawImage(ink, 0, 0, W, H);
|
|
102
|
+
if (seal > 0) { // the red seal, pressed in at the lower right
|
|
103
|
+
const s = box.s * 0.1, x = box.x + box.s * 1.13, y = box.y + box.s * 0.95, k = 1 + (1 - seal) * 0.25;
|
|
104
|
+
ctx.save(); ctx.translate(x, y); ctx.scale(k, k); ctx.globalAlpha *= seal * 0.92;
|
|
105
|
+
ctx.fillStyle = '#b3261e'; ctx.beginPath(); ctx.roundRect(-s / 2, -s / 2, s, s, s * 0.08); ctx.fill();
|
|
106
|
+
ctx.strokeStyle = '#efe9dc'; ctx.lineWidth = s * 0.07; ctx.lineCap = 'square';
|
|
107
|
+
const g = [[-0.3, -0.3, 0.3, -0.3], [0, -0.3, 0, 0.32], [-0.3, 0, 0.3, 0], [-0.3, 0.32, -0.05, 0.12], [0.3, 0.32, 0.05, 0.12]]; // carved lines
|
|
108
|
+
ctx.beginPath(); for (const [a, b, c, d] of g) { ctx.moveTo(a * s, b * s); ctx.lineTo(c * s, d * s); } ctx.stroke();
|
|
109
|
+
ctx.restore();
|
|
110
|
+
}
|
|
111
|
+
ctx.globalAlpha = 1;
|
|
112
|
+
raf = requestAnimationFrame(frame);
|
|
113
|
+
}
|
|
114
|
+
raf = requestAnimationFrame(frame);
|
|
115
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.remove(); };
|
|
116
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Chladni plate: a square brass plate bolted at its centre to a speaker, with sixty-five thousand grains of sand on
|
|
2
|
+
// it. The tone sweeps slowly upward. Near each of the plate's resonant frequencies one standing wave dominates; the
|
|
3
|
+
// plate buzzes everywhere except along its nodal lines, so the sand is bounced off the moving parts and comes to rest
|
|
4
|
+
// on the still ones, drawing the figure Ernst Chladni showed in 1787. Between resonances two modes mix and the sand
|
|
5
|
+
// drifts from one figure toward the next. The grains live on the GPU: each frame every one hops by however hard its
|
|
6
|
+
// spot is shaking, and slides a little downhill toward stillness.
|
|
7
|
+
const PARAMS = { grains: 256, sweep: 1, shake: 1 };
|
|
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
|
+
const label = document.createElement('div');
|
|
13
|
+
label.style.cssText = 'position:absolute;left:14px;bottom:12px;font:500 12px/1.3 ui-monospace,SFMono-Regular,Menlo,monospace;color:rgba(235,225,205,.6);letter-spacing:.04em;pointer-events:none';
|
|
14
|
+
stage.append(canvas, label);
|
|
15
|
+
const gl = canvas.getContext('webgl2', { antialias: false });
|
|
16
|
+
if (!gl || !gl.getExtension('EXT_color_buffer_float')) { stage.textContent = 'This needs WebGL2 with float render targets.'; return () => canvas.remove(); }
|
|
17
|
+
const G = Math.max(64, Math.round(PARAMS.grains)), COUNT = G * G;
|
|
18
|
+
// the plate's modes: (n, m, sign), in rising frequency; frequency grows with n² + m²
|
|
19
|
+
const MODES = [[1, 2, -1], [2, 2, 1], [1, 3, 1], [2, 3, -1], [1, 4, -1], [3, 3, 1], [2, 4, 1], [3, 4, -1], [1, 5, 1], [2, 5, -1], [4, 4, 1], [3, 5, 1], [4, 5, -1], [2, 6, 1], [5, 5, 1], [4, 6, -1], [3, 7, 1], [5, 6, -1], [6, 6, 1], [5, 7, 1]];
|
|
20
|
+
const FREQ = MODES.map(([n, m]) => 62 * (n * n + m * m));
|
|
21
|
+
const K = MODES.length;
|
|
22
|
+
const WAVE = `
|
|
23
|
+
uniform vec3 MD[${K}]; uniform float WT[${K}];
|
|
24
|
+
// the plate's shape right now: each mode's standing wave, weighted by how close the tone is to its resonance
|
|
25
|
+
float wave(vec2 p) { // p in −1..1
|
|
26
|
+
float s = 0.;
|
|
27
|
+
for (int k = 0; k < ${K}; k++) {
|
|
28
|
+
if (WT[k] < .003) continue;
|
|
29
|
+
vec2 a = MD[k].xy * 1.5707963;
|
|
30
|
+
s += WT[k] * (cos(a.x * p.x) * cos(a.y * p.y) + MD[k].z * cos(a.y * p.x) * cos(a.x * p.y));
|
|
31
|
+
}
|
|
32
|
+
return s;
|
|
33
|
+
}`;
|
|
34
|
+
const QUAD = `#version 300 es
|
|
35
|
+
in vec2 p; out vec2 uv; void main() { uv = p * .5 + .5; gl_Position = vec4(p, 0, 1); }`;
|
|
36
|
+
// the sand: hop by the shaking, slide toward stillness, stay on the plate
|
|
37
|
+
const sandFS = `#version 300 es
|
|
38
|
+
precision highp float; in vec2 uv; out vec4 o; uniform sampler2D S; uniform float F, SH;
|
|
39
|
+
${WAVE}
|
|
40
|
+
vec2 h2(vec2 p) { vec3 q = fract(vec3(p.xyx) * vec3(.1031, .103, .0973)); q += dot(q, q.yzx + 33.33); return fract((q.xx + q.yz) * q.zy); }
|
|
41
|
+
void main() {
|
|
42
|
+
vec2 p = texture(S, uv).xy;
|
|
43
|
+
float a = abs(wave(p)), e = .004;
|
|
44
|
+
vec2 g = vec2(abs(wave(p + vec2(e, 0))) - abs(wave(p - vec2(e, 0))), abs(wave(p + vec2(0, e))) - abs(wave(p - vec2(0, e)))) / (2. * e);
|
|
45
|
+
vec2 r = h2(gl_FragCoord.xy + F * 13.7) - .5;
|
|
46
|
+
p += r * (min(a, 1.4) * .02 + .0035) * SH - g * .0007 * SH; // a floor of jitter: real grains have size, so lines have width
|
|
47
|
+
p = clamp(p, -.985, .985);
|
|
48
|
+
o = vec4(p, 0, 1);
|
|
49
|
+
}`;
|
|
50
|
+
// the plate: brushed brass under a soft studio light, with a bevelled edge and the bolt at its heart
|
|
51
|
+
const plateFS = `#version 300 es
|
|
52
|
+
precision highp float; in vec2 uv; out vec4 o; uniform vec2 R; uniform float PS, T;
|
|
53
|
+
${WAVE}
|
|
54
|
+
float h(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
55
|
+
float n2(vec2 p) { vec2 i = floor(p), f = fract(p); f = f * f * (3. - 2. * f); return mix(mix(h(i), h(i + vec2(1, 0)), f.x), mix(h(i + vec2(0, 1)), h(i + 1.), f.x), f.y); }
|
|
56
|
+
void main() {
|
|
57
|
+
vec2 q = (uv * R - R * .5) / (PS * .5); // plate units: −1..1 across the plate
|
|
58
|
+
vec3 bg = vec3(.07, .068, .066) * (1. - .45 * length(uv - .5));
|
|
59
|
+
vec2 sd = abs(q - vec2(.035, -.05)) - 1.; float shadow = smoothstep(.12, -.02, max(sd.x, sd.y));
|
|
60
|
+
bg *= 1. - .6 * shadow;
|
|
61
|
+
float edge = max(abs(q.x), abs(q.y));
|
|
62
|
+
vec3 col = bg;
|
|
63
|
+
if (edge < 1.) {
|
|
64
|
+
float brushed = n2(vec2(q.x * 3., q.y * 260.)) * .5 + n2(vec2(q.x * 9., q.y * 900.)) * .5;
|
|
65
|
+
vec3 brass = vec3(.74, .55, .27) * (.82 + .25 * brushed);
|
|
66
|
+
float light = .55 + .45 * smoothstep(1.6, -.6, length(q - vec2(-.5, .55))); // a big soft light from the upper left
|
|
67
|
+
float sheen = exp(-pow((q.x + q.y * .6 - .35) * 1.8, 2.)) * .35; // and its long reflection
|
|
68
|
+
col = brass * light + vec3(1., .86, .6) * sheen;
|
|
69
|
+
col *= 1. + .05 * abs(wave(q)) * sin(T * 90.); // the buzz, barely visible
|
|
70
|
+
float bevel = smoothstep(.975, 1., edge);
|
|
71
|
+
col = mix(col, col * .55, bevel) + vec3(1., .9, .7) * .25 * smoothstep(.96, .975, edge) * (1. - bevel);
|
|
72
|
+
float bolt = length(q);
|
|
73
|
+
col = mix(col, vec3(.2, .19, .18), smoothstep(.055, .045, bolt));
|
|
74
|
+
col += vec3(.7) * smoothstep(.045, .03, length(q - vec2(-.012, .014))) * smoothstep(.0, .03, bolt) * .4;
|
|
75
|
+
}
|
|
76
|
+
o = vec4(pow(col, vec3(.95)), 1);
|
|
77
|
+
}`;
|
|
78
|
+
// each grain: a little sand-coloured disc with its shadow drawn first, so the pattern has relief
|
|
79
|
+
const grainVS = `#version 300 es
|
|
80
|
+
uniform sampler2D S; uniform int G; uniform vec2 R; uniform float PS, SZ, SHADOW;
|
|
81
|
+
out float k;
|
|
82
|
+
void main() {
|
|
83
|
+
int i = gl_VertexID; vec2 p = texelFetch(S, ivec2(i % G, i / G), 0).xy;
|
|
84
|
+
vec2 px = p * PS * .5 + SHADOW * vec2(.9, -1.1);
|
|
85
|
+
gl_Position = vec4(px / (R * .5), 0, 1); gl_PointSize = SZ; k = fract(sin(float(i) * 12.9898) * 43758.5453);
|
|
86
|
+
}`;
|
|
87
|
+
const grainFS = `#version 300 es
|
|
88
|
+
precision highp float; in float k; out vec4 o; uniform float SHADOW;
|
|
89
|
+
void main() { vec2 q = gl_PointCoord * 2. - 1.; if (dot(q, q) > 1.) discard;
|
|
90
|
+
o = SHADOW > 0. ? vec4(0, 0, 0, .18) : vec4(mix(vec3(.86, .8, .66), vec3(.98, .95, .86), k) * (1. - .25 * q.y * -1.), 1); }`;
|
|
91
|
+
|
|
92
|
+
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; };
|
|
93
|
+
const prog = (vs, fs) => {
|
|
94
|
+
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);
|
|
95
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
96
|
+
const u = {}; for (let i = 0, n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS); i < n; i++) { const nm = gl.getActiveUniform(p, i).name.replace('[0]', ''); u[nm] = gl.getUniformLocation(p, nm); }
|
|
97
|
+
return { p, u };
|
|
98
|
+
};
|
|
99
|
+
const sandP = prog(QUAD, sandFS), plateP = prog(QUAD, plateFS), grainP = prog(grainVS, grainFS);
|
|
100
|
+
const quad = gl.createVertexArray(); gl.bindVertexArray(quad);
|
|
101
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
102
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
103
|
+
const empty = gl.createVertexArray();
|
|
104
|
+
const init = new Float32Array(COUNT * 4); for (let i = 0; i < COUNT; i++) init.set([Math.random() * 1.96 - 0.98, Math.random() * 1.96 - 0.98, 0, 1], i * 4); // sprinkled evenly
|
|
105
|
+
const tgt = () => {
|
|
106
|
+
const t = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, G, G, 0, gl.RGBA, gl.FLOAT, init);
|
|
107
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
108
|
+
const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0); return { t, fb };
|
|
109
|
+
};
|
|
110
|
+
let A = tgt(), B = tgt();
|
|
111
|
+
let W = 1, H = 1;
|
|
112
|
+
const resize = () => { const d = Math.min(devicePixelRatio, 2); W = canvas.width = Math.max(1, (stage.clientWidth * d) | 0); H = canvas.height = Math.max(1, (stage.clientHeight * d) | 0); };
|
|
113
|
+
const ro = new ResizeObserver(resize); ro.observe(stage); resize();
|
|
114
|
+
|
|
115
|
+
const md = new Float32Array(K * 3); MODES.forEach((m, i) => md.set(m, i * 3));
|
|
116
|
+
const wt = new Float32Array(K);
|
|
117
|
+
let raf, last = performance.now(), t = 0, frameNo = 0, shown = '';
|
|
118
|
+
function frame(now) {
|
|
119
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; t += dt; frameNo++;
|
|
120
|
+
// the tone: a slow upward sweep that lingers near each resonance, then starts over
|
|
121
|
+
const span = FREQ.length, pos = (t * 0.11 * PARAMS.sweep + 7) % span, // start partway up, where the figures get intricate
|
|
122
|
+
k0 = Math.floor(pos), frac = pos - k0;
|
|
123
|
+
const linger = frac < 0.62 ? 0 : (frac - 0.62) / 0.38, ease = linger * linger * (3 - 2 * linger);
|
|
124
|
+
const f = FREQ[k0] * Math.pow(FREQ[(k0 + 1) % span] / FREQ[k0], k0 + 1 < span ? ease : 0);
|
|
125
|
+
let sum = 0; for (let k = 0; k < K; k++) { const x = (f - FREQ[k]) / (FREQ[k] * 0.06); wt[k] = 1 / (1 + x * x); sum += wt[k]; }
|
|
126
|
+
for (let k = 0; k < K; k++) wt[k] /= Math.max(1, sum * 0.85);
|
|
127
|
+
const top = wt.indexOf(Math.max(...wt));
|
|
128
|
+
const txt = `${Math.round(f).toLocaleString('en-US')} Hz mode ${MODES[top][0]},${MODES[top][1]}`;
|
|
129
|
+
if (txt !== shown) { label.textContent = txt; shown = txt; }
|
|
130
|
+
const PS = Math.min(W, H) * 0.86;
|
|
131
|
+
const setWave = P => { gl.uniform3fv(P.u.MD, md); gl.uniform1fv(P.u.WT, wt); };
|
|
132
|
+
gl.disable(gl.BLEND); gl.bindVertexArray(quad);
|
|
133
|
+
for (let s = 0; s < 2; s++) {
|
|
134
|
+
gl.useProgram(sandP.p); gl.bindFramebuffer(gl.FRAMEBUFFER, B.fb); gl.viewport(0, 0, G, G);
|
|
135
|
+
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, A.t); gl.uniform1i(sandP.u.S, 0); gl.uniform1f(sandP.u.F, (frameNo * 2 + s) % 997); gl.uniform1f(sandP.u.SH, PARAMS.shake); setWave(sandP);
|
|
136
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3); [A, B] = [B, A];
|
|
137
|
+
}
|
|
138
|
+
gl.useProgram(plateP.p); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, W, H);
|
|
139
|
+
gl.uniform2f(plateP.u.R, W, H); gl.uniform1f(plateP.u.PS, PS); gl.uniform1f(plateP.u.T, t); setWave(plateP);
|
|
140
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
141
|
+
gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
142
|
+
gl.useProgram(grainP.p); gl.bindVertexArray(empty); gl.bindTexture(gl.TEXTURE_2D, A.t);
|
|
143
|
+
gl.uniform1i(grainP.u.S, 0); gl.uniform1i(grainP.u.G, G); gl.uniform2f(grainP.u.R, W, H); gl.uniform1f(grainP.u.PS, PS);
|
|
144
|
+
const sz = Math.max(1.3, PS / 300);
|
|
145
|
+
gl.uniform1f(grainP.u.SZ, sz * 1.15); gl.uniform1f(grainP.u.SHADOW, sz); gl.drawArrays(gl.POINTS, 0, COUNT); // shadows
|
|
146
|
+
gl.uniform1f(grainP.u.SZ, sz); gl.uniform1f(grainP.u.SHADOW, 0); gl.drawArrays(gl.POINTS, 0, COUNT);
|
|
147
|
+
gl.disable(gl.BLEND);
|
|
148
|
+
raf = requestAnimationFrame(frame);
|
|
149
|
+
}
|
|
150
|
+
raf = requestAnimationFrame(frame);
|
|
151
|
+
return () => { cancelAnimationFrame(raf); ro.disconnect(); gl.getExtension('WEBGL_lose_context')?.loseContext(); canvas.remove(); label.remove(); };
|
|
152
|
+
}
|