Skip to main content

motion_canvas_rs/core/animation/
paint.rs

1use crate::core::animation::tween::Tweenable;
2use kurbo::Point;
3use peniko::{Brush, Color, ColorStop, ColorStops, Gradient, GradientKind};
4
5/// The default length and radius used when creating standard gradients.
6pub const DEFAULT_GRADIENT_LENGTH: f64 = 100.0;
7
8/// A representable and animatable paint property that can be either a solid color or a gradient.
9///
10/// Wraps `peniko::Color` and `peniko::Gradient` and implements `Tweenable` to enable
11/// smooth color-to-gradient and gradient-to-gradient transitions.
12#[derive(Clone, Debug, PartialEq)]
13pub enum Paint {
14    /// No paint, or fallback to legacy deprecated color signals if configured.
15    None,
16    /// A solid single-color paint.
17    Solid(Color),
18    /// A gradient paint (linear, radial, sweep, etc.).
19    Gradient(Gradient),
20}
21
22impl Paint {
23    /// Converts this `Paint` into a standard Peniko `Brush`.
24    pub fn to_brush(&self) -> Brush {
25        match self {
26            Paint::None => Brush::Solid(Color::TRANSPARENT),
27            Paint::Solid(color) => Brush::Solid(*color),
28            Paint::Gradient(grad) => Brush::Gradient(grad.clone()),
29        }
30    }
31
32    /// Resolves this `Paint` to a `Brush` and scales its transparency by the given opacity factor.
33    pub fn to_brush_with_opacity(&self, opacity: f32) -> Brush {
34        match self {
35            Paint::None => Brush::Solid(Color::TRANSPARENT),
36            Paint::Solid(color) => {
37                let mut c = *color;
38                c.a = (color.a as f32 * opacity).clamp(0.0, 255.0) as u8;
39                Brush::Solid(c)
40            }
41            Paint::Gradient(grad) => {
42                let mut stops = Vec::new();
43                for stop in grad.stops.iter() {
44                    let mut c = stop.color;
45                    c.a = (stop.color.a as f32 * opacity).clamp(0.0, 255.0) as u8;
46                    stops.push(ColorStop {
47                        offset: stop.offset,
48                        color: c,
49                    });
50                }
51                Brush::Gradient(Gradient {
52                    kind: grad.kind.clone(),
53                    extend: grad.extend,
54                    stops: ColorStops::from(stops),
55                })
56            }
57        }
58    }
59}
60
61impl From<Color> for Paint {
62    fn from(c: Color) -> Self {
63        Paint::Solid(c)
64    }
65}
66
67impl From<Gradient> for Paint {
68    fn from(g: Gradient) -> Self {
69        Paint::Gradient(g)
70    }
71}
72
73fn lerp(a: f32, b: f32, t: f32) -> f32 {
74    a + (b - a) * t
75}
76
77fn interpolate_point(p1: Point, p2: Point, t: f32) -> Point {
78    let t = t as f64;
79    Point::new(p1.x + (p2.x - p1.x) * t, p1.y + (p2.y - p1.y) * t)
80}
81
82/// Sample a gradient's color at a given offset by interpolating between adjacent stops.
83fn sample_color_at(stops: &[ColorStop], offset: f32) -> Color {
84    if stops.is_empty() {
85        return Color::TRANSPARENT;
86    }
87    if stops.len() == 1 || offset <= stops[0].offset {
88        return stops[0].color;
89    }
90    let last = stops.len() - 1;
91    if offset >= stops[last].offset {
92        return stops[last].color;
93    }
94    for i in 1..stops.len() {
95        if offset <= stops[i].offset {
96            let range = stops[i].offset - stops[i - 1].offset;
97            if range < 1e-6 {
98                return stops[i].color;
99            }
100            let local_t = (offset - stops[i - 1].offset) / range;
101            return Color::interpolate(&stops[i - 1].color, &stops[i].color, local_t);
102        }
103    }
104    stops[last].color
105}
106
107fn interpolate_stops(stops1: &[ColorStop], stops2: &[ColorStop], t: f32) -> ColorStops {
108    // Collect the union of all offsets from both stop arrays
109    let mut offsets: Vec<f32> = stops1
110        .iter()
111        .map(|s| s.offset)
112        .chain(stops2.iter().map(|s| s.offset))
113        .collect();
114    offsets.sort_by(|a, b| a.partial_cmp(b).unwrap());
115    offsets.dedup_by(|a, b| (*a - *b).abs() < 1e-4);
116
117    let mut result = Vec::with_capacity(offsets.len());
118    for &offset in &offsets {
119        let c1 = sample_color_at(stops1, offset);
120        let c2 = sample_color_at(stops2, offset);
121        result.push(ColorStop {
122            offset,
123            color: Color::interpolate(&c1, &c2, t),
124        });
125    }
126    ColorStops::from(result)
127}
128
129fn promote_solid_to_gradient(color: Color, target: &Gradient) -> Gradient {
130    let mut stops = Vec::new();
131    for stop in target.stops.iter() {
132        stops.push(ColorStop {
133            offset: stop.offset,
134            color,
135        });
136    }
137    Gradient {
138        kind: target.kind.clone(),
139        extend: target.extend,
140        stops: ColorStops::from(stops),
141    }
142}
143
144impl Tweenable for Paint {
145    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
146        let t = t.clamp(0.0, 1.0);
147        match (a, b) {
148            (Paint::None, Paint::None) => Paint::None,
149            (Paint::None, Paint::Solid(c)) => {
150                let mut start_c = *c;
151                start_c.a = 0;
152                Paint::Solid(Color::interpolate(&start_c, c, t))
153            }
154            (Paint::Solid(c), Paint::None) => {
155                let mut end_c = *c;
156                end_c.a = 0;
157                Paint::Solid(Color::interpolate(c, &end_c, t))
158            }
159            (Paint::None, Paint::Gradient(g)) => {
160                let mut transparent_g = g.clone();
161                let mut stops = Vec::new();
162                for stop in g.stops.iter() {
163                    let mut c = stop.color;
164                    c.a = 0;
165                    stops.push(ColorStop {
166                        offset: stop.offset,
167                        color: c,
168                    });
169                }
170                transparent_g.stops = ColorStops::from(stops);
171                Paint::interpolate(&Paint::Gradient(transparent_g), b, t)
172            }
173            (Paint::Gradient(g), Paint::None) => {
174                let mut transparent_g = g.clone();
175                let mut stops = Vec::new();
176                for stop in g.stops.iter() {
177                    let mut c = stop.color;
178                    c.a = 0;
179                    stops.push(ColorStop {
180                        offset: stop.offset,
181                        color: c,
182                    });
183                }
184                transparent_g.stops = ColorStops::from(stops);
185                Paint::interpolate(a, &Paint::Gradient(transparent_g), t)
186            }
187            (Paint::Solid(c1), Paint::Solid(c2)) => Paint::Solid(Color::interpolate(c1, c2, t)),
188            (Paint::Gradient(g1), Paint::Gradient(g2)) => {
189                let kind = match (&g1.kind, &g2.kind) {
190                    (
191                        GradientKind::Linear { start: s1, end: e1 },
192                        GradientKind::Linear { start: s2, end: e2 },
193                    ) => GradientKind::Linear {
194                        start: interpolate_point(*s1, *s2, t),
195                        end: interpolate_point(*e1, *e2, t),
196                    },
197                    (
198                        GradientKind::Radial {
199                            start_center: sc1,
200                            start_radius: sr1,
201                            end_center: ec1,
202                            end_radius: er1,
203                        },
204                        GradientKind::Radial {
205                            start_center: sc2,
206                            start_radius: sr2,
207                            end_center: ec2,
208                            end_radius: er2,
209                        },
210                    ) => GradientKind::Radial {
211                        start_center: interpolate_point(*sc1, *sc2, t),
212                        start_radius: lerp(*sr1, *sr2, t),
213                        end_center: interpolate_point(*ec1, *ec2, t),
214                        end_radius: lerp(*er1, *er2, t),
215                    },
216                    (k1, k2) => {
217                        if t < 0.5 {
218                            k1.clone()
219                        } else {
220                            k2.clone()
221                        }
222                    }
223                };
224
225                let extend = if t < 0.5 { g1.extend } else { g2.extend };
226                let stops = interpolate_stops(&g1.stops, &g2.stops, t);
227
228                Paint::Gradient(Gradient {
229                    kind,
230                    extend,
231                    stops,
232                })
233            }
234            (Paint::Solid(c), Paint::Gradient(g)) => {
235                let dummy_g = promote_solid_to_gradient(*c, g);
236                Paint::interpolate(&Paint::Gradient(dummy_g), b, t)
237            }
238            (Paint::Gradient(g), Paint::Solid(c)) => {
239                let dummy_g = promote_solid_to_gradient(*c, g);
240                Paint::interpolate(a, &Paint::Gradient(dummy_g), t)
241            }
242        }
243    }
244
245    fn state_hash(&self) -> u64 {
246        let mut h = crate::assets::hash::Hasher::new();
247        match self {
248            Paint::None => {
249                h.update_u64(0);
250            }
251            Paint::Solid(c) => {
252                h.update_u64(1);
253                h.update_u64(Color::state_hash(c));
254            }
255            Paint::Gradient(g) => {
256                h.update_u64(2);
257                match &g.kind {
258                    GradientKind::Linear { start, end } => {
259                        h.update_u64(0);
260                        h.update_u64(crate::assets::hash::hash_f32(start.x as f32));
261                        h.update_u64(crate::assets::hash::hash_f32(start.y as f32));
262                        h.update_u64(crate::assets::hash::hash_f32(end.x as f32));
263                        h.update_u64(crate::assets::hash::hash_f32(end.y as f32));
264                    }
265                    GradientKind::Radial {
266                        start_center,
267                        start_radius,
268                        end_center,
269                        end_radius,
270                    } => {
271                        h.update_u64(1);
272                        h.update_u64(crate::assets::hash::hash_f32(start_center.x as f32));
273                        h.update_u64(crate::assets::hash::hash_f32(start_center.y as f32));
274                        h.update_u64(crate::assets::hash::hash_f32(*start_radius));
275                        h.update_u64(crate::assets::hash::hash_f32(end_center.x as f32));
276                        h.update_u64(crate::assets::hash::hash_f32(end_center.y as f32));
277                        h.update_u64(crate::assets::hash::hash_f32(*end_radius));
278                    }
279                    _ => {
280                        h.update_u64(2);
281                    }
282                }
283                for stop in g.stops.iter() {
284                    h.update_u64(crate::assets::hash::hash_f32(stop.offset));
285                    h.update_u64(Color::state_hash(&stop.color));
286                }
287            }
288        }
289        h.finish()
290    }
291}
292
293/// Macro to create a linear gradient with N equidistant color stops.
294///
295/// Requires at least 2 colors. The gradient is centered with a default length of 100.0.
296///
297/// ### Example
298/// ```rust
299/// # use motion_canvas_rs::prelude::*;
300/// let grad = linear_gradient!(Color::RED, Color::BLUE);
301/// ```
302#[macro_export]
303macro_rules! linear_gradient {
304    ($($color:expr),+ $(,)?) => {{
305        let colors = [$($color),+];
306        assert!(colors.len() >= 2, "Gradients require at least 2 colors");
307        let mut stops = Vec::with_capacity(colors.len());
308        let n = colors.len() as f32;
309        for (i, &color) in colors.iter().enumerate() {
310            stops.push($crate::prelude::ColorStop {
311                offset: (i as f32) / (n - 1.0),
312                color,
313            });
314        }
315        $crate::prelude::Gradient {
316            kind: $crate::prelude::GradientKind::Linear {
317                start: $crate::prelude::Point::new(-$crate::core::animation::paint::DEFAULT_GRADIENT_LENGTH, 0.0),
318                end: $crate::prelude::Point::new($crate::core::animation::paint::DEFAULT_GRADIENT_LENGTH, 0.0),
319            },
320            extend: $crate::prelude::Extend::Pad,
321            stops: $crate::prelude::ColorStops::from(stops),
322        }
323    }};
324}
325
326/// Macro to create a radial gradient with N equidistant color stops.
327///
328/// Requires at least 2 colors. The gradient has a default outer radius of 100.0.
329///
330/// ### Example
331/// ```rust
332/// # use motion_canvas_rs::prelude::*;
333/// let grad = radial_gradient!(Color::RED, Color::BLUE);
334/// ```
335#[macro_export]
336macro_rules! radial_gradient {
337    ($($color:expr),+ $(,)?) => {{
338        let colors = [$($color),+];
339        assert!(colors.len() >= 2, "Gradients require at least 2 colors");
340        let mut stops = Vec::with_capacity(colors.len());
341        let n = colors.len() as f32;
342        for (i, &color) in colors.iter().enumerate() {
343            stops.push($crate::prelude::ColorStop {
344                offset: (i as f32) / (n - 1.0),
345                color,
346            });
347        }
348        $crate::prelude::Gradient {
349            kind: $crate::prelude::GradientKind::Radial {
350                start_center: $crate::prelude::Point::new(0.0, 0.0),
351                start_radius: 0.0,
352                end_center: $crate::prelude::Point::new(0.0, 0.0),
353                end_radius: $crate::core::animation::paint::DEFAULT_GRADIENT_LENGTH as f32,
354            },
355            extend: $crate::prelude::Extend::Pad,
356            stops: $crate::prelude::ColorStops::from(stops),
357        }
358    }};
359}