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.

Writing your own node

A struct that draws itself becomes a node: animatable fields, layout, effects and checks, like the built-in ones.

Loading live preview…

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

A struct that draws

Turn on the unstable-node-api feature of oxframe. The API may change until it has outside users.

oxframe = { version = "…", features = ["unstable-node-api"] }
#[derive(Node)]
pub struct Gauge {
    #[signal] value: f64,   // 0..1
    #[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 * self.value).stroke(Accent, 18.0);
    }
    fn size(&self) -> (f64, f64) {
        (self.size, self.size)
    }
}

#[derive(Node)] on a struct that implements Draw makes it a node. Each field marked #[signal] gets a setter on the builder, Gauge::new(), and a signal on the handle, GaugeHandle, so gauge.value().to(0.8, 1.0) animates it. Fields without the mark are set once on the builder. Every field type needs Default and Clone.

let gauge = view.add(Gauge::new().size(240.0));
gauge.value().to(0.8, 1.0).await;

Drawing

Draw::draw paints with the same DrawCtx as Custom: ctx.line, ctx.circle, ctx.rect, ctx.path and DrawCtx::arc for gauges and rings, each followed by .fill(..) or .stroke(.., width) with a color or a theme role. Coordinates are the node's own, with the origin at the center of its box.

Draw only from the fields and ctx.time(), and take randomness from ctx.rng(seed). The frame is painted again on every seek, and oxframe check reports a node whose two paints of one frame differ.

Its box

Draw::size gives the box that flex layout, Camera::fit and oxframe check use; the default is 100 by 100. Everything else comes with the node: position, rotation, scale, opacity, relative placement such as to_edge, effects, flex properties and theme colors. check and inspect name it by its type, so an issue reads Gauge, not Custom.

A crate of your own

oxframe init --template node my-node

oxframe init --template node my-node makes a library crate: the node in src/lib.rs, a demo scene in examples/demo.rs (cargo run --example demo -- render) and a reference test that renders the node on the CPU and checks its pixels. Publish it like any crate; scenes add it as a dependency.

The playground has the feature on, so a node written in a scene there works too.