Documentation
Dotfield
Zero-dependency living stipple particle library. Install as an npm package, pick from thousands of named modes, and customize palette, density, speed, and motion.
Package dotfield · current API v1.1.0
Introduction
Dotfield draws a soft, film-like field of dots on a <canvas>.
Particles ride noise flow fields and geometric attractors. You select a
mode id from a large catalog, optionally override appearance
and intensity, and control the field with a small public API.
- Browser:
Dotfield.create(canvas, options) - Headless / tests:
Dotfield.createSimulation(options) - Catalog:
Dotfield.listModes()/getMode(id)
The demo site uses two independent fields: a fixed full-page backdrop and a large gallery preview that switches modes when you pick a card. Your app typically needs only one field.
Installation
Install Dotfield as a dependency:
npm install dotfield
Package page:
npmjs.com/package/dotfield.
Entry is main / exports → lib/dotfield.js.
CommonJS
const Dotfield = require('dotfield');
ES modules / bundlers
import Dotfield from 'dotfield';
// or
import * as Dotfield from 'dotfield';
Plain script tag (no bundler)
Point a script at the package main file (after install, typically
node_modules/dotfield/lib/dotfield.js). The library attaches
window.Dotfield.
<script src="node_modules/dotfield/lib/dotfield.js"></script>
<script>
const field = Dotfield.create(document.getElementById('dots'), {
mode: 'flow-calm-cw-mid',
fillParent: true,
});
</script>
Quick start
<canvas id="dots"></canvas>
<script src="node_modules/dotfield/lib/dotfield.js"></script>
<script>
const field = Dotfield.create(document.getElementById('dots'), {
mode: 'orbit-steady-cw-mid',
fillParent: true,
palette: 'terracotta',
background: 'cream',
speed: 1,
intensity: 1.1,
});
// Later…
field.setMode('vortex-brisk-ccw-fine');
field.setConfig({ palette: 'ocean', density: 1.2 });
// field.stop(); field.destroy();
</script>
Headless (Node tests, SSR-safe stepping without a real canvas):
const Dotfield = require('dotfield');
const sim = Dotfield.createSimulation({
width: 400,
height: 300,
count: 48,
seed: 42,
mode: 'wave-calm-cw-mid',
speed: 1.2,
});
for (let i = 0; i < 60; i++) sim.step(1 / 60);
const positions = sim.getPositions();
API reference
The public surface is intentionally small. Everything below is stable for
application code. Underscore-prefixed properties (e.g. _catalogLength)
are internal.
Dotfield.create(canvas, options?)
Creates a live animated field on a canvas element. Returns a field instance.
Throws if canvas is missing or not a canvas.
| Option | Type | Default | Description |
|---|---|---|---|
mode | string | flow-calm-cw-mid | Catalog mode id |
fillParent | boolean | contextual | true sizes to window; false sizes to parent/canvas box |
wrap | boolean | false | Loop particles from each edge to the opposite edge instead of bouncing |
wrapInset | number | 0 | Off-screen gutter used before wrapping to the opposite side; edge-tangent particles are gently carried through instead of forming border lanes |
width / height | number | — | Fixed logical size when set |
count | number | auto | Fixed particle count |
maxParticles / minParticles | number | 5200 / 1200 | Auto-density clamps |
density | number | 1 | Relative density when count is auto |
autoStart | boolean | true | Begin RAF loop immediately |
interactive | boolean | true | Master cursor on/off — set false to disable all pointer forces |
mouseMode | string | 'repel' | repel · attract · swirl · off |
mouseForce / mouseRadius | number | mode-tuned | Strength & pixel radius of cursor influence |
autoMouse | boolean | true | Re-tune radius/force on mode change & resize |
mouseSmooth | number | 0.12 | Pointer follow ease (0–1; higher = snappier) |
seed | number | random | Particle spawn seed |
paused | boolean | false | Start paused (simulation still can draw) |
palette / colors | see Config | mode default | Dot colors |
background / bg | see Config | mode default | Trail clear color |
speed, speedScale, intensity, … | number | mode | Motion overrides (see Customization) |
Dotfield.createSimulation(options?)
Pure simulation without canvas I/O. Ideal for unit tests and deterministic
stepping. Shares the same motion math as create.
const sim = Dotfield.createSimulation({
width: 320,
height: 240,
count: 32,
seed: 1,
mode: 'spiral-rush-cw-vast',
palette: 'ember',
});
sim.step(1 / 60);
sim.setMode('grid-calm-ccw-fine');
sim.setConfig({ speed: 2 });
const snap = sim.getSnapshot();
listModes() / getMode(id)
Dotfield.listModes() returns an array of:
{
id: string, // stable, e.g. "orbit-brisk-ccw-mid"
label: string, // human-readable
family: string, // e.g. "orbit"
familyLabel: string,
description: string
}
Dotfield.getMode(id) returns the same plus params
(motion parameters), or null if unknown.
Also available: Dotfield.families, Dotfield.palettes,
Dotfield.backgrounds, Dotfield.version.
Instance methods
| Method | Description |
|---|---|
setMode(id) | Switch catalog mode; keeps consumer overrides |
getMode() | Current mode id string |
getModeInfo() | Full mode metadata for current id |
setConfig(opts) | Merge customization overrides (see below) |
getConfig() | Mode, overrides, resolved palette/bg, count |
listModes() | Same as Dotfield.listModes() |
pulse() | Radial burst impulse |
getPositions() | Array of {x,y,vx,vy} |
getParticleCount() / getFps() | Diagnostics (canvas field) |
retuneMouse() | Force recompute of mode-tuned radius/force for current size |
getMouseSettings() | { mouseRadius, mouseForce, mouseMode, interactive, autoMouse, mouseSmooth } |
resize() | Recompute canvas size & density |
step(dt?) | Advance one tick (also on simulation) |
setConfig(options)
Merges overrides onto the current mode. Overrides persist across subsequent
setMode calls until you change them again.
field.setConfig({
mode: 'bloom-steady-cw-mid', // optional: also switches mode
palette: 'ocean', // or 'rainbow'
background: '#0c0c0e',
speedScale: 1.4,
intensity: 1.2,
density: 1.3,
// or count: 2000,
trail: false, // disable stain / motion smear
interactive: true, // false = fully disable cursor
mouseMode: 'swirl',
mouseForce: 2,
mouseRadius: 160,
mouseSmooth: 0.2,
autoMouse: false, // lock manual force/radius
});
Lifecycle
| Method | Description |
|---|---|
start() | Start animation loop (no-op if already running) |
stop() | Stop RAF / timeout loop |
destroy() | Stop, unbind listeners, clear particles |
pause() / resume() / togglePause() | Programmatic pause (no site UI button) |
isRunning() / isPaused() | State flags |
isAutoPaused() | Whether visibility/intersection safeguards currently suppress simulation |
Customization
Pass options at create time or via setConfig. Named palettes and
backgrounds ship with the library; you can also pass raw RGB or hex.
Colors
- Named palettes:
terracotta,ink,ocean,dusk,forest,ember,frost,gold,rose - Rainbow:
palette: 'rainbow'orrainbow: true— particles cycle through hues continuously (animated) - Custom:
[{ r, g, b }, …],['#c45c3e', …], or a single hex string - Backgrounds:
cream,paper,night,slate,mist,sand,ink, or hex /{r,g,b}
Motion & density
| Key | Effect |
|---|---|
speed | Absolute speed multiplier for the mode |
speedScale | Multiplies existing speed |
intensity | Scales noise amplitude and attractor pull |
scale | Noise spatial scale |
noiseAmp | Flow noise strength |
spin | 1 / -1 direction bias |
pull / morphBias | Attractor strength |
rings / clusters | Orbit / constellation structure |
waveFreq / waveAmp | Wave / ribbon parameters |
count | Exact particle count (respawns) |
density | Relative density when count is automatic |
quality | 'high' | 'medium' | 'low' — DPR, glow, particle defaults |
adaptiveQuality | Reduces draw work when measured FPS drops; simulation remains deterministic |
autoPause | Pauses animation work while the document is hidden or the canvas is offscreen |
glow | Soft radial glow highlights (costly on large fields) |
interactive | Enable pointer / mouse forces on the canvas |
Cursor interaction
Cursor behaviour is fully optional. When enabled, the field reacts to the pointer using canvas-local coordinates (embedded previews and full-bleed heroes). You can turn it off entirely, pick a force mode, lock or auto-tune radius/force, and control how snappy the follow feels.
Disable cursor interaction
Three equivalent ways to turn cursor forces off:
- Master switch:
interactive: false— no pointer tracking, no forces. Prefer this for pure decorative fields or reduced-motion-friendly pages. - Mode off:
mouseMode: 'off'(alias'none') — keeps listeners but applies zero force. - Zero force:
mouseForce: 0— same effect as mode off for force application.
// Decorative only — no cursor reaction
Dotfield.create(canvas, {
mode: 'serene-float-whisper-cw-soft',
fillParent: true,
interactive: false,
});
// Runtime toggle (e.g. accessibility control)
field.setConfig({ interactive: false });
field.setConfig({ interactive: true, mouseMode: 'repel' });
All cursor options
| Key | Default | Effect |
|---|---|---|
interactive | true | Master on/off. false disables pointer forces completely |
mouseMode | 'repel' | repel · attract · swirl (alias vortex) · off / none |
mouseForce | mode-tuned | Strength of the cursor influence. 0 = no push |
mouseRadius | mode-tuned | Pixel radius of influence — auto-scales to mode + canvas size |
autoMouse | true | When true, radius/force re-tune on setMode / resize |
mouseSmooth | 0.12 | Pointer follow ease (0–1). Low = soft lag, high = snappy, 1 = instant |
Radius and force are derived from the mode’s engine, speed, calm flag, and canvas
size (e.g. open flow/drift fields get a wider hand; dense
grid modes get a tighter one; calm modes get a gentler, smaller radius).
Setting mouseRadius or mouseForce via setConfig
locks auto-tune until the next setMode or
setConfig({ autoMouse: true }).
// Interactive swirl with manual lock
Dotfield.create(canvas, {
mode: 'orbit-steady-cw-mid',
fillParent: true,
interactive: true,
mouseMode: 'swirl',
mouseForce: 2.2,
mouseRadius: 180,
mouseSmooth: 0.2,
autoMouse: false, // keep fixed force/radius across modes
});
// Auto-fit radius to each mode
field.setConfig({ autoMouse: true });
field.setMode('serene-float-whisper-cw-soft'); // retunes radius
field.retuneMouse(); // force recompute for current size
field.getMouseSettings();
// → { mouseRadius, mouseForce, mouseMode, interactive, autoMouse, mouseSmooth }
// Soft attract with snappy pointer
field.setConfig({
mouseMode: 'attract',
mouseSmooth: 0.45,
mouseRadius: 220,
mouseForce: 1.2,
});
The gallery preview’s Show controls panel exposes the same options: master On/Off, mode (Off · Repel · Attract · Swirl), Force, Radius, Smooth, and Auto cursor. The embed snippet updates when you change them.
Trail (stain) & rainbow
By default the renderer clears each frame with a translucent background fill.
That creates the soft trail / stain left when particles cross an area.
Set trail: false for a clean solid wipe every frame (no smear).
| Key | Default | Effect |
|---|---|---|
trail | true | Soft motion smear. false = solid clear, no stain |
rainbow | false | Animated multi-hue particles (same as palette: 'rainbow') |
palette: 'rainbow' | — | Alias for animated rainbow colouring |
// Clean field, multicolour particles
Dotfield.create(canvas, {
mode: 'flow-calm-cw-mid',
fillParent: true,
trail: false, // no stain / smear
rainbow: true, // or palette: 'rainbow'
interactive: true,
mouseMode: 'repel',
});
field.setConfig({ trail: true }); // re-enable soft paths
field.setConfig({ rainbow: false, palette: 'ocean' });
// Dark product page accent
Dotfield.create(canvas, {
mode: 'constellation-calm-cw-mid',
fillParent: true,
palette: [
{ r: 120, g: 200, b: 255 },
{ r: 80, g: 140, b: 220 },
],
background: 'night',
intensity: 1.35,
count: 3200,
trail: true,
});
Modes & catalog
Modes are parametric combinations of families (motion engines), speed, spin, and noise scale, plus tint variants and a large serene / calm series tuned to be smooth and easy on the eyes. Each mode has a stable string id safe to hard-code in apps.
const modes = Dotfield.listModes();
console.log(modes.length); // thousands
// Calm-only catalog (whisper / gentle / still / calm + serene-*)
const calm = Dotfield.listCalmModes();
// or: modes.filter((m) => m.calm || m.serene || Dotfield.isCalmMode(m.id));
// Filter by family
const orbits = modes.filter((m) => m.family === 'orbit');
// Resolve metadata
const info = Dotfield.getMode('serene-float-whisper-cw-soft');
// info.params.engine, info.params.speed, info.calm, info.serene …
Calm & serene modes
Speeds whisper, gentle, still, and calm
are marked calm. Dedicated families (float, glide, hush, murmur, lull, breath, meadow,
glass, zen, cloud, velvet, silk, mistflow, lagoon, emberglow, moonring, softgrid,
driftwood) and every serene-* id use lower noise, softer pull, higher
damping, and slower field evolution so motion stays smooth.
// Easy-on-the-eyes hero
Dotfield.create(canvas, {
mode: 'serene-zen-gentle-cw-ease',
fillParent: true,
trail: false,
interactive: true,
});
Families (examples)
Core engines include flow, orbit, constellation, wave, spiral, vortex, grid, ribbon, bloom, rain, figure8, helix, scatter, pulse, drift, magnet, weave, halo, cascade, swarm. Extended family names (aurora, tide, lattice, comet, fog, mill, braid, echo, nexus, gyre, petal, cord, lance, stream, infinity, poles) map onto those engines with distinct parameter biases so motion still differs.
Id shape
// family-speed-spin-scale
'flow-calm-cw-mid'
'orbit-brisk-ccw-fine'
'vortex-rush-cw-vast'
// tint variants
'orbit-tint-ocean-tight'
// short legacy aliases (family-speed-spin) also resolve
'flow-calm-cw'
Browse every mode on the live gallery — the large preview updates without changing the page backdrop.
Examples
Hero background
const field = Dotfield.create(document.getElementById('hero-canvas'), {
mode: 'flow-calm-cw-mid',
fillParent: true,
interactive: true,
maxParticles: 4000,
palette: 'terracotta',
background: 'cream',
});
Mode picker in your UI
const select = document.getElementById('mode-select');
Dotfield.listModes()
.filter((m) => m.family === 'orbit')
.slice(0, 40)
.forEach((m) => {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.label;
select.appendChild(opt);
});
select.addEventListener('change', () => {
field.setMode(select.value);
});
Package consumer (Node)
See
examples/package-consumer/
in the repo — it installs via file: and require('dotfield')
(Node sample, not a hosted web page).
const Dotfield = require('dotfield');
const sim = Dotfield.createSimulation({
seed: 42,
count: 24,
mode: 'orbit-steady-cw-mid',
palette: 'ocean',
background: 'night',
});
for (let i = 0; i < 20; i++) sim.step(1 / 60);
console.log(sim.getPositions().length);
Minimal HTML
Live:
examples/minimal.html
· source on
GitHub.
Gallery site
Live demo: trinlol.github.io/dotfield · docs: /docs/
To run the gallery from a clone (optional):
npm start
# then open the URL printed by the static server (default :8080)
The backdrop randomly rotates between Driftwood, Glide, Float, and Drift on each load/rotation, with particles wrapping from every edge to the opposite side.
- Backdrop — full-page field, fixed mode, not changed by gallery picks
- Preview stage — large live canvas; mode select only updates this field
- Preview controls — colour, motion, cursor (on/off, mode, force, radius, smooth, auto), trail, rainbow…
- Install section — npm snippets mirror the current preview settings
Testing
npm test # catalog size, motion, customization
npm run test:gallery # dual-field isolation + markup
npm run test:consumer # file: package require path
npm run test:browser # Playwright probe (optional)
Prefer createSimulation in unit tests: fixed seed, equal step
counts, compare positions. Assert customization by comparing snapshots after
different speed / palette / count options.
FAQ
Does Dotfield need a build step?
No. The shipped file is plain IIFE + CommonJS-friendly. Bundlers optional.
Can I use it with React / Vue?
Yes — create the field in useEffect / onMounted with a
canvas ref, and call destroy() on unmount. No official wrappers ship
in this package.
How do I keep performance high?
Cap maxParticles or set a fixed count. Preview surfaces
can use fewer particles than full-bleed heroes. Prefer one field per page when possible.
Is pause available?
Yes programmatically via field.pause() / resume() /
togglePause(). The demo site does not expose a Pause button.
How do I disable cursor / mouse interaction?
Pass interactive: false at create time, or call
field.setConfig({ interactive: false }) at runtime.
Alternatives: mouseMode: 'off' or mouseForce: 0.
See Cursor interaction → Disable.
Where is the source of truth for modes?
Generated inside lib/dotfield.js at load time. Always query
listModes() rather than hard-coding the count.