@npmring/particle-wave
v1.5.0
Published
Interactive, physics-driven particle cloud engine for the browser. Loads a .pwcloud point cloud and renders it on a canvas with spring physics, hover forces, and click-driven wave fronts. Zero runtime dependencies.
Maintainers
Readme
@npmring/particle-wave
Interactive, physics-driven particle cloud for the browser. Loads a .pwcloud
point cloud, holds every particle on a spring at its rest position, and lets the
cursor and clicks disturb it. Canvas 2-D, ES module, zero runtime dependencies.
Point clouds are produced by the companion Python CLI,
particle-wave, which
turns any image into a weighted set of points.
Install
npm install @npmring/particle-waveUse
import ParticleWave from '@npmring/particle-wave';
import '@npmring/particle-wave/style.css';
const pw = await ParticleWave.init(document.getElementById('canvas'), {
src: '/assets/logo.pwcloud',
particleColor: '#7b93ff',
mouseMode: 'repel',
leftClickMode: 'outward_wave',
rightClickMode: 'inward_wave',
});
pw.setMode('attract');
pw.triggerWave({ x: 300, y: 200 });
pw.setConfig({ waveStrength: 200 });
pw.pause();
pw.resume();
pw.destroy();The canvas needs a CSS size; the engine sets the backing store itself and keeps
it in step with a ResizeObserver.
Changing shape
morphTo moves the cloud into another shape instead of replacing it. Each
particle is paired with a point of the new cloud along a shared space-filling
curve, so neighbours stay neighbours and the field deforms rather than
scrambles; the spring the particles are already under chases the result, and the
lag of that chase is what makes it look like the cloud is being pulled.
const pw = await ParticleWave.init(canvas, {
src: '/clouds/logo.pwcloud',
capacity: 8000, // the largest cloud this instance will ever show
});
await pw.morphTo('/clouds/orrery.pwcloud', { duration: 1400, stagger: 0.4 });Point counts do not have to match. A denser cloud fills spare capacity and is
thinned along the curve if there is none; a sparser one lets the surplus
dissolve in flight. Weight, radius, alpha and per-particle source colour travel
with position.
Set capacity at init if you intend to morph. Without it the buffers are
sized to the first cloud and a denser one is thinned to fit, which still works
and still looks right, only at the first cloud's density.
Calling morphTo again mid-transition takes over from wherever the field has
got to, so a visitor clicking quickly between subjects sees one continuous
motion instead of a queue. The promise resolves when the field lands;
pw:morphstart and pw:morphend fire on the canvas, and pw.isMorphing
reports the state.
Interaction model
Two distinct things happen on click, and the difference matters when tuning:
- A wave is a front that travels outward (or inward) from the click and
kicks each particle once as it passes.
waveSpeed,waveWidth,waveStrength,rippleCount. - A burst is a stationary radial field that holds everything inside its
radius for as long as the button is down.
burstRadiusScale,burstStopRadius,burstDuration,burstOutwardGain.
Wave fronts are drawn on the canvas as a soft additive band around a crisp core
line (clickWaveVisual*). Without that the wave is only visible through the
motion it imparts, which reads as the cloud twitching for no reason.
Types
particle-wave.d.ts ships with the package and declares the full config
surface. It is hand-maintained rather than generated, because the inferred types
are too narrow to be useful at the call site — DEFAULTS.src is null, so
TypeScript would infer src as null | undefined and reject the URL string the
engine actually requires.
Config schema
engine_fields.json describes every tunable option with its range, unit,
default, and help text, grouped for display. Import it to drive a settings panel
without restating the schema:
import fields from '@npmring/particle-wave/engine-fields.json' with { type: 'json' };Documentation
Full API, algorithm, and .pwcloud format specification:
https://github.com/CoronRing/ParticleWave
License
MIT
