Skip to main content

azul_css/props/basic/
animation.rs

1//! SVG geometry primitives (points, curves, rects, vectors) and animation interpolation functions.
2
3use crate::impl_option;
4
5/// Precision-reducing `usize` → `f64` for Bézier sample indices. The step count
6/// is tiny so no precision is actually lost; `as` is the only `usize`→`f64` form,
7/// isolated here behind a documented attribute.
8#[inline]
9#[allow(clippy::cast_precision_loss)]
10const fn idx_to_f64(v: usize) -> f64 {
11    v as f64
12}
13
14/// Truncating `f64` → `f32` for SVG curve sample coordinates. Behaviour-preserving
15/// (`as f32` rounds to the nearest representable value); isolates the narrowing.
16#[inline]
17#[allow(clippy::cast_possible_truncation)]
18const fn f64_to_f32(v: f64) -> f32 {
19    v as f32
20}
21
22/// Holds context needed to resolve animation interpolation relative to parent and current rects.
23#[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/// A 2D point with f32 coordinates, used in SVG paths and bezier curves.
34#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
35#[repr(C)]
36pub struct SvgPoint {
37    pub x: f32,
38    pub y: f32,
39}
40
41/// A cubic bezier curve defined by start, two control points, and end point.
42#[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)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
51/// Represents an animation timing function.
52#[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/// An axis-aligned rectangle with optional rounded corners.
64#[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/// A 2D vector with f64 coordinates, used for tangent and direction calculations.
78#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
79#[repr(C)]
80pub struct SvgVector {
81    pub x: f64,
82    pub y: f64,
83}
84
85/// A quadratic bezier curve defined by start, one control point, and end point.
86#[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    /// Creates a new `SvgPoint` from x and y coordinates
102    #[inline]
103    #[must_use] pub const fn new(x: f32, y: f32) -> Self {
104        Self { x, y }
105    }
106
107    /// Returns the Euclidean distance between this point and `other`.
108    #[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    /// Expands this rect to also contain `other`.
118    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    /// Note: does not incorporate rounded edges!
141    /// Origin of x and y is assumed to be the top left corner
142    #[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    /// Expands the rect with a certain amount of padding
150    #[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    /// Returns the center point of the rect.
167    #[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// Bézier sampling keeps the explicit `a*b + c` forms rather than `mul_add`:
179// `f32::mul_add` lowers to a software `fmaf` call (slower) on targets without
180// `+fma`, and changes results bit-for-bit. (clippy::suboptimal_flops)
181#[allow(clippy::suboptimal_flops)]
182impl SvgCubicCurve {
183    /// Creates a new `SvgCubicCurve` from start, two control points, and end point
184    #[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    /// Reverses the curve direction in place, swapping start/end and `ctrl_1/ctrl_2`.
190    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    /// Returns the start point of the curve.
196    #[must_use] pub const fn get_start(&self) -> SvgPoint {
197        self.start
198    }
199    /// Returns the end point of the curve.
200    #[must_use] pub const fn get_end(&self) -> SvgPoint {
201        self.end
202    }
203
204    /// Evaluates the x coordinate of the curve at parameter `t` in [0, 1].
205    #[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    /// Evaluates the y coordinate of the curve at parameter `t` in [0, 1].
214    #[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    /// Returns the approximate arc length of the curve using linear sampling.
223    #[must_use] pub fn get_length(&self) -> f64 {
224        // NOTE: this arc length parametrization is not very precise, but fast
225        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    /// Returns the parameter `t` corresponding to a given arc-length `offset`.
242    #[must_use] pub fn get_t_at_offset(&self, offset: f64) -> f64 {
243        // step through the line until the offset is reached,
244        // then interpolate linearly between the
245        // current at the last sampled point
246        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            // linearly interpolate between last t and current t
262            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    /// Returns the normalized tangent vector at parameter `t`.
275    #[must_use] pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
276        // 1. Calculate the derivative of the bezier curve.
277        //
278        // This means that we go from 4 points to 3 points and redistribute
279        // the weights of the control points according to the formula:
280        //
281        // w'0 = 3 * (w1-w0)
282        // w'1 = 3 * (w2-w1)
283        // w'2 = 3 * (w3-w2)
284
285        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        // The first derivative of a cubic bezier curve is a quadratic
307        // bezier curve. Luckily, the first derivative is also the tangent
308        // vector (slope) of the curve. So all we need to do is to sample the
309        // quadratic curve at t
310        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    /// Returns the axis-aligned bounding box of the curve's control points.
319    #[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    /// Returns the angle of the vector in degrees
361    #[inline]
362    #[must_use] pub fn angle_degrees(&self) -> f64 {
363        (-self.y).atan2(self.x).to_degrees()
364    }
365
366    /// Returns a unit-length vector in the same direction, or zero if the length is zero.
367    #[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    /// Rotate the vector 90 degrees counter-clockwise
381    #[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// Explicit FP math (mul_add is slower without `+fma`); see SvgCubicCurve.
392#[allow(clippy::suboptimal_flops)]
393impl SvgQuadraticCurve {
394    /// Creates a new `SvgQuadraticCurve` from start, control, and end points
395    #[inline]
396    #[must_use] pub const fn new(start: SvgPoint, ctrl: SvgPoint, end: SvgPoint) -> Self {
397        Self { start, ctrl, end }
398    }
399
400    /// Reverses the curve direction in place.
401    pub const fn reverse(&mut self) {
402        core::mem::swap(&mut self.start, &mut self.end);
403    }
404    /// Returns the start point of the curve.
405    #[must_use] pub const fn get_start(&self) -> SvgPoint {
406        self.start
407    }
408    /// Returns the end point of the curve.
409    #[must_use] pub const fn get_end(&self) -> SvgPoint {
410        self.end
411    }
412    /// Returns the axis-aligned bounding box of the curve's control points.
413    #[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    /// Evaluates the x coordinate of the curve at parameter `t` in [0, 1].
433    #[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    /// Evaluates the y coordinate of the curve at parameter `t` in [0, 1].
441    #[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    /// Returns the approximate arc length by converting to a cubic curve.
449    #[must_use] pub fn get_length(&self) -> f64 {
450        self.to_cubic().get_length()
451    }
452
453    /// Returns the parameter `t` corresponding to a given arc-length `offset`.
454    #[must_use] pub fn get_t_at_offset(&self, offset: f64) -> f64 {
455        self.to_cubic().get_t_at_offset(offset)
456    }
457
458    /// Returns the normalized tangent vector at parameter `t`.
459    #[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    /// Converts this quadratic curve to an equivalent cubic bezier curve.
464    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    /// Returns the cubic bezier curve corresponding to this timing function.
482    #[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    /// Evaluates the interpolation function at time `t`, returning the eased value.
520    #[must_use] pub fn evaluate(self, t: f64) -> f32 {
521        f64_to_f32(self.get_curve().get_y_at_t(t))
522    }
523}