Motion and kit
Spring and Curve
Springs and CSS timing curves that describe how a value moves to its target.
Spring::snappyuse crownui::prelude::*;
button("Spring").hover(|style| style.scale(1.05)).transition(Spring::snappy());
button("Timed").hover(|style| style.opacity(0.8)).transition(Curve::ease_in_out(200));
button("Custom").transition(Spring::new(0.35, 0.75));
// a spinner arm: turn once per second, forever
vstack(()).w(2.0).h(8.0).bg(BLACK)
.rotate(360.0)
.enter(|style| style.rotate(0.0))
.transition(Curve::linear(1000).repeating());#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Spring { /* private */ }
impl Spring {
pub fn new(response: f32, damping_ratio: f32) -> Self
pub fn snappy() -> Self
pub fn smooth() -> Self
pub fn crisp() -> Self
pub fn bouncy() -> Self
pub fn interactive() -> Self
pub fn stiffness(self) -> f32
pub fn damping(self) -> f32
pub fn damping_ratio(self) -> f32
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Curve {
Spring(Spring),
Timing(TimingCurve),
}
impl Curve {
pub fn linear(millis: u64) -> Self
pub fn ease(millis: u64) -> Self
pub fn ease_in(millis: u64) -> Self
pub fn ease_out(millis: u64) -> Self
pub fn ease_in_out(millis: u64) -> Self
pub fn repeating(self) -> Self
}
impl From<Spring> for CurveSpring and Curve describe how a value moves to a new target. A Spring is a damped spring described by how fast it responds and how much it bounces. A Curve is either a spring or a CSS timing curve with a fixed duration. Both come from the motion crate and are in crownui::prelude.
Every API that animates takes impl Into<Curve>, so a Spring and a Curve work in the same places:
Element::transition(curve)animates every change of an element and its subtree. See variants.cx.with_animation(curve, |cx| ..)animates the changes made in one closure. See animation.
All of these types are Copy, so closures capture them by value and helpers return them without allocation.
Spring::new
Creates a spring the SwiftUI way, from a response and a damping ratio.
The period of the undamped oscillation in seconds. Smaller is faster. Values below 0.001 are raised to 0.001.
1.0 is critically damped and arrives without overshoot. Below 1.0 the spring overshoots and bounces, and the lower the value the more it bounces. Above 1.0 it creeps in slowly. Negative values are raised to 0.
let gentle = Spring::new(0.6, 1.0); // slow, no overshoot
let playful = Spring::new(0.45, 0.6); // medium speed, clear bounceA spring has no duration. It ends when it is within a tiny distance of the target and nearly still, and then it lands exactly on the target value.
Spring presets
| Preset | Response | Damping ratio | Use it for |
|---|---|---|---|
Spring::snappy() | 0.25 | 0.86 | State changes. Quick with a hint of overshoot. The default choice. |
Spring::smooth() | 0.4 | 1.0 | Layout and theme changes. Calm, no overshoot. |
Spring::crisp() | 0.15 | 1.0 | Press feedback. Very fast, no overshoot. |
Spring::bouncy() | 0.5 | 0.7 | Motion that should visibly bounce. |
Spring::interactive() | 0.15 | 0.86 | Following a finger or a pointer during a drag. |
Spring::smooth() is also what enter and exit use when no transition covers the node.
vstack(text("Card"))
.hover(|style| style.translate_y(-2.0).shadow(Shadow::lg()))
.active(|style| style.scale(0.98))
.transition(Spring::snappy())Spring accessors
The accessors convert a spring to the physics form, for unit mass. Use them to match a spring in another tool or to check a custom spring.
The spring constant k. It equals the square of the natural frequency, which is 2π / response.
The damping coefficient c, equal to 2 × damping_ratio × natural frequency.
The damping ratio you passed in, after clamping.
let spring = Spring::snappy();
println!("k = {}, c = {}", spring.stiffness(), spring.damping());Curve constructors
Each constructor is a CSS timing function over a duration in milliseconds.
| Constructor | CSS | Cubic bezier |
|---|---|---|
Curve::linear(millis) | linear | (0, 0, 1, 1) |
Curve::ease(millis) | ease | (0.25, 0.1, 0.25, 1) |
Curve::ease_in(millis) | ease-in | (0.42, 0, 1, 1) |
Curve::ease_out(millis) | ease-out | (0, 0, 0.58, 1) |
Curve::ease_in_out(millis) | ease-in-out | (0.42, 0, 0.58, 1) |
The duration in milliseconds.
let shown = cx.signal(true);
text("Fades").opacity(bind(move |rt| if shown.get(rt) { 1.0 } else { 0.0 }))
.transition(Curve::ease_out(150))Curve::repeating
Returns the same timing curve set to start over from its first value every time it ends, forever. Use it for indicators that never arrive anywhere, such as spinners and shimmer. A spring settles by nature, so repeating returns a spring unchanged.
canvas(|_rt, size, list| {
list.fill(Rect::from_xywh(size.width / 2.0 - 1.0, 0.0, 2.0, 6.0), CornerRadii::uniform(1.0), BLACK)
})
.size(16.0)
.rotate(360.0)
.enter(|style| style.rotate(0.0))
.transition(Curve::linear(1000).repeating())The enter style starts the node at 0 degrees and the transition carries it to 360. Because the curve repeats, the node keeps turning until it unmounts. Mount a repeating animation only while it is needed, because it keeps the render thread drawing frames.
Custom cubic bezier curves
Curve::Timing holds a TimingCurve with three public fields. Build one directly for an easing that has no constructor. TimingCurve and CubicBezier live in the motion crate, which the published crownui crate exposes as crownui::motion.
use std::time::Duration;
use crownui::motion::{CubicBezier, TimingCurve};
let emphasized = Curve::Timing(TimingCurve {
duration: Duration::from_millis(350),
easing: CubicBezier::new(0.2, 0.0, 0.0, 1.0),
repeats: false,
});
vstack(text("Panel")).transition(emphasized)The total time of one run.
The CSS cubic-bezier(x1, y1, x2, y2) function. CubicBezier::new clamps x1 and x2 to the range 0 to 1, like CSS. The constants CubicBezier::LINEAR, EASE, EASE_IN, EASE_OUT and EASE_IN_OUT hold the CSS presets.
Whether the run starts over forever.
Into Curve
impl From<Spring> for Curve is the only conversion. It wraps the spring in Curve::Spring. Write your own helpers against impl Into<Curve> to accept both.
fn fade_in(view: impl View, curve: impl Into<Curve>) -> impl View {
vstack(view).enter(|style| style.opacity(0.0)).transition(curve)
}
fade_in(text("Hi"), Spring::smooth());
fade_in(text("Hi"), Curve::ease(250));Choosing between a spring and a timing curve
Springs are the default in CrownUI for three reasons.
- They are interruptible. A retargeted spring keeps its current velocity, so a hover that reverses halfway curves back smoothly. A timing curve restarts its easing from the current value and loses the velocity, which shows as a small kink.
- Their duration follows the distance. A short move ends quickly and a long move takes longer, which feels natural without tuning.
- They are evaluated in closed form. The
motioncrate computes position and velocity at any timestamp directly, so sampling at the frame's presentation time gives identical results on the render thread and in the CrownOS compositor, which uses the same crate.
Timing curves win when the duration matters more than the feel. A spinner needs a fixed period, a progress shimmer needs a steady speed, and a fade that has to finish before something else starts needs a known end. Use Curve for those.
The cost of springs is that you cannot read a duration off them. If two animations have to end together, give them the same spring and the same distance, or use the same timing curve for both. The motion crate also has no keyframes and no physics beyond springs. A multi-step motion is a chain of state changes, each with its own curve.