API reference

Every public class, method, and event in @codexo/exojs. Generated from source.

C

classParticleSystem

@codexo/exojs-particles / particles / stable

The central coordinator of the particle pipeline. `ParticleSystem` is a Drawable that owns: - **Particle storage** - one channel per attribute (position, velocity, scale, rotation, color, timing, ...), sized to a fixed capacity at construction. Modules and render modes address it by name through a ParticleBatch; user code brings particles into existence with emit. - **Spawn modules** - fill freshly emitted particles. - **Update modules** - mutate the live range each frame (forces, color blends, scale curves, drag, ...). Built-in modules ship both CPU and WGSL implementations; custom modules can opt into GPU acceleration by implementing `wgsl()`. - **Death modules** - fire once per dying particle, before its slot is recycled (sub-emitters, event hooks). **Auto-routing CPU vs GPU:** at first update, the system checks: if a `WebGpuBackend` was supplied AND every registered update module has `wgsl()` AND the render mode is GPU-eligible, the GPU path engages - a composite compute pipeline runs integration plus all module bodies in one dispatch and writes directly into the renderer's instance buffer (no CPU readback). Otherwise the CPU path runs the existing per-module `apply()` loops. **Per-frame order in update (CPU mode):** 1. Run every spawn module. 2. Integrate position from velocity, rotation from rotationSpeed, advance `elapsed`. 3. Run every update module on the live range. 4. Compact: scan `[0, liveCount)` forward, fire death modules on expired slots, copy survivors down. `liveCount` shrinks to the survivor count. **Per-frame order in update (GPU mode):** 1. Run every spawn module (CPU writes initial values into the spawn slot). 2. Detect expiries on CPU (via `elapsed >= lifetime`); fire death modules; set `lifetime[slot] = -1` sentinel + clear `alive[slot]` so the GPU shader skips them. **No compaction** - slots are recycled on next spawn. 3. Dispatch the composite compute pipeline. Integration + update modules + pack-instances run in one pass; the instance buffer is written directly. CPU SoA stays as-is for spawn writes. **Coordinate space:** particle positions are LOCAL to the system. The system's `getGlobalTransform()` is applied on top during rendering - both the WebGL2 and WebGPU shaders multiply `projection * translation * rotated`. Setting world-space positions on individual particles double-translates. Position the system itself via `system.setPosition(...)` and emit relative to `(0, 0)`. **View culling:** a system is created with `cullable = false`. Its local bounds cover one texture frame at the local origin, because the particles themselves are simulated on the GPU in half the configurations and no emitted extent is tracked in either - so culling against those bounds would remove the entire cloud as soon as the emitter's own origin left the view. For a system whose reach is known, set the node's `cullArea` to a rectangle in local space covering where its particles travel and set `cullable = true` again; the viewport check then uses that rectangle instead of the bounds. `getBounds()` still reports the one-frame box, not an extent of the live particles. **Pixel snapping:** Drawable.pixelSnapMode is intentionally ignored for particle systems. Particle instances bake their own per-particle transforms in the emitter/compute path rather than reading the shared pixel-snap transform row, so a snap mode set on the system has no effect on rendered output - snapping thousands of independently-moving sub-pixel particles to the device grid is neither meaningful nor desirable.

50
props
47
methods
14
events
Import
import { ParticleSystem } from '@codexo/exojs-particles'

The central coordinator of the particle pipeline. `ParticleSystem` is a Drawable that owns:

- **Particle storage** - one channel per attribute (position, velocity, scale, rotation, color, timing, ...), sized to a fixed capacity at construction. Modules and render modes address it by name through a ParticleBatch; user code brings particles into existence with emit. - **Spawn modules** - fill freshly emitted particles. - **Update modules** - mutate the live range each frame (forces, color blends, scale curves, drag, ...). Built-in modules ship both CPU and WGSL implementations; custom modules can opt into GPU acceleration by implementing `wgsl()`. - **Death modules** - fire once per dying particle, before its slot is recycled (sub-emitters, event hooks).

**Auto-routing CPU vs GPU:** at first update, the system checks: if a `WebGpuBackend` was supplied AND every registered update module has `wgsl()` AND the render mode is GPU-eligible, the GPU path engages - a composite compute pipeline runs integration plus all module bodies in one dispatch and writes directly into the renderer's instance buffer (no CPU readback). Otherwise the CPU path runs the existing per-module `apply()` loops.

