Fundamentals
Clips
Video, audio, image, text, caption and HTML clips on the timeline.
project.dispatch({
type: "clip/add",
payload: {
kind: "video",
id: "intro",
trackId: "main",
assetId: "intro-mp4",
startUs: 0,
durationUs: 5_000_000,
},
});Kinds
| Kind | Needs | Track | Guide |
|---|---|---|---|
video | assetId of a video asset | video | — |
audio | assetId of an audio asset | audio | Audio |
image | assetId of an image asset | video | — |
text | text | video | Text & typography |
caption | words with timing | video | Captions |
html | template | video | HTML clips |
You can also register your own kinds: Custom clip kinds.
Fields every clip has
| Field | Default | Meaning |
|---|---|---|
id, trackId | required | |
startUs | required | Position on the timeline |
durationUs | required | Length on the timeline |
transform | { x: 0.5, y: 0.5, scale: 1, rotation: 0, opacity: 1 } | Placement; see below |
animations | absent | Keyframes per property: Keyframes |
effects | absent | Effect stack: Effects |
Transform
| Field | Unit |
|---|---|
x, y | Clip center, normalized: 0.5, 0.5 is the canvas center, 0, 0 the top-left corner |
scale | 1 fits the clip inside the canvas, keeping its aspect ratio |
rotation | Degrees |
opacity | 0 to 1 |
Because scale: 1 means "fit", a 4K source on a 720p canvas fills the frame, and the layout doesn't change between preview and export.
Video and audio clips
| Field | Default | Meaning |
|---|---|---|
trimStartUs | 0 | Where in the source the clip starts playing |
volume | 1 | Gain; can be keyframed |
fadeInUs, fadeOutUs | absent | Linear fades at the clip's edges |
The clip plays source media from trimStartUs to trimStartUs + durationUs. Source media outside that range is the clip's headroom; transitions draw from it. clipHeadroomUs(doc, clip, "in" | "out") reads it.
Editing
// Move on the timeline, or to another track
project.dispatch({ type: "clip/move", payload: { clipId: "intro", startUs: 1_000_000 } });
// Trim: set any of startUs, durationUs, trimStartUs
project.dispatch({
type: "clip/trim",
payload: { clipId: "intro", startUs: 2_000_000, durationUs: 3_000_000, trimStartUs: 1_000_000 },
});
// Split at a timeline time
project.dispatch({ type: "clip/split", payload: { clipId: "intro", atUs: 3_000_000, newClipId: "intro-b" } });
// Change properties: nested transform fields merge
project.dispatch({
type: "clip/set-property",
payload: { clipId: "intro", transform: { x: 0.25 }, volume: 0.5 },
});
// null clears an optional field
project.dispatch({ type: "clip/set-property", payload: { clipId: "intro", fadeInUs: null } });| Command | Payload | Notes |
|---|---|---|
clip/add | Kind-specific, see above | |
clip/remove | { clipId } | |
clip/move | { clipId, startUs?, trackId? } | The target track must accept the kind |
clip/trim | { clipId, startUs?, durationUs?, trimStartUs? } | |
clip/split | { clipId, atUs, newClipId? } | The right half gets newClipId (generated if omitted) and its trimStartUs advances. Fade-in stays on the left half, fade-out moves to the right |
clip/duplicate | { clipId, newClipId?, startUs?, trackId? } | Without startUs, the copy sits at the same time as the original |
clip/set-property | { clipId, …fields } | Any editable field of the clip's kind |
What the core doesn't enforce
Your editor decides these rules:
- Overlap: clips on the same track may overlap. Where they do, the clip that starts later is drawn on top. Most editors prevent overlaps in the UI and use separate tracks for layering.
- Source length:
durationUscan run past the end of the source media. Clamp againstasset.durationUswhen trimming. - Selection: removing a clip doesn't remove its id from
selection. Filter out ids that no longer exist.
Reading a clip
import { isTextClip, isVideoClip } from "@miraiclip/core";
const clip = project.getState().doc.clips["intro"];
if (clip && isVideoClip(clip)) console.log(clip.trimStartUs, clip.volume);
if (clip && isTextClip(clip)) console.log(clip.text);Guards: isVideoClip, isAudioClip, isImageClip, isTextClip, isCaptionClip, isHtmlClip.