Early days: oxframe is under very active development. The API is not stable yet and may change from version to version. Pin the exact version in your Cargo.toml and check the changelog before upgrading.

Camera

A camera zooms, pans and turns the view of everything added to it. Show a whole diagram, move in on one detail, follow something as it travels — while titles stay put.

Loading live preview…

Source of camera — edit in the playground
//! 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<Rect> = 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())
}

World and screen

Add a Camera to the view and build the world inside it. Everything you add to the camera is seen through it; everything you add to the view itself stays on screen, which is where titles, captions and labels belong.

let cam = view.add(Camera::new());
let diagram = cam.add(Group::new());      // the world: seen through the camera
diagram.add(Rect::new().width(360.0).height(160.0));
view.add(Txt::new().text("Title").h2().to_edge(Edge::Top)); // on screen

Moving the camera

A camera has four signals, animated like any other property: x and y are the point it looks at, zoom is how close it is (2 = twice as big), and rotation turns the view (in degrees, clockwise).

cam.fit(diagram, 120.0, 0.0).await;   // all of it in the frame, 120 px to spare
cam.center_on(api, 1.0).await;         // bring one node to the middle
cam.zoom().to(2.5, 1.0).await;         // move in
cam.rotation().to(15.0, 1.0).await;    // tilt
cam.reset(1.2).await;                  // back to x = y = 0, zoom 1, no rotation

fit measures the node when it is called, so a layout that has grown is framed at its current size. A duration of 0.0 jumps there at once — handy for the first frame. To move and zoom together, run both in all:

all((cam.center_on(api, 1.2), cam.zoom().to(1.6, 1.2))).await;

Following a node

Bind the camera's position to a node and it follows it every frame. The next animation of the camera's x or y releases it.

let (px, py) = (packet.x(), packet.y());
cam.x().bind(move || px.get());
cam.y().bind(move || py.get());
packet.x().to(900.0, 2.0).await;       // the camera rides along
cam.fit(diagram, 120.0, 1.4).await;    // and lets go here

Sharp at any zoom

Nothing is stretched: every frame is drawn again at its final scale. Text and outlines stay crisp at ×10, and blurs, shadows and glows grow with the zoom instead of turning to mush — on the GPU and the CPU renderer alike.

Checking

oxframe check does not report nodes inside a camera as out of frame: when you zoom in on a detail, the rest of the world is supposed to be outside. Text overlap and contrast are still checked, and nodes added to the view are checked as usual.