KeyframeTrack
4 partsRoot 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
Anatomy
Import the parts and compose them. Each part forwards attributes to its underlying element.
import {
KeyframeTrackRoot,
KeyframeTrackKeyframe,
KeyframeTrackSegment,
KeyframeTrackEasingEditor,
} from '@robonen/primitives/canvas/keyframe-track';
<KeyframeTrackRoot>
<KeyframeTrackKeyframe />
<KeyframeTrackSegment />
<KeyframeTrackEasingEditor />
</KeyframeTrackRoot>API Reference
KeyframeTrackRoot
RootRoot 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`.
| Prop | Type | Description |
|---|---|---|
modelValue? | KeyframeTrackKeyframeData[] | Controlled keyframes (v-model). |
defaultValue? | KeyframeTrackKeyframeData[] | Uncontrolled initial keyframes (ignored when v-model is bound). |
property? | string | The animated property name (drives the a11y label / value text). |
valueAxis? | boolean | Keyframes 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? | number | Total track duration in seconds. When omitted it is auto-derived from the
keyframes (largest time) standalone, or inherited from a Timeline. |
fps? | number | Frame rate (timecode + frame snapping + keyboard nudge). |
step? | number | Keyboard nudge step in seconds. |
largeStep? | number | Large keyboard step in seconds (Shift+Arrow). |
valueStep? | number | Value-axis keyboard nudge step (per Arrow Up/Down in valueAxis mode). |
snapStep? | number | Snap step in seconds (frame grid). |
snapping? | boolean | Enable magnetic snapping to the frame grid. |
allowOverlap? | boolean | Allow keyframes to overlap in time (else neighbour-clamped to keep order). |
minTimeBetween? | number | Minimum time gap between neighbours (seconds) when allowOverlap is false. |
snapThresholdPx? | number | Snap radius in pixels. |
selectedId? | string | null | Selected keyframe id (v-model:selectedId). |
disabled? | boolean | Disable all interaction. |
dir? | Direction | Writing direction. When omitted it is inherited from the nearest
ConfigProvider (falling back to 'ltr'); an explicit value wins. |
| Event | Payload |
|---|---|
update:modelValue | [value: KeyframeTrackKeyframeData[]] |
update:selectedId | [value: string | null] |
KeyframeTrackKeyframe
KeyframeA 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.
| Prop | Type | Description |
|---|---|---|
keyframeId | string | The id of the keyframe this slider renders. |
KeyframeTrackSegment
SegmentThe 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.
| Prop | Type | Description |
|---|---|---|
keyframeId | string | The id of the keyframe that STARTS this segment. |
samples? | number | When set, render an SVG path of the eased value curve sampled this many
times across the segment (exposed as the path slot prop). |
KeyframeTrackEasingEditor
EasingEditorEmbeds 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).
| Prop | Type | Description |
|---|---|---|
samples? | number | Sample count for the rendered easing polyline. |