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#[allow(variant_size_differences)] #[derive(Debug, Copy, Clone, PartialEq)]
53#[repr(C, u8)]
54pub enum AnimationInterpolationFunction {
55 Ease,
56 Linear,
57 EaseIn,
58 EaseOut,
59 EaseInOut,
60 CubicBezier(SvgCubicCurve),
61}
62
63#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
65#[repr(C)]
66pub struct SvgRect {
67 pub width: f32,
68 pub height: f32,
69 pub x: f32,
70 pub y: f32,
71 pub radius_top_left: f32,
72 pub radius_top_right: f32,
73 pub radius_bottom_left: f32,
74 pub radius_bottom_right: f32,
75}
76
77#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
79#[repr(C)]
80pub struct SvgVector {
81 pub x: f64,
82 pub y: f64,
83}
84
85#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
87#[repr(C)]
88pub struct SvgQuadraticCurve {
89 pub start: SvgPoint,
90 pub ctrl: SvgPoint,
91 pub end: SvgPoint,
92}
93
94impl_option!(
95 SvgPoint,
96 OptionSvgPoint,
97 [Debug, Clone, PartialEq, PartialOrd]
98);
99
100impl SvgPoint {
101 #[inline]
103 #[must_use] pub const fn new(x: f32, y: f32) -> Self {
104 Self { x, y }
105 }
106
107 #[inline]
109 #[must_use] pub fn distance(&self, other: Self) -> f64 {
110 let dx = other.x - self.x;
111 let dy = other.y - self.y;
112 f64::from(libm::hypotf(dx, dy))
113 }
114}
115
116impl SvgRect {
117 pub fn union_with(&mut self, other: &Self) {
119 let self_max_x = self.x + self.width;
120 let self_max_y = self.y + self.height;
121 let self_min_x = self.x;
122 let self_min_y = self.y;
123
124 let other_max_x = other.x + other.width;
125 let other_max_y = other.y + other.height;
126 let other_min_x = other.x;
127 let other_min_y = other.y;
128
129 let max_x = self_max_x.max(other_max_x);
130 let max_y = self_max_y.max(other_max_y);
131 let min_x = self_min_x.min(other_min_x);
132 let min_y = self_min_y.min(other_min_y);
133
134 self.x = min_x;
135 self.y = min_y;
136 self.width = max_x - min_x;
137 self.height = max_y - min_y;
138 }
139
140 #[must_use] pub fn contains_point(&self, point: SvgPoint) -> bool {
143 point.x > self.x
144 && point.x < self.x + self.width
145 && point.y > self.y
146 && point.y < self.y + self.height
147 }
148
149 #[must_use] pub fn expand(
151 &self,
152 padding_top: f32,
153 padding_bottom: f32,
154 padding_left: f32,
155 padding_right: f32,
156 ) -> Self {
157 Self {
158 width: self.width + padding_left + padding_right,
159 height: self.height + padding_top + padding_bottom,
160 x: self.x - padding_left,
161 y: self.y - padding_top,
162 ..*self
163 }
164 }
165
166 #[must_use] pub fn get_center(&self) -> SvgPoint {
168 SvgPoint {
169 x: self.x + (self.width / 2.0),
170 y: self.y + (self.height / 2.0),
171 }
172 }
173}
174
175const STEP_SIZE: usize = 20;
176const STEP_SIZE_F64: f64 = 0.05;
177
178#[allow(clippy::suboptimal_flops)]
182impl SvgCubicCurve {
183 #[inline]
185 #[must_use] pub const fn new(start: SvgPoint, ctrl_1: SvgPoint, ctrl_2: SvgPoint, end: SvgPoint) -> Self {
186 Self { start, ctrl_1, ctrl_2, end }
187 }
188
189 pub const fn reverse(&mut self) {
191 core::mem::swap(&mut self.start, &mut self.end);
192 core::mem::swap(&mut self.ctrl_1, &mut self.ctrl_2);
193 }
194
195 #[must_use] pub const fn get_start(&self) -> SvgPoint {
197 self.start
198 }
199 #[must_use] pub const fn get_end(&self) -> SvgPoint {
201 self.end
202 }
203
204 #[must_use] pub fn get_x_at_t(&self, t: f64) -> f64 {
206 let c_x = 3.0 * (f64::from(self.ctrl_1.x) - f64::from(self.start.x));
207 let b_x = 3.0 * (f64::from(self.ctrl_2.x) - f64::from(self.ctrl_1.x)) - c_x;
208 let a_x = f64::from(self.end.x) - f64::from(self.start.x) - c_x - b_x;
209
210 (a_x * t * t * t) + (b_x * t * t) + (c_x * t) + f64::from(self.start.x)
211 }
212
213 #[must_use] pub fn get_y_at_t(&self, t: f64) -> f64 {
215 let c_y = 3.0 * (f64::from(self.ctrl_1.y) - f64::from(self.start.y));
216 let b_y = 3.0 * (f64::from(self.ctrl_2.y) - f64::from(self.ctrl_1.y)) - c_y;
217 let a_y = f64::from(self.end.y) - f64::from(self.start.y) - c_y - b_y;
218
219 (a_y * t * t * t) + (b_y * t * t) + (c_y * t) + f64::from(self.start.y)
220 }
221
222 #[must_use] pub fn get_length(&self) -> f64 {
224 let mut arc_length = 0.0;
226 let mut prev_point = self.get_start();
227
228 for i in 0..STEP_SIZE {
229 let t_next = idx_to_f64(i + 1) * STEP_SIZE_F64;
230 let next_point = SvgPoint {
231 x: f64_to_f32(self.get_x_at_t(t_next)),
232 y: f64_to_f32(self.get_y_at_t(t_next)),
233 };
234 arc_length += prev_point.distance(next_point);
235 prev_point = next_point;
236 }
237
238 arc_length
239 }
240
241 #[must_use] pub fn get_t_at_offset(&self, offset: f64) -> f64 {
243 let mut arc_length = 0.0;
247 let mut t_current = 0.0;
248 let mut prev_point = self.get_start();
249
250 for i in 0..STEP_SIZE {
251 let t_next = idx_to_f64(i + 1) * STEP_SIZE_F64;
252 let next_point = SvgPoint {
253 x: f64_to_f32(self.get_x_at_t(t_next)),
254 y: f64_to_f32(self.get_y_at_t(t_next)),
255 };
256
257 let distance = prev_point.distance(next_point);
258
259 arc_length += distance;
260
261 if arc_length > offset {
263 let remaining = arc_length - offset;
264 return t_current + ((distance - remaining) / distance) * STEP_SIZE_F64;
265 }
266
267 prev_point = next_point;
268 t_current = t_next;
269 }
270
271 t_current
272 }
273
274 #[must_use] pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
276 let w0 = SvgPoint {
286 x: self.ctrl_1.x - self.start.x,
287 y: self.ctrl_1.y - self.start.y,
288 };
289
290 let w1 = SvgPoint {
291 x: self.ctrl_2.x - self.ctrl_1.x,
292 y: self.ctrl_2.y - self.ctrl_1.y,
293 };
294
295 let w2 = SvgPoint {
296 x: self.end.x - self.ctrl_2.x,
297 y: self.end.y - self.ctrl_2.y,
298 };
299
300 let quadratic_curve = SvgQuadraticCurve {
301 start: w0,
302 ctrl: w1,
303 end: w2,
304 };
305
306 let tangent_vector = SvgVector {
311 x: quadratic_curve.get_x_at_t(t),
312 y: quadratic_curve.get_y_at_t(t),
313 };
314
315 tangent_vector.normalize()
316 }
317
318 #[must_use] pub fn get_bounds(&self) -> SvgRect {
320 let min_x = self
321 .start
322 .x
323 .min(self.end.x)
324 .min(self.ctrl_1.x)
325 .min(self.ctrl_2.x);
326 let max_x = self
327 .start
328 .x
329 .max(self.end.x)
330 .max(self.ctrl_1.x)
331 .max(self.ctrl_2.x);
332
333 let min_y = self
334 .start
335 .y
336 .min(self.end.y)
337 .min(self.ctrl_1.y)
338 .min(self.ctrl_2.y);
339 let max_y = self
340 .start
341 .y
342 .max(self.end.y)
343 .max(self.ctrl_1.y)
344 .max(self.ctrl_2.y);
345
346 let width = (max_x - min_x).abs();
347 let height = (max_y - min_y).abs();
348
349 SvgRect {
350 width,
351 height,
352 x: min_x,
353 y: min_y,
354 ..SvgRect::default()
355 }
356 }
357}
358
359impl SvgVector {
360 #[inline]
362 #[must_use] pub fn angle_degrees(&self) -> f64 {
363 (-self.y).atan2(self.x).to_degrees()
364 }
365
366 #[inline]
368 #[must_use = "returns a new vector"]
369 pub fn normalize(&self) -> Self {
370 let tangent_length = libm::hypot(self.x, self.y);
371 if tangent_length == 0.0 {
372 return Self { x: 0.0, y: 0.0 };
373 }
374 Self {
375 x: self.x / tangent_length,
376 y: self.y / tangent_length,
377 }
378 }
379
380 #[must_use = "returns a new vector"]
382 #[inline]
383 pub fn rotate_90deg_ccw(&self) -> Self {
384 Self {
385 x: -self.y,
386 y: self.x,
387 }
388 }
389}
390
391#[allow(clippy::suboptimal_flops)]
393impl SvgQuadraticCurve {
394 #[inline]
396 #[must_use] pub const fn new(start: SvgPoint, ctrl: SvgPoint, end: SvgPoint) -> Self {
397 Self { start, ctrl, end }
398 }
399
400 pub const fn reverse(&mut self) {
402 core::mem::swap(&mut self.start, &mut self.end);
403 }
404 #[must_use] pub const fn get_start(&self) -> SvgPoint {
406 self.start
407 }
408 #[must_use] pub const fn get_end(&self) -> SvgPoint {
410 self.end
411 }
412 #[must_use] pub fn get_bounds(&self) -> SvgRect {
414 let min_x = self.start.x.min(self.end.x).min(self.ctrl.x);
415 let max_x = self.start.x.max(self.end.x).max(self.ctrl.x);
416
417 let min_y = self.start.y.min(self.end.y).min(self.ctrl.y);
418 let max_y = self.start.y.max(self.end.y).max(self.ctrl.y);
419
420 let width = (max_x - min_x).abs();
421 let height = (max_y - min_y).abs();
422
423 SvgRect {
424 width,
425 height,
426 x: min_x,
427 y: min_y,
428 ..SvgRect::default()
429 }
430 }
431
432 #[must_use] pub fn get_x_at_t(&self, t: f64) -> f64 {
434 let one_minus = 1.0 - t;
435 one_minus * one_minus * f64::from(self.start.x)
436 + 2.0 * one_minus * t * f64::from(self.ctrl.x)
437 + t * t * f64::from(self.end.x)
438 }
439
440 #[must_use] pub fn get_y_at_t(&self, t: f64) -> f64 {
442 let one_minus = 1.0 - t;
443 one_minus * one_minus * f64::from(self.start.y)
444 + 2.0 * one_minus * t * f64::from(self.ctrl.y)
445 + t * t * f64::from(self.end.y)
446 }
447
448 #[must_use] pub fn get_length(&self) -> f64 {
450 self.to_cubic().get_length()
451 }
452
453 #[must_use] pub fn get_t_at_offset(&self, offset: f64) -> f64 {
455 self.to_cubic().get_t_at_offset(offset)
456 }
457
458 #[must_use] pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
460 self.to_cubic().get_tangent_vector_at_t(t)
461 }
462
463 fn to_cubic(self) -> SvgCubicCurve {
465 SvgCubicCurve {
466 start: self.start,
467 ctrl_1: SvgPoint {
468 x: self.start.x + (2.0 / 3.0) * (self.ctrl.x - self.start.x),
469 y: self.start.y + (2.0 / 3.0) * (self.ctrl.y - self.start.y),
470 },
471 ctrl_2: SvgPoint {
472 x: self.end.x + (2.0 / 3.0) * (self.ctrl.x - self.end.x),
473 y: self.end.y + (2.0 / 3.0) * (self.ctrl.y - self.end.y),
474 },
475 end: self.end,
476 }
477 }
478}
479
480impl AnimationInterpolationFunction {
481 #[must_use]
483 pub const fn get_curve(self) -> SvgCubicCurve {
484 match self {
485 Self::Ease => SvgCubicCurve {
486 start: SvgPoint { x: 0.0, y: 0.0 },
487 ctrl_1: SvgPoint { x: 0.25, y: 0.1 },
488 ctrl_2: SvgPoint { x: 0.25, y: 1.0 },
489 end: SvgPoint { x: 1.0, y: 1.0 },
490 },
491 Self::Linear => SvgCubicCurve {
492 start: SvgPoint { x: 0.0, y: 0.0 },
493 ctrl_1: SvgPoint { x: 0.0, y: 0.0 },
494 ctrl_2: SvgPoint { x: 1.0, y: 1.0 },
495 end: SvgPoint { x: 1.0, y: 1.0 },
496 },
497 Self::EaseIn => SvgCubicCurve {
498 start: SvgPoint { x: 0.0, y: 0.0 },
499 ctrl_1: SvgPoint { x: 0.42, y: 0.0 },
500 ctrl_2: SvgPoint { x: 1.0, y: 1.0 },
501 end: SvgPoint { x: 1.0, y: 1.0 },
502 },
503 Self::EaseOut => SvgCubicCurve {
504 start: SvgPoint { x: 0.0, y: 0.0 },
505 ctrl_1: SvgPoint { x: 0.0, y: 0.0 },
506 ctrl_2: SvgPoint { x: 0.58, y: 1.0 },
507 end: SvgPoint { x: 1.0, y: 1.0 },
508 },
509 Self::EaseInOut => SvgCubicCurve {
510 start: SvgPoint { x: 0.0, y: 0.0 },
511 ctrl_1: SvgPoint { x: 0.42, y: 0.0 },
512 ctrl_2: SvgPoint { x: 0.58, y: 1.0 },
513 end: SvgPoint { x: 1.0, y: 1.0 },
514 },
515 Self::CubicBezier(c) => c,
516 }
517 }
518
519 #[must_use] pub fn evaluate(self, t: f64) -> f32 {
521 f64_to_f32(self.get_curve().get_y_at_t(t))
522 }
523}
524
525#[cfg(test)]
526#[allow(clippy::float_cmp, clippy::unreadable_literal)]
527mod autotest_generated {
528 use super::*;
529
530 fn approx(a: f64, b: f64, eps: f64) -> bool {
533 (a - b).abs() <= eps
534 }
535
536 fn approx_f32(a: f32, b: f32, eps: f32) -> bool {
537 (a - b).abs() <= eps
538 }
539
540 fn p(x: f32, y: f32) -> SvgPoint {
541 SvgPoint::new(x, y)
542 }
543
544 fn exact_curve() -> SvgCubicCurve {
547 SvgCubicCurve::new(p(0.0, 0.0), p(0.25, 0.5), p(0.75, 0.5), p(1.0, 1.0))
548 }
549
550 fn degenerate_curve() -> SvgCubicCurve {
552 SvgCubicCurve::new(p(5.0, 5.0), p(5.0, 5.0), p(5.0, 5.0), p(5.0, 5.0))
553 }
554
555 const ALL_VARIANTS: [AnimationInterpolationFunction; 5] = [
556 AnimationInterpolationFunction::Ease,
557 AnimationInterpolationFunction::Linear,
558 AnimationInterpolationFunction::EaseIn,
559 AnimationInterpolationFunction::EaseOut,
560 AnimationInterpolationFunction::EaseInOut,
561 ];
562
563 const NASTY_F64: [f64; 12] = [
565 0.0,
566 -0.0,
567 1.0,
568 -1.0,
569 2.0,
570 1e-300,
571 1e300,
572 f64::MAX,
573 f64::MIN,
574 f64::INFINITY,
575 f64::NEG_INFINITY,
576 f64::NAN,
577 ];
578
579 #[test]
582 fn idx_to_f64_zero_and_small_values_are_exact() {
583 assert_eq!(idx_to_f64(0), 0.0);
584 assert_eq!(idx_to_f64(1), 1.0);
585 assert_eq!(idx_to_f64(20), 20.0);
586 assert_eq!(idx_to_f64(STEP_SIZE), 20.0);
587 }
588
589 #[test]
590 fn idx_to_f64_is_strictly_monotonic_over_the_sampling_range() {
591 for i in 0..STEP_SIZE {
592 assert!(
593 idx_to_f64(i + 1) > idx_to_f64(i),
594 "not monotonic at i = {i}"
595 );
596 }
597 }
598
599 #[test]
600 fn idx_to_f64_at_usize_max_does_not_panic_and_stays_finite() {
601 let v = idx_to_f64(usize::MAX);
604 assert!(v.is_finite(), "usize::MAX must not become inf/NaN: {v}");
605 assert!(v > 0.0);
606 assert!(v >= idx_to_f64(STEP_SIZE));
607 }
608
609 #[test]
610 fn idx_to_f64_covers_the_full_bezier_domain() {
611 assert!(approx(idx_to_f64(STEP_SIZE) * STEP_SIZE_F64, 1.0, 1e-12));
614 }
615
616 #[test]
619 fn f64_to_f32_zero_preserves_sign() {
620 assert_eq!(f64_to_f32(0.0), 0.0_f32);
621 assert!(f64_to_f32(0.0).is_sign_positive());
622 assert!(f64_to_f32(-0.0).is_sign_negative());
623 }
624
625 #[test]
626 fn f64_to_f32_overflow_saturates_to_infinity_not_a_panic() {
627 assert_eq!(f64_to_f32(f64::MAX), f32::INFINITY);
629 assert_eq!(f64_to_f32(f64::MIN), f32::NEG_INFINITY);
630 assert_eq!(f64_to_f32(1e300), f32::INFINITY);
631 assert_eq!(f64_to_f32(-1e300), f32::NEG_INFINITY);
632 }
633
634 #[test]
635 fn f64_to_f32_underflow_flushes_to_signed_zero() {
636 let tiny = f64_to_f32(1e-300);
637 assert_eq!(tiny, 0.0_f32);
638 assert!(tiny.is_sign_positive());
639
640 let neg_tiny = f64_to_f32(-1e-300);
641 assert_eq!(neg_tiny, 0.0_f32);
642 assert!(neg_tiny.is_sign_negative(), "sign must survive underflow");
643 }
644
645 #[test]
646 fn f64_to_f32_nan_and_inf_are_defined_and_do_not_panic() {
647 assert!(f64_to_f32(f64::NAN).is_nan());
648 assert_eq!(f64_to_f32(f64::INFINITY), f32::INFINITY);
649 assert_eq!(f64_to_f32(f64::NEG_INFINITY), f32::NEG_INFINITY);
650 }
651
652 #[test]
653 fn f64_to_f32_round_trips_values_that_originate_as_f32() {
654 for original in [
656 0.0_f32,
657 1.0,
658 -1.0,
659 0.25,
660 0.1,
661 f32::MAX,
662 f32::MIN,
663 f32::MIN_POSITIVE,
664 f32::EPSILON,
665 ] {
666 assert_eq!(
667 f64_to_f32(f64::from(original)),
668 original,
669 "round-trip failed for {original}"
670 );
671 }
672 }
673
674 #[test]
677 fn svg_point_new_stores_fields_verbatim_including_extremes() {
678 for (x, y) in [
679 (0.0_f32, 0.0_f32),
680 (-1.5, 2.5),
681 (f32::MAX, f32::MIN),
682 (f32::MIN_POSITIVE, -f32::MIN_POSITIVE),
683 (f32::INFINITY, f32::NEG_INFINITY),
684 ] {
685 let pt = SvgPoint::new(x, y);
686 assert_eq!(pt.x, x);
687 assert_eq!(pt.y, y);
688 }
689
690 let nan_point = SvgPoint::new(f32::NAN, f32::NAN);
691 assert!(nan_point.x.is_nan() && nan_point.y.is_nan());
692 assert_ne!(nan_point, nan_point);
694 }
695
696 #[test]
697 fn svg_point_default_is_the_origin() {
698 assert_eq!(SvgPoint::default(), p(0.0, 0.0));
699 }
700
701 #[test]
704 fn distance_basic_values_and_identity() {
705 assert_eq!(p(0.0, 0.0).distance(p(3.0, 4.0)), 5.0);
706 assert_eq!(p(0.0, 0.0).distance(p(0.0, 0.0)), 0.0);
707 assert_eq!(p(-3.0, -4.0).distance(p(0.0, 0.0)), 5.0);
708 }
709
710 #[test]
711 fn distance_is_symmetric() {
712 let a = p(-12.5, 7.25);
713 let b = p(3.0, -9.75);
714 assert_eq!(a.distance(b), b.distance(a));
715 }
716
717 #[test]
718 fn distance_overflows_to_infinity_because_the_delta_is_computed_in_f32() {
719 let d = p(-f32::MAX, 0.0).distance(p(f32::MAX, 0.0));
722 assert!(d.is_infinite() && d > 0.0, "expected +inf, got {d}");
723 }
724
725 #[test]
726 fn distance_between_extreme_corners_never_underreports() {
727 let d = p(0.0, 0.0).distance(p(f32::MAX, f32::MAX));
730 assert!(!d.is_nan());
731 assert!(d >= f64::from(f32::MAX), "distance underreported: {d}");
732 }
733
734 #[test]
735 fn distance_with_nan_or_inf_coordinates_does_not_panic() {
736 assert!(p(0.0, 0.0).distance(p(f32::NAN, 1.0)).is_nan());
738 assert!(p(f32::NAN, f32::NAN).distance(p(0.0, 0.0)).is_nan());
739 assert!(p(0.0, 0.0).distance(p(f32::INFINITY, 0.0)).is_infinite());
740 assert!(
741 p(0.0, 0.0)
742 .distance(p(f32::NAN, f32::INFINITY))
743 .is_infinite()
744 );
745 }
746
747 fn rect(width: f32, height: f32, x: f32, y: f32) -> SvgRect {
750 SvgRect {
751 width,
752 height,
753 x,
754 y,
755 ..SvgRect::default()
756 }
757 }
758
759 #[test]
760 fn union_with_expands_to_cover_both_rects() {
761 let mut a = rect(10.0, 10.0, 0.0, 0.0);
762 a.union_with(&rect(10.0, 10.0, 20.0, 30.0));
763 assert_eq!(a, rect(30.0, 40.0, 0.0, 0.0));
764 }
765
766 #[test]
767 fn union_with_self_is_idempotent() {
768 let mut a = rect(10.0, 20.0, -5.0, -7.0);
769 let before = a;
770 a.union_with(&before);
771 assert_eq!(a, before);
772 a.union_with(&before);
773 assert_eq!(a, before, "union must be idempotent");
774 }
775
776 #[test]
777 fn union_with_contained_rect_leaves_the_outer_rect_unchanged() {
778 let mut outer = rect(100.0, 100.0, 0.0, 0.0);
779 let before = outer;
780 outer.union_with(&rect(1.0, 1.0, 50.0, 50.0));
781 assert_eq!(outer, before);
782 }
783
784 #[test]
785 fn union_with_default_rect_always_drags_the_origin_in() {
786 let mut a = rect(5.0, 5.0, 10.0, 10.0);
789 a.union_with(&SvgRect::default());
790 assert_eq!(a, rect(15.0, 15.0, 0.0, 0.0));
791 }
792
793 #[test]
794 fn union_with_nan_rect_is_a_no_op_because_min_max_ignore_nan() {
795 let mut a = rect(10.0, 10.0, 0.0, 0.0);
798 let before = a;
799 a.union_with(&rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN));
800 assert_eq!(a, before, "NaN rect must not poison the union");
801 }
802
803 #[test]
804 fn union_with_infinite_rect_yields_infinite_extent_without_panicking() {
805 let mut a = rect(10.0, 10.0, 0.0, 0.0);
806 a.union_with(&rect(f32::INFINITY, f32::INFINITY, 0.0, 0.0));
807 assert!(a.width.is_infinite() && a.height.is_infinite());
808 assert_eq!(a.x, 0.0);
809 assert_eq!(a.y, 0.0);
810 }
811
812 #[test]
813 fn union_with_extreme_opposite_rects_does_not_panic() {
814 let mut a = rect(f32::MAX, f32::MAX, f32::MIN, f32::MIN);
815 a.union_with(&rect(f32::MAX, f32::MAX, f32::MAX, f32::MAX));
816 assert!(!a.width.is_nan());
818 assert!(!a.height.is_nan());
819 }
820
821 #[test]
824 fn contains_point_is_strictly_exclusive_on_every_edge() {
825 let r = rect(10.0, 10.0, 0.0, 0.0);
826 assert!(r.contains_point(p(5.0, 5.0)));
827 assert!(!r.contains_point(p(0.0, 0.0)));
829 assert!(!r.contains_point(p(10.0, 10.0)));
830 assert!(!r.contains_point(p(0.0, 5.0)));
831 assert!(!r.contains_point(p(10.0, 5.0)));
832 assert!(!r.contains_point(p(5.0, 0.0)));
833 assert!(!r.contains_point(p(5.0, 10.0)));
834 }
835
836 #[test]
837 fn contains_point_zero_sized_rect_contains_nothing() {
838 let r = SvgRect::default();
839 assert!(!r.contains_point(p(0.0, 0.0)));
840 assert!(!r.contains_point(p(1.0, 1.0)));
841 assert!(!r.contains_point(p(-1.0, -1.0)));
842 }
843
844 #[test]
845 fn contains_point_negative_size_rect_contains_nothing() {
846 let r = rect(-10.0, -10.0, 0.0, 0.0);
848 for pt in [p(0.0, 0.0), p(-5.0, -5.0), p(5.0, 5.0), p(-10.0, -10.0)] {
849 assert!(!r.contains_point(pt), "{pt:?} must not be contained");
850 }
851 }
852
853 #[test]
854 fn contains_point_negative_origin_quadrant_works() {
855 let r = rect(10.0, 10.0, -20.0, -20.0);
856 assert!(r.contains_point(p(-15.0, -15.0)));
857 assert!(!r.contains_point(p(-25.0, -15.0)));
858 assert!(!r.contains_point(p(0.0, 0.0)));
859 }
860
861 #[test]
862 fn contains_point_with_nan_coordinates_is_false_not_a_panic() {
863 let r = rect(10.0, 10.0, 0.0, 0.0);
864 assert!(!r.contains_point(p(f32::NAN, 5.0)));
865 assert!(!r.contains_point(p(5.0, f32::NAN)));
866 assert!(!r.contains_point(p(f32::NAN, f32::NAN)));
867
868 let nan_rect = rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN);
870 assert!(!nan_rect.contains_point(p(0.0, 0.0)));
871 }
872
873 #[test]
874 fn contains_point_infinite_rect_contains_finite_points_but_not_infinity() {
875 let r = rect(f32::INFINITY, f32::INFINITY, 0.0, 0.0);
876 assert!(r.contains_point(p(1e30, 1e30)));
877 assert!(!r.contains_point(p(f32::INFINITY, f32::INFINITY)));
878 assert!(!r.contains_point(p(-1.0, 1.0)));
879 }
880
881 #[test]
882 fn contains_point_at_f32_extremes_does_not_panic() {
883 let r = rect(f32::MAX, f32::MAX, f32::MIN, f32::MIN);
884 let _ = r.contains_point(p(f32::MAX, f32::MAX));
889 let _ = r.contains_point(p(f32::MIN, f32::MIN));
890 assert!(!r.contains_point(p(0.0, 0.0)));
891 }
892
893 #[test]
896 fn expand_by_zero_is_the_identity() {
897 let r = SvgRect {
898 width: 10.0,
899 height: 20.0,
900 x: 1.0,
901 y: 2.0,
902 radius_top_left: 3.0,
903 radius_top_right: 4.0,
904 radius_bottom_left: 5.0,
905 radius_bottom_right: 6.0,
906 };
907 assert_eq!(r.expand(0.0, 0.0, 0.0, 0.0), r);
908 }
909
910 #[test]
911 fn expand_grows_the_rect_and_preserves_the_corner_radii() {
912 let r = SvgRect {
913 width: 10.0,
914 height: 10.0,
915 x: 0.0,
916 y: 0.0,
917 radius_top_left: 3.0,
918 radius_top_right: 4.0,
919 radius_bottom_left: 5.0,
920 radius_bottom_right: 6.0,
921 };
922 let e = r.expand(1.0, 2.0, 4.0, 8.0);
923 assert_eq!(e.width, 10.0 + 4.0 + 8.0);
924 assert_eq!(e.height, 10.0 + 1.0 + 2.0);
925 assert_eq!(e.x, -4.0);
926 assert_eq!(e.y, -1.0);
927 assert_eq!(e.radius_top_left, 3.0);
929 assert_eq!(e.radius_top_right, 4.0);
930 assert_eq!(e.radius_bottom_left, 5.0);
931 assert_eq!(e.radius_bottom_right, 6.0);
932 }
933
934 #[test]
935 fn expand_with_negative_padding_shrinks_and_may_invert_the_rect() {
936 let r = rect(10.0, 10.0, 0.0, 0.0);
937 assert_eq!(r.expand(-1.0, -1.0, -1.0, -1.0), rect(8.0, 8.0, 1.0, 1.0));
938
939 let inverted = r.expand(-100.0, -100.0, -100.0, -100.0);
941 assert!(inverted.width < 0.0, "expand does not clamp to zero");
942 assert!(!inverted.contains_point(p(5.0, 5.0)));
943 }
944
945 #[test]
946 fn expand_overflow_saturates_to_infinity_instead_of_panicking() {
947 let r = rect(f32::MAX, f32::MAX, 0.0, 0.0);
948 let e = r.expand(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
949 assert!(e.width.is_infinite() && e.width > 0.0);
951 assert!(e.height.is_infinite() && e.height > 0.0);
952 assert_eq!(e.x, -f32::MAX);
954 assert_eq!(e.y, -f32::MAX);
955 assert!(e.x.is_finite() && e.y.is_finite());
956 }
957
958 #[test]
959 fn expand_with_nan_padding_poisons_the_rect_but_does_not_panic() {
960 let r = rect(10.0, 10.0, 0.0, 0.0);
961 let e = r.expand(f32::NAN, 0.0, 0.0, 0.0);
962 assert!(e.height.is_nan());
963 assert!(e.y.is_nan());
964 assert!(!e.contains_point(p(5.0, 5.0)));
966 }
967
968 #[test]
969 fn expand_with_infinite_padding_produces_infinite_extent() {
970 let r = rect(1.0, 1.0, 0.0, 0.0);
971 let e = r.expand(f32::INFINITY, f32::INFINITY, f32::INFINITY, f32::INFINITY);
972 assert!(e.width.is_infinite());
973 assert!(e.x.is_infinite() && e.x < 0.0);
974 }
975
976 #[test]
979 fn get_center_of_a_known_rect() {
980 assert_eq!(rect(10.0, 20.0, 2.0, 4.0).get_center(), p(7.0, 14.0));
981 assert_eq!(rect(1.0, 1.0, 0.0, 0.0).get_center(), p(0.5, 0.5));
982 }
983
984 #[test]
985 fn get_center_of_default_rect_is_the_origin() {
986 assert_eq!(SvgRect::default().get_center(), SvgPoint::default());
987 }
988
989 #[test]
990 fn get_center_of_a_contained_rect_is_inside_it() {
991 let r = rect(10.0, 10.0, -3.0, 7.5);
992 assert!(r.contains_point(r.get_center()));
993 }
994
995 #[test]
996 fn get_center_at_extremes_does_not_panic() {
997 let inf = rect(f32::INFINITY, f32::INFINITY, 0.0, 0.0).get_center();
998 assert!(inf.x.is_infinite() && inf.y.is_infinite());
999
1000 let huge = rect(f32::MAX, f32::MAX, 0.0, 0.0).get_center();
1002 assert!(huge.x.is_finite() && huge.y.is_finite());
1003
1004 let nan = rect(f32::NAN, f32::NAN, 0.0, 0.0).get_center();
1005 assert!(nan.x.is_nan() && nan.y.is_nan());
1006 }
1007
1008 #[test]
1011 fn cubic_new_stores_all_four_control_points_verbatim() {
1012 let c = SvgCubicCurve::new(p(1.0, 2.0), p(3.0, 4.0), p(5.0, 6.0), p(7.0, 8.0));
1013 assert_eq!(c.start, p(1.0, 2.0));
1014 assert_eq!(c.ctrl_1, p(3.0, 4.0));
1015 assert_eq!(c.ctrl_2, p(5.0, 6.0));
1016 assert_eq!(c.end, p(7.0, 8.0));
1017 assert_eq!(c.get_start(), c.start);
1018 assert_eq!(c.get_end(), c.end);
1019 }
1020
1021 #[test]
1022 fn cubic_new_accepts_extreme_control_points() {
1023 let c = SvgCubicCurve::new(
1024 p(f32::MIN, f32::MAX),
1025 p(f32::INFINITY, f32::NEG_INFINITY),
1026 p(f32::MIN_POSITIVE, -0.0),
1027 p(0.0, 0.0),
1028 );
1029 assert!(c.get_start().x.is_finite());
1030 assert!(c.ctrl_1.x.is_infinite());
1031 assert_eq!(c.get_end(), p(0.0, 0.0));
1032 }
1033
1034 #[test]
1035 fn cubic_reverse_swaps_the_endpoints_and_the_control_points() {
1036 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));
1037 c.reverse();
1038 assert_eq!(c.start, p(7.0, 8.0));
1039 assert_eq!(c.ctrl_1, p(5.0, 6.0));
1040 assert_eq!(c.ctrl_2, p(3.0, 4.0));
1041 assert_eq!(c.end, p(1.0, 2.0));
1042 }
1043
1044 #[test]
1045 fn cubic_reverse_twice_is_the_identity() {
1046 let original = exact_curve();
1047 let mut c = original;
1048 c.reverse();
1049 assert_ne!(c, original);
1050 c.reverse();
1051 assert_eq!(c, original, "reverse must be an involution");
1052 }
1053
1054 #[test]
1055 fn cubic_reverse_mirrors_the_parameterization() {
1056 let original = exact_curve();
1058 let mut reversed = original;
1059 reversed.reverse();
1060 for step in 0..=10 {
1061 let t = f64::from(step) / 10.0;
1062 assert!(approx(
1063 reversed.get_x_at_t(t),
1064 original.get_x_at_t(1.0 - t),
1065 1e-12
1066 ));
1067 assert!(approx(
1068 reversed.get_y_at_t(t),
1069 original.get_y_at_t(1.0 - t),
1070 1e-12
1071 ));
1072 }
1073 }
1074
1075 #[test]
1076 fn cubic_reverse_on_a_degenerate_curve_does_not_panic() {
1077 let mut c = degenerate_curve();
1078 c.reverse();
1079 assert_eq!(c, degenerate_curve());
1080 }
1081
1082 #[test]
1085 fn cubic_endpoints_are_hit_exactly_at_t_0_and_t_1() {
1086 let c = exact_curve();
1087 assert_eq!(c.get_x_at_t(0.0), f64::from(c.start.x));
1088 assert_eq!(c.get_y_at_t(0.0), f64::from(c.start.y));
1089 assert!(approx(c.get_x_at_t(1.0), f64::from(c.end.x), 1e-12));
1090 assert!(approx(c.get_y_at_t(1.0), f64::from(c.end.y), 1e-12));
1091 }
1092
1093 #[test]
1094 fn cubic_negative_zero_t_behaves_like_zero() {
1095 let c = exact_curve();
1096 assert_eq!(c.get_x_at_t(-0.0), c.get_x_at_t(0.0));
1097 assert_eq!(c.get_y_at_t(-0.0), c.get_y_at_t(0.0));
1098 }
1099
1100 #[test]
1101 fn cubic_stays_within_the_control_hull_for_t_in_unit_range() {
1102 let c = exact_curve();
1104 let bounds = c.get_bounds();
1105 for step in 0..=20 {
1106 let t = f64::from(step) / 20.0;
1107 let x = c.get_x_at_t(t);
1108 let y = c.get_y_at_t(t);
1109 assert!(
1110 x >= f64::from(bounds.x) - 1e-9
1111 && x <= f64::from(bounds.x + bounds.width) + 1e-9,
1112 "x left the hull at t = {t}: {x}"
1113 );
1114 assert!(
1115 y >= f64::from(bounds.y) - 1e-9
1116 && y <= f64::from(bounds.y + bounds.height) + 1e-9,
1117 "y left the hull at t = {t}: {y}"
1118 );
1119 }
1120 }
1121
1122 #[test]
1123 fn cubic_evaluation_extrapolates_outside_the_unit_range_without_clamping() {
1124 let c = AnimationInterpolationFunction::Linear.get_curve();
1126 assert_eq!(c.get_y_at_t(-1.0), 5.0);
1128 assert_eq!(c.get_y_at_t(2.0), -4.0);
1129 }
1130
1131 #[test]
1132 fn cubic_evaluation_at_nan_and_inf_is_defined_and_never_panics() {
1133 let c = exact_curve();
1134 assert!(c.get_x_at_t(f64::NAN).is_nan());
1135 assert!(c.get_y_at_t(f64::NAN).is_nan());
1136
1137 for t in NASTY_F64 {
1138 let x = c.get_x_at_t(t);
1139 let y = c.get_y_at_t(t);
1140 if (0.0..=1.0).contains(&t) {
1143 assert!(x.is_finite() && y.is_finite(), "finite t={t} gave {x}/{y}");
1144 }
1145 }
1146 }
1147
1148 #[test]
1149 fn cubic_evaluation_at_huge_t_overflows_instead_of_returning_a_bogus_finite() {
1150 let c = AnimationInterpolationFunction::Linear.get_curve();
1151 for t in [f64::MAX, f64::MIN, 1e300, -1e300, f64::INFINITY] {
1152 assert!(
1153 !c.get_x_at_t(t).is_finite(),
1154 "t = {t} must not produce a finite x"
1155 );
1156 assert!(!c.get_y_at_t(t).is_finite());
1157 }
1158 }
1159
1160 #[test]
1161 fn cubic_with_infinite_control_points_yields_nan_not_a_panic() {
1162 let c = SvgCubicCurve::new(
1163 p(f32::INFINITY, 0.0),
1164 p(0.0, 0.0),
1165 p(0.0, 0.0),
1166 p(1.0, 1.0),
1167 );
1168 assert!(!c.get_x_at_t(0.5).is_finite());
1170 }
1171
1172 #[test]
1175 fn cubic_length_of_the_linear_timing_curve_is_the_unit_diagonal() {
1176 let len = AnimationInterpolationFunction::Linear.get_curve().get_length();
1178 assert!(
1179 approx(len, core::f64::consts::SQRT_2, 1e-4),
1180 "expected ~sqrt(2), got {len}"
1181 );
1182 }
1183
1184 #[test]
1185 fn cubic_length_of_a_degenerate_curve_is_exactly_zero() {
1186 assert_eq!(degenerate_curve().get_length(), 0.0);
1187 }
1188
1189 #[test]
1190 fn cubic_length_is_non_negative_and_at_least_the_chord() {
1191 let c = exact_curve();
1192 let chord = c.get_start().distance(c.get_end());
1193 let len = c.get_length();
1194 assert!(len >= 0.0);
1195 assert!(
1196 len >= chord - 1e-6,
1197 "arc length {len} shorter than chord {chord}"
1198 );
1199 }
1200
1201 #[test]
1202 fn cubic_length_is_invariant_under_reverse() {
1203 let mut c = exact_curve();
1204 let forward = c.get_length();
1205 c.reverse();
1206 assert!(approx(c.get_length(), forward, 1e-5));
1207 }
1208
1209 #[test]
1210 fn cubic_length_at_extremes_does_not_panic() {
1211 let inf = SvgCubicCurve::new(
1212 p(f32::MIN, f32::MIN),
1213 p(0.0, 0.0),
1214 p(0.0, 0.0),
1215 p(f32::MAX, f32::MAX),
1216 )
1217 .get_length();
1218 assert!(!inf.is_nan());
1219 assert!(inf > 0.0);
1220
1221 let nan = SvgCubicCurve::new(
1222 p(f32::NAN, f32::NAN),
1223 p(0.0, 0.0),
1224 p(0.0, 0.0),
1225 p(1.0, 1.0),
1226 )
1227 .get_length();
1228 assert!(nan.is_nan() || nan >= 0.0);
1229 }
1230
1231 #[test]
1234 fn cubic_t_at_offset_zero_is_zero() {
1235 let c = AnimationInterpolationFunction::Linear.get_curve();
1236 assert_eq!(c.get_t_at_offset(0.0), 0.0);
1237 }
1238
1239 #[test]
1240 fn cubic_t_at_half_length_is_the_midpoint_of_the_linear_curve() {
1241 let c = AnimationInterpolationFunction::Linear.get_curve();
1244 let t = c.get_t_at_offset(c.get_length() / 2.0);
1245 assert!(approx(t, 0.5, 0.06), "expected t ~ 0.5, got {t}");
1246 }
1247
1248 #[test]
1249 fn cubic_t_at_offset_is_monotonic_and_bounded_across_the_curve() {
1250 let c = exact_curve();
1251 let len = c.get_length();
1252 let mut prev = f64::NEG_INFINITY;
1253 for step in 0..=10 {
1254 let offset = len * f64::from(step) / 10.0;
1255 let t = c.get_t_at_offset(offset);
1256 assert!((-1e-9..=1.0 + 1e-9).contains(&t), "t out of range: {t}");
1257 assert!(t >= prev - 1e-9, "t went backwards: {prev} -> {t}");
1258 prev = t;
1259 }
1260 }
1261
1262 #[test]
1263 fn cubic_t_at_offset_beyond_the_curve_saturates_at_one() {
1264 let c = AnimationInterpolationFunction::Linear.get_curve();
1265 for offset in [10.0, 1e300, f64::MAX, f64::INFINITY] {
1266 let t = c.get_t_at_offset(offset);
1267 assert!(
1268 approx(t, 1.0, 1e-9),
1269 "offset {offset} should saturate at t = 1, got {t}"
1270 );
1271 }
1272 }
1273
1274 #[test]
1275 fn cubic_t_at_offset_with_nan_falls_through_to_one() {
1276 let c = AnimationInterpolationFunction::Linear.get_curve();
1279 let t = c.get_t_at_offset(f64::NAN);
1280 assert!(!t.is_nan(), "NaN offset must not leak into the result");
1281 assert!(approx(t, 1.0, 1e-9), "got {t}");
1282 }
1283
1284 #[test]
1285 fn cubic_t_at_negative_offset_extrapolates_backwards_without_clamping() {
1286 let c = AnimationInterpolationFunction::Linear.get_curve();
1288 let t = c.get_t_at_offset(-1.0);
1289 assert!(t.is_finite(), "expected a finite (negative) t, got {t}");
1290 assert!(t < 0.0, "negative offset should yield t < 0, got {t}");
1291 }
1292
1293 #[test]
1294 fn cubic_t_at_offset_on_a_degenerate_curve_divides_by_zero_but_does_not_panic() {
1295 let c = degenerate_curve();
1299 let t = c.get_t_at_offset(-1.0);
1300 assert!(
1301 t.is_infinite() && t < 0.0,
1302 "zero-length curve + negative offset should give -inf, got {t}"
1303 );
1304
1305 let t0 = c.get_t_at_offset(0.0);
1307 assert!(approx(t0, 1.0, 1e-9), "got {t0}");
1308 assert!(!t0.is_nan());
1309 }
1310
1311 #[test]
1312 fn cubic_t_at_offset_survives_every_nasty_input() {
1313 let c = exact_curve();
1314 for offset in NASTY_F64 {
1315 let t = c.get_t_at_offset(offset);
1316 if offset >= 0.0 {
1319 assert!(!t.is_nan(), "offset {offset} produced NaN");
1320 }
1321 }
1322 }
1323
1324 #[test]
1327 fn cubic_tangent_of_the_linear_curve_points_along_the_diagonal() {
1328 let c = AnimationInterpolationFunction::Linear.get_curve();
1329 let v = c.get_tangent_vector_at_t(0.5);
1330 let expected = core::f64::consts::FRAC_1_SQRT_2;
1331 assert!(approx(v.x, expected, 1e-12), "x = {}", v.x);
1332 assert!(approx(v.y, expected, 1e-12), "y = {}", v.y);
1333 }
1334
1335 #[test]
1336 fn cubic_tangent_is_a_unit_vector_or_exactly_zero() {
1337 let c = exact_curve();
1338 for step in 0..=20 {
1339 let t = f64::from(step) / 20.0;
1340 let v = c.get_tangent_vector_at_t(t);
1341 let len = libm::hypot(v.x, v.y);
1342 assert!(
1343 len == 0.0 || approx(len, 1.0, 1e-9),
1344 "tangent at t = {t} has length {len}"
1345 );
1346 }
1347 }
1348
1349 #[test]
1350 fn cubic_tangent_at_a_cusp_degenerates_to_the_zero_vector() {
1351 let c = AnimationInterpolationFunction::Linear.get_curve();
1354 for t in [0.0, 1.0] {
1355 let v = c.get_tangent_vector_at_t(t);
1356 assert_eq!(v.x, 0.0, "t = {t}");
1357 assert_eq!(v.y, 0.0, "t = {t}");
1358 }
1359 }
1360
1361 #[test]
1362 fn cubic_tangent_of_a_degenerate_curve_is_the_zero_vector() {
1363 let v = degenerate_curve().get_tangent_vector_at_t(0.5);
1364 assert_eq!(v.x, 0.0);
1365 assert_eq!(v.y, 0.0);
1366 }
1367
1368 #[test]
1369 fn cubic_tangent_at_nan_t_is_nan_not_a_panic() {
1370 let v = exact_curve().get_tangent_vector_at_t(f64::NAN);
1371 assert!(v.x.is_nan() && v.y.is_nan());
1372 }
1373
1374 #[test]
1375 fn cubic_tangent_survives_every_nasty_t() {
1376 let c = exact_curve();
1377 for t in NASTY_F64 {
1378 let v = c.get_tangent_vector_at_t(t);
1379 assert!(
1381 v.x.is_nan() || (-1.0..=1.0).contains(&v.x),
1382 "t = {t} gave x = {}",
1383 v.x
1384 );
1385 assert!(
1386 v.y.is_nan() || (-1.0..=1.0).contains(&v.y),
1387 "t = {t} gave y = {}",
1388 v.y
1389 );
1390 }
1391 }
1392
1393 #[test]
1396 fn cubic_bounds_of_a_known_curve() {
1397 let c = AnimationInterpolationFunction::Linear.get_curve();
1398 assert_eq!(c.get_bounds(), rect(1.0, 1.0, 0.0, 0.0));
1399 }
1400
1401 #[test]
1402 fn cubic_bounds_are_never_negative_and_ignore_the_radii() {
1403 let c = SvgCubicCurve::new(p(10.0, 10.0), p(-5.0, 30.0), p(0.0, -2.0), p(3.0, 3.0));
1404 let b = c.get_bounds();
1405 assert_eq!(b.x, -5.0);
1406 assert_eq!(b.y, -2.0);
1407 assert_eq!(b.width, 15.0);
1408 assert_eq!(b.height, 32.0);
1409 assert!(b.width >= 0.0 && b.height >= 0.0);
1410 assert_eq!(b.radius_top_left, 0.0);
1411 assert_eq!(b.radius_bottom_right, 0.0);
1412 }
1413
1414 #[test]
1415 fn cubic_bounds_of_a_degenerate_curve_are_a_zero_size_rect() {
1416 let b = degenerate_curve().get_bounds();
1417 assert_eq!(b, rect(0.0, 0.0, 5.0, 5.0));
1418 }
1419
1420 #[test]
1421 fn cubic_bounds_contain_every_sampled_curve_point() {
1422 let c = exact_curve();
1423 let b = c.get_bounds();
1424 for step in 1..20 {
1425 let t = f64::from(step) / 20.0;
1426 let pt = p(
1427 f64_to_f32(c.get_x_at_t(t)),
1428 f64_to_f32(c.get_y_at_t(t)),
1429 );
1430 assert!(
1431 pt.x >= b.x && pt.x <= b.x + b.width,
1432 "x outside bounds at t = {t}"
1433 );
1434 assert!(
1435 pt.y >= b.y && pt.y <= b.y + b.height,
1436 "y outside bounds at t = {t}"
1437 );
1438 }
1439 }
1440
1441 #[test]
1442 fn cubic_bounds_with_infinite_points_do_not_panic() {
1443 let c = SvgCubicCurve::new(
1444 p(f32::NEG_INFINITY, 0.0),
1445 p(0.0, 0.0),
1446 p(0.0, 0.0),
1447 p(f32::INFINITY, 1.0),
1448 );
1449 let b = c.get_bounds();
1450 assert!(b.width.is_infinite());
1451 assert!(b.x.is_infinite() && b.x < 0.0);
1452 }
1453
1454 #[test]
1455 fn cubic_bounds_ignore_nan_control_points() {
1456 let c = SvgCubicCurve::new(
1458 p(0.0, 0.0),
1459 p(f32::NAN, f32::NAN),
1460 p(2.0, 4.0),
1461 p(1.0, 1.0),
1462 );
1463 let b = c.get_bounds();
1464 assert!(!b.width.is_nan(), "NaN leaked into the bounds width");
1465 assert_eq!(b.x, 0.0);
1466 assert_eq!(b.width, 2.0);
1467 assert_eq!(b.height, 4.0);
1468 }
1469
1470 fn vec2(x: f64, y: f64) -> SvgVector {
1473 SvgVector { x, y }
1474 }
1475
1476 #[test]
1477 fn angle_degrees_of_the_cardinal_directions() {
1478 assert!(approx(vec2(1.0, 0.0).angle_degrees(), 0.0, 1e-12));
1480 assert!(approx(vec2(0.0, -1.0).angle_degrees(), 90.0, 1e-12));
1481 assert!(approx(vec2(0.0, 1.0).angle_degrees(), -90.0, 1e-12));
1482 assert!(approx(vec2(1.0, -1.0).angle_degrees(), 45.0, 1e-12));
1483 assert!(approx(vec2(-1.0, 0.0).angle_degrees().abs(), 180.0, 1e-12));
1484 }
1485
1486 #[test]
1487 fn angle_degrees_is_always_within_plus_minus_180() {
1488 for (x, y) in [
1489 (1.0, 2.0),
1490 (-1.0, -2.0),
1491 (1e300, -1e300),
1492 (1e-300, 1e-300),
1493 (f64::MAX, f64::MIN),
1494 ] {
1495 let a = vec2(x, y).angle_degrees();
1496 assert!(
1497 (-180.0..=180.0).contains(&a),
1498 "angle out of range for ({x}, {y}): {a}"
1499 );
1500 }
1501 }
1502
1503 #[test]
1504 fn angle_degrees_of_the_zero_vector_is_defined() {
1505 let a = vec2(0.0, 0.0).angle_degrees();
1507 assert!(!a.is_nan());
1508 assert_eq!(a, 0.0);
1509 }
1510
1511 #[test]
1512 fn angle_degrees_of_infinite_vectors_is_finite() {
1513 let a = vec2(f64::INFINITY, f64::INFINITY).angle_degrees();
1515 assert!(approx(a, -45.0, 1e-12), "got {a}");
1516 }
1517
1518 #[test]
1519 fn angle_degrees_of_nan_is_nan_not_a_panic() {
1520 assert!(vec2(f64::NAN, 1.0).angle_degrees().is_nan());
1521 assert!(vec2(1.0, f64::NAN).angle_degrees().is_nan());
1522 }
1523
1524 #[test]
1527 fn normalize_of_a_known_vector() {
1528 let v = vec2(3.0, 4.0).normalize();
1529 assert!(approx(v.x, 0.6, 1e-12));
1530 assert!(approx(v.y, 0.8, 1e-12));
1531 assert!(approx(libm::hypot(v.x, v.y), 1.0, 1e-12));
1532 }
1533
1534 #[test]
1535 fn normalize_of_the_zero_vector_returns_zero_not_nan() {
1536 let v = vec2(0.0, 0.0).normalize();
1537 assert_eq!(v.x, 0.0);
1538 assert_eq!(v.y, 0.0);
1539
1540 let v = vec2(-0.0, -0.0).normalize();
1541 assert!(!v.x.is_nan() && !v.y.is_nan());
1542 }
1543
1544 #[test]
1545 fn normalize_is_idempotent() {
1546 let once = vec2(-7.0, 24.0).normalize();
1547 let twice = once.normalize();
1548 assert!(approx(once.x, twice.x, 1e-12));
1549 assert!(approx(once.y, twice.y, 1e-12));
1550 }
1551
1552 #[test]
1553 fn normalize_of_a_tiny_vector_does_not_underflow_to_zero() {
1554 let v = vec2(f64::MIN_POSITIVE, 0.0).normalize();
1555 assert!(approx(v.x, 1.0, 1e-12), "tiny vector collapsed: {}", v.x);
1556 assert_eq!(v.y, 0.0);
1557 }
1558
1559 #[test]
1560 fn normalize_of_a_huge_vector_stays_bounded() {
1561 let v = vec2(f64::MAX, f64::MAX).normalize();
1565 assert!(!v.x.is_nan() && !v.y.is_nan());
1566 assert_eq!(v.x, v.y, "symmetry broken");
1567 let len = libm::hypot(v.x, v.y);
1568 assert!(
1569 len == 0.0 || approx(len, 1.0, 1e-9),
1570 "normalize returned a non-unit, non-zero vector of length {len}"
1571 );
1572 }
1573
1574 #[test]
1575 fn normalize_of_an_infinite_vector_yields_nan_not_a_panic() {
1576 let v = vec2(f64::INFINITY, 1.0).normalize();
1578 assert!(v.x.is_nan(), "expected NaN, got {}", v.x);
1579 assert_eq!(v.y, 0.0);
1580 }
1581
1582 #[test]
1583 fn normalize_of_a_nan_vector_is_nan_not_a_panic() {
1584 let v = vec2(f64::NAN, 0.0).normalize();
1585 assert!(v.x.is_nan());
1586 }
1587
1588 #[test]
1591 fn rotate_90deg_ccw_of_the_cardinal_directions() {
1592 let v = vec2(1.0, 0.0).rotate_90deg_ccw();
1593 assert_eq!(v.x, 0.0); assert_eq!(v.y, 1.0);
1595
1596 let v = vec2(0.0, 1.0).rotate_90deg_ccw();
1597 assert_eq!(v.x, -1.0);
1598 assert_eq!(v.y, 0.0);
1599 }
1600
1601 #[test]
1602 fn rotate_90deg_ccw_four_times_is_the_identity() {
1603 let original = vec2(1.5, -2.5);
1604 let v = original
1605 .rotate_90deg_ccw()
1606 .rotate_90deg_ccw()
1607 .rotate_90deg_ccw()
1608 .rotate_90deg_ccw();
1609 assert_eq!(v, original);
1610 }
1611
1612 #[test]
1613 fn rotate_90deg_ccw_preserves_length_and_turns_by_90_degrees() {
1614 let original = vec2(3.0, 4.0);
1615 let rotated = original.rotate_90deg_ccw();
1616 assert_eq!(
1617 libm::hypot(original.x, original.y),
1618 libm::hypot(rotated.x, rotated.y)
1619 );
1620 assert_eq!(original.x.mul_add(rotated.x, original.y * rotated.y), 0.0);
1622 }
1623
1624 #[test]
1625 fn rotate_90deg_ccw_of_extremes_does_not_panic() {
1626 let v = vec2(f64::MAX, f64::MIN).rotate_90deg_ccw();
1627 assert_eq!(v.x, f64::MAX);
1628 assert_eq!(v.y, f64::MAX);
1629
1630 let v = vec2(f64::NAN, f64::INFINITY).rotate_90deg_ccw();
1631 assert!(v.x.is_infinite() && v.x < 0.0);
1632 assert!(v.y.is_nan());
1633 }
1634
1635 fn quad() -> SvgQuadraticCurve {
1638 SvgQuadraticCurve::new(p(0.0, 0.0), p(10.0, 20.0), p(30.0, 0.0))
1639 }
1640
1641 #[test]
1642 fn quadratic_new_stores_all_three_control_points_verbatim() {
1643 let q = SvgQuadraticCurve::new(p(1.0, 2.0), p(3.0, 4.0), p(5.0, 6.0));
1644 assert_eq!(q.start, p(1.0, 2.0));
1645 assert_eq!(q.ctrl, p(3.0, 4.0));
1646 assert_eq!(q.end, p(5.0, 6.0));
1647 assert_eq!(q.get_start(), q.start);
1648 assert_eq!(q.get_end(), q.end);
1649 }
1650
1651 #[test]
1652 fn quadratic_new_accepts_extreme_control_points() {
1653 let q = SvgQuadraticCurve::new(
1654 p(f32::MAX, f32::MIN),
1655 p(f32::INFINITY, f32::NAN),
1656 p(0.0, 0.0),
1657 );
1658 assert_eq!(q.get_start().x, f32::MAX);
1659 assert!(q.ctrl.y.is_nan());
1660 assert_eq!(q.get_end(), p(0.0, 0.0));
1661 }
1662
1663 #[test]
1664 fn quadratic_reverse_swaps_only_the_endpoints() {
1665 let mut q = quad();
1666 q.reverse();
1667 assert_eq!(q.start, p(30.0, 0.0));
1668 assert_eq!(q.ctrl, p(10.0, 20.0), "ctrl must stay put");
1669 assert_eq!(q.end, p(0.0, 0.0));
1670 }
1671
1672 #[test]
1673 fn quadratic_reverse_twice_is_the_identity() {
1674 let mut q = quad();
1675 q.reverse();
1676 q.reverse();
1677 assert_eq!(q, quad(), "reverse must be an involution");
1678 }
1679
1680 #[test]
1681 fn quadratic_reverse_mirrors_the_parameterization() {
1682 let original = quad();
1683 let mut reversed = original;
1684 reversed.reverse();
1685 for step in 0..=10 {
1686 let t = f64::from(step) / 10.0;
1687 assert!(approx(
1688 reversed.get_x_at_t(t),
1689 original.get_x_at_t(1.0 - t),
1690 1e-12
1691 ));
1692 assert!(approx(
1693 reversed.get_y_at_t(t),
1694 original.get_y_at_t(1.0 - t),
1695 1e-12
1696 ));
1697 }
1698 }
1699
1700 #[test]
1701 fn quadratic_bounds_of_a_known_curve_are_the_control_hull_not_the_tight_box() {
1702 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(5.0, -10.0), p(10.0, 0.0));
1703 let b = q.get_bounds();
1704 assert_eq!(b, rect(10.0, 10.0, 0.0, -10.0));
1705
1706 assert_eq!(q.get_y_at_t(0.5), -5.0);
1709 assert!(b.contains_point(p(5.0, -5.0)));
1710 }
1711
1712 #[test]
1713 fn quadratic_bounds_of_a_degenerate_curve_are_zero_sized() {
1714 let q = SvgQuadraticCurve::new(p(2.0, 3.0), p(2.0, 3.0), p(2.0, 3.0));
1715 assert_eq!(q.get_bounds(), rect(0.0, 0.0, 2.0, 3.0));
1716 }
1717
1718 #[test]
1719 fn quadratic_bounds_ignore_nan_and_survive_infinities() {
1720 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(f32::NAN, f32::NAN), p(4.0, 8.0));
1721 let b = q.get_bounds();
1722 assert!(!b.width.is_nan());
1723 assert_eq!(b, rect(4.0, 8.0, 0.0, 0.0));
1724
1725 let q = SvgQuadraticCurve::new(p(f32::NEG_INFINITY, 0.0), p(0.0, 0.0), p(1.0, 1.0));
1726 assert!(q.get_bounds().width.is_infinite());
1727 }
1728
1729 #[test]
1732 fn quadratic_endpoints_are_hit_exactly() {
1733 let q = quad();
1734 assert_eq!(q.get_x_at_t(0.0), f64::from(q.start.x));
1735 assert_eq!(q.get_y_at_t(0.0), f64::from(q.start.y));
1736 assert_eq!(q.get_x_at_t(1.0), f64::from(q.end.x));
1737 assert_eq!(q.get_y_at_t(1.0), f64::from(q.end.y));
1738 }
1739
1740 #[test]
1741 fn quadratic_midpoint_matches_the_closed_form() {
1742 let q = quad();
1744 let expected_x = (2.0f64.mul_add(f64::from(q.ctrl.x), f64::from(q.start.x)) + f64::from(q.end.x)) / 4.0;
1745 let expected_y = (2.0f64.mul_add(f64::from(q.ctrl.y), f64::from(q.start.y)) + f64::from(q.end.y)) / 4.0;
1746 assert!(approx(q.get_x_at_t(0.5), expected_x, 1e-12));
1747 assert!(approx(q.get_y_at_t(0.5), expected_y, 1e-12));
1748 }
1749
1750 #[test]
1751 fn quadratic_extrapolates_outside_the_unit_range_without_clamping() {
1752 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(0.0, 0.0), p(1.0, 1.0));
1753 assert_eq!(q.get_x_at_t(2.0), 4.0);
1755 assert_eq!(q.get_x_at_t(-1.0), 1.0);
1756 }
1757
1758 #[test]
1759 fn quadratic_evaluation_at_nan_and_inf_never_panics() {
1760 let q = quad();
1761 assert!(q.get_x_at_t(f64::NAN).is_nan());
1762 assert!(q.get_y_at_t(f64::NAN).is_nan());
1763 for t in NASTY_F64 {
1764 let x = q.get_x_at_t(t);
1765 let y = q.get_y_at_t(t);
1766 if (0.0..=1.0).contains(&t) {
1767 assert!(x.is_finite() && y.is_finite(), "finite t={t} gave {x}/{y}");
1768 }
1769 }
1770 }
1771
1772 #[test]
1773 fn quadratic_evaluation_at_huge_t_overflows_rather_than_lying() {
1774 let q = quad();
1775 for t in [f64::MAX, f64::MIN, 1e300, f64::INFINITY, f64::NEG_INFINITY] {
1776 assert!(
1777 !q.get_x_at_t(t).is_finite(),
1778 "t = {t} must not produce a finite x"
1779 );
1780 }
1781 }
1782
1783 #[test]
1786 fn quadratic_length_of_a_straight_line_matches_the_chord() {
1787 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(1.5, 2.0), p(3.0, 4.0));
1789 assert!(approx(q.get_length(), 5.0, 1e-3), "got {}", q.get_length());
1790 }
1791
1792 #[test]
1793 fn quadratic_length_of_a_degenerate_curve_is_zero() {
1794 let q = SvgQuadraticCurve::new(p(1.0, 1.0), p(1.0, 1.0), p(1.0, 1.0));
1795 assert_eq!(q.get_length(), 0.0);
1796 }
1797
1798 #[test]
1799 fn quadratic_length_is_at_least_the_chord_and_invariant_under_reverse() {
1800 let mut q = quad();
1801 let len = q.get_length();
1802 let chord = q.get_start().distance(q.get_end());
1803 assert!(len >= chord - 1e-6, "arc {len} < chord {chord}");
1804 q.reverse();
1805 assert!(approx(q.get_length(), len, 1e-4));
1806 }
1807
1808 #[test]
1809 fn quadratic_t_at_offset_zero_is_zero_and_huge_saturates_at_one() {
1810 let q = quad();
1811 assert_eq!(q.get_t_at_offset(0.0), 0.0);
1812 assert!(approx(q.get_t_at_offset(f64::MAX), 1.0, 1e-9));
1813 assert!(approx(q.get_t_at_offset(f64::INFINITY), 1.0, 1e-9));
1814 }
1815
1816 #[test]
1817 fn quadratic_t_at_offset_with_nan_is_deterministic() {
1818 let t = quad().get_t_at_offset(f64::NAN);
1819 assert!(!t.is_nan());
1820 assert!(approx(t, 1.0, 1e-9), "got {t}");
1821 }
1822
1823 #[test]
1824 fn quadratic_t_at_offset_is_monotonic_and_bounded() {
1825 let q = quad();
1826 let len = q.get_length();
1827 let mut prev = f64::NEG_INFINITY;
1828 for step in 0..=10 {
1829 let t = q.get_t_at_offset(len * f64::from(step) / 10.0);
1830 assert!((-1e-9..=1.0 + 1e-9).contains(&t), "t out of range: {t}");
1831 assert!(t >= prev - 1e-9, "t went backwards: {prev} -> {t}");
1832 prev = t;
1833 }
1834 }
1835
1836 #[test]
1837 fn quadratic_tangent_is_unit_length_or_zero() {
1838 let q = quad();
1839 for step in 0..=20 {
1840 let t = f64::from(step) / 20.0;
1841 let v = q.get_tangent_vector_at_t(t);
1842 let len = libm::hypot(v.x, v.y);
1843 assert!(
1844 len == 0.0 || approx(len, 1.0, 1e-9),
1845 "tangent at t = {t} has length {len}"
1846 );
1847 }
1848 }
1849
1850 #[test]
1851 fn quadratic_tangent_of_a_straight_line_is_constant() {
1852 let q = SvgQuadraticCurve::new(p(0.0, 0.0), p(1.5, 2.0), p(3.0, 4.0));
1853 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
1854 let v = q.get_tangent_vector_at_t(t);
1855 assert!(approx(v.x, 0.6, 1e-6), "t = {t}: x = {}", v.x);
1856 assert!(approx(v.y, 0.8, 1e-6), "t = {t}: y = {}", v.y);
1857 }
1858 }
1859
1860 #[test]
1861 fn quadratic_tangent_at_nan_t_is_nan_not_a_panic() {
1862 let v = quad().get_tangent_vector_at_t(f64::NAN);
1863 assert!(v.x.is_nan() && v.y.is_nan());
1864 }
1865
1866 #[test]
1869 fn to_cubic_preserves_the_endpoints() {
1870 let q = quad();
1871 let c = q.to_cubic();
1872 assert_eq!(c.start, q.start);
1873 assert_eq!(c.end, q.end);
1874 }
1875
1876 #[test]
1877 fn to_cubic_produces_an_equivalent_curve() {
1878 let q = quad();
1881 let c = q.to_cubic();
1882 for step in 0..=20 {
1883 let t = f64::from(step) / 20.0;
1884 assert!(
1885 approx(c.get_x_at_t(t), q.get_x_at_t(t), 1e-4),
1886 "x mismatch at t = {t}: {} vs {}",
1887 c.get_x_at_t(t),
1888 q.get_x_at_t(t)
1889 );
1890 assert!(
1891 approx(c.get_y_at_t(t), q.get_y_at_t(t), 1e-4),
1892 "y mismatch at t = {t}: {} vs {}",
1893 c.get_y_at_t(t),
1894 q.get_y_at_t(t)
1895 );
1896 }
1897 }
1898
1899 #[test]
1900 fn to_cubic_of_a_degenerate_curve_is_degenerate() {
1901 let q = SvgQuadraticCurve::new(p(7.0, 7.0), p(7.0, 7.0), p(7.0, 7.0));
1902 let c = q.to_cubic();
1903 assert_eq!(c.start, p(7.0, 7.0));
1904 assert_eq!(c.ctrl_1, p(7.0, 7.0));
1905 assert_eq!(c.ctrl_2, p(7.0, 7.0));
1906 assert_eq!(c.end, p(7.0, 7.0));
1907 assert_eq!(c.get_length(), 0.0);
1908 }
1909
1910 #[test]
1911 fn to_cubic_with_extreme_points_does_not_panic() {
1912 let q = SvgQuadraticCurve::new(p(f32::MIN, 0.0), p(f32::MAX, 0.0), p(0.0, 0.0));
1913 let c = q.to_cubic();
1914 assert!(
1918 c.ctrl_1.x.is_infinite() && c.ctrl_1.x > 0.0,
1919 "expected +inf, got {}",
1920 c.ctrl_1.x
1921 );
1922 assert!(c.ctrl_2.x.is_finite() && c.ctrl_2.x > 0.0);
1924 assert_eq!(c.start, p(f32::MIN, 0.0));
1925 assert_eq!(c.end, p(0.0, 0.0));
1926
1927 let q = SvgQuadraticCurve::new(p(f32::NAN, 0.0), p(0.0, 0.0), p(1.0, 1.0));
1928 assert!(q.to_cubic().ctrl_1.x.is_nan());
1929 }
1930
1931 #[test]
1934 fn get_curve_round_trips_a_custom_cubic_bezier() {
1935 let custom = SvgCubicCurve::new(p(0.0, 0.0), p(0.1, 0.9), p(0.9, 0.1), p(1.0, 1.0));
1937 assert_eq!(
1938 AnimationInterpolationFunction::CubicBezier(custom).get_curve(),
1939 custom
1940 );
1941 }
1942
1943 #[test]
1944 fn get_curve_round_trips_even_a_nonsensical_cubic_bezier() {
1945 let nasty = SvgCubicCurve::new(
1946 p(f32::NAN, f32::INFINITY),
1947 p(f32::MAX, f32::MIN),
1948 p(-0.0, 0.0),
1949 p(1e30, -1e30),
1950 );
1951 let out = AnimationInterpolationFunction::CubicBezier(nasty).get_curve();
1952 assert!(out.start.x.is_nan());
1954 assert!(out.start.y.is_infinite());
1955 assert_eq!(out.ctrl_1, nasty.ctrl_1);
1956 assert_eq!(out.end, nasty.end);
1957 }
1958
1959 #[test]
1960 fn every_builtin_timing_curve_runs_from_0_0_to_1_1() {
1961 for f in ALL_VARIANTS {
1962 let c = f.get_curve();
1963 assert_eq!(c.get_start(), p(0.0, 0.0), "{f:?} does not start at (0,0)");
1964 assert_eq!(c.get_end(), p(1.0, 1.0), "{f:?} does not end at (1,1)");
1965 }
1966 }
1967
1968 #[test]
1969 fn every_builtin_timing_curve_keeps_its_control_points_in_the_unit_box() {
1970 for f in ALL_VARIANTS {
1972 let c = f.get_curve();
1973 for ctrl in [c.ctrl_1, c.ctrl_2] {
1974 assert!(
1975 (0.0..=1.0).contains(&ctrl.x),
1976 "{f:?} has an out-of-range ctrl x: {}",
1977 ctrl.x
1978 );
1979 assert!((0.0..=1.0).contains(&ctrl.y), "{f:?}: {}", ctrl.y);
1980 }
1981 }
1982 }
1983
1984 #[test]
1987 fn evaluate_at_the_endpoints_is_exactly_0_and_1() {
1988 for f in ALL_VARIANTS {
1989 assert_eq!(f.evaluate(0.0), 0.0, "{f:?} at t = 0");
1990 assert!(
1991 approx_f32(f.evaluate(1.0), 1.0, 1e-6),
1992 "{f:?} at t = 1: {}",
1993 f.evaluate(1.0)
1994 );
1995 }
1996 }
1997
1998 #[test]
1999 fn evaluate_is_monotonically_non_decreasing_on_the_unit_interval() {
2000 for f in ALL_VARIANTS {
2001 let mut prev = f32::NEG_INFINITY;
2002 for step in 0..=100 {
2003 let t = f64::from(step) / 100.0;
2004 let v = f.evaluate(t);
2005 assert!(v >= prev - 1e-6, "{f:?} went backwards at t = {t}");
2006 prev = v;
2007 }
2008 }
2009 }
2010
2011 #[test]
2012 fn evaluate_stays_within_0_1_on_the_unit_interval() {
2013 for f in ALL_VARIANTS {
2014 for step in 0..=100 {
2015 let t = f64::from(step) / 100.0;
2016 let v = f.evaluate(t);
2017 assert!(
2018 (-1e-6..=1.0 + 1e-6).contains(&v),
2019 "{f:?} left [0,1] at t = {t}: {v}"
2020 );
2021 }
2022 }
2023 }
2024
2025 #[test]
2026 fn evaluate_samples_the_curve_by_parameter_t_not_by_progress_x() {
2027 let linear = AnimationInterpolationFunction::Linear;
2032 assert_eq!(linear.evaluate(0.5), 0.5);
2033 assert_eq!(linear.evaluate(0.25), 0.15625);
2034 assert_eq!(linear.evaluate(0.75), 0.84375);
2035 assert!(
2036 linear.evaluate(0.25) != 0.25,
2037 "if this ever becomes 0.25, evaluate() started doing the x-inversion"
2038 );
2039 }
2040
2041 #[test]
2042 fn evaluate_cannot_distinguish_four_of_the_five_timing_functions() {
2043 let same = [
2048 AnimationInterpolationFunction::Linear,
2049 AnimationInterpolationFunction::EaseIn,
2050 AnimationInterpolationFunction::EaseOut,
2051 AnimationInterpolationFunction::EaseInOut,
2052 ];
2053 for step in 0..=10 {
2054 let t = f64::from(step) / 10.0;
2055 let reference = same[0].evaluate(t);
2056 for f in same {
2057 assert_eq!(f.evaluate(t), reference, "{f:?} vs Linear at t = {t}");
2058 }
2059 }
2060 assert!(
2061 AnimationInterpolationFunction::Ease.evaluate(0.5)
2062 != AnimationInterpolationFunction::Linear.evaluate(0.5),
2063 "Ease must at least differ from Linear"
2064 );
2065 }
2066
2067 #[test]
2068 fn evaluate_outside_the_unit_interval_extrapolates_without_clamping() {
2069 let linear = AnimationInterpolationFunction::Linear;
2071 assert_eq!(linear.evaluate(-1.0), 5.0);
2072 assert_eq!(linear.evaluate(2.0), -4.0);
2073 }
2074
2075 #[test]
2076 fn evaluate_at_nan_is_nan_for_every_variant() {
2077 for f in ALL_VARIANTS {
2078 assert!(f.evaluate(f64::NAN).is_nan(), "{f:?}");
2079 }
2080 }
2081
2082 #[test]
2083 fn evaluate_at_extreme_t_never_panics_and_never_lies() {
2084 for f in ALL_VARIANTS {
2085 for t in [f64::MAX, f64::MIN, 1e300, -1e300, f64::INFINITY, f64::NEG_INFINITY] {
2086 let v = f.evaluate(t);
2087 assert!(
2088 !v.is_finite(),
2089 "{f:?} at t = {t} returned a plausible-looking {v}"
2090 );
2091 }
2092 }
2093 }
2094
2095 #[test]
2096 fn evaluate_of_a_nan_cubic_bezier_is_nan_not_a_panic() {
2097 let f = AnimationInterpolationFunction::CubicBezier(SvgCubicCurve::new(
2098 p(0.0, f32::NAN),
2099 p(0.0, 0.0),
2100 p(1.0, 1.0),
2101 p(1.0, 1.0),
2102 ));
2103 assert!(f.evaluate(0.5).is_nan());
2104 }
2105
2106 #[test]
2107 fn evaluate_of_a_huge_cubic_bezier_stays_in_f32_range_inside_the_unit_interval() {
2108 let f = AnimationInterpolationFunction::CubicBezier(SvgCubicCurve::new(
2111 p(0.0, 0.0),
2112 p(0.0, f32::MAX),
2113 p(1.0, f32::MAX),
2114 p(1.0, f32::MAX),
2115 ));
2116 for step in 0..=10 {
2117 let v = f.evaluate(f64::from(step) / 10.0);
2118 assert!(v.is_finite(), "overflowed inside [0,1] at step {step}: {v}");
2119 assert!((0.0..=f32::MAX).contains(&v));
2120 }
2121 }
2122
2123 #[test]
2124 fn evaluate_of_a_huge_cubic_bezier_saturates_to_infinity_when_extrapolated() {
2125 let f = AnimationInterpolationFunction::CubicBezier(SvgCubicCurve::new(
2129 p(0.0, 0.0),
2130 p(0.0, f32::MAX),
2131 p(1.0, f32::MAX),
2132 p(1.0, f32::MAX),
2133 ));
2134 let v = f.evaluate(3.0);
2135 assert!(v.is_infinite() && v > 0.0, "expected +inf, got {v}");
2136 }
2137
2138 #[test]
2139 fn evaluate_of_a_degenerate_flat_bezier_is_constant_zero() {
2140 let f = AnimationInterpolationFunction::CubicBezier(SvgCubicCurve::new(
2141 p(0.0, 0.0),
2142 p(0.0, 0.0),
2143 p(1.0, 0.0),
2144 p(1.0, 0.0),
2145 ));
2146 for step in 0..=10 {
2147 let t = f64::from(step) / 10.0;
2148 assert_eq!(f.evaluate(t), 0.0, "t = {t}");
2149 }
2150 }
2151
2152 #[test]
2155 fn option_svg_point_round_trips_through_std_option() {
2156 let pt = p(1.5, -2.5);
2157
2158 let some: OptionSvgPoint = Some(pt).into();
2159 assert!(some.is_some());
2160 assert!(!some.is_none());
2161 assert_eq!(some.as_ref(), Some(&pt));
2162 assert_eq!(Option::<SvgPoint>::from(some), Some(pt));
2163
2164 let none: OptionSvgPoint = OptionSvgPoint::None;
2165 assert!(none.is_none());
2166 assert_eq!(none.as_ref(), None);
2167 assert_eq!(Option::<SvgPoint>::from(none), None);
2168
2169 assert!(OptionSvgPoint::default().is_none());
2170 }
2171
2172 #[test]
2173 fn option_svg_point_replace_returns_the_previous_value() {
2174 let mut o = OptionSvgPoint::None;
2175 let prev = o.replace(p(1.0, 2.0));
2176 assert!(prev.is_none());
2177 assert!(o.is_some());
2178
2179 let prev = o.replace(p(3.0, 4.0));
2180 assert_eq!(prev.as_ref(), Some(&p(1.0, 2.0)));
2181 assert_eq!(o.as_ref(), Some(&p(3.0, 4.0)));
2182 }
2183
2184 #[test]
2187 fn interpolate_resolver_stores_its_fields_verbatim() {
2188 let r = InterpolateResolver {
2189 interpolate_func: AnimationInterpolationFunction::EaseInOut,
2190 parent_rect_width: 100.0,
2191 parent_rect_height: f32::NAN,
2192 current_rect_width: f32::INFINITY,
2193 current_rect_height: -0.0,
2194 };
2195 assert_eq!(r.interpolate_func, AnimationInterpolationFunction::EaseInOut);
2196 assert_eq!(r.parent_rect_width, 100.0);
2197 assert!(r.parent_rect_height.is_nan());
2198 assert!(r.current_rect_width.is_infinite());
2199 assert!(r.current_rect_height.is_sign_negative());
2200 assert_ne!(r, r);
2202 }
2203}