A fixed Canvas 2D layer maintains hundreds of tiny drifting organisms and a handful of siphonophore colonies. On every frame, each organism’s brightness is calculated from its actual distance to the pointer. The colonies are rebuilt as moving point chains and joined with quadratic Bézier curves, so the forms keep soft, unrepeatable motion rather than travelling a fixed SVG path.
The same pointer coordinates drive the document mask. A second, on-demand Canvas 2D scope below the instruments turns the visitor into a patient observer: the photon-counter dial looks for two narrow emissions, rather than rewarding every wavelength with the same result.
index.html / distance falloff + Bézier chain
const distanceLight = (x, y, radius = 370) => {
const d = Math.hypot(x - state.x, y - state.y);
return Math.max(0, 1 - d / radius);
};
const l = distanceLight(p.x, p.y);
if (!l) return;
const color = p.violet ? '138,92,240' : '63,224,208';
ctx.fillStyle = `rgba(${color}, ${Math.pow(l, 1.85) * pulse})`;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length - 1; i++) {
const midX = (points[i].x + points[i + 1].x) / 2;
const midY = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, midX, midY);
}
ctx.stroke();
index.html / photon sweep detection
const returns = [
{ center: 470, span: 11, label: 'copepod return acquired' },
{ center: 495, span: 10, label: 'siphonophore return acquired' }
];
const signalAt = (wavelength) =>
returns.find((signal) => Math.abs(wavelength - signal.center) <= signal.span);
readout.textContent = signal
? `${wavelength} nm · ${signal.label}`
: `${wavelength} nm · water quiet · no usable return in this band`;