How it works
01 Decaying sample ring-buffer
Every pointermove event deposits a sample into a 16-slot ring-buffer,
a Float32Array of [x, y, strength] triples. Each RAF frame
subtracts a fixed decay from every sample's strength (~55 frames to zero, roughly
one second at 60 fps). Old slots naturally die out; new ones keep strength at 1.
No timestamps, no sorting, no GC pressure.
02 Metaball field in a single pass
The 16 samples are uploaded as a uniform vec3[16] array each frame.
The fragment shader computes a metaball field: for each pixel it sums the
radial kernel strength × R² / (d² + ε) over all 16 samples, then
soft-thresholds to a crisp iso-surface plus an outer glow. Two blobs connect when
their combined field exceeds the iso threshold. That's the classic metaball merge.
Color shifts teal → violet as the field weakens outward.
// 1. aging ring-buffer (JS, 16 samples × [x, y, strength])
const pts = new Float32Array(16 * 3);
let head = 0;
function deposit(x, y) {
pts[head*3] = x; pts[head*3+1] = y; pts[head*3+2] = 1.0;
head = (head + 1) % 16;
}
// each RAF frame: decay every sample toward zero
for (let i = 0; i < 16; i++) pts[i*3+2] = Math.max(0, pts[i*3+2] - 0.018);
// 2. metaball field (GLSL, one pass, no FBOs)
// R = 5.5% of canvas height; kernel: strength × R² / (d² + ε)
float R2 = pow(u_res.y * 0.055, 2.0);
float field = 0.0;
for (int i = 0; i < 16; i++) {
vec2 sp = vec2(u_pts[i].x, 1.0 - u_pts[i].y) * u_res; // UV → pixel
float d2 = dot(px - sp, px - sp);
field += u_pts[i].z * R2 / (d2 + R2 * 0.005);
}
float iso = smoothstep(0.65, 1.15, field); // crisp iso-surface
float glow = smoothstep(0.08, 0.85, field) * 0.55; // soft outer halo 03 Cheap, robust, no state machine
The whole thing is one fullscreen triangle pass: no render targets, no
history buffers, no multi-pass compositing. The glQuad harness
handles DPR scaling, canvas resize, off-screen pause, reduced-motion guard,
and teardown. This action adds ~50 lines of JS on top. Under prefers-reduced-motion the loop doesn't run and the canvas stays
dark. The effect is purely interactive, not decorative.
A self-contained Svelte action, <canvas use:fluidTrail>.
16-slot ring-buffer + one GLSL metaball pass. Open to borrow.