Keyframes & animation
Animate any clip property with easings.
// Fade a clip in over its first second
project.dispatch({
type: "keyframe/set",
payload: { clipId: "title", property: "opacity", timeUs: 0, value: 0, easing: "easeOut" },
});
project.dispatch({
type: "keyframe/set",
payload: { clipId: "title", property: "opacity", timeUs: 1_000_000, value: 1 },
});Keyframe times are relative to the clip's start, in µs. Keyframes play in preview and in every export.
Properties
| Property | Range | Clips |
|---|---|---|
x, y | Normalized position, like transform | All kinds |
scale | ≥ 0 | All kinds |
rotation | Degrees | All kinds |
opacity | 0 to 1 | All kinds |
volume | ≥ 0 | video and audio only |
A property without keyframes uses the clip's static value (transform or volume). Keyframes on a property replace that value. Custom clip kinds animate the same way.
Commands
// Same time and anchor as an existing keyframe: updates it in place
project.dispatch({
type: "keyframe/set",
payload: { clipId: "title", property: "opacity", timeUs: 0, value: 0.5 },
});
// Remove one keyframe: timeUs and anchor must match exactly
project.dispatch({ type: "keyframe/remove", payload: { clipId: "title", property: "opacity", timeUs: 0 } });
// Clear one property, or every property
project.dispatch({ type: "keyframe/clear", payload: { clipId: "title", property: "opacity" } });
project.dispatch({ type: "keyframe/clear", payload: { clipId: "title" } });| Command | Payload | Notes |
|---|---|---|
keyframe/set | { clipId, property, timeUs, value, easing?, anchor? } | Upserts by timeUs + anchor. easing defaults to linear |
keyframe/remove | { clipId, property, timeUs, anchor? } | Rejected with keyframe-not-found if nothing matches |
keyframe/clear | { clipId, property? } | Omit property to clear all |
Rejections: out-of-range (opacity outside 0..1, or negative scale/volume), no-audio (volume on a clip without sound).
Easings
The easing on a keyframe shapes the segment from that keyframe to the next one.
| Easing | Stored as |
|---|---|
linear | { kind: "bezier", x1: 0, y1: 0, x2: 1, y2: 1 } |
easeIn | { kind: "bezier", x1: 0.42, y1: 0, x2: 1, y2: 1 } |
easeOut | { kind: "bezier", x1: 0, y1: 0, x2: 0.58, y2: 1 } |
easeInOut | { kind: "bezier", x1: 0.42, y1: 0, x2: 0.58, y2: 1 } |
hold | { kind: "hold" }: keep the value, jump at the next keyframe |
Preset names are input shorthand. The document always stores the expanded form, so read keyframe.easing.kind, not a preset name. Pass any cubic bézier directly (CSS cubic-bezier() convention):
// Overshoot, then settle
project.dispatch({
type: "keyframe/set",
payload: {
clipId: "title",
property: "y",
timeUs: 0,
value: 0.6,
easing: { kind: "bezier", x1: 0.34, y1: 1.56, x2: 0.64, y2: 1 },
},
});Animate out from the end
With anchor: "end", timeUs counts back from the clip's end (0 is the last instant). Trimming or resizing the clip moves these keyframes with the end.
// Fade out over the last 0.5 s, however long the clip becomes
project.dispatch({
type: "keyframe/set",
payload: { clipId: "title", property: "opacity", timeUs: 500_000, anchor: "end", value: 1 },
});
project.dispatch({
type: "keyframe/set",
payload: { clipId: "title", property: "opacity", timeUs: 0, anchor: "end", value: 0 },
});- Start- and end-anchored keyframes can share a property. If the clip becomes shorter than both animations, the keyframes interleave by time.
clip/splitdrops end-anchored keyframes from the left half.- In the document, end-anchored keyframes carry
anchor: "end". Start-anchored ones have noanchorfield.
Presets
Core ships ready-made animations built from ordinary keyframes. A clip's animation is a recipe with up to three slots: in, loop and out.
import { animationCommands, applyCommands, describeAnimation, readAnimation } from "@miraiclip/core";
const clip = project.getState().doc.clips["title"]!;
applyCommands(
project,
animationCommands(clip, {
in: { preset: "in:pop", durationUs: 600_000, easing: "smooth" },
loop: { preset: "loop:float", durationUs: 0, easing: "linear" },
out: { preset: "out:fade", durationUs: 400_000, easing: "smooth" },
}),
); // one undo step
const { recipe, custom } = readAnimation(project.getState().doc.clips["title"]!);
describeAnimation(recipe); // "Pop in · Float · Fade out"| Slot | Preset ids |
|---|---|
in, out | fade, up, down, left, right, zoom, spin, pop (as in:fade, out:fade, …) |
loop | loop:pulse, loop:float, loop:sway, loop:kenburns |
| Field | Meaning |
|---|---|
preset | A preset id from the table above. ANIMATION_PRESETS lists them with labels; animationPreset(id) looks one up |
durationUs | Slot length. Loops span the time between In and Out, so pass 0 |
easing | "smooth", "linear" or "snappy" (ANIMATION_EASINGS). Loops have fixed timing |
| Helper | What it does |
|---|---|
animationCommands(clip, recipe) | Commands that clear the clip's visual keyframes and set the recipe's. Volume keyframes are kept. {} removes the animation |
readAnimation(clip) | Recognizes the recipe from the keyframes. custom: true when keyframes match no preset; stale: true when a trim left the loop out of date (apply the recipe again) |
describeAnimation(recipe) | A short label for a panel |
fitAnimation(recipe, clipUs) | Shortens In and Out so both fit a clip of that length |
Out-slot keyframes are end-anchored, so they follow trims. The recipe itself isn't stored: readAnimation works after a reload or an undo because presets are deterministic.
Reading animated values
import { evaluateClipAt } from "@miraiclip/core";
const clip = project.getState().doc.clips["title"]!;
const local = 750_000; // clip-relative µs
const { x, y, scale, rotation, opacity, volume } = evaluateClipAt(clip, local);| Function | Use |
|---|---|
evaluateClipAt(clip, clipTimeUs) | Every animatable value at one time, with static values as fallback |
evaluateClipInto(clip, clipTimeUs, out) | Same, written into a reused object (no allocation per frame) |
evaluateKeyframes(keyframes, timeUs, fallback, clipDurationUs?) | One property. Pass clipDurationUs when the list may contain end-anchored keyframes |
resolveKeyframes(keyframes, clipDurationUs) | Keyframes with every time measured from the clip start, sorted |
resolveEasing(input) | A preset name or easing in, the stored form out |
Before the first keyframe the first value holds; after the last, the last value holds.
Related
- Fade audio and duck music: Audio
- Keyframes apply on top of transitions and effects: Transitions, Effects