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 Canvas | oxframe |
|---|---|
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, easeOutBack | presets::IN_OUT_CUBIC, presets::OUT_BACK |
<Layout direction="column" gap={20}> | Layout::new().column().gap(20.0) |
<Code> with edit and selection | Code with .code().to(next, s), insert, replace, focus_lines |
<Line>, <Spline>, <Ray> | Line (.smooth(true), .end_arrow(true)), Path |
<Img>, <Video>, the project's audio | Img, Video (feature video), Audio::track(…) |
slideTransition(Direction.Left) | transition(Transition::slide(Dir::Left)).await |
beginSlide('name') and the presenter | slide_break().await and oxframe present |
makeProject({scenes: […]}) | Project::new("name").scene(a()).scene(b()) |
| rendering from the editor | oxframe 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.addreturns a handle, and handles areCopy, 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 aVec2.
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
- Port the scenes one by one and keep their names: markers move with the same names.
- Replace
yield*with.await, refs with handles, signals withSignal; keep the durations, they are seconds in both. - Run
oxframe devto scrub the result and drag the markers back into place, thenoxframe 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”.