Skip to main content

pfx_core/
motion.rs

1pub const RATE: f32 = 240.0;
2pub const STEP: f32 = 1.0 / 240.0;
3
4#[derive(Clone, Copy, Debug, PartialEq)]
5pub struct Rest {
6    pub position: f32,
7    pub velocity: f32,
8}
9
10impl Rest {
11    pub const COARSE: Self = Self {
12        position: 1e-4,
13        velocity: 1e-3,
14    };
15    pub const FINE: Self = Self {
16        position: 1e-5,
17        velocity: 1e-4,
18    };
19}
20
21impl Default for Rest {
22    fn default() -> Self {
23        Self::COARSE
24    }
25}
26
27pub trait Sample: Copy + Default {
28    fn integrate(
29        &mut self,
30        velocity: &mut Self,
31        target: Self,
32        stiffness: f32,
33        damping: f32,
34        h: f32,
35    );
36    fn settled(self, target: Self, velocity: Self, rest: Rest) -> bool;
37    fn lerp(self, to: Self, t: f32) -> Self;
38}
39
40impl Sample for f32 {
41    fn integrate(
42        &mut self,
43        velocity: &mut Self,
44        target: Self,
45        stiffness: f32,
46        damping: f32,
47        h: f32,
48    ) {
49        let force = -stiffness * (*self - target) - damping * *velocity;
50        *velocity += force * h;
51        *self += *velocity * h;
52    }
53
54    fn settled(self, target: Self, velocity: Self, rest: Rest) -> bool {
55        (self - target).abs() < rest.position && velocity.abs() < rest.velocity
56    }
57
58    fn lerp(self, to: Self, t: f32) -> Self {
59        self * (1.0 - t) + to * t
60    }
61}
62
63impl Sample for [f32; 3] {
64    fn integrate(
65        &mut self,
66        velocity: &mut Self,
67        target: Self,
68        stiffness: f32,
69        damping: f32,
70        h: f32,
71    ) {
72        self[0].integrate(&mut velocity[0], target[0], stiffness, damping, h);
73        self[1].integrate(&mut velocity[1], target[1], stiffness, damping, h);
74        self[2].integrate(&mut velocity[2], target[2], stiffness, damping, h);
75    }
76
77    fn settled(self, target: Self, velocity: Self, rest: Rest) -> bool {
78        self[0].settled(target[0], velocity[0], rest)
79            && self[1].settled(target[1], velocity[1], rest)
80            && self[2].settled(target[2], velocity[2], rest)
81    }
82
83    fn lerp(self, to: Self, t: f32) -> Self {
84        [
85            self[0].lerp(to[0], t),
86            self[1].lerp(to[1], t),
87            self[2].lerp(to[2], t),
88        ]
89    }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq)]
93pub struct Spring<T: Sample> {
94    pub value: T,
95    pub velocity: T,
96    pub target: T,
97    pub stiffness: f32,
98    pub damping_ratio: f32,
99    pub rest: Rest,
100}
101
102impl<T: Sample> Spring<T> {
103    pub fn new(stiffness: f32) -> Self {
104        Self {
105            value: T::default(),
106            velocity: T::default(),
107            target: T::default(),
108            stiffness,
109            damping_ratio: 1.0,
110            rest: Rest::default(),
111        }
112    }
113
114    pub fn with_rest(mut self, rest: Rest) -> Self {
115        self.rest = rest;
116        self
117    }
118
119    pub fn at(stiffness: f32, value: T) -> Self {
120        let mut spring = Self::new(stiffness);
121        spring.value = value;
122        spring.target = value;
123        spring
124    }
125
126    pub fn step(&mut self, dt: f32) {
127        let steps = (dt / STEP).ceil().max(1.0) as u32;
128        let h = dt / steps as f32;
129        let damping = 2.0 * self.stiffness.sqrt() * self.damping_ratio;
130        for _ in 0..steps {
131            self.value
132                .integrate(&mut self.velocity, self.target, self.stiffness, damping, h);
133        }
134        if self.settled() {
135            self.value = self.target;
136            self.velocity = T::default();
137        }
138    }
139
140    pub fn settled(&self) -> bool {
141        self.value.settled(self.target, self.velocity, self.rest)
142    }
143}
144
145#[derive(Clone, Copy, Debug, PartialEq)]
146pub struct Cascade<T: Sample> {
147    pub lead: Spring<T>,
148    pub follow: Spring<T>,
149}
150
151impl<T: Sample> Cascade<T> {
152    pub fn new(stiffness: f32, value: T) -> Self {
153        Self {
154            lead: Spring::at(stiffness, value),
155            follow: Spring::at(stiffness, value),
156        }
157    }
158
159    pub fn with_rest(mut self, rest: Rest) -> Self {
160        self.lead.rest = rest;
161        self.follow.rest = rest;
162        self
163    }
164
165    pub fn step(&mut self, dt: f32) {
166        self.lead.step(dt);
167        self.follow.target = self.lead.value;
168        self.follow.step(dt);
169    }
170
171    pub fn value(&self) -> T {
172        self.follow.value
173    }
174
175    pub fn settled(&self) -> bool {
176        self.lead.settled() && self.follow.settled()
177    }
178}
179
180#[derive(Clone, Copy, Debug, PartialEq)]
181pub struct Stepper {
182    time: f64,
183    ticks: u64,
184}
185
186impl Default for Stepper {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192impl Stepper {
193    pub fn new() -> Self {
194        Self {
195            time: 0.0,
196            ticks: 0,
197        }
198    }
199
200    pub fn time(&self) -> f64 {
201        self.time
202    }
203
204    pub fn advance(&mut self, dt: f32) -> u32 {
205        if !dt.is_finite() || dt <= 0.0 {
206            return 0;
207        }
208        self.advance_f64(self.time + f64::from(dt))
209    }
210
211    pub fn advance_to(&mut self, to: f32) -> u32 {
212        self.advance_f64(f64::from(to))
213    }
214
215    fn advance_f64(&mut self, to: f64) -> u32 {
216        if !to.is_finite() || to < self.time {
217            return 0;
218        }
219        let due = (to * f64::from(RATE)).floor().max(0.0) as u64;
220        let n = due.saturating_sub(self.ticks);
221        self.ticks = due;
222        self.time = to;
223        u32::try_from(n).unwrap_or(u32::MAX)
224    }
225}
226
227pub fn smoothstep(t: f32) -> f32 {
228    let t = t.clamp(0.0, 1.0);
229    t * t * (3.0 - 2.0 * t)
230}
231
232pub fn smootherstep(t: f32) -> f32 {
233    let t = t.clamp(0.0, 1.0);
234    let u = if t <= 0.5 { t } else { 1.0 - t };
235    let y = u * u * u * (u * (u * 6.0 - 15.0) + 10.0);
236    if t <= 0.5 { y } else { 1.0 - y }
237}
238
239pub fn quad_in(t: f32) -> f32 {
240    let t = t.clamp(0.0, 1.0);
241    t * t
242}
243
244pub fn quad_out(t: f32) -> f32 {
245    let t = t.clamp(0.0, 1.0);
246    let u = 1.0 - t;
247    1.0 - u * u
248}
249
250pub fn cubic_in(t: f32) -> f32 {
251    let t = t.clamp(0.0, 1.0);
252    t * t * t
253}
254
255pub fn cubic_out(t: f32) -> f32 {
256    let t = t.clamp(0.0, 1.0);
257    let u = 1.0 - t;
258    1.0 - u * u * u
259}
260
261pub fn cubic_in_out(t: f32) -> f32 {
262    let t = t.clamp(0.0, 1.0);
263    if t < 0.5 {
264        4.0 * t * t * t
265    } else {
266        let u = -2.0 * t + 2.0;
267        1.0 - u * u * u / 2.0
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    fn fine_reference(
276        mut value: f32,
277        mut velocity: f32,
278        target: f32,
279        stiffness: f32,
280        dt: f32,
281    ) -> (f32, f32) {
282        let steps = (dt / (1.0 / 240.0)).ceil().max(1.0) as u32;
283        let h = dt / steps as f32;
284        let damping = 2.0 * stiffness.sqrt();
285        for _ in 0..steps {
286            let force = -stiffness * (value - target) - damping * velocity;
287            velocity += force * h;
288            value += velocity * h;
289        }
290        if (value - target).abs() < 1e-5 && velocity.abs() < 1e-4 {
291            value = target;
292            velocity = 0.0;
293        }
294        (value, velocity)
295    }
296
297    #[test]
298    fn a_fine_spring_follows_small_moves_that_a_coarse_one_snaps() {
299        let mut fine = Spring::at(40.0, 0.0f32).with_rest(Rest::FINE);
300        let mut coarse = Spring::at(40.0, 0.0f32);
301        fine.target = 5e-5;
302        coarse.target = 5e-5;
303        fine.step(1.0 / 240.0);
304        coarse.step(1.0 / 240.0);
305        assert_eq!(coarse.value, 5e-5);
306        assert!(fine.value < 5e-5);
307        assert!(!fine.settled());
308    }
309
310    #[test]
311    fn a_fine_spring_matches_its_reference_step_for_step() {
312        let mut spring = Spring::at(37.0, 0.0f32).with_rest(Rest::FINE);
313        let (mut value, mut velocity) = (0.0f32, 0.0f32);
314        let frames = [1.0 / 60.0, 1.0 / 144.0, 0.02, 1.0 / 240.0, 0.05, 1.0 / 30.0];
315        for (index, target) in [0.004f32, -0.0003, 0.00002, 0.0].iter().enumerate() {
316            spring.target = *target;
317            for dt in frames.iter().cycle().skip(index).take(200) {
318                spring.step(*dt);
319                (value, velocity) = fine_reference(value, velocity, *target, 37.0, *dt);
320                assert_eq!(spring.value.to_bits(), value.to_bits());
321                assert_eq!(spring.velocity.to_bits(), velocity.to_bits());
322            }
323        }
324    }
325
326    #[test]
327    fn a_cascade_carries_its_rest_to_both_springs() {
328        let cascade = Cascade::new(10.0, 1.0f32).with_rest(Rest::FINE);
329        assert_eq!(cascade.lead.rest, Rest::FINE);
330        assert_eq!(cascade.follow.rest, Rest::FINE);
331        assert_eq!(Cascade::new(10.0, 1.0f32).lead.rest, Rest::COARSE);
332    }
333
334    fn coarse_reference(
335        mut value: f32,
336        mut velocity: f32,
337        target: f32,
338        stiffness: f32,
339        ratio: f32,
340        dt: f32,
341    ) -> (f32, f32) {
342        let steps = (dt / (1.0 / 240.0)).ceil().max(1.0) as u32;
343        let h = dt / steps as f32;
344        let damping = 2.0 * stiffness.sqrt() * ratio;
345        for _ in 0..steps {
346            let force = -stiffness * (value - target) - damping * velocity;
347            velocity += force * h;
348            value += velocity * h;
349        }
350        if (value - target).abs() < 1e-4 && velocity.abs() < 1e-3 {
351            value = target;
352            velocity = 0.0;
353        }
354        (value, velocity)
355    }
356
357    fn drive(spring: &mut Spring<f32>, frames: &[f32]) -> u32 {
358        let mut clock = Stepper::new();
359        let mut ticks = 0;
360        for &dt in frames {
361            let n = clock.advance(dt);
362            ticks += n;
363            for _ in 0..n {
364                spring.step(STEP);
365            }
366        }
367        ticks
368    }
369
370    #[test]
371    fn settling_time() {
372        let mut spring = Spring::new(100.0);
373        spring.target = 1.0;
374        let dt = 1.0 / 60.0;
375        let mut time = 0.0;
376        let mut previous = 0.0;
377        while !spring.settled() {
378            spring.step(dt);
379            assert!(spring.value >= previous);
380            assert!(spring.value <= 1.0);
381            previous = spring.value;
382            time += dt;
383            assert!(time < 3.0, "still moving at {time}");
384        }
385        assert!(time > 1.0 && time < 1.5, "settled at {time}");
386        assert_eq!(spring.value, 1.0);
387        assert_eq!(spring.velocity, 0.0);
388        spring.step(dt);
389        assert_eq!(spring.value, 1.0);
390        assert_eq!(spring.velocity, 0.0);
391    }
392
393    #[test]
394    fn frame_patterns_agree() {
395        let patterns: [Vec<f32>; 4] = [
396            vec![1.0 / 60.0; 30],
397            vec![1.0 / 240.0; 120],
398            vec![1.0 / 120.0; 60],
399            vec![1.0 / 30.0; 15],
400        ];
401        let mut states = Vec::new();
402        for frames in &patterns {
403            let mut spring = Spring::new(44.0);
404            spring.target = 1.0;
405            let ticks = drive(&mut spring, frames);
406            assert_eq!(ticks, 120);
407            states.push(spring);
408        }
409        let mut jumped = Spring::new(44.0);
410        jumped.target = 1.0;
411        let mut clock = Stepper::new();
412        let ticks = clock.advance_to(0.5);
413        assert_eq!(ticks, 120);
414        for _ in 0..ticks {
415            jumped.step(STEP);
416        }
417        for spring in states {
418            assert!((spring.value - jumped.value).abs() < 1e-6);
419            assert!((spring.velocity - jumped.velocity).abs() < 1e-6);
420        }
421
422        let mut odd = Spring::new(44.0);
423        odd.target = 1.0;
424        let left = drive(&mut odd, &[1.0 / 144.0; 72]);
425        let mut even = Spring::new(44.0);
426        even.target = 1.0;
427        let right = drive(&mut even, &[1.0 / 60.0; 30]);
428        assert_eq!(left, right);
429        assert!((odd.value - even.value).abs() < 1e-6);
430        assert!((odd.velocity - even.velocity).abs() < 1e-6);
431
432        let mut once = Spring::new(44.0);
433        once.target = [0.2, -0.4, 0.8];
434        let mut other = once;
435        let mut a = Stepper::new();
436        let mut b = Stepper::new();
437        for _ in 0..40 {
438            for _ in 0..a.advance(1.0 / 60.0) {
439                once.step(STEP);
440            }
441            for _ in 0..4 {
442                for _ in 0..b.advance(1.0 / 240.0) {
443                    other.step(STEP);
444                }
445            }
446        }
447        for ((left, left_v), (right, right_v)) in once
448            .value
449            .into_iter()
450            .zip(once.velocity)
451            .zip(other.value.into_iter().zip(other.velocity))
452        {
453            assert!((left - right).abs() < 1e-6);
454            assert!((left_v - right_v).abs() < 1e-6);
455        }
456    }
457
458    #[test]
459    fn easing_endpoints_and_monotonicity() {
460        let curves: [fn(f32) -> f32; 7] = [
461            smoothstep,
462            smootherstep,
463            quad_in,
464            quad_out,
465            cubic_in,
466            cubic_out,
467            cubic_in_out,
468        ];
469        for curve in curves {
470            assert_eq!(curve(0.0), 0.0);
471            assert_eq!(curve(1.0), 1.0);
472            assert_eq!(curve(-0.4), 0.0);
473            assert_eq!(curve(1.7), 1.0);
474            let mut previous = 0.0;
475            for i in 0..=4000 {
476                let y = curve(i as f32 / 4000.0);
477                assert!((0.0..=1.0).contains(&y));
478                assert!(y >= previous);
479                previous = y;
480            }
481        }
482        assert_eq!(smoothstep(0.5), 0.5);
483        assert_eq!(smootherstep(0.5), 0.5);
484        assert_eq!(quad_in(0.5), 0.25);
485        assert_eq!(quad_out(0.5), 0.75);
486        assert_eq!(cubic_in(0.5), 0.125);
487        assert_eq!(cubic_out(0.5), 0.875);
488        assert_eq!(cubic_in_out(0.5), 0.5);
489        assert_eq!(smoothstep(0.25), 0.15625);
490    }
491
492    #[test]
493    fn a_coarse_spring_matches_its_reference_step_for_step() {
494        let mut spring = Spring::new(900.0);
495        spring.target = 1.0;
496        for _ in 0..8 {
497            spring.step(1.0 / 60.0);
498        }
499        assert_eq!(spring.value.to_bits(), 0x3f68_168c);
500        assert_eq!(spring.velocity.to_bits(), 0x4007_7e40);
501
502        let mut turn = Spring::new(220.0);
503        turn.target = 1.0;
504        for _ in 0..12 {
505            turn.step(1.0 / 60.0);
506        }
507        assert_eq!(turn.value.to_bits(), 0x3f4c_e683);
508        assert_eq!(turn.velocity.to_bits(), 0x400c_876c);
509
510        let stiffness = [5.0, 11.0, 44.0, 100.0, 220.0, 900.0];
511        let dts = [STEP, 1.0 / 60.0, 1.0 / 144.0, 0.007, 0.02];
512        let ratios = [1.0, 0.5, 1.4];
513        for &k in &stiffness {
514            for &dt in &dts {
515                for &ratio in &ratios {
516                    let mut value = 0.2;
517                    let mut velocity = -0.3;
518                    let mut spring = Spring::at(k, value);
519                    spring.velocity = velocity;
520                    spring.target = 0.7;
521                    spring.damping_ratio = ratio;
522                    for _ in 0..25 {
523                        let (next, speed) = coarse_reference(value, velocity, 0.7, k, ratio, dt);
524                        spring.step(dt);
525                        assert_eq!(spring.value.to_bits(), next.to_bits());
526                        assert_eq!(spring.velocity.to_bits(), speed.to_bits());
527                        value = next;
528                        velocity = speed;
529                    }
530                }
531            }
532        }
533
534        let mut scalar = [Spring::new(132.0), Spring::new(132.0), Spring::new(132.0)];
535        let targets = [0.4, -1.2, 2.5];
536        for (spring, target) in scalar.iter_mut().zip(targets) {
537            spring.target = target;
538        }
539        let mut vector = Spring::new(132.0);
540        vector.target = targets;
541        for _ in 0..17 {
542            for spring in &mut scalar {
543                spring.step(0.007);
544            }
545            vector.step(0.007);
546        }
547        for (lane, (value, velocity)) in scalar
548            .iter()
549            .zip(vector.value.into_iter().zip(vector.velocity))
550        {
551            assert_eq!(value.to_bits(), lane.value.to_bits());
552            assert_eq!(velocity.to_bits(), lane.velocity.to_bits());
553        }
554    }
555
556    #[test]
557    fn cascade_lags_its_leader() {
558        let mut cascade = Cascade::new(44.0, 0.0);
559        cascade.lead.target = 1.0;
560        cascade.step(1.0 / 60.0);
561        assert!(cascade.follow.value > 0.0);
562        assert!(cascade.follow.value < cascade.lead.value);
563        assert!(cascade.lead.value < 1.0);
564        for _ in 0..24 {
565            cascade.step(1.0 / 60.0);
566            assert!(cascade.follow.value <= cascade.lead.value);
567        }
568        assert!(cascade.lead.value - cascade.follow.value > 0.05);
569        assert!(!cascade.settled());
570
571        let mut spatial = Cascade::new(44.0, [0.0; 3]);
572        spatial.lead.target = [1.0, -2.0, 0.5];
573        for _ in 0..8 {
574            spatial.step(1.0 / 60.0);
575        }
576        for (lead, follow) in spatial.lead.value.into_iter().zip(spatial.follow.value) {
577            assert!(follow.abs() < lead.abs());
578        }
579    }
580
581    #[test]
582    fn a_lower_damping_ratio_overshoots() {
583        let mut spring = Spring::new(100.0);
584        spring.damping_ratio = 0.4;
585        spring.target = 1.0;
586        let mut peak = 0.0f32;
587        for _ in 0..180 {
588            spring.step(1.0 / 60.0);
589            peak = peak.max(spring.value);
590        }
591        assert!(peak > 1.2);
592    }
593}