Skip to main content

ravenrs_widgets/
lottie.rs

1//! Plays a Lottie animation.
2
3use std::cell::Cell;
4use std::fmt;
5use std::rc::Rc;
6
7use ravenrs_core::{PathFill, Rect, Role};
8use ravenrs_layout::LayoutStyle;
9use ravenrs_lottie::Animation;
10
11use crate::context::{BuildCx, NodeId, Paint, Semantics};
12use crate::view::View;
13
14/// Plays a Lottie animation, drawn with curves on the GPU, sharp at any size. It
15/// sleeps when it ends or is paused, so that an idle window stays idle. Where motion
16/// is reduced it shows its last frame, still.
17///
18/// ```
19/// use ravenrs_widgets::{Lottie, LottieAnimation, View};
20///
21/// let json = br#"{"v":"5.5.7","fr":30,"ip":0,"op":30,"w":100,"h":100,"layers":[]}"#;
22/// let animation = LottieAnimation::from_json(json).expect("a Lottie");
23/// let view = Lottie::new(animation).size(96.0, 96.0).looping(false).alt("Our logo");
24/// ```
25#[derive(Clone)]
26#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
27pub struct Lottie {
28    animation: Animation,
29    width: f32,
30    height: f32,
31    looping: bool,
32    speed: f64,
33    playing: Option<ravenrs_reactive::Binding<bool>>,
34    alt: Option<String>,
35}
36
37impl fmt::Debug for Lottie {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.debug_struct("Lottie")
40            .field("width", &self.width)
41            .field("height", &self.height)
42            .field("looping", &self.looping)
43            .finish_non_exhaustive()
44    }
45}
46
47#[cfg_attr(feature = "live", ravenrs_live::expose)]
48impl Lottie {
49    /// Plays `animation` at its own size, over and over.
50    pub fn new(animation: Animation) -> Self {
51        Self {
52            width: animation.width(),
53            height: animation.height(),
54            animation,
55            looping: true,
56            speed: 1.0,
57            playing: None,
58            alt: None,
59        }
60    }
61
62    /// The size it takes, in logical pixels. It keeps its proportions inside it.
63    pub fn size(mut self, width: f32, height: f32) -> Self {
64        self.width = width;
65        self.height = height;
66        self
67    }
68
69    /// Whether it starts over at the end, or stays on its last frame.
70    pub fn looping(mut self, looping: bool) -> Self {
71        self.looping = looping;
72        self
73    }
74
75    /// How fast it plays, 1.0 being as authored.
76    pub fn speed(mut self, speed: f64) -> Self {
77        self.speed = speed;
78        self
79    }
80
81    /// Pauses it while this is false. Without one, it plays from when it first shows.
82    pub fn playing(mut self, playing: ravenrs_reactive::Binding<bool>) -> Self {
83        self.playing = Some(playing);
84        self
85    }
86
87    /// What screen readers say it shows. Without it, it is decoration they skip.
88    pub fn alt(mut self, alt: impl Into<String>) -> Self {
89        self.alt = Some(alt.into());
90        self
91    }
92}
93
94/// Where a moment is, given how long it has been playing.
95fn moment(played: f64, duration: f64, looping: bool) -> f64 {
96    if duration <= 0.0 {
97        0.0
98    } else if looping {
99        played.rem_euclid(duration)
100    } else {
101        played.min(duration)
102    }
103}
104
105impl View for Lottie {
106    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
107        let now = cx.now();
108        let reduced = cx.reduced_motion();
109        let duration = self.animation.duration();
110        // How far it has played, the clock when that was true, and whether it was
111        // playing then: nothing rebuilds it during a pause, which then does not count.
112        // Not reactive: writing it here must not ask for another build.
113        let state = cx.use_state(|| Rc::new(Cell::new((0.0_f64, now, true)))).peek().clone();
114        let (played_before, since, was_on) = state.get();
115        let on = self.playing.as_ref().is_none_or(|playing| playing.get());
116        // Frames between builds only paint, so a build that pauses it still counts up to now.
117        let ran = if was_on { (now - since).max(0.0) } else { 0.0 };
118        let played = played_before + ran * self.speed;
119        state.set((played, now, on));
120        let moves = on && !reduced && (self.looping || played < duration);
121        // Only until it ends, so that it can show its last frame and rest.
122        if on && !reduced && !self.looping && played < duration {
123            cx.wake_at(now + (duration - played) / self.speed.max(f64::EPSILON));
124        }
125
126        let (animation, speed, looping) = (self.animation.clone(), self.speed, self.looping);
127        let (comp_w, comp_h) = (animation.width().max(1.0), animation.height().max(1.0));
128        let draw = move |frame: Rect, at: f64, scene: &mut ravenrs_core::Scene| {
129            let scale = (frame.width() / comp_w).min(frame.height() / comp_h);
130            let x = frame.x() + (frame.width() - comp_w * scale) / 2.0;
131            let y = frame.y() + (frame.height() - comp_h * scale) / 2.0;
132            // What is past the edges of the composition is not part of the picture.
133            scene.push_clip(Rect::new(x, y, comp_w * scale, comp_h * scale));
134            // Twice for a dense display: only how finely strokes are cut depends on it.
135            for path in animation.paths_at(at, scale * 2.0) {
136                scene.push_path(PathFill { transform: [scale, 0.0, 0.0, scale, x, y], ..path });
137            }
138            scene.pop_clip();
139        };
140        let paint = if moves {
141            // Each frame repeats only the painting: the rest of the screen did not change.
142            Paint::Live(Rc::new(move |frame, time, scene| {
143                let played = played + (time - now).max(0.0) * speed;
144                draw(frame, moment(played, duration, looping), scene);
145            }))
146        } else {
147            let at = if reduced { duration } else { moment(played, duration, self.looping) };
148            Paint::Custom(Rc::new(move |frame, scene| draw(frame, at, scene)))
149        };
150        let node = cx.container(
151            LayoutStyle { width: Some(self.width), height: Some(self.height), ..LayoutStyle::leaf() },
152            &[],
153            paint,
154        );
155        match &self.alt {
156            Some(alt) => cx.semantics(node, Semantics::new(Role::Image).name(alt.clone())),
157            None => cx.semantics(node, Semantics::hidden()),
158        }
159        node
160    }
161}