1use core::f32::consts::{FRAC_PI_2, PI};
22use std::collections::HashMap;
23
24use crate::{CurveError, Point};
25
26pub trait AnimationCurve: Send + Sync + core::fmt::Debug {
30 fn eval(&self, t: f32) -> f32;
31}
32
33#[derive(Copy, Clone, Debug, PartialEq)]
39pub enum BuiltinEasing {
40 Linear,
42
43 EaseIn,
45 EaseOut,
46 EaseInOut,
47 QuadIn,
48 QuadOut,
49 QuadInOut,
50
51 CubicIn,
53 CubicOut,
54 CubicInOut,
55
56 SineIn,
58 SineOut,
59 SineInOut,
60
61 CubicBezier { c1x: f32, c1y: f32, c2x: f32, c2y: f32 },
65}
66
67impl BuiltinEasing {
68 pub fn eval(&self, t: f32) -> f32 {
70 let t = t.clamp(0.0, 1.0);
71 use BuiltinEasing::*;
72 match *self {
73 Linear => t,
74 EaseIn | QuadIn => t * t,
75 EaseOut | QuadOut => t * (2.0 - t),
76 EaseInOut | QuadInOut => {
77 if t < 0.5 {
78 2.0 * t * t
79 } else {
80 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
81 }
82 }
83 CubicIn => t * t * t,
84 CubicOut => {
85 let u = 1.0 - t;
86 1.0 - u * u * u
87 }
88 CubicInOut => {
89 if t < 0.5 {
90 4.0 * t * t * t
91 } else {
92 let u = -2.0 * t + 2.0;
93 1.0 - u * u * u / 2.0
94 }
95 }
96 SineIn => 1.0 - (t * FRAC_PI_2).cos(),
97 SineOut => (t * FRAC_PI_2).sin(),
98 SineInOut => -((PI * t).cos() - 1.0) / 2.0,
99 CubicBezier { c1x, c1y, c2x, c2y } => cubic_bezier_eval(c1x, c1y, c2x, c2y, t),
100 }
101 }
102
103 pub fn validate(&self) -> Result<(), CurveError> {
105 if let BuiltinEasing::CubicBezier { c1x, c2x, .. } = *self {
106 if !c1x.is_finite() || !c2x.is_finite() || !(0.0..=1.0).contains(&c1x)
107 || !(0.0..=1.0).contains(&c2x)
108 {
109 return Err(CurveError::InvalidBezier { c1x, c2x });
110 }
111 }
112 Ok(())
113 }
114}
115
116fn cubic_bezier_eval(c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32) -> f32 {
120 if x <= 0.0 {
121 return 0.0;
122 }
123 if x >= 1.0 {
124 return 1.0;
125 }
126
127 let cx = 3.0 * c1x;
129 let bx = 3.0 * (c2x - c1x) - cx;
130 let ax = 1.0 - cx - bx;
131 let cy = 3.0 * c1y;
132 let by = 3.0 * (c2y - c1y) - cy;
133 let ay = 1.0 - cy - by;
134
135 let sample_x = |s: f32| ((ax * s + bx) * s + cx) * s;
136 let sample_dx = |s: f32| (3.0 * ax * s + 2.0 * bx) * s + cx;
137 let sample_y = |s: f32| ((ay * s + by) * s + cy) * s;
138
139 let mut s = x;
141 for _ in 0..8 {
142 let x_est = sample_x(s);
143 let dx = sample_dx(s);
144 if dx.abs() < 1e-6 {
145 break;
146 }
147 let delta = (x_est - x) / dx;
148 s -= delta;
149 if delta.abs() < 1e-6 {
150 return sample_y(s.clamp(0.0, 1.0));
151 }
152 }
153
154 let (mut lo, mut hi) = (0.0f32, 1.0f32);
156 let mut s = x;
157 for _ in 0..32 {
158 let x_est = sample_x(s);
159 if (x_est - x).abs() < 1e-7 {
160 break;
161 }
162 if x_est < x {
163 lo = s;
164 } else {
165 hi = s;
166 }
167 s = (lo + hi) * 0.5;
168 }
169 sample_y(s.clamp(0.0, 1.0))
170}
171
172#[derive(Copy, Clone, Debug, PartialEq)]
178pub struct Keyframe {
179 pub t: f32,
180 pub v: f32,
181}
182
183impl Keyframe {
184 #[inline]
185 pub const fn new(t: f32, v: f32) -> Self {
186 Self { t, v }
187 }
188}
189
190#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
192pub enum KeyframeInterp {
193 #[default]
195 Linear,
196 Step,
198 SmoothStep,
200}
201
202#[derive(Clone, Debug, PartialEq)]
211pub struct KeyframeCurve {
212 keys: Vec<Keyframe>,
213 interp: KeyframeInterp,
214}
215
216pub const VALUE_CLAMP: f32 = 100.0;
223
224pub const MIN_SAMPLES: u32 = 2;
226pub const MAX_SAMPLES: u32 = 4096;
228
229impl KeyframeCurve {
230 pub fn new(keys: Vec<Keyframe>, interp: KeyframeInterp) -> Result<Self, CurveError> {
232 Self::validate(&keys)?;
233 Ok(Self { keys, interp })
234 }
235
236 #[inline]
238 pub fn keys(&self) -> &[Keyframe] {
239 &self.keys
240 }
241
242 #[inline]
243 pub fn interpolation(&self) -> KeyframeInterp {
244 self.interp
245 }
246
247 pub fn eval(&self, t: f32) -> f32 {
249 let t = t.clamp(0.0, 1.0);
250 let idx = self.keys.partition_point(|k| k.t <= t);
254 if idx == 0 {
255 return self.keys[0].v;
256 }
257 if idx >= self.keys.len() {
258 return self.keys[self.keys.len() - 1].v;
259 }
260 let a = self.keys[idx - 1];
261 let b = self.keys[idx];
262 let span = b.t - a.t;
263 if span <= f32::EPSILON {
264 return b.v;
265 }
266 let local = (t - a.t) / span;
267 match self.interp {
268 KeyframeInterp::Linear => a.v + (b.v - a.v) * local,
269 KeyframeInterp::Step => a.v,
270 KeyframeInterp::SmoothStep => {
271 let s = local * local * (3.0 - 2.0 * local);
272 a.v + (b.v - a.v) * s
273 }
274 }
275 }
276
277 pub fn from_curve_fn<F>(
287 mut f: F,
288 samples: u32,
289 interp: KeyframeInterp,
290 ) -> Result<Self, CurveError>
291 where
292 F: FnMut(f32) -> f32,
293 {
294 Self::sample(samples, interp, |t| f(t))
295 }
296
297 pub fn from_motion_fn<F>(
304 mut f: F,
305 ctx: &MotionContext,
306 samples: u32,
307 interp: KeyframeInterp,
308 ) -> Result<Self, CurveError>
309 where
310 F: FnMut(&MotionContext, f32) -> f32,
311 {
312 Self::sample(samples, interp, |t| f(ctx, t))
313 }
314
315 fn sample<F: FnMut(f32) -> f32>(
316 samples: u32,
317 interp: KeyframeInterp,
318 mut f: F,
319 ) -> Result<Self, CurveError> {
320 if !(MIN_SAMPLES..=MAX_SAMPLES).contains(&samples) {
321 return Err(CurveError::InvalidSampleCount { got: samples });
322 }
323
324 let n = samples as usize;
325 let mut raw: Vec<Keyframe> = Vec::with_capacity(n);
326 let last_i = n - 1;
327 for i in 0..n {
328 let t = if i == 0 {
330 0.0
331 } else if i == last_i {
332 1.0
333 } else {
334 i as f32 / last_i as f32
335 };
336 let v = f(t);
337 if !v.is_finite() || v < -VALUE_CLAMP || v > VALUE_CLAMP {
338 return Err(CurveError::SampledValueInvalid { t, v });
339 }
340 raw.push(Keyframe { t, v });
341 }
342
343 let mut dedup: Vec<Keyframe> = Vec::with_capacity(raw.len());
346 for (i, k) in raw.iter().enumerate() {
347 if i == 0 || i == last_i {
348 dedup.push(*k);
349 continue;
350 }
351 let prev_v = raw[i - 1].v;
352 let next_v = raw[i + 1].v;
353 if prev_v == k.v && next_v == k.v {
354 continue;
355 }
356 dedup.push(*k);
357 }
358
359 Self::new(dedup, interp)
360 }
361
362 fn validate(keys: &[Keyframe]) -> Result<(), CurveError> {
363 if keys.len() < 2 {
364 return Err(CurveError::TooFewKeys(keys.len()));
365 }
366 if keys[0].t != 0.0 {
367 return Err(CurveError::FirstKeyNotZero(keys[0].t));
368 }
369 let last = keys[keys.len() - 1];
370 if last.t != 1.0 {
371 return Err(CurveError::LastKeyNotOne(last.t));
372 }
373 let mut prev_t = f32::NEG_INFINITY;
374 for (i, k) in keys.iter().enumerate() {
375 if !k.t.is_finite() || k.t < 0.0 || k.t > 1.0 || k.t <= prev_t {
376 if !(i == 0 && k.t == 0.0) {
378 return Err(CurveError::InvalidKeyOrder { index: i, t: k.t });
379 }
380 }
381 if !k.v.is_finite() {
382 return Err(CurveError::NonFiniteValue {
383 index: i,
384 t: k.t,
385 v: k.v,
386 });
387 }
388 prev_t = k.t;
389 }
390 Ok(())
391 }
392}
393
394impl AnimationCurve for KeyframeCurve {
395 #[inline]
396 fn eval(&self, t: f32) -> f32 {
397 self.eval(t)
398 }
399}
400
401#[derive(Clone, Debug)]
407pub struct MotionContext {
408 pub origin: Point,
409 pub target: Point,
410 pub distance_px: f32,
412 pub duration: std::time::Duration,
414 pub params: HashMap<String, f64>,
416}
417
418impl MotionContext {
419 pub fn new(origin: Point, target: Point, duration: std::time::Duration) -> Self {
421 Self {
422 origin,
423 target,
424 distance_px: Point::distance(origin, target),
425 duration,
426 params: HashMap::new(),
427 }
428 }
429
430 #[must_use]
431 pub fn with_params(mut self, params: HashMap<String, f64>) -> Self {
432 self.params = params;
433 self
434 }
435
436 #[must_use]
437 pub fn with_param(mut self, key: impl Into<String>, value: f64) -> Self {
438 self.params.insert(key.into(), value);
439 self
440 }
441
442 #[inline]
443 pub fn param(&self, key: &str) -> Option<f64> {
444 self.params.get(key).copied()
445 }
446}
447
448#[derive(Clone, Debug, PartialEq)]
457pub enum Curve {
458 Builtin(BuiltinEasing),
459 Keyframe(KeyframeCurve),
460}
461
462impl Curve {
463 pub fn linear() -> Self {
466 Self::Builtin(BuiltinEasing::Linear)
467 }
468 pub fn ease_in() -> Self {
469 Self::Builtin(BuiltinEasing::EaseIn)
470 }
471 pub fn ease_out() -> Self {
472 Self::Builtin(BuiltinEasing::EaseOut)
473 }
474 pub fn ease_in_out() -> Self {
475 Self::Builtin(BuiltinEasing::EaseInOut)
476 }
477 pub fn quad_in() -> Self {
478 Self::Builtin(BuiltinEasing::QuadIn)
479 }
480 pub fn quad_out() -> Self {
481 Self::Builtin(BuiltinEasing::QuadOut)
482 }
483 pub fn quad_in_out() -> Self {
484 Self::Builtin(BuiltinEasing::QuadInOut)
485 }
486 pub fn cubic_in() -> Self {
487 Self::Builtin(BuiltinEasing::CubicIn)
488 }
489 pub fn cubic_out() -> Self {
490 Self::Builtin(BuiltinEasing::CubicOut)
491 }
492 pub fn cubic_in_out() -> Self {
493 Self::Builtin(BuiltinEasing::CubicInOut)
494 }
495 pub fn sine_in() -> Self {
496 Self::Builtin(BuiltinEasing::SineIn)
497 }
498 pub fn sine_out() -> Self {
499 Self::Builtin(BuiltinEasing::SineOut)
500 }
501 pub fn sine_in_out() -> Self {
502 Self::Builtin(BuiltinEasing::SineInOut)
503 }
504
505 pub fn cubic_bezier(c1x: f32, c1y: f32, c2x: f32, c2y: f32) -> Result<Self, CurveError> {
507 let e = BuiltinEasing::CubicBezier { c1x, c1y, c2x, c2y };
508 e.validate()?;
509 Ok(Self::Builtin(e))
510 }
511
512 pub fn keyframes(keys: Vec<Keyframe>, interp: KeyframeInterp) -> Result<Self, CurveError> {
515 Ok(Self::Keyframe(KeyframeCurve::new(keys, interp)?))
516 }
517
518 pub fn from_curve_fn<F>(f: F, samples: u32, interp: KeyframeInterp) -> Result<Self, CurveError>
519 where
520 F: FnMut(f32) -> f32,
521 {
522 Ok(Self::Keyframe(KeyframeCurve::from_curve_fn(
523 f, samples, interp,
524 )?))
525 }
526
527 pub fn from_motion_fn<F>(
528 f: F,
529 ctx: &MotionContext,
530 samples: u32,
531 interp: KeyframeInterp,
532 ) -> Result<Self, CurveError>
533 where
534 F: FnMut(&MotionContext, f32) -> f32,
535 {
536 Ok(Self::Keyframe(KeyframeCurve::from_motion_fn(
537 f, ctx, samples, interp,
538 )?))
539 }
540}
541
542impl AnimationCurve for Curve {
543 #[inline]
544 fn eval(&self, t: f32) -> f32 {
545 match self {
546 Curve::Builtin(e) => e.eval(t),
547 Curve::Keyframe(k) => KeyframeCurve::eval(k, t),
548 }
549 }
550}
551
552#[cfg(test)]
557mod tests {
558 use super::*;
559
560 const EPS: f32 = 1e-4;
561
562 fn near(a: f32, b: f32) {
563 assert!((a - b).abs() < EPS, "expected ~{b}, got {a}");
564 }
565
566 #[test]
569 fn builtin_endpoints_for_non_overshoot() {
570 for e in [
571 BuiltinEasing::Linear,
572 BuiltinEasing::EaseIn,
573 BuiltinEasing::EaseOut,
574 BuiltinEasing::EaseInOut,
575 BuiltinEasing::QuadIn,
576 BuiltinEasing::QuadOut,
577 BuiltinEasing::QuadInOut,
578 BuiltinEasing::CubicIn,
579 BuiltinEasing::CubicOut,
580 BuiltinEasing::CubicInOut,
581 BuiltinEasing::SineIn,
582 BuiltinEasing::SineOut,
583 BuiltinEasing::SineInOut,
584 ] {
585 near(e.eval(0.0), 0.0);
586 near(e.eval(1.0), 1.0);
587 }
588 }
589
590 #[test]
591 fn linear_is_identity() {
592 for i in 0..=10 {
593 let t = i as f32 / 10.0;
594 near(BuiltinEasing::Linear.eval(t), t);
595 }
596 }
597
598 #[test]
599 fn builtin_saturates_outside_unit_interval() {
600 near(BuiltinEasing::CubicInOut.eval(-5.0), 0.0);
601 near(BuiltinEasing::CubicInOut.eval(5.0), 1.0);
602 }
603
604 #[test]
605 fn cubic_bezier_matches_linear_for_default_line() {
606 let c = Curve::cubic_bezier(1.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0, 2.0 / 3.0).unwrap();
608 for i in 0..=10 {
609 let t = i as f32 / 10.0;
610 near(c.eval(t), t);
611 }
612 }
613
614 #[test]
615 fn cubic_bezier_rejects_out_of_range_x() {
616 let err = Curve::cubic_bezier(-0.1, 0.0, 1.1, 0.0).unwrap_err();
617 assert!(matches!(err, CurveError::InvalidBezier { .. }));
618 }
619
620 #[test]
623 fn keyframe_rejects_too_few_keys() {
624 let err = KeyframeCurve::new(vec![Keyframe::new(0.0, 0.0)], KeyframeInterp::Linear)
625 .unwrap_err();
626 assert!(matches!(err, CurveError::TooFewKeys(1)));
627 }
628
629 #[test]
630 fn keyframe_rejects_bad_endpoints() {
631 let err = KeyframeCurve::new(
632 vec![Keyframe::new(0.1, 0.0), Keyframe::new(1.0, 1.0)],
633 KeyframeInterp::Linear,
634 )
635 .unwrap_err();
636 assert!(matches!(err, CurveError::FirstKeyNotZero(_)));
637
638 let err = KeyframeCurve::new(
639 vec![Keyframe::new(0.0, 0.0), Keyframe::new(0.9, 1.0)],
640 KeyframeInterp::Linear,
641 )
642 .unwrap_err();
643 assert!(matches!(err, CurveError::LastKeyNotOne(_)));
644 }
645
646 #[test]
647 fn keyframe_rejects_non_ascending_times() {
648 let err = KeyframeCurve::new(
649 vec![
650 Keyframe::new(0.0, 0.0),
651 Keyframe::new(0.5, 0.5),
652 Keyframe::new(0.4, 0.4),
653 Keyframe::new(1.0, 1.0),
654 ],
655 KeyframeInterp::Linear,
656 )
657 .unwrap_err();
658 assert!(matches!(err, CurveError::InvalidKeyOrder { .. }));
659 }
660
661 #[test]
662 fn keyframe_rejects_nan_value() {
663 let err = KeyframeCurve::new(
664 vec![
665 Keyframe::new(0.0, 0.0),
666 Keyframe::new(0.5, f32::NAN),
667 Keyframe::new(1.0, 1.0),
668 ],
669 KeyframeInterp::Linear,
670 )
671 .unwrap_err();
672 assert!(matches!(err, CurveError::NonFiniteValue { .. }));
673 }
674
675 #[test]
676 fn keyframe_linear_interpolates() {
677 let c = KeyframeCurve::new(
678 vec![
679 Keyframe::new(0.0, 0.0),
680 Keyframe::new(0.5, 0.2),
681 Keyframe::new(1.0, 1.0),
682 ],
683 KeyframeInterp::Linear,
684 )
685 .unwrap();
686 near(c.eval(0.0), 0.0);
687 near(c.eval(0.25), 0.1);
688 near(c.eval(0.5), 0.2);
689 near(c.eval(0.75), 0.6);
690 near(c.eval(1.0), 1.0);
691 }
692
693 #[test]
694 fn keyframe_step_interpolates() {
695 let c = KeyframeCurve::new(
696 vec![
697 Keyframe::new(0.0, 0.0),
698 Keyframe::new(0.5, 1.0),
699 Keyframe::new(1.0, 1.0),
700 ],
701 KeyframeInterp::Step,
702 )
703 .unwrap();
704 near(c.eval(0.49), 0.0);
705 near(c.eval(0.5), 1.0);
706 near(c.eval(0.99), 1.0);
707 }
708
709 #[test]
710 fn keyframe_smoothstep_matches_hermite_midpoint() {
711 let c = KeyframeCurve::new(
712 vec![Keyframe::new(0.0, 0.0), Keyframe::new(1.0, 1.0)],
713 KeyframeInterp::SmoothStep,
714 )
715 .unwrap();
716 near(c.eval(0.5), 0.5);
718 near(c.eval(0.25), 0.15625);
720 }
721
722 #[test]
725 fn from_curve_fn_rejects_bad_sample_counts() {
726 for &bad in &[0u32, 1, MAX_SAMPLES + 1, u32::MAX] {
727 let err =
728 KeyframeCurve::from_curve_fn(|t| t, bad, KeyframeInterp::Linear).unwrap_err();
729 assert!(matches!(err, CurveError::InvalidSampleCount { .. }), "sample {bad}");
730 }
731 }
732
733 #[test]
734 fn from_curve_fn_samples_linear_identity() {
735 let c = KeyframeCurve::from_curve_fn(|t| t, 33, KeyframeInterp::Linear).unwrap();
736 for i in 0..=10 {
737 let t = i as f32 / 10.0;
738 near(c.eval(t), t);
739 }
740 }
741
742 #[test]
743 fn from_curve_fn_rejects_nan_or_infinite() {
744 let err = KeyframeCurve::from_curve_fn(|_| f32::NAN, 8, KeyframeInterp::Linear)
745 .unwrap_err();
746 assert!(matches!(err, CurveError::SampledValueInvalid { .. }));
747
748 let err = KeyframeCurve::from_curve_fn(|_| f32::INFINITY, 8, KeyframeInterp::Linear)
749 .unwrap_err();
750 assert!(matches!(err, CurveError::SampledValueInvalid { .. }));
751 }
752
753 #[test]
754 fn from_curve_fn_rejects_out_of_clamp() {
755 let err =
756 KeyframeCurve::from_curve_fn(|_| 200.0, 8, KeyframeInterp::Linear).unwrap_err();
757 assert!(matches!(err, CurveError::SampledValueInvalid { t: _, v }
758 if (v - 200.0).abs() < 1e-6));
759 }
760
761 #[test]
762 fn from_curve_fn_only_calls_callable_at_build_time() {
763 let count = std::sync::atomic::AtomicUsize::new(0);
764 let curve = KeyframeCurve::from_curve_fn(
765 |t| {
766 count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
767 t
768 },
769 10,
770 KeyframeInterp::Linear,
771 )
772 .unwrap();
773 let calls_after_build = count.load(std::sync::atomic::Ordering::Relaxed);
774 for _ in 0..1_000 {
777 let _ = curve.eval(0.42);
778 }
779 assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), calls_after_build);
780 assert_eq!(calls_after_build, 10);
781 }
782
783 #[test]
784 fn from_curve_fn_deduplicates_flat_middle() {
785 let c = KeyframeCurve::from_curve_fn(
788 |t| if t < 0.25 || t > 0.75 { t } else { 0.5 },
789 33,
790 KeyframeInterp::Linear,
791 )
792 .unwrap();
793 assert!(c.keys().len() < 33, "dedup left {} keys", c.keys().len());
795 near(c.eval(0.0), 0.0);
796 near(c.eval(1.0), 1.0);
797 }
798
799 #[test]
800 fn from_motion_fn_receives_context() {
801 let ctx = MotionContext::new(
802 Point::new(0, 0),
803 Point::new(300, 400),
804 std::time::Duration::from_secs(1),
805 )
806 .with_param("g", 9.81);
807 assert!((ctx.distance_px - 500.0).abs() < 1e-4);
808 assert_eq!(ctx.param("g"), Some(9.81));
809
810 let curve = KeyframeCurve::from_motion_fn(
811 |ctx, t| {
812 let g = ctx.params.get("g").copied().unwrap_or(1.0) as f32;
814 (t * g / 10.0).min(1.0)
815 },
816 &ctx,
817 16,
818 KeyframeInterp::Linear,
819 )
820 .unwrap();
821 assert!(curve.eval(1.0) > 0.9);
823 }
824
825 #[test]
828 fn curve_dispatch_matches_underlying_variant() {
829 let a = Curve::linear();
830 let b = Curve::from_curve_fn(|t| t, 8, KeyframeInterp::Linear).unwrap();
831 near(a.eval(0.42), 0.42);
832 near(b.eval(0.42), 0.42);
833 }
834
835 fn assert_send_sync<T: Send + Sync>() {}
838
839 #[test]
840 fn curves_are_send_sync() {
841 assert_send_sync::<Curve>();
842 assert_send_sync::<KeyframeCurve>();
843 assert_send_sync::<BuiltinEasing>();
844 }
845}