v0.7.1 (build 925)

What this page covers #

Time-based keyframe animation for renderables: position/origin/rotation/scale/colour/opacity, sprite frame stepping, shader params, and procedural noise modulation.

By task #

Overview #

Animations are evaluated using monotonic engine time in double-precision seconds. You provide keyframes with any subset of supported fields; missing fields are left unchanged. Animations are visual and compositional, and do not redefine parent layout flow.

Concepts #

  • Build animations by providing an array of frames, each with a time in seconds and any properties you want to animate. Only properties you include are animated; others are left as-is.
  • Animations use double-precision engine time; startAnimation uses the current engine time so multiple animations can start in sync.
  • Time evaluation is suitable for long-running sessions (months to years) without visible quantization steps in normal UI transition ranges.
  • When the animation reaches the last keyframe time it completes once; if looped, it restarts automatically. Use onAnimationEnd to run code when a non-looping animation finishes.
  • Dynamic t:0 start: if an animation has no authored keyframe at time: 0.0, starting it injects a runtime t:0 keyframe from the element's current visual state. This enables smooth handoff between clips without snapping back to baseline.
  • Persistent holds: setAnimationTime pins an element to an authored animation pose at a clip-local time until it is cleared or normal playback is started. Times outside the authored range clamp to the first or last keyframe.

Sampling and persistent holds #

UI.setAnimationTime(windowId, elementId, animationId, localTime) holds the renderable at a deterministic authored pose. Sampling is side-effect-free: it does not start, complete, or loop the clip and never fires onAnimationEnd. It uses authored keyframes only, so a runtime dynamic t:0 pose captured by earlier playback cannot affect the result. Values before the first keyframe clamp to the first pose, and values at or after the final keyframe clamp to the final pose.

The hold persists across engine frames. Procedural noise uses the fixed localTime while held, making the complete pose deterministic; ordinary playback continues to use absolute engine time for noise. Setting another hold switches directly to that animation. clearAnimationTime removes the hold and disables the current animation without callbacks. Starting a valid animation also releases the hold and resumes normal playback semantics.

Import #

import * as UI from 'Helix/UserInterface';

Keyframe fields (complete reference) #

