Skip to main content

azul_css/props/basic/
animation.rs

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