Markers, eases, tunables
The full story behind lesson 7: values that live outside your code so you can adjust them in the editor.
The three kinds
wait_until("beat").await; // marker: a point on the timeline
x.to(300.0, 1.0).ease(ease!("slide")).await; // named ease: a curve you can bend
let speed = tune!("speed", 1.4); // tunable: a number with a slider | Marker | wait_until(name) pauses the current task until the marker's time. The first time
it runs, the marker is placed where the code reached it — so nothing changes until you drag it. |
| Ease | ease!(name) is a cubic Bézier curve, like CSS cubic-bezier(). It
starts as a smooth in-out; edit it in the curve editor or pick a preset there. |
| Tunable | tune!(name, default) returns a number. Use it for anything you want to dial in by
eye: sizes, speeds, counts, delays. |
The meta file
Each scene has a src/<scene>.meta.json next to its source. Code provides defaults;
the file stores what you changed. Names missing from the file are added with their defaults the first
time the scene runs.
{
"eases": { "slide": [0.65, 0.0, 0.35, 1.0] },
"markers": { "beat": 1.25 },
"tunables": { "speed": 1.4 },
"version": 1
} Keys are sorted and unknown keys are preserved, so the file is safe to edit by hand and gives clean diffs. Commit it together with the code.
Good to know
- Changing a value in the editor does not recompile — the preview updates instantly.
- Names are per scene. Two scenes can each have their own
"speed". - Renaming a marker in code creates a new one at its default position; the old entry stays in the file until you delete it.
oxframe checkvalidates all meta files.
A bigger example
This chart uses a marker to sync the bars with a beat and named eases for their motion.
Loading live preview…
Source of chart — edit in the playground
//! 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<Vec2> {
(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())
}