use frust_core::anim::{Curve, SpringDesc};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MotionSpring {
pub damping_ratio: f64,
pub stiffness: f64,
}
impl MotionSpring {
const fn new(damping_ratio: f64, stiffness: f64) -> Self {
Self {
damping_ratio,
stiffness,
}
}
}
impl From<MotionSpring> for SpringDesc {
fn from(spring: MotionSpring) -> Self {
SpringDesc {
mass: 1.0,
stiffness: spring.stiffness,
damping_ratio: spring.damping_ratio,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MotionDurations {
pub instant: f64,
pub fast: f64,
pub base: f64,
pub slow: f64,
pub deliberate: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CosmeticLoopRate(f32);
impl CosmeticLoopRate {
pub const FLOOR_HZ: f32 = 10.0;
pub const fn new(hz: f32) -> Self {
if hz.is_nan() || hz < Self::FLOOR_HZ {
Self(Self::FLOOR_HZ)
} else {
Self(hz)
}
}
pub const fn hz(self) -> f32 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EasingSet {
pub spatial: Curve,
pub effects: Curve,
pub exit: Curve,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MotionScheme {
pub fast_spatial: MotionSpring,
pub fast_effects: MotionSpring,
pub default_spatial: MotionSpring,
pub default_effects: MotionSpring,
pub slow_spatial: MotionSpring,
pub slow_effects: MotionSpring,
pub durations: MotionDurations,
pub easing: EasingSet,
pub reduce_motion: bool,
pub cosmetic_loop_rate: CosmeticLoopRate,
}
impl MotionScheme {
pub const fn neutral() -> Self {
Self {
fast_spatial: MotionSpring::new(1.0, 700.0),
fast_effects: MotionSpring::new(1.0, 1800.0),
default_spatial: MotionSpring::new(1.0, 400.0),
default_effects: MotionSpring::new(1.0, 900.0),
slow_spatial: MotionSpring::new(1.0, 200.0),
slow_effects: MotionSpring::new(1.0, 450.0),
durations: MotionDurations {
instant: 100.0,
fast: 150.0,
base: 250.0,
slow: 400.0,
deliberate: 600.0,
},
easing: EasingSet {
spatial: Curve::EaseOut,
effects: Curve::EaseInOut,
exit: Curve::EaseIn,
},
reduce_motion: false,
cosmetic_loop_rate: CosmeticLoopRate::new(30.0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spring_desc_round_trips_neutral_presets() {
let m = MotionScheme::neutral();
let presets = [
m.fast_spatial,
m.fast_effects,
m.default_spatial,
m.default_effects,
m.slow_spatial,
m.slow_effects,
];
for preset in presets {
let desc: SpringDesc = preset.into();
assert_eq!(desc.mass, 1.0);
assert_eq!(desc.stiffness, preset.stiffness);
assert_eq!(desc.damping_ratio, preset.damping_ratio);
}
}
#[test]
fn cosmetic_loop_rate_clamps_to_the_floor() {
assert_eq!(CosmeticLoopRate::new(0.0).hz(), CosmeticLoopRate::FLOOR_HZ);
assert_eq!(CosmeticLoopRate::new(-5.0).hz(), CosmeticLoopRate::FLOOR_HZ);
assert_eq!(CosmeticLoopRate::new(5.0).hz(), CosmeticLoopRate::FLOOR_HZ);
assert_eq!(
CosmeticLoopRate::new(CosmeticLoopRate::FLOOR_HZ).hz(),
CosmeticLoopRate::FLOOR_HZ
);
}
#[test]
fn cosmetic_loop_rate_passes_through_above_the_floor() {
assert_eq!(CosmeticLoopRate::new(30.0).hz(), 30.0);
assert_eq!(CosmeticLoopRate::new(60.0).hz(), 60.0);
}
#[test]
fn cosmetic_loop_rate_clamps_nan_to_the_floor() {
let rate = CosmeticLoopRate::new(f32::NAN);
assert_eq!(rate.hz(), CosmeticLoopRate::FLOOR_HZ);
assert!(rate.hz().is_finite());
assert!((1.0 / rate.hz()).is_finite());
}
#[test]
fn cosmetic_loop_rate_new_is_const_fn() {
const RATE: CosmeticLoopRate = CosmeticLoopRate::new(45.0);
assert_eq!(RATE.hz(), 45.0);
}
#[test]
fn theme_builder_map_motion_overrides_the_cosmetic_loop_rate() {
use crate::theme::Theme;
let base = Theme::neutral();
let theme = Theme::builder(base.clone())
.map_motion(|m| MotionScheme {
cosmetic_loop_rate: CosmeticLoopRate::new(15.0),
..m
})
.build();
assert_eq!(theme.motion.cosmetic_loop_rate.hz(), 15.0);
assert_eq!(theme.motion.fast_spatial, base.motion.fast_spatial);
assert_eq!(theme.motion.durations, base.motion.durations);
}
#[test]
fn neutral_springs_are_all_critically_damped() {
let m = MotionScheme::neutral();
for s in [
m.fast_spatial,
m.fast_effects,
m.default_spatial,
m.default_effects,
m.slow_spatial,
m.slow_effects,
] {
assert_eq!(s.damping_ratio, 1.0);
}
}
#[test]
fn neutral_stiffness_still_varies_by_speed_tier() {
let m = MotionScheme::neutral();
assert!(m.fast_spatial.stiffness > m.default_spatial.stiffness);
assert!(m.default_spatial.stiffness > m.slow_spatial.stiffness);
assert!(m.fast_effects.stiffness > m.default_effects.stiffness);
assert!(m.default_effects.stiffness > m.slow_effects.stiffness);
}
#[test]
fn neutral_easing_uses_only_generic_curves() {
let m = MotionScheme::neutral();
assert_eq!(m.easing.spatial, Curve::EaseOut);
assert_eq!(m.easing.effects, Curve::EaseInOut);
assert_eq!(m.easing.exit, Curve::EaseIn);
}
#[test]
fn neutral_cosmetic_loop_rate_defaults_to_30hz() {
assert_eq!(MotionScheme::neutral().cosmetic_loop_rate.hz(), 30.0);
}
#[test]
fn neutral_reduce_motion_defaults_to_false() {
assert!(!MotionScheme::neutral().reduce_motion);
}
const NEUTRAL_CONST: MotionScheme = MotionScheme::neutral();
#[test]
fn neutral_stays_const_fn() {
assert_eq!(NEUTRAL_CONST, MotionScheme::neutral());
}
}