Skip to content

Options reference

Every option is optional. Anything you omit falls back to the default, and nested groups deep-merge — passing { major: { size: 8 } } keeps every other major setting intact.

ts
new Stipple('#hero', { count: 4000, major: { size: 8 } });
stipple.setOptions({ jelly: { intensity: 0 } });

Top level

optiontypedefaultdescription
countnumber3500Major (morphing) particles. Set to 0 for an ambient-only field.
minorCountnumber260Ambient drift particles. These never morph.
mode'background' | 'container' | 'page''background'Canvas positioning and pointer scope. See Modes.
colorColorSpec'#5ec8f2'A CSS colour string, a ramp across the field, or the source SVG's own fills. See Colour.
minorColorColorSpec | nullnullAmbient layer colour. null inherits color. A ramp collapses to its from.
backgroundstring''CSS background applied to the canvas. Empty keeps it transparent.
opacitynumber1Global multiplier over every particle's alpha.
blend'normal' | 'additive''normal'additive makes overlapping particles glow. Best on dark backgrounds.
softnessnumber1.35Width of the glow halo around each particle. Higher is hazier, lower is tighter.
corenumber0.72How much of a hard bright centre each particle gets. 0 is pure haze, 1 is a solid dot with a faint halo.
dprnumber | 'auto''auto'Device pixel ratio. 'auto' reads devicePixelRatio.
maxDprnumber2Hard ceiling on DPR. The single biggest performance lever.
maxFpsnumber0Frame cap. 0 disables the cap and runs at display refresh.
autoPausebooleantrueStop the loop when the canvas is offscreen or the tab is hidden.
reducedMotion'respect' | 'ignore''respect'respect renders one static frame when the user prefers reduced motion.
adaptiveQualitybooleantrueDrop render resolution when frame time exceeds the budget.
assignAssignMode | AssignFn'angular'How particles pair with shape points.
behaviorsBehavior[] | nullnullReplace the simulation pipeline. null uses the defaults. Each entry may declare a phase.
backend() => SimulationBackend | nullnullSwap the simulation backend.
onReady(instance) => void | nullnullFires once the instance is constructed.
onError(error) => void | nullnullFires on WebGL2 failure and async shape-loading errors.

Modes

  • backgroundposition: fixed, covering the viewport. Pointer listeners attach to window. Use for full-page hero effects.
  • containerposition: absolute, filling the host element. Pointer listeners attach to the host, so multiple instances on one page stay independent. The host needs a non-static position and a real height.
  • pageposition: absolute at the top of the document, spanning the full page width. Combine with setPageHeight(px) to cover a scrolling region taller than the viewport.

major — the morphing pool

optiontypedefaultdescription
sizenumber6Base sprite diameter in CSS pixels, before DPR.
sizeVariationnumber0.85Per-particle size spread. 0 makes every particle identical.
sizeBiasnumber1.8Skews the size distribution. 1 is uniform; above 1 makes most particles small with a few large, which is what reads as a starfield.
follownumber0.1How hard particles chase their target once shaped. Higher is snappier.
followSpreadnumber0.016Same, while dispersed. Low values give a loose floating feel.
velocitynumber0.002Weight of residual per-particle velocity.
dampingnumber0.97Velocity retention while morphing.
twinklenumber0.18Brightness flicker amplitude when shaped.
depthnumber0.8How strongly z position scales size and brightness while dispersed.
settlenumber0.1Follow strength once fully morphed and undisturbed. A steady state, not a transition — which is why it lives here.

follow and followSpread are frame-rate normalised, so the motion looks the same at 60 Hz and 144 Hz.


assign — pairing particles with shape points

assign is a top-level option, not part of transition. It decides which target each particle gets, and it applies every time a shape is set — entering, or swapping one shape for another. The choreography decides how they travel; this decides where.

valuedescription
'angular'Sorts both the dispersed particles and the sampled shape points by angle around their centroids, then pairs them in order. Particles travel far shorter distances and the shape snaps into focus instead of churning. For two concentric rings this reaches the mathematically optimal pairing.
'index'Pairs by array order — cheapest, and useful when you generate shape points yourself in a meaningful sequence.
'random'Pairs each particle with an arbitrary shape point, so a particle on the left of the sphere may fly to the right of the shape. The result is a scramble.
AssignFnYour own pairing. Receives the sampled points and the spread positions, and writes targets into the output arrays.

transition — how a move is performed

A move between states is a choreography. There are three places one can run, and they all take the same shape:

