use std::time::Duration;
use frust::Curve;
#[derive(Debug)]
pub enum MaterialMotion {}
impl MaterialMotion {
pub const SHORT_1: Duration = Duration::from_millis(50);
pub const SHORT_2: Duration = Duration::from_millis(100);
pub const SHORT_3: Duration = Duration::from_millis(150);
pub const SHORT_4: Duration = Duration::from_millis(200);
pub const MEDIUM_1: Duration = Duration::from_millis(250);
pub const MEDIUM_2: Duration = Duration::from_millis(300);
pub const MEDIUM_3: Duration = Duration::from_millis(350);
pub const MEDIUM_4: Duration = Duration::from_millis(400);
pub const LONG_1: Duration = Duration::from_millis(450);
pub const LONG_2: Duration = Duration::from_millis(500);
pub const LONG_3: Duration = Duration::from_millis(550);
pub const LONG_4: Duration = Duration::from_millis(600);
pub const EXTRA_LONG_1: Duration = Duration::from_millis(700);
pub const EXTRA_LONG_2: Duration = Duration::from_millis(800);
pub const EXTRA_LONG_3: Duration = Duration::from_millis(900);
pub const EXTRA_LONG_4: Duration = Duration::from_millis(1000);
pub const STANDARD: Curve = Curve::Cubic(0.2, 0.0, 0.0, 1.0);
pub const STANDARD_ACCELERATE: Curve = Curve::Cubic(0.3, 0.0, 1.0, 1.0);
pub const STANDARD_DECELERATE: Curve = Curve::Cubic(0.0, 0.0, 0.0, 1.0);
pub const EMPHASIZED: Curve = Curve::Cubic(0.2, 0.0, 0.0, 1.0);
pub const EMPHASIZED_ACCELERATE: Curve = Curve::Cubic(0.3, 0.0, 0.8, 0.15);
pub const EMPHASIZED_DECELERATE: Curve = Curve::Cubic(0.05, 0.7, 0.1, 1.0);
pub const LINEAR: Curve = Curve::Cubic(0.0, 0.0, 1.0, 1.0);
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MaterialSpring {
pub stiffness: f64,
pub damping_ratio: f64,
}
impl MaterialSpring {
const fn new(stiffness: f64, damping_ratio: f64) -> Self {
Self {
stiffness,
damping_ratio,
}
}
pub const SPATIAL_FAST: Self = Self::new(1400.0, 0.9);
pub const SPATIAL_DEFAULT: Self = Self::new(700.0, 0.9);
pub const SPATIAL_SLOW: Self = Self::new(300.0, 0.9);
pub const AOSP_SPATIAL: Self = Self::new(380.0, 1.0);
pub const EXPRESSIVE_SPATIAL_FAST: Self = Self::new(800.0, 0.6);
pub const EXPRESSIVE_SPATIAL_DEFAULT: Self = Self::new(380.0, 0.8);
pub const EXPRESSIVE_SPATIAL_PRESS: Self = Self::new(380.0, 0.55);
pub const EXPRESSIVE_SPATIAL_SLOW: Self = Self::new(200.0, 0.8);
pub const EFFECTS_FAST: Self = Self::new(3800.0, 1.0);
pub const EFFECTS_DEFAULT: Self = Self::new(1600.0, 1.0);
pub const EFFECTS_SLOW: Self = Self::new(800.0, 1.0);
}
impl From<MaterialSpring> for frust::MotionSpring {
fn from(spring: MaterialSpring) -> Self {
frust::MotionSpring {
damping_ratio: spring.damping_ratio,
stiffness: spring.stiffness,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn durations_match_reference_table_exactly() {
let table: [(Duration, u64); 16] = [
(MaterialMotion::SHORT_1, 50),
(MaterialMotion::SHORT_2, 100),
(MaterialMotion::SHORT_3, 150),
(MaterialMotion::SHORT_4, 200),
(MaterialMotion::MEDIUM_1, 250),
(MaterialMotion::MEDIUM_2, 300),
(MaterialMotion::MEDIUM_3, 350),
(MaterialMotion::MEDIUM_4, 400),
(MaterialMotion::LONG_1, 450),
(MaterialMotion::LONG_2, 500),
(MaterialMotion::LONG_3, 550),
(MaterialMotion::LONG_4, 600),
(MaterialMotion::EXTRA_LONG_1, 700),
(MaterialMotion::EXTRA_LONG_2, 800),
(MaterialMotion::EXTRA_LONG_3, 900),
(MaterialMotion::EXTRA_LONG_4, 1000),
];
for (actual, expected_ms) in table {
assert_eq!(actual, Duration::from_millis(expected_ms));
}
}
type EasingRow = (&'static str, Curve, (f64, f64, f64, f64));
fn easing_table() -> [EasingRow; 7] {
[
("standard", MaterialMotion::STANDARD, (0.2, 0.0, 0.0, 1.0)),
(
"standard_accelerate",
MaterialMotion::STANDARD_ACCELERATE,
(0.3, 0.0, 1.0, 1.0),
),
(
"standard_decelerate",
MaterialMotion::STANDARD_DECELERATE,
(0.0, 0.0, 0.0, 1.0),
),
(
"emphasized",
MaterialMotion::EMPHASIZED,
(0.2, 0.0, 0.0, 1.0),
),
(
"emphasized_accelerate",
MaterialMotion::EMPHASIZED_ACCELERATE,
(0.3, 0.0, 0.8, 0.15),
),
(
"emphasized_decelerate",
MaterialMotion::EMPHASIZED_DECELERATE,
(0.05, 0.7, 0.1, 1.0),
),
("linear", MaterialMotion::LINEAR, (0.0, 0.0, 1.0, 1.0)),
]
}
#[test]
fn easings_match_reference_control_points_exactly() {
for (name, curve, expected) in easing_table() {
match curve {
Curve::Cubic(x1, y1, x2, y2) => {
assert_eq!((x1, y1, x2, y2), expected, "curve `{name}`");
}
other => panic!("curve `{name}` is not a Cubic: {other:?}"),
}
}
}
#[test]
fn easing_endpoints_are_exact() {
for (name, curve, _) in easing_table() {
assert!(
curve.transform(0.0).abs() < 1e-9,
"curve `{name}` at t=0 should be 0, got {}",
curve.transform(0.0)
);
assert!(
(curve.transform(1.0) - 1.0).abs() < 1e-9,
"curve `{name}` at t=1 should be 1, got {}",
curve.transform(1.0)
);
}
}
#[test]
fn easings_are_monotonic_in_t() {
for (name, curve, _) in easing_table() {
let mut prev = curve.transform(0.0);
for i in 1..=20 {
let t = i as f64 / 20.0;
let cur = curve.transform(t);
assert!(
cur + 1e-9 >= prev,
"curve `{name}` not monotonic at t={t}: {cur} < prev {prev}"
);
prev = cur;
}
}
}
#[test]
fn springs_match_reference_table_exactly() {
let table: [(MaterialSpring, f64, f64); 11] = [
(MaterialSpring::SPATIAL_FAST, 1400.0, 0.9),
(MaterialSpring::SPATIAL_DEFAULT, 700.0, 0.9),
(MaterialSpring::SPATIAL_SLOW, 300.0, 0.9),
(MaterialSpring::AOSP_SPATIAL, 380.0, 1.0),
(MaterialSpring::EXPRESSIVE_SPATIAL_FAST, 800.0, 0.6),
(MaterialSpring::EXPRESSIVE_SPATIAL_DEFAULT, 380.0, 0.8),
(MaterialSpring::EXPRESSIVE_SPATIAL_PRESS, 380.0, 0.55),
(MaterialSpring::EXPRESSIVE_SPATIAL_SLOW, 200.0, 0.8),
(MaterialSpring::EFFECTS_FAST, 3800.0, 1.0),
(MaterialSpring::EFFECTS_DEFAULT, 1600.0, 1.0),
(MaterialSpring::EFFECTS_SLOW, 800.0, 1.0),
];
for (spring, stiffness, damping_ratio) in table {
assert_eq!(spring.stiffness, stiffness);
assert_eq!(spring.damping_ratio, damping_ratio);
}
}
#[test]
fn spring_into_motion_spring_preserves_fields() {
let converted: frust::MotionSpring = MaterialSpring::SPATIAL_FAST.into();
assert_eq!(converted.stiffness, 1400.0);
assert_eq!(converted.damping_ratio, 0.9);
}
#[test]
fn thirty_four_tokens_total() {
let durations = 16;
let easings = easing_table().len();
let springs = 11;
assert_eq!(durations + easings + springs, 34);
}
}