//! Motion as data: a node declares what it eases and how, and the core
//! tweens whatever changes between frames. Six things, each one prop:
//!
//! - `transition(ms)`: a value that changes eases to its new one over that
//! long — the bar's width follows the buttons;
//! - `easing`: the curve it takes, one racer per `Easing`, racing on a
//! click — the four springs are named bounces, from `smooth`, which
//! glides in, to `bouncy`, which overshoots most, and the last racer
//! sets its own with `bounce`;
//! - `slide`: a float whose *position* eases too, so a card jumps between
//! two anchors along a path rather than appearing at the other;
//! - `keyframes`: a cycle of stops the node walks by itself — width, colour,
//! radius, opacity — with `repeat` saying which way (normal, reverse,
//! alternate) and `delay` holding siblings out of phase, which is what a
//! chase light is;
//!
//! Nothing here calls an animation; each frame declares the value it wants
//! and the core is between the last frame and this one.
//!
//! Run: cargo run -p kui-native --example transition [-- --headless]
use kui_devtools::{Drive, Example};
use kui_native::{
Align, App, Core, Easing, FloatConfig, Keyframe, NodeSpec, Repeat, Sizing, TextStyle, Ui,
UiEvent, Value,
};
/// Each racer's easing, and the bounce it gives the spring in place of the
/// easing's own.
const EASINGS: [(&str, Easing, Option<f32>); 9] = [
("linear", Easing::Linear, None),
("ease-out", Easing::EaseOut, None),
("ease-in", Easing::EaseIn, None),
("ease-in-out", Easing::EaseInOut, None),
("smooth", Easing::Smooth, None),
("snappy", Easing::Snappy, None),
("spring", Easing::Spring, None),
("bouncy", Easing::Bouncy, None),
("bounce 0.7", Easing::Spring, Some(0.7)),
];
#[derive(Default)]
struct Motion {
/// The bar's width, as a fraction: what the buttons set and the
/// transition follows.
level: f32,
/// Whether the racers are at the far end.
far: bool,
/// Which anchor the sliding card sits at.
right: bool,
}
impl App for Motion {
fn view(&mut self, ui: &mut Ui<'_>) {
let t = ui.theme();
ui.with(
NodeSpec::column()
.fill()
.pad(24.0)
.gap(18.0)
.cross_align(Align::Start)
.scroll_y(),
|ui| {
// transition: the width follows the level, eased.
ui.text("transition · the bar eases to whatever the buttons set", TextStyle::new(12.0).color(t.muted));
ui.with(NodeSpec::row().gap(8.0).cross_align(Align::Center), |ui| {
for (label, level) in [("0%", 0.0), ("40%", 0.4), ("100%", 1.0)] {
kui_native::widgets::button(
ui,
label,
Value::map([("kind", Value::str("level")), ("to", Value::Float(level))]),
);
}
});
ui.with(
NodeSpec::row()
.size(420.0, 14.0)
.bg(t.sunken)
.radius(7.0),
|ui| {
ui.leaf_keyed(
"bar",
NodeSpec::row()
.width(Sizing::Percent(self.level))
.grow_height()
.bg(t.accent)
.radius(7.0)
.transition(600.0));
},
);
// easing: the same travel on five curves.
ui.text("easing · click a lane and the six race on their own curves", TextStyle::new(12.0).color(t.muted));
ui.with_keyed(
"lanes",
NodeSpec::column()
.width(420.0)
.gap(6.0)
.pad(8.0)
.bg(t.surface)
.radius(8.0)
.border(1.0, t.border)
.on_click("race"),
|ui| {
for (name, easing, bounce) in EASINGS {
ui.with(NodeSpec::row().grow_width().gap(8.0).cross_align(Align::Center), |ui| {
ui.text_in(NodeSpec::row().width(80.0), name, TextStyle::new(11.0).color(t.muted));
ui.with(
NodeSpec::row().grow_width().height(16.0),
|ui| {
// The racer floats inside its lane; `slide`
// is what makes its *position* ease.
let racer = NodeSpec::row()
.float(
FloatConfig::parent()
.inside(if self.far { Align::End } else { Align::Start }, Align::Center),
)
.size(16.0, 16.0)
.radius(8.0)
.bg(t.accent)
.transition(900.0)
.easing(easing)
.slide();
ui.leaf_keyed(
name,
match bounce {
Some(b) => racer.bounce(b),
None => racer,
},
);
},
);
});
}
},
);
// slide: a float that eases between two anchors.
ui.text("slide · the card eases between its two anchors instead of appearing at the other", TextStyle::new(12.0).color(t.muted));
ui.with_keyed(
"stage",
NodeSpec::row()
.size(420.0, 64.0)
.bg(t.sunken)
.radius(8.0)
.on_click("flip"),
|ui| {
let side = if self.right { Align::End } else { Align::Start };
ui.text_in_keyed("card", NodeSpec::column()
.float(FloatConfig::parent().inside(side, Align::Center).offset(0.0, 0.0))
.pad_xy(14.0, 10.0)
.bg(t.raised)
.radius(8.0)
.border(1.0, t.border)
.transition(500.0)
.easing(Easing::EaseInOut)
.slide(), "click the stage", TextStyle::new(13.0));
},
);
// keyframes, repeat, delay: a cycle each node walks alone.
ui.text("keyframes · repeat · delay — a cycle, a direction, and siblings out of phase", TextStyle::new(12.0).color(t.muted));
ui.with(NodeSpec::row().gap(10.0).cross_align(Align::End), |ui| {
for (name, repeat) in [
("normal", Repeat::Normal),
("reverse", Repeat::Reverse),
("alternate", Repeat::Alternate),
("alt-reverse", Repeat::AlternateReverse),
]
.into_iter()
{
ui.with(NodeSpec::column().gap(4.0).cross_align(Align::Center), |ui| {
ui.leaf_keyed(
name,
NodeSpec::row()
.size(48.0, 48.0)
.bg(t.accent)
.radius(6.0)
.transition(1200.0)
.repeat(repeat)
.keyframes(vec![
Keyframe::default().at(0.0).bg(t.accent).radius(6.0).opacity(1.0),
Keyframe::default().at(0.5).bg(t.success).radius(24.0).opacity(0.6),
Keyframe::default().at(1.0).bg(t.danger).radius(6.0).opacity(1.0),
]));
ui.text(name, TextStyle::new(10.0).color(t.muted));
});
}
// The chase: one cycle, five delays.
ui.with(NodeSpec::row().gap(4.0).cross_align(Align::Center), |ui| {
for i in 0..5 {
ui.leaf_keyed(
&format!("chase{i}"),
NodeSpec::row()
.size(12.0, 12.0)
.radius(6.0)
.bg(t.faint)
.transition(800.0)
.delay(i as f32 * 160.0)
.keyframes(vec![
Keyframe::default().at(0.0).bg(t.faint).opacity(0.4),
Keyframe::default().at(0.5).bg(t.accent).opacity(1.0),
Keyframe::default().at(1.0).bg(t.faint).opacity(0.4),
]));
}
});
});
ui.text(
"every value above is what this frame declared; the core is between the last frame and this one",
TextStyle::new(12.0).color(t.faint),
);
},
);
}
fn on_event(&mut self, ev: UiEvent) {
match ev.payload.as_str() {
Some("race") => self.far = !self.far,
Some("flip") => self.right = !self.right,
_ => {
if let Some(to) = ev.payload.get_float("to") {
self.level = to as f32;
}
}
}
}
}
impl Example for Motion {
const KEYS: &'static [(&'static str, &'static str)] = &[
("click a lane", "race the easings"),
("click the stage", "slide the card"),
];
fn window(&self) -> kui_devtools::Window {
kui_devtools::Window::default().size(520.0, 630.0)
}
/// The bar's width is tweened: one frame after the level changes the
/// core is animating on its account, and a click that changes nothing
/// starts nothing. (The keyframe cycles run forever, so "settled" is
/// not a state this frame has; a tween's end is checked in
/// `kui-core`'s own tests.)
fn headless(&mut self, core: &mut Core) -> Result<(), String> {
let mut d = Drive::new(core, 520.0, 630.0);
d.frame(self);
d.frame(self);
let full = d.key_of("100%").ok_or("no 100% button")?;
d.click_key(self, full);
d.check(self.level == 1.0, "the click sets the level")?;
d.advance(0.016);
d.frame(self);
d.check(d.core.animating(), "one frame in, the bar is still moving")?;
let stage = d.key_of("stage").ok_or("no stage")?;
d.click_key(self, stage);
d.frame(self);
d.check(self.right, "the stage flips the card's anchor")?;
d.check(
d.core.animating(),
"and the float slides rather than jumping",
)
}
}
kui_devtools::main!(Motion::default());