slotwhen it runs
enterspread → shape
exitshape → spread. 'mirror' reuses enter at 70% of its speed.
swapshape → shape. 'none' retargets instantly, with no interpolation.
ts
stipple.setOptions({
  transition: {
    enter: 'sweep',
    exit: 'mirror',
    swap: 'burst',
  },
});

Named choreographies

namewhat it does
'condense'Centre-out, quick, no direction to notice. The default.
'uniform'Everything moves together. This is how you turn the wipe off.
'sweep'A directional wipe, left to right. Slower and more deliberate than condense.
'burst'Centre-out, fast out of the gate, with a flash on the wavefront.

Writing one out

Any slot also accepts an object. Anything you leave out falls back to the condense baseline.

optiontypedefaultdescription
speednumber0.05Rate the progress value approaches its target, per frame at 60fps. A morph completes in a little over a second.
easingEasing | EasingNameeaseOutExpoCurve applied to each particle's own flight.
staggernumber0.68How far apart launch times are pushed, 0 to 0.9.
order'random' | 'x' | 'y' | 'radial' | 'radar''radial'Which direction the wavefront travels. Ignored when stagger is 0.
turbulencenumber14Noise displacement while in flight, peaking mid-move and fading on arrival.
flashnumber0Brightness and size boost on particles the wavefront is crossing.
flashWidthnumber0.22Width of the flashing band, in progress units.

The sweep is stagger and order

Nothing else. order derives each particle's launch time from where it sits in the field; stagger decides how far apart those launch times are pushed. A wave then travels across the field in that direction, and each particle launches toward the shape as the wave reaches it.

The two numbers work against each other, which is the part worth internalising:

launch = delay * stagger        // when this particle leaves,  0 .. stagger
span   = 1 - stagger            // how long its own flight lasts

Push stagger up and launches spread wide while each flight gets short, so only a narrow band is ever moving — a crisp wipe. Pull it down and every particle is in flight almost the whole time, which reads as the whole field arriving at once no matter what order says. At the default 0.82 roughly a third of the field is moving at the busiest moment; at 0.38 it is effectively all of it.

order: 'x' is a left-to-right wipe, 'y' top-to-bottom, 'radial' reads as the shape condensing from the centre out, and 'radar' sweeps around like a radar hand.

order: 'radar' was called 'angular' before v0.2. It was renamed because assign: 'angular' is an unrelated mechanism, and having the same word mean two things in adjacent options was a trap.

flash is a separate, optional flourish layered on top, off by default. Turning it up makes the flash the thing you notice instead of the wipe, so reach for it only when you want that.

Swapping one shape for another

When a shape is already on screen and you set another, the swap choreography interpolates between them. The outgoing targets are held in their own buffer, so this is a real move rather than a retarget.

ts
await stipple.morphTo(logoA);
await stipple.morphTo(logoB); // swaps, using transition.swap
await stipple.morphTo(logoC, { swap: 'none' }); // instant retarget

A swap runs on its own clock and composes with the morph: you can swap while the field is only half-formed, and both progress independently.

Easing by name

easing accepts either a function or the name of a built-in:

ts
stipple.setOptions({ transition: { enter: { easing: 'outExpo' } } });

import { easeOutExpo } from 'stipple-gl';
stipple.setOptions({ transition: { enter: { easing: easeOutExpo } } });

Names: linear · inOutCubic · inOutQuad · outExpo · outBack · inOutElastic.

Prefer the name. It is shorter, it tree-shakes the same, and it is the only form that survives worker mode, where functions cannot cross the thread boundary.


Colour

color accepts three forms.

ts
color: '#5ec8f2'                                             // solid
color: { type: 'ramp', from: '#0ea5e9', to: '#f472b6', by: 'depth' }
color: { type: 'shape', fallback: '#5ec8f2' }                 // the SVG's own fills

A ramp spreads two colours across the field. by picks what drives the gradient:

bygradient runs
'depth'back to front through the sphere
'radius'centre outward
'index'in particle order

The ramp position is precomputed once per layout, so this costs three lerps per particle per frame and nothing else.

A shape source reads each particle's colour from the pixel it was sampled from, so the field takes on the artwork's own palette. fallback is used while dispersed and for any SVG that carries no fills — the colour fades from fallback into the sampled colour as the shape forms, and interpolates again across a swap.


spread — the dispersed state