**Per-frame order in update (CPU mode):** 1. Run every spawn module. 2. Integrate position from velocity, rotation from rotationSpeed, advance `elapsed`. 3. Run every update module on the live range. 4. Compact: scan `[0, liveCount)` forward, fire death modules on expired slots, copy survivors down. `liveCount` shrinks to the survivor count.

**Per-frame order in update (GPU mode):** 1. Run every spawn module (CPU writes initial values into the spawn slot). 2. Detect expiries on CPU (via `elapsed >= lifetime`); fire death modules; set `lifetime[slot] = -1` sentinel + clear `alive[slot]` so the GPU shader skips them. **No compaction** - slots are recycled on next spawn. 3. Dispatch the composite compute pipeline. Integration + update modules + pack-instances run in one pass; the instance buffer is written directly. CPU SoA stays as-is for spawn writes.

**Coordinate space:** particle positions are LOCAL to the system. The system's `getGlobalTransform()` is applied on top during rendering - both the WebGL2 and WebGPU shaders multiply `projection * translation * rotated`. Setting world-space positions on individual particles double-translates. Position the system itself via `system.setPosition(...)` and emit relative to `(0, 0)`.

**View culling:** a system is created with `cullable = false`. Its local bounds cover one texture frame at the local origin, because the particles themselves are simulated on the GPU in half the configurations and no emitted extent is tracked in either - so culling against those bounds would remove the entire cloud as soon as the emitter's own origin left the view. For a system whose reach is known, set the node's `cullArea` to a rectangle in local space covering where its particles travel and set `cullable = true` again; the viewport check then uses that rectangle instead of the bounds. `getBounds()` still reports the one-frame box, not an extent of the live particles.

**Pixel snapping:** Drawable.pixelSnapMode is intentionally ignored for particle systems. Particle instances bake their own per-particle transforms in the emitter/compute path rather than reading the shared pixel-snap transform row, so a snap mode set on the system has no effect on rendered output - snapping thousands of independently-moving sub-pixel particles to the device grid is neither meaningful nor desirable.

