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
| Remotion | oxframe |
|---|---|
<Composition> with fps, size, duration | Project::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 styles | the View; Layout::new().column().gap(Md) for rows and columns; to_edge, next_to |
| CSS colors and type | the 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 props | ordinary Rust values; read inputs from a file or the environment before building the project |
npx remotion render | oxframe render; --jobs renders 2-second segments in parallel |
| Remotion Lambda | a 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 iswait_for(1.0). The title is still moving then, so its animation runs inspawn.- The counter is a
Signalanimated 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,
Layoutand the theme. Inter and JetBrains Mono are built in; other fonts load withoxframe::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
- List the compositions and the sequences inside each.
- Write one scene per composition, or per large sequence, and turn frame numbers into seconds (frames ÷ fps).
- Replace hand-built boxes with components where they fit:
TitleCard,BulletList,LowerThird,EndCard. - Run
oxframe checkuntil it passes, then compare stills withoxframe 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”.