Sequences
Motion graphics as one timeline - the stage kit for scripted HTML clips, its API, the rules that keep every frame exact, and how to lint a sequence.
Coming in the next release
The stage kit, lintSequence and scripted libraries ship in the next @miraiclip/renderer release.
A sequence is a scripted HTML clip built with the stage kit. The whole piece runs on one animation timeline: elements stay on screen and change from beat to beat, a camera moves through the frame, and beats are named labels. This is how motion graphics stay one continuous move rather than a row of slides.
The kit runs anime.js v4 (MIT) inside the clip's frame, behind a small API of its own. Miraiclip seeks the timeline for every frame and never plays it, so preview, scrubbing and every export show the same frame for the same time.
Register the kit
import { registerStageKit } from "@miraiclip/renderer";
registerStageKit(); // once, before html clips render: in the editor and in your export page@miraiclip/server-export registers it in its own harness, so server exports render sequences too.
A sequence
<div class="stage" data-libs="stage" data-duration="8"
style="position:relative;width:100%;height:100%;overflow:hidden;container-type:size;background:#0b1030">
<style>
.world{position:absolute;inset:0}
.orb{position:absolute;left:50%;top:50%;width:14cqmin;height:14cqmin;margin:-7cqmin 0 0 -7cqmin;border-radius:50%;background:#ffcf5a}
.line{position:absolute;inset:14cqh 0 auto;text-align:center;font:800 7cqmin Inter,sans-serif;color:#fff}
</style>
<div class="world">
<div class="orb"></div>
<div class="line">{{headline}}</div>
</div>
</div>
<script>
stage.build((tl, S) => {
S.split(".line", "words");
tl.label("in", 0)
.from(".orb", { scale: 0, blur: 12, duration: 0.6, ease: "back.out" }, "in")
.from(".line .word", { y: S.h(5), opacity: 0, duration: 0.5, stagger: 0.12, ease: "expo.out" }, "in+=0.3")
.camera({ zoom: 1.15, tiltX: 6, duration: 4, ease: "sine.inOut" }, "in")
.label("travel", 3)
.to(".orb", { x: S.w(30), y: -S.h(10), duration: 1.2, ease: "expo.inOut" }, "travel")
.camera({ zoom: 1, tiltX: 0, duration: 4.5, ease: "sine.inOut" }, "travel");
});
</script>data-libs="stage"brings the kit and its animation engine into the frame. The code is injected inline, with no network.data-durationis the sequence's length. The timeline must end within it..worldis the layer the camera moves. A.hudbeside it stays put.{{headline}}is a param, as in any html clip.
Add it as an html clip with scripted: true:
project.dispatch({
type: "clip/add",
payload: { kind: "html", id: "act-1", trackId: "scenes", startUs: 0, durationUs: 8_000_000, template, params: { headline: "One place for everything" }, scripted: true },
});The timeline: tl
All times are in seconds. Every method returns tl.
| Call | Does |
|---|---|
tl.to(targets, vars, position) | Animate to vars |
tl.from(targets, vars, position) | Animate from vars to the element's CSS state |
tl.fromTo(targets, fromVars, toVars, position) | Both ends explicit |
tl.set(targets, vars, position) | Change instantly |
tl.label(name, position) | Name a beat |
tl.camera(vars, position) | Move .world: x, y, zoom, rotate, tiltX, tiltY |
-
Targets: a selector, an element, or a list of elements.
-
Vars:
xandy(useS.w/S.h/S.min),scale,rotate,rotateX,rotateY;opacity,blur(px), colours, sizes,clipPath, CSS variables;- keyframe arrays;
duration,delay,easeandstagger.
Values may be functions
(i, el) => value. -
Eases:
"expo.out","expo.in","power3.inOut","back.out","elastic.out","sine.inOut","linear","steps(n)";- anime.js names such as
"out(3)".
-
Positions: seconds, a label,
"label+=0.3","<"(with the previous step),">"(after it),"+=0.5".
The stage: S
| Helper | Gives |
|---|---|
S.w(p), S.h(p), S.min(p) | p percent of the stage's width, height or shorter side, in pixels |
S.width, S.height, S.portrait | The stage size |
S.split(target, "words" | "chars") | Spans (.word / .char) to animate one by one. Styled children are kept |
S.rand(seed) | A repeatable random function |
S.beats, S.beat(n) | Beat times from the root's data-beats (seconds into the sequence). Without them, S.beat(n) is n × 0.5 s |
The root reports back on itself once built: data-stage-duration, data-stage-labels (JSON) and, on failure, data-stage-error. snapshotScriptedHtml returns this markup, so tools can read a sequence's beats and catch script errors before placing it.
Rules
Every frame must be the same however often and in whatever order it's rendered, and a sequence must fit any frame size. lintSequence(template) checks a template before it renders:
import { lintSequence } from "@miraiclip/renderer";
const { errors, warnings } = lintSequence(template);| Rule | Why | Instead |
|---|---|---|
random | Math.random() differs every render | S.rand(seed) |
clock | Date and performance.now() aren't the timeline | Timeline positions |
async-loop | Frames are sought, never played | Put every change on the timeline |
network | No network in a frame | asset:<id> images, data-libs libraries |
infinite | An endless loop can't be sought | A finite repeat |
owned-hooks | The kit owns the seek hooks | stage.build only |
gsap | Not available here | The tl API |
units | Viewport units render at the wrong size | cqw / cqh / cqmin, and S.* |
fixed | Doesn't work in a clip | position: absolute |
external-script, build, duration, libs | The template's shape | One inline stage.build, data-duration, data-libs="stage" |
The lint also returns these warnings:
pixels: raw pixel values won't fit another frame size.engine: direct anime.js calls bypass the kit's seeking.big-blur: a largefilter: blur()layer can render with tile artifacts under a 3D camera tilt on some GPUs. Use a radial gradient for glows.
Your own libraries
registerScriptedLibraries makes any library available to scripted clips through data-libs:
import { registerScriptedLibraries } from "@miraiclip/renderer";
registerScriptedLibraries({
confetti: confettiSource, // code as a string
myKit: { code: myKitSource, requires: ["confetti"] }, // loaded after its dependencies
});A template with data-libs="myKit" gets both, inlined in dependency order. Check each library's licence for the way your app uses it.
Checking the motion
Motion metrics measure how much a sequence actually moves, and where it holds still.