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/// Holds context needed to resolve animation interpolation relative to parent and current rects.
6#[derive(Debug, Clone, PartialEq)]
7#[repr(C)]
8pub struct InterpolateResolver {
9    pub interpolate_func: AnimationInterpolationFunction,
10    pub parent_rect_width: f32,
11    pub parent_rect_height: f32,
12    pub current_rect_width: f32,
13    pub current_rect_height: f32,
14}
15
16/// A 2D point with f32 coordinates, used in SVG paths and bezier curves.
17#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
18#[repr(C)]
19pub struct SvgPoint {
20    pub x: f32,
21    pub y: f32,
22}
23
24/// A cubic bezier curve defined by start, two control points, and end point.
25#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
26#[repr(C)]
27pub struct SvgCubicCurve {
28    pub start: SvgPoint,
29    pub ctrl_1: SvgPoint,
30    pub ctrl_2: SvgPoint,
31    pub end: SvgPoint,
32}
33
34/// Represents an animation timing function.
35#[derive(Debug, Copy, Clone, PartialEq)]
36#[repr(C, u8)]
37pub enum AnimationInterpolationFunction {
38    Ease,
39    Linear,
40    EaseIn,
41    EaseOut,
42    EaseInOut,
43    CubicBezier(SvgCubicCurve),
44}
45
46/// An axis-aligned rectangle with optional rounded corners.
47#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
48#[repr(C)]
49pub struct SvgRect {
50    pub width: f32,
51    pub height: f32,
52    pub x: f32,
53    pub y: f32,
54    pub radius_top_left: f32,
55    pub radius_top_right: f32,
56    pub radius_bottom_left: f32,
57    pub radius_bottom_right: f32,
58}
59
60/// A 2D vector with f64 coordinates, used for tangent and direction calculations.
61#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
62#[repr(C)]
63pub struct SvgVector {
64    pub x: f64,
65    pub y: f64,
66}
67
68/// A quadratic bezier curve defined by start, one control point, and end point.
69#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
70#[repr(C)]
71pub struct SvgQuadraticCurve {
72    pub start: SvgPoint,
73    pub ctrl: SvgPoint,
74    pub end: SvgPoint,
75}
76
77impl_option!(
78    SvgPoint,
79    OptionSvgPoint,
80    [Debug, Clone, PartialEq, PartialOrd]
81);
82
83impl SvgPoint {
84    /// Creates a new SvgPoint from x and y coordinates
85    #[inline]
86    pub const fn new(x: f32, y: f32) -> Self {
87        Self { x, y }
88    }
89
90    /// Returns the Euclidean distance between this point and `other`.
91    #[inline]
92    pub fn distance(&self, other: Self) -> f64 {
93        let dx = other.x - self.x;
94        let dy = other.y - self.y;
95        libm::hypotf(dx, dy) as f64
96    }
97}
98
99impl SvgRect {
100    /// Expands this rect to also contain `other`.
101    pub fn union_with(&mut self, other: &Self) {
102        let self_max_x = self.x + self.width;
103        let self_max_y = self.y + self.height;
104        let self_min_x = self.x;
105        let self_min_y = self.y;
106
107        let other_max_x = other.x + other.width;
108        let other_max_y = other.y + other.height;
109        let other_min_x = other.x;
110        let other_min_y = other.y;
111
112        let max_x = self_max_x.max(other_max_x);
113        let max_y = self_max_y.max(other_max_y);
114        let min_x = self_min_x.min(other_min_x);
115        let min_y = self_min_y.min(other_min_y);
116
117        self.x = min_x;
118        self.y = min_y;
119        self.width = max_x - min_x;
120        self.height = max_y - min_y;
121    }
122
123    /// Note: does not incorporate rounded edges!
124    /// Origin of x and y is assumed to be the top left corner
125    pub fn contains_point(&self, point: SvgPoint) -> bool {
126        point.x > self.x
127            && point.x < self.x + self.width
128            && point.y > self.y
129            && point.y < self.y + self.height
130    }
131
132    /// Expands the rect with a certain amount of padding
133    pub fn expand(
134        &self,
135        padding_top: f32,
136        padding_bottom: f32,
137        padding_left: f32,
138        padding_right: f32,
139    ) -> SvgRect {
140        SvgRect {
141            width: self.width + padding_left + padding_right,
142            height: self.height + padding_top + padding_bottom,
143            x: self.x - padding_left,
144            y: self.y - padding_top,
145            ..*self
146        }
147    }
148
149    /// Returns the center point of the rect.
150    pub fn get_center(&self) -> SvgPoint {
151        SvgPoint {
152            x: self.x + (self.width / 2.0),
153            y: self.y + (self.height / 2.0),
154        }
155    }
156}
157
158const STEP_SIZE: usize = 20;
159const STEP_SIZE_F64: f64 = 0.05;
160
161impl SvgCubicCurve {
162    /// Creates a new SvgCubicCurve from start, two control points, and end point
163    #[inline]
164    pub const fn new(start: SvgPoint, ctrl_1: SvgPoint, ctrl_2: SvgPoint, end: SvgPoint) -> Self {
165        Self { start, ctrl_1, ctrl_2, end }
166    }
167
168    /// Reverses the curve direction in place, swapping start/end and ctrl_1/ctrl_2.
169    pub fn reverse(&mut self) {
170        core::mem::swap(&mut self.start, &mut self.end);
171        core::mem::swap(&mut self.ctrl_1, &mut self.ctrl_2);
172    }
173
174    /// Returns the start point of the curve.
175    pub fn get_start(&self) -> SvgPoint {
176        self.start
177    }
178    /// Returns the end point of the curve.
179    pub fn get_end(&self) -> SvgPoint {
180        self.end
181    }
182
183    /// Evaluates the x coordinate of the curve at parameter `t` in [0, 1].
184    pub fn get_x_at_t(&self, t: f64) -> f64 {
185        let c_x = 3.0 * (self.ctrl_1.x as f64 - self.start.x as f64);
186        let b_x = 3.0 * (self.ctrl_2.x as f64 - self.ctrl_1.x as f64) - c_x;
187        let a_x = self.end.x as f64 - self.start.x as f64 - c_x - b_x;
188
189        (a_x * t * t * t) + (b_x * t * t) + (c_x * t) + self.start.x as f64
190    }
191
192    /// Evaluates the y coordinate of the curve at parameter `t` in [0, 1].
193    pub fn get_y_at_t(&self, t: f64) -> f64 {
194        let c_y = 3.0 * (self.ctrl_1.y as f64 - self.start.y as f64);
195        let b_y = 3.0 * (self.ctrl_2.y as f64 - self.ctrl_1.y as f64) - c_y;
196        let a_y = self.end.y as f64 - self.start.y as f64 - c_y - b_y;
197
198        (a_y * t * t * t) + (b_y * t * t) + (c_y * t) + self.start.y as f64
199    }
200
201    /// Returns the approximate arc length of the curve using linear sampling.
202    pub fn get_length(&self) -> f64 {
203        // NOTE: this arc length parametrization is not very precise, but fast
204        let mut arc_length = 0.0;
205        let mut prev_point = self.get_start();
206
207        for i in 0..STEP_SIZE {
208            let t_next = (i + 1) as f64 * STEP_SIZE_F64;
209            let next_point = SvgPoint {
210                x: self.get_x_at_t(t_next) as f32,
211                y: self.get_y_at_t(t_next) as f32,
212            };
213            arc_length += prev_point.distance(next_point);
214            prev_point = next_point;
215        }
216
217        arc_length
218    }
219
220    /// Returns the parameter `t` corresponding to a given arc-length `offset`.
221    pub fn get_t_at_offset(&self, offset: f64) -> f64 {
222        // step through the line until the offset is reached,
223        // then interpolate linearly between the
224        // current at the last sampled point
225        let mut arc_length = 0.0;
226        let mut t_current = 0.0;
227        let mut prev_point = self.get_start();
228
229        for i in 0..STEP_SIZE {
230            let t_next = (i + 1) as f64 * STEP_SIZE_F64;
231            let next_point = SvgPoint {
232                x: self.get_x_at_t(t_next) as f32,
233                y: self.get_y_at_t(t_next) as f32,
234            };
235
236            let distance = prev_point.distance(next_point);
237
238            arc_length += distance;
239
240            // linearly interpolate between last t and current t
241            if arc_length > offset {
242                let remaining = arc_length - offset;
243                return t_current + ((distance - remaining) / distance) * STEP_SIZE_F64;
244            }
245
246            prev_point = next_point;
247            t_current = t_next;
248        }
249
250        t_current
251    }
252
253    /// Returns the normalized tangent vector at parameter `t`.
254    pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
255        // 1. Calculate the derivative of the bezier curve.
256        //
257        // This means that we go from 4 points to 3 points and redistribute
258        // the weights of the control points according to the formula:
259        //
260        // w'0 = 3 * (w1-w0)
261        // w'1 = 3 * (w2-w1)
262        // w'2 = 3 * (w3-w2)
263
264        let w0 = SvgPoint {
265            x: self.ctrl_1.x - self.start.x,
266            y: self.ctrl_1.y - self.start.y,
267        };
268
269        let w1 = SvgPoint {
270            x: self.ctrl_2.x - self.ctrl_1.x,
271            y: self.ctrl_2.y - self.ctrl_1.y,
272        };
273
274        let w2 = SvgPoint {
275            x: self.end.x - self.ctrl_2.x,
276            y: self.end.y - self.ctrl_2.y,
277        };
278
279        let quadratic_curve = SvgQuadraticCurve {
280            start: w0,
281            ctrl: w1,
282            end: w2,
283        };
284
285        // The first derivative of a cubic bezier curve is a quadratic
286        // bezier curve. Luckily, the first derivative is also the tangent
287        // vector (slope) of the curve. So all we need to do is to sample the
288        // quadratic curve at t
289        let tangent_vector = SvgVector {
290            x: quadratic_curve.get_x_at_t(t),
291            y: quadratic_curve.get_y_at_t(t),
292        };
293
294        tangent_vector.normalize()
295    }
296
297    /// Returns the axis-aligned bounding box of the curve's control points.
298    pub fn get_bounds(&self) -> SvgRect {
299        let min_x = self
300            .start
301            .x
302            .min(self.end.x)
303            .min(self.ctrl_1.x)
304            .min(self.ctrl_2.x);
305        let max_x = self
306            .start
307            .x
308            .max(self.end.x)
309            .max(self.ctrl_1.x)
310            .max(self.ctrl_2.x);
311
312        let min_y = self
313            .start
314            .y
315            .min(self.end.y)
316            .min(self.ctrl_1.y)
317            .min(self.ctrl_2.y);
318        let max_y = self
319            .start
320            .y
321            .max(self.end.y)
322            .max(self.ctrl_1.y)
323            .max(self.ctrl_2.y);
324
325        let width = (max_x - min_x).abs();
326        let height = (max_y - min_y).abs();
327
328        SvgRect {
329            width,
330            height,
331            x: min_x,
332            y: min_y,
333            ..SvgRect::default()
334        }
335    }
336}
337
338impl SvgVector {
339    /// Returns the angle of the vector in degrees
340    #[inline]
341    pub fn angle_degrees(&self) -> f64 {
342        (-self.y).atan2(self.x).to_degrees()
343    }
344
345    /// Returns a unit-length vector in the same direction, or zero if the length is zero.
346    #[inline]
347    #[must_use = "returns a new vector"]
348    pub fn normalize(&self) -> Self {
349        let tangent_length = libm::hypot(self.x, self.y);
350        if tangent_length == 0.0 {
351            return Self { x: 0.0, y: 0.0 };
352        }
353        Self {
354            x: self.x / tangent_length,
355            y: self.y / tangent_length,
356        }
357    }
358
359    /// Rotate the vector 90 degrees counter-clockwise
360    #[must_use = "returns a new vector"]
361    #[inline]
362    pub fn rotate_90deg_ccw(&self) -> Self {
363        Self {
364            x: -self.y,
365            y: self.x,
366        }
367    }
368}
369
370impl SvgQuadraticCurve {
371    /// Creates a new SvgQuadraticCurve from start, control, and end points
372    #[inline]
373    pub const fn new(start: SvgPoint, ctrl: SvgPoint, end: SvgPoint) -> Self {
374        Self { start, ctrl, end }
375    }
376
377    /// Reverses the curve direction in place.
378    pub fn reverse(&mut self) {
379        core::mem::swap(&mut self.start, &mut self.end);
380    }
381    /// Returns the start point of the curve.
382    pub fn get_start(&self) -> SvgPoint {
383        self.start
384    }
385    /// Returns the end point of the curve.
386    pub fn get_end(&self) -> SvgPoint {
387        self.end
388    }
389    /// Returns the axis-aligned bounding box of the curve's control points.
390    pub fn get_bounds(&self) -> SvgRect {
391        let min_x = self.start.x.min(self.end.x).min(self.ctrl.x);
392        let max_x = self.start.x.max(self.end.x).max(self.ctrl.x);
393
394        let min_y = self.start.y.min(self.end.y).min(self.ctrl.y);
395        let max_y = self.start.y.max(self.end.y).max(self.ctrl.y);
396
397        let width = (max_x - min_x).abs();
398        let height = (max_y - min_y).abs();
399
400        SvgRect {
401            width,
402            height,
403            x: min_x,
404            y: min_y,
405            ..SvgRect::default()
406        }
407    }
408
409    /// Evaluates the x coordinate of the curve at parameter `t` in [0, 1].
410    pub fn get_x_at_t(&self, t: f64) -> f64 {
411        let one_minus = 1.0 - t;
412        one_minus * one_minus * self.start.x as f64
413            + 2.0 * one_minus * t * self.ctrl.x as f64
414            + t * t * self.end.x as f64
415    }
416
417    /// Evaluates the y coordinate of the curve at parameter `t` in [0, 1].
418    pub fn get_y_at_t(&self, t: f64) -> f64 {
419        let one_minus = 1.0 - t;
420        one_minus * one_minus * self.start.y as f64
421            + 2.0 * one_minus * t * self.ctrl.y as f64
422            + t * t * self.end.y as f64
423    }
424
425    /// Returns the approximate arc length by converting to a cubic curve.
426    pub fn get_length(&self) -> f64 {
427        self.to_cubic().get_length()
428    }
429
430    /// Returns the parameter `t` corresponding to a given arc-length `offset`.
431    pub fn get_t_at_offset(&self, offset: f64) -> f64 {
432        self.to_cubic().get_t_at_offset(offset)
433    }
434
435    /// Returns the normalized tangent vector at parameter `t`.
436    pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
437        self.to_cubic().get_tangent_vector_at_t(t)
438    }
439
440    /// Converts this quadratic curve to an equivalent cubic bezier curve.
441    fn to_cubic(&self) -> SvgCubicCurve {
442        SvgCubicCurve {
443            start: self.start,
444            ctrl_1: SvgPoint {
445                x: self.start.x + (2.0 / 3.0) * (self.ctrl.x - self.start.x),
446                y: self.start.y + (2.0 / 3.0) * (self.ctrl.y - self.start.y),
447            },
448            ctrl_2: SvgPoint {
449                x: self.end.x + (2.0 / 3.0) * (self.ctrl.x - self.end.x),
450                y: self.end.y + (2.0 / 3.0) * (self.ctrl.y - self.end.y),
451            },
452            end: self.end,
453        }
454    }
455}
456
457impl AnimationInterpolationFunction {
458    /// Returns the cubic bezier curve corresponding to this timing function.
459    pub const fn get_curve(self) -> SvgCubicCurve {
460        match self {
461            AnimationInterpolationFunction::Ease => SvgCubicCurve {
462                start: SvgPoint { x: 0.0, y: 0.0 },
463                ctrl_1: SvgPoint { x: 0.25, y: 0.1 },
464                ctrl_2: SvgPoint { x: 0.25, y: 1.0 },
465                end: SvgPoint { x: 1.0, y: 1.0 },
466            },
467            AnimationInterpolationFunction::Linear => SvgCubicCurve {
468                start: SvgPoint { x: 0.0, y: 0.0 },
469                ctrl_1: SvgPoint { x: 0.0, y: 0.0 },
470                ctrl_2: SvgPoint { x: 1.0, y: 1.0 },
471                end: SvgPoint { x: 1.0, y: 1.0 },
472            },
473            AnimationInterpolationFunction::EaseIn => SvgCubicCurve {
474                start: SvgPoint { x: 0.0, y: 0.0 },
475                ctrl_1: SvgPoint { x: 0.42, y: 0.0 },
476                ctrl_2: SvgPoint { x: 1.0, y: 1.0 },
477                end: SvgPoint { x: 1.0, y: 1.0 },
478            },
479            AnimationInterpolationFunction::EaseOut => SvgCubicCurve {
480                start: SvgPoint { x: 0.0, y: 0.0 },
481                ctrl_1: SvgPoint { x: 0.0, y: 0.0 },
482                ctrl_2: SvgPoint { x: 0.58, y: 1.0 },
483                end: SvgPoint { x: 1.0, y: 1.0 },
484            },
485            AnimationInterpolationFunction::EaseInOut => SvgCubicCurve {
486                start: SvgPoint { x: 0.0, y: 0.0 },
487                ctrl_1: SvgPoint { x: 0.42, y: 0.0 },
488                ctrl_2: SvgPoint { x: 0.58, y: 1.0 },
489                end: SvgPoint { x: 1.0, y: 1.0 },
490            },
491            AnimationInterpolationFunction::CubicBezier(c) => c,
492        }
493    }
494
495    /// Evaluates the interpolation function at time `t`, returning the eased value.
496    pub fn evaluate(self, t: f64) -> f32 {
497        self.get_curve().get_y_at_t(t) as f32
498    }
499}