Compound effects that don't fit cleanly into a single data type. Simulation zones live here.
Circle Collider
collider-circle
Circular collider for the Particle Simulator. Outside-mode treats the disc as solid; inside-mode bounces particles off the inside (fish-tank).
space
param:position,param:radius=canvas01
gotcha
position/radius are authored (canvas01) coordinates like splines/points — the solver converts around the canvas aspect internally so the disc stays round.
gotcha
mode=inside flips the collider so particles bounce off the inside of the disc instead of being blocked from entering it.
Produces a force descriptor only; the damping (vel *= max(0, 1 - coeff*dt)) runs inside whichever simulator consumes it (sim-kernel.ts, particle shader).
gotcha
coeff is a per-second damping rate, not a 0..1 fraction — a large coeff*dt can zero out velocity in one step rather than merely slowing it.
Parameters
Name
Type
Default
Coefficient
coeff
scalar
0 … 10
1
Sockets
Direction
Name
Type
outputprimary
out
force
Gravity
force-gravity
Constant directional pull. Negative gy points up in canvas-UV space.
gotcha
Produces a force descriptor only; gx/gy are added straight to velocity each step (vel += g*dt) by whichever simulator consumes it.
gotcha
gx/gy are accelerations in the authored canvas01 frame (Y-down, width-isotropic): positive gy pulls toward the bottom of the canvas.
Parameters
Name
Type
Default
Gx
gx
scalar
-5 … 5
0
Gy
gy
scalar
-5 … 5
1
Sockets
Direction
Name
Type
outputprimary
out
force
Image Mask Collider
collider-image-mask
Image-based collider — pixels with alpha above the threshold act as solid geometry. Particles bounce off (gradient as surface normal) or die on contact.
gotcha
kill=true drops particles on contact and hides restitution — bounce and kill are mutually exclusive.
gotcha
In Matter Simulator this collider bakes into a CPU obstacle SDF instead of being sampled directly, since image masks can't be read on the WebGPU compute path.
Parameters
Name
Type
Default
Threshold
threshold
scalar
0 … 1
0.5
Kill on contact
kill
boolean
false
Bounce
restitution
scalar
0 … 1
0.5
Sockets
Direction
Name
Type
input*
mask
mask
image
outputprimary
out
collider
Image Mask Emitter
emitter-image-mask
Emit particles into the pixels of a mask image, weighted by alpha.
space
param:velocity=canvas01
gotcha
A missing or non-image mask input emits nothing (no primary output) instead of erroring.
gotcha
threshold rejects alpha samples below it via up to 4 rejection-sample tries per particle per frame; a sparse/low-alpha mask can silently under-spawn.
gotcha
velocity is canvas01 (authored) space: width px/sec on both axes, independent of canvas aspect ratio.
gotcha
Emits a descriptor only; the Particle Simulator reads rate/threshold/vJitter/lifetime on its own spawn pass — nothing spawns until wired to one.
Parameters
Name
Type
Default
Threshold
threshold
scalar
0 … 1
0.5
Rate
rate
scalar
0 … 100000
1000
Velocity
velocity
vec2
[0,0]
Velocity Jitter
vJitter
scalar
0 … 5
0.5
Lifetime
lifetime
scalar
0.05 … 60
2
Sockets
Direction
Name
Type
input*
mask
mask
image
outputprimary
out
emitter
Line Collider
collider-line
Half-plane collider — an infinite line. Particles bounce off the side opposite the normal. Use 'angle 0, point (0.5, 0.9)' for a ground.
space
param:point=canvas01
gotcha
angle 0 is a horizontal wall with its normal pointing up (blocks below); angle 90 is a vertical wall with the normal pointing right.
gotcha
It's an infinite half-plane, not a segment — the whole canvas on the far side of the line is blocked regardless of where `point` sits along it.
Parameters
Name
Type
Default
Point on wall
point
vec2
[0.5,0.9]
Angle (deg)
angle
scalar
-180 … 180
0
Bounce
restitution
scalar
0 … 1
0.6
Sockets
Direction
Name
Type
outputprimary
out
collider
Matter Simulator
matter-simulatorpolymorphic
MLS-MPM deformable matter on WebGPU — liquid, jelly, and snow are one solver with per-material dials (liquid: stiffness + viscosity; jelly: stiffness; snow: stiffness + crumble + hardening). Wire points into `seed` (or a mask into `region`) to place the material; `particle_radius` packs the seeding at a rest spacing so the material keeps that separation. Obstacles three ways: any spline into `obstacle`, Image Mask Colliders, or Circle/Line colliders — all free-slip, inflated by `collider_radius` so sprites rest ON surfaces. Takes the same force nodes as the Particle Simulator. Output is a `particles` socket for Particles to Image plus a `points` aux for the whole points ecosystem — Copy to Points on goo, Points to Spline surfaces. Runs one frame behind (WebGPU readback); needs a WebGPU-capable browser. Restarting the timeline reseeds.
particles (primary) is raw per-axis grid-index/(nx,ny) UV for Particles to Image; the points aux is converted back to authored canvas01 (aspectUncorrectY) for the points ecosystem.
gotcha
Force/collider descriptor coordinates are authored canvas01, the same convention the Particle Simulator uses, so one Force node feels identical wired into either.
gotcha
gravity is a canvas-width-relative acceleration internally scaled by grid nx (not ny), matching a wired Gravity force node's magnitude on non-square canvases.
gotcha
particle_radius sets seed spacing only when no seed points are wired; wired seed points use their own spacing and particle_radius is ignored.
gotcha
Reseeds on a fresh graph, a timeline wrap back near t=0, or any change to seed_jitter/material/particle_radius/the wired seed points or region identity.
gotcha
drive_by_scene_time steps only while the wired time input strictly increases past its last value; without it, stepping instead requires ctx.playing (or offline export advancing).
gotcha
Image Mask Colliders can't be sampled live on WebGPU; they're baked once into the same CPU obstacle SDF as the obstacle spline, alongside Circle/Line colliders.
gotcha
Runs one frame behind because the WebGPU readback is async; the very first frame after (re)seeding has no output image yet.
Parameters
Name
Type
Default
Particles
count
scalar
256 … 131072
8192
Material
material
enum
"liquid"
liquid · jelly · snow
Stiffness
liquid_stiffness
scalar
0 … 1
0.5
Viscosity
liquid_viscosity
scalar
0 … 1
0.08
Stiffness
jelly_stiffness
scalar
0 … 1
0.55
Stiffness
snow_stiffness
scalar
0 … 1
0.45
Crumble
snow_yield
scalar
0 … 1
0.5
Hardening
hardening
scalar
0 … 20
10
Gravity
gravity
scalar
-2 … 2
0.6
Damping
damping
scalar
0 … 2
0
Particle radius
particle_radius
scalar
0 … 0.02
0
Speed
speed
scalar
0.1 … 4
1
Grid resolution
grid_res
scalar
32 … 256
96
Substeps / frame
substeps_per_frame
scalar
2 … 24
10
Force slots
forceCount
scalar
0 … 6
2
Collider slots
colliderCount
scalar
0 … 4
1
Collider radius
collider_radius
scalar
0 … 0.05
0.004
Seed
seed_jitter
scalar
0 … 10000
1
Drive by time input
drive_by_scene_time
boolean
false
Sockets
Direction
Name
Type
input
seed
seed
points
input
region
region
mask
input
obstacle
obstacle
spline
input
force1
force1
force
input
force2
force2
force
input
collider1
collider1
collider
outputprimary
out
particles
outputaux
points
points
Particle Simulator
particle-simulatorpolymorphic
GPU particle simulator. Wire forces and emitters; output is a particles socket consumed by Particles to Image. Counts up to ~65k particles run in real-time on most GPUs.
space
out=uv01
reads
time
flags
simulation, unstable
gotcha
backend=webgpu implements only gravity/drag forces and screen bounds; point/vortex/wind/turbulence forces and every emitter/collider are stubbed to zero.
gotcha
backend=webgpu never returns a points aux (no PointsValue path) even though outputPoints still adds the socket, so a wire from it gets nothing.
gotcha
backend=webgpu keeps stepping every compute() call regardless of ctx.playing; backend=webgl only advances while playing or on a new offline export frame.
gotcha
fixedDt is a constant step taken once per compute() call rather than derived from elapsed time, so sim speed scales with output fps, not wall-clock time.
gotcha
particles (primary) is raw, non-aspect-corrected canvas UV; wired Force/Emitter/Collider descriptors are authored canvas01 (engine/aspect.ts), same seam as Matter Simulator.
gotcha
outputPoints (webgl only) synchronously reads back only the first 8192 particles (POINTS_READBACK_CAP) each frame and drops dead/expired ones.
Parameters
Name
Type
Default
Max particles
count
scalar
64 … 262144
4096
Emitter slots
emitterCount
scalar
1 … 4
1
Force slots
forceCount
scalar
0 … 8
2
Collider slots
colliderCount
scalar
0 … 6
0
Screen bounds
boundsMode
enum
"off"
off · bounce · wrap · clamp · kill
Bounds bounce
boundsRestitution
scalar
0 … 1
0.6
Output points (CPU readback)
outputPoints
boolean
false
Substep dt
fixedDt
scalar
0.001 … 0.1
0.016666666666666666
Backend
backend
enum
"webgl"
webgl · webgpu
Sockets
Direction
Name
Type
input
emitter1
emitter1
emitter
input
force1
force1
force
input
force2
force2
force
outputprimary
out
particles
Particles to Image
particles-to-image
Render a particles socket as point sprites onto an image. Each particle becomes a soft-edged dot; size, color, and blend mode are uniform.
space
in:particles=uv01 param:pointSize=pixels
flags
unstable
gotcha
particles positions are read as raw canvas UV (Y-down, not aspect-corrected) and mapped straight to clip space; a particles source using a different convention renders offset/skewed.
gotcha
pointSize is a literal gl_PointSize in device pixels at render resolution, not canvas-relative; it does not scale with output resolution.
gotcha
blendMode=alpha blends an already-premultiplied fragment (rgb*a, a) via gl.blendFunc(SRC_ALPHA, ONE_MINUS_SRC_ALPHA), so soft edges and fadeOut render dimmer than a true straight-alpha over.
gotcha
color, size, and opacity are uniform across every particle; there is no per-particle variation from velocity or age besides the fadeOut toggle.
Parameters
Name
Type
Default
Color
color
color
"#ffffff"
Opacity
opacity
scalar
0 … 1
1
Point size
pointSize
scalar
1 … 64
4
Blend
blendMode
enum
"additive"
additive · alpha
Fade with age
fadeOut
boolean
true
Sockets
Direction
Name
Type
input*
particles
particles
particles
input
background
background
image
outputprimary
out
image
Point Emitter
emitter-point
Emit particles from a single point with positional and velocity jitter.
position, spread, and velocity are canvas01 (authored) space: width-relative and vertically undistorted, independent of canvas aspect ratio.
gotcha
velocity is width px/sec on both axes, matching Point Force and other simulators so combined forces feel consistent across sim types.
gotcha
Emits a descriptor only; nothing spawns until wired into a Particle Simulator, which reads rate/vJitter/lifetime on its own spawn pass.
Parameters
Name
Type
Default
Position
position
vec2
[0.5,0.5]
Spread
spread
scalar
0 … 1
0.02
Rate
rate
scalar
0 … 100000
200
Velocity
velocity
vec2
[0,-0.5]
Velocity Jitter
vJitter
scalar
0 … 5
0.2
Lifetime
lifetime
scalar
0.05 … 60
2
Sockets
Direction
Name
Type
outputprimary
out
emitter
Point Force
force-point
Radial attractor or repeller around a point in canvas-UV space.
space
param:position,param:radius=canvas01
gotcha
Produces a force descriptor only; the consuming simulator evaluates it against particle positions in the same canvas01 frame as position.
gotcha
Force magnitude is strength*(1-r/radius)^falloff for r<radius, zero beyond; mode=attract flips the sign to pull toward position, mode=repel pushes away.
Parameters
Name
Type
Default
Position
position
vec2
[0.5,0.5]
Strength
strength
scalar
0 … 100
1
Radius
radius
scalar
0.001 … 1.5
0.3
Falloff
falloff
scalar
0 … 8
2
Mode
mode
enum
"attract"
attract · repel
Sockets
Direction
Name
Type
outputprimary
out
force
Turbulence
force-turbulence
Curl-noise turbulence field that advects through time at the given speed.
gotcha
Produces a force descriptor only; the consuming simulator advances curl noise through time (time*speed) and adds the curl to velocity each step.
gotcha
scale multiplies the canvas01 position before sampling curl noise, so it behaves as a spatial frequency (more cells across the canvas), not a canvas distance.
Parameters
Name
Type
Default
Scale
scale
scalar
0.5 … 32
4
Strength
strength
scalar
0 … 10
0.5
Speed
speed
scalar
0 … 10
1
Sockets
Direction
Name
Type
outputprimary
out
force
Vortex
force-vortex
Tangential swirl around a point. Strength sign flips rotation direction.
space
param:position,param:radius=canvas01
gotcha
Produces a force descriptor only; the consuming simulator evaluates the tangential swirl against particle positions in the same canvas01 frame.
gotcha
Force magnitude is strength*(1-r/radius)^falloff for r<radius, zero beyond radius; strength's sign flips which way the swirl rotates.
Parameters
Name
Type
Default
Position
position
vec2
[0.5,0.5]
Strength
strength
scalar
-10 … 10
1
Radius
radius
scalar
0.001 … 1.5
0.3
Falloff
falloff
scalar
0 … 8
1.5
Sockets
Direction
Name
Type
outputprimary
out
force
WebGPU Particle Test
webgpu-particle-test
Phase 0 spike for the WebGPU particle migration. Renders an isolated WebGPU particle system on an overlay canvas. Validates device boot, kernel compile, dual-canvas compositing, and 100k particles @ 60fps before the real simulator is ported. Not a graph node — produces no output.
space
param:pointSize=pixels
flags
terminal
gotcha
compute() is an intentional no-op; WebGPUParticleOverlay.tsx reads this node's params directly off the graph and drives a separate WebGPU canvas overlaid on the WebGL2 preview.
gotcha
pointSize is pixels at the overlay's own framebuffer resolution (pointClip = pointSize / fbWidth), independent of the main canvas or output size.
gotcha
seed only re-seeds the particle buffers when it changes (treated as a nonce); gravity/damping apply live without a rebuild.
gotcha
count changing rebuilds the WebGPU buffers/pipeline from scratch since it resizes the particle buffer allocation.
Parameters
Name
Type
Default
Count
count
enum
"100k"
1k · 10k · 100k · 500k
Gravity
gravity
scalar
-5 … 5
0.8
Damping
damping
scalar
0.9 … 1
0.999
Point size (px)
pointSize
scalar
1 … 12
2.5
Seed
seed
scalar
0 … 1000000
0
Sockets
Direction
Name
Type
—
(no sockets)
Wind
force-wind
Constant directional push. The simulator normalizes the direction vector.
gotcha
Produces a force descriptor only; the consuming simulator normalizes (dx,dy) to a unit direction before scaling by strength each step.
gotcha
A zero direction vector [0,0] is coerced to [1,0] in this node before the descriptor is built.