Miraiclip SDK
Creative

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-duration is the sequence's length. The timeline must end within it.
  • .world is the layer the camera moves. A .hud beside 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.

CallDoes
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:

    • x and y (use S.w / S.h / S.min), scale, rotate, rotateX, rotateY;
    • opacity, blur (px), colours, sizes, clipPath, CSS variables;
    • keyframe arrays;
    • duration, delay, ease and stagger.

    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

HelperGives
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.portraitThe 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);
RuleWhyInstead
randomMath.random() differs every renderS.rand(seed)
clockDate and performance.now() aren't the timelineTimeline positions
async-loopFrames are sought, never playedPut every change on the timeline
networkNo network in a frameasset:<id> images, data-libs libraries
infiniteAn endless loop can't be soughtA finite repeat
owned-hooksThe kit owns the seek hooksstage.build only
gsapNot available hereThe tl API
unitsViewport units render at the wrong sizecqw / cqh / cqmin, and S.*
fixedDoesn't work in a clipposition: absolute
external-script, build, duration, libsThe template's shapeOne 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 large filter: 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.

On this page