Transitions
Dissolves, dips, wipes and slides.
// Split a clip and bridge the cut
project.transaction(() => {
project.dispatch({ type: "clip/split", payload: { clipId: "intro", atUs: 4_000_000, newClipId: "intro-b" } });
project.dispatch({
type: "transition/add",
payload: { kind: "dipToBlack", fromClipId: "intro", toClipId: "intro-b", durationUs: 700_000 },
});
});A transition bridges a cut: the point where one clip ends and the next starts on the same track. The transition is centered on the cut. Both halves of a split come from the same source, so a fresh split always has room for a transition.
Kinds
kind | Look | params |
|---|---|---|
crossDissolve | The incoming clip fades in over the outgoing one | none |
dipToBlack | Fade to black, then back in | none |
dipToWhite | Fade to white, then back in | none |
wipe | A moving edge reveals the incoming clip | direction: "left" (default), "right", "up", "down" |
slide | The incoming clip slides in over the outgoing one | direction: "left" (default), "right", "up", "down" |
project.dispatch({
type: "transition/add",
payload: { kind: "slide", fromClipId: "a", toClipId: "b", durationUs: 600_000, params: { direction: "up" } },
});Audio on both clips ramps with an equal-power curve: the outgoing clip fades out up to the cut, the incoming clip fades in after it. This applies to every kind.
Commands
project.dispatch({
type: "transition/add",
payload: { id: "t1", kind: "crossDissolve", fromClipId: "a", toClipId: "b", durationUs: 800_000 },
});
project.dispatch({ type: "transition/update", payload: { transitionId: "t1", durationUs: 1_000_000 } });
project.dispatch({ type: "transition/update", payload: { transitionId: "t1", params: {} } });
project.dispatch({ type: "transition/remove", payload: { transitionId: "t1" } });| Command | Payload | Notes |
|---|---|---|
transition/add | { kind, fromClipId, toClipId, durationUs, params?, id? } | fromClipId ends at the cut, toClipId starts there. id is generated if omitted |
transition/update | { transitionId, durationUs?, params? } | params merge. A new duration is checked against headroom again |
transition/remove | { transitionId } |
Transitions live in doc.transitions, keyed by id: { id, trackId, fromClipId, toClipId, kind, durationUs, alignment: "centered", params }.
Rejections
| Code | Meaning |
|---|---|
different-tracks | The two clips are on different tracks |
not-adjacent | toClipId doesn't start exactly where fromClipId ends |
duplicate-boundary | This cut already has a transition |
insufficient-handles | A clip lacks the source media the transition needs (see below) |
unknown-kind | No transition kind with that name is registered |
invalid-params | params don't match the kind, e.g. direction: "diagonal" |
transition-not-found | transition/update or transition/remove with an unknown id |
Headroom
During the transition, the outgoing clip keeps playing past its end and the incoming clip starts before its start. Each side needs half the transition's length of source media outside the clip's visible range. This is the clip's headroom.
import { clipHeadroomUs } from "@miraiclip/core";
const doc = project.getState().doc;
const outgoing = doc.clips["a"]!;
const incoming = doc.clips["b"]!;
clipHeadroomUs(doc, outgoing, "out"); // source after the clip's end: asset.durationUs - (trimStartUs + durationUs)
clipHeadroomUs(doc, incoming, "in"); // source before the clip's start: trimStartUs- Two clips that each play a whole file have no headroom. Trim their edges, or split a longer clip.
- Image, text, caption and HTML clips have unlimited headroom.
- An asset without
durationUscounts as unlimited on the "out" side. SetdurationUson video and audio assets so the check is real. - Core checks headroom for every kind, dips included.
Find the cuts
findCuts(doc) lists the cuts on video tracks, with the longest transition each one allows. Use it for a transitions panel or a "dissolve everything" action.
import { applyCommands, findCuts } from "@miraiclip/core";
const commands = findCuts(project.getState().doc)
.filter((cut) => !cut.transition && cut.maxTransitionUs >= 200_000)
.map((cut) => ({
type: "transition/add" as const,
payload: {
kind: "crossDissolve",
fromClipId: cut.fromClipId,
toClipId: cut.toClipId,
durationUs: Math.min(600_000, cut.maxTransitionUs),
},
}));
applyCommands(project, commands); // one undo stepCut field | Meaning |
|---|---|
trackId, fromClipId, toClipId | Where the cut is |
atUs | Timeline position of the cut |
transition | The transition on this cut, if any |
maxTransitionUs | Longest transition the headroom and both clip lengths allow. 0 when a side has no headroom |
short | "from", "to" or "both": the side without headroom, when there is one |
findCuts covers video, image, text and HTML clips on video tracks. Its second argument, { minTransitionUs }, marks a side short when its headroom allows less than that length (default 1: only a side with no headroom).
Notes
- Moving, trimming, splitting or removing either clip removes the transition, because the cut it was built on changes.
- Two clips of the same asset can transition.
- Keyframes and effects still apply to both clips during a transition.
- Your own kinds: Custom transitions.