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

Manimoxframe
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, FadeOutm.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=smooththe seconds argument, .ease(presets::IN_OUT_CUBIC)
ValueTracker, always_redraw, updatersSignal::new(…), .bind(move || …)
Axes, axes.plot, NumberLineAxes::new(), axes.plot(|x| …), NumberLine::new(…) — charts
Matrix, Table, VectorMatrix::new, Table::new, axes.vector — matrices
MovingCameraSceneCamera: zoom, center_on, fit
ThreeDSceneView3d: lit meshes and glTF models inside a 2D scene
MathTex, Texnot yet — formulas are planned
manim -pql scene.py Soxframe 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())
}
  • Transform is a path morph: the shape's data signal animates to another path. Any two paths morph, with matched start points so they do not twist.
  • The ValueTracker becomes a Signal, and axes.point_on adds a dot that follows it — what always_redraw does 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 MathTex yet. 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 check tells it what went wrong.

Porting a project

  1. One oxframe scene per Manim scene class, in the same order.
  2. Turn units into pixels (× 135 at 1920 × 1080) and flip y. Prefer placing with next_to, to_edge and Layout over coordinates.
  3. Each self.play(…) becomes one .await, or all((…)).await for several animations.
  4. Run oxframe dev to compare, then oxframe 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”.

Edit this page on GitHub