KeyframeTrack

4 parts

Root of the headless keyframe track: animation keyframes laid out on a time axis, each segment carrying an editable cubic-bezier easing. It owns the keyframe array (two-way via v-model or uncontrolled via defaultValue), builds the time↔pixel projection (its own useScale when standalone, or the injected Timeline's scale when nested as a lane), and exposes the live sampler sampleAt(time) plus the easing editor binding. Transient drag positions are written to an in-flight overlay and committed on pointerup (commit); an external v-model write during a gesture is ignored (the isMutating early-return) so it never clobbers the live drag — mirroring the Timeline reconcile. Provides KeyframeTrackContext to every part: the projection, the shared frame-grid snap engine, the keyframe actions, and the roving-focus registry. When nested in a Timeline it derives duration / fps from that context and renders as a listitem; standalone it measures its own lane and renders as a group.

Demo

Loading demo…

Anatomy

Import the parts and compose them. Each part forwards attributes to its underlying element.

vue
import {
  KeyframeTrackRoot,
  KeyframeTrackKeyframe,
  KeyframeTrackSegment,
  KeyframeTrackEasingEditor,
} from '@robonen/primitives/canvas/keyframe-track';

<KeyframeTrackRoot>
  <KeyframeTrackKeyframe />
  <KeyframeTrackSegment />
  <KeyframeTrackEasingEditor />
</KeyframeTrackRoot>

API Reference

KeyframeTrackRoot

Root

Root of the headless keyframe track: animation keyframes laid out on a time axis, each segment carrying an editable cubic-bezier easing. It owns the keyframe array (two-way via `v-model` or uncontrolled via `defaultValue`), builds the time↔pixel projection (its own `useScale` when standalone, or the injected Timeline's scale when nested as a lane), and exposes the live sampler `sampleAt(time)` plus the easing editor binding. Transient drag positions are written to an in-flight overlay and committed on pointerup (`commit`); an external `v-model` write during a gesture is ignored (the `isMutating` early-return) so it never clobbers the live drag — mirroring the Timeline reconcile. Provides `KeyframeTrackContext` to every part: the projection, the shared frame-grid snap engine, the keyframe actions, and the roving-focus registry. When nested in a Timeline it derives `duration` / `fps` from that context and renders as a `listitem`; standalone it measures its own lane and renders as a `group`.

PropTypeDescription
modelValue?KeyframeTrackKeyframeData[]Controlled keyframes (v-model).
defaultValue?KeyframeTrackKeyframeData[]Uncontrolled initial keyframes (ignored when v-model is bound).
property?stringThe animated property name (drives the a11y label / value text).
valueAxis?booleanKeyframes move vertically to edit value (else a single horizontal lane).
valueRange?[number, number]Value domain [min, max] (the y-axis extent in valueAxis mode).
duration?numberTotal track duration in seconds. When omitted it is auto-derived from the keyframes (largest time) standalone, or inherited from a Timeline.
fps?numberFrame rate (timecode + frame snapping + keyboard nudge).
step?numberKeyboard nudge step in seconds.
largeStep?numberLarge keyboard step in seconds (Shift+Arrow).
valueStep?numberValue-axis keyboard nudge step (per Arrow Up/Down in valueAxis mode).
snapStep?numberSnap step in seconds (frame grid).
snapping?booleanEnable magnetic snapping to the frame grid.
allowOverlap?booleanAllow keyframes to overlap in time (else neighbour-clamped to keep order).
minTimeBetween?numberMinimum time gap between neighbours (seconds) when allowOverlap is false.
snapThresholdPx?numberSnap radius in pixels.
selectedId?string | nullSelected keyframe id (v-model:selectedId).
disabled?booleanDisable all interaction.
dir?DirectionWriting direction. When omitted it is inherited from the nearest ConfigProvider (falling back to 'ltr'); an explicit value wins.
Emits
EventPayload
update:modelValue[value: KeyframeTrackKeyframeData[]]
update:selectedId[value: string | null]

KeyframeTrackKeyframe

Keyframe

A single draggable keyframe on the track, rendered as `role="slider"`. It positions itself by its `time` (horizontal projection) and — in `valueAxis` mode — its `value` (vertical projection), and handles pointer drags plus keyboard editing. The single `aria-valuenow` carries the keyframe TIME in seconds (or its value in `valueAxis` mode); `aria-valuetext` announces the formatted time, the property name, and the value. Keyframes share one tab-stop (roving focus): Tab moves between them, the selected keyframe is the active stop. Left/Right nudge the time by `step` (Shift = `largeStep`, dir-aware, neighbour-clamped); Up/Down nudge the `value` by `valueStep` in `valueAxis` mode (else roving focus); Home/End jump the time to min/max; Delete removes the keyframe.

PropTypeDescription
keyframeIdstringThe id of the keyframe this slider renders.

KeyframeTrackSegment

Segment

The interval between two adjacent keyframes — the visual + interactive representation of a segment's easing. It spans from the keyframe identified by `keyframeId` to the next keyframe in time order, and clicking it selects the starting keyframe (so the easing editor can edit this segment's curve). It is `role="presentation"` by default (decorative); the keyframes themselves are the focusable controls. When `samples` is set it also renders an SVG `<path>` of the eased value curve across the segment (sampled via the shared spline), so consumers get a ready-to-style preview of the easing.

PropTypeDescription
keyframeIdstringThe id of the keyframe that STARTS this segment.
samples?numberWhen set, render an SVG path of the eased value curve sampled this many times across the segment (exposed as the path slot prop).

KeyframeTrackEasingEditor

EasingEditor

Embeds a `CurveEditorRoot` (in `'bezier'` interpolation) bound to the SELECTED keyframe's segment easing. The curve's two anchors are pinned at `(0, 0)` and `(1, 1)` (CSS `cubic-bezier` semantics); the start anchor's `outHandle` and the end anchor's `inHandle` map to the easing tuple `[x1, y1, x2, y2]`. The binding is one-way IN (the editor is seeded from the keyframe's easing via the CurveEditor `defaultValue`) and one-way OUT (every anchor commit reads the handles back and calls `ctx.setEasing`). The CurveEditor is remounted (keyed on the selected id) whenever the selection changes so its seed always reflects the newly selected keyframe. Renders nothing unless a keyframe with a FOLLOWING segment is selected (the last keyframe has no outgoing segment to ease).

PropTypeDescription
samples?numberSample count for the rendered easing polyline.