v0.7.1 (build 925)

particles creates a modular GPU-simulated emitter as an element's renderable. Shape, motion, appearance, and noise modules cover sparks, smoke, fire, trails, rings, bursts, and similar Unity-style effects without copying Unity's inspector one-for-one. A particle detaches after spawning, so moving the element naturally leaves a trail.

Imports #

import * as UI from 'Helix/UserInterface';
import * as Particles from 'Helix/Renderable';

Create an emitter #

const emitter = await UI.createElement(win, {
  renderable: {
    type: 'particles',
    texture: '/rom/effects/sparks.png',
    frameMode: 'random',
    position: { x: 200, y: 240 },
    config: {
      running: true,
      main: { seed: 1001, lifetime: { min: 1.0, max: 1.8 } },
      emission: { rate: 90 },
      shape: {
        type: 'ellipse-edge', radius: { x: 12, y: 5 },
        arc: { start: 0, angle: Math.PI },
        direction: 'outward', coneAngle: 0.25
      },
      motion: {
        speed: { min: 75, max: 135 },
        acceleration: { x: 0, y: 105 },
        velocityJitter: { x: 15, y: 8 },
        drag: { curve: [{ time: 0, value: 0 }, { time: 1, value: 2.5 }] },
        noise: {
          strength: { x: 28, y: 10 }, frequency: 0.025,
          scrollSpeed: 0.8, octaves: 3,
          strengthOverLifetime: [{ time: 0, value: 0.2 }, { time: 0.4, value: 1 }, { time: 1, value: 0 }]
        }
      },
      appearance: {
        size: {
          minCurve: [{ time: 0, value: 7 }, { time: 0.25, value: 13 }, { time: 1, value: 1 }],
          maxCurve: [{ time: 0, value: 10 }, { time: 0.25, value: 17 }, { time: 1, value: 3 }]
        },
        colour: [
          { time: 0, colour: '#fff0a0ff' },
          { time: 0.35, colour: '#ff5c12ee' },
          { time: 1, colour: '#4d080000' }
        ],
        initialRotation: { min: -Math.PI, max: Math.PI },
        angularVelocity: { min: -5, max: 5 }
      }
    }
  }
});
await UI.attachRoot(win, emitter);
Note

A Koya particle emitter starts running by default; set running: false for a burst-only or manually started emitter. It is created in the overlay channel. Omitting texture retains the original white quad, tinted by the configured colours.

Definition #

ParticleRenderable
Element renderable that submits a GPU particle emitter.
  • type 'particles' - Renderable type identifier.
  • config ParticleEmitterConfig optional - Emitter configuration. Fields may alternatively be placed directly beside type.
  • texture string optional - Image asset sampled by every particle. The full image is used when frames is omitted; no texture uses a white fallback quad.
  • frames ParticleFrame[] optional - Pixel-coordinate rectangles within texture. Empty or omitted means one full-image frame.
  • frameMode 'fixed' | 'random' | 'over-lifetime' optional - Frame selection strategy. Default fixed.
  • frame number optional - Zero-based frame used by fixed, clamped to the available frame count. Default 0.
  • position { x: number, y: number } optional - Emitter position in the element's logical coordinate space. Default { x: 0, y: 0 }.
  • origin { x: number, y: number } optional - Transform origin inherited from shared renderable properties.
  • scale { x: number, y: number } optional - Emitter transform scale. It affects the spawn position/direction transform; spawned particles are detached.
  • rotation number optional - Emitter transform rotation in degrees, consistent with other Koya renderables. Spawned particles retain the transformed direction after emission.
ParticleEmitterConfig
Modules describing particle lifetime, spawning, motion, and appearance.
  • channel 'overlay' | 'world' optional - Runtime channel used by setParticleConfig. UI.createElement always creates the emitter as overlay and ignores this field during creation. The full-replacement runtime default is world.
  • running boolean optional - Creation-only initial running state. Default true. Runtime reconfiguration uses startParticles and stopParticles.
  • main ParticleMainModule optional - Seed and lifetime shared by all particles.
  • emission ParticleEmissionModule optional - Continuous spawning controls.
  • shape ParticleShapeModule optional - Spawn region and initial direction.
  • motion ParticleMotionModule optional - Initial speed, force, drag, jitter, and coherent noise.
  • appearance ParticleAppearanceModule optional - Size, colour, and rotation over normalized lifetime.
