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 Remotion

Remotion builds every frame from React components. oxframe runs each scene as a Rust async fn on a timeline. This page maps the concepts, ports one composition line by line, and lists what has no counterpart yet.

How the two models differ

A Remotion component is a function of the frame number: useCurrentFrame() and interpolate() compute every value on every frame, and a <Sequence> shifts time for its children. An oxframe scene says what happens in order instead: title.opacity().to(1.0, 0.5).await animates for half a second, then the next line runs. Values are signals; all((…)) runs animations together and spawn runs one alongside the rest.

Seeking works the same way in both: oxframe replays the scene up to the frame you ask for, so every frame is deterministic and the editor scrubs freely. Rendering needs no browser. oxframe draws with its own renderer, on the GPU or with --cpu on any machine, and encodes with ffmpeg.

Concepts side by side

Remotionoxframe
<Composition> with fps, size, durationProject::new("name").fps(30).size(1920, 1080).scene(main()); the length is however long the scene runs
useCurrentFrame() + interpolate()node.opacity().to(1.0, 0.5).await, or a Signal animated with .to()
spring()an ease: .ease(presets::OUT_BACK) overshoots, oxframe::elastic_out bounces
<Sequence from={30}>wait_for(1.0).await (frames ÷ fps) before it, spawn to keep something else going, or a scene of its own
<Series>, <TransitionSeries>scenes in order; transition(Transition::fade()).await at the start of the next one
<AbsoluteFill>, flexbox stylesthe View; Layout::new().column().gap(Md) for rows and columns; to_edge, next_to
CSS colors and typethe theme: Accent, Surface, .h1(), .radius(Lg)
<Img>, staticFile()Img::new().file("assets/a.png"), Svg::new().file("assets/logo.svg")
<OffthreadVideo>, <Audio>Video (feature video), sound("hit.wav").play(), Audio::track("music.mp3")
defaultProps, input propsordinary Rust values; read inputs from a file or the environment before building the project
npx remotion renderoxframe render; --jobs renders 2-second segments in parallel
Remotion Lambdaa plain CI runner; --cpu needs no GPU

The same scene in both

A title rises in with a spring, and one second later a line counts up to 100. In Remotion every value is computed from the frame; in oxframe the scene plays the same beats in order.

Remotion

import {AbsoluteFill, Sequence, interpolate, spring, useCurrentFrame, useVideoConfig} from 'remotion';

// <Composition id="Hello" component={Hello} durationInFrames={120} fps={30} width={1920} height={1080} />
export const Hello: React.FC = () => {
  const frame = useCurrentFrame();
  const {fps} = useVideoConfig();
  const rise = spring({frame, fps});
  const opacity = interpolate(frame, [0, 20], [0, 1], {extrapolateRight: 'clamp'});
  const percent = interpolate(frame, [30, 90], [0, 100], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  return (
    <AbsoluteFill style={{backgroundColor: '#141414', color: 'white', justifyContent: 'center', alignItems: 'center'}}>
      <h1 style={{fontSize: 96, opacity, transform: `translateY(${(1 - rise) * 60}px)`}}>Release 2.0</h1>
      <Sequence from={30} layout="none">
        <p style={{fontSize: 40}}>{Math.round(percent)}% faster builds</p>
      </Sequence>
    </AbsoluteFill>
  );
};

oxframe open it in the playground

use oxframe::prelude::*;

#[oxframe::scene]
pub async fn hello(view: View) {
    let title = view.add(Txt::new().text("Release 2.0").h1().opacity(0.0).y(-20.0));
    // Runs alongside the rest of the scene, like an element that is already mounted.
    spawn(async move {
        all((
            title.opacity().to(1.0, 0.66),
            title.y().to(-80.0, 0.8).ease(presets::OUT_BACK),
        ))
        .await;
    });
    wait_for(1.0).await; // <Sequence from={30}> at 30 fps

    let percent = Signal::new(0.0);
    let line = view.add(Txt::new().body().y(40.0));
    line.text().bind({
        let percent = percent.clone();
        move || format!("{:.0}% faster builds", percent.get())
    });
    percent.to(100.0, 2.0).await;
    wait_for(1.0).await;
}

pub fn project() -> Project {
    Project::new("hello").size(1920, 1080).fps(30).scene(hello())
}
  • spring() becomes an ease with overshoot. It is a curve, not a physics simulation, so its length is the one you give it.
  • <Sequence from={30}> at 30 fps is wait_for(1.0). The title is still moving then, so its animation runs in spawn.
  • The counter is a Signal animated with .to(); the text is bound to it and follows on every frame.
  • The styles become theme roles: .h1() and .body() for the type, the theme's background and text colors for the rest.

What does not carry over

  • HTML and CSS. oxframe does not render web pages, so DOM layouts, React component libraries and CSS effects have to be rebuilt with nodes, Layout and the theme. Inter and JetBrains Mono are built in; other fonts load with oxframe::register_font(include_bytes!(…)) and .family("Name").
  • npm packages for visuals. There is no Lottie player. For 3D, View3d covers lit meshes and glTF models, not a full three.js scene.
  • Embedding. There is no published player component for your own web app yet; share a rendered video or a playground link instead.

Porting a project

  1. List the compositions and the sequences inside each.
  2. Write one scene per composition, or per large sequence, and turn frame numbers into seconds (frames ÷ fps).
  3. Replace hand-built boxes with components where they fit: TitleCard, BulletList, LowerThird, EndCard.
  4. Run oxframe check until it passes, then compare stills with oxframe snapshot --at <seconds>.

With an agent: npx skills add oxygenframe/oxframe-sdk installs the remotion-to-oxframe skill next to the main one. Then ask it to port a composition, for example “Port src/Intro.tsx to an oxframe scene and make oxframe check pass”.

Edit this page on GitHub