ravenrs_widgets/
lottie.rs1use 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#[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 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 pub fn size(mut self, width: f32, height: f32) -> Self {
64 self.width = width;
65 self.height = height;
66 self
67 }
68
69 pub fn looping(mut self, looping: bool) -> Self {
71 self.looping = looping;
72 self
73 }
74
75 pub fn speed(mut self, speed: f64) -> Self {
77 self.speed = speed;
78 self
79 }
80
81 pub fn playing(mut self, playing: ravenrs_reactive::Binding<bool>) -> Self {
83 self.playing = Some(playing);
84 self
85 }
86
87 pub fn alt(mut self, alt: impl Into<String>) -> Self {
89 self.alt = Some(alt.into());
90 self
91 }
92}
93
94fn 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 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 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 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 scene.push_clip(Rect::new(x, y, comp_w * scale, comp_h * scale));
134 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 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}