KeyFrame
A single animation keyframe. Include any subset of fields; missing fields are left unchanged.
  • time number - Seconds from the start of the animation when this keyframe takes effect. Parsed as double-precision.
  • ease string optional - Easing for the transition into this keyframe from the previous one. Default 'linear'.
  • position { x, y } optional - Pixel offset applied additively on top of the renderable's own position.
  • origin { x, y } optional - Pivot point in local pixels used when rotating/scaling.
  • rotation number optional - Degrees. Absolute target orientation. 0 -> 360 spins once.
  • scale { x, y } optional - Absolute target scale, interpolated per-axis.
  • colour [r,g,b,a] | string optional - Tint colour. Accepts [r,g,b,a] arrays or hex strings (#RGB, #RGBA, #RRGGBB, #RRGGBBAA, 0xRRGGBB[AA]). Interpolated per-channel. Also accepted as color.
  • opacity number optional - 0..1. Multiplies only the alpha channel. Propagates down the UI subtree so you can fade whole branches.
  • frame number optional - Sprite-only. Switches to the given sprite frame index at that keyframe time.
  • noise object optional - Procedural modulation applied additively from this key to the next (see Noise fields below).
  • shaderParams { v0?: [x,y,z,w], v1?: [x,y,z,w] } optional - Push-constant vectors for shaders. Overrides the window's global shader params when present.
  • looping boolean optional - If true on any keyframe, the animation loops when reaching the last keyframe.
NoiseOptions
Procedural noise modulation applied additively between keyframes.
  • type string - One of: value, valueFractal, perlin, perlinFractal, simplex, simplexFractal, cellular, whiteNoise, cubic, cubicFractal.
  • seed number optional - Random seed. Default 1337.
  • timeScale { x, y } optional - Scales the time fed into the noise per-axis.
  • octaves number optional - Noise octaves. Default 4.
  • frequency number optional - Noise frequency. Default 1.
  • position { x, y } optional - Amplitude applied to position channels.
  • scale { x, y } optional - Amplitude applied to scale channels.
  • rotation number optional - Amplitude in degrees applied to rotation.

Supported easing strings #

  • linear
  • Quadratic: inQuad, outQuad, inOutQuad
  • Cubic: inCubic, outCubic, inOutCubic
  • Quartic: inQuart, outQuart, inOutQuart
  • Quintic: inQuint, outQuint, inOutQuint

Opacity inheritance #

Opacity propagates down the UI subtree: a parent's opacity multiplies its children's, so you can fade whole branches with a single key.

Relationship to colour.a: final alpha = (colour.a if present else 1) × opacity × inherited parent opacities.

Looping #

Add { looping: true } on any frame to loop the animation. The animation restarts automatically when it reaches the last keyframe.

API #

UI.addAnimation
Register an animation and return its id.
  • frames is an Array<KeyFrame> with any subset of supported fields per keyframe.
UI.addAnimation(windowId, elementId, frames): number
UI.updateAnimation
Replace an existing animation's keyframes while retaining its id and completion callback.
  • frames replaces the complete animation definition, including timing, easing, animated fields, and looping state.
  • If the animation is active, it restarts immediately. A replacement without an authored time: 0 frame transitions from the current visible pose; an explicit zero-time frame is applied as authored. Inactive animations use the replacement on their next start.
  • Returns false for an unknown animation id or an empty frame array.
UI.updateAnimation(windowId, elementId, animationId, frames): boolean
UI.startAnimation
Start or restart the animation at t=0 using the current engine time.
UI.startAnimation(windowId, elementId, animationId)
UI.stopAnimation
Stop the active animation (internally starts animation -1).
UI.stopAnimation(windowId, elementId)
UI.setAnimationTime
Hold the element at an authored clip-local animation time.
  • The hold persists until clearAnimationTime or a valid startAnimation call.
  • Sampling clamps outside the authored keyframe range and suppresses completion callbacks and looping.
  • Returns false for an invalid window, element, renderable, animation id, or non-finite time; existing animation state is left unchanged.
UI.setAnimationTime(windowId, elementId, animationId, localTime): boolean
UI.clearAnimationTime
Clear an animation hold and directly disable the current animation without callbacks.
  • Returns false for an invalid window, element, or renderable.
UI.clearAnimationTime(windowId, elementId): boolean
UI.onAnimationEnd
Register a callback that fires when a non-looping animation completes.
  • cb receives (time) — the engine time at completion.
  • Fires once per startAnimation call.
UI.onAnimationEnd(windowId, elementId, animationId, cb)

Examples #

Position + colour pulse #

Combines horizontal movement with an alpha fade to create a looping attention pulse. Useful for drawing the eye to a notification badge or active indicator.

const anim = await UI.addAnimation(win, id, [
  { time: 0.0, position: { x: 0, y: 0 }, colour: [1,1,1,1] },
  { time: 0.5, position: { x: 20, y: 0 }, colour: [1,1,1,0.6], ease: 'inOutQuad' },
  { time: 1.0, position: { x: 0, y: 0 }, colour: [1,1,1,1], ease: 'inOutQuad', looping: true }
]);
await UI.startAnimation(win, id, anim);

Sprite frame stepping #

Cycles through sprite atlas frames at fixed intervals to play a looping frame-by-frame animation. This is the standard pattern for animated icons, loading spinners, or character walk cycles.

const anim = await UI.addAnimation(win, spriteId, [
  { time: 0.0, frame: 0 },
  { time: 0.1, frame: 1 },
  { time: 0.2, frame: 2 },
  { time: 0.3, frame: 3, looping: true }
]);
await UI.startAnimation(win, spriteId, anim);

Perlin noise sway #

Applies continuous Perlin noise to an element's position for organic, non-repeating idle motion. Good for ambient sway on foliage, floating particles, or gentle UI wobble.

const anim = await UI.addAnimation(win, id, [
  {
    time: 0.0,
    noise: { type: 'perlin', seed: 42, timeScale: { x: 1, y: 1 }, position: { x: 15, y: 0 } }
  },
  {
    time: 2.0,
    noise: { type: 'perlin', seed: 42, timeScale: { x: 1, y: 1 }, position: { x: 15, y: 0 } },
    looping: true
  }
]);
await UI.startAnimation(win, id, anim);

Animate shader parameters #

Drives custom shader push-constant vectors over time, letting you animate GPU-side effects like colour sweeps, dissolve thresholds, or procedural patterns without rebuilding geometry.

const shaded = await UI.createElement(win, {
  renderable: {
    type: 'box',
    shader: {
      vert: '/rom/shaders/texture_only.vert.spv',
      frag: '/rom/shaders/texture_only.frag.spv'
    }
  }
});
await UI.attach(win, root, shaded);

const anim = await UI.addAnimation(win, shaded, [
  { time: 0.0, shaderParams: { v0: [0.0, 0.0, 0.0, 0.0] } },
  { time: 0.5, shaderParams: { v0: [1.0, 0.0, 0.0, 0.0] }, ease: 'inOutQuad' },
  { time: 1.0, shaderParams: { v0: [0.0, 0.0, 0.0, 0.0] }, ease: 'inOutQuad', looping: true }
]);
await UI.startAnimation(win, shaded, anim);

Fade a branch with opacity #

Fades an entire UI subtree by animating the parent's opacity. Because opacity is inherited, this single animation hides all children — ideal for dismissing panels, tooltips, or notification groups.

const fade = await UI.addAnimation(win, id, [
  { time: 0.0, opacity: 1.0 },
  { time: 0.3, opacity: 0.0, ease: 'outQuad' }
]);
await UI.startAnimation(win, id, fade);

Dynamic t:0 handoff between clips #

Chains two animations so each starts from the element's current visual state instead of snapping to a fixed origin. The onAnimationEnd callback alternates clips, producing a seamless back-and-forth motion without authoring explicit time: 0 keyframes.

const clipA = await UI.addAnimation(win, id, [
  { time: 1.0, position: { x: 120, y: -70 }, rotation: 35, scale: { x: 1.35, y: 0.70 } }
]);
const clipB = await UI.addAnimation(win, id, [
  { time: 1.0, position: { x: -110, y: 85 }, rotation: -30, scale: { x: 0.72, y: 1.25 } }
]);

let nextA = true;
const playNext = async () => {
  if (nextA) {
    await UI.startAnimation(win, id, clipA);
    nextA = false;
    return;
  }
  await UI.startAnimation(win, id, clipB);
  nextA = true;
};

await UI.onAnimationEnd(win, id, clipA, playNext);
await UI.onAnimationEnd(win, id, clipB, playNext);
await playNext();

Notes #

  • Animations run per-renderable; different elements maintain separate animation ids.
  • Colour animations are multiplied with any renderable-specific tint (e.g. Sprite's tintColor). Colours update every frame without requiring a geometry rebuild.
  • Opacity multiplies final alpha and is inherited by children. Combine with colour alpha for fine control.
  • Non-looping animations fire onAnimationEnd once per start.
  • shaderParams on a keyframe override the window's global params for that element while active. See Shaders page for push-constant layout and default shaders.
  • Dynamic t:0 applies only when no authored time: 0.0 keyframe exists. If time: 0.0 is authored, that explicit frame is used as-is.
  • Animation holds sample authored keyframes only; runtime dynamic t:0 captures remain exclusive to ordinary playback.
  • Easing is destination-based: the ease on keyframe B shapes interpolation from keyframe A to B.
  • If a time: 0.0 keyframe is authored, its ease has no incoming segment and is therefore not used.