1use crate::impl_option;
4
5#[inline]
9#[allow(clippy::cast_precision_loss)]
10const fn idx_to_f64(v: usize) -> f64 {
11 v as f64
12}
13
14#[inline]
17#[allow(clippy::cast_possible_truncation)]
18const fn f64_to_f32(v: f64) -> f32 {
19 v as f32
20}
21
22#[derive(Debug, Copy, Clone, PartialEq)]
24#[repr(C)]
25pub struct InterpolateResolver {
26 pub interpolate_func: AnimationInterpolationFunction,
27 pub parent_rect_width: f32,
28 pub parent_rect_height: f32,
29 pub current_rect_width: f32,
30 pub current_rect_height: f32,
31}
32
33#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
35#[repr(C)]
36pub struct SvgPoint {
37 pub x: f32,
38 pub y: f32,
39}
40
41#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
43#[repr(C)]
44pub struct SvgCubicCurve {
45 pub start: SvgPoint,
46 pub ctrl_1: SvgPoint,
47 pub ctrl_2: SvgPoint,
48 pub end: SvgPoint,
49}
50#[derive(Debug, Copy, Clone, PartialEq)]
56#[repr(C)]
57pub struct SpringCurve {
58 pub stiffness: f32,
60 pub damping: f32,
62 pub mass: f32,
64}
65
66impl SpringCurve {
67 pub const SMOOTH: Self = Self {
69 stiffness: 170.0,
70 damping: 26.0,
71 mass: 1.0,
72 };
73 pub const GENTLE: Self = Self {
75 stiffness: 120.0,
76 damping: 20.0,
77 mass: 1.0,
78 };
79 pub const SNAPPY: Self = Self {
81 stiffness: 260.0,
82 damping: 20.0,
83 mass: 1.0,
84 };
85
86 #[must_use]
88 pub fn damping_ratio(&self) -> f32 {
89 let denom = 2.0 * (self.stiffness * self.mass).sqrt();
90 if denom == 0.0 {
91 0.0
92 } else {
93 self.damping / denom
94 }
95 }
96
97 #[must_use]
102 pub fn step(&self, value: f32, target: f32, velocity: f32, dt: f32) -> (f32, f32) {
103 let dt = dt.clamp(0.0, Self::MAX_STEP_SECS);
104 if self.mass <= 0.0 {
105 return (target, 0.0);
107 }
108 #[allow(clippy::suboptimal_flops)]
114 let force = -self.stiffness * (value - target) - self.damping * velocity;
115 #[allow(clippy::suboptimal_flops)]
116 let new_velocity = velocity + (force / self.mass) * dt;
117 #[allow(clippy::suboptimal_flops)]
118 let new_value = value + new_velocity * dt;
119 (new_value, new_velocity)
120 }
121
122 pub const MAX_STEP_SECS: f32 = 0.05;
124
125 #[must_use]
130 pub fn is_settled(&self, value: f32, target: f32, velocity: f32) -> bool {
131 (value - target).abs() < Self::EPSILON_VALUE && velocity.abs() < Self::EPSILON_VELOCITY
132 }
133
134 pub const EPSILON_VALUE: f32 = 0.06;
136 pub const EPSILON_VELOCITY: f32 = 0.06;
138}
139
140impl Default for SpringCurve {
141 fn default() -> Self {
142 Self::SMOOTH
143 }
144}
145
146#[allow(variant_size_differences)]
147#[derive(Debug, Copy, Clone, PartialEq)]
150#[repr(C, u8)]
151pub enum AnimationInterpolationFunction {
152 Ease,
153 Linear,
154 EaseIn,
155 EaseOut,
156 EaseInOut,
157 CubicBezier(SvgCubicCurve),
158 Spring(SpringCurve),
167}
168
169#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
171#[repr(C)]
172pub struct SvgRect {
173 pub width: f32,
174 pub height: f32,
175 pub x: f32,
176 pub y: f32,
177 pub radius_top_left: f32,
178 pub radius_top_right: f32,
179 pub radius_bottom_left: f32,
180 pub radius_bottom_right: f32,
181}
182
183#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
185#[repr(C)]
186pub struct SvgVector {
187 pub x: f64,
188 pub y: f64,
189}
190
191#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
193#[repr(C)]
194pub struct SvgQuadraticCurve {
195 pub start: SvgPoint,
196 pub ctrl: SvgPoint,
197 pub end: SvgPoint,
198}
199
200impl_option!(
201 SvgPoint,
202 OptionSvgPoint,
203 [Debug, Clone, PartialEq, PartialOrd]
204);
205
206impl SvgPoint {
207 #[inline]
209 #[must_use]
210 pub const fn new(x: f32, y: f32) -> Self {
211 Self { x, y }
212 }
213
214 #[inline]
216 #[must_use]
217 pub fn distance(&self, other: Self) -> f64 {
218 let dx = other.x - self.x;
219 let dy = other.y - self.y;
220 f64::from(libm::hypotf(dx, dy))
221 }
222}
223
224impl SvgRect {
225 pub fn union_with(&mut self, other: &Self) {
227 let self_max_x = self.x + self.width;
228 let self_max_y = self.y + self.height;
229 let self_min_x = self.x;
230 let self_min_y = self.y;
231
232 let other_max_x = other.x + other.width;
233 let other_max_y = other.y + other.height;
234 let other_min_x = other.x;
235 let other_min_y = other.y;
236
237 let max_x = self_max_x.max(other_max_x);
238 let max_y = self_max_y.max(other_max_y);
239 let min_x = self_min_x.min(other_min_x);
240 let min_y = self_min_y.min(other_min_y);
241
242 self.x = min_x;
243 self.y = min_y;
244 self.width = max_x - min_x;
245 self.height = max_y - min_y;
246 }
247
248 #[must_use]
251 pub fn contains_point(&self, point: SvgPoint) -> bool {
252 point.x > self.x
253 && point.x < self.x + self.width
254 && point.y > self.y
255 && point.y < self.y + self.height
256 }
257
258 #[must_use]
260 pub fn expand(
261 &self,
262 padding_top: f32,
263 padding_bottom: f32,
264 padding_left: f32,
265 padding_right: f32,
266 ) -> Self {
267 Self {
268 width: self.width + padding_left + padding_right,
269 height: self.height + padding_top + padding_bottom,
270 x: self.x - padding_left,
271 y: self.y - padding_top,
272 ..*self
273 }
274 }
275
276 #[must_use]
278 pub fn get_center(&self) -> SvgPoint {
279 SvgPoint {
280 x: self.x + (self.width / 2.0),
281 y: self.y + (self.height / 2.0),
282 }
283 }
284}
285
286const STEP_SIZE: usize = 20;
287const STEP_SIZE_F64: f64 = 0.05;
288
289#[allow(clippy::suboptimal_flops)]
293impl SvgCubicCurve {
294 #[inline]
296 #[must_use]
297 pub const fn new(start: SvgPoint, ctrl_1: SvgPoint, ctrl_2: SvgPoint, end: SvgPoint) -> Self {
298 Self {
299 start,
300 ctrl_1,
301 ctrl_2,
302 end,
303 }
304 }
305
306 pub const fn reverse(&mut self) {
308 core::mem::swap(&mut self.start, &mut self.end);
309 core::mem::swap(&mut self.ctrl_1, &mut self.ctrl_2);
310 }
311
312 #[must_use]
314 pub const fn get_start(&self) -> SvgPoint {
315 self.start
316 }
317 #[must_use]
319 pub const fn get_end(&self) -> SvgPoint {
320 self.end
321 }
322
323 #[must_use]
325 pub fn get_x_at_t(&self, t: f64) -> f64 {
326 let c_x = 3.0 * (f64::from(self.ctrl_1.x) - f64::from(self.start.x));
327 let b_x = 3.0 * (f64::from(self.ctrl_2.x) - f64::from(self.ctrl_1.x)) - c_x;
328 let a_x = f64::from(self.end.x) - f64::from(self.start.x) - c_x - b_x;
329
330 (a_x * t * t * t) + (b_x * t * t) + (c_x * t) + f64::from(self.start.x)
331 }
332
333 #[must_use]
335 pub fn get_y_at_t(&self, t: f64) -> f64 {
336 let c_y = 3.0 * (f64::from(self.ctrl_1.y) - f64::from(self.start.y));
337 let b_y = 3.0 * (f64::from(self.ctrl_2.y) - f64::from(self.ctrl_1.y)) - c_y;
338 let a_y = f64::from(self.end.y) - f64::from(self.start.y) - c_y - b_y;
339
340 (a_y * t * t * t) + (b_y * t * t) + (c_y * t) + f64::from(self.start.y)
341 }
342
343 #[must_use]
345 pub fn get_length(&self) -> f64 {
346 let mut arc_length = 0.0;
348 let mut prev_point = self.get_start();
349
350 for i in 0..STEP_SIZE {
351 let t_next = idx_to_f64(i + 1) * STEP_SIZE_F64;
352 let next_point = SvgPoint {
353 x: f64_to_f32(self.get_x_at_t(t_next)),
354 y: f64_to_f32(self.get_y_at_t(t_next)),
355 };
356 arc_length += prev_point.distance(next_point);
357 prev_point = next_point;
358 }
359
360 arc_length
361 }
362
363 #[must_use]
365 pub fn get_t_at_offset(&self, offset: f64) -> f64 {
366 let mut arc_length = 0.0;
370 let mut t_current = 0.0;
371 let mut prev_point = self.get_start();
372
373 for i in 0..STEP_SIZE {
374 let t_next = idx_to_f64(i + 1) * STEP_SIZE_F64;
375 let next_point = SvgPoint {
376 x: f64_to_f32(self.get_x_at_t(t_next)),
377 y: f64_to_f32(self.get_y_at_t(t_next)),
378 };
379
380 let distance = prev_point.distance(next_point);
381
382 arc_length += distance;
383
384 if arc_length > offset {
386 let remaining = arc_length - offset;
387 return t_current + ((distance - remaining) / distance) * STEP_SIZE_F64;
388 }
389
390 prev_point = next_point;
391 t_current = t_next;
392 }
393
394 t_current
395 }
396
397 #[must_use]
399 pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
400 let w0 = SvgPoint {
410 x: self.ctrl_1.x - self.start.x,
411 y: self.ctrl_1.y - self.start.y,
412 };
413
414 let w1 = SvgPoint {
415 x: self.ctrl_2.x - self.ctrl_1.x,
416 y: self.ctrl_2.y - self.ctrl_1.y,
417 };
418
419 let w2 = SvgPoint {
420 x: self.end.x - self.ctrl_2.x,
421 y: self.end.y - self.ctrl_2.y,
422 };
423
424 let quadratic_curve = SvgQuadraticCurve {
425 start: w0,
426 ctrl: w1,
427 end: w2,
428 };
429
430 let tangent_vector = SvgVector {
435 x: quadratic_curve.get_x_at_t(t),
436 y: quadratic_curve.get_y_at_t(t),
437 };
438
439 tangent_vector.normalize()
440 }
441
442 #[must_use]
444 pub fn get_bounds(&self) -> SvgRect {
445 let min_x = self
446 .start
447 .x
448 .min(self.end.x)
449 .min(self.ctrl_1.x)
450 .min(self.ctrl_2.x);
451 let max_x = self
452 .start
453 .x
454 .max(self.end.x)
455 .max(self.ctrl_1.x)
456 .max(self.ctrl_2.x);
457
458 let min_y = self
459 .start
460 .y
461 .min(self.end.y)
462 .min(self.ctrl_1.y)
463 .min(self.ctrl_2.y);
464 let max_y = self
465 .start
466 .y
467 .max(self.end.y)
468 .max(self.ctrl_1.y)
469 .max(self.ctrl_2.y);
470
471 let width = (max_x - min_x).abs();
472 let height = (max_y - min_y).abs();
473
474 SvgRect {
475 width,
476 height,
477 x: min_x,
478 y: min_y,
479 ..SvgRect::default()
480 }
481 }
482}
483
484impl SvgVector {
485 #[inline]
487 #[must_use]
488 pub fn angle_degrees(&self) -> f64 {
489 (-self.y).atan2(self.x).to_degrees()
490 }
491
492 #[inline]
494 #[must_use = "returns a new vector"]
495 pub fn normalize(&self) -> Self {
496 let tangent_length = libm::hypot(self.x, self.y);
497 if tangent_length == 0.0 {
498 return Self { x: 0.0, y: 0.0 };
499 }
500 Self {
501 x: self.x / tangent_length,
502 y: self.y / tangent_length,
503 }
504 }
505
506 #[must_use = "returns a new vector"]
508 #[inline]
509 pub fn rotate_90deg_ccw(&self) -> Self {
510 Self {
511 x: -self.y,
512 y: self.x,
513 }
514 }
515}
516
517#[allow(clippy::suboptimal_flops)]
519impl SvgQuadraticCurve {
520 #[inline]
522 #[must_use]
523 pub const fn new(start: SvgPoint, ctrl: SvgPoint, end: SvgPoint) -> Self {
524 Self { start, ctrl, end }
525 }
526
527 pub const fn reverse(&mut self) {
529 core::mem::swap(&mut self.start, &mut self.end);
530 }
531 #[must_use]
533 pub const fn get_start(&self) -> SvgPoint {
534 self.start
535 }
536 #[must_use]
538 pub const fn get_end(&self) -> SvgPoint {
539 self.end
540 }
541 #[must_use]
543 pub fn get_bounds(&self) -> SvgRect {
544 let min_x = self.start.x.min(self.end.x).min(self.ctrl.x);
545 let max_x = self.start.x.max(self.end.x).max(self.ctrl.x);
546
547 let min_y = self.start.y.min(self.end.y).min(self.ctrl.y);
548 let max_y = self.start.y.max(self.end.y).max(self.ctrl.y);
549
550 let width = (max_x - min_x).abs();
551 let height = (max_y - min_y).abs();
552
553 SvgRect {
554 width,
555 height,
556 x: min_x,
557 y: min_y,
558 ..SvgRect::default()
559 }
560 }
561
562 #[must_use]
564 pub fn get_x_at_t(&self, t: f64) -> f64 {
565 let one_minus = 1.0 - t;
566 one_minus * one_minus * f64::from(self.start.x)
567 + 2.0 * one_minus * t * f64::from(self.ctrl.x)
568 + t * t * f64::from(self.end.x)
569 }
570
571 #[must_use]
573 pub fn get_y_at_t(&self, t: f64) -> f64 {
574 let one_minus = 1.0 - t;
575 one_minus * one_minus * f64::from(self.start.y)
576 + 2.0 * one_minus * t * f64::from(self.ctrl.y)
577 + t * t * f64::from(self.end.y)
578 }
579
580 #[must_use]
582 pub fn get_length(&self) -> f64 {
583 self.to_cubic().get_length()
584 }
585
586 #[must_use]
588 pub fn get_t_at_offset(&self, offset: f64) -> f64 {
589 self.to_cubic().get_t_at_offset(offset)
590 }
591
592 #[must_use]
594 pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
595 self.to_cubic().get_tangent_vector_at_t(t)
596 }
597
598 fn to_cubic(self) -> SvgCubicCurve {
600 SvgCubicCurve {
601 start: self.start,
602 ctrl_1: SvgPoint {
603 x: self.start.x + (2.0 / 3.0) * (self.ctrl.x - self.start.x),
604 y: self.start.y + (2.0 / 3.0) * (self.ctrl.y - self.start.y),
605 },
606 ctrl_2: SvgPoint {
607 x: self.end.x + (2.0 / 3.0) * (self.ctrl.x - self.end.x),
608 y: self.end.y + (2.0 / 3.0) * (self.ctrl.y - self.end.y),
609 },
610 end: self.end,
611 }
612 }
613}
614
615impl AnimationInterpolationFunction {
616 #[must_use]
618 pub const fn get_curve(self) -> SvgCubicCurve {
619 match self {
620 Self::Ease => SvgCubicCurve {
621 start: SvgPoint { x: 0.0, y: 0.0 },
622 ctrl_1: SvgPoint { x: 0.25, y: 0.1 },
623 ctrl_2: SvgPoint { x: 0.25, y: 1.0 },
624 end: SvgPoint { x: 1.0, y: 1.0 },
625 },
626 Self::Linear => SvgCubicCurve {
627 start: SvgPoint { x: 0.0, y: 0.0 },
628 ctrl_1: SvgPoint { x: 0.0, y: 0.0 },
629 ctrl_2: SvgPoint { x: 1.0, y: 1.0 },
630 end: SvgPoint { x: 1.0, y: 1.0 },
631 },
632 Self::EaseIn => SvgCubicCurve {
633 start: SvgPoint { x: 0.0, y: 0.0 },
634 ctrl_1: SvgPoint { x: 0.42, y: 0.0 },
635 ctrl_2: SvgPoint { x: 1.0, y: 1.0 },
636 end: SvgPoint { x: 1.0, y: 1.0 },
637 },
638 Self::EaseOut => SvgCubicCurve {
639 start: SvgPoint { x: 0.0, y: 0.0 },
640 ctrl_1: SvgPoint { x: 0.0, y: 0.0 },
641 ctrl_2: SvgPoint { x: 0.58, y: 1.0 },
642 end: SvgPoint { x: 1.0, y: 1.0 },
643 },
644 Self::EaseInOut => SvgCubicCurve {
645 start: SvgPoint { x: 0.0, y: 0.0 },
646 ctrl_1: SvgPoint { x: 0.42, y: 0.0 },
647 ctrl_2: SvgPoint { x: 0.58, y: 1.0 },
648 end: SvgPoint { x: 1.0, y: 1.0 },
649 },
650 Self::CubicBezier(c) => c,
651 Self::Spring(_) => Self::EaseInOut.get_curve(),
659 }
660 }
661
662 #[must_use]
668 pub const fn is_spring(self) -> bool {
669 matches!(self, Self::Spring(_))
670 }
671
672 #[must_use]
678 pub fn evaluate(self, t: f64) -> f32 {
679 f64_to_f32(self.get_curve().get_y_at_t(t))
680 }
681}
682
683#[cfg(test)]
684#[allow(clippy::float_cmp, clippy::unreadable_literal)]
685mod autotest_generated {
686 use super::*;
687
688 fn approx(a: f64, b: f64, eps: f64) -> bool {
691 (a - b).abs() <= eps
692 }
693
694 fn approx_f32(a: f32, b: f32, eps: f32) -> bool {
695 (a - b).abs() <= eps
696 }
697
698 fn p(x: f32, y: f32) -> SvgPoint {
699 SvgPoint::new(x, y)
700 }
701
702 fn exact_curve() -> SvgCubicCurve {
705 SvgCubicCurve::new(p(0.0, 0.0), p(0.25, 0.5), p(0.75, 0.5), p(1.0, 1.0))
706 }
707
708 fn degenerate_curve() -> SvgCubicCurve {
710 SvgCubicCurve::new(p(5.0, 5.0), p(5.0, 5.0), p(5.0, 5.0), p(5.0, 5.0))
711 }
712
713 const ALL_VARIANTS: [AnimationInterpolationFunction; 5] = [
714 AnimationInterpolationFunction::Ease,
715 AnimationInterpolationFunction::Linear,
716 AnimationInterpolationFunction::EaseIn,
717 AnimationInterpolationFunction::EaseOut,
718 AnimationInterpolationFunction::EaseInOut,
719 ];
720
721 const NASTY_F64: [f64; 12] = [
723 0.0,
724 -0.0,
725 1.0,
726 -1.0,
727 2.0,
728 1e-300,
729 1e300,
730 f64::MAX,
731 f64::MIN,
732 f64::INFINITY,
733 f64::NEG_INFINITY,
734 f64::NAN,
735 ];
736
737 #[test]
740 fn idx_to_f64_zero_and_small_values_are_exact() {
741 assert_eq!(idx_to_f64(0), 0.0);
742 assert_eq!(idx_to_f64(1), 1.0);
743 assert_eq!(idx_to_f64(20), 20.0);
744 assert_eq!(idx_to_f64(STEP_SIZE), 20.0);
745 }
746
747 #[test]
748 fn idx_to_f64_is_strictly_monotonic_over_the_sampling_range() {
749 for i in 0..STEP_SIZE {
750 assert!(
751 idx_to_f64(i + 1) > idx_to_f64(i),
752 "not monotonic at i = {i}"
753 );
754 }
755 }
756
757 #[test]
758 fn idx_to_f64_at_usize_max_does_not_panic_and_stays_finite() {
759 let v = idx_to_f64(usize::MAX);
762 assert!(v.is_finite(), "usize::MAX must not become inf/NaN: {v}");
763 assert!(v > 0.0);
764 assert!(v >= idx_to_f64(STEP_SIZE));
765 }
766
767 #[test]
768 fn idx_to_f64_covers_the_full_bezier_domain() {
769 assert!(approx(idx_to_f64(STEP_SIZE) * STEP_SIZE_F64, 1.0, 1e-12));
772 }
773
774 #[test]
777 fn f64_to_f32_zero_preserves_sign() {
778 assert_eq!(f64_to_f32(0.0), 0.0_f32);
779 assert!(f64_to_f32(0.0).is_sign_positive());
780 assert!(f64_to_f32(-0.0).is_sign_negative());
781 }
782
783 #[test]
784 fn f64_to_f32_overflow_saturates_to_infinity_not_a_panic() {
785 assert_eq!(f64_to_f32(f64::MAX), f32::INFINITY);
787 assert_eq!(f64_to_f32(f64::MIN), f32::NEG_INFINITY);
788 assert_eq!(f64_to_f32(1e300), f32::INFINITY);
789 assert_eq!(f64_to_f32(-1e300), f32::NEG_INFINITY);
790 }
791
792 #[test]
793 fn f64_to_f32_underflow_flushes_to_signed_zero() {
794 let tiny = f64_to_f32(1e-300);
795 assert_eq!(tiny, 0.0_f32);
796 assert!(tiny.is_sign_positive());
797
798 let neg_tiny = f64_to_f32(-1e-300);
799 assert_eq!(neg_tiny, 0.0_f32);
800 assert!(neg_tiny.is_sign_negative(), "sign must survive underflow");
801 }
802
803 #[test]
804 fn f64_to_f32_nan_and_inf_are_defined_and_do_not_panic() {
805 assert!(f64_to_f32(f64::NAN).is_nan());
806 assert_eq!(f64_to_f32(f64::INFINITY), f32::INFINITY);
807 assert_eq!(f64_to_f32(f64::NEG_INFINITY), f32::NEG_INFINITY);
808 }
809
810 #[test]
811 fn f64_to_f32_round_trips_values_that_originate_as_f32() {
812 for original in [
814 0.0_f32,
815 1.0,
816 -1.0,
817 0.25,
818 0.1,
819 f32::MAX,
820 f32::MIN,
821 f32::MIN_POSITIVE,
822 f32::EPSILON,
823 ] {
824 assert_eq!(
825 f64_to_f32(f64::from(original)),
826 original,
827 "round-trip failed for {original}"
828 );
829 }
830 }
831
832 #[test]
835 fn svg_point_new_stores_fields_verbatim_including_extremes() {
836 for (x, y) in [
837 (0.0_f32, 0.0_f32),
838 (-1.5, 2.5),
839 (f32::MAX, f32::MIN),
840 (f32::MIN_POSITIVE, -f32::MIN_POSITIVE),
841 (f32::INFINITY, f32::NEG_INFINITY),
842 ] {
843 let pt = SvgPoint::new(x, y);
844 assert_eq!(pt.x, x);
845 assert_eq!(pt.y, y);
846 }
847
848 let nan_point = SvgPoint::new(f32::NAN, f32::NAN);
849 assert!(nan_point.x.is_nan() && nan_point.y.is_nan());
850 assert_ne!(nan_point, nan_point);
852 }
853
854 #[test]
855 fn svg_point_default_is_the_origin() {
856 assert_eq!(SvgPoint::default(), p(0.0, 0.0));
857 }
858
859 #[test]
862 fn distance_basic_values_and_identity() {
863 assert_eq!(p(0.0, 0.0).distance(p(3.0, 4.0)), 5.0);
864 assert_eq!(p(0.0, 0.0).distance(p(0.0, 0.0)), 0.0);
865 assert_eq!(p(-3.0, -4.0).distance(p(0.0, 0.0)), 5.0);
866 }
867
868 #[test]
869 fn distance_is_symmetric() {
870 let a = p(-12.5, 7.25);
871 let b = p(3.0, -9.75);
872 assert_eq!(a.distance(b), b.distance(a));
873 }
874
875 #[test]
876 fn distance_overflows_to_infinity_because_the_delta_is_computed_in_f32() {
877 let d = p(-f32::MAX, 0.0).distance(p(f32::MAX, 0.0));
880 assert!(d.is_infinite() && d > 0.0, "expected +inf, got {d}");
881 }
882
883 #[test]
884 fn distance_between_extreme_corners_never_underreports() {
885 let d = p(0.0, 0.0).distance(p(f32::MAX, f32::MAX));
888 assert!(!d.is_nan());
889 assert!(d >= f64::from(f32::MAX), "distance underreported: {d}");
890 }
891
892 #[test]
893 fn distance_with_nan_or_inf_coordinates_does_not_panic() {
894 assert!(p(0.0, 0.0).distance(p(f32::NAN, 1.0)).is_nan());
896 assert!(p(f32::NAN, f32::NAN).distance(p(0.0, 0.0)).is_nan());
897 assert!(p(0.0, 0.0).distance(p(f32::INFINITY, 0.0)).is_infinite());
898 assert!(p(0.0, 0.0)
899 .distance(p(f32::NAN, f32::INFINITY))
900 .is_infinite());
901 }
902
903 fn rect(width: f32, height: f32, x: f32, y: f32) -> SvgRect {
906 SvgRect {
907 width,
908 height,
909 x,
910 y,
911 ..SvgRect::default()
912 }
913 }
914
915 #[test]
916 fn union_with_expands_to_cover_both_rects() {
917 let mut a = rect(10.0, 10.0, 0.0, 0.0);
918 a.union_with(&rect(10.0, 10.0, 20.0, 30.0));
919 assert_eq!(a, rect(30.0, 40.0, 0.0, 0.0));
920 }
921
922 #[test]
923 fn union_with_self_is_idempotent() {
924 let mut a = rect(10.0, 20.0, -5.0, -7.0);
925 let before = a;
926 a.union_with(&before);
927 assert_eq!(a, before);
928 a.union_with(&before);
929 assert_eq!(a, before, "union must be idempotent");
930 }
931
932 #[test]
933 fn union_with_contained_rect_leaves_the_outer_rect_unchanged() {
934 let mut outer = rect(100.0, 100.0, 0.0, 0.0);
935 let before = outer;
936 outer.union_with(&rect(1.0, 1.0, 50.0, 50.0));
937 assert_eq!(outer, before);
938 }
939
940 #[test]
941 fn union_with_default_rect_always_drags_the_origin_in() {
942 let mut a = rect(5.0, 5.0, 10.0, 10.0);
945 a.union_with(&SvgRect::default());
946 assert_eq!(a, rect(15.0, 15.0, 0.0, 0.0));
947 }
948
949 #[test]
950 fn union_with_nan_rect_is_a_no_op_because_min_max_ignore_nan() {
951 let mut a = rect(10.0, 10.0, 0.0, 0.0);
954 let before = a;
955 a.union_with(&rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN));
956 assert_eq!(a, before, "NaN rect must not poison the union");
957 }
958
959 #[test]
960 fn union_with_infinite_rect_yields_infinite_extent_without_panicking() {
961 let mut a = rect(10.0, 10.0, 0.0, 0.0);
962 a.union_with(&rect(f32::INFINITY, f32::INFINITY, 0.0, 0.0));
963 assert!(a.width.is_infinite() && a.height.is_infinite());
964 assert_eq!(a.x, 0.0);
965 assert_eq!(a.y, 0.0);
966 }
967
968 #[test]
969 fn union_with_extreme_opposite_rects_does_not_panic() {
970 let mut a = rect(f32::MAX, f32::MAX, f32::MIN, f32::MIN);
971 a.union_with(&rect(f32::MAX, f32::MAX, f32::MAX, f32::MAX));
972 assert!(!a.width.is_nan());
974 assert!(!a.height.is_nan());
975 }
976
977 #[test]
980 fn contains_point_is_strictly_exclusive_on_every_edge() {
981 let r = rect(10.0, 10.0, 0.0, 0.0);
982 assert!(r.contains_point(p(5.0, 5.0)));
983 assert!(!r.contains_point(p(0.0, 0.0)));
985 assert!(!r.contains_point(p(10.0, 10.0)));
986 assert!(!r.contains_point(p(0.0, 5.0)));
987 assert!(!r.contains_point(p(10.0, 5.0)));
988 assert!(!r.contains_point(p(5.0, 0.0)));
989 assert!(!r.contains_point(p(5.0, 10.0)));
990 }
991
992 #[test]
993 fn contains_point_zero_sized_rect_contains_nothing() {
994 let r = SvgRect::default();
995 assert!(!r.contains_point(p(0.0, 0.0)));
996 assert!(!r.contains_point(p(1.0, 1.0)));
997 assert!(!r.contains_point(p(-1.0, -1.0)));
998 }
999
1000 #[test]
1001 fn contains_point_negative_size_rect_contains_nothing() {
1002 let r = rect(-10.0, -10.0, 0.0, 0.0);
1004 for pt in [p(0.0, 0.0), p(-5.0, -5.0), p(5.0, 5.0), p(-10.0, -10.0)] {
1005 assert!(!r.contains_point(pt), "{pt:?} must not be contained");
1006 }
1007 }
1008
1009 #[test]
1010 fn contains_point_negative_origin_quadrant_works() {
1011 let r = rect(10.0, 10.0, -20.0, -20.0);
1012 assert!(r.contains_point(p(-15.0, -15.0)));
1013 assert!(!r.contains_point(p(-25.0, -15.0)));
1014 assert!(!r.contains_point(p(0.0, 0.0)));
1015 }
1016
1017 #[test]
1018 fn contains_point_with_nan_coordinates_is_false_not_a_panic() {
1019 let r = rect(10.0, 10.0, 0.0, 0.0);
1020 assert!(!r.contains_point(p(f32::NAN, 5.0)));
1021 assert!(!r.contains_point(p(5.0, f32::NAN)));
1022 assert!(!r.contains_point(p(f32::NAN, f32::NAN)));
1023
1024 let nan_rect = rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN);
1026 assert!(!nan_rect.contains_point(p(0.0, 0.0)));
1027 }
1028
1029 #[test]
1030 fn contains_point_infinite_rect_contains_finite_points_but_not_infinity() {
1031 let r = rect(f32::INFINITY, f32::INFINITY, 0.0, 0.0);
1032 assert!(r.contains_point(p(1e30, 1e30)));
1033 assert!(!r.contains_point(p(f32::INFINITY, f32::INFINITY)));
1034 assert!(!r.contains_point(p(-1.0, 1.0)));
1035 }
1036
1037 #[test]
1038 fn contains_point_at_f32_extremes_does_not_panic() {
1039 let r = rect(f32::MAX, f32::MAX, f32::MIN, f32::MIN);
1040 let _ = r.contains_point(p(f32::MAX, f32::MAX));
1045 let _ = r.contains_point(p(f32::MIN, f32::MIN));
1046 assert!(!r.contains_point(p(0.0, 0.0)));
1047 }
1048
1049 #[test]
1052 fn expand_by_zero_is_the_identity() {
1053 let r = SvgRect {
1054 width: 10.0,
1055 height: 20.0,
1056 x: 1.0,
1057 y: 2.0,
1058 radius_top_left: 3.0,
1059 radius_top_right: 4.0,
1060 radius_bottom_left: 5.0,
1061 radius_bottom_right: 6.0,
1062 };
1063 assert_eq!(r.expand(0.0, 0.0, 0.0, 0.0), r);
1064 }
1065
1066 #[test]
1067 fn expand_grows_the_rect_and_preserves_the_corner_radii() {
1068 let r = SvgRect {
1069 width: 10.0,
1070 height: 10.0,
1071 x: 0.0,
1072 y: 0.0,
1073 radius_top_left: 3.0,
1074 radius_top_right: 4.0,
1075 radius_bottom_left: 5.0,
1076 radius_bottom_right: 6.0,
1077 };
1078 let e = r.expand(1.0, 2.0, 4.0, 8.0);
1079 assert_eq!(e.width, 10.0 + 4.0 + 8.0);
1080 assert_eq!(e.height, 10.0 + 1.0 + 2.0);
1081 assert_eq!(e.x, -4.0);
1082 assert_eq!(e.y, -1.0);
1083 assert_eq!(e.radius_top_left, 3.0);
1085 assert_eq!(e.radius_top_right, 4.0);
1086 assert_eq!(e.radius_bottom_left, 5.0);
1087 assert_eq!(e.radius_bottom_right, 6.0);
1088 }
1089
1090 #[test]
1091 fn expand_with_negative_padding_shrinks_and_may_invert_the_rect() {
1092 let r = rect(10.0, 10.0, 0.0, 0.0);
1093 assert_eq!(r.expand(-1.0, -1.0, -1.0, -1.0), rect(8.0, 8.0, 1.0, 1.0));
1094
1095 let inverted = r.expand(-100.0, -100.0, -100.0, -100.0);
1097 assert!(inverted.width < 0.0, "expand does not clamp to zero");
1098 assert!(!inverted.contains_point(p(5.0, 5.0)));
1099 }
1100
1101 #[test]
1102 fn expand_overflow_saturates_to_infinity_instead_of_panicking() {
1103 let r = rect(f32::MAX, f32::MAX, 0.0, 0.0);
1104 let e = r.expand(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
1105 assert!(e.width.is_infinite() && e.width > 0.0);
1107 assert!(e.height.is_infinite() && e.height > 0.0);
1108 assert_eq!(e.x, -f32::MAX);
1110 assert_eq!(e.y, -f32::MAX);
1111 assert!(e.x.is_finite() && e.y.is_finite());
1112 }
1113
1114 #[test]
1115 fn expand_with_nan_padding_poisons_the_rect_but_does_not_panic() {
1116 let r = rect(10.0, 10.0, 0.0, 0.0);
1117 let e = r.expand(f32::NAN, 0.0, 0.0, 0.0);
1118 assert!(e.height.is_nan());
1119 assert!(e.y.is_nan());
1120 assert!(!e.contains_point(p(5.0, 5.0)));
1122 }
1123
1124 #[test]
1125 fn expand_with_infinite_padding_produces_infinite_extent() {
1126 let r = rect(1.0, 1.0, 0.0, 0.0);
1127 let e = r.expand(f32::INFINITY, f32::INFINITY, f32::INFINITY, f32::INFINITY);
1128 assert!(e.width.is_infinite());
1129 assert!(e.x.is_infinite() && e.x < 0.0);
1130 }
1131
1132 #[test]
1135 fn get_center_of_a_known_rect() {
1136 assert_eq!(rect(10.0, 20.0, 2.0, 4.0).get_center(), p(7.0, 14.0));
1137 assert_eq!(rect(1.0, 1.0, 0.0, 0.0).get_center(), p(0.5, 0.5));
1138 }
1139
1140 #[test]
1141 fn get_center_of_default_rect_is_the_origin() {
1142 assert_eq!(SvgRect::default().get_center(), SvgPoint::default());
1143 }
1144
1145 #[test]
1146 fn get_center_of_a_contained_rect_is_inside_it() {
1147 let r = rect(10.0, 10.0, -3.0, 7.5);
1148 assert!(r.contains_point(r.get_center()));
1149 }
1150
1151 #[test]
1152 fn get_center_at_extremes_does_not_panic() {
1153 let inf = rect(f32::INFINITY, f32::INFINITY, 0.0, 0.0).get_center();
1154 assert!(inf.x.is_infinite() && inf.y.is_infinite());
1155
1156 let huge = rect(f32::MAX, f32::MAX, 0.0, 0.0).get_center();
1158 assert!(huge.x.is_finite() && huge.y.is_finite());
1159
1160 let nan = rect(f32::NAN, f32::NAN, 0.0, 0.0).get_center();
1161 assert!(nan.x.is_nan() && nan.y.is_nan());
1162 }
1163
1164 #[test]
1167 fn cubic_new_stores_all_four_control_points_verbatim() {
1168 let c = SvgCubicCurve::new(p(1.0, 2.0), p(3.0, 4.0), p(5.0, 6.0), p(7.0, 8.0));
1169 assert_eq!(c.start, p(1.0, 2.0));
1170 assert_eq!(c.ctrl_1, p(3.0, 4.0));
1171 assert_eq!(c.ctrl_2, p(5.0, 6.0));
1172 assert_eq!(c.end, p(7.0, 8.0));
1173 assert_eq!(c.get_start(), c.start);
1174 assert_eq!(c.get_end(), c.end);
1175 }
1176
1177 #[test]
1178 fn cubic_new_accepts_extreme_control_points() {
1179 let c = SvgCubicCurve::new(
1180 p(f32::MIN, f32::MAX),
1181 p(f32::INFINITY, f32::NEG_INFINITY),
1182 p(f32::MIN_POSITIVE, -0.0),
1183 p(0.0, 0.0),
1184 );
1185 assert!(c.get_start().x.is_finite());
1186 assert!(c.ctrl_1.x.is_infinite());
1187 assert_eq!(c.get_end(), p(0.0, 0.0));
1188 }
1189
1190 #[test]
1191 fn cubic_reverse_swaps_the_endpoints_and_the_control_points() {
1192 let mut c = SvgCubicCurve::new(p(1.0, 2.0), p(3.0, 4.0), p(5.0, 6.0), p(7.0, 8.0));
1193 c.reverse();
1194 assert_eq!(c.start, p(7.0, 8.0));
1195 assert_eq!(c.ctrl_1, p(5.0, 6.0));
1196 assert_eq!(c.ctrl_2, p(3.0, 4.0));
1197 assert_eq!(c.end, p(1.0, 2.0));
1198 }
1199
1200 #[test]
1201 fn cubic_reverse_twice_is_the_identity() {
1202 let original = exact_curve();
1203 let mut c = original;
1204 c.reverse();
1205 assert_ne!(c, original);
1206 c.reverse();
1207 assert_eq!(c, original, "reverse must be an involution");
1208 }
1209
1210 #[test]
1211 fn cubic_reverse_mirrors_the_parameterization() {
1212 let original = exact_curve();
1214 let mut reversed = original;
1215 reversed.reverse();
1216 for step in 0..=10 {
1217 let t = f64::from(step) / 10.0;
1218 assert!(approx(
1219 reversed.get_x_at_t(t),
1220 original.get_x_at_t(1.0 - t),
1221 1e-12
1222 ));
1223 assert!(approx(
1224 reversed.get_y_at_t(t),
1225 original.get_y_at_t(1.0 - t),
1226 1e-12
1227 ));
1228 }
1229 }
1230
1231 #[test]
1232 fn cubic_reverse_on_a_degenerate_curve_does_not_panic() {
1233 let mut c = degenerate_curve();
1234 c.reverse();
1235 assert_eq!(c, degenerate_curve());
1236 }
1237
1238 #[test]
1241 fn cubic_endpoints_are_hit_exactly_at_t_0_and_t_1() {
1242 let c = exact_curve();
1243 assert_eq!(c.get_x_at_t(0.0), f64::from(c.start.x));
1244 assert_eq!(c.get_y_at_t(0.0), f64::from(c.start.y));
1245 assert!(approx(c.get_x_at_t(1.0), f64::from(c.end.x), 1e-12));
1246 assert!(approx(c.get_y_at_t(1.0), f64::from(c.end.y), 1e-12));
1247 }
1248
1249 #[test]
1250 fn cubic_negative_zero_t_behaves_like_zero() {
1251 let c = exact_curve();
1252 assert_eq!(c.get_x_at_t(-0.0), c.get_x_at_t(0.0));
1253 assert_eq!(c.get_y_at_t(-0.0), c.get_y_at_t(0.0));
1254 }
1255
1256 #[test]
1257 fn cubic_stays_within_the_control_hull_for_t_in_unit_range() {
1258 let c = exact_curve();
1260 let bounds = c.get_bounds();
1261 for step in 0..=20 {
1262 let t = f64::from(step) / 20.0;
1263 let x = c.get_x_at_t(t);
1264 let y = c.get_y_at_t(t);
1265 assert!(
1266 x >= f64::from(bounds.x) - 1e-9 && x <= f64::from(bounds.x + bounds.width) + 1e-9,
1267 "x left the hull at t = {t}: {x}"
1268 );
1269 assert!(
1270 y >= f64::from(bounds.y) - 1e-9 && y <= f64::from(bounds.y + bounds.height) + 1e-9,
1271 "y left the hull at t = {t}: {y}"
1272 );
1273 }
1274 }
1275
1276 #[test]
1277 fn cubic_evaluation_extrapolates_outside_the_unit_range_without_clamping() {
1278 let c = AnimationInterpolationFunction::Linear.get_curve();
1280 assert_eq!(c.get_y_at_t(-1.0), 5.0);
1282 assert_eq!(c.get_y_at_t(2.0), -4.0);
1283 }
1284
1285 #[test]
1286 fn cubic_evaluation_at_nan_and_inf_is_defined_and_never_panics() {
1287 let c = exact_curve();
1288 assert!(c.get_x_at_t(f64::NAN).is_nan());
1289 assert!(c.get_y_at_t(f64::NAN).is_nan());
1290
1291 for t in NASTY_F64 {
1292 let x = c.get_x_at_t(t);
1293 let y = c.get_y_at_t(t);
1294 if (0.0..=1.0).contains(&t) {
1297 assert!(x.is_finite() && y.is_finite(), "finite t={t} gave {x}/{y}");
1298 }
1299 }
1300 }
1301
1302 #[test]
1303 fn cubic_evaluation_at_huge_t_overflows_instead_of_returning_a_bogus_finite() {
1304 let c = AnimationInterpolationFunction::Linear.get_curve();
1305 for t in [f64::MAX, f64::MIN, 1e300, -1e300, f64::INFINITY] {
1306 assert!(
1307 !c.get_x_at_t(t).is_finite(),
1308 "t = {t} must not produce a finite x"
1309 );
1310 assert!(!c.get_y_at_t(t).is_finite());
1311 }
1312 }
1313
1314 #[test]
1315 fn cubic_with_infinite_control_points_yields_nan_not_a_panic() {
1316 let c = SvgCubicCurve::new(p(f32::INFINITY, 0.0), p(0.0, 0.0), p(0.0, 0.0), p(1.0, 1.0));
1317 assert!(!c.get_x_at_t(0.5).is_finite());
1319 }
1320
1321 #[test]
1324 fn cubic_length_of_the_linear_timing_curve_is_the_unit_diagonal() {
1325 let len = AnimationInterpolationFunction::Linear
1327 .get_curve()
1328 .get_length();
1329 assert!(
1330 approx(len, core::f64::consts::SQRT_2, 1e-4),
1331 "expected ~sqrt(2), got {len}"
1332 );
1333 }
1334
1335 #[test]
1336 fn cubic_length_of_a_degenerate_curve_is_exactly_zero() {
1337 assert_eq!(degenerate_curve().get_length(), 0.0);
1338 }
1339
1340 #[test]
1341 fn cubic_length_is_non_negative_and_at_least_the_chord() {
1342 let c = exact_curve();
1343 let chord = c.get_start().distance(c.get_end());
1344 let len = c.get_length();
1345 assert!(len >= 0.0);
1346 assert!(
1347 len >= chord - 1e-6,
1348 "arc length {len} shorter than chord {chord}"
1349 );
1350 }
1351
1352 #[test]
1353 fn cubic_length_is_invariant_under_reverse() {
1354 let mut c = exact_curve();
1355 let forward = c.get_length();
1356 c.reverse();
1357 assert!(approx(c.get_length(), forward, 1e-5));
1358 }
1359
1360 #[test]
1361 fn cubic_length_at_extremes_does_not_panic() {
1362 let inf = SvgCubicCurve::new(
1363 p(f32::MIN, f32::MIN),
1364 p(0.0, 0.0),
1365 p(0.0, 0.0),
1366 p(f32::MAX, f32::MAX),
1367 )
1368 .get_length();
1369 assert!(!inf.is_nan());
1370 assert!(inf > 0.0);
1371
1372 let nan = SvgCubicCurve::new(p(f32::NAN, f32::NAN), p(0.0, 0.0), p(0.0, 0.0), p(1.0, 1.0))
1373 .get_length();
1374 assert!(nan.is_nan() || nan >= 0.0);
1375 }
1376
1377 #[test]
1380 fn cubic_t_at_offset_zero_is_zero() {
1381 let c = AnimationInterpolationFunction::Linear.get_curve();
1382 assert_eq!(c.get_t_at_offset(0.0), 0.0);
1383 }
1384
1385 #[test]
1386 fn cubic_t_at_half_length_is_the_midpoint_of_the_linear_curve() {
1387 let c = AnimationInterpolationFunction::Linear.get_curve();
1390 let t = c.get_t_at_offset(c.get_length() / 2.0);
1391 assert!(approx(t, 0.5, 0.06), "expected t ~ 0.5, got {t}");
1392 }
1393
1394 #[test]
1395 fn cubic_t_at_offset_is_monotonic_and_bounded_across_the_curve() {
1396 let c = exact_curve();
1397 let len = c.get_length();
1398 let mut prev = f64::NEG_INFINITY;
1399 for step in 0..=10 {
1400 let offset = len * f64::from(step) / 10.0;
1401 let t = c.get_t_at_offset(offset);
1402 assert!((-1e-9..=1.0 + 1e-9).contains(&t), "t out of range: {t}");
1403 assert!(t >= prev - 1e-9, "t went backwards: {prev} -> {t}");
1404 prev = t;
1405 }
1406 }
1407
1408 #[test]
1409 fn cubic_t_at_offset_beyond_the_curve_saturates_at_one() {
1410 let c = AnimationInterpolationFunction::Linear.get_curve();
1411 for offset in [10.0, 1e300, f64::MAX, f64::INFINITY] {
1412 let t = c.get_t_at_offset(offset);
1413 assert!(
1414 approx(t, 1.0, 1e-9),
1415 "offset {offset} should saturate at t = 1, got {t}"
1416 );
1417 }
1418 }
1419
1420 #[test]
1421 fn cubic_t_at_offset_with_nan_falls_through_to_one() {
1422 let c = AnimationInterpolationFunction::Linear.get_curve();
1425 let t = c.get_t_at_offset(f64::NAN);
1426 assert!(!t.is_nan(), "NaN offset must not leak into the result");
1427 assert!(approx(t, 1.0, 1e-9), "got {t}");
1428 }
1429
1430 #[test]
1431 fn cubic_t_at_negative_offset_extrapolates_backwards_without_clamping() {
1432 let c = AnimationInterpolationFunction::Linear.get_curve();
1434 let t = c.get_t_at_offset(-1.0);
1435 assert!(t.is_finite(), "expected a finite (negative) t, got {t}");
1436 assert!(t < 0.0, "negative offset should yield t < 0, got {t}");
1437 }
1438
1439 #[test]
1440 fn cubic_t_at_offset_on_a_degenerate_curve_divides_by_zero_but_does_not_panic() {
1441 let c = degenerate_curve();
1445 let t = c.get_t_at_offset(-1.0);
1446 assert!(
1447 t.is_infinite() && t < 0.0,
1448 "zero-length curve + negative offset should give -inf, got {t}"
1449 );
1450
1451 let t0 = c.get_t_at_offset(0.0);
1453 assert!(approx(t0, 1.0, 1e-9), "got {t0}");
1454 assert!(!t0.is_nan());
1455 }
1456
1457 #[test]
1458 fn cubic_t_at_offset_survives_every_nasty_input() {
1459 let c = exact_curve();
1460 for offset in NASTY_F64 {
1461 let t = c.get_t_at_offset(offset);
1462 if offset >= 0.0 {
1465 assert!(!t.is_nan(), "offset {offset} produced NaN");
1466 }
1467 }
1468 }
1469
1470 #[test]
1473 fn cubic_tangent_of_the_linear_curve_points_along_the_diagonal() {
1474 let c = AnimationInterpolationFunction::Linear.get_curve();
1475 let v = c.get_tangent_vector_at_t(0.5);
1476 let expected = core::f64::consts::FRAC_1_SQRT_2;
1477 assert!(approx(v.x, expected, 1e-12), "x = {}", v.x);
1478 assert!(approx(v.y, expected, 1e-12), "y = {}", v.y);
1479 }
1480
1481 #[test]
1482 fn cubic_tangent_is_a_unit_vector_or_exactly_zero() {
1483 let c = exact_curve();
1484 for step in 0..=20 {
1485 let t = f64::from(step) / 20.0;
1486 let v = c.get_tangent_vector_at_t(t);
1487 let len = libm::hypot(v.x, v.y);
1488 assert!(
1489 len == 0.0 || approx(len, 1.0, 1e-9),
1490 "tangent at t = {t} has length {len}"
1491 );
1492 }
1493 }
1494
1495 #[test]
1496 fn cubic_tangent_at_a_cusp_degenerates_to_the_zero_vector() {
1497 let c = AnimationInterpolationFunction::Linear.get_curve();
1500 for t in [0.0, 1.0] {
1501 let v = c.get_tangent_vector_at_t(t);
1502 assert_eq!(v.x, 0.0, "t = {t}");
1503 assert_eq!(v.y, 0.0, "t = {t}");
1504 }
1505 }
1506
1507 #[test]
1508 fn cubic_tangent_of_a_degenerate_curve_is_the_zero_vector() {
1509 let v = degenerate_curve().get_tangent_vector_at_t(0.5);
1510 assert_eq!(v.x, 0.0);
1511 assert_eq!(v.y, 0.0);
1512 }
1513
1514 #[test]
1515 fn cubic_tangent_at_nan_t_is_nan_not_a_panic() {
1516 let v = exact_curve().get_tangent_vector_at_t(f64::NAN);
1517 assert!(v.x.is_nan() && v.y.is_nan());
1518 }
1519
1520 #[test]
1521 fn cubic_tangent_survives_every_nasty_t() {
1522 let c = exact_curve();
1523 for t in NASTY_F64 {
1524 let v = c.get_tangent_vector_at_t(t);
1525 assert!(
1527 v.x.is_nan() || (-1.0..=1.0).contains(&v.x),
1528 "t = {t} gave x = {}",
1529 v.x
1530 );
1531 assert!(
1532 v.y.is_nan() || (-1.0..=1.0).contains(&v.y),
1533 "t = {t} gave y = {}",
1534 v.y
1535 );
1536 }
1537 }
1538
1539 #[test]
1542 fn cubic_bounds_of_a_known_curve() {
1543 let c = AnimationInterpolationFunction::Linear.get_curve();
1544 assert_eq!(c.get_bounds(), rect(1.0, 1.0, 0.0, 0.0));
1545 }
1546
1547 #[test]
1548 fn cubic_bounds_are_never_negative_and_ignore_the_radii() {
1549 let c = SvgCubicCurve::new(p(10.0, 10.0), p(-5.0, 30.0), p(0.0, -2.0), p(3.0, 3.0));
1550 let b = c.get_bounds();
1551 assert_eq!(b.x, -5.0);
1552 assert_eq!(b.y, -2.0);
1553 assert_eq!(b.width, 15.0);
1554 assert_eq!(b.height, 32.0);
1555 assert!(b.width >= 0.0 && b.height >= 0.0);
1556 assert_eq!(b.radius_top_left, 0.0);
1557 assert_eq!(b.radius_bottom_right, 0.0);
1558 }
1559
1560 #[test]
1561 fn cubic_bounds_of_a_degenerate_curve_are_a_zero_size_rect() {
1562 let b = degenerate_curve().get_bounds();
1563 assert_eq!(b, rect(0.0, 0.0, 5.0, 5.0));
1564 }
1565
1566 #[test]
1567 fn cubic_bounds_contain_every_sampled_curve_point() {
1568 let c = exact_curve();
1569 let b = c.get_bounds();
1570 for step in 1..20 {
1571 let t = f64::from(step) / 20.0;
1572 let pt = p(f64_to_f32(c.get_x_at_t(t)), f64_to_f32(c.get_y_at_t(t)));
1573 assert!(
1574 pt.x >= b.x && pt.x <= b.x + b.width,
1575 "x outside bounds at t = {t}"
1576 );
1577 assert!(
1578 pt.y >= b.y && pt.y <= b.y + b.height,
1579 "y outside bounds at t = {t}"
1580 );
1581 }
1582 }
1583
1584 #[test]
1585 fn cubic_bounds_with_infinite_points_do_not_panic() {
1586 let c = SvgCubicCurve::new(
1587 p(f32::NEG_INFINITY, 0.0),
1588 p(0.0, 0.0),
1589 p(0.0, 0.0),
1590 p(f32::INFINITY, 1.0),
1591 );
1592 let b = c.get_bounds();
1593 assert!(b.width.is_infinite());
1594 assert!(b.x.is_infinite() && b.x < 0.0);
1595 }
1596
1597 #[test]
1598 fn cubic_bounds_ignore_nan_control_points() {
1599 let c = SvgCubicCurve::new(p(0.0, 0.0), p(f32::NAN, f32::NAN), p(2.0, 4.0), p(1.0, 1.0));
1601 let b = c.get_bounds();
1602 assert!(!b.width.is_nan(), "NaN leaked into the bounds width");
1603 assert_eq!(b.x, 0.0);
1604 assert_eq!(b.width, 2.0);
1605 assert_eq!(b.height, 4.0);
1606 }
1607
1608 fn vec2(x: f64, y: f64) -> SvgVector {
1611 SvgVector { x, y }
1612 }
1613
1614 #[test]
1615 fn angle_degrees_of_the_cardinal_directions() {
1616 assert!(approx(vec2(1.0, 0.0).angle_degrees(), 0.0, 1e-12));
1618 assert!(approx(vec2(0.0, -1.0).angle_degrees(), 90.0, 1e-12));
1619 assert!(approx(vec2(0.0, 1.0).angle_degrees(), -90.0, 1e-12));
1620 assert!(approx(vec2(1.0, -1.0).angle_degrees(), 45.0, 1e-12));
1621 assert!(approx(vec2(-1.0, 0.0).angle_degrees().abs(), 180.0, 1e-12));
1622 }
1623
1624 #[test]
1625 fn angle_degrees_is_always_within_plus_minus_180() {
1626 for (x, y) in [
1627 (1.0, 2.0),
1628 (-1.0, -2.0),
1629 (1e300, -1e300),
1630 (1e-300, 1e-300),
1631 (f64::MAX, f64::MIN),
1632 ] {
1633 let a = vec2(x, y).angle_degrees();
1634 assert!(
1635 (-180.0..=180.0).contains(&a),
1636 "angle out of range for ({x}, {y}): {a}"
1637 );
1638 }
1639 }
1640
1641 #[test]
1642 fn angle_degrees_of_the_zero_vector_is_defined() {
1643 let a = vec2(0.0, 0.0).angle_degrees();
1645 assert!(!a.is_nan());
1646 assert_eq!(a, 0.0);
1647 }
1648
1649 #[test]
1650 fn angle_degrees_of_infinite_vectors_is_finite() {
1651 let a = vec2(f64::INFINITY, f64::INFINITY).angle_degrees();
1653 assert!(approx(a, -45.0, 1e-12), "got {a}");
1654 }
1655
1656 #[test]
1657 fn angle_degrees_of_nan_is_nan_not_a_panic() {
1658 assert!(vec2(f64::NAN, 1.0).angle_degrees().is_nan());
1659 assert!(vec2(1.0, f64::NAN).angle_degrees().is_nan());
1660 }
1661
1662 #[test]
1665 fn normalize_of_a_known_vector() {
1666 let v = vec2(3.0, 4.0).normalize();
1667 assert!(approx(v.x, 0.6, 1e-12));
1668 assert!(approx(v.y, 0.8, 1e-12));
1669 assert!(approx(libm::hypot(v.x, v.y), 1.0, 1e-12));
1670 }
1671
1672 #[test]
1673 fn normalize_of_the_zero_vector_returns_zero_not_nan() {
1674 let v = vec2(0.0, 0.0).normalize();
1675 assert_eq!(v.x, 0.0);
1676 assert_eq!(v.y, 0.0);
1677
1678 let v = vec2(-0.0, -0.0).normalize();
1679 assert!(!v.x.is_nan() && !v.y.is_nan());
1680 }
1681
1682 #[test]
1683 fn normalize_is_idempotent() {
1684 let once = vec2(-7.0, 24.0).normalize();
1685 let twice = once.normalize();
1686 assert!(approx(once.x, twice.x, 1e-12));
1687 assert!(approx(once.y, twice.y, 1e-12));
1688 }
1689
1690 #[test]
1691 fn normalize_of_a_tiny_vector_does_not_underflow_to_zero() {
1692 let v = vec2(f64::MIN_POSITIVE, 0.0).normalize();
1693 assert!(approx(v.x, 1.0, 1e-12), "tiny vector collapsed: {}", v.x);
1694 assert_eq!(v.y, 0.0);
1695 }
1696
1697 #[test]
1698 fn normalize_of_a_huge_vector_stays_bounded() {
1699 let v = vec2(f64::MAX, f64::MAX).normalize();
1703 assert!(!v.x.is_nan() && !v.y.is_nan());
1704 assert_eq!(v.x, v.y, "symmetry broken");
1705 let len = libm::hypot(v.x, v.y);
1706 assert!(
1707 len == 0.0 || approx(len, 1.0, 1e-9),
1708 "normalize returned a non-unit, non-zero vector of length {len}"
1709 );
1710 }
1711
1712 #[test]
1713 fn normalize_of_an_infinite_vector_yields_nan_not_a_panic() {
1714 let v = vec2(f64::INFINITY, 1.0).normalize();
1716 assert!(v.x.is_nan(), "expected NaN, got {}", v.x);
1717 assert_eq!(v.y, 0.0);
1718 }
1719
1720 #[test]
1721 fn normalize_of_a_nan_vector_is_nan_not_a_panic() {
1722 let v = vec2(f64::NAN, 0.0).normalize();
1723 assert!(v.x.is_nan());
1724 }
1725
1726 #[test]
1729 fn rotate_90deg_ccw_of_the_cardinal_directions() {
1730 let v = vec2(1.0, 0.0).rotate_90deg_ccw();
1731 assert_eq!(v.x, 0.0); assert_eq!(v.y, 1.0);
1733
1734 let v = vec2(0.0, 1.0).rotate_90deg_ccw();
1735 assert_eq!(v.x, -1.0);
1736 assert_eq!(v.y, 0.0);
1737 }
1738
1739 #[test]
1740 fn rotate_90deg_ccw_four_times_is_the_identity() {
1741 let original = vec2(1.5, -2.5);
1742 let v = original
1743 .rotate_90deg_ccw()
1744 .rotate_90deg_ccw()
1745 .rotate_90deg_ccw()
1746 .rotate_90deg_ccw();
1747 assert_eq!(v, original);
1748 }
1749
1750 #[test]
1751 fn rotate_90deg_ccw_preserves_length_and_turns_by_90_degrees() {
1752 let original = vec2(3.0, 4.0);
1753 let rotated = original.rotate_90deg_ccw();
1754 assert_eq!(
1755 libm::hypot(original.x, original.y),
1756 libm::hypot(rotated.x, rotated.y)
1757 );
1758 assert_eq!(original.x.mul_add(rotated.x, original.y * rotated.y), 0.0);
1760 }
1761
1762 #[test]
1763 fn rotate_90deg_ccw_of_extremes_does_not_panic() {
1764 let v = vec2(f64::MAX, f64::MIN).rotate_90deg_ccw();
1765 assert_eq!(v.x, f64::MAX);
1766 assert_eq!(v.y, f64::MAX);
1767
1768 let v = vec2(f64::NAN, f64::INFINITY).rotate_90deg_ccw();
1769 assert!(v.x.is_infinite() && v.x < 0.0);
1770 assert!(v.y.is_nan());
1771 }
1772
1773 fn quad() -> SvgQuadraticCurve {
1776 SvgQuadraticCurve::new(p(0.0, 0.0), p(10.0, 20.0), p(30.0, 0.0))
1777 }
1778
1779 #[test]
1780 fn quadratic_new_stores_all_three_control_points_verbatim() {
1781 let q = SvgQuadraticCurve::new(p(1.0, 2.0), p(3.0, 4.0), p(5.0, 6.0));
1782 assert_eq!(q.start, p(1.0, 2.0));
1783 assert_eq!(q.ctrl, p(3.0, 4.0));
1784 assert_eq!(q.end, p(5.0, 6.0));
1785 assert_eq!(q.get_start(), q.start);
1786 assert_eq!(q.get_end(), q.end);
1787 }
1788
1789 #[test]
1790 fn quadratic_new_accepts_extreme_control_points() {
1791 let q = SvgQuadraticCurve::new(
1792 p(f32::MAX, f32::MIN),
1793 p(f32::INFINITY, f32::NAN),
1794 p(0.0, 0.0),
1795 );
1796 assert_eq!(q.get_start().x, f32::MAX);
1797 assert!(q.ctrl.y.is_nan());
1798 assert_eq!(q.get_end(), p(0.0, 0.0));
1799 }
1800
1801 #[test]
1802 fn quadratic_reverse_swaps_only_the_endpoints() {
1803 let mut q = quad();
1804 q.reverse();
1805 assert_eq!(q.start, p(30.0, 0.0));
1806 assert_eq!(q.ctrl, p(10.0, 20.0), "ctrl must stay put");
1807 assert_eq!(q.end, p(0.0, 0.0));
1808 }
1809
1810 #[test]
1811 fn quadratic_reverse_twice_is_the_identity() {
1812 let mut q = quad();
1813 q.reverse();
1814 q.reverse();
1815 assert_eq!(q, quad(), "reverse must be an involution");
1816 }
1817
1818 #[test]
1819 fn quadratic_reverse_mirrors_the_parameterization() {
1820 let original = quad();
1821 let mut reversed = original;
1822 reversed.reverse();
1823 for step in 0..=10 {
1824 let t = f64::from(step) / 10.0;
1825 assert!(approx(
1826 reversed.get_x_at_t(t),
1827 original.get_x_at_t(1.0 - t),
1828 1e-12
1829 ));
1830 assert!(approx(
1831 reversed.get_y_at_t(t),
1832 original.get_y_at_t(1.0 - t),
1833 1e-12
1834 ));
1835 }
1836 }
1837
1838 #[test]
1839 fn quadratic_bounds_of_a_known_curve_are_the_control_hull_not_the_tight_box() {
1840 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(5.0, -10.0), p(10.0, 0.0));
1841 let b = q.get_bounds();
1842 assert_eq!(b, rect(10.0, 10.0, 0.0, -10.0));
1843
1844 assert_eq!(q.get_y_at_t(0.5), -5.0);
1847 assert!(b.contains_point(p(5.0, -5.0)));
1848 }
1849
1850 #[test]
1851 fn quadratic_bounds_of_a_degenerate_curve_are_zero_sized() {
1852 let q = SvgQuadraticCurve::new(p(2.0, 3.0), p(2.0, 3.0), p(2.0, 3.0));
1853 assert_eq!(q.get_bounds(), rect(0.0, 0.0, 2.0, 3.0));
1854 }
1855
1856 #[test]
1857 fn quadratic_bounds_ignore_nan_and_survive_infinities() {
1858 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(f32::NAN, f32::NAN), p(4.0, 8.0));
1859 let b = q.get_bounds();
1860 assert!(!b.width.is_nan());
1861 assert_eq!(b, rect(4.0, 8.0, 0.0, 0.0));
1862
1863 let q = SvgQuadraticCurve::new(p(f32::NEG_INFINITY, 0.0), p(0.0, 0.0), p(1.0, 1.0));
1864 assert!(q.get_bounds().width.is_infinite());
1865 }
1866
1867 #[test]
1870 fn quadratic_endpoints_are_hit_exactly() {
1871 let q = quad();
1872 assert_eq!(q.get_x_at_t(0.0), f64::from(q.start.x));
1873 assert_eq!(q.get_y_at_t(0.0), f64::from(q.start.y));
1874 assert_eq!(q.get_x_at_t(1.0), f64::from(q.end.x));
1875 assert_eq!(q.get_y_at_t(1.0), f64::from(q.end.y));
1876 }
1877
1878 #[test]
1879 fn quadratic_midpoint_matches_the_closed_form() {
1880 let q = quad();
1882 let expected_x =
1883 (2.0f64.mul_add(f64::from(q.ctrl.x), f64::from(q.start.x)) + f64::from(q.end.x)) / 4.0;
1884 let expected_y =
1885 (2.0f64.mul_add(f64::from(q.ctrl.y), f64::from(q.start.y)) + f64::from(q.end.y)) / 4.0;
1886 assert!(approx(q.get_x_at_t(0.5), expected_x, 1e-12));
1887 assert!(approx(q.get_y_at_t(0.5), expected_y, 1e-12));
1888 }
1889
1890 #[test]
1891 fn quadratic_extrapolates_outside_the_unit_range_without_clamping() {
1892 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(0.0, 0.0), p(1.0, 1.0));
1893 assert_eq!(q.get_x_at_t(2.0), 4.0);
1895 assert_eq!(q.get_x_at_t(-1.0), 1.0);
1896 }
1897
1898 #[test]
1899 fn quadratic_evaluation_at_nan_and_inf_never_panics() {
1900 let q = quad();
1901 assert!(q.get_x_at_t(f64::NAN).is_nan());
1902 assert!(q.get_y_at_t(f64::NAN).is_nan());
1903 for t in NASTY_F64 {
1904 let x = q.get_x_at_t(t);
1905 let y = q.get_y_at_t(t);
1906 if (0.0..=1.0).contains(&t) {
1907 assert!(x.is_finite() && y.is_finite(), "finite t={t} gave {x}/{y}");
1908 }
1909 }
1910 }
1911
1912 #[test]
1913 fn quadratic_evaluation_at_huge_t_overflows_rather_than_lying() {
1914 let q = quad();
1915 for t in [f64::MAX, f64::MIN, 1e300, f64::INFINITY, f64::NEG_INFINITY] {
1916 assert!(
1917 !q.get_x_at_t(t).is_finite(),
1918 "t = {t} must not produce a finite x"
1919 );
1920 }
1921 }
1922
1923 #[test]
1926 fn quadratic_length_of_a_straight_line_matches_the_chord() {
1927 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(1.5, 2.0), p(3.0, 4.0));
1929 assert!(approx(q.get_length(), 5.0, 1e-3), "got {}", q.get_length());
1930 }
1931
1932 #[test]
1933 fn quadratic_length_of_a_degenerate_curve_is_zero() {
1934 let q = SvgQuadraticCurve::new(p(1.0, 1.0), p(1.0, 1.0), p(1.0, 1.0));
1935 assert_eq!(q.get_length(), 0.0);
1936 }
1937
1938 #[test]
1939 fn quadratic_length_is_at_least_the_chord_and_invariant_under_reverse() {
1940 let mut q = quad();
1941 let len = q.get_length();
1942 let chord = q.get_start().distance(q.get_end());
1943 assert!(len >= chord - 1e-6, "arc {len} < chord {chord}");
1944 q.reverse();
1945 assert!(approx(q.get_length(), len, 1e-4));
1946 }
1947
1948 #[test]
1949 fn quadratic_t_at_offset_zero_is_zero_and_huge_saturates_at_one() {
1950 let q = quad();
1951 assert_eq!(q.get_t_at_offset(0.0), 0.0);
1952 assert!(approx(q.get_t_at_offset(f64::MAX), 1.0, 1e-9));
1953 assert!(approx(q.get_t_at_offset(f64::INFINITY), 1.0, 1e-9));
1954 }
1955
1956 #[test]
1957 fn quadratic_t_at_offset_with_nan_is_deterministic() {
1958 let t = quad().get_t_at_offset(f64::NAN);
1959 assert!(!t.is_nan());
1960 assert!(approx(t, 1.0, 1e-9), "got {t}");
1961 }
1962
1963 #[test]
1964 fn quadratic_t_at_offset_is_monotonic_and_bounded() {
1965 let q = quad();
1966 let len = q.get_length();
1967 let mut prev = f64::NEG_INFINITY;
1968 for step in 0..=10 {
1969 let t = q.get_t_at_offset(len * f64::from(step) / 10.0);
1970 assert!((-1e-9..=1.0 + 1e-9).contains(&t), "t out of range: {t}");
1971 assert!(t >= prev - 1e-9, "t went backwards: {prev} -> {t}");
1972 prev = t;
1973 }
1974 }
1975
1976 #[test]
1977 fn quadratic_tangent_is_unit_length_or_zero() {
1978 let q = quad();
1979 for step in 0..=20 {
1980 let t = f64::from(step) / 20.0;
1981 let v = q.get_tangent_vector_at_t(t);
1982 let len = libm::hypot(v.x, v.y);
1983 assert!(
1984 len == 0.0 || approx(len, 1.0, 1e-9),
1985 "tangent at t = {t} has length {len}"
1986 );
1987 }
1988 }
1989
1990 #[test]
1991 fn quadratic_tangent_of_a_straight_line_is_constant() {
1992 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(1.5, 2.0), p(3.0, 4.0));
1993 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
1994 let v = q.get_tangent_vector_at_t(t);
1995 assert!(approx(v.x, 0.6, 1e-6), "t = {t}: x = {}", v.x);
1996 assert!(approx(v.y, 0.8, 1e-6), "t = {t}: y = {}", v.y);
1997 }
1998 }
1999
2000 #[test]
2001 fn quadratic_tangent_at_nan_t_is_nan_not_a_panic() {
2002 let v = quad().get_tangent_vector_at_t(f64::NAN);
2003 assert!(v.x.is_nan() && v.y.is_nan());
2004 }
2005
2006 #[test]
2009 fn to_cubic_preserves_the_endpoints() {
2010 let q = quad();
2011 let c = q.to_cubic();
2012 assert_eq!(c.start, q.start);
2013 assert_eq!(c.end, q.end);
2014 }
2015
2016 #[test]
2017 fn to_cubic_produces_an_equivalent_curve() {
2018 let q = quad();
2021 let c = q.to_cubic();
2022 for step in 0..=20 {
2023 let t = f64::from(step) / 20.0;
2024 assert!(
2025 approx(c.get_x_at_t(t), q.get_x_at_t(t), 1e-4),
2026 "x mismatch at t = {t}: {} vs {}",
2027 c.get_x_at_t(t),
2028 q.get_x_at_t(t)
2029 );
2030 assert!(
2031 approx(c.get_y_at_t(t), q.get_y_at_t(t), 1e-4),
2032 "y mismatch at t = {t}: {} vs {}",
2033 c.get_y_at_t(t),
2034 q.get_y_at_t(t)
2035 );
2036 }
2037 }
2038
2039 #[test]
2040 fn to_cubic_of_a_degenerate_curve_is_degenerate() {
2041 let q = SvgQuadraticCurve::new(p(7.0, 7.0), p(7.0, 7.0), p(7.0, 7.0));
2042 let c = q.to_cubic();
2043 assert_eq!(c.start, p(7.0, 7.0));
2044 assert_eq!(c.ctrl_1, p(7.0, 7.0));
2045 assert_eq!(c.ctrl_2, p(7.0, 7.0));
2046 assert_eq!(c.end, p(7.0, 7.0));
2047 assert_eq!(c.get_length(), 0.0);
2048 }
2049
2050 #[test]
2051 fn to_cubic_with_extreme_points_does_not_panic() {
2052 let q = SvgQuadraticCurve::new(p(f32::MIN, 0.0), p(f32::MAX, 0.0), p(0.0, 0.0));
2053 let c = q.to_cubic();
2054 assert!(
2058 c.ctrl_1.x.is_infinite() && c.ctrl_1.x > 0.0,
2059 "expected +inf, got {}",
2060 c.ctrl_1.x
2061 );
2062 assert!(c.ctrl_2.x.is_finite() && c.ctrl_2.x > 0.0);
2064 assert_eq!(c.start, p(f32::MIN, 0.0));
2065 assert_eq!(c.end, p(0.0, 0.0));
2066
2067 let q = SvgQuadraticCurve::new(p(f32::NAN, 0.0), p(0.0, 0.0), p(1.0, 1.0));
2068 assert!(q.to_cubic().ctrl_1.x.is_nan());
2069 }
2070
2071 #[test]
2074 fn get_curve_round_trips_a_custom_cubic_bezier() {
2075 let custom = SvgCubicCurve::new(p(0.0, 0.0), p(0.1, 0.9), p(0.9, 0.1), p(1.0, 1.0));
2077 assert_eq!(
2078 AnimationInterpolationFunction::CubicBezier(custom).get_curve(),
2079 custom
2080 );
2081 }
2082
2083 #[test]
2084 fn get_curve_round_trips_even_a_nonsensical_cubic_bezier() {
2085 let nasty = SvgCubicCurve::new(
2086 p(f32::NAN, f32::INFINITY),
2087 p(f32::MAX, f32::MIN),
2088 p(-0.0, 0.0),
2089 p(1e30, -1e30),
2090 );
2091 let out = AnimationInterpolationFunction::CubicBezier(nasty).get_curve();
2092 assert!(out.start.x.is_nan());
2094 assert!(out.start.y.is_infinite());
2095 assert_eq!(out.ctrl_1, nasty.ctrl_1);
2096 assert_eq!(out.end, nasty.end);
2097 }
2098
2099 #[test]
2100 fn every_builtin_timing_curve_runs_from_0_0_to_1_1() {
2101 for f in ALL_VARIANTS {
2102 let c = f.get_curve();
2103 assert_eq!(c.get_start(), p(0.0, 0.0), "{f:?} does not start at (0,0)");
2104 assert_eq!(c.get_end(), p(1.0, 1.0), "{f:?} does not end at (1,1)");
2105 }
2106 }
2107
2108 #[test]
2109 fn every_builtin_timing_curve_keeps_its_control_points_in_the_unit_box() {
2110 for f in ALL_VARIANTS {
2112 let c = f.get_curve();
2113 for ctrl in [c.ctrl_1, c.ctrl_2] {
2114 assert!(
2115 (0.0..=1.0).contains(&ctrl.x),
2116 "{f:?} has an out-of-range ctrl x: {}",
2117 ctrl.x
2118 );
2119 assert!((0.0..=1.0).contains(&ctrl.y), "{f:?}: {}", ctrl.y);
2120 }
2121 }
2122 }
2123
2124 #[test]
2127 fn evaluate_at_the_endpoints_is_exactly_0_and_1() {
2128 for f in ALL_VARIANTS {
2129 assert_eq!(f.evaluate(0.0), 0.0, "{f:?} at t = 0");
2130 assert!(
2131 approx_f32(f.evaluate(1.0), 1.0, 1e-6),
2132 "{f:?} at t = 1: {}",
2133 f.evaluate(1.0)
2134 );
2135 }
2136 }
2137
2138 #[test]
2139 fn evaluate_is_monotonically_non_decreasing_on_the_unit_interval() {
2140 for f in ALL_VARIANTS {
2141 let mut prev = f32::NEG_INFINITY;
2142 for step in 0..=100 {
2143 let t = f64::from(step) / 100.0;
2144 let v = f.evaluate(t);
2145 assert!(v >= prev - 1e-6, "{f:?} went backwards at t = {t}");
2146 prev = v;
2147 }
2148 }
2149 }
2150
2151 #[test]
2152 fn evaluate_stays_within_0_1_on_the_unit_interval() {
2153 for f in ALL_VARIANTS {
2154 for step in 0..=100 {
2155 let t = f64::from(step) / 100.0;
2156 let v = f.evaluate(t);
2157 assert!(
2158 (-1e-6..=1.0 + 1e-6).contains(&v),
2159 "{f:?} left [0,1] at t = {t}: {v}"
2160 );
2161 }
2162 }
2163 }
2164
2165 #[test]
2166 fn evaluate_samples_the_curve_by_parameter_t_not_by_progress_x() {
2167 let linear = AnimationInterpolationFunction::Linear;
2172 assert_eq!(linear.evaluate(0.5), 0.5);
2173 assert_eq!(linear.evaluate(0.25), 0.15625);
2174 assert_eq!(linear.evaluate(0.75), 0.84375);
2175 assert!(
2176 linear.evaluate(0.25) != 0.25,
2177 "if this ever becomes 0.25, evaluate() started doing the x-inversion"
2178 );
2179 }
2180
2181 #[test]
2182 fn evaluate_cannot_distinguish_four_of_the_five_timing_functions() {
2183 let same = [
2188 AnimationInterpolationFunction::Linear,
2189 AnimationInterpolationFunction::EaseIn,
2190 AnimationInterpolationFunction::EaseOut,
2191 AnimationInterpolationFunction::EaseInOut,
2192 ];
2193 for step in 0..=10 {
2194 let t = f64::from(step) / 10.0;
2195 let reference = same[0].evaluate(t);
2196 for f in same {
2197 assert_eq!(f.evaluate(t), reference, "{f:?} vs Linear at t = {t}");
2198 }
2199 }
2200 assert!(
2201 AnimationInterpolationFunction::Ease.evaluate(0.5)
2202 != AnimationInterpolationFunction::Linear.evaluate(0.5),
2203 "Ease must at least differ from Linear"
2204 );
2205 }
2206
2207 #[test]
2208 fn evaluate_outside_the_unit_interval_extrapolates_without_clamping() {
2209 let linear = AnimationInterpolationFunction::Linear;
2211 assert_eq!(linear.evaluate(-1.0), 5.0);
2212 assert_eq!(linear.evaluate(2.0), -4.0);
2213 }
2214
2215 #[test]
2216 fn evaluate_at_nan_is_nan_for_every_variant() {
2217 for f in ALL_VARIANTS {
2218 assert!(f.evaluate(f64::NAN).is_nan(), "{f:?}");
2219 }
2220 }
2221
2222 #[test]
2223 fn evaluate_at_extreme_t_never_panics_and_never_lies() {
2224 for f in ALL_VARIANTS {
2225 for t in [
2226 f64::MAX,
2227 f64::MIN,
2228 1e300,
2229 -1e300,
2230 f64::INFINITY,
2231 f64::NEG_INFINITY,
2232 ] {
2233 let v = f.evaluate(t);
2234 assert!(
2235 !v.is_finite(),
2236 "{f:?} at t = {t} returned a plausible-looking {v}"
2237 );
2238 }
2239 }
2240 }
2241
2242 #[test]
2243 fn evaluate_of_a_nan_cubic_bezier_is_nan_not_a_panic() {
2244 let f = AnimationInterpolationFunction::CubicBezier(SvgCubicCurve::new(
2245 p(0.0, f32::NAN),
2246 p(0.0, 0.0),
2247 p(1.0, 1.0),
2248 p(1.0, 1.0),
2249 ));
2250 assert!(f.evaluate(0.5).is_nan());
2251 }
2252
2253 #[test]
2254 fn evaluate_of_a_huge_cubic_bezier_stays_in_f32_range_inside_the_unit_interval() {
2255 let f = AnimationInterpolationFunction::CubicBezier(SvgCubicCurve::new(
2258 p(0.0, 0.0),
2259 p(0.0, f32::MAX),
2260 p(1.0, f32::MAX),
2261 p(1.0, f32::MAX),
2262 ));
2263 for step in 0..=10 {
2264 let v = f.evaluate(f64::from(step) / 10.0);
2265 assert!(v.is_finite(), "overflowed inside [0,1] at step {step}: {v}");
2266 assert!((0.0..=f32::MAX).contains(&v));
2267 }
2268 }
2269
2270 #[test]
2271 fn evaluate_of_a_huge_cubic_bezier_saturates_to_infinity_when_extrapolated() {
2272 let f = AnimationInterpolationFunction::CubicBezier(SvgCubicCurve::new(
2276 p(0.0, 0.0),
2277 p(0.0, f32::MAX),
2278 p(1.0, f32::MAX),
2279 p(1.0, f32::MAX),
2280 ));
2281 let v = f.evaluate(3.0);
2282 assert!(v.is_infinite() && v > 0.0, "expected +inf, got {v}");
2283 }
2284
2285 #[test]
2288 fn style_animation_shorthand_parses_delay_iterations_and_lists() {
2289 let a = parse_style_animation("spin 1s linear infinite").unwrap();
2290 assert_eq!(a.name.as_str(), "spin");
2291 assert_eq!(a.duration.millis(), 1000);
2292 assert_eq!(a.delay.millis(), 0);
2293 assert_eq!(a.iterations, AnimationIterationCount::Infinite);
2294 assert_eq!(a.timing, AnimationTiming::Linear);
2295
2296 let b = parse_style_animation("all 2s 500ms").unwrap();
2298 assert_eq!(b.duration.millis(), 2000);
2299 assert_eq!(b.delay.millis(), 500);
2300 assert_eq!(b.iterations, AnimationIterationCount::Count(1));
2301
2302 let c = parse_style_animation("bounce 300ms ease-out 3").unwrap();
2303 assert_eq!(c.iterations, AnimationIterationCount::Count(3));
2304
2305 let v = parse_style_animation_vec("width 1s linear, color 2s ease-out").unwrap();
2307 let v = v.as_ref();
2308 assert_eq!(v.len(), 2);
2309 assert_eq!(v[0].name.as_str(), "width");
2310 assert_eq!(v[0].duration.millis(), 1000);
2311 assert_eq!(v[1].name.as_str(), "color");
2312 assert_eq!(v[1].duration.millis(), 2000);
2313 assert_eq!(v[1].timing, AnimationTiming::EaseOut);
2314
2315 let n = parse_style_animation("slideOut 1s no-clip").unwrap();
2318 assert!(!n.clip);
2319 assert!(
2320 parse_style_animation("slideOut 1s").unwrap().clip,
2321 "default clipped"
2322 );
2323
2324 let cb = parse_style_animation("swoosh 1s cubic-bezier(0.4, 0, 0.2, 1)").unwrap();
2328 match cb.timing {
2329 AnimationTiming::CubicBezier(b) => {
2330 assert_eq!((b.x1, b.y1, b.x2, b.y2), (400, 0, 200, 1000));
2331 }
2332 other => panic!("expected a bezier, got {other:?}"),
2333 }
2334 assert!(cb.timing.evaluate(0.0).abs() < 1e-3);
2336 assert!((cb.timing.evaluate(1.0) - 1.0).abs() < 1e-3);
2337 assert!(parse_style_animation("bad 1s cubic-bezier(1.5, 0, 0.2, 1)").is_err());
2339
2340 assert!(parse_style_animation("foo bar 1s").is_err());
2342 assert!(parse_style_animation_vec("").is_err());
2343 }
2344
2345 #[test]
2346 fn evaluate_of_a_degenerate_flat_bezier_is_constant_zero() {
2347 let f = AnimationInterpolationFunction::CubicBezier(SvgCubicCurve::new(
2348 p(0.0, 0.0),
2349 p(0.0, 0.0),
2350 p(1.0, 0.0),
2351 p(1.0, 0.0),
2352 ));
2353 for step in 0..=10 {
2354 let t = f64::from(step) / 10.0;
2355 assert_eq!(f.evaluate(t), 0.0, "t = {t}");
2356 }
2357 }
2358
2359 #[test]
2362 fn option_svg_point_round_trips_through_std_option() {
2363 let pt = p(1.5, -2.5);
2364
2365 let some: OptionSvgPoint = Some(pt).into();
2366 assert!(some.is_some());
2367 assert!(!some.is_none());
2368 assert_eq!(some.as_ref(), Some(&pt));
2369 assert_eq!(Option::<SvgPoint>::from(some), Some(pt));
2370
2371 let none: OptionSvgPoint = OptionSvgPoint::None;
2372 assert!(none.is_none());
2373 assert_eq!(none.as_ref(), None);
2374 assert_eq!(Option::<SvgPoint>::from(none), None);
2375
2376 assert!(OptionSvgPoint::default().is_none());
2377 }
2378
2379 #[test]
2380 fn option_svg_point_replace_returns_the_previous_value() {
2381 let mut o = OptionSvgPoint::None;
2382 let prev = o.replace(p(1.0, 2.0));
2383 assert!(prev.is_none());
2384 assert!(o.is_some());
2385
2386 let prev = o.replace(p(3.0, 4.0));
2387 assert_eq!(prev.as_ref(), Some(&p(1.0, 2.0)));
2388 assert_eq!(o.as_ref(), Some(&p(3.0, 4.0)));
2389 }
2390
2391 #[test]
2394 fn interpolate_resolver_stores_its_fields_verbatim() {
2395 let r = InterpolateResolver {
2396 interpolate_func: AnimationInterpolationFunction::EaseInOut,
2397 parent_rect_width: 100.0,
2398 parent_rect_height: f32::NAN,
2399 current_rect_width: f32::INFINITY,
2400 current_rect_height: -0.0,
2401 };
2402 assert_eq!(
2403 r.interpolate_func,
2404 AnimationInterpolationFunction::EaseInOut
2405 );
2406 assert_eq!(r.parent_rect_width, 100.0);
2407 assert!(r.parent_rect_height.is_nan());
2408 assert!(r.current_rect_width.is_infinite());
2409 assert!(r.current_rect_height.is_sign_negative());
2410 assert_ne!(r, r);
2412 }
2413}
2414
2415#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2434#[repr(C)]
2435pub struct AnimationTimingBezier {
2436 pub x1: u16,
2437 pub y1: i16,
2438 pub x2: u16,
2439 pub y2: i16,
2440}
2441
2442#[allow(variant_size_differences)]
2454#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2455#[repr(C, u8)]
2456pub enum AnimationTiming {
2457 #[default]
2458 Ease,
2459 Linear,
2460 EaseIn,
2461 EaseOut,
2462 EaseInOut,
2463 Spring,
2465 SpringGentle,
2466 SpringSnappy,
2467 CubicBezier(AnimationTimingBezier),
2471}
2472
2473impl AnimationTiming {
2474 #[must_use]
2476 pub fn to_interpolation(self) -> AnimationInterpolationFunction {
2477 match self {
2478 Self::Ease => AnimationInterpolationFunction::Ease,
2479 Self::Linear => AnimationInterpolationFunction::Linear,
2480 Self::EaseIn => AnimationInterpolationFunction::EaseIn,
2481 Self::EaseOut => AnimationInterpolationFunction::EaseOut,
2482 Self::EaseInOut => AnimationInterpolationFunction::EaseInOut,
2483 Self::Spring => AnimationInterpolationFunction::Spring(SpringCurve::SMOOTH),
2484 Self::SpringGentle => AnimationInterpolationFunction::Spring(SpringCurve::GENTLE),
2485 Self::SpringSnappy => AnimationInterpolationFunction::Spring(SpringCurve::SNAPPY),
2486 Self::CubicBezier(b) => AnimationInterpolationFunction::CubicBezier(SvgCubicCurve {
2487 start: SvgPoint { x: 0.0, y: 0.0 },
2488 ctrl_1: SvgPoint {
2489 x: f32::from(b.x1) / 1000.0,
2490 y: f32::from(b.y1) / 1000.0,
2491 },
2492 ctrl_2: SvgPoint {
2493 x: f32::from(b.x2) / 1000.0,
2494 y: f32::from(b.y2) / 1000.0,
2495 },
2496 end: SvgPoint { x: 1.0, y: 1.0 },
2497 }),
2498 }
2499 }
2500
2501 #[must_use]
2504 pub fn evaluate(self, t: f32) -> f32 {
2505 self.to_interpolation().evaluate(f64::from(t))
2506 }
2507
2508 #[must_use]
2509 pub fn as_css_string(self) -> String {
2510 use alloc::string::ToString;
2511 match self {
2512 Self::Ease => "ease".to_string(),
2513 Self::Linear => "linear".to_string(),
2514 Self::EaseIn => "ease-in".to_string(),
2515 Self::EaseOut => "ease-out".to_string(),
2516 Self::EaseInOut => "ease-in-out".to_string(),
2517 Self::Spring => "spring".to_string(),
2518 Self::SpringGentle => "spring-gentle".to_string(),
2519 Self::SpringSnappy => "spring-snappy".to_string(),
2520 Self::CubicBezier(b) => alloc::format!(
2521 "cubic-bezier({}, {}, {}, {})",
2522 f32::from(b.x1) / 1000.0,
2523 f32::from(b.y1) / 1000.0,
2524 f32::from(b.x2) / 1000.0,
2525 f32::from(b.y2) / 1000.0,
2526 ),
2527 }
2528 }
2529
2530 #[must_use]
2531 pub fn from_css_str(s: &str) -> Option<Self> {
2532 if let Some(inner) = s
2533 .strip_prefix("cubic-bezier(")
2534 .and_then(|r| r.strip_suffix(')'))
2535 {
2536 let mut nums = inner.split(',').map(str::trim);
2537 let x1: f32 = nums.next()?.parse().ok()?;
2538 let y1: f32 = nums.next()?.parse().ok()?;
2539 let x2: f32 = nums.next()?.parse().ok()?;
2540 let y2: f32 = nums.next()?.parse().ok()?;
2541 if nums.next().is_some() {
2542 return None;
2543 }
2544 if !(0.0..=1.0).contains(&x1) || !(0.0..=1.0).contains(&x2) {
2547 return None;
2548 }
2549 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2550 return Some(Self::CubicBezier(AnimationTimingBezier {
2552 x1: (x1 * 1000.0).round() as u16,
2553 y1: (y1 * 1000.0).round().clamp(-32767.0, 32767.0) as i16,
2554 x2: (x2 * 1000.0).round() as u16,
2555 y2: (y2 * 1000.0).round().clamp(-32767.0, 32767.0) as i16,
2556 }));
2557 }
2558 Some(match s {
2559 "ease" => Self::Ease,
2560 "linear" => Self::Linear,
2561 "ease-in" => Self::EaseIn,
2562 "ease-out" => Self::EaseOut,
2563 "ease-in-out" => Self::EaseInOut,
2564 "spring" => Self::Spring,
2565 "spring-gentle" => Self::SpringGentle,
2566 "spring-snappy" => Self::SpringSnappy,
2567 _ => return None,
2568 })
2569 }
2570}
2571
2572#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2576#[repr(C, u8)]
2577pub enum AnimationIterationCount {
2578 Count(u16),
2579 Infinite,
2580}
2581
2582impl Default for AnimationIterationCount {
2583 fn default() -> Self {
2584 Self::Count(1)
2585 }
2586}
2587
2588#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2594#[repr(C)]
2595pub struct StyleAnimation {
2596 pub name: crate::AzString,
2600 pub duration: crate::props::basic::time::CssDuration,
2603 pub delay: crate::props::basic::time::CssDuration,
2607 pub iterations: AnimationIterationCount,
2609 pub timing: AnimationTiming,
2611 pub clip: bool,
2618}
2619
2620impl Default for StyleAnimation {
2621 fn default() -> Self {
2622 Self {
2623 name: crate::AzString::from_const_str(""),
2624 duration: crate::props::basic::time::CssDuration::from_millis(0),
2625 delay: crate::props::basic::time::CssDuration::from_millis(0),
2626 iterations: AnimationIterationCount::Count(1),
2627 timing: AnimationTiming::Ease,
2628 clip: true,
2629 }
2630 }
2631}
2632
2633impl crate::css::PrintAsCssValue for StyleAnimation {
2634 fn print_as_css_value(&self) -> String {
2635 use alloc::string::ToString;
2636 let mut out = alloc::format!(
2637 "{} {}",
2638 self.name.as_str(),
2639 self.duration.print_as_css_value(),
2640 );
2641 if self.delay.millis() != 0 {
2642 out.push(' ');
2643 out.push_str(&self.delay.print_as_css_value());
2644 }
2645 match self.iterations {
2646 AnimationIterationCount::Count(1) => {}
2647 AnimationIterationCount::Count(n) => {
2648 use core::fmt::Write;
2649 let _ = write!(out, " {n}");
2650 }
2651 AnimationIterationCount::Infinite => out.push_str(" infinite"),
2652 }
2653 out.push(' ');
2654 out.push_str(&self.timing.as_css_string());
2655 if !self.clip {
2656 out.push_str(" no-clip");
2657 }
2658 out.trim().to_string()
2659 }
2660}
2661
2662#[derive(Debug, Clone, PartialEq, Eq)]
2663pub enum StyleAnimationParseError<'a> {
2664 Empty(&'a str),
2666 Duration(crate::props::basic::time::DurationParseError<'a>),
2668}
2669
2670impl core::fmt::Display for StyleAnimationParseError<'_> {
2671 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2672 match self {
2673 Self::Empty(s) => write!(f, "Invalid animation value: \"{s}\""),
2674 Self::Duration(e) => write!(f, "Invalid animation duration: {e}"),
2675 }
2676 }
2677}
2678
2679#[derive(Debug, Clone, PartialEq, Eq)]
2680#[repr(C, u8)]
2681pub enum StyleAnimationParseErrorOwned {
2682 Empty(crate::AzString),
2686 Duration(crate::props::basic::time::DurationParseErrorOwned),
2687}
2688
2689impl StyleAnimationParseError<'_> {
2690 #[must_use]
2691 pub fn to_contained(&self) -> StyleAnimationParseErrorOwned {
2692 match self {
2693 Self::Empty(s) => StyleAnimationParseErrorOwned::Empty((*s).into()),
2694 Self::Duration(e) => StyleAnimationParseErrorOwned::Duration(e.to_contained()),
2695 }
2696 }
2697}
2698
2699impl StyleAnimationParseErrorOwned {
2700 #[must_use]
2701 pub fn to_shared(&self) -> StyleAnimationParseError<'_> {
2702 match self {
2703 Self::Empty(s) => StyleAnimationParseError::Empty(s.as_str()),
2704 Self::Duration(e) => StyleAnimationParseError::Duration(e.to_shared()),
2705 }
2706 }
2707}
2708
2709pub fn parse_style_animation(input: &str) -> Result<StyleAnimation, StyleAnimationParseError<'_>> {
2720 let mut name: Option<&str> = None;
2724 let mut duration: Option<crate::props::basic::time::CssDuration> = None;
2725 let mut delay: Option<crate::props::basic::time::CssDuration> = None;
2726 let mut timing: Option<AnimationTiming> = None;
2727 let mut iterations: Option<AnimationIterationCount> = None;
2728 let mut clip: Option<bool> = None;
2729 let mut tokens: Vec<&str> = Vec::new();
2732 {
2733 let bytes = input.as_bytes();
2734 let mut depth = 0usize;
2735 let mut start: Option<usize> = None;
2736 for (i, b) in bytes.iter().enumerate() {
2737 match b {
2738 b'(' => depth += 1,
2739 b')' => depth = depth.saturating_sub(1),
2740 b' ' | b'\t' | b'\n' | b'\r' if depth == 0 => {
2741 if let Some(st) = start.take() {
2742 tokens.push(&input[st..i]);
2743 }
2744 continue;
2745 }
2746 _ => {}
2747 }
2748 if start.is_none() {
2749 start = Some(i);
2750 }
2751 }
2752 if let Some(st) = start {
2753 tokens.push(&input[st..]);
2754 }
2755 }
2756 for tok in tokens {
2757 if let Ok(d) = crate::props::basic::time::parse_duration(tok) {
2758 if duration.is_none() {
2759 duration = Some(d);
2760 } else if delay.is_none() {
2761 delay = Some(d);
2762 } else {
2763 return Err(StyleAnimationParseError::Empty(input));
2764 }
2765 } else if let Some(t) = AnimationTiming::from_css_str(tok) {
2766 if timing.replace(t).is_some() {
2767 return Err(StyleAnimationParseError::Empty(input));
2768 }
2769 } else if tok.eq_ignore_ascii_case("no-clip") {
2770 if clip.replace(false).is_some() {
2771 return Err(StyleAnimationParseError::Empty(input));
2772 }
2773 } else if tok.eq_ignore_ascii_case("infinite") {
2774 if iterations
2775 .replace(AnimationIterationCount::Infinite)
2776 .is_some()
2777 {
2778 return Err(StyleAnimationParseError::Empty(input));
2779 }
2780 } else if let Ok(n) = tok.parse::<u16>() {
2781 if iterations
2782 .replace(AnimationIterationCount::Count(n))
2783 .is_some()
2784 {
2785 return Err(StyleAnimationParseError::Empty(input));
2786 }
2787 } else if name.replace(tok).is_some() {
2788 return Err(StyleAnimationParseError::Empty(input));
2790 }
2791 }
2792 let name = name.ok_or(StyleAnimationParseError::Empty(input))?;
2793 Ok(StyleAnimation {
2794 name: name.to_string().into(),
2795 duration: duration.unwrap_or(crate::props::basic::time::CssDuration::from_millis(0)),
2796 delay: delay.unwrap_or(crate::props::basic::time::CssDuration::from_millis(0)),
2797 iterations: iterations.unwrap_or_default(),
2798 timing: timing.unwrap_or(AnimationTiming::Ease),
2799 clip: clip.unwrap_or(true),
2800 })
2801}
2802
2803pub fn parse_style_animation_vec(
2812 input: &str,
2813) -> Result<StyleAnimationVec, StyleAnimationParseError<'_>> {
2814 let mut out = Vec::new();
2815 for seg in input.split(',') {
2816 let seg = seg.trim();
2817 if seg.is_empty() {
2818 continue;
2819 }
2820 out.push(parse_style_animation(seg)?);
2821 }
2822 if out.is_empty() {
2823 return Err(StyleAnimationParseError::Empty(input));
2824 }
2825 Ok(out.into())
2826}
2827
2828crate::impl_vec!(
2829 StyleAnimation,
2830 StyleAnimationVec,
2831 StyleAnimationVecDestructor,
2832 StyleAnimationVecDestructorType,
2833 StyleAnimationVecSlice,
2834 OptionStyleAnimation
2835);
2836crate::impl_vec_debug!(StyleAnimation, StyleAnimationVec);
2837crate::impl_vec_clone!(
2838 StyleAnimation,
2839 StyleAnimationVec,
2840 StyleAnimationVecDestructor
2841);
2842crate::impl_vec_partialeq!(StyleAnimation, StyleAnimationVec);
2843crate::impl_vec_eq!(StyleAnimation, StyleAnimationVec);
2844crate::impl_vec_hash!(StyleAnimation, StyleAnimationVec);
2845crate::impl_vec_partialord!(StyleAnimation, StyleAnimationVec);
2846crate::impl_vec_ord!(StyleAnimation, StyleAnimationVec);
2847crate::impl_option!(
2848 StyleAnimation,
2849 OptionStyleAnimation,
2850 copy = false,
2851 [Debug, Clone, PartialEq, Eq]
2852);
2853
2854impl crate::css::PrintAsCssValue for StyleAnimationVec {
2855 fn print_as_css_value(&self) -> String {
2856 self.as_ref()
2857 .iter()
2858 .map(crate::css::PrintAsCssValue::print_as_css_value)
2859 .collect::<Vec<_>>()
2860 .join(", ")
2861 }
2862}
2863
2864impl crate::codegen::format::FormatAsRustCode for StyleAnimationVec {
2865 fn format_as_rust_code(&self, tabs: usize) -> String {
2866 use crate::codegen::format::FormatAsRustCode as _;
2867 alloc::format!(
2868 "StyleAnimationVec::from_const_slice(&[{}])",
2869 self.as_ref()
2870 .iter()
2871 .map(|a| a.format_as_rust_code(tabs))
2872 .collect::<Vec<_>>()
2873 .join(", ")
2874 )
2875 }
2876}
2877
2878impl crate::codegen::format::FormatAsRustCode for StyleAnimation {
2879 fn format_as_rust_code(&self, _tabs: usize) -> String {
2880 use crate::codegen::format::FormatAsRustCode as _;
2881 alloc::format!(
2882 "StyleAnimation {{ name: AzString::from_const_str({:?}), duration: {}, delay: {}, iterations: AnimationIterationCount::{:?}, timing: AnimationTiming::{:?}, clip: {} }}",
2883 self.name.as_str(),
2884 self.duration.format_as_rust_code(0),
2885 self.delay.format_as_rust_code(0),
2886 self.iterations,
2887 self.timing,
2888 self.clip
2889 )
2890 }
2891}