# oxframe oxframe is procedural animation in Rust. Scenes are async functions; awaiting an animation takes time. This is oxframe 0.24.3, pre-1.0: the API is not stable and changes between versions. Pin the exact version and follow these docs for it, not model memory. Full text: https://oxframe.dev/llms-full.txt Every page also has a Markdown version: add .md to its URL. ## Start here - [Welcome](https://oxframe.dev/docs): Learn oxframe: make animations and motion graphics by writing Rust code, with a live editor to scrub through them and fine-tune the timing. - [Install and first project](https://oxframe.dev/docs/getting-started): Install the oxframe CLI with cargo, create your first animation project and open the live editor — five minutes from nothing to a moving scene. - [Versions and upgrades](https://oxframe.dev/docs/versions): How oxframe versions work before 1.0: weekly releases, breaking changes batched into minor versions, pinning the SDK, and oxframe upgrade. - [Troubleshooting](https://oxframe.dev/docs/troubleshooting): Fixes for the errors of a first oxframe install or render: linker cc not found, old rustc, no ffmpeg, a busy port, protocol versions, WSL and disk space. ## Guide - [1. Your first animation](https://oxframe.dev/docs/guide/hello): Lesson 1 of the oxframe guide: add a circle to the screen and animate it with Rust. The basic pattern behind every animation, with a live example. - [2. In order, or all at once](https://oxframe.dev/docs/guide/together): Lesson 2: run animations in order, all at once or as a ripple with all and stagger, plus ready-made entrances like fade_in and pop_in. - [3. Shapes, colors, transforms](https://oxframe.dev/docs/guide/shapes): Lesson 3: rectangles, circles, lines and polygons; fills and outlines; rotation, scale and color — the properties every oxframe shape shares. - [4. Easing](https://oxframe.dev/docs/guide/easing): Lesson 4: easing curves explained — why some animations feel natural and others robotic, and how to pick the right ease-in and ease-out curve. - [5. Text](https://oxframe.dev/docs/guide/text): Lesson 5: animate text in Rust — titles, typewriter effects, mixed styles and syntax-highlighted code blocks with oxframe. - [6. Groups and layout](https://oxframe.dev/docs/guide/groups): Lesson 6: nest shapes in groups and let flexbox-style layout arrange them, so whole compositions move and scale together. - [7. Tweak timing in the editor](https://oxframe.dev/docs/guide/editor): Lesson 7: adjust timing by eye in the oxframe editor — markers, eases and tunables you can drag without touching or recompiling code. - [8. Signals](https://oxframe.dev/docs/guide/signals): Lesson 8: signals make one value follow another — a line that always connects two dots, a label that always shows a number. - [9. Drawing and morphing](https://oxframe.dev/docs/guide/drawing): Lesson 9: draw an outline as if with a pen, then morph one shape into another — path drawing and shape morphing in oxframe. - [10. Randomness and background tasks](https://oxframe.dev/docs/guide/random): Lesson 10: background tasks and seeded randomness — many things moving on their own while the story continues, rendering the same video every time. - [11. Scenes and transitions](https://oxframe.dev/docs/guide/scenes): Lesson 11: build a video from several scenes and blend them with slide, fade, wipe and zoom transitions, all in one oxframe render. - [12. Sound and voice](https://oxframe.dev/docs/guide/sound): Lesson 12: add sound effects on the beat, a soundtrack that ducks under speech, and time animations to the words of a spoken line. - [13. Shaders and effects](https://oxframe.dev/docs/guide/shaders): Lesson 13: animated water, drops on the camera lens, ripples on a node and a water transition — shader presets you animate like any property. - [14. Charts and graphs](https://oxframe.dev/docs/guide/charts): Lesson 14: plot functions on axes, ride a dot along a curve, morph it into another function and zoom — the whole chart follows the ranges. - [15. Vectors and matrices](https://oxframe.dev/docs/guide/matrices): Lesson 15: draw a vector on axes, stretch the whole plane with a 2 × 2 matrix and show the numbers in brackets — the grid and the arrow follow. ## Going further - [Components](https://oxframe.dev/docs/guide/components): Build an explainer from ready-made blocks: title cards, bullet lists, code, flow diagrams, callouts, lower thirds and end cards. - [All nodes](https://oxframe.dev/docs/nodes): Reference of every oxframe node and its properties: Rect, Circle, Line, Path, Polygon, Star, Txt, Code, Img, Layout and Grid. - [Effects](https://oxframe.dev/docs/effects): Gradients, dashes, shadows, blur, blend modes and clipping in oxframe. Every effect is a signal and animates like any other property. - [Animated charts and plots](https://oxframe.dev/docs/charts): Animated charts in Rust: axes with labels, function plots that follow animated ranges, bars that grow with their values, and number lines. - [Matrices, vectors and tables](https://oxframe.dev/docs/matrices): Animated matrices, vectors and tables in Rust: matrix multiplication step by step, linear maps that bend the grid, and rows that sort themselves. - [3D scenes in 2D animations](https://oxframe.dev/docs/3d): Add lit 3D models to oxframe animations: cubes, spheres and glTF models, a camera and lights as signals, and 2D labels pinned to 3D points. - [Presentations from scenes](https://oxframe.dev/docs/presentations): Turn oxframe scenes into a talk: slide_break stops, speaker notes in a presenter window, clicker keys, and every stop as a PDF page. - [Writing your own node](https://oxframe.dev/docs/custom-nodes): Make a node of your own in Rust: a struct that draws with DrawCtx, animatable signal fields, layout and checks, and a crate template. - [Icons: Lucide, Tabler and brand logos](https://oxframe.dev/docs/icons): Lucide, Tabler and Simple Icons in Rust animations: icon!("lucide:server") by name, colors and line widths as signals, and icons in flow diagrams. - [Formulas: Typst math in Rust animations](https://oxframe.dev/docs/formulas): Typeset math in oxframe with Typst: Math::new("e^(i pi) + 1 = 0"), font size and theme color, display style, and writing a formula on glyph by glyph. - [Camera](https://oxframe.dev/docs/camera): Zoom, pan and rotate the view in oxframe: fit a whole diagram, center on a detail, follow a moving node, and keep titles on screen. - [Markers, eases, tunables](https://oxframe.dev/docs/timing): Markers, custom eases and tunables: timing values stored outside your Rust code so you can adjust them visually in the oxframe editor. - [The editor](https://oxframe.dev/docs/editor): A tour of the oxframe editor: viewport, timeline, inspector and hot reload — the same app in oxframe dev and in the browser playground. - [Rendering video](https://oxframe.dev/docs/rendering): Render oxframe animations to MP4, WebM with alpha, ProRes 4444, an animated GIF or PNG frames with one command, with audio, voice and scene transitions. - [Embed an animation in a web page](https://oxframe.dev/docs/embed): Play an oxframe animation on any website with the element: one script tag, no video file, sharp at every size, with controls and a JS API. - [CLI reference](https://oxframe.dev/docs/cli): Reference for the oxframe command line: init, dev, render, snapshot, check, agents, upgrade and mcp, with all options. - [Working with AI agents](https://oxframe.dev/docs/agents): Use Claude Code, Codex or Cursor to write oxframe animations: every project ships agent instructions and a way for agents to see what they made. - [MCP server for Claude Code, Codex, Cursor](https://oxframe.dev/docs/mcp): Connect oxframe to Claude Code, Codex, Cursor, VS Code or ChatGPT over MCP: the local server checks and renders videos, the hosted one serves the docs. - [Check rules](https://oxframe.dev/docs/check-rules): Every rule in an oxframe check report, out_of_frame to too_fast and low_contrast, with before and after frames and how to fix each one. ## Migrating - [Migrating from Remotion](https://oxframe.dev/docs/migrate/remotion): Move a Remotion video to oxframe: useCurrentFrame, interpolate, Sequence and spring mapped to Rust scenes, one composition ported, and what changes. - [Migrating from Motion Canvas](https://oxframe.dev/docs/migrate/motion-canvas): Port Motion Canvas scenes to oxframe: generators become async Rust, refs become handles, with a table of every concept and one scene in both. - [Migrating from Manim](https://oxframe.dev/docs/migrate/manim): Port Manim scenes to oxframe: self.play, Create, Transform, ValueTracker and Axes mapped to Rust, two scenes ported, and what LaTeX users should know. - [Migrating from Revideo](https://oxframe.dev/docs/migrate/revideo): Move Revideo projects to oxframe: scenes port like Motion Canvas, variables become environment or files, and renderVideo becomes oxframe render. ## Under the hood - [How oxframe works](https://oxframe.dev/docs/how-it-works): How oxframe works inside: scenes as Rust coroutines, deterministic rendering, instant scrubbing, and compiling Rust to WebAssembly for the browser. ## Guide lessons (in order, each builds on the previous one) ### learn_01_hello: Lesson 1 — Hello: add a shape and move it. ```rust //! Lesson 1 — Hello: add a shape and move it. //! //! A scene is an `async fn`. You add shapes to the `view`, then animate //! their properties. `.await` means "wait until this animation is done". use oxframe::prelude::*; #[oxframe::scene] pub async fn hello(view: View) { // Add a circle on the left. `view.add` gives back a handle to animate it. let ball = view.add(Circle::new().size(160.0).x(-400.0).fill(color!("#68abdf"))); // Move it to the right over 1 second. ball.x().to(400.0, 1.0).await; // When that is done, shrink it to nothing over half a second. ball.size().to(0.0, 0.5).await; } // Try: // - change `1.0` to `3.0`: the move takes three seconds; // - add `ball.fill().to(color!("#e13238"), 1.0).await;` before the shrink. pub fn project() -> Project { Project::new("hello").scene(hello()) } ``` ### learn_02_together: Lesson 2 — One after another, or all at once. ```rust //! Lesson 2 — One after another, or all at once. //! //! Each `.await` waits, so animations written one after another play in //! order. `all((..))` plays several at the same time; `stagger` starts them //! with a small delay between each. use oxframe::prelude::*; #[oxframe::scene] pub async fn together(view: View) { let row = |y: f64, c: Color| view.add(Rect::new().size(90.0).position((-500.0, y)).fill(c)); // 1. In order: the second square starts when the first one stops. let a = row(-250.0, color!("#e13238")); let b = row(-130.0, color!("#e13238")); a.x().to(500.0, 0.8).await; b.x().to(500.0, 0.8).await; // 2. At the same time: `all` waits for the longest one. let c = row(10.0, color!("#68abdf")); let d = row(130.0, color!("#68abdf")); all((c.x().to(500.0, 0.8), d.x().to(500.0, 1.6))).await; // 3. A pause, then a staggered wave of five squares. wait_for(0.3).await; let wave: Vec<_> = (0..5) .map(|i| { view.add( Circle::new() .size(60.0) .position((-500.0 + i as f64 * 70.0, 280.0)) .fill(color!("#2ecc71")), ) }) .collect(); stagger(0.1, &wave, |c| c.y().to(200.0, 0.4)).await; } // Try: swap the two lines in step 1 into an `all((..))` and watch them move together. pub fn project() -> Project { Project::new("together").scene(together()) } ``` ### learn_03_shapes: Lesson 3 — Shapes, colors and transforms. ```rust //! Lesson 3 — Shapes, colors and transforms. //! //! Every shape is made with a builder (`Rect::new().size(..)`) that sets its //! starting look. After `view.add`, the same names without arguments give you //! the property to animate: `.size(120.0)` sets it, `.size()` animates it. use oxframe::prelude::*; #[oxframe::scene] pub async fn shapes(view: View) { let square = view.add( Rect::new() .size(140.0) .radius(16.0) .x(-450.0) .fill(color!("#e13238")), ); let circle = view.add(Circle::new().size(140.0).x(-150.0).fill(color!("#f5c242"))); let hexagon = view.add( Polygon::new() .sides(6.0) .size(150.0) .x(150.0) .fill(color!("#68abdf")), ); let star = view.add( Star::new() .points(5.0) .size(160.0) .x(450.0) .fill(color!("#2ecc71")), ); // Outlines: `stroke` is the line color, `line_width` its thickness. let arrow = view.add( Line::new() .points([(-450.0, 220.0), (450.0, 220.0)]) .stroke(Color::WHITE) .line_width(6.0) .end_arrow(true), ); // Transforms: rotation (degrees), scale, and position. all(( square.rotation().to(45.0, 1.0), circle.scale().to(Vec2::new(1.4, 0.7), 1.0), hexagon.sides().to(3.0, 1.0), star.rotation().to(144.0, 1.0), arrow.y().to(-440.0, 1.0), )) .await; // Colors interpolate smoothly too. all(( square.fill().to(color!("#9b59b6"), 0.8), circle.fill().to(color!("#e67e22"), 0.8), )) .await; } // Try: give the hexagon an outline with `.stroke(Color::WHITE).line_width(8.0)`. pub fn project() -> Project { Project::new("shapes").scene(shapes()) } ``` ### learn_04_easing: Lesson 4 — Easing: how an animation speeds up and slows down. ```rust //! Lesson 4 — Easing: how an animation speeds up and slows down. //! //! By default animations start slowly, speed up and end slowly. `.ease(..)` //! picks another curve. Watch the three balls: same distance, same time, //! different feel. use oxframe::prelude::*; #[oxframe::scene] pub async fn easing(view: View) { let lane = |y: f64, name: &str| { view.add( Txt::new() .text(name) .family(MONO) .font_size(28.0) .position((-620.0, y)) .fill(Color::WHITE.with_alpha(0.6)), ); view.add( Circle::new() .size(70.0) .position((-400.0, y)) .fill(color!("#68abdf")), ) }; let linear = lane(-150.0, "LINEAR"); let smooth = lane(0.0, "default"); let bouncy = lane(150.0, "OUT_BACK"); all(( linear.x().to(500.0, 1.5).ease(presets::LINEAR), smooth.x().to(500.0, 1.5), bouncy.x().to(500.0, 1.5).ease(presets::OUT_BACK), )) .await; wait_for(0.5).await; } // Try: `presets::IN_OUT_EXPO`, `presets::OUT_CUBIC`, or the functions // `bounce_out` and `elastic_out` (as `.ease(oxframe::bounce_out)`). pub fn project() -> Project { Project::new("easing").scene(easing()) } ``` ### learn_05_text: Lesson 5 — Text. ```rust //! Lesson 5 — Text. //! //! `Txt` draws text with the built-in Inter font (or JetBrains Mono with //! `.family(MONO)`). Text animates like anything else: tweening the text //! types it out, `end` reveals it letter by letter, and `rich` mixes styles. use oxframe::prelude::*; #[oxframe::scene] pub async fn text(view: View) { let title = view.add(Txt::new().text("").font_size(96.0).weight(800.0).y(-150.0)); // Typewriter: from "" to the full text. title .text() .to("Hello, text", 1.0) .ease(presets::LINEAR) .await; let line = view.add( Txt::new() .font_size(44.0) .rich([ span("Mix "), span("bold").bold(), span(", "), span("color").color(color!("#f5c242")), span(" and "), span("italic").italic(), ]) .end(0.0), ); // Reveal the line letter by letter. line.end().to(1.0, 1.0).ease(presets::LINEAR).await; let code = view.add( Code::new() .code("let x = 42; // code, highlighted") .font_size(36.0) .y(150.0) .opacity(0.0), ); code.opacity().to(1.0, 0.5).await; wait_for(0.5).await; } // Try: `title.morph_to("Hello, world", 1.0).await;` — shared letters slide into place. pub fn project() -> Project { Project::new("text").scene(text()) } ``` ### learn_06_groups: Lesson 6 — Groups and layout. ```rust //! Lesson 6 — Groups and layout. //! //! Nodes can have children. Children move, rotate and fade with their parent, //! and their positions are relative to it. A `Layout` places its children in //! a row or a column for you. use oxframe::prelude::*; #[oxframe::scene] pub async fn groups(view: View) { // A tiny solar system: the moon orbits the planet, the planet the sun. let sun = view.add( Group::new() .x(-350.0) .child(Circle::new().size(120.0).fill(color!("#f5c242"))), ); let planet = sun.add(Group::new().x(220.0)); planet.add(Circle::new().size(50.0).fill(color!("#68abdf"))); planet.add(Circle::new().size(18.0).x(60.0).fill(Color::WHITE)); // A row of cards laid out automatically. let row = view.add( Layout::new() .gap(16.0) .padding(16.0) .radius(16.0) .fill(color!("#26282e")) .position((380.0, 0.0)), ); for c in ["#e13238", "#f5c242", "#68abdf"] { row.add( Rect::new() .size(70.0) .radius(10.0) .fill(Color::from_hex(c).unwrap()), ); } all(( sun.rotation().to(360.0, 3.0).ease(presets::LINEAR), planet.rotation().to(720.0, 3.0).ease(presets::LINEAR), // Growing the gap re-flows the row. row.gap().to(40.0, 1.5), )) .await; } // Try: add a fourth card with `.grow(1.0)` and give the row `.width(500.0)`. pub fn project() -> Project { Project::new("groups").scene(groups()) } ``` ### learn_07_editor: Lesson 7 — Timing you can change in the editor. ```rust //! Lesson 7 — Timing you can change in the editor. //! //! Some values are easier to tune by eye than in code. `wait_until("name")` //! creates a marker on the timeline, `tune!("name", default)` a slider in the //! inspector and `ease!("name")` a curve in the curve editor. Changing them //! does not recompile anything: the values are stored in `.meta.json`. use oxframe::prelude::*; #[oxframe::scene] pub async fn editor(view: View) { let size = tune!("size", 160.0); let dot = view.add(Circle::new().size(size).x(-400.0).fill(color!("#68abdf"))); // Drag the "go" marker on the timeline to start the move later or earlier. wait_until("go").await; dot.x().to(400.0, 1.0).ease(ease!("move")).await; // Drag the "fade" marker, too. wait_until("fade").await; dot.opacity().to(0.0, 0.5).await; } // Try (in the editor): drag the markers, move the `size` slider, click the // `move` ease and bend the curve. pub fn project() -> Project { Project::new("editor").scene(editor()) } ``` ### learn_08_signals: Lesson 8 — Signals: properties that follow other properties. ```rust //! Lesson 8 — Signals: properties that follow other properties. //! //! Every property is a `Signal`. Besides animating it, you can `bind` it to a //! formula over other signals; it then updates by itself. Here the line and //! the label follow the two dots — only the dots are animated. use oxframe::prelude::*; #[oxframe::scene] pub async fn signals(view: View) { let a = view.add( Circle::new() .size(40.0) .position((-300.0, -100.0)) .fill(color!("#e13238")), ); let b = view.add( Circle::new() .size(40.0) .position((300.0, 100.0)) .fill(color!("#68abdf")), ); let link = view.add( Line::new() .stroke(Color::WHITE.with_alpha(0.6)) .line_width(4.0) .z_index(-1.0), ); let label = view.add(Txt::new().font_size(32.0).family(MONO)); // The line's points are computed from the dots' positions. let (ax, ay, bx, by) = (a.x(), a.y(), b.x(), b.y()); link.points() .bind(move || vec![Vec2::new(ax.get(), ay.get()), Vec2::new(bx.get(), by.get())]); // The label sits in the middle and shows the distance. let (ax, ay, bx, by) = (a.x(), a.y(), b.x(), b.y()); label.text().bind(move || { format!( "{:.0} px", Vec2::new(bx.get() - ax.get(), by.get() - ay.get()).hypot() ) }); let (ax, bx) = (a.x(), b.x()); label.x().bind(move || (ax.get() + bx.get()) / 2.0); let (ay, by) = (a.y(), b.y()); label.y().bind(move || (ay.get() + by.get()) / 2.0 - 40.0); all((a.y().to(150.0, 1.5), b.x().to(-50.0, 1.5))).await; all((a.x().to(100.0, 1.0), b.y().to(-200.0, 1.0))).await; } // Try: bind `b.fill()` so the blue dot turns red when it gets close to the red one. pub fn project() -> Project { Project::new("signals").scene(signals()) } ``` ### learn_09_drawing: Lesson 9 — Drawing and morphing shapes. ```rust //! Lesson 9 — Drawing and morphing shapes. //! //! `start` and `end` choose which part of an outline is drawn: animate `end` //! from 0 to 1 to draw a shape on. A `Path` can morph into any other path. use oxframe::prelude::*; #[oxframe::scene] pub async fn drawing(view: View) { let star = BezPath::from_svg( "M0 -150 L44 -46 L150 -46 L64 18 L96 130 L0 64 L-96 130 L-64 18 L-150 -46 L-44 -46 Z", ) .unwrap(); let heart = BezPath::from_svg( "M0 140 C-60 80 -160 30 -160 -50 C-160 -110 -110 -150 -60 -150 C-30 -150 -10 -130 0 -110 \ C10 -130 30 -150 60 -150 C110 -150 160 -110 160 -50 C160 30 60 80 0 140 Z", ) .unwrap(); let shape = view.add( Path::new() .data(star) .fill(Color::TRANSPARENT) .stroke(color!("#f5c242")) .line_width(8.0) .end(0.0), ); // Draw the outline on, then fill it. shape.end().to(1.0, 1.2).await; shape.fill().to(color!("#f5c242"), 0.4).await; // Morph into a heart, changing color on the way. all(( shape.data().to(heart, 1.2), shape.fill().to(color!("#e13238"), 1.2), shape.stroke().to(color!("#e13238"), 1.2), )) .await; wait_for(0.5).await; } // Try: morph back into the star, or animate `start` to 1 to erase the outline. pub fn project() -> Project { Project::new("drawing").scene(drawing()) } ``` ### learn_10_random: Lesson 10 — Randomness and background tasks. ```rust //! Lesson 10 — Randomness and background tasks. //! //! `random()` gives numbers that look random but are the same on every run: //! they come from the project's seed, so renders are reproducible. `spawn` //! starts an animation in the background while the scene continues. use oxframe::prelude::*; #[oxframe::scene] pub async fn random_lesson(view: View) { // Confetti in the background. let rain = spawn(async move { for _ in 0..60 { let x = random_range(-800.0, 800.0); let c = Color::rgb(random() as f32, 0.6, 1.0 - random() as f32); let piece = view.add( Rect::new() .size(random_range(12.0, 28.0)) .position((x, -600.0)) .fill(c), ); // Each piece falls on its own, also in the background. spawn(async move { all(( piece.y().to(600.0, 2.0).ease(presets::LINEAR), piece.rotation().to(360.0, 2.0), )) .await; piece.remove(); }); wait_for(0.05).await; } }); // Meanwhile, in the foreground: let title = view.add( Txt::new() .text("seed = 7") .font_size(110.0) .weight(800.0) .scale(0.0), ); title .scale() .to(Vec2::new(1.0, 1.0), 0.8) .ease(presets::OUT_BACK) .await; rain.await; wait_for(2.0).await; } // Try: change `.seed(7)` below to another number — a different, but again // repeatable, confetti. pub fn project() -> Project { Project::new("random").seed(7).scene(random_lesson()) } ``` ### learn_11_scenes: Lesson 11 — Scenes and transitions. ```rust //! Lesson 11 — Scenes and transitions. //! //! A project is a list of scenes, played in order. A scene that starts with //! `transition(...)` blends in over the one before it, so one render gives //! the whole video without cutting it together elsewhere. use oxframe::prelude::*; #[oxframe::scene] pub async fn first(view: View) { let title = view.add(TitleCard::new("Scene one").subtitle("a title card")); title.enter().await; wait_for(0.6).await; } #[oxframe::scene] pub async fn second(view: View) { // The previous scene holds its last frame while this one slides in. // The call waits out the blend; its length comes from the theme. transition(Transition::slide(Dir::Left)).await; let list = view.add(BulletList::new([ "Each scene is an async function", "Project::scene adds it in order", "transition blends it in", ])); list.enter().await; list.next().await; list.next().await; wait_for(0.4).await; } #[oxframe::scene] pub async fn third(view: View) { transition(Transition::fade().duration(0.8)).await; let end = view.add(EndCard::new("Three scenes, one video")); end.enter().await; } // Try: `Transition::wipe(Dir::Up)` or `Transition::zoom_in()` in `second`. pub fn project() -> Project { Project::new("scenes") .scene(first()) .scene(second()) .scene(third()) } ``` ### learn_12_sound: Lesson 12 — Sound and voice. ```rust //! Lesson 12 — Sound and voice. //! //! A sound starts at the frame where the scene starts it, so it stays in //! sync when you change the timing. A soundtrack plays under the whole //! project and ducks when a voice speaks. use oxframe::prelude::*; fn asset(name: &str) -> String { format!("{}/assets/{name}", env!("CARGO_MANIFEST_DIR")) } #[oxframe::scene] pub async fn sound_lesson(view: View) { let dot = view.add(Circle::new().size(24.0).fill(Accent)); // A click on the beat: start it, then animate. sound(asset("click.wav")).trim(0.0, 0.18).play(); dot.size().to(140.0, 0.2).await; dot.size().to(24.0, 0.3).await; // A spoken line. Without a speech provider its length is estimated, // which is enough to time the picture to it. let line = voice("Sound follows the picture").start(); line.at_word("follows").await; dot.size().to(200.0, 0.3).await; line.await; } // Try: move the second `dot.size()` before `line.at_word(..)`. pub fn project() -> Project { Project::new("sound") .audio( Audio::track(asset("tone.wav")) .volume(0.4) .duck_under_voice(), ) .scene(sound_lesson()) } ``` ### learn_13_shaders: Lesson 13 — Shaders and effects. ```rust //! Lesson 13 — Shaders and effects. //! //! Presets paint pixels with a shader: water as a node, drops on the lens //! over everything drawn before them, rings on a node, and water rising //! into the next scene. Their parameters are signals, so you animate them //! like any other property. use oxframe::prelude::*; #[oxframe::scene] pub async fn sea(view: View) { let water = view.add( shaders::waves() .deep(Accent) .foam(Surface) .width(pct(100)) .height(pct(40)) .to_edge(Edge::Bottom), ); let title = view.add(Txt::new().text("Shaders").display().to_edge(Edge::Top)); // Added last: it bends everything drawn before it. let lens = view.add(shaders::lens_drops().wetness(0.0).fullscreen()); title.fade_in().await; water.amp().to(0.12, 1.0).await; // An effect changes one node's own pixels. let rings = water.effect(shaders::ripple().strength(0.0)); rings.strength().to(1.0, 0.4).await; rings.strength().to(0.0, 0.8).await; lens.wetness().to(1.0, 0.5).await; wait_for(0.4).await; } #[oxframe::scene] pub async fn land(view: View) { // Added before the transition, so the rising water uncovers it. view.add(Txt::new().text("Dry land").display()); transition(Transition::shader(shaders::water_wipe())).await; wait_for(0.6).await; } // Try: `.wetness(0.3)` for a light drizzle, or `water_wipe().wave(0.1)`. pub fn project() -> Project { Project::new("shaders").scene(sea()).scene(land()) } ``` ### learn_14_charts: Lesson 14 — Charts and graphs. ```rust //! Lesson 14 — Charts and graphs. //! //! `Axes` maps data coordinates into a box. A plot is a function of x, //! sampled again every frame, so it follows when the ranges move. Values //! that change are signals, like everything else in oxframe. use oxframe::prelude::*; #[oxframe::scene] pub async fn charts_lesson(view: View) { let axes = view.add(Axes::new().x_range(-4.0..4.0).y_range(-1.5..1.5)); // A curve draws itself on, like any path. let curve = axes.plot(|x| (x * 1.5).sin()); curve.draw_on().await; // A dot rides on the curve: its x is a signal. let x = Signal::new(-4.0); axes.point_on(&curve, x.clone()); x.to(4.0, 2.0).await; // Morph into another function, then zoom out. curve .to_fn(|x| x.sin() * 0.5 + (3.0 * x).sin() * 0.4, 1.0) .await; axes.x_range().to(-8.0..8.0, 1.0).await; wait_for(0.4).await; } // Try: plot `|x| x.tan()` — the gaps stay gaps, no line jumps across. pub fn project() -> Project { Project::new("charts_lesson").scene(charts_lesson()) } ``` ### learn_15_matrices: Lesson 15 — Vectors and matrices. ```rust //! Lesson 15 — Vectors and matrices. //! //! A vector is an arrow on `Axes`. `apply_matrix` maps the whole plane //! through a 2 × 2 matrix: the grid and every vector follow. `Matrix` shows //! the numbers next to it. use oxframe::prelude::*; #[oxframe::scene] pub async fn matrices_lesson(view: View) { let axes = view.add( Axes::new() .x_range(-3.0..3.0) .y_range(-3.0..3.0) .width(640.0) .height(640.0) .x(-220.0), ); // An arrow from the origin; its tip is two signals. let v = axes.vector(1.0, 2.0); v.draw_on().await; v.label("v"); // The matrix, beside the axes. let m = view.add(Matrix::new([[2, 0], [0, 1]]).font_size(56.0).x(480.0)); m.fade_in().await; // Stretch x by 2: the grid and the vector follow. axes.apply_matrix([[2.0, 0.0], [0.0, 1.0]], 1.5).await; wait_for(0.5).await; // Move the tip on its own; it is still stretched. v.to(-1.0, 1.0, 1.0).await; wait_for(0.4).await; } // Try: a shear, `axes.apply_matrix([[1.0, 1.0], [0.0, 1.0]], 1.5)`. pub fn project() -> Project { Project::new("matrices_lesson").scene(matrices_lesson()) } ``` ## Examples ### intro: A square slides from left to right and turns into a circle in 1 s. ```rust //! A square slides from left to right and turns into a circle in 1 s. use oxframe::prelude::*; #[oxframe::scene] pub async fn intro(view: View) { let square = view.add(Rect::new().size(200.0).x(-300.0).fill(color!("#e13238"))); all(( square.x().to(300.0, 1.0), square.radius().to(100.0, 1.0), square.fill().to(color!("#68abdf"), 1.0), )) .await; } pub fn project() -> Project { Project::new("intro").scene(intro()) } ``` ### architecture: An architecture diagram with icons: `icon!` names an icon from Lucide, ```rust //! An architecture diagram with icons: `icon!` names an icon from Lucide, //! Tabler or Simple Icons, and only those end up in the binary. use oxframe::prelude::*; #[oxframe::scene] pub async fn request(view: View) { view.add( Txt::new() .text("How a request is served") .h1() .to_edge(Edge::Top), ); let flow = view.add( FlowDiagram::new() .edge("browser", "edge") .edge("edge", "api") .edge("api", "cache") .edge("api", "database") .icon("browser", icon!("lucide:globe")) .icon("edge", icon!("lucide:cloud")) .icon("api", icon!("lucide:server")) .icon("cache", icon!("tabler:bolt")) .icon("database", icon!("lucide:database")) .opacity(0.0), ); flow.enter().await; for _ in 0..flow.len() { flow.next().await; } // A slow query: the database turns red and back. if let Some(db) = flow.icon("database") { db.color().to(Danger, 0.3).await; db.stroke_width().to(3.0, 0.3).await; wait_for(0.4).await; all((db.color().to(Text, 0.4), db.stroke_width().to(2.0, 0.4))).await; } let logos = view.add(Layout::new().gap(56.0).y(300.0).opacity(0.0)); for name in [ icon!("simple:github"), icon!("simple:rust"), icon!("simple:cloudflare"), ] { logos.add(name.size(56.0).color(Muted)); } logos.fade_in().await; wait_for(0.6).await; } pub fn project() -> Project { Project::new("architecture").scene(request()) } ``` ### camera: A camera tour of a system diagram: see all of it, zoom onto one step, ```rust //! A camera tour of a system diagram: see all of it, zoom onto one step, //! follow a request as it travels, then back out. The title stays put: it //! is added to the view, not to the camera. use oxframe::prelude::*; const STEPS: [&str; 6] = ["Browser", "CDN", "API", "Queue", "Worker", "Database"]; /// Where step `i` sits in the world the camera looks at (wider than a frame). fn spot(i: usize) -> (f64, f64) { let x = -1500.0 + i as f64 * 600.0; let y = if i.is_multiple_of(2) { -140.0 } else { 140.0 }; (x, y) } #[oxframe::scene] pub async fn camera(view: View) { let cam = view.add(Camera::new().name("camera")); let world = cam.add(Group::new().name("diagram")); for i in 1..STEPS.len() { let (a, b) = (spot(i - 1), spot(i)); world.add( Line::new() .points([(a.0 + 180.0, a.1), (b.0 - 180.0, b.1)]) .stroke(Muted) .line_width(6.0) .end_arrow(true), ); } let boxes: Vec = STEPS .iter() .enumerate() .map(|(i, name)| { let (x, y) = spot(i); world.add( Rect::new() .width(360.0) .height(160.0) .radius(Md) .fill(Surface) .stroke(Surface) .line_width(6.0) .position((x, y)) .name(*name) .child(Txt::new().text(*name).h2()), ) }) .collect(); let title = view.add( Txt::new() .text("One request, end to end") .h2() .to_edge(Edge::Top) .name("title"), ); // The whole diagram, then the steps appear one by one. cam.fit(world, 140.0, 0.0).await; title.fade_in().await; stagger(0.12, &boxes, |b| b.pop_in()).await; wait_for(0.4).await; // A closer look at the API. all((cam.center_on(boxes[2], 1.2), cam.zoom().to(1.6, 1.2))).await; wait_for(0.6).await; // A request travels on; the camera follows it. let (x0, y0) = spot(2); let packet = world.add( Circle::new() .size(44.0) .fill(Accent) .position((x0, y0 + 110.0)), ); packet.pop_in().await; let (px, py) = (packet.x(), packet.y()); cam.x().bind(move || px.get()); cam.y().bind(move || py.get() - 110.0); // Each step it reaches lights up (an outline: text on a solid accent // fill would be hard to read). for (i, step) in boxes.iter().enumerate().skip(3) { let (x, y) = spot(i); all((packet.x().to(x, 1.0), packet.y().to(y + 110.0, 1.0))).await; step.stroke().to(Accent, 0.3).await; } // Back out to the whole picture. cam.fit(world, 140.0, 1.4).await; wait_for(1.0).await; } pub fn project() -> Project { Project::new("camera").scene(camera()) } ``` ### chart: A function plot: grid, axes with arrows, a curve drawn on with `end`, ```rust //! A function plot: grid, axes with arrows, a curve drawn on with `end`, //! a dot following it — timed by markers that can be moved in the editor. use oxframe::prelude::*; const W: f64 = 1400.0; const H: f64 = 700.0; fn curve(phase: f64) -> Vec { (0..=200) .map(|i| { let t = i as f64 / 200.0; let x = -W / 2.0 + t * W; let y = -(t * std::f64::consts::TAU * 2.0 + phase).sin() * H * 0.3; Vec2::new(x, y) }) .collect() } #[oxframe::scene] pub async fn chart(view: View) { view.add(Grid::new().width(W).height(H).spacing(70.0).opacity(0.0)); let grid = view.children()[0]; let axes = [ view.add( Line::new() .points([(-W / 2.0 - 20.0, 0.0), (W / 2.0 + 30.0, 0.0)]) .end_arrow(true) .end(0.0), ), view.add( Line::new() .points([(-W / 2.0, H / 2.0), (-W / 2.0, -H / 2.0 - 30.0)]) .end_arrow(true) .end(0.0), ), ]; let plot = view.add( Line::new() .points(curve(0.0)) .stroke(color!("#68abdf")) .line_width(6.0) .end(0.0), ); let label = view.add( Txt::new() .text("y = sin(4πx)") .font_size(44.0) .position((0.0, -H / 2.0 - 60.0)) .opacity(0.0), ); all(( grid.opacity().to(1.0, 0.6), axes[0].end().to(1.0, 0.6), axes[1].end().to(1.0, 0.6), )) .await; // Marker: move it in the timeline to retime everything after it. wait_until("draw").await; all(( plot.end() .to(1.0, 2.0) .ease(ease!("draw", presets::IN_OUT_SINE)), label.opacity().to(1.0, 1.0), )) .await; let dot = view.add( Circle::new() .size(28.0) .fill(color!("#f5c242")) .position(curve(0.0)[0]), ); let pts = curve(0.0); let t = Signal::new(0.0f64); { // Bind the dot's position to a progress signal. let (t, pts) = (t.clone(), pts.clone()); let pos = Signal::derived(move || { let u = t.get().clamp(0.0, 1.0) * (pts.len() - 1) as f64; let i = (u as usize).min(pts.len() - 2); pts[i].lerp(pts[i + 1], u - i as f64) }); let p = pos.clone(); dot.x().bind(move || p.get().x); dot.y().bind(move || pos.get().y); } wait_until("dot").await; t.to(1.0, tune!("dot_seconds", 2.5)).await; // Shift the phase: points tween element-wise. plot.points().to(curve(std::f64::consts::PI), 1.2).await; wait_for(0.5).await; } pub fn project() -> Project { Project::new("chart").scene(chart()) } ``` ### charts: Axes and plots: a curve with a dot riding on it, a morph into another ```rust //! Axes and plots: a curve with a dot riding on it, a morph into another //! function, a zoom out, then a benchmark whose bars grow with their labels. use oxframe::prelude::*; #[oxframe::scene] pub async fn functions(view: View) { let axes = view.add(Axes::new().x_range(-5.0..5.0).y_range(-2.0..2.0)); let wave = axes.plot(|x| x.sin()); wave.draw_on().await; // A signal drives the dot along the curve. let x = Signal::new(-5.0); axes.point_on(&wave, x.clone()); x.to(5.0, 2.0).await; wave.to_fn(|x| (2.0 * x).sin() * 1.5, 1.0).await; // The grid, ticks, labels and the curve follow the range. axes.x_range().to(-10.0..10.0, 1.2).await; wait_for(0.4).await; } #[oxframe::scene] pub async fn build_times(view: View) { transition(Transition::fade()).await; let title = view.add(Txt::new().text("Build time, s").h1().to_edge(Edge::Top)); let axes = view.add( Axes::new() .x_range(-0.5..3.5) .y_range(0.0..10.0) .height(pct(55)) .y(40.0), ); let bars = axes .bars([0.0, 0.0, 0.0, 0.0]) .labels(["rustc", "gcc", "clang", "zig"]); title.fade_in().await; stagger( 0.15, [3.2, 7.5, 5.1, 9.4].into_iter().enumerate(), |(i, v)| bars.value(i).to(v, 1.0), ) .await; wait_for(0.6).await; } pub fn project() -> Project { Project::new("charts") .scene(functions()) .scene(build_times()) } ``` ### code_steps: Write a function in steps: unchanged tokens slide, new ones appear. ```rust //! Write a function in steps: unchanged tokens slide, new ones appear. use oxframe::prelude::*; #[oxframe::scene] pub async fn steps(view: View) { let code = view.add( Code::new() .language("rust") .font_size(42.0) .code("fn double(x: i32) -> i32 {\n}\n"), ); wait_for(0.3).await; // The body appears on its own line. code.insert(2, 0, " x\n", 0.7).await; code.focus_lines(2, 2, 0.3).await; // `x` slides down as the new statement pushes it. code.insert(2, 4, "let y = x * 2;\n ", 0.8).flash().await; code.replace(((3, 4), (3, 5)), "y", 0.5).await; code.focus_lines(2, 3, 0.4).await; wait_for(0.4).await; } pub fn project() -> Project { Project::new("code_steps").scene(steps()) } ``` ### components: A short explainer built only from components. ```rust //! A short explainer built only from components. //! The scene body stays under 40 lines: the blocks already know the theme, //! the safe area and how to appear. use oxframe::prelude::*; #[oxframe::scene] pub async fn explainer(view: View) { let title = view.add(TitleCard::new("Binary search").subtitle("O(log n)")); title.enter().await; title.next().await; title.exit().await; let chapter = view.add(ChapterMarker::new("1 Search")); chapter.enter().await; chapter.next().await; chapter.exit().await; let steps = view.add(BulletList::new(["Take the middle", "Compare", "Drop half"])); steps.enter().await; for _ in 0..steps.len() { steps.next().await; } steps.exit().await; let code = view.add(CodeExplainer::new("lo + (hi - lo) / 2").step(1, 1, "the middle")); code.enter().await; code.next().await; code.exit().await; let flow = view.add(FlowDiagram::new().edge("Client", "API").edge("API", "DB")); flow.enter().await; for _ in 0..flow.len() { flow.next().await; } flow.exit().await; let card = view.add(Rect::new().size(280.0).fill(Surface).opacity(0.0)); let call = view.add(Callout::new(card).text("the middle")); all((card.fade_in(), call.enter())).await; call.next().await; all((card.fade_out(), call.exit())).await; let lower = view.add(LowerThird::new("Ada").role("walks the half")); lower.enter().await; lower.next().await; lower.exit().await; let end = view.add(EndCard::new("That's the search").note("Repeat from the top")); end.enter().await; end.next().await; end.exit().await; } pub fn project() -> Project { Project::new("components") .aspects([Aspect::Landscape, Aspect::Portrait, Aspect::Square]) .scene(explainer()) } ``` ### cube_3d: A 3D node inside a 2D diagram: a lit cube turns in a `View3d`, and 2D ```rust //! A 3D node inside a 2D diagram: a lit cube turns in a `View3d`, and 2D //! labels follow points on its faces. use oxframe::prelude::*; /// A label and a leader line pinned to a point on the cube. fn pin(world: &Scene3d, cube: &Object3d, frame: View3d, text: &str, local: Vec3, offset: Vec2) { let (w, c, to_view) = (world.clone(), cube.clone(), frame.projector()); let at = move || to_view(w.world_point(&c, local)); let label = frame.add(Txt::new().text(text).caption().opacity(0.0)); let line = frame.add( Line::new() .points([(0.0, 0.0), (0.0, 0.0)]) .stroke(Muted) .line_width(2.0) .opacity(0.0), ); let a = at.clone(); label.x().bind(move || a().x + offset.x); let a = at.clone(); label.y().bind(move || a().y + offset.y); line.points().bind(move || { let p = at(); vec![p + offset * 0.8, p] }); spawn(async move { all((label.fade_in(), line.fade_in())).await; }); } #[oxframe::scene] pub async fn diagram(view: View) { view.add( Txt::new() .text("A 3D node in a 2D diagram") .h1() .to_edge(Edge::Top), ); let world = Scene3d::new(); let cube = world.add(Mesh::cube().color(Accent).roughness(0.35)); let frame = view.add(View3d::new(&world).size(760.0, 520.0).y(40.0)); // The diagram around it is plain 2D. let code = view.add( Rect::new() .width(340.0) .height(90.0) .radius(Lg) .fill(Surface) .x(-740.0) .y(40.0), ); code.add(Txt::new().text("Scene3d").body()); let out = view.add( Rect::new() .width(340.0) .height(90.0) .radius(Lg) .fill(Surface) .x(740.0) .y(40.0), ); out.add(Txt::new().text("MP4 · PNG · check").body()); for (a, b) in [ ((-560.0, 40.0), (-400.0, 40.0)), ((400.0, 40.0), (560.0, 40.0)), ] { view.add( Line::new() .points([a, b]) .end_arrow(true) .stroke(Muted) .line_width(4.0), ); } pin( &world, &cube, frame, "top", Vec3::new(0.0, 0.5, 0.0), Vec2::new(-120.0, -110.0), ); pin( &world, &cube, frame, "front", Vec3::new(0.0, 0.0, 0.5), Vec2::new(260.0, 170.0), ); // One full turn while the camera moves in. all(( cube.rotation().to(Vec3::new(0.0, 360.0, 0.0), 6.0), world.camera().position().to(Vec3::new(2.2, 1.8, 3.0), 6.0), )) .await; cube.color().to(Success, 0.6).await; wait_for(0.5).await; } pub fn project() -> Project { Project::new("cube_3d").scene(diagram()) } ``` ### effects: Paint and compositing: gradients, dashes, group opacity, clipping, blend ```rust //! Paint and compositing: gradients, dashes, group opacity, clipping, blend //! modes, blur, shadows and glow — all animatable. use oxframe::prelude::*; #[oxframe::scene] pub async fn effects(view: View) { // A gradient card with marching-ants border and a soft shadow. let card = view.add( Rect::new() .width(520.0) .height(300.0) .radius(28.0) .position((-400.0, -120.0)) .fill_gradient( Gradient::linear((-260.0, -150.0), (260.0, 150.0)) .stops([color!("#e13238"), color!("#f5c242")]), ) .stroke(Color::WHITE) .line_width(5.0) .dash(vec![18.0, 12.0]) .shadow(Color::BLACK.with_alpha(0.6), 18.0, (0.0, 16.0)), ); spawn(async move { card.dash_offset() .to(-300.0, 6.0) .ease(presets::LINEAR) .await; }); // Clipped content: a stripe sweeping inside a circle. let lens = view.add( Circle::new() .size(300.0) .position((400.0, -120.0)) .fill(color!("#1b1d22")) .clip(true), ); let stripe = lens.add( Rect::new() .width(80.0) .height(400.0) .x(-220.0) .rotation(20.0) .fill(color!("#68abdf")), ); // Glow that breathes, blur that focuses. let orb = view.add( Circle::new() .size(120.0) .position((-300.0, 260.0)) .fill(color!("#68abdf")) .glow(color!("#68abdf"), 4.0), ); let title = view.add( Txt::new() .text("in focus") .font_size(80.0) .weight(800.0) .position((200.0, 260.0)) .blur(14.0), ); all(( stripe.x().to(220.0, 2.0), orb.shadow_blur().to(30.0, 2.0), title.blur().to(0.0, 2.0), )) .await; // Group opacity: the overlap of the two circles never gets darker. let pair = view.add( Group::new() .y(260.0) .x(-40.0) .opacity(1.0) .child(Circle::new().size(140.0).x(-50.0).fill(color!("#2ecc71"))) .child(Circle::new().size(140.0).x(50.0).fill(color!("#2ecc71"))), ); pair.opacity().to(0.4, 1.0).await; // Multiply blend over the card. let mul = view.add( Circle::new() .size(200.0) .position((-400.0, -120.0)) .fill(color!("#68abdf")) .blend(BlendMode::Multiply) .scale(0.0), ); mul.scale() .to(Vec2::new(1.0, 1.0), 0.8) .ease(presets::OUT_BACK) .await; wait_for(1.0).await; } pub fn project() -> Project { Project::new("effects").scene(effects()) } ``` ### gauge: A node of your own: a gauge that draws an arc with `DrawCtx` and ```rust //! A node of your own: a gauge that draws an arc with `DrawCtx` and //! animates its value like any built-in node. use oxframe::prelude::*; /// A dial from 0 to 1. #[derive(Node)] pub struct Gauge { /// 0 is empty, 1 is full. #[signal] value: f64, /// Diameter in pixels. #[signal] size: f64, } impl Draw for Gauge { fn draw(&self, ctx: &mut DrawCtx) { let r = self.size / 2.0 - 12.0; ctx.arc((0.0, 0.0), r, 135.0, 270.0).stroke(Surface, 18.0); ctx.arc((0.0, 0.0), r, 135.0, 270.0 * self.value.clamp(0.0, 1.0)) .stroke(Accent, 18.0); } fn size(&self) -> (f64, f64) { (self.size, self.size) } } #[oxframe::scene] pub async fn dials(view: View) { let row = view.add(Layout::new().gap(80.0)); let gauges: Vec = [0.35, 0.8, 0.55] .into_iter() .map(|_| row.add(Gauge::new().size(260.0))) .collect(); view.add( Txt::new() .text("A node of your own") .h1() .to_edge(Edge::Top), ); stagger(0.2, [0.35, 0.8, 0.55].into_iter().enumerate(), |(i, v)| { gauges[i].value().to(v, 1.0) }) .await; wait_for(0.5).await; } pub fn project() -> Project { Project::new("gauge").scene(dials()) } ``` ### layout: Layout: rows and columns size themselves to content; animating a ```rust //! Layout: rows and columns size themselves to content; animating a //! child's size re-flows its siblings. use oxframe::prelude::*; #[oxframe::scene] pub async fn layout(view: View) { let colors = [ color!("#e13238"), color!("#f5c242"), color!("#68abdf"), color!("#2ecc71"), ]; let row = view.add( Layout::new() .gap(24.0) .padding(24.0) .radius(24.0) .fill(color!("#262626")), ); let mut cards = Vec::new(); for (i, c) in colors.into_iter().enumerate() { let card = row.add( Layout::new() .column() .gap(12.0) .padding(20.0) .radius(16.0) .fill(c) .opacity(0.0) .child( Txt::new() .text(format!("{}", i + 1)) .font_size(64.0) .weight(800.0), ) .child(Txt::new().text("card").font_size(28.0)), ); cards.push(card); } // Staggered fade-in. stagger(0.15, &cards, |c| c.opacity().to(1.0, 0.5)).await; // Growing the gap and one card's padding pushes the others aside. all((row.gap().to(60.0, 0.8), cards[1].padding().to(60.0, 0.8))).await; wait_for(0.4).await; all((row.gap().to(24.0, 0.8), cards[1].padding().to(20.0, 0.8))).await; // Removing a card re-flows the row. cards[2].opacity().to(0.0, 0.4).await; cards[2].remove(); wait_for(0.6).await; } pub fn project() -> Project { Project::new("layout").scene(layout()) } ``` ### linear_map: A linear map on the plane: the basis vectors and the grid follow a ```rust //! A linear map on the plane: the basis vectors and the grid follow a //! shear, then a rotation, while the matrix of the map is shown beside it. use oxframe::prelude::*; #[oxframe::scene] pub async fn shear(view: View) { let axes = view.add( Axes::new() .x_range(-4.0..4.0) .y_range(-3.0..3.0) .width(880.0) .height(660.0) .x(-180.0) .y(30.0), ); let i = axes.vector(1.0, 0.0); i.label("i"); let j = axes.vector(0.0, 1.0).stroke(Success); j.label("j"); let v = axes.vector(2.0, 1.0).stroke(Warning); v.label("v"); let m = view.add( Matrix::new([[1, 0], [0, 1]]) .font_size(56.0) .cell_width(56.0) .x(640.0) .y(30.0), ); wait_for(0.5).await; // Shear: x picks up y. m.cell(0, 1).text().set("1"); m.cell(0, 1).fill().set(Accent); axes.apply_matrix([[1.0, 1.0], [0.0, 1.0]], 1.5).await; wait_for(0.5).await; // Then a quarter turn, after the shear: the product shows in the matrix. let turn = Mat2([[0.0, -1.0], [1.0, 0.0]]); let total = turn.after(&axes.matrix().get()); axes.apply_matrix(turn, 1.5).await; for (r, row) in total.0.iter().enumerate() { for (c, value) in row.iter().enumerate() { m.cell(r, c).text().set(format!("{value}")); } } wait_for(0.5).await; // Back to the identity; the vector's tip is a signal of its own. all((axes.matrix().to(Mat2::IDENTITY, 1.2), v.to(-1.0, 2.0, 1.2))).await; for (r, row) in [[1, 0], [0, 1]].iter().enumerate() { for (c, value) in row.iter().enumerate() { m.cell(r, c).text().set(value.to_string()); m.cell(r, c).fill().set(Text); } } wait_for(0.6).await; } pub fn project() -> Project { Project::new("linear_map").scene(shear()) } ``` ### magnifier: A magnifying glass written in WGSL: the shader reads the frame drawn ```rust //! A magnifying glass written in WGSL: the shader reads the frame drawn //! before it (`Input::frame()`) and bends it inside a moving circle. //! //! A custom WGSL shader runs on the CPU, in `oxframe dev`, `check`, //! `render` and the playground alike. use oxframe::prelude::*; /// `cx`, `cy`: the lens center (0..1 across the frame); `radius` in pixels; /// `zoom`: how much the middle is magnified. const LENS: &str = r#" struct Params { cx: f32, cy: f32, radius: f32, zoom: f32 } @group(1) @binding(0) var p: Params; fn shade(i: ShadeIn) -> vec4 { let res = ox.resolution; let center = vec2(p.cx, p.cy) * res; let d = i.screen - center; let r = length(d) / p.radius; if (r > 1.0) { return sample_scene(i.screen / res); } // A lens bends the middle most and the rim not at all. let at = center + d * mix(1.0 / p.zoom, 1.0, r * r); // Glass splits colors a little toward the rim. let fringe = d * 0.015 * r; let color = vec3( sample_scene((at + fringe) / res).r, sample_scene(at / res).g, sample_scene((at - fringe) / res).b, ); let rim = smoothstep(0.9, 1.0, r); return vec4(mix(color, vec3(1.0), rim * 0.7), 1.0); } "#; #[oxframe::scene] pub async fn magnifier(view: View) { let title = view.add(Txt::new().text("Read the fine print").h1().y(-330.0)); let print = view.add( Txt::new() .text( "oxframe draws every frame on the CPU, so a render is the same \ bytes on every machine. Your own WGSL runs there too, a tile \ of pixels at a time, and can read what was drawn before it.", ) .font_size(32) .max_width(1200.0) .fill(Muted) .y(40.0), ); // Added last, so it reads the title and the text. let lens = view.add( Shader::source(LENS) .uniform("cx", 0.27) .uniform("cy", 0.53) .uniform("radius", 0.0) .uniform("zoom", 2.2) .input("scene", Input::frame()) .fullscreen(), ); all((title.fade_in(), print.fade_in())).await; lens.uniform("radius").to(170.0, 0.5).await; lens.uniform("cx").to(0.73, 3.0).await; all(( lens.uniform("cy").to(0.2, 1.0), lens.uniform("cx").to(0.5, 1.0), )) .await; lens.uniform("zoom").to(1.4, 0.8).await; lens.uniform("radius").to(0.0, 0.4).await; } pub fn project() -> Project { Project::new("magnifier").scene(magnifier()) } ``` ### matrix_multiply: Matrix multiplication step by step: a row of A meets a column of B and ```rust //! Matrix multiplication step by step: a row of A meets a column of B and //! their dot product fills one cell of the result. use oxframe::prelude::*; #[oxframe::scene] pub async fn multiply(view: View) { view.add( Txt::new() .text("Multiplying matrices") .h1() .to_edge(Edge::Top), ); let a_vals = [[1, 2], [3, 4]]; let b_vals = [[5, 6], [7, 8]]; let a = view.add(Matrix::new(a_vals).font_size(64.0).x(-560.0)); view.add(Txt::new().text("×").h1().x(-310.0)); let b = view.add(Matrix::new(b_vals).font_size(64.0).x(-60.0)); view.add(Txt::new().text("=").h1().x(190.0)); let c = view.add( Matrix::new([["?", "?"], ["?", "?"]]) .font_size(64.0) .cell_width(80.0) .x(470.0), ); for cell in (0..2).flat_map(|i| c.row(i)) { cell.fill().set(Muted); } let sum = view.add(Txt::new().text("").h2().y(280.0)); let rows = [a.row_frame(0), a.row_frame(1)]; let cols = [b.column_frame(0), b.column_frame(1)]; wait_for(0.4).await; for i in 0..2 { for j in 0..2 { all(( rows[i].opacity().to(1.0, 0.3), cols[j].opacity().to(1.0, 0.3), )) .await; let terms: Vec = (0..2) .map(|k| format!("{}·{}", a_vals[i][k], b_vals[k][j])) .collect(); let value: i32 = (0..2).map(|k| a_vals[i][k] * b_vals[k][j]).sum(); sum.text().set(format!("{} = {value}", terms.join(" + "))); sum.opacity().set(0.0); sum.fade_in().await; let cell = c.cell(i, j); cell.text().set(value.to_string()); cell.fill().set(Accent); wait_for(0.4).await; all(( rows[i].opacity().to(0.0, 0.3), cols[j].opacity().to(0.0, 0.3), cell.fill().to(Text, 0.3), )) .await; } } sum.fade_out().await; wait_for(0.6).await; } pub fn project() -> Project { Project::new("matrix_multiply").scene(multiply()) } ``` ### morph: Morphing between arbitrary paths and drawing outlines on with `end`. ```rust //! Morphing between arbitrary paths and drawing outlines on with `end`. use oxframe::prelude::*; const STAR: &str = "M0 -150 L44 -46 L150 -46 L64 18 L96 130 L0 64 L-96 130 L-64 18 L-150 -46 L-44 -46 Z"; const HEART: &str = "M0 140 C-60 80 -160 30 -160 -50 C-160 -110 -110 -150 -60 -150 C-30 -150 -10 -130 0 -110 \ C10 -130 30 -150 60 -150 C110 -150 160 -110 160 -50 C160 30 60 80 0 140 Z"; fn circle(r: f64) -> BezPath { use oxframe::core::kurbo::Shape; oxframe::core::kurbo::Circle::new((0.0, 0.0), r).to_path(0.1) } fn svg(d: &str) -> BezPath { BezPath::from_svg(d).expect("valid path") } #[oxframe::scene] pub async fn morph(view: View) { // Draw the outline on, then fill it. let shape = view.add( Path::new() .svg(STAR) .fill(Color::TRANSPARENT) .stroke(color!("#f5c242")) .line_width(8.0) .end(0.0), ); shape.end().to(1.0, 1.2).await; shape.fill().to(color!("#f5c242"), 0.5).await; // Star → circle → heart → star: alignment picks start points and // directions so the shapes do not twist. shape.data().to(circle(140.0), 1.0).await; all(( shape.data().to(svg(HEART), 1.0), shape.fill().to(color!("#e13238"), 1.0), shape.stroke().to(color!("#e13238"), 1.0), )) .await; wait_for(0.3).await; all(( shape.data().to(svg(STAR), 1.0).ease(presets::OUT_BACK), shape.rotation().to(144.0, 1.0), shape.fill().to(color!("#68abdf"), 1.0), shape.stroke().to(color!("#68abdf"), 1.0), )) .await; // Erase from the start. shape.fill().to(Color::TRANSPARENT, 0.4).await; shape.start().to(1.0, 0.8).await; } pub fn project() -> Project { Project::new("morph").scene(morph()) } ``` ### not_found: The oxframe.dev 404 page: two fours, and a magnifying glass looking for the zero. ```rust //! The oxframe.dev 404 page: two fours, and a magnifying glass looking for the zero. use oxframe::prelude::*; #[oxframe::scene] pub async fn not_found(view: View) { let red = color!("#e13238"); let blue = color!("#68abdf"); let four = |x: f64| { view.add( Txt::new() .text("4") .font_size(360.0) .weight(800.0) .position((x, -900.0)), ) }; let fours = [four(-300.0), four(300.0)]; // The glass is a ring and a handle in one group, so they move together. let glass = view.add(Group::new().position((-1300.0, 0.0))); let ring = glass.add( Circle::new() .size(190.0) .fill(blue.with_alpha(0.15)) .stroke(blue) .line_width(28.0), ); let handle = glass.add( Rect::new() .width(32.0) .height(130.0) .radius(16.0) .position((105.0, 105.0)) .rotation(-45.0) .fill(red), ); let caption = view.add( Txt::new() .text("") .font_size(46.0) .fill(Color::WHITE.with_alpha(0.65)) .y(300.0), ); // The fours drop in; the glass comes looking for the missing zero. stagger(0.12, &fours, |f| f.y().to(0.0, 0.7).ease(presets::OUT_BACK)).await; glass.x().to(-650.0, 0.6).await; glass.x().to(650.0, 1.1).await; glass.x().to(0.0, 0.7).ease(presets::OUT_BACK).await; // Found it: the handle goes and the ring stands up as the zero. all(( handle.opacity().to(0.0, 0.3), ring.scale().to(Vec2::new(0.9, 1.3), 0.5).ease(presets::OUT_BACK), ring.fill().to(blue.with_alpha(0.0), 0.5), ring.line_width().to(46.0, 0.5), caption .text() .to("This page is not here.", 0.8) .ease(presets::LINEAR), )) .await; wait_for(1.6).await; // Fade out, so the loop starts clean. all(( fours[0].opacity().to(0.0, 0.4), fours[1].opacity().to(0.0, 0.4), ring.opacity().to(0.0, 0.4), caption.opacity().to(0.0, 0.4), )) .await; } pub fn project() -> Project { Project::new("not_found").scene(not_found()) } ``` ### ocean: The target scene of hybrid rendering: waves under a camera, a splash ```rust //! The target scene of hybrid rendering: waves under a camera, a splash //! toward the lens, a dive into the water, and the sea washing into the //! shore. Every shader here runs on the CPU, so it renders without a GPU. //! //! Drag the `swell` and `splash` markers in the editor to retime the beats. use oxframe::prelude::*; #[oxframe::scene] pub async fn ocean(view: View) { let cam = view.add(Camera::new()); let sea = cam.add( shaders::waves() .amp(0.04) .deep(Accent) .foam(Surface) .width(pct(100)) .height(pct(55)) .to_edge(Edge::Bottom), ); let title = view.add( Txt::new() .text("Into the wave") .display() .to_edge(Edge::Top), ); let spray = view.add( Particles::new() .seed(7) .gravity(1400.0) .fill(Text) // white foam in the dark theme .toward_camera(), ); // Added last, so the drops bend everything above. let lens = view.add(shaders::lens_drops().wetness(0.0).seed(3).fullscreen()); all((title.fade_in(), cam.zoom().to(1.1, 2.0))).await; wait_for(0.4).await; wait_until("swell").await; sea.amp().to(0.14, 1.2).await; wait_until("splash").await; all((spray.burst((0.0, 140.0), 400), lens.wetness().to(1.0, 0.4))).await; cam.center_on(sea, 0.6).await; cam.zoom().to(4.0, 0.8).await; // into the wave } #[oxframe::scene] pub async fn shore(view: View) { // Added before the transition, so the rising water uncovers calm sea. view.add( shaders::waves() .amp(0.01) .speed(0.3) .deep(Accent) .foam(Surface) .width(pct(100)) .height(pct(12)) .to_edge(Edge::Bottom), ); transition(Transition::shader(shaders::water_wipe())).await; view.add(EndCard::new("").made_by()).enter().await; } pub fn project() -> Project { Project::new("ocean") .size(960, 540) .fps(30) .scene(ocean()) .scene(shore()) } ``` ### particles: Seeded randomness and background tasks: the same seed always gives the ```rust //! Seeded randomness and background tasks: the same seed always gives the //! same video. use oxframe::prelude::*; #[oxframe::scene] pub async fn particles(view: View) { let (w, h) = view.size(); let (w, h) = (f64::from(w), f64::from(h)); let palette = [ color!("#e13238"), color!("#f5c242"), color!("#68abdf"), color!("#2ecc71"), ]; // A background task keeps emitting while the main script continues. let emitter = spawn(async move { for i in 0..120 { let x = random_range(-w / 2.0, w / 2.0); let size = random_range(10.0, 40.0); let c = palette[(random() * palette.len() as f64) as usize % palette.len()]; let p = view.add( Circle::new() .size(size) .fill(c) .position((x, h / 2.0 + 40.0)), ); let rise = random_range(1.5, 3.0); spawn(async move { all(( p.y().to(-h / 2.0 - 40.0, rise).ease(presets::LINEAR), p.opacity().to(0.0, rise), )) .await; p.remove(); }); if i % 3 == 0 { wait_for(0.05).await; } } }); let title = view.add( Txt::new() .text("seed = 7") .font_size(120.0) .weight(800.0) .scale(0.0), ); title .scale() .to(Vec2::new(1.0, 1.0), 0.8) .ease(presets::OUT_BACK) .await; emitter.await; wait_for(3.0).await; } pub fn project() -> Project { Project::new("particles").seed(7).scene(particles()) } ``` ### quicksort: Quicksort, step by step: the code with a moving focus, synced to bars that compare, swap and settle. ```rust //! Quicksort, step by step: the code with a moving focus, synced to bars that compare, swap and settle. //! //! A tour of most of oxframe in one scene: highlighted `Code` with focus, //! hundreds of coordinated tweens (`all`, `stagger`), seeded random data, //! `tune!` values you can drag in the inspector (bar count, speed), named //! eases, markers, glow, gradients and text morphing. //! //! Try: change the seed in `project()`, or the `bars` and `speed` tunables. use oxframe::prelude::*; const CODE: &str = "fn quicksort(a: &mut [u32], lo: i32, hi: i32) { if lo >= hi { return; } let p = partition(a, lo, hi); quicksort(a, lo, p - 1); quicksort(a, p + 1, hi); } fn partition(a: &mut [u32], lo: i32, hi: i32) -> i32 { let pivot = a[hi]; let mut i = lo; for j in lo..hi { if a[j] < pivot { a.swap(i, j); i += 1; } } a.swap(i, hi); i }"; /// One step of the algorithm, recorded up front and then animated. #[derive(Clone, Copy)] enum Step { Range { lo: usize, hi: usize }, Pivot { at: usize }, Compare { j: usize, i: usize, less: bool }, Swap { a: usize, b: usize, line: usize }, Placed { at: usize }, } fn trace(values: &[u32]) -> Vec { fn sort(a: &mut Vec, lo: i64, hi: i64, out: &mut Vec) { if lo > hi { return; } let (l, h) = (lo as usize, hi as usize); out.push(Step::Range { lo: l, hi: h }); if lo == hi { out.push(Step::Placed { at: l }); return; } out.push(Step::Pivot { at: h }); let pivot = a[h]; let mut i = l; for j in l..h { let less = a[j] < pivot; out.push(Step::Compare { j, i, less }); if less { if i != j { a.swap(i, j); out.push(Step::Swap { a: i, b: j, line: 13, }); } i += 1; } } if i != h { a.swap(i, h); out.push(Step::Swap { a: i, b: h, line: 17, }); } out.push(Step::Placed { at: i }); sort(a, lo, i as i64 - 1, out); sort(a, i as i64 + 1, hi, out); } let mut a = values.to_vec(); let mut out = Vec::new(); let last = a.len() as i64 - 1; sort(&mut a, 0, last, &mut out); out } const BASE: Color = Color::rgb(0.40, 0.67, 0.87); const COMPARE: Color = Color::rgb(0.96, 0.55, 0.20); const PIVOT: Color = Color::rgb(0.96, 0.76, 0.26); const DONE: Color = Color::rgb(0.18, 0.80, 0.44); #[derive(Clone, Copy)] struct Bar { group: Group, rect: Rect, } #[oxframe::scene] pub async fn quicksort(view: View) { let n = tune!("bars", 14.0).clamp(4.0, 28.0) as usize; let speed = tune!("speed", 1.0).max(0.1); let beat = 0.32 / speed; let swap_ease = ease!("swap", presets::IN_OUT_CUBIC); // --- title ----------------------------------------------------------- let title = view.add( Txt::new() .text("oxframe") .font_size(120.0) .weight(800.0) .y(-60.0), ); wait_for(0.4).await; title.morph_to("quicksort", 0.9).await; all((title.font_size().to(56.0, 0.8), title.y().to(-470.0, 0.8))).await; let subtitle = view.add( Txt::new() .y(-405.0) .font_size(28.0) .rich([ span("partition around a "), span("pivot").bold().color(PIVOT), span(", then sort both "), span("halves").italic().color(BASE), ]) .end(0.0), ); subtitle.end().to(1.0, 0.6).ease(presets::LINEAR).await; // --- code panel -------------------------------------------------------- let panel = view.add( Rect::new() .width(900.0) .height(680.0) .radius(24.0) .position((-470.0, 60.0)) .fill(color!("#1b1d23")) .shadow(Color::BLACK.with_alpha(0.55), 24.0, (0.0, 18.0)) .opacity(0.0) .scale(0.96), ); let code = view.add( Code::new() .code(CODE) .font_size(23.0) .position((-470.0, 60.0)) .focus(1, 6) .opacity(0.0), ); all(( panel.opacity().to(1.0, 0.5), panel.scale().to(Vec2::new(1.0, 1.0), 0.5), code.opacity().to(1.0, 0.6), )) .await; // --- bars --------------------------------------------------------------- let values: Vec = (0..n).map(|_| random_range(8.0, 100.0) as u32).collect(); let (left, width, baseline) = (80.0, 800.0, 300.0); let step = width / n as f64; let slot = |i: usize| left + step * (i as f64 + 0.5); let bar_w = (step * 0.72).min(64.0); // Active range behind the bars. let range = view.add( Rect::new() .height(560.0) .width(width) .radius(14.0) .position((left + width / 2.0, baseline - 255.0)) .fill(Color::WHITE.with_alpha(0.05)) .opacity(0.0), ); let mut bars = Vec::with_capacity(n); for (i, v) in values.iter().enumerate() { let group = view.add(Group::new().position((slot(i), baseline))); let rect = group.add( Rect::new() .width(bar_w) .height(0.0) .radius(6.0) .anchor((0.0, 1.0)) .fill(BASE) .fill_gradient( Gradient::linear((0.0, -260.0), (0.0, 260.0)) .stops([BASE, BASE.with_alpha(0.55)]), ), ); group.add( Txt::new() .text(v.to_string()) .family(MONO) .font_size((bar_w * 0.45).clamp(12.0, 22.0)) .fill(Color::WHITE.with_alpha(0.75)) .y(26.0), ); bars.push((Bar { group, rect }, *v)); } // Bars grow in, left to right. stagger(0.04, &bars, |(b, v)| { b.rect .height() .to(f64::from(*v) * 5.0, 0.6) .ease(presets::OUT_BACK) }) .await; // Pointers `i` and `j` under the bars. let pointer = |label: &str, c: Color, row: f64| { let g = view.add( Group::new() .position((slot(0), baseline + 64.0 + row)) .opacity(0.0), ); g.add(Polygon::new().sides(3.0).size(22.0).fill(c)); g.add( Txt::new() .text(label) .family(MONO) .font_size(22.0) .fill(c) .y(28.0), ); g }; let pi = pointer("i", PIVOT, 0.0); let pj = pointer("j", COMPARE, 56.0); let counter = view.add( Txt::new() .family(MONO) .font_size(24.0) .text("comparisons 0 swaps 0") .fill(Color::WHITE.with_alpha(0.7)) .position((left + width / 2.0, baseline + 190.0)), ); wait_until("sort").await; // --- the algorithm -------------------------------------------------------- let (mut comparisons, mut swaps) = (0, 0); let color = |b: Bar, c: Color, d: f64| { all(( b.rect.fill().to(c, d), b.rect.fill_gradient().to( Some(Gradient::linear((0.0, -260.0), (0.0, 260.0)).stops([c, c.with_alpha(0.55)])), d, ), )) }; let mut placed = vec![false; n]; for s in trace(&values) { match s { Step::Range { lo, hi } => { let (x0, x1) = (slot(lo) - step / 2.0, slot(hi) + step / 2.0); all(( code.focus_lines(1, 3, beat * 0.6), range.opacity().to(1.0, beat), range.x().to((x0 + x1) / 2.0, beat), range.width().to(x1 - x0, beat), pi.x().to(slot(lo), beat), pj.x().to(slot(lo), beat), pi.opacity().to(if lo < hi { 1.0 } else { 0.0 }, beat), pj.opacity().to(if lo < hi { 1.0 } else { 0.0 }, beat), )) .await; } Step::Pivot { at } => { let b = bars[at].0; all(( code.focus_lines(8, 10, beat * 0.6), color(b, PIVOT, beat), b.rect.shadow_color().to(PIVOT.with_alpha(0.8), beat), b.rect.shadow_blur().to(14.0, beat), )) .await; } Step::Compare { j, i, less } => { comparisons += 1; counter .text() .set(format!("comparisons {comparisons} swaps {swaps}")); let b = bars[j].0; all(( code.focus_lines(11, 12, beat * 0.4), pj.x().to(slot(j), beat * 0.5), pi.x().to(slot(i), beat * 0.5), color(b, COMPARE, beat * 0.5), )) .await; // Smaller than the pivot: it will move left of `i`. b.group .y() .to(baseline - if less { 18.0 } else { 0.0 }, beat * 0.3) .await; all(( color(b, BASE, beat * 0.4), b.group.y().to(baseline, beat * 0.4), )) .await; } Step::Swap { a, b, line } => { swaps += 1; counter .text() .set(format!("comparisons {comparisons} swaps {swaps}")); let (ba, bb) = (bars[a].0, bars[b].0); let lift = 40.0; all(( code.focus_lines(line, line, beat * 0.4), ba.group.x().to(slot(b), beat * 1.4).ease(swap_ease), bb.group.x().to(slot(a), beat * 1.4).ease(swap_ease), async move { all(( ba.group.y().to(baseline - lift, beat * 0.7), bb.group.y().to(baseline + lift * 0.5, beat * 0.7), )) .await; all(( ba.group.y().to(baseline, beat * 0.7), bb.group.y().to(baseline, beat * 0.7), )) .await; }, )) .await; bars.swap(a, b); } Step::Placed { at } => { placed[at] = true; let b = bars[at].0; all(( code.focus_lines(17, 18, beat * 0.4), color(b, DONE, beat), b.rect.shadow_color().to(Color::TRANSPARENT, beat), b.rect.scale().to(Vec2::new(1.08, 1.08), beat * 0.5), )) .await; b.rect.scale().to(Vec2::new(1.0, 1.0), beat * 0.5).await; } } } // --- finale --------------------------------------------------------------- wait_until("finale").await; all(( range.opacity().to(0.0, 0.5), pi.opacity().to(0.0, 0.5), pj.opacity().to(0.0, 0.5), code.focus_lines(0, 0, 0.5), )) .await; stagger(0.05, &bars, |(b, _)| { let r = b.rect; async move { all(( r.shadow_color().to(DONE.with_alpha(0.9), 0.3), r.shadow_blur().to(18.0, 0.3), )) .await; all(( r.shadow_color().to(Color::TRANSPARENT, 0.5), r.shadow_blur().to(0.0, 0.5), )) .await; } }) .await; title.morph_to("sorted", 0.8).await; wait_for(1.2).await; } pub fn project() -> Project { Project::new("quicksort").seed(42).scene(quicksort()) } ``` ### relative: A title, a card and a caption with no pixel coordinates. ```rust //! A title, a card and a caption with no pixel coordinates. //! `Project::aspects` is what makes the same scene 16:9, 9:16 and 1:1. use oxframe::prelude::*; #[oxframe::scene] pub async fn relative(view: View) { let title = view.add( Txt::new() .text("Binary search") .h1() .to_edge(Edge::Top) .name("title"), ); let card = view.add( Rect::new() .width(pct(80)) .height(280.0) .radius(Lg) .fill(Surface) .name("card"), ); view.add( Txt::new() .text("mid") .caption() .fill(Muted) .next_to(card, Side::Bottom) .name("label"), ); title.fade_in().await; // The label is tied to the card, so it moves with it. card.y().to(-40.0, 1.0).await; } pub fn project() -> Project { Project::new("relative") .aspects([Aspect::Landscape, Aspect::Portrait, Aspect::Square]) .scene(relative()) } ``` ### release: A release's notes as a video: the version, then each section with its ```rust //! A release's notes as a video: the version, then each section with its //! changes appearing word by word, a halo over each one, and an end card. //! //! release-video-action generates scenes like these from a GitHub release. use oxframe::nodes::Ink; use oxframe::prelude::*; /// Where each word of `text` ends, as a fraction of its drawn glyphs /// (`end` counts glyphs, and spaces draw none). fn word_ends(text: &str) -> Vec { let total = text.chars().filter(|c| !c.is_whitespace()).count().max(1) as f64; let mut seen = 0; text.split_whitespace() .map(|word| { seen += word.chars().count(); seen as f64 / total }) .collect() } /// The text color at rest: a step towards the background, so a halo can /// brighten it. fn rest() -> Color { let theme = oxframe::nodes::current_theme(); theme.ink(Text).mix(theme.ink(Background), 0.18) } /// A short halo over `line`: its glow and brightness go up, then back. async fn halo(line: Txt, glow: Color) { let peak = oxframe::nodes::current_theme().ink(Text); all(( line.shadow_blur().to(16.0, 0.16), line.shadow_color().to(glow, 0.16), line.fill().to(peak, 0.16), )) .await; all(( line.shadow_blur().to(0.0, 0.34), line.shadow_color().to(glow.with_alpha(0.0), 0.34), line.fill().to(rest(), 0.34), )) .await; } /// Writes `line` on word by word, then runs the halo over it. async fn write_on(line: Txt, text: &str, glow: Color) { for end in word_ends(text) { line.end().to(end, 0.06).ease(presets::OUT_CUBIC).await; } halo(line, glow).await; } /// A calm sea along the bottom edge; it keeps moving while the text holds. fn sea(view: View) { let h = f64::from(view.size().1); view.add( shaders::waves() .amp(0.06) .speed(0.5) .deep(Accent) .foam(Background) .width(pct(100)) .height(h * 0.11) .y(h / 2.0 - h * 0.055) .opacity(0.35), ); } /// One section: its heading in its color, then the changes one by one, /// earlier ones stepping back. async fn section(view: View, heading: &str, tone: Ink, items: &[&str]) { let (w, h) = view.size(); let (w, h) = (f64::from(w), f64::from(h)); let width = w - 280.0; let glow = oxframe::nodes::current_theme().ink(tone); // Added before the transition, so the heading comes in with the scene. let title = view.add( Txt::new() .text(heading) .font_size(72.0) .weight(800.0) .fill(tone) .to_edge(Edge::Left) .y(-h * 0.34), ); transition(Transition::slide(Dir::Left)).await; let list = view.add( Layout::new() .column() .gap(44.0) .align(LayoutAlign::Start) .width(width) .to_edge(Edge::Left), ); let mut rows = Vec::new(); for &text in items { let row = list.add( Layout::new() .row() .gap(30.0) .align(LayoutAlign::Start) .justify(Justify::Start) .width(width), ); let mark = row.add( Rect::new() .size(20.0) .radius(5.0) .fill(tone) .margin(20.0) .scale(0.0), ); let line = row.add( Txt::new() .text(text) .font_size(56.0) .weight(600.0) .line_height(1.25) .max_width(width - 110.0) .fill(rest()) .end(0.0) .glow(glow.with_alpha(0.0), 0.0), ); rows.push((row, mark, line, text)); } // The list hangs under the heading, so a long one grows downwards. let top = title.y().get() + title.size().1 / 2.0 + 56.0; list.y().set(top + list.size().1 / 2.0); for (i, &(_, mark, line, text)) in rows.iter().enumerate() { if i > 0 { rows[i - 1].0.opacity().to(0.45, 0.3).await; } mark.scale() .to(Vec2::new(1.0, 1.0), 0.25) .ease(presets::OUT_BACK) .await; write_on(line, text, glow).await; // Time to read the rest of it. wait_for((text.chars().count() as f64 / 30.0).clamp(1.0, 3.0)).await; } } #[oxframe::scene] pub async fn version(view: View) { sea(view); let repo = view.add( Txt::new() .text("oxygenframe/oxframe-sdk") .h2() .fill(Muted) .y(-110.0) .opacity(0.0), ); let version = view.add( Txt::new() .text("v0.20.0") .font_size(150.0) .weight(800.0) .y(20.0) .fill(rest()) .opacity(0.0), ); all((repo.fade_in(), version.slide_in(Side::Bottom))).await; halo(version, oxframe::nodes::current_theme().ink(Accent)).await; wait_for(1.2).await; } #[oxframe::scene] pub async fn added(view: View) { sea(view); section( view, "Added", Success, &[ "render --jobs renders a video in 2-second segments, several at once.", "The file does not depend on the number of jobs: --jobs 1 and --jobs 8 write the same bytes.", ], ) .await; } #[oxframe::scene] pub async fn changed(view: View) { sea(view); section( view, "Changed", Warning, &["A video longer than two seconds has a key frame every two seconds, where the segments meet."], ) .await; } #[oxframe::scene] pub async fn credits(view: View) { sea(view); transition(Transition::fade()).await; let version = view.add( Txt::new() .text("v0.20.0") .font_size(110.0) .weight(800.0) .y(-30.0) .fill(rest()) .opacity(0.0), ); let credit = view.add(MadeBy::new().font_size(40.0).y(80.0)); version.fade_in().await; halo(version, oxframe::nodes::current_theme().ink(Accent)).await; credit.enter().await; wait_for(1.2).await; } pub fn project() -> Project { Project::new("release") .scene(version()) .scene(added()) .scene(changed()) .scene(credits()) } ``` ### shapes: Shapes and layout: polygons, stars, arcs, smooth curves and a flexbox ```rust //! Shapes and layout: polygons, stars, arcs, smooth curves and a flexbox //! row whose items grow and wrap. use oxframe::prelude::*; #[oxframe::scene] pub async fn shapes(view: View) { let poly = view.add( Polygon::new() .sides(3.0) .size(220.0) .position((-600.0, -250.0)) .fill(color!("#68abdf")), ); let star = view.add( Star::new() .points(5.0) .size(240.0) .position((-250.0, -250.0)) .fill(color!("#f5c242")), ); let ring = view.add( Circle::new() .size(220.0) .position((100.0, -250.0)) .end_angle(0.0) .fill(Color::TRANSPARENT) .stroke(color!("#e13238")) .line_width(18.0) .line_cap(LineCap::Round), ); let pie = view.add( Circle::new() .size(220.0) .position((450.0, -250.0)) .pie(true) .end_angle(0.0) .fill(color!("#2ecc71")), ); let wave = view.add( Line::new() .points([ (-700.0, 60.0), (-450.0, -40.0), (-200.0, 120.0), (50.0, 0.0), (300.0, 110.0), (600.0, 20.0), ]) .smooth(true) .stroke(color!("#68abdf")) .line_width(8.0) .end(0.0), ); all(( poly.sides().to(8.0, 2.0), star.points().to(9.0, 2.0), star.inner().to(0.7, 2.0), ring.end_angle().to(300.0, 2.0), pie.end_angle().to(290.0, 2.0), wave.end().to(1.0, 2.0), )) .await; // Flexbox: the middle card grows, the row wraps when it gets narrow. let row = view.add( Layout::new() .width(1200.0) .gap(20.0) .padding(20.0) .radius(24.0) .fill(color!("#26282e")) .wrap(true) .y(300.0), ); let colors = ["#e13238", "#f5c242", "#68abdf", "#2ecc71", "#9b59b6"]; let mut cards = Vec::new(); for (i, c) in colors.iter().enumerate() { let card = row.add( Rect::new() .width(160.0) .height(100.0) .radius(16.0) .fill(Color::from_hex(c).unwrap()), ); if i == 1 { card.grow().set(1.0); } cards.push(card); } wait_for(0.6).await; row.width().to(Some(600.0), 1.5).await; wait_for(0.6).await; row.width().to(Some(1200.0), 1.5).await; wait_for(0.6).await; } pub fn project() -> Project { Project::new("shapes").scene(shapes()) } ``` ### showcase: Several scenes in one project, with markers, named eases and tunables ```rust //! Several scenes in one project, with markers, named eases and tunables //! that the editor can adjust without recompiling. use oxframe::prelude::*; #[oxframe::scene] pub async fn title(view: View) { let t = view.add( Txt::new() .text("oxframe") .font_size(180.0) .weight(900.0) .opacity(0.0) .y(40.0), ); let sub = view.add( Txt::new() .text("procedural animation in Rust") .font_size(48.0) .y(140.0) .opacity(0.0), ); all(( t.opacity().to(1.0, 0.8), t.y() .to(-20.0, 0.8) .ease(ease!("title_in", presets::OUT_CUBIC)), )) .await; wait_until("subtitle").await; sub.opacity().to(1.0, 0.6).await; wait_for(tune!("title_hold", 1.0)).await; all((t.opacity().to(0.0, 0.5), sub.opacity().to(0.0, 0.5))).await; } #[oxframe::scene] pub async fn shapes(view: View) { let n = tune!("count", 7.0) as usize; let spacing = 200.0; let start = -(n as f64 - 1.0) * spacing / 2.0; let items: Vec<_> = (0..n) .map(|i| { let hue = i as f32 / n as f32; view.add( Rect::new() .size(120.0) .x(start + i as f64 * spacing) .fill(Color::rgb(0.3 + 0.7 * hue, 0.4, 1.0 - 0.6 * hue)) .scale(0.0), ) }) .collect(); let pop = ease!("pop", presets::OUT_BACK); stagger(0.08, &items, |r| { r.scale().to(Vec2::new(1.0, 1.0), 0.5).ease(pop) }) .await; wait_until("spin").await; stagger(0.05, &items, |r| { all((r.rotation().to(180.0, 0.8), r.radius().to(60.0, 0.8))) }) .await; wait_for(0.5).await; } pub fn project() -> Project { Project::new("showcase").scene(title()).scene(shapes()) } ``` ### slides: A talk: `oxframe present` stops at every `slide_break` and at the end ```rust //! A talk: `oxframe present` stops at every `slide_break` and at the end //! of each scene, and shows the notes in the presenter window. Rendered, //! the same scenes play straight through as a video. use oxframe::prelude::*; #[oxframe::scene] pub async fn opening(view: View) { notes("Introduce yourself. One line on why video from code."); let title = view.add(TitleCard::new("Video as code").subtitle("A talk in three stops")); title.enter().await; title.next().await; // The video plays through the stops, so hold before each one: in the // presentation the hold is a still frame before it stops anyway. wait_for(1.5).await; slide_break().await; notes("Each point appears on the next click."); title.exit().await; let points = view.add(BulletList::new([ "Scenes are async Rust", "Every value is a signal", "The agent checks its own frames", ])); points.enter().await; // The last point ends the scene, so its stop is the scene's end; the // fade into the next scene takes the list away. for _ in 0..3 { points.next().await; wait_for(1.5).await; slide_break().await; } } #[oxframe::scene] pub async fn closing(view: View) { transition(Transition::fade()).await; notes("Point to the docs and take questions."); let end = view.add(EndCard::new("Questions?").note("oxframe.dev")); end.enter().await; end.next().await; wait_for(0.5).await; } pub fn project() -> Project { Project::new("slides").scene(opening()).scene(closing()) } ``` ### sounds: A beat that plays with the picture, and a tone that ducks under it. ```rust //! A beat that plays with the picture, and a tone that ducks under it. use oxframe::prelude::*; fn asset(name: &str) -> String { format!("{}/assets/{name}", env!("CARGO_MANIFEST_DIR")) } #[oxframe::scene] pub async fn sounds(view: View) { let dot = view.add(Circle::new().size(16.0).fill(color!("#e13238"))); sound(asset("click.wav")).volume(0.8).trim(0.0, 0.18).play(); dot.size().to(140.0, 0.18).await; dot.size().to(28.0, 0.35).await; sound(asset("click.wav")) .volume(0.5) .fade_out(0.04) .trim(0.0, 0.18) .priority() .play(); dot.size().to(140.0, 0.18).await; wait_for(0.4).await; } pub fn project() -> Project { Project::new("sounds") .size(960, 540) .fps(60) .audio( Audio::track(asset("tone.wav")) .duck_under_voice() .volume(0.4) .master_fade(0.05, 0.15), ) .scene(sounds()) } ``` ### splash: A drop rings the water, a splash wets the lens, and the water rises into ```rust //! A drop rings the water, a splash wets the lens, and the water rises into //! the next scene. All three shaders run on the CPU, so the example renders //! without a GPU. use oxframe::prelude::*; #[oxframe::scene] pub async fn sea(view: View) { let water = view.add( shaders::waves() .deep(Accent) .foam(Surface) .width(pct(100)) .height(pct(45)) .to_edge(Edge::Bottom), ); let title = view.add( Txt::new() .text("Into the wave") .display() .to_edge(Edge::Top), ); let lens = view.add(shaders::lens_drops().wetness(0.0).seed(7).fullscreen()); title.fade_in().await; let rings = water.effect(shaders::ripple().strength(0.0)); rings.strength().to(1.0, 0.4).await; all((water.amp().to(0.14, 1.0), rings.strength().to(0.0, 1.0))).await; lens.wetness().to(1.0, 0.4).await; wait_for(0.6).await; } #[oxframe::scene] pub async fn shore(view: View) { // Added before the transition, so the rising water uncovers it. view.add( shaders::waves() .amp(0.02) .speed(0.3) .deep(Accent) .foam(Surface) .width(pct(100)) .height(pct(20)) .to_edge(Edge::Bottom), ); transition(Transition::shader(shaders::water_wipe()).duration(1.0)).await; let end = view.add(EndCard::new("").made_by()); end.enter().await; } pub fn project() -> Project { Project::new("splash") .size(960, 540) .fps(30) .scene(sea()) .scene(shore()) } ``` ### spray: Rain and a spray that come out the same wherever you seek. ```rust //! Rain and a spray that come out the same wherever you seek. use oxframe::prelude::*; #[oxframe::scene] pub async fn spray(view: View) { let _rain = view.add( Custom::new(|ctx, t| { let mut rng = ctx.rng(42); let drops = (48.0 * ctx.quality()).round() as i32; for _ in 0..drops { let x = rng.range(-960.0..960.0); let y = (rng.range(0.0..1080.0) + t * 420.0) % 1080.0 - 540.0; ctx.line((x, y), (x, y + 16.0)).stroke(Muted, 2.0); } }) .fullscreen() .name("rain"), ); let spray = view.add( Particles::new() .seed(7) .gravity(900.0) .fill(Surface) .toward_camera() .name("spray"), ); spray.burst((0.0, 40.0), 48).await; wait_for(1.2).await; } pub fn project() -> Project { Project::new("spray").seed(7).scene(spray()) } ``` ### svg: Draw a logo on, one path at a time, then recolor one part. ```rust //! Draw a logo on, one path at a time, then recolor one part. use oxframe::prelude::*; const LOGO: &[u8] = br##" "##; #[oxframe::scene] pub async fn logo(view: View) { let logo = view.add(Svg::new().src(LOGO).width(640.0)); stagger(0.15, logo.paths(), |p| p.draw_on()).await; // The id comes from the file, so one part can be recolored on its own. logo.find("dot").fill().to(color!("#e13238"), 0.6).await; wait_for(0.4).await; } pub fn project() -> Project { Project::new("svg").scene(logo()) } ``` ### table_sort: A table that changes on screen: a row slides in, the rows sort by a ```rust //! A table that changes on screen: a row slides in, the rows sort by a //! column, and the slowest one leaves. use oxframe::prelude::*; #[oxframe::scene] pub async fn ranking(view: View) { view.add( Txt::new() .text("Frame time at 1080p") .h1() .to_edge(Edge::Top), ); let table = view.add( Table::new(["Renderer", "Backend", "Frame"]) .row(["skia", "CPU", "12 ms"]) .row(["vello", "GPU", "4 ms"]) .row(["cairo", "CPU", "18 ms"]) .row(["tiny-skia", "CPU", "9 ms"]) .font_size(44.0) .row_height(92.0) .widths([300.0, 220.0, 200.0]) .y(40.0), ); wait_for(0.6).await; table.insert_row(2, ["wgpu", "GPU", "3 ms"]).await; wait_for(0.4).await; table.sort_by_column(2, 0.8).await; let fastest = table.row(0); fastest.background().fade_in().await; wait_for(0.4).await; let last = table.len() - 1; table.remove_row(last).await; wait_for(0.8).await; } pub fn project() -> Project { Project::new("table_sort").scene(ranking()) } ``` ### text: Text: typewriter effect, animated weight and size, glyph-by-glyph reveal. ```rust //! Text: typewriter effect, animated weight and size, glyph-by-glyph reveal. use oxframe::prelude::*; #[oxframe::scene] pub async fn text(view: View) { let title = view.add(Txt::new().text("").font_size(96.0).weight(300.0).y(-120.0)); // Strings tween like a typewriter. title .text() .to("Hello, oxframe", 1.2) .ease(presets::LINEAR) .await; all(( title.weight().to(800.0, 0.8), title.font_size().to(120.0, 0.8), )) .await; // Retype: deletes back to the common prefix, then types the rest. title .text() .to("Hello, Rust", 1.0) .ease(presets::LINEAR) .await; let code = view.add( Txt::new() .text("async fn intro(view: View) { .. }") .family(MONO) .font_size(44.0) .fill(color!("#68abdf")) .y(60.0) .end(0.0), ); // `end` reveals glyph by glyph. code.end().to(1.0, 1.5).ease(presets::LINEAR).await; let note = view.add( Txt::new() .text("Кириллица тоже работает — встроенный Inter") .font_size(40.0) .fill(color!("#f5c242")) .y(180.0) .opacity(0.0), ); note.opacity().to(1.0, 0.6).await; wait_for(1.0).await; } pub fn project() -> Project { Project::new("text").scene(text()) } ``` ### theme: A card, a title and a caption with no hardcoded colors or sizes. ```rust //! A card, a title and a caption with no hardcoded colors or sizes. //! `Project::theme` is the one line that changes the whole picture. use oxframe::prelude::*; #[oxframe::scene] pub async fn theme_scene(view: View) { let card = view.add( Rect::new() .size(720.0) .radius(Lg) .fill(Surface) .elevation(Md) .name("card"), ); let title = card.add( Txt::new() .text("Binary search") .display() .y(-48.0) .name("title"), ); card.add( Txt::new() .text("O(log n)") .caption() .fill(Muted) .y(72.0) .name("note"), ); title.fade_in().await; } pub fn project() -> Project { Project::new("theme") .theme(Theme::dark().accent(color!("#68abdf"))) .scene(theme_scene()) } ``` ### transitions: Slide from a title into an end card. ```rust //! Slide from a title into an end card. use oxframe::prelude::*; #[oxframe::scene] pub async fn intro(view: View) { let title = view.add(TitleCard::new("Scene one")); title.enter().await; wait_for(0.4).await; } #[oxframe::scene] pub async fn outro(view: View) { transition(Transition::slide(Dir::Left)).await; let end = view.add(EndCard::new("Thanks for watching")); end.enter().await; } pub fn project() -> Project { Project::new("transitions").scene(intro()).scene(outro()) } ``` ### typography: Text beyond plain strings: glyph morphing between words, rich spans and ```rust //! Text beyond plain strings: glyph morphing between words, rich spans and //! code with syntax highlighting and a moving focus. use oxframe::prelude::*; #[oxframe::scene] pub async fn typography(view: View) { // Letters shared by both words travel; the rest fade. let word = view.add( Txt::new() .text("oxframe") .font_size(140.0) .weight(800.0) .y(-300.0), ); wait_for(0.4).await; word.morph_to("frame", 1.0).await; word.morph_to("flame", 1.0).await; let line = view.add( Txt::new() .y(-150.0) .font_size(44.0) .rich([ span("Signals are "), span("lazy").bold().color(color!("#f5c242")), span(", tweens are "), span("futures").italic().color(color!("#68abdf")), ]) .end(0.0), ); line.end().to(1.0, 1.0).ease(presets::LINEAR).await; let code = view.add( Code::new() .y(150.0) .font_size(30.0) .focus(1, 1) .code( "use oxframe::prelude::*;\n\ \n\ #[oxframe::scene]\n\ async fn intro(view: View) {\n\ \x20 let r = view.add(Rect::new().size(200.0));\n\ \x20 r.x().to(300.0, 1.0).await; // 1 s\n\ }", ) .opacity(0.0), ); code.opacity().to(1.0, 0.5).await; // Walk the focus through the code. for (a, b) in [(3, 4), (5, 5), (6, 6), (0, 0)] { wait_for(0.6).await; code.focus_lines(a, b, 0.4).await; } wait_for(0.8).await; } pub fn project() -> Project { Project::new("typography").scene(typography()) } ``` ### voice: A spoken line. The circle grows when the second word starts. ```rust //! A spoken line. The circle grows when the second word starts. use oxframe::prelude::*; #[oxframe::scene] pub async fn spoken(view: View) { let dot = view.add(Circle::new().size(24.0).fill(color!("#e13238"))); let line = voice("one two three").start(); line.at_word("two").await; dot.size().to(160.0, 0.3).await; line.await; } pub fn project() -> Project { Project::new("voice").size(960, 540).fps(60).scene(spoken()) } ``` ### waves: A wave that grows. The crest is a shader preset, so it renders without a GPU. ```rust //! A wave that grows. The crest is a shader preset, so it renders without a GPU. use oxframe::prelude::*; #[oxframe::scene] pub async fn sea(view: View) { let water = view.add( shaders::waves() .amp(0.05) .deep(Accent) .foam(Surface) .width(pct(100)) .height(pct(40)) .to_edge(Edge::Bottom), ); view.add(Txt::new().text("Waves").display().to_edge(Edge::Top)); water.amp().to(0.12, 1.2).await; } pub fn project() -> Project { Project::new("waves").size(960, 540).fps(30).scene(sea()) } ```