UI/Animation
Time-based keyframe animation for renderables with easing and noise modulation.
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 #
- Fade a whole subtree: see Fade a branch with opacity
- Add idle sway/wobble: see Perlin noise sway
- Drive shader push constants over time: see Examples
- Check valid keyframe fields and easing names: see Keyframe fields
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
timein 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;
startAnimationuses 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
onAnimationEndto run code when a non-looping animation finishes. - Dynamic
t:0start: if an animation has no authored keyframe attime: 0.0, starting it injects a runtimet:0keyframe from the element's current visual state. This enables smooth handoff between clips without snapping back to baseline. - Persistent holds:
setAnimationTimepins 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) #
timenumber - Seconds from the start of the animation when this keyframe takes effect. Parsed as double-precision.easestring 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.rotationnumber optional - Degrees. Absolute target orientation.0 -> 360spins 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 ascolor.opacitynumber optional -0..1. Multiplies only the alpha channel. Propagates down the UI subtree so you can fade whole branches.framenumber optional - Sprite-only. Switches to the given sprite frame index at that keyframe time.noiseobject 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.loopingboolean optional - Iftrueon any keyframe, the animation loops when reaching the last keyframe.
typestring - One of:value,valueFractal,perlin,perlinFractal,simplex,simplexFractal,cellular,whiteNoise,cubic,cubicFractal.seednumber optional - Random seed. Default1337.timeScale{ x, y } optional - Scales the time fed into the noise per-axis.octavesnumber optional - Noise octaves. Default4.frequencynumber optional - Noise frequency. Default1.position{ x, y } optional - Amplitude applied to position channels.scale{ x, y } optional - Amplitude applied to scale channels.rotationnumber 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 #
framesis anArray<KeyFrame>with any subset of supported fields per keyframe.
UI.addAnimation(windowId, elementId, frames): numberframesreplaces 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: 0frame 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
falsefor an unknown animation id or an empty frame array.
UI.updateAnimation(windowId, elementId, animationId, frames): booleanUI.startAnimation(windowId, elementId, animationId)-1).UI.stopAnimation(windowId, elementId)- The hold persists until
clearAnimationTimeor a validstartAnimationcall. - Sampling clamps outside the authored keyframe range and suppresses completion callbacks and looping.
- Returns
falsefor an invalid window, element, renderable, animation id, or non-finite time; existing animation state is left unchanged.
UI.setAnimationTime(windowId, elementId, animationId, localTime): boolean- Returns
falsefor an invalid window, element, or renderable.
UI.clearAnimationTime(windowId, elementId): booleancbreceives(time)— the engine time at completion.- Fires once per
startAnimationcall.
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
onAnimationEndonce per start. shaderParamson 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:0applies only when no authoredtime: 0.0keyframe exists. Iftime: 0.0is authored, that explicit frame is used as-is. - Animation holds sample authored keyframes only; runtime dynamic
t:0captures remain exclusive to ordinary playback. - Easing is destination-based: the
easeon keyframe B shapes interpolation from keyframe A to B. - If a
time: 0.0keyframe is authored, itseasehas no incoming segment and is therefore not used.