Constructors4
Methods47
_updateOrigin(): void
Re-derive origin from the fractional anchor and the CURRENT layout box. Uses local (untransformed) bounds on purpose: the transform multiplies the origin by scale itself, so deriving from world bounds would double-apply scale whenever the anchor is set after scaling. Subclasses whose layout box changes after construction (e.g. a sprite switching to a texture sub-frame) must call this to keep an anchored node anchored. The anchor measures against the drawable's declared LAYOUT BOX - its extent, taken from its own local origin - and never against where its AABB happens to start. Only the size term participates, so the mapping from anchor to origin is a pure function of the anchor and the box size: anchor = (0, 0) always means origin = (0, 0), whatever the anchor was set to before. Folding the bounds origin in would make it path-dependent instead - a drawable whose rectangle starts off-origin would keep that corner baked into its origin after returning to the default anchor. A drawable whose layout box is NOT its local bounds overrides this: text measures against its typographic advance, an AnimatedSprite against the untrimmed source frame rather than the per-frame trimmed rectangle.
Registers an update module. Modules run in registration order, each seeing what the previous ones did. Modules may be added and removed at any time, including mid-flight: the next update rebuilds whatever the change invalidated. On the GPU path that is the compute program alone - live particles keep the state the device has been integrating. Adding a module without a wgsl() implementation to a running GPU system is the one change that cannot preserve them: the simulation moves to the CPU, which has no copy of the integrated state, so the system clears its live particles rather than continuing from stale values (see clearParticles).
blur(): this
Release keyboard focus from this node if it currently holds it.
clearDeathModules(): this
clearFilters(): this
clearParticles(): this
Resets the system to zero live particles without destroying it.
clearSpawnModules(): this
clearUpdateModules(): this
Compute a full CollisionResponse between this shape and target. Returns null in two cases: - the shapes do not overlap, **or** - the specific shape-pair combination does not support response generation (e.g. Line against any shape, Ellipse against Ellipse or Polygon). Use intersectsWith for a universal boolean overlap check that works across all supported shape pairs.
contains(x: number, y: number): boolean
Hit-test the world-space point (x, y) against this node, honouring hitArea: with one set, the point is mapped into local space and tested against the shape; without one, the inherited bounds/oriented-box test applies. A subclass that overrides this with its own geometry test must forward to super.contains while hitArea is set, or the override silently defeats the caller's pick shape.
destroy(): void
Brings one particle into existence and returns a writer for its initial values, or null when the system is at capacity. The particle starts at the spawn defaults - origin, no velocity, unit scale, no rotation, opaque white, frame 0, one second of life - so a caller writes only what it varies. The returned writer is a cursor that the next emit() rebinds, so it must not be stored. Emission is the only per-particle write that is true on every backend: spawn values originate on the CPU and are uploaded from there, while everything the simulation integrates afterwards lives wherever the simulation runs.
focus(): this
Request keyboard focus for this node through its owning focus service.
Axis-aligned bounding box of this node in its GLOBAL-transform space. That is world space for ordinary nodes, but GROUP-LOCAL space for nodes inside an engaged RetainedContainer transform group (the group matrix is applied on the GPU, not here) - this is deliberate and matches the rendering convention. For a true world-space extent of such a node, lift this rect by the group's getWorldTransform matrix. Pass out to receive a copy you own. Without it the return value is this node's **cached** rectangle, rebuilt in place whenever the transform or the local extent changes - retaining it across frames hands you a value that silently moves. The cache is why the no-arg form does not allocate: this runs per node per frame for culling and hit-testing.
This node's untransformed extent in its own local coordinate space. Returns the LIVE internal rectangle, not a copy: it is read on hot paths (updateBounds re-reads it on every transform-dirty recompute, i.e. potentially every frame for a moving node), so copying it here would add a per-frame allocation. It is therefore typed as a ReadonlyRectangle - reads are unchanged, writes are rejected at compile time. Writing to it directly would skip the bounds/content invalidation the engine needs, leaving culling, hit-testing and retained render fragments on a stale extent. A custom Drawable that owns its own size sets it through setLocalBounds, which writes and invalidates in one step.
Return the outward-facing edge normals used by the SAT solver. The array should be cached and reused across calls.
The node's TRUE world-space transform, composed through every transform-group boundary (RetainedContainer) in the ancestor chain. getGlobalTransform deliberately stops at the nearest engaged boundary (descendants resolve group-RELATIVE transforms; the renderer multiplies the group matrix back in on the GPU), so it is the right space for rendering but the wrong one for spatial queries. Use THIS accessor whenever a real world position/orientation is needed - picking, spatial audio, physics, world-space math against nodes outside the group. Without any engaged boundary ancestor it returns the exact getGlobalTransform matrix (same instance, no extra work). With one, it lazily caches groupLocal × groupWorld and revalidates on read via version/stamp compares - including runtime space flips such as RetainedContainer's deep-barrier sub-branch escape.
Test whether this shape overlaps target using a fast boolean algorithm (no penetration depth or normal computed). Prefer this over collidesWith when only the yes/no result is needed.
invalidateCache(): this
invalidateContent(): this
Mark this node's visual content dirty without going through a standard setter - e.g. a custom Drawable subclass backed by externally mutable data (the pattern TileChunkNode in @codexo/exojs-tilemap already uses via its own _chunk.revision compare). Call this after mutating such state so the Track-B retained-plan skip does not serve a stale frame for this node.
move(x: number, y: number): this
render(backend: RenderBackend): this
Raw rendering entry point. Direct backend access - bypasses the RenderPlan pipeline machinery. Prefer the high-level RenderingContext.render path via the owning RenderingContext wherever possible.
resetTextureFrame(): this
rotate(degrees: number): this
setAnchor(x: number, y: number): this
Set the normalized anchor and re-derive origin from it.
Change the blend mode. No-ops if the value is unchanged. Invalidates the render cache when the blend mode actually changes.
setLocalBounds(x: number, y: number, width: number, height: number): this
Write this node's local extent and run the bounds invalidation the change implies - the node's own bounds flag, the ancestor bounds cascade, and the content-dirty stamp that keeps retained fragments from replaying the old extent. This is the only supported way to resize a node from outside getLocalBounds; the rectangle itself is handed out read-only so the invalidation cannot be forgotten. Built-in drawables (Sprite, Text, BitmapText, Mesh, ...) and custom ones alike go through here. Part of the renderer SDK contract for extension renderers.
setOrigin(x: number, y: number): this
setPosition(x: number, y: number): this
setRotation(degrees: number): this
setScale(x: number, y: number): this
setSkew(x: number, y: number): this
Set the tint colour by copying color into the internal Color instance. Invalidates the render cache so the change is picked up on the next frame. A retained product recognises a tint-only change and rewrites the affected row rather than re-recording, so tinting per frame stays cheap. Writing through the returned tint instance instead (sprite.tint.r = 8) bypasses that entirely and is not observed at all - assign a colour, or call RenderNode.invalidateContent after mutating in place.
updateBounds(): this
updateParentTransform(): this
updateTransform(): this
Properties50
capacity: number
Maximum particle count this system will store. Fixed at construction.
cursor: null | string
draggable: boolean
When true and interactive is also true, this node will be automatically repositioned to follow the pointer during a drag gesture. The framework captures the pointer offset at drag-start so the node doesn't snap to the cursor position. Both interactive and draggable must be set for dragging to work - a draggable but non-interactive node will never receive pointerdown and therefore cannot start a drag.
focusable: boolean
When true, this node can receive keyboard focus - via focus, Tab traversal, or app.interaction.focus(node) - and is delivered key events through onKeyDown / onKeyUp while focused, or while any of its descendants holds focus (key events bubble up the parent chain like pointer InteractionEvents do). A Widget additionally has to be enabled: disabling one takes it out of the Tab order and rejects programmatic focus, without touching this flag.
Optional pick shape in this node's LOCAL space, replacing the bounds test contains would otherwise perform. Defaults to null. The world-space point is mapped through the inverse of the node's global transform before the shape is tested, so the region follows the node's position, rotation, scale and skew like the rendered output does - the shape itself is never re-transformed and never needs updating when the node moves. Affects picking only. Bounds, culling and rendering ignore it entirely, and because the interaction system finds candidates by their bounds, a hit area reaching outside the node's bounds is only reliably picked where the two overlap. Use it to shrink or reshape a pick region, not to grow one. The shape is the caller's: it is read live on every hit test, so mutating it in place takes effect immediately, and the node never destroys it.
name: null | string
Optional human-readable identity for this node. Defaults to null. Purely a label the engine never interprets: useful for debugging, find-by-name lookups, prefab references, and as a stable key when merging serialized state back onto an existing tree. Not required to be unique.
tabIndex: number
Tab-traversal order among focusable nodes in the same focus scope. Lower values are visited first; equal values keep document (tree) order.
aliveCount: number
Actual count of live particles. May be below liveCount on the GPU path.
Normalized anchor in 0..1 along each axis that derives origin from this drawable's layout box. (0, 0) = top-left, (0.5, 0.5) = centre, (1, 1) = bottom-right. Updates origin whenever the anchor or the layout box changes. The mapping is a pure function of the anchor and the box size - the same anchor value always yields the same origin, whatever it was set to before - so (0, 0), the default, always means origin = (0, 0). Set origin directly instead when the pivot is not a fraction of the box.
cacheAsTexture: boolean
Bake this node's subtree into a RenderTexture once and replay that texture until the subtree changes, instead of walking and drawing it every frame. Worth it for a subtree that is expensive to draw and rarely changes. The cache is invalidated by anything that moves the node's world bounds - the node's own transform included - so a node that animates re-bakes every frame and is strictly slower than not caching it at all. Setting it to false frees the texture immediately.
cacheResolution: TargetResolution
Resolution the cacheAsTexture texture is baked at, in device pixels per logical unit. 'inherit' (the default) matches the surface the cache is composited into, so enabling the cache does not soften the picture on a HiDPI display. Pin it to a number to trade sharpness for memory and bake cost - a cache is resolution² texels, so 1 on a DPR-3 phone is a ninth of the VRAM and a ninth of the fill per re-bake. Changing it invalidates the cache.
clip: boolean
When true, descendants are geometrically clipped to clipShape. Unlike mask (which is alpha/visibility masking), clip is a hard geometric boundary: - clipShape === null - clip to this node's world-space bounds (getBounds), using the GPU scissor fast path. - clipShape is a Rectangle - clip to that world-space rectangle via scissor. - clipShape is a Geometry - clip to the geometry's silhouette via the stencil buffer (WebGL2). Only fragments inside the shape survive. Clipping wraps the node's final (filtered/masked) output and acts as a render barrier: draw commands are never reordered or batched across the clip boundary.
cullable: boolean
When false, this node is never culled by the viewport check and is always considered in-view. Defaults to true.
Custom rectangle used for viewport cull intersection test. When set, replaces the default node bounds in cull checks. Set to null to restore default bounds-based culling.
destroyed: boolean
true once destroy has run on this node. A destroyed node has released its pooled resources (transform/bounds), has been unlinked from its parent, renders nothing, and must not be reused or re-attached. The render plan skips a destroyed node, so even one handed straight to a renderer as a detached root contributes nothing.
Atlas frames declared on this system, or empty when the texture is used as a single frame. Each particle's textureIndex selects an entry from this list; anything out of range shows frame 0.
gpuMode: boolean
true when the system is running on the GPU compute pipeline.
gpuState: ParticleGpuState | null
GPU-side state, or null in CPU mode.
hasAtlas: boolean
true when the system declares more than one atlas frame.
interactive: boolean
isAlignedBox: boolean
liveCount: number
Upper bound of the slot range that can hold live particles. Exact on the CPU path: after each update() slots [0, liveCount) are all alive. On the GPU path it is a high-water mark whose range can contain dead holes that future emissions fill; aliveCount counts the live ones.
The mask source that controls visibility of this node's render output. See MaskSource for accepted source types and their semantics. Setting to null removes any active mask. Setting a RenderNode that is this is rejected (a node cannot mask itself). Indirect cycles (a.mask = b; b.mask = a) are rejected as well: the candidate's mask chain is walked and any cycle - whether it closes on this or was already present in the chain - fails the assignment.
Render-only pixel-snapping policy for this drawable. Aligns the rendered origin (PixelSnapMode.Position) or origin plus shared geometry boundaries (PixelSnapMode.Geometry) to the active render target's device-pixel grid. Purely visual: logical x/y, transforms, bounds, collision, tween and physics state are never affected, and getBounds/getGlobalTransform keep returning logical values. PixelSnapMode.Geometry is guaranteed only for axis-aligned transforms; rotation or skew (on this node, an ancestor, or the view) downgrade it to PixelSnapMode.Position for the affected frame, with no logical-state change. Snapping targets device pixels (× view scale × pixel ratio), not integer world units. Setting the current value is a no-op. Setting a value outside the PixelSnapMode enum throws and leaves the prior mode unchanged.
preserveDrawOrder: boolean
When true, material-aware overlap reordering is disabled for this node's draw-order scope. Draw commands are submitted in exact document order (after scope-local z-sorting), preserving the painter's guarantee irrespective of material compatibility or AABB safety analysis. Adjacency coalescing of consecutive same-material draws still applies; it does not change visual output order.
The render mode this system's particles are drawn with. Fixed at construction via ParticleSystemOptions.render; the backend renderers read it every draw to learn the vertex layout, shader and draw model. Without that option this is the shared default mode - the same instance every other defaulted system draws with, so do not destroy it or mutate its material.
rotation: number
Rotation angle in degrees. Wraps via trimRotation on assignment.
skewX: number
Horizontal skew angle in degrees. Shears the node along the X axis (positive values lean the top edge right). Combines correctly with rotation and scale.
skewY: number
Vertical skew angle in degrees. Shears the node along the Y axis (positive values lean the left edge downward). Combines correctly with rotation and scale.
texCoords: Uint32Array
vertices: Float32Array
visible: boolean
x: number
y: number
zIndex: number
Events14
Fired when a pointer requests a context menu over this node - right-click, or a long-press/touch gesture that has an attributable pointer. Bubbles like the other pointer events, so a scene-wide fallback can listen on an ancestor. Carries no native event - whether the browser's own menu appears is decided by ApplicationOptions.input.allowNativeContextMenu, independently of this. Requires an attributable pointer: a pointerless keyboard-only request (the context-menu key, or Shift+F10, with no pointer ever having touched the surface - see ContextMenuRequest's doc comment) has nothing to hit-test or bubble with, so it never reaches this per-node event. It only ever reaches the engine-wide, scene-graph-independent app.input.onContextMenu.
Source