Text is rasterized once and sampled into points; each point springs home to its letter while the pointer pushes it away.
The word is drawn once to an offscreen 2D canvas and its opaque pixels are sampled at an even stride into about 9000 points, mapped to world units so the word spans 80% of the stage. Each point is a spring toward its home with damping on the CPU, and the pointer, projected onto the z=0 plane, pushes points outward with a quadratic falloff. Changing the word only reassigns homes, pairing particles and targets by x order so the morph travels short distances.
// sampling: even stride through the opaque pixels of the rasterized word
for (let i = 0; i < count; i += 1) {
const idx = Math.min(total - 1, Math.floor(((i + Math.random()) * total) / count));
out[i * 2] = (px[idx * 2] + Math.random() - RASTER_W / 2) * scale;
out[i * 2 + 1] = -(px[idx * 2 + 1] + Math.random() - h / 2) * scale;
}
// per frame: spring home, pointer push, damping
vx += (home[ix] - x) * k;
vy += (home[ix + 1] - y) * k;
if (pushing) {
const dx = x - ptr.x, dy = y - ptr.y;
const d2 = dx * dx + dy * dy;
if (d2 < r2) {
const d = Math.sqrt(d2) || 0.0001;
const f = 1 - d / radius;
vx += (dx / d) * force * f * f;
vy += (dy / d) * force * f * f;
}
}
vx *= damp; vy *= damp;
pos[ix] = x + vx * s; pos[ix + 1] = y + vy * s;