API reference
Every public class, method, and event in @codexo/exojs. Generated from source.
classParticleSystem
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.
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.
new(texture: TextureA static GPU texture sourced from an image, canvas, or video element. Holds the pixel source, its sampling state (ScaleModes, WrapModes) and its upload state (…, options?: ParticleSystemOptionsOptions for ParticleSystem's constructor - orthogonal config that's independent of the texture source. Texture / frames / spritesheet live in positional argume…): ParticleSystemnew(texture: TextureA static GPU texture sourced from an image, canvas, or video element. Holds the pixel source, its sampling state (ScaleModes, WrapModes) and its upload state (…, frames: readonly RectangleMutable axis-aligned rectangle defined by a top-left origin `(x, y)` and dimensions `(width, height)`. Implements Collidable with full SAT collision response f…[], options?: ParticleSystemOptionsOptions for ParticleSystem's constructor - orthogonal config that's independent of the texture source. Texture / frames / spritesheet live in positional argume…): ParticleSystemnew(spritesheet: SpritesheetSlices a single Texture into named frames and optional named animation sequences. Each frame is stored as both a Rectangle (the pixel region) and a pre-configu…, options?: ParticleSystemOptionsOptions for ParticleSystem's constructor - orthogonal config that's independent of the texture source. Texture / frames / spritesheet live in positional argume…): ParticleSystem_updateOrigin(): voidblur(): thisclearDeathModules(): thisclearFilters(): thisclearParticles(): thisclearSpawnModules(): thisclearUpdateModules(): thiscollidesWith(target: CollidableContract for objects that participate in collision detection. Implemented by all concrete shape classes as well as `SceneNode`.): CollisionResponseResult of a successful Collidable.collidesWith call. Contains the two participating shapes, the penetration depth, containment flags, and the minimum-translati… | nullcontains(x: number, y: number): booleandestroy(): voidfocus(): thisgetBounds(out?: RectangleMutable axis-aligned rectangle defined by a top-left origin `(x, y)` and dimensions `(width, height)`. Implements Collidable with full SAT collision response f…): RectangleMutable axis-aligned rectangle defined by a top-left origin `(x, y)` and dimensions `(width, height)`. Implements Collidable with full SAT collision response f…invalidateCache(): thisinvalidateContent(): thismove(x: number, y: number): thisproject(axis: VectorConcrete mutable 2D vector with full AbstractVector arithmetic and Collidable collision support (treated as a point collider). `Vector.temp` provides a shared…, result: IntervalA closed scalar interval `[min, max]` used by the SAT collision solver to represent the projection of a shape onto a separating axis. `Interval.temp` provides…): IntervalA closed scalar interval `[min, max]` used by the SAT collision solver to represent the projection of a shape onto a separating axis. `Interval.temp` provides…render(backend: RenderBackend): thisresetTextureFrame(): thisrotate(degrees: number): thissetAnchor(x: number, y: number): thissetLocalBounds(x: number, y: number, width: number, height: number): thissetOrigin(x: number, y: number): thissetPosition(x: number, y: number): thissetRotation(degrees: number): thissetScale(x: number, y: number): thissetSkew(x: number, y: number): thisupdateBounds(): thisupdateParentTransform(): thisupdateTransform(): thiscapacity: numbercursor: null | stringdraggable: booleanfocusable: booleanname: null | stringtabIndex: numberaliveCount: numbercacheAsTexture: booleancacheResolution: TargetResolutionclip: booleanclipShape: GeometryNon-renderable geometry data object used by advanced rendering paths. Geometry owns only vertex/index data and its layout metadata; it is not a scene node, has… | RectangleMutable axis-aligned rectangle defined by a top-left origin `(x, y)` and dimensions `(width, height)`. Implements Collidable with full SAT collision response f… | nullcullable: booleandestroyed: booleangpuMode: booleangpuState: ParticleGpuState | nullhasAtlas: booleaninteractive: booleanisAlignedBox: booleanliveCount: numberpreserveDrawOrder: booleanrotation: numberskewX: numberskewY: numbertexCoords: Uint32Arrayvertices: Float32Arrayvisible: booleanx: numbery: numberzIndex: numberonBlur: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[RenderNodeSceneNode that can produce visual output. Adds the rendering pipeline features on top of the structural transform/bounds carried by SceneNode: post-process `fi…]>onContextMenu: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>onDrag: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>onDragEnd: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>onDragStart: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>onFocus: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[RenderNodeSceneNode that can produce visual output. Adds the rendering pipeline features on top of the structural transform/bounds carried by SceneNode: post-process `fi…]>onKeyDown: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[KeyEventEnvelope dispatched by `app.interaction` to the focused RenderNode for keyboard input, then bubbled up its entire parent chain - same DOM-style shape as Intera…]>onKeyUp: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[KeyEventEnvelope dispatched by `app.interaction` to the focused RenderNode for keyboard input, then bubbled up its entire parent chain - same DOM-style shape as Intera…]>onPointerDown: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>onPointerMove: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>onPointerOut: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>onPointerOver: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>onPointerTap: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>onPointerUp: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[InteractionEventDOM-Event-shaped envelope dispatched by InteractionSystem to interactive scene nodes. Bubbles up the *entire* parent chain - `target` stays pinned to the hit-d…]>