optiontypedefaultdescription
radiusnumber0.62Sphere radius as a fraction of the canvas half-diagonal. Because density fades toward the edge, values above 1 still look natural.
flownumber0.0015Spatial frequency of the noise flow field. Higher is more turbulent.
breathenumber1Slow brightness pulsing. 0 disables it.
zoomnumber1.1Camera zoom while dispersed. Returns to 1 when shaped.
pan{ x, y }{ x: 0.02, y: -0.015 }Static camera offset while dispersed.
driftnumber0.02Speed of the slow automatic camera wander.
speednumber0.01How quickly the camera eases toward its target.
rotationnumber0.05Radians per second the dispersed sphere spins about its vertical axis. Negative reverses it; 0 holds it still.
tiltnumber0.16Fixed tilt of the spin axis, in radians. Without a tilt the rotation reads as flat.
volumenumber11 distributes particles evenly through the sphere's volume, so density fades smoothly to nothing at the edge. 0 places them on the shell, which produces a visible rim.

minor — the ambient layer

optiontypedefaultdescription
sizenumber3.4Base size.
sizeBiasnumber2.4Size distribution skew, as with major.sizeBias.
sizeJitternumber1Per-particle size randomisation.
sizeScalenumber1Multiplier applied at render time.
speednumber1Strength of the noise force.
turbulencenumber0.4Chaos in the flow field.
dragnumber0.99Velocity retention. Lower settles faster.
maxSpeednumber0.28Hard velocity clamp.
opacity{ x, y }{ x: 0.22, y: 1 }Alpha range. The distribution is skewed toward the minimum, so most particles are dim and a few are bright.
respawnChancenumber0.0005Per-particle chance per frame of teleporting elsewhere.

emission — sparks

optiontypedefaultdescription
enabledbooleantrueTurn the whole layer off.
maxnumber140Hard cap on live sparks.
lifespannumber62Base lifetime.
speednumber0.85Initial velocity multiplier.
ratenumber0.016Spawn probability per sampled source particle per frame.
burst[number, number][1, 2]Min and max sparks per spawn event.
spiralnumber0.0008Curl force that makes sparks orbit.
turbulencenumber0.003Noise applied to spark velocity.

Emission behaves differently by state: while shaped, sparks drift upward from the figure; while dispersed, they float outward slowly and live much longer.


pointer — interaction

optiontypedefaultdescription
enabledbooleantrueMaster switch. Disables listener registration entirely.
radiusnumber150Influence radius in CSS pixels.
forcenumber10Repulsion strength.
falloffnumber1.6Falloff exponent. Higher concentrates the force near the cursor.
pressnumber1.25Force multiplier while the pointer is down.
shockwavebooleantrueEmit an expanding ring on press.
shockwaveForcenumber14Ring displacement strength.
shockwaveSpeednumber0.18Ring expansion rate in px/ms.
shockwaveLifenumber1600Ring lifetime in ms.
shockwaveThicknessnumber110Ring band width in px.

Pointer forces apply only when the field is fully morphed — a dispersed cloud does not react. Hit-testing uses the shape's bounding box, so cost does not scale with particle count.

Fire a wave programmatically with stipple.pulse(x, y, strength).


jelly — wobble

optiontypedefaultdescription
intensitynumber2.4Displacement amplitude. 0 freezes the shape solid.
speednumber1.35Oscillation rate.

The wobble is depth-weighted, so particles further back move more, which reads as volume. It damps to 45% while the pointer is active so interaction stays legible.


ShapeConfig — what a shape carries

setShape and morphTo take a ShapeConfig. Build one with the helpers rather than by hand.

fieldtypedescription
pathsSVGPathData[]Vector geometry. Empty for a raster-backed shape.
imageImageSourceA raster source — ImageBitmap, <img>, <canvas>, <video>, OffscreenCanvas. Takes precedence over paths.
mask'alpha' | 'dark' | 'light'Which pixels of image count as ink. Default 'alpha'.
thresholdnumberCutoff for mask, 0..1. Defaults to 0.03 for alpha, 0.5 otherwise.
detail'uniform' | 'edges' | 'density'Where the particle budget goes. Default 'uniform'. Use 'edges' for flat-filled illustrations.
detailStrengthnumberHow hard detail is applied, 0..1. Default 0.85.
viewBoxstringSource coordinate system, for the vector route.
scalenumberSize relative to the canvas.
positionXYCentre, in 0..1 canvas coordinates.
countnumberCap on sampled points, clamped to the field's count.
colorstringTint the field takes on while morphed into this shape.

See images.md for the formats, the masks, and when SVG is rasterised.

Released under the MIT License.