1use crate::curve::Curve;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct Eps {
9 pub pos: f32,
11 pub vel: f32,
13}
14
15const MIN_EPS: f32 = 1e-7;
16
17impl Eps {
18 pub const fn new(pos: f32, vel: f32) -> Eps {
20 Eps { pos: eps_value(pos), vel: eps_value(vel) }
21 }
22
23 pub const fn sanitized(self) -> Eps {
25 Eps::new(self.pos, self.vel)
26 }
27}
28
29const fn eps_value(v: f32) -> f32 {
30 if v.is_nan() { 1e-3 } else { v.clamp(MIN_EPS, f32::MAX) }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
37pub enum Class {
38 Spatial,
40 Scale,
42 Opacity,
44 Color,
46 Gesture,
48 #[default]
50 Other,
51}
52
53const PPP_RANGE: (f32, f32) = (0.25, 16.0);
55
56impl Class {
57 pub fn eps(self, pixels_per_point: f32) -> Eps {
60 let ppp = if pixels_per_point.is_nan() { 1.0 } else { pixels_per_point.clamp(PPP_RANGE.0, PPP_RANGE.1) };
61 match self {
62 Class::Spatial | Class::Gesture => Eps::new(0.25 / ppp, 4.0 / ppp),
63 Class::Scale => Eps::new(5e-4, 5e-3),
64 Class::Opacity | Class::Color => Eps::new(1.0 / 1024.0, 1.0 / 64.0),
65 Class::Other => Eps::new(1e-3, 1e-2),
66 }
67 }
68
69 pub const fn is_effect(self) -> bool {
71 matches!(self, Class::Opacity | Class::Color)
72 }
73
74 pub const fn reduce(self, curve: Curve) -> Curve {
78 match self {
79 Class::Spatial | Class::Scale | Class::Other => Curve::Instant,
80 Class::Opacity | Class::Color => {
81 if curve.is_instant() {
82 Curve::Instant
83 } else {
84 crate::tokens::REDUCED_FADE
85 }
86 }
87 Class::Gesture => curve,
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq)]
94pub struct MotionPrefs {
95 pub reduced: bool,
97 pub time_scale: f32,
100}
101
102impl Default for MotionPrefs {
103 fn default() -> Self {
104 MotionPrefs::DEFAULT
105 }
106}
107
108impl MotionPrefs {
109 pub const DEFAULT: MotionPrefs = MotionPrefs { reduced: false, time_scale: 1.0 };
111 pub const REDUCED: MotionPrefs = MotionPrefs { reduced: true, time_scale: 1.0 };
113 pub const MAX_TIME_SCALE: f32 = 10.0;
115
116 pub const fn new(reduced: bool, time_scale: f32) -> MotionPrefs {
118 MotionPrefs { reduced, time_scale: scale(time_scale) }
119 }
120
121 pub const fn scale(self) -> f32 {
123 scale(self.time_scale)
124 }
125}
126
127const fn scale(s: f32) -> f32 {
128 if s.is_nan() { 1.0 } else { s.clamp(0.0, MotionPrefs::MAX_TIME_SCALE) }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::{Easing, Spring, tokens};
135
136 #[test]
137 fn reduced_rules() {
138 let s = Curve::Spring(Spring::new(0.4, 0.2));
139 assert_eq!(Class::Spatial.reduce(s), Curve::Instant);
140 assert_eq!(Class::Scale.reduce(s), Curve::Instant);
141 assert_eq!(Class::Opacity.reduce(s), tokens::REDUCED_FADE);
142 assert_eq!(Class::Color.reduce(s), tokens::REDUCED_FADE);
143 assert_eq!(Class::Opacity.reduce(Curve::Instant), Curve::Instant);
144 assert_eq!(Class::Gesture.reduce(s), s);
145 assert!(matches!(tokens::REDUCED_FADE, Curve::Tween { duration, easing } if (duration - 0.18).abs() < 1e-6 && easing == Easing::EASE_IN_OUT));
146 }
147
148 #[test]
149 fn eps_and_prefs_clamp() {
150 let e = Class::Spatial.eps(f32::NAN);
151 assert_eq!(e, Class::Spatial.eps(1.0));
152 assert!(Class::Spatial.eps(2.0).pos < e.pos);
153 assert!(Class::Spatial.eps(f32::INFINITY).pos > 0.0);
154 assert_eq!(Eps::new(f32::NAN, -1.0), Eps { pos: 1e-3, vel: MIN_EPS });
155 assert_eq!(MotionPrefs::new(false, f32::NAN).time_scale, 1.0);
156 assert_eq!(MotionPrefs::new(false, 99.0).scale(), 10.0);
157 assert_eq!(MotionPrefs { reduced: false, time_scale: -1.0 }.scale(), 0.0);
158 }
159}