The keyframe animation system enables property animation over time with bezier curve editing.
| Property | Range | Default |
|---|---|---|
opacity |
0-1 | 1 |
position.x |
-∞ to +∞ | 0 |
position.y |
-∞ to +∞ | 0 |
position.z |
-∞ to +∞ | 0 (depth) |
scale.x |
0 to ∞ | 1 |
scale.y |
0 to ∞ | 1 |
rotation.x |
degrees | 0 |
rotation.y |
degrees | 0 |
rotation.z |
degrees | 0 |
Any numeric effect parameter can be keyframed:
effect.{effectId}.{paramName}
Example: effect.effect_123.shift for hue shift animation
- Expand track to show properties
- Click diamond icon (◇) next to property
- Keyframe added at current playhead
- Enable recording mode (toggle button)
- Move playhead to desired time
- Change property value
- Keyframe auto-created
interface Keyframe {
id: string; // kf_{timestamp}_{random}
clipId: string; // Reference to clip
time: number; // Relative to clip start (seconds)
property: string; // e.g., 'opacity', 'position.x'
value: number; // Interpolated value
easing: EasingType; // 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bezier'
handleIn?: BezierHandle; // Custom in-tangent
handleOut?: BezierHandle; // Custom out-tangent
}- Drag keyframe diamond horizontally
- Shift+drag for fine control (10x slower)
- Clamped to clip duration [0, clipDuration]
- Live preview updates during drag
- Position playhead on keyframe
- Adjust value in Clip Properties panel
- Keyframe value updates automatically
- Select keyframe(s)
- Press
Deletekey - Or right-click → Delete
- Copy:
Ctrl+Cwith keyframes selected copies only keyframes (not clips) - Paste:
Ctrl+Vpastes keyframes at playhead position on the selected clip - Pasted keyframes maintain relative timing between each other
- Select multiple keyframes with
Shift+Click - Drag any selected keyframe to move all by the same time delta
- All selected keyframes move together maintaining relative spacing
- Toggling keyframes off for a property saves the current value
- All keyframes for that property are deleted cleanly
- Property reverts to a static value
- Small amber diamond markers at the bottom of clip bars
- Show keyframe positions without needing to expand tracks
- Visible at all zoom levels for quick keyframe overview
addKeyframe(clipId, property, value, time?, easing)
removeKeyframe(keyframeId)
updateKeyframe(keyframeId, updates)
moveKeyframe(keyframeId, newTime)
deleteSelectedKeyframes()| Mode | Bezier Points | Behavior |
|---|---|---|
linear |
[0,0] → [1,1] | Constant rate |
ease-in |
[0.42,0] → [1,1] | Slow start |
ease-out |
[0,0] → [0.58,1] | Slow end |
ease-in-out |
[0.42,0] → [0.58,1] | Smooth both |
bezier |
Custom handles | User-defined |
Each easing mode shows unique diamond shape:
- Linear: ◇ regular diamond
- Ease In: ◀ left-pointed
- Ease Out: ▶ right-pointed
- Ease In-Out: ◆ filled
- Bezier: custom shape
- Right-click keyframe
- Select easing from context menu
- Or modify bezier handles in curve editor
- Expand track to show properties
- Click curve icon next to property
- Editor appears below property row
- SVG-based with grid background
- Bezier curves drawn between keyframes
- Value range auto-computed with padding
- Auto-scale Y-axis fits curve tightly to visible range
- Shift+wheel to resize curve editor height
- Single editor open — only one curve editor at a time to prevent UI clutter
| Action | Effect |
|---|---|
| Click+drag point | Move time and value |
| Shift+drag | Constrain to horizontal or vertical |
| Click empty | Deselect all |
- In-handle: controls incoming curve (x ≤ 0)
- Out-handle: controls outgoing curve (x ≥ 0)
- Shift+drag handle: constrain to horizontal
- Horizontal: time axis (from timeline scroll)
- Vertical: value axis (auto-scaled)
- Major/minor grid lines with labels
toggleKeyframeRecording(clipId, property)- Format:
{clipId}:{property}in Set - Visual indicator when active
- Property changes create/update keyframes at playhead
- Existing keyframe at time → updates value
- No keyframe at time → creates new one
- Property changes update static clip values
- No keyframes created automatically
- Calculate normalized time
tbetween keyframes - Apply easing function to get eased time
- Linear interpolate value:
v1 + (v2 - v1) * easedT
Uses cubic Bezier with Newton-Raphson solver:
- 10 iterations
- Epsilon: 0.0001
- Solves for X to get eased time
- No keyframes → returns default value
- Single keyframe → returns its value
- Before first → returns first value
- After last → returns last value
PROPERTY_ROW_HEIGHT = 18px
CURVE_EDITOR_HEIGHT = 250px
BEZIER_HANDLE_SIZE = 8px
KEYFRAME_TOLERANCE = 0.01s (10ms)- Property groups (Position, Scale, Rotation, Opacity)
- Individual property lanes with diamonds
- Only properties with keyframes displayed
baseHeight
+ (propertyCount × PROPERTY_ROW_HEIGHT)
+ (expandedCurves × CURVE_EDITOR_HEIGHT)
Speed is an animatable property that uses keyframes to control playback rate over time. The speedIntegration.ts module provides utilities for the complex mapping between timeline time and source time when clip speed is keyframed.
| Function | Purpose |
|---|---|
calculateSourceTime(clip, timelineTime) |
Maps a timeline position to the corresponding source media position, integrating the speed curve |
getSpeedAtTime(clip, timelineTime) |
Returns the instantaneous speed value at a given timeline time (interpolated from keyframes) |
calculateTimelineDuration(clip, sourceDuration) |
Computes how long a clip occupies on the timeline given its source duration and speed keyframes |
- Source time is computed as the integral of the speed curve over the clip's timeline duration
- Supports smooth transitions between speeds (e.g., ramping from 100% to 50%)
- Handles direction changes (forward to reverse) when speed crosses zero
- Negative speed values play the source media backwards
- Timeline - Main editing interface
- Effects - Effect parameter keyframes
- Preview - See animated results
- Keyboard Shortcuts
| Test File | Tests | Coverage |
|---|---|---|
keyframeSlice.test.ts |
96 | Keyframe CRUD operations |
keyframeInterpolation.test.ts |
112 | Easing, bezier, interpolation |
Run tests: npx vitest run
Source: src/stores/timeline/keyframeSlice.ts, src/utils/keyframeInterpolation.ts, src/components/timeline/CurveEditor.tsx