Particle modules
Module fields are optional and compose independently.
  • main { seed?: number, lifetime?: ParticleScalarRange } - lifetime is seconds and is clamped above zero.
  • emission { rate?: number } - Particles per second; fractional rates accumulate across frames.
  • shape ParticleShapeModule - type: point, line, box, box-edge, ellipse, ellipse-edge (aliases: circle, disc, ring). size is the line vector or box dimensions; radius is the ellipse radii. offset, innerRadius (0–1), arc: { start, angle }, and positionJitter further restrict or vary the region. direction is fixed, outward, inward, tangent-cw, or tangent-ccw; vector supplies fixed direction and coneAngle adds randomized angular spread.
  • motion ParticleMotionModule - speed is a scalar range; acceleration (alias force) is constant. velocityJitter accepts a symmetric point or { min: Point, max: Point }. drag is a ParticleValue. noise configures coherent velocity and display variation.
  • appearance ParticleAppearanceModule - size is a ParticleValue, colour/color a ParticleGradient, and initialRotation and angularVelocity are scalar ranges in radians.
ParticleNoiseModule
Coherent fractal noise evaluated in world/time space, not independent frame-to-frame randomness.
  • strength number | Point optional - Velocity acceleration amplitude per axis.
  • frequency number optional - Spatial frequency; default 0.05.
  • scrollSpeed number optional - Rate at which the field moves through time.
  • octaves 1 | 2 | 3 | 4 optional - Fractal detail layers.
  • strengthOverLifetime ParticleValue optional - Multiplies velocity-noise strength over lifetime.
  • positionAmount number | Point optional - Visual position displacement without changing velocity.
  • rotationAmount number optional - Visual rotation modulation in radians.
  • sizeAmount number optional - Visual size modulation.
ParticleFrame
A pixel rectangle inside the source texture. { aabb: { min, max } } is also accepted.
  • min { x: number, y: number } - Inclusive top-left pixel coordinate.
  • max { x: number, y: number } - Exclusive bottom-right pixel coordinate.
ParticleScalarRange
Either one fixed number or an inclusive random range { min, max }.
  • min number optional - Lower endpoint. If greater than max, the endpoints are exchanged.
  • max number optional - Upper endpoint. Defaults to min when omitted.
ParticleValue
A scalar constant, curve, or randomized blend between two curves.
  • curve { time: number, value: number }[] optional - One lifetime curve. Passing the key array directly is equivalent.
  • minCurve { time: number, value: number }[] optional - Minimum lifetime curve.
  • maxCurve { time: number, value: number }[] optional - Maximum lifetime curve. Each particle chooses a stable blend between min and max.
ParticleGradient
A fixed colour, gradient, or randomized blend between two gradients.
  • gradient { time: number, colour: Colour }[] optional - One lifetime gradient. Passing the key array directly is equivalent.
  • minGradient { time: number, colour: Colour }[] optional - Minimum lifetime gradient.
  • maxGradient { time: number, colour: Colour }[] optional - Maximum lifetime gradient. Each particle chooses a stable blend between min and max.

Values, curves, and gradients #

// Fixed or randomized scalar
motion: { speed: 80 }
motion: { speed: { min: 60, max: 100 } }

// One curve, or a randomized value between two curves
appearance: { size: [
  { time: 0, value: 2 },
  { time: 0.3, value: 12 },
  { time: 1, value: 0 }
] }
appearance: { size: { minCurve: [...], maxCurve: [...] } }

// One gradient, or a randomized blend between two gradients
appearance: { colour: [
  { time: 0, colour: '#ffffffff' },
  { time: 1, colour: '#ffffff00' }
] }
appearance: { colour: { minGradient: [...], maxGradient: [...] } }
  • Curve and gradient key time is normalized particle lifetime from 0 to 1. Keys are sorted and linearly interpolated; at most four keys are retained.
  • A ParticleValue accepts a number, a key array, { curve }, { min, max }, or { minCurve, maxCurve }. Each particle gets a stable random blend between its minimum and maximum curve.
  • A ParticleGradient accepts a fixed colour, a gradient key array, { gradient }, or { minGradient, maxGradient }. Koya colours use normalized arrays or hexadecimal strings.
  • Changing a curve, gradient, drag, or noise profile also affects the emitter's already-live particles; spawn position, direction, speed, and lifetime remain fixed at birth.

Runtime control #

Particle mutation uses a renderable handle. Resolve the element's handle with UI.getRenderable, then pass it to Helix/Renderable. All calls are asynchronous and should normally be awaited.

const handle = await UI.getRenderable(win, emitter);

