Animate on the timeline
Keyframes, easing curves and springs live in node.animations — raw data, never rasterized.
The runtime for Figma Motion. Export a timeline to a tinyMotionDoc, play it with a pure-JS engine — the same motion, frame for frame, on every platform. No video bake. No rebuild in CSS.
No black box in the middle. Your animation becomes an open document, then a deterministic render tree — the same maths everywhere it plays.
Keyframes, easing curves and springs live in node.animations — raw data, never rasterized.
The plugin reads the timeline and emits a tiny MotionDoc — no PNGs, no video, just your motion.
The pure render method walks the doc, solves every track at time t, returns a resolved tree of final numbers.
Every adapter paints the same resolved render tree, so an animation looks and times identically wherever it runs. The adapters are thin; the engine does the thinking.
Full-fidelity CSS adapter — nested divs, gradients, SVG vector paths with arrowheads, clip-path stars, masks and procedural shaders.
import { create } from "@blinn-motion/dom";
create(el, doc, { loop: true }).play();A pure-JS 2D canvas painter for the very same resolved tree — one immediate-mode draw per frame, no DOM nodes.
import { create } from "@blinn-motion/canvas";
create(el, doc, { loop: true }).play();A declarative component and hook. Switch the painter with one prop; play, pause and seek through a ref.
<BlinnMotion doc={doc} renderer="canvas" loop autoplay />The same timing on native <View> / <Text>. No native module, no Skia dependency — just the shared core maths.
<BlinnMotionView doc={doc} loop />@blinn-motion/core is time-based and DOM-free. The whole engine hangs off a single deterministic function — everything else is a thin painter.
// THE render method — pure, DOM-free
export function sample(doc: MotionDoc, t: number) return doc.layers.map((layer) =>
resolve(layer, t) // walk + sample every track
);
// each track, eased + composed over the base
const v = ease(key.easing, local);
node[p] = op === "offset"
? base + lerp(a, b, v) // additive
: lerp(a, b, v); // replace
// → resolved RenderNode tree (final numbers)The core speaks one language: t in seconds. The playback clock is shared, so play / pause / seek / loop behave the same everywhere.
Map any 0…1 signal — scroll, drag, state — with setProgress or sample(doc, progress × duration). No black-box player required.
Multiple tracks per property compose over the base value — set replaces, offset adds — so springs and curves layer cleanly.
Every transform, RGBA color and shape vertex comes out resolved. Adapters never re-interpret intent; they just paint.
Keep motion where product teams already work — then resolve it to numbers and paint it the same way on every surface you ship to.
Select a Motion timeline, preview live, download MotionDoc — no rebuild in CSS, no After Effects detour.
sample(doc, t) is DOM-free and unit-tested. Only adapters touch a platform — the engine runs anywhere JS does.
Linear, hold, cubic-bezier (Newton-Raphson) and damped springs — sampled per frame, not pre-baked.
Small, readable JSON. Inspectable, git-diffable, versionable — designed for product handoff, not a black box.
Same resolved tree, same shared clock. An animation times the same on DOM, Canvas, React & Native.
Play on a shared clock — or drive with setProgress(0…1) / a React progress prop for scroll- and gesture-linked motion.
Nested transforms, gradients, vectors, masks, path trim, shaders — real Figma frames, not only illustration loops.
No runtime dependencies in the core. Drop it into vanilla JS, React or React Native without ceremony.
Hand the engine a MotionDoc and a host element. Play on a clock, or drive with setProgress(0…1) / a React progress prop for scroll- and gesture-linked motion. Same surface on every adapter.
npm i @blinn-motion/domimport { create } from "@blinn-motion/dom";
import doc from "./card.motion.json";
const player = create(
document.getElementById("stage")!,
doc,
{ loop: true }
);
player.play();
player.seek(0.8); // seconds
player.setProgress(0.5); // 0…1 — scroll / gestureimport { create } from "@blinn-motion/canvas";
import doc from "./card.motion.json";
const player = create(canvasHost, doc, { loop: true });
player.play();
// drive from scroll instead of the clock:
// player.setProgress(scrollY / range);import { BlinnMotion } from "@blinn-motion/react";
import doc from "./card.motion.json";
// clock-driven
<BlinnMotion doc={doc} renderer="canvas" loop autoplay />
// progress-driven (scroll, drag, state)
<BlinnMotion doc={doc} progress={scrollP} />import { BlinnMotionView } from "@blinn-motion/react-native";
import doc from "./card.motion.json";
// clock
<BlinnMotionView doc={doc} loop autoplay />
// or progress-driven
<BlinnMotionView doc={doc} progress={gestureP} />Product teams already animate in Figma — then rebuild in CSS, export a video, or leave for another tool. Blinn is the runtime for motion that already lives in Figma.
Blinn Motion is the runtime for Figma Motion — not a rebuild of another motion format. MIT-licensed and independent.
Export from the Motion timeline, drop the MotionDoc into your app, and play it identically — on the web, in React, on native.