Documentation

Keyframes

Animate registered clip properties with engine-level keyframes, headless React hooks, and canvas-rendered segments.

Browse documentation

Keyframes are generic scalar animation data stored on clips and evaluated by the core engine. The engine does not reserve opacity, volume, scale, or any other property id. Apps register the properties they want to keyframe, then the same engine, React hooks, and renderer geometry work for every registered property.

import {
TimelineEngine,
createTimelineScalarKeyframeProperty,
fromSeconds,
type Track,
} from '@techsquidtv/canvas-timeline';
const opacityProperty = createTimelineScalarKeyframeProperty({
id: 'opacity',
label: 'Opacity',
min: 0,
max: 1,
defaultValue: 1,
formatValue: (value) => `${Math.round(value * 100)}%`,
getBaseValue: (clip) => clip.opacity ?? 1,
});
const tracks: Track<'visual'>[] = [
{
id: 'visual-1',
kind: 'visual',
selected: true,
locked: false,
muted: false,
visible: true,
clips: [
{
id: 'intro',
sourceId: 'source-intro',
timelineStart: fromSeconds(0),
timelineEnd: fromSeconds(8),
sourceStart: fromSeconds(0),
selected: true,
keyframes: [
{ id: 'fade-in-start', property: 'opacity', time: fromSeconds(0), value: 0 },
{ id: 'fade-in-end', property: 'opacity', time: fromSeconds(2), value: 1 },
{
id: 'fade-out-start',
property: 'opacity',
time: fromSeconds(6),
value: 1,
outgoing: { interpolation: 'bezier', handle: { x: 0.42, y: 0 } },
},
{
id: 'fade-out-end',
property: 'opacity',
time: fromSeconds(8),
value: 0,
incoming: { interpolation: 'bezier', handle: { x: 0.58, y: 1 } },
},
],
},
],
},
];
const engine = new TimelineEngine({
tracks,
keyframeProperties: [opacityProperty],
});

Ownership

The engine owns clip-scoped keyframe storage, property registration, sorting, evaluation, geometry, hit testing, selection, undo/redo, copy/paste, split/trim/move behavior, and mutation events. It rejects creation or evaluation for unregistered properties.

React owns headless hooks, DOM interaction layers, pointer capture, padded hit targets, focus behavior, keyboard affordances, ARIA labels, and package CSS. It does not decide which properties exist or what presets mean.

The renderer owns dense canvas drawing for a configured property. Pass a registered property id to CanvasRenderer; the main thread prepares keyframe geometry through the engine, then the worker draws that geometry without reinterpreting keyframe values.

Apps own property definitions, inspector UI, presets, shortcuts, graph-editor layout, media/effect application, and editing policy. Opacity is an example property, not a privileged keyframe type.

Engine API

Use the core engine for persistence-safe edits and evaluation:

engine.setClipKeyframe({
clipId: 'intro',
property: 'opacity',
time: fromSeconds(3),
value: 0.6,
});
engine.updateClipKeyframeSide({
clipId: 'intro',
keyframeId: 'fade-out-start',
side: 'outgoing',
patch: {
interpolation: 'bezier',
handle: { x: 0.16, y: 1 },
},
});
engine.updateClipKeyframeSides({
clipId: 'intro',
keyframeId: 'fade-out-end',
incoming: {
interpolation: 'bezier',
handle: { x: 0.58, y: 1 },
},
});
const opacity = engine.getClipPropertyValueAtTime('intro', 'opacity', fromSeconds(3.5));

setClipKeyframe() adds or updates a keyframe at an exact clip/property/time. New keyframes do not inherit side data from neighbors; missing side data normalizes to linear during evaluation. Use updateClipKeyframe(), updateClipKeyframeSide(), updateClipKeyframeSides(), and removeClipKeyframe() for existing keyframes. Pass handle: null in a side patch to reset a Bezier tangent to its deterministic default instead of preserving the current handle.

Segment Semantics

A segment is formed by a left startKeyframe and a right endKeyframe.

Mode Source Behavior
linear Missing side data or explicit linear Interpolates evenly from start value to end value.
hold startKeyframe.outgoing Holds the start value until the next keyframe.
bezier Either side is bezier Uses start outgoing.handle and end incoming.handle tangents.

Dragging an outgoing tangent mutates the anchor keyframe’s outgoing side. Dragging an incoming tangent mutates the anchor keyframe’s incoming side. Missing Bezier handles use deterministic defaults.

React Hooks

Use useTimelineKeyframes() for keyframe state, geometry, evaluation, and commands. Compose useTimelineKeyframeDrag() when you want custom point dragging.

For segment and tangent editing, use useTimelineKeyframeSegments() and useTimelineKeyframeTangentDrag():

import {
useTimelineKeyframeSegments,
useTimelineKeyframeTangentDrag,
} from '@techsquidtv/canvas-timeline-react/hooks';
function TangentHandles() {
const segments = useTimelineKeyframeSegments({
property: 'opacity',
selectedClipOnly: true,
selectedKeyframeOnly: true,
});
const drag = useTimelineKeyframeTangentDrag({ property: 'opacity' });
return segments.visibleTangentHandles.map((handle) => (
<button
key={`${handle.segmentId}:${handle.side}`}
type="button"
onPointerDown={() =>
drag.startKeyframeTangentDrag({
tangentHandle: handle,
})
}
/>
));
}

Interaction Layers

The default canvas renderer draws keyframe segments and diamonds for the configured property. Add DOM interaction layers over the canvas when you want editable handles:

import { CanvasRenderer, Timeline } from '@techsquidtv/canvas-timeline';
<Timeline.Root>
<CanvasRenderer keyframeProperty="opacity" />
<Timeline.ClipInteractionLayer />
<Timeline.KeyframeInteractionLayer property="opacity" selectedClipOnly />
<Timeline.KeyframeTangentInteractionLayer
property="opacity"
selectedClipOnly
selectedKeyframeOnly
onTangentHandleDoubleClick={(handle) => {
console.log(handle.segmentId, handle.side);
}}
/>
<Timeline.PlayheadGrabber />
</Timeline.Root>;

The tangent layer uses padded hit targets, pointer capture with document-level fallback listeners, focus handling, and ARIA labels. Apps still decide when to convert sides to Bezier, reset handles, link tangents, or apply presets.

Custom Renderers

Keyframe behavior is not tied to the built-in canvas renderer. DOM renderers can read keyframeRects or visibleKeyframes from useTimelineKeyframes(), and segments or visibleTangentHandles from useTimelineKeyframeSegments(). Custom canvas layers can call the engine geometry APIs directly.

Set showKeyframes={false} on CanvasRenderer when you want to draw keyframe visuals yourself while keeping the same engine and hook APIs.

The Opacity Keyframes demo shows opacity registered explicitly as an example property, canvas-rendered segments, DOM drag handles, Bezier preset editing, tangent dragging, and an HTML video preview whose opacity follows the current playhead.