await Particles.setParticleTexture(handle, '/rom/effects/explosion.png');
await Particles.setParticleFrames(handle, {
  frames: [
    { min: { x: 0, y: 0 }, max: { x: 64, y: 64 } },
    { min: { x: 64, y: 0 }, max: { x: 128, y: 64 } }
  ],
  frameMode: 'over-lifetime'
});
await Particles.burstParticles(handle, 200);
await Particles.stopParticles(handle);  // live particles finish naturally
await Particles.startParticles(handle);
await Particles.clearParticles(handle); // immediately retires this emitter's live particles
UI.getRenderable
Returns the opaque renderable handle required by Helix/Renderable.
UI.getRenderable(windowId, elementId): Promise<number>
Particles.setParticleConfig
Replaces the complete emitter configuration. This is not a partial merge; omitted fields return to their defaults. Include channel: 'overlay' to retain Koya overlay placement.
Particles.setParticleConfig(handle, config): Promise<boolean>
Particles.setParticleTexture
Changes the texture used by future spawns. Existing particles retain their atlas frame.
Particles.setParticleTexture(handle, path): Promise<boolean>
Particles.setParticleFrames
Changes frame rectangles and selection for future spawns. Passing an array directly is shorthand for fixed mode, frame 0.
Particles.setParticleFrames(handle, { frames, frameMode, frame }): Promise<boolean>
Particles.clearTexture
Restores the white fallback for future spawns; existing particles are unchanged.
Particles.clearTexture(handle): Promise<boolean>
Particles.startParticles
Starts continuous rate-based emission. Calling it repeatedly does not duplicate the emitter.
Particles.startParticles(handle): Promise<boolean>
Particles.stopParticles
Stops future continuous emission. Existing detached particles live until expiry.
Particles.stopParticles(handle): Promise<boolean>
Particles.burstParticles
Requests one explicit burst. A burst works while continuous emission is stopped; count 0 has no effect.
Particles.burstParticles(handle, count): Promise<boolean>
Particles.clearParticles
Requests retirement of all live particles spawned by this emitter on the next main particle simulation, without changing its running state.
Particles.clearParticles(handle): Promise<boolean>
Warning

setParticleConfig replaces every configuration field but does not change the emitter's running state. Running state is controlled separately. Its colour parser accepts normalized arrays or hexadecimal strings. Always include channel: 'overlay' for the normal Koya overlay path; the runtime config default is world.

Channels and composition #

ChannelPlacementPost-processingTypical use
overlayAfter ordinary UI and final compositionNot affected by scene/UI post-FXKoya effects, confetti, cursor trails
worldAt the renderer's scene/UI boundaryReceives scene-target post-FXShared renderer integrations; uncommon in a UI-only Koya window
  • UI.createElement initially creates Koya emitters in overlay, regardless of a creation-time channel field.
  • Overlay particles are deliberately outside element clipping, masks, sibling order, subtree compositing, and composite opacity. They can escape a clipped parent and draw above other UI.
  • The element supplies only the emitter transform. A spawned particle is detached from the element, so moving or animating the element produces a trail instead of dragging existing particles.
  • Rendering an element into an offscreen texture does not simulate or draw its particles. The emitter is submitted once to the next main window frame.

Lifecycle, capacity, and determinism #

  • Each window/renderer owns an independent shared pool. The default capacity is 65,536 live particles across both channels and all emitters in that window.
  • Each renderer also owns one fixed 1024×1024×8 RGBA atlas. Texture paths are decoded and appended on first use, cached by path, and never repacked, so repeated explosions do not upload or allocate the same texture again.
  • Atlas entries are immutable for the renderer lifetime. If generated /ram image bytes change, publish them under a new asset path before calling setParticleTexture; reusing the same path deliberately reuses the existing atlas entry.
  • First use decodes and uploads the image synchronously. Configure commonly used explosion and smoke textures during UI/window setup to prewarm the atlas before latency-sensitive effects.
  • Texture and frame changes affect new spawns only. Live particles keep stable atlas coordinates, preventing an animation or explosion from changing underneath them.
  • A source texture must fit within one 1022×1022 atlas page after padding. If loading, decoding, atlas space, or the 4096-frame table fails, future particles use the white fallback and rendering continues.
  • When the pool is full, excess spawns are dropped. Existing particles are not replaced, and normal rendering performs no particle-count readback.
  • Expired and explicitly cleared slots are recycled on the GPU.
  • Destroying the element stops future submissions but does not kill particles already detached from it. For explicit cleanup, call clearParticles and keep the enabled emitter attached until a main renderer frame has processed the request before destroying it.
  • The seed and logical spawn sequence determine sampled attributes independently of GPU slot allocation. Matching emitters given the same spawn timing and commands produce the same attribute stream; bit-identical floating-point motion across different GPU vendors is not guaranteed.
  • Device/backend recreation discards transient live particles. A still-running emitter resumes continuous emission without replaying old bursts.

Current limitations #

  • Textured particles use one image, optional sprite-sheet frames, colour tinting, and premultiplied-alpha blending. There are no per-particle material or sampler overrides.
  • There is no depth or distance sorting, collision, local-space attachment, per-element clipping, runtime atlas eviction/repacking, arbitrary material batching, runtime pool growth, or CPU particle telemetry.
  • The shared pool is fixed at renderer initialization; JavaScript cannot resize it at runtime.

Visual test #

The bundled particle-visual-test.js example exercises line, box, ellipse, ring, and arc emission; min/max curves and gradients; drag; coherent velocity/position noise; textured and fallback particles; moving detached trails; seeded twins; clipping independence; dense recycling; post-FX ordering; and Vulkan screenshot capture.