Migrating from Manim
Manim scenes are Python classes that play animations one self.play at a time. oxframe
scenes are Rust async fns that do the same with .await, with a live
editor, plots, matrices and morphing built in. This page maps the concepts and ports two scenes.
How the two models differ
In Manim, construct builds mobjects and hands animations to self.play, which renders them before the next line runs. In oxframe each node has
signals — position, color, the path itself — and .to(target, seconds).await animates
one and waits. all((…)) is self.play with several animations; wait_for is self.wait.
Instead of rendering a file and opening it, oxframe dev shows the scene in an editor
you can scrub, and every frame can be reached instantly. The same code runs in the browser playground, with no Cairo, LaTeX or Python to install.
Two habits to drop: Manim measures in units (the frame is about 14.2 wide) with y pointing up;
oxframe measures in pixels from the center of the frame, with y pointing down. At 1920 × 1080 one
Manim unit is about 135 px, and UP is negative y.
Concepts side by side
| Manim | oxframe |
|---|---|
class S(Scene): def construct(self) | #[oxframe::scene] async fn s(view: View) |
self.add(m) | view.add(…) |
self.play(Create(m)) | m.draw_on().await |
FadeIn, FadeOut | m.fade_in().await, m.fade_out().await |
Write(text) | text.end().to(1.0, 1.0).await reveals it letter by letter |
Transform(a, b) | a.data().to(path, 1.0) for shapes, t.morph_to("new", 1.0) for text |
m.animate.shift(RIGHT * 2) | m.x().to(m.x().get() + 270.0, 1.0).await |
self.play(a, b) | all((a, b)).await |
LaggedStart(…, lag_ratio=0.1) | stagger(0.1, &items, |m| m.fade_in()).await |
run_time=2, rate_func=smooth | the seconds argument, .ease(presets::IN_OUT_CUBIC) |
ValueTracker, always_redraw, updaters | Signal::new(…), .bind(move || …) |
Axes, axes.plot, NumberLine | Axes::new(), axes.plot(|x| …), NumberLine::new(…) — charts |
Matrix, Table, Vector | Matrix::new, Table::new, axes.vector — matrices |
MovingCameraScene | Camera: zoom, center_on, fit |
ThreeDScene | View3d: lit meshes and glTF models inside a 2D scene |
MathTex, Tex | not yet — formulas are planned |
manim -pql scene.py S | oxframe dev to watch, oxframe render for the file |
The same scenes in both
A circle is drawn, becomes a square and moves right; then a sine wave is drawn on axes and a dot rides along it.
Manim
from manim import *
class Hello(Scene):
def construct(self):
circle = Circle(radius=1.5, color=RED)
self.play(Create(circle))
self.play(Transform(circle, Square(side_length=3, color=BLUE)))
self.play(circle.animate.shift(RIGHT * 2))
self.wait()
class Graph(Scene):
def construct(self):
axes = Axes(x_range=[-3, 3], y_range=[-1.5, 1.5])
sine = axes.plot(lambda x: np.sin(x), color=BLUE)
self.add(axes)
self.play(Create(sine))
x = ValueTracker(-3)
dot = always_redraw(lambda: Dot(axes.i2gp(x.get_value(), sine)))
self.add(dot)
self.play(x.animate.set_value(3), run_time=2)
self.wait(0.5) oxframe open it in the playground
use oxframe::prelude::*;
fn circle(r: f64) -> BezPath {
use oxframe::core::kurbo::Shape;
oxframe::core::kurbo::Circle::new((0.0, 0.0), r).to_path(0.1)
}
#[oxframe::scene]
pub async fn hello(view: View) {
let shape = view.add(
Path::new()
.data(circle(150.0))
.fill(Color::TRANSPARENT)
.stroke(color!("#e13238"))
.line_width(6.0)
.end(0.0),
);
shape.draw_on().await; // Create(circle)
all((
// Transform(circle, Square(...)): any path morphs into any other
shape.data().to(BezPath::from_svg("M-150 -150 H150 V150 H-150 Z").unwrap(), 1.0),
shape.stroke().to(color!("#68abdf"), 1.0),
))
.await;
shape.x().to(300.0, 1.0).await; // circle.animate.shift(RIGHT * 2)
wait_for(1.0).await; // self.wait()
}
#[oxframe::scene]
pub async fn graph(view: View) {
let axes = view.add(
Axes::new()
.x_range(-3.0..3.0)
.y_range(-1.5..1.5)
.width(pct(80))
.height(pct(60)),
);
let sine = axes.plot(|x| x.sin()).stroke(Accent);
sine.draw_on().await; // Create(sine)
let x = Signal::new(-3.0); // ValueTracker(-3)
axes.point_on(&sine, x.clone()); // the dot follows x, like always_redraw
x.to(3.0, 2.0).await; // x.animate.set_value(3), run_time=2
wait_for(0.5).await;
}
pub fn project() -> Project {
Project::new("hello").scene(hello()).scene(graph())
}Transformis a path morph: the shape'sdatasignal animates to another path. Any two paths morph, with matched start points so they do not twist.- The
ValueTrackerbecomes aSignal, andaxes.point_onadds a dot that follows it — whatalways_redrawdoes in Manim. - Two Manim scene classes become two scenes of one project; they play in order and a transition can join them.
What does not carry over
- LaTeX. There is no
MathTexyet. Formulas are planned; until then, write them with rich text and Unicode, or as an SVG made elsewhere. - Surfaces and 3D plots. 3D covers meshes (cube, sphere, plane, cylinder, glTF) inside a 2D frame, not parametric surfaces or 3D axes.
- Python. The scenes are Rust. They are short and mostly calls like the ones
above; an agent with the oxframe skill writes them well, and
oxframe checktells it what went wrong.
Porting a project
- One oxframe scene per Manim scene class, in the same order.
- Turn units into pixels (× 135 at 1920 × 1080) and flip y. Prefer placing with
next_to,to_edgeandLayoutover coordinates. - Each
self.play(…)becomes one.await, orall((…)).awaitfor several animations. - Run
oxframe devto compare, thenoxframe check.
With an agent: npx skills add oxygenframe/oxframe-sdk installs the manim-to-oxframe skill. Then ask, for example, “Port
scenes.py to oxframe, one scene per class, and make oxframe check pass”.