This guide shows how to use the core library as an end user: creating an engine, selecting a runtime (CPU/WebGPU), configuring modules, adding particles, and using oscillators. It also documents all built-in force and render modules with their main inputs and simple examples.
npm install @cazala/partyimport {
Engine,
// Force modules
Environment,
Boundary,
Collisions,
Behavior,
Fluids,
Sensors,
Interaction,
Joints,
Grab,
// Render modules
Trails,
Lines,
Particles,
} from "@cazala/party";
const canvas = document.querySelector("canvas")!;
const forces = [
// Environment: gravity + damping/friction/inertia
new Environment({
gravityStrength: 600,
gravityDirection: "down", // "up"|"down"|"left"|"right"|"inwards"|"outwards"|"custom"
inertia: 0.05,
friction: 0.01,
damping: 0.0,
}),
// Boundary: keep particles within view; small tangential friction
new Boundary({
mode: "bounce", // "bounce"|"warp"|"kill"|"none"
restitution: 0.9,
friction: 0.1,
repelDistance: 20,
repelStrength: 50,
}),
// Collisions: elastic-ish collisions
new Collisions({ restitution: 0.85 }),
// Behavior: boids-style steering
new Behavior({
cohesion: 1.5,
alignment: 1.2,
repulsion: 2.0,
separation: 12,
viewRadius: 100,
viewAngle: Math.PI, // 180° field of view
wander: 20,
}),
// Fluids: SPH approximation; conservative defaults
new Fluids({
influenceRadius: 80,
targetDensity: 1.0,
pressureMultiplier: 25,
viscosity: 0.8,
nearPressureMultiplier: 40,
nearThreshold: 18,
enableNearPressure: true,
maxAcceleration: 60,
}),
// Sensors: physarum polycephalum slime
new Sensors({
sensorDistance: 30,
sensorAngle: Math.PI / 6,
sensorRadius: 3,
sensorThreshold: 0.15,
sensorStrength: 800,
followBehavior: "any", // "any"|"same"|"different"|"none"
fleeBehavior: "none",
colorSimilarityThreshold: 0.5,
fleeAngle: Math.PI / 2,
}),
// Interaction: point attract/repel (inactive until setActive(true))
new Interaction({
mode: "attract",
strength: 12000,
radius: 300,
active: false,
}),
// Joints: constraints (no joints yet, but configure dynamics)
new Joints({
momentum: 0.7,
restitution: 0.9,
separation: 0.5,
steps: 2,
friction: 0.02,
enableParticleCollisions: false,
enableJointCollisions: false,
}),
// Grab: single-particle dragging (provide inputs at interaction time)
new Grab(),
];
const render = [
new Trails({ trailDecay: 10, trailDiffuse: 4 }),
new Lines({ lineWidth: 2 }),
new Particles({ colorType: 2, hue: 0.55 }), // 2 = Hue, try 0..1
];
const engine = new Engine({
canvas,
forces,
render,
runtime: "auto", // "auto" picks WebGPU when available, otherwise CPU
});
await engine.initialize();
engine.play();-
Construction (required):
new Engine({ canvas, forces, render, runtime, ... })- canvas: HTMLCanvasElement used for rendering
- forces:
Module[]list of force modules - render:
Module[]list of render modules - runtime:
"cpu" | "webgpu" | "auto"(use "auto" for best experience) - Optional:
constrainIterations,clearColor,cellSize,maxNeighbors,maxParticles,workgroupSize
-
Lifecycle:
initialize(),play(),pause(),stop(),toggle(),destroy() -
State:
isPlaying(),getFPS() -
View:
getSize(),setSize(w,h),setCamera(x,y),getCamera(),setZoom(z),getZoom() -
Particles:
addParticle(p),setParticles(p[]),setParticle(i, p),setParticleMass(i, mass),getParticles(),getParticlesInRadius(center, radius, opts?),getParticle(i),clear(),getCount() -
Config:
getClearColor()/setClearColor(),getCellSize()/setCellSize(),getMaxNeighbors()/setMaxNeighbors(),getMaxParticles()/setMaxParticles(),getConstrainIterations()/setConstrainIterations() -
Modules:
getModule(name)returns the module instance by name -
Serialization:
export()returns{ [moduleName]: settings };import(settings)applies them -
Oscillators: see “Oscillators” below
Notes
- When
runtime: "auto", the engine tries WebGPU first, then falls back to CPU if unavailable. - Pinned particles are represented by a negative
mass. The top-levelEnginealso includes helperspinParticles([...]),unpinParticles([...]),unpinAll()(CPU + WebGPU friendly). addParticle(p)returns the index of the created particle (or-1if capacity is reached).getParticles()can be expensive on WebGPU because it requires a GPU → CPU readback of the particle buffer; prefergetParticlesInRadius(...)for tool-like / local queries.getParticlesInRadius(center, radius, { maxResults })returns{ particles, truncated }with only the fields needed for local occupancy (position,size,mass).
The Spawner helper generates IParticle[] for common shapes, including text and images.
import { Spawner } from "@cazala/party";
const spawner = new Spawner();
const particles = spawner.initParticles({
count: 5000,
shape: "text",
center: { x: 0, y: 0 },
position: { x: 0, y: 0 },
align: { horizontal: "center", vertical: "center" },
text: "Party",
font: "sans-serif",
textSize: 80,
size: 3,
mass: 1,
colors: ["#ffffff"],
});
engine.setParticles(particles);Notes:
sizecontrols particle radius;textSizeis the font size used to rasterize text.position+aligndefine the anchor point for the text bounds.- Supported fonts in the playground UI:
sans-serif,serif,monospace.
Image example:
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const particles = spawner.initParticles({
count: 12000,
shape: "image",
center: { x: 0, y: 0 },
position: { x: 0, y: 0 },
align: { horizontal: "center", vertical: "center" },
imageData,
imageSize: 400, // scales to this max dimension
size: 3,
mass: 1,
});Notes:
imageDatais required and must be provided synchronously.- Fully transparent pixels are ignored; particle colors come from the image pixels.
initialize()- Creates runtime resources (GPU device/queues or CPU canvas context), binds module uniforms, and builds pipelines. Await this before
play(). - In
runtime: "auto", falls back to CPU if WebGPU initialization fails.
- Creates runtime resources (GPU device/queues or CPU canvas context), binds module uniforms, and builds pipelines. Await this before
play()/pause()/stop()/toggle()- Controls the animation loop. Per frame the engine updates oscillators, runs simulation (state → apply → constrain×N → correct), then renders.
stop()halts and cancels the loop;pause()only toggles playing state.
destroy()- Disposes GPU/canvas resources and detaches listeners. Call when the canvas/engine is no longer needed.
getSize()/setSize(w, h)- Updates view and internal textures/buffers on resize. Call on window/canvas size changes.
setCamera(x, y)/getCamera()andsetZoom(z)/getZoom()- Adjusts the world-to-screen transform. Affects bounds, neighbor grid extents, and rendering.
addParticle(p)/setParticles(p[])/setParticle(i, p)/setParticleMass(i, mass)/getParticles()/getParticlesInRadius(center, radius, opts?)/getParticle(i)/clear()- Manage particle data. For bulk changes prefer
setParticles()to minimize sync overhead. - Use
setParticleMass(i, 0)to remove a particle (matches engine semantics wheremass === 0is “removed”). - Prefer
getParticlesInRadius(...)for local queries (e.g. brush/pin/remove tools) to avoid full-scene readback on WebGPU.
- Manage particle data. For bulk changes prefer
getCount()/getFPS()- Inspect particle count and smoothed FPS estimate.
getCount()returns the effective count (actual count limited bymaxParticlesif set).
- Inspect particle count and smoothed FPS estimate.
export()/import(settings)- Serialize/restore module inputs (including
enabled). Great for presets and sharing scenes.
- Serialize/restore module inputs (including
getModule(name)- Fetch a module instance to tweak inputs at runtime.
getActualRuntime()- Returns
"cpu" | "webgpu"for the active runtime.
- Returns
Performance-critical settings
setCellSize(size: number)- Spatial grid resolution for neighbor queries. Smaller cells improve locality but increase bookkeeping; larger cells reduce overhead but widen searches. Typical: 8–64.
setMaxNeighbors(value: number)- Cap neighbors considered per particle in neighbor-based modules (collisions, behavior, fluids). Higher = more accurate in dense scenes, but slower. Typical: 64–256.
setMaxParticles(value: number | null)- Limit the number of particles processed in simulation and rendering. When set to a number, only particles with index <
maxParticlesare processed. Set tonull(default) to process all particles. Useful for performance tuning: if you have 100k particles but setmaxParticlesto 20k, only the first 20k will be simulated/rendered.getCount()returns the effective count (min of actual count andmaxParticles).
- Limit the number of particles processed in simulation and rendering. When set to a number, only particles with index <
setConstrainIterations(iterations: number)- Number of constraint iterations per frame (affects boundary/collision correction and joints). More = more stable/rigid, but slower. Defaults: CPU ≈ 5, WebGPU ≈ 50.
- Use
runtime: "auto"unless you explicitly need one runtime. - WebGPU unlocks large particle counts and GPU compute; CPU offers maximum compatibility.
- World coordinates are independent of canvas pixels;
setCamera(x,y)centers the view andsetZoom(z)controls scale. - Bounds-aware modules (e.g.,
Boundary) use the camera and zoom to compute visible extents consistently across runtimes.
Oscillators modulate module inputs continuously over time.
addOscillator(params)- Add an oscillator to animate a module inputremoveOscillator(moduleName, inputName)- Remove a specific oscillatorupdateOscillatorSpeed(moduleName, inputName, speedHz)- Change oscillation speedupdateOscillatorBounds(moduleName, inputName, min, max)- Change oscillation rangeclearOscillators()- Remove all oscillatorsclearModuleOscillators(moduleName)- Remove all oscillators for a specific module
// Animate boundary restitution between 0.4 and 0.95 at 0.2 Hz
const oscId = engine.addOscillator({
moduleName: "boundary",
inputName: "restitution",
min: 0.4,
max: 0.95,
speedHz: 0.2,
});
// Later
engine.updateOscillatorSpeed("boundary", "restitution", 0.4);
engine.removeOscillator("boundary", "restitution");
// Clear all oscillators for a specific module
engine.clearModuleOscillators("boundary");
// Clear all oscillators
engine.clearOscillators();Inputs are addressed by the module’s input keys (documented per module below). Oscillators write values exactly as if you had called the module’s setters.
Modules come in two public roles:
- Force: contribute to simulation (acceleration/velocity/constraints)
- Render: draw into the scene texture or canvas
Differences and when they run
- Force modules execute during the simulation step. They may:
- Add forces to
particle.acceleration(e.g., gravity, boids steering) - Directly modify
particle.velocity(e.g., viscosity, sensor steering) - Adjust
particle.positionin constraint phases (e.g., collisions, joints)
- Add forces to
- Render modules execute after simulation each frame. They may:
- Draw instanced particles or lines via fullscreen passes
- Post-process the scene texture via compute-like passes (e.g., trails decay/diffuse)
- Toggle modules on/off at runtime via
setEnabled(boolean)to isolate effects and optimize performance.
Each module exposes a name and typed inputs. You can toggle any module on/off with module.setEnabled(boolean) and read current inputs via module.read(); use getModule(name) to retrieve instances from the engine.
- Purpose: global gravity, inertia, friction, velocity damping
- Inputs (defaults in parentheses):
gravityStrength(0): magnitude of gravity acceleration applied toward a direction/origin.dirX,dirY(derived): gravity direction whenmodeis directional/custom; normalized internally.inertia(0): acceleration term along current velocity (velocity * dt * inertia) to preserve momentum.friction(0): deceleration opposite to velocity (-velocity * friction).damping(0): multiplicative velocity damping each step.mode(0): 0 directional/custom, 1 inwards (to view center), 2 outwards (from view center).
- Helpers:
setGravityStrength(v)setGravityDirection("up"|"down"|"left"|"right"|"inwards"|"outwards"|"custom")setGravityAngle(radians)(used when direction iscustom)setDirection(x,y),setInertia(v),setFriction(v),setDamping(v)
Example
const env = new Environment({
gravityStrength: 1200,
gravityDirection: "down",
});
env.setFriction(0.02);- Purpose: enforce world bounds with optional repel force
- Inputs (defaults):
restitution(0.9): bounce energy retention.friction(0.1): tangential damping on contact.mode("bounce"): 0 bounce, 1 warp (wrap once fully outside), 2 kill (remove bymass=0), 3 none.repelDistance(0): inner distance from edges to start push.repelStrength(0): magnitude of inward push (outside=full, inside=scaled).
Example
const boundary = new Boundary({
mode: "bounce",
restitution: 0.85,
friction: 0.1,
});- Purpose: particle–particle collision resolution and bounce impulse
- Inputs (defaults):
restitution(0.8): elasticity along contact normal. Notes
- Uses spatial grid neighbor iteration up to
maxNeighbors; resolves deepest overlap and applies impulse; small jitter reduces bias.
const collisions = new Collisions({ restitution: 0.8 });- Purpose: boids-like steering (separation, alignment, cohesion, chase/avoid, wander)
- Inputs (defaults):
wander(20): pseudo-random lateral perturbation magnitude.cohesion(1.5): steer toward neighbor centroid.alignment(1.5): steer toward neighbor average velocity.repulsion(2): steer away when withinseparationdistance.chase(0): chase lighter neighbors (mass delta bias).avoid(0): flee heavier neighbors (within halfviewRadius).separation(10): personal space radius for repulsion.viewRadius(100): neighbor search radius.viewAngle(1.5π): field-of-view in radians. Notes
- FOV uses velocity direction; falls back to a default forward if nearly zero velocity.
const behavior = new Behavior({
cohesion: 1.5,
alignment: 1.5,
separation: 10,
viewRadius: 100,
});- Purpose: SPH-inspired fluid approximation (density pre-pass + pressure/viscosity apply)
- Inputs (defaults):
influenceRadius(100): neighbor radius for kernels.targetDensity(1): rest density.pressureMultiplier(30): scales pressure from density difference.viscosity(1): smooths velocity differences.nearPressureMultiplier(50): strong short-range pressure.nearThreshold(20): near-pressure distance.enableNearPressure(true): toggle for near-pressure.maxAcceleration(75): clamp for stability. Notes
- Two passes:
state(density/near-density),apply(pressure/viscosity → velocity).
const fluids = new Fluids({
influenceRadius: 80,
pressureMultiplier: 25,
viscosity: 0.8,
});- Purpose: trail/color sampling based steering (follow and/or flee)
- Inputs (defaults):
sensorDistance(30),sensorAngle(π/6),sensorRadius(3)sensorThreshold(0.1),sensorStrength(1000)colorSimilarityThreshold(0.4)followBehavior(any): 0 any, 1 same, 2 different, 3 nonefleeBehavior(none): 0 any, 1 same, 2 different, 3 nonefleeAngle(π/2) Notes
- Samples scene texture consistently across runtimes; no trails required.
const sensors = new Sensors({
sensorDistance: 30,
sensorAngle: Math.PI / 6,
followBehavior: "any",
});- Purpose: point attract/repel under user control
- Inputs (defaults):
mode(attract: 0/repel: 1),strength(10000),radius(500)positionX/Y(0),active(false)
const interaction = new Interaction({
mode: "attract",
radius: 300,
strength: 12000,
});
interaction.setPosition(0, 0);
interaction.setActive(true);- Purpose: distance constraints between particles, optional collisions, momentum preservation
- Inputs (defaults):
- Arrays:
aIndexes[],bIndexes[],restLengths[], CSRincidentJointOffsets/incidentJointIndices, derivedgroupIds[] - Scalars:
enableParticleCollisions(0),enableJointCollisions(0),momentum(0.7),restitution(0.9),separation(0.5),steps(1),friction(0.01)
- Arrays:
- Helpers:
setJoints([...]),add({ aIndex, bIndex, restLength }),remove(a,b),removeAll(), setters for all scalar inputs
const joints = new Joints();
joints.setJoints([{ aIndex: 0, bIndex: 1, restLength: 50 }]);
joints.setMomentum(0.7);- Purpose: efficient mouse-drag grabbing of a single particle (updates one particle per frame)
- Inputs:
grabbedIndex,positionX,positionY - Helpers:
grabParticle(index, {x,y}),releaseParticle(),isGrabbing()
const grab = new Grab();
grab.grabParticle(42, { x: 100, y: 100 });- Purpose: draw particles as soft discs; pinned particles render as rings
- Inputs (defaults):
colorType(Default: 0, Custom: 1, Hue: 2)customColorR/G/B(1/1/1) whencolorType=Customhue(0) whencolorType=Hue
const particles = new Particles();
particles.setColorType(2); // Hue
particles.setHue(0.5);- Purpose: decay + diffuse passes over the scene texture
- Inputs (defaults):
trailDecay(10): fade speed toward clear colortrailDiffuse(0): blur radius (0–12 typical)
const trails = new Trails({ trailDecay: 12, trailDiffuse: 4 });- Purpose: draw lines between particle pairs (indices)
- Inputs (defaults):
aIndexes[],bIndexes[]: segment endpoints by particle indexlineWidth(1.5)lineColorR/G/B(-1/-1/-1): negative = use particle color
- Helpers:
setLines([...]),add({ aIndex, bIndex }),remove(a,b),setLineWidth(v),setLineColor(color|null)
const lines = new Lines({ lines: [{ aIndex: 0, bIndex: 1 }] });
lines.setLineWidth(2);- Start with a small number of modules enabled; add more as needed.
- Increase
cellSizefor sparser scenes; reduce it for dense ones. - WebGPU: prefer
runtime: "auto"and let the engine fall back if needed.