frame-select.ts
PURPOSE
Pure function that maps an animated VFX instance’s runtime state (age, fps, loop mode, optional driver parameter) to a single frame index inside a fixed-length frame array. Used by the VFX workbench preview/playback path to drive sprite-strip animation.
OWNS
FrameSelectArgsinterface — input shape:loopMode('oneshot' | 'loop' | 'parameterized'),frameCount: number,fps: number,ageMs: number, optionalparam?: number.pickFrameIndex(a: FrameSelectArgs): number— pure frame-index resolver. No state, no allocations, no side effects.
READS FROM
- Only its own argument struct. No imports.
PUSHES TO
- Nothing. Returns an integer in
[0, frameCount - 1].
DOES NOT
- Does not load, decode, or composite frame textures.
- Does not advance time, schedule ticks, or read
performance.now(). - Does not handle
frameCount === 0as an error — returns0defensively (the one boundary fallback in the file). - Does not interpolate between frames (integer index only).
- Does not respect playback rate other than via
fpsandageMs. - Does not emit telemetry.
Signals
loopMode === 'parameterized'—paramis clamped to[0, 1], then mapped tofloor(p * frameCount)and clamped to[0, frameCount - 1].ageMsandfpsare ignored on this branch.param ?? 0means a missingparamplays frame 0.loopMode === 'oneshot'—elapsedFrames = ageMs * fps / 1000, thenfloorand clamp to last frame. Holds on the final frame after the strip completes.loopMode === 'loop'— sameelapsedFrames, thenfloor(elapsedFrames) % frameCount; them < 0 ? m + frameCount : mbranch handles negativeageMsso the function stays total even on unexpected inputs.frameCount <= 0short-circuits to0before any branch runs.
Entry points
- Called by VFX workbench render code that picks which frame of a sprite strip to draw for a given particle/effect instance each tick. (No imports inside this file reveal the caller; it is a leaf utility.)
Pattern notes
- Pure function, no
this, no closures, no module-level state — trivially testable and safe to call inside hot render loops. - Branch order is intentional: the
parameterizedbranch returns before theelapsedFramesmath runs, so it never pays the multiply/divide cost. - All three return paths clamp to
[0, frameCount - 1]so the caller can index directly into a frames array without bounds checks. - The negative-
ageMsguard on the loop branch is the only place this file tolerates “bad” input rather than relying on caller invariants — matches a single-frame paranoia atframeCount <= 0. loopModeis a string union rather than an enum to keep this file dependency-free.
EXTRACT-CANDIDATE
Frame-index resolution from (ageMs, fps, frameCount, loopMode, param) is a generic primitive that applies to any sprite-strip animation in the engine, not just the VFX workbench. If a second consumer ever needs the same mapping (e.g. enemy idle-loop frames, UI spinners), promote this file from engine/vfx-workbench/ to a shared engine/animation/ or engine/rendering/ module without changing its surface. The parameterized mode (drive frame by a 0..1 scalar instead of time) is the part most likely to be reused — it is effectively a “lookup table sampler” and could be named as such.