Pre-1.0: the API is not stable yet and may change from version to version. Pin the exact version in your Cargo.toml and check the changelog before upgrading.

Migrating from Motion Canvas

Motion Canvas is the closest tool to oxframe: scenes are coroutines over signals in both. Porting is mostly syntax — yield* becomes .await, refs become handles — and this page maps the rest.

How the two models differ

A Motion Canvas scene is a TypeScript generator: yield* circle().x(300, 1) tweens a signal and waits for it. An oxframe scene is a Rust async fn: circle.x().to(300.0, 1.0).await does the same. Both replay a scene from the start to reach any frame, both keep markers and timing edits in a meta file next to the scene, and both have an editor with a timeline.

The difference is around the scene. oxframe is a compiled Rust program: oxframe render writes the video headless, with no editor open, so it runs in CI, and oxframe check reviews every frame for text off screen, overlaps, low contrast and text that is gone too fast.

Concepts side by side

Motion Canvasoxframe
makeScene2D(function* (view) { … })#[oxframe::scene] async fn name(view: View) { … }
yield* node().x(300, 1)node.x().to(300.0, 1.0).await
createRef<Circle>() and ref={…}the handle that view.add(…) returns
createSignal(0), () => …Signal::new(0.0), .bind(move || …)
all(…), chain(…)all((…)).await; awaits one after another
sequence(0.1, …), loop(3, …)stagger(0.1, &items, …), repeat(3, …)
yield a task without *spawn(async move { … })
waitFor(1), waitUntil('event')wait_for(1.0), wait_until("event")
easeInOutCubic, easeOutBackpresets::IN_OUT_CUBIC, presets::OUT_BACK
<Layout direction="column" gap={20}>Layout::new().column().gap(20.0)
<Code> with edit and selectionCode with .code().to(next, s), insert, replace, focus_lines
<Line>, <Spline>, <Ray>Line (.smooth(true), .end_arrow(true)), Path
<Img>, <Video>, the project's audioImg, Video (feature video), Audio::track(…)
slideTransition(Direction.Left)transition(Transition::slide(Dir::Left)).await
beginSlide('name') and the presenterslide_break().await and oxframe present
makeProject({scenes: […]})Project::new("name").scene(a()).scene(b())
rendering from the editoroxframe render, headless

The same scene in both

A circle moves, turns blue while it grows, and a number below counts to 100.

Motion Canvas

import {Circle, Txt, makeScene2D} from '@motion-canvas/2d';
import {all, createRef, createSignal, waitFor} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
  const circle = createRef<Circle>();
  const count = createSignal(0);
  view.add(
    <>
      <Circle ref={circle} size={320} fill={'#e13238'} x={-300} />
      <Txt text={() => count().toFixed(0)} fontSize={80} y={300} fill={'white'} />
    </>,
  );

  yield* circle().x(300, 1);
  yield* all(circle().fill('#68abdf', 1), circle().scale(1.5, 1));
  yield* count(100, 2);
  yield* waitFor(0.5);
});

oxframe open it in the playground

use oxframe::prelude::*;

#[oxframe::scene]
pub async fn hello(view: View) {
    let circle = view.add(Circle::new().size(320.0).fill(color!("#e13238")).x(-300.0));
    let count = Signal::new(0.0);
    let label = view.add(Txt::new().font_size(80.0).y(300.0));
    label.text().bind({
        let count = count.clone();
        move || format!("{:.0}", count.get())
    });

    circle.x().to(300.0, 1.0).await;
    all((
        circle.fill().to(color!("#68abdf"), 1.0),
        circle.scale().to(Vec2::new(1.5, 1.5), 1.0),
    ))
    .await;
    count.to(100.0, 2.0).await;
    wait_for(0.5).await;
}

pub fn project() -> Project {
    Project::new("hello").scene(hello())
}
  • The refs disappear: view.add returns a handle, and handles are Copy, so they can go into closures and background tasks.
  • The computed text () => count().toFixed(0) becomes a binding; it is recomputed when the signal changes.
  • scale(1.5, 1) sets both axes from one number; in oxframe the target is a Vec2.

What does not carry over

  • LaTeX. There is no <Latex> node yet; formulas are planned.
  • Custom TSX components. A component made of other nodes becomes a Rust function that adds them to a Group; a node that draws itself uses the node API.
  • Plugins for the editor. oxframe's editor is not extensible yet.

Porting a project

  1. Port the scenes one by one and keep their names: markers move with the same names.
  2. Replace yield* with .await, refs with handles, signals with Signal; keep the durations, they are seconds in both.
  3. Run oxframe dev to scrub the result and drag the markers back into place, then oxframe check.

With an agent: npx skills add oxygenframe/oxframe-sdk installs the motion-canvas-to-oxframe skill. Then ask, for example, “Port src/scenes/intro.tsx to oxframe and make oxframe check pass”.

Edit this page on GitHub