1use std::cell::{Cell, RefCell};
6use std::panic::{self, AssertUnwindSafe};
7use std::rc::Rc;
8use std::time::Instant;
9
10use crabshell_reactive::{ReadSignal, untrack};
11
12use crate::cx::Cx;
13use crate::geometry::Point;
14use crate::input::panic_message;
15use crate::style::Color;
16
17#[derive(Clone, Copy, Debug, Default, PartialEq)]
20pub enum Easing {
21 Linear,
23 InQuad,
25 OutQuad,
27 InOutQuad,
29 InCubic,
31 #[default]
34 OutCubic,
35 InOutCubic,
37 Bezier(f32, f32, f32, f32),
41}
42
43impl Easing {
44 pub fn value_at(self, t: f32) -> f32 {
48 let t = if t.is_nan() { 1.0 } else { t.clamp(0.0, 1.0) };
49 match self {
50 Easing::Linear => t,
51 Easing::InQuad => t * t,
52 Easing::OutQuad => 1.0 - (1.0 - t) * (1.0 - t),
53 Easing::InOutQuad if t < 0.5 => 2.0 * t * t,
54 Easing::InOutQuad => 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0,
55 Easing::InCubic => t * t * t,
56 Easing::OutCubic => 1.0 - (1.0 - t).powi(3),
57 Easing::InOutCubic if t < 0.5 => 4.0 * t * t * t,
58 Easing::InOutCubic => 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0,
59 Easing::Bezier(x1, y1, x2, y2) => bezier(t, x1.clamp(0.0, 1.0), y1, x2.clamp(0.0, 1.0), y2),
60 }
61 }
62}
63
64fn bezier(x: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
68 let at = |a: f32, b: f32, s: f32| {
69 let r = 1.0 - s;
70 3.0 * r * r * s * a + 3.0 * r * s * s * b + s * s * s
71 };
72 let (mut low, mut high) = (0.0_f32, 1.0_f32);
73 for _ in 0..32 {
74 let middle = (low + high) / 2.0;
75 if at(x1, x2, middle) < x {
76 low = middle;
77 } else {
78 high = middle;
79 }
80 }
81 at(y1, y2, (low + high) / 2.0)
82}
83
84#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct Transition {
88 pub ms: u32,
90 pub easing: Easing,
92}
93
94impl Transition {
95 pub const fn new(ms: u32) -> Transition {
97 Transition {
98 ms,
99 easing: Easing::OutCubic,
100 }
101 }
102
103 pub const fn easing(self, easing: Easing) -> Transition {
105 Transition { easing, ..self }
106 }
107
108 pub(crate) fn progress(self, start: Instant, now: Instant) -> (f32, bool) {
111 if self.ms == 0 {
112 return (1.0, true);
113 }
114 let elapsed = now.saturating_duration_since(start).as_secs_f32() * 1000.0;
115 let t = elapsed / self.ms as f32;
116 if t >= 1.0 {
117 (1.0, true)
118 } else {
119 (self.easing.value_at(t), false)
120 }
121 }
122}
123
124pub trait Animate: Clone + PartialEq + 'static {
128 fn lerp(&self, to: &Self, t: f32) -> Self;
130}
131
132impl Animate for f32 {
133 fn lerp(&self, to: &f32, t: f32) -> f32 {
134 self + (to - self) * t
135 }
136}
137
138impl Animate for Point {
139 fn lerp(&self, to: &Point, t: f32) -> Point {
140 Point::new(self.x.lerp(&to.x, t), self.y.lerp(&to.y, t))
141 }
142}
143
144impl Animate for Color {
145 fn lerp(&self, to: &Color, t: f32) -> Color {
146 let channel = |a: u8, b: u8| {
147 f32::from(a)
148 .lerp(&f32::from(b), t)
149 .round()
150 .clamp(0.0, 255.0) as u8
151 };
152 Color::rgba(
153 channel(self.r, to.r),
154 channel(self.g, to.g),
155 channel(self.b, to.b),
156 channel(self.a, to.a),
157 )
158 }
159}
160
161type Step = Box<dyn FnMut(Instant) -> bool>;
164
165thread_local! {
166 static RUNNING: RefCell<Vec<(Rc<str>, Step)>> = const { RefCell::new(Vec::new()) };
169 static LAST_TICK: Cell<Option<Instant>> = const { Cell::new(None) };
171}
172
173pub(crate) fn start(label: Rc<str>, step: Step) {
176 RUNNING.with(|running| running.borrow_mut().push((label, step)));
177}
178
179pub(crate) fn start_time() -> Option<Instant> {
183 if animating() {
184 LAST_TICK.with(Cell::get)
185 } else {
186 None
187 }
188}
189
190pub fn animating() -> bool {
193 RUNNING.with(|running| !running.borrow().is_empty())
194}
195
196pub fn advance_animations(now: Instant) -> bool {
201 LAST_TICK.with(|last| last.set(Some(now)));
202 let steps = RUNNING.with(|running| std::mem::take(&mut *running.borrow_mut()));
203 let mut kept = Vec::with_capacity(steps.len());
204 for (label, mut step) in steps {
205 match panic::catch_unwind(AssertUnwindSafe(|| step(now))) {
206 Ok(true) => kept.push((label, step)),
207 Ok(false) => drop_isolated(&label, step),
208 Err(payload) => {
209 tracing::error!(
210 "animation {label} panicked and stops: {}",
211 panic_message(payload.as_ref())
212 );
213 drop_isolated(&label, step);
214 }
215 }
216 }
217 RUNNING.with(|running| {
218 let mut running = running.borrow_mut();
220 kept.append(&mut running);
221 *running = kept;
222 if running.is_empty() {
223 LAST_TICK.with(|last| last.set(None));
226 }
227 !running.is_empty()
228 })
229}
230
231fn drop_isolated(label: &str, step: Step) {
233 if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(move || drop(step))) {
234 tracing::error!(
235 "dropping animation {label} panicked: {}",
236 panic_message(payload.as_ref())
237 );
238 }
239}
240
241impl Cx {
242 #[track_caller]
257 pub fn animate<T: Animate>(
258 &self,
259 mut target: impl FnMut() -> T + 'static,
260 transition: Transition,
261 ) -> ReadSignal<T> {
262 let label: Rc<str> = format!("cx.animate at {}", std::panic::Location::caller()).into();
263 let first = untrack(&mut target);
264 let value = self.signal(first.clone());
265 let mut last = first;
266 let generation = Rc::new(Cell::new(0_u64));
268 self.effect(move || {
269 let to = target();
270 if to == last {
271 return;
272 }
273 last = to.clone();
274 let Some(from) = untrack(|| value.try_get()) else {
275 return;
276 };
277 let current = generation.get() + 1;
278 generation.set(current);
279 if transition.ms == 0 {
281 value.try_set(to);
282 return;
283 }
284 let generation = Rc::clone(&generation);
285 let mut started = start_time();
286 start(
287 Rc::clone(&label),
288 Box::new(move |now| {
289 if generation.get() != current {
290 return false;
291 }
292 let begun = *started.get_or_insert(now);
293 let (t, ended) = transition.progress(begun, now);
294 let next = if ended { to.clone() } else { from.lerp(&to, t) };
295 value.try_set(next) && !ended
297 }),
298 );
299 });
300 value.read_only()
301 }
302}