1use std::time::Duration;
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct SampledSpring {
20 pub duration: Duration,
21 samples: [f32; 25],
22}
23
24impl SampledSpring {
25 pub fn position(self, progress: f32) -> f32 {
28 if !progress.is_finite() {
29 return 0.;
30 }
31 let progress = progress.clamp(0., 1.);
32 if progress >= 1. {
33 return 1.;
34 }
35 let position = progress * 24.;
36 let index = position.floor() as usize;
37 let fraction = position - index as f32;
38 self.samples[index] + (self.samples[index + 1] - self.samples[index]) * fraction
39 }
40}
41
42pub const FAST_SPATIAL: SampledSpring = SampledSpring {
43 duration: Duration::from_millis(425),
44 samples: [
45 0.0, 0.102, 0.323, 0.57, 0.784, 0.942, 1.039, 1.085, 1.094, 1.082, 1.06, 1.037, 1.018,
46 1.004, 0.995, 0.992, 0.991, 0.992, 0.995, 0.997, 0.999, 1.0, 1.001, 1.001, 1.0,
47 ],
48};
49
50pub const DEFAULT_SPATIAL: SampledSpring = SampledSpring {
51 duration: Duration::from_millis(450),
52 samples: [
53 0.0, 0.055, 0.18, 0.332, 0.483, 0.619, 0.733, 0.823, 0.891, 0.94, 0.973, 0.994, 1.007,
54 1.013, 1.015, 1.015, 1.013, 1.011, 1.009, 1.006, 1.005, 1.003, 1.002, 1.001, 1.0,
55 ],
56};
57
58pub const SLOW_SPATIAL: SampledSpring = SampledSpring {
59 duration: Duration::from_millis(625),
60 samples: [
61 0.0, 0.056, 0.182, 0.335, 0.488, 0.624, 0.738, 0.827, 0.895, 0.943, 0.975, 0.996, 1.007,
62 1.013, 1.015, 1.015, 1.013, 1.011, 1.008, 1.006, 1.004, 1.003, 1.002, 1.001, 1.0,
63 ],
64};
65
66pub const FAST_EFFECTS: SampledSpring = SampledSpring {
67 duration: Duration::from_millis(175),
68 samples: [
69 0.0, 0.075, 0.227, 0.39, 0.537, 0.657, 0.751, 0.822, 0.874, 0.912, 0.939, 0.958, 0.971,
70 0.98, 0.987, 0.991, 0.994, 0.996, 0.997, 0.998, 0.999, 0.999, 0.999, 1.0, 1.0,
71 ],
72};
73
74pub const DEFAULT_EFFECTS: SampledSpring = SampledSpring {
75 duration: Duration::from_millis(250),
76 samples: [
77 0.0, 0.066, 0.203, 0.355, 0.496, 0.616, 0.713, 0.788, 0.845, 0.888, 0.92, 0.943, 0.96,
78 0.971, 0.98, 0.986, 0.99, 0.993, 0.995, 0.997, 0.998, 0.998, 0.999, 0.999, 1.0,
79 ],
80};
81
82pub const SLOW_EFFECTS: SampledSpring = SampledSpring {
83 duration: Duration::from_millis(350),
84 samples: [
85 0.0, 0.065, 0.2, 0.351, 0.491, 0.611, 0.708, 0.783, 0.841, 0.885, 0.917, 0.941, 0.958,
86 0.97, 0.979, 0.985, 0.99, 0.993, 0.995, 0.997, 0.998, 0.998, 0.999, 0.999, 1.0,
87 ],
88};
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn non_finite_progress_paints_the_start() {
96 for progress in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
97 assert_eq!(FAST_SPATIAL.position(progress), 0.);
98 }
99 assert_eq!(FAST_SPATIAL.position(1.), 1.);
100 }
101
102 #[test]
103 fn curves_match_browser_computed_transforms() {
104 let reference: serde_json::Value = serde_json::from_str(include_str!(
105 "../../../../conformance/fixtures/md3-expressive-browser.json"
106 ))
107 .unwrap();
108 let curves = reference["curves"].as_array().unwrap();
109 assert_eq!(curves.len(), 6);
110 let mut names = std::collections::BTreeSet::new();
111 for curve in curves {
112 let token = curve["token"].as_str().unwrap();
113 assert!(names.insert(token), "duplicate reference curve {token}");
114 assert_eq!(curve["samples"].as_array().unwrap().len(), 29);
115 let spring = match token {
116 "fast-spatial" => FAST_SPATIAL,
117 "default-spatial" => DEFAULT_SPATIAL,
118 "slow-spatial" => SLOW_SPATIAL,
119 "fast-effects" => FAST_EFFECTS,
120 "default-effects" => DEFAULT_EFFECTS,
121 "slow-effects" => SLOW_EFFECTS,
122 _ => panic!("unexpected reference token {token}"),
123 };
124 assert_eq!(
125 spring.duration.as_millis(),
126 curve["duration_ms"].as_u64().unwrap() as u128
127 );
128 for sample in curve["samples"].as_array().unwrap() {
129 let progress = sample[0].as_f64().unwrap() as f32;
130 let expected = sample[1].as_f64().unwrap() as f32;
131 let actual = spring.position(progress);
132 assert!(
133 (actual - expected).abs() < 0.00001,
134 "{token} at {progress}: {actual} vs browser {expected}"
135 );
136 }
137 }
138 }
139
140 #[test]
141 fn spatial_overshoot_is_preserved_and_effects_are_bounded() {
142 for spring in [FAST_SPATIAL, DEFAULT_SPATIAL, SLOW_SPATIAL] {
143 assert!((0..=1000).any(|t| spring.position(t as f32 / 1000.) > 1.01));
144 assert_eq!(spring.position(-1.), 0.);
145 assert_eq!(spring.position(1.), 1.);
146 assert_eq!(spring.position(2.), 1.);
147 }
148 for spring in [FAST_EFFECTS, DEFAULT_EFFECTS, SLOW_EFFECTS] {
149 let values = (0..=1000)
150 .map(|t| spring.position(t as f32 / 1000.))
151 .collect::<Vec<_>>();
152 assert!(values.iter().all(|v| (0. ..=1.).contains(v)));
153 assert!(values.windows(2).all(|p| p[0] <= p[1]));
154 }
155 }
156}