1use std::cell::{Cell, RefCell};
6use std::panic::{self, AssertUnwindSafe};
7use std::rc::Rc;
8use std::time::{Duration, Instant};
9
10use crabshell_reactive::{ReadSignal, Signal, 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 InSine,
39 OutSine,
41 InOutSine,
43 InQuart,
45 OutQuart,
47 InOutQuart,
49 InQuint,
51 OutQuint,
53 InOutQuint,
55 InExpo,
57 OutExpo,
59 InOutExpo,
61 InCirc,
63 OutCirc,
65 InOutCirc,
67 InBack,
69 OutBack,
71 InOutBack,
73 Bezier(f32, f32, f32, f32),
77 Spline(&'static [[f32; 6]]),
97}
98
99impl Easing {
100 pub fn value_at(self, t: f32) -> f32 {
104 use std::f32::consts::PI;
105 const C1: f32 = 1.70158;
107 const C2: f32 = C1 * 1.525;
108 let t = if t.is_nan() { 1.0 } else { t.clamp(0.0, 1.0) };
109 match self {
110 Easing::Linear => t,
111 Easing::InQuad => t * t,
112 Easing::OutQuad => 1.0 - (1.0 - t) * (1.0 - t),
113 Easing::InOutQuad if t < 0.5 => 2.0 * t * t,
114 Easing::InOutQuad => 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0,
115 Easing::InCubic => t * t * t,
116 Easing::OutCubic => 1.0 - (1.0 - t).powi(3),
117 Easing::InOutCubic if t < 0.5 => 4.0 * t * t * t,
118 Easing::InOutCubic => 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0,
119 Easing::InSine => 1.0 - (t * PI / 2.0).cos(),
120 Easing::OutSine => (t * PI / 2.0).sin(),
121 Easing::InOutSine => (1.0 - (PI * t).cos()) / 2.0,
122 Easing::InQuart => t.powi(4),
123 Easing::OutQuart => 1.0 - (1.0 - t).powi(4),
124 Easing::InOutQuart if t < 0.5 => 8.0 * t.powi(4),
125 Easing::InOutQuart => 1.0 - (-2.0 * t + 2.0).powi(4) / 2.0,
126 Easing::InQuint => t.powi(5),
127 Easing::OutQuint => 1.0 - (1.0 - t).powi(5),
128 Easing::InOutQuint if t < 0.5 => 16.0 * t.powi(5),
129 Easing::InOutQuint => 1.0 - (-2.0 * t + 2.0).powi(5) / 2.0,
130 Easing::InExpo | Easing::OutExpo | Easing::InOutExpo if t == 0.0 || t == 1.0 => t,
132 Easing::InExpo => 2_f32.powf(10.0 * t - 10.0),
133 Easing::OutExpo => 1.0 - 2_f32.powf(-10.0 * t),
134 Easing::InOutExpo if t < 0.5 => 2_f32.powf(20.0 * t - 10.0) / 2.0,
135 Easing::InOutExpo => (2.0 - 2_f32.powf(-20.0 * t + 10.0)) / 2.0,
136 Easing::InCirc => 1.0 - (1.0 - t * t).sqrt(),
137 Easing::OutCirc => (1.0 - (t - 1.0).powi(2)).sqrt(),
138 Easing::InOutCirc if t < 0.5 => (1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0,
139 Easing::InOutCirc => ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0,
140 Easing::InBack => (C1 + 1.0) * t.powi(3) - C1 * t * t,
141 Easing::OutBack => 1.0 + (C1 + 1.0) * (t - 1.0).powi(3) + C1 * (t - 1.0).powi(2),
142 Easing::InOutBack if t < 0.5 => (2.0 * t).powi(2) * ((C2 + 1.0) * 2.0 * t - C2) / 2.0,
143 Easing::InOutBack => {
144 ((2.0 * t - 2.0).powi(2) * ((C2 + 1.0) * (2.0 * t - 2.0) + C2) + 2.0) / 2.0
145 }
146 Easing::Bezier(x1, y1, x2, y2) => bezier(t, (0.0, 0.0), [x1, y1, x2, y2, 1.0, 1.0]),
147 Easing::Spline(segments) => spline(t, segments),
148 }
149 }
150}
151
152fn spline(x: f32, segments: &[[f32; 6]]) -> f32 {
155 let mut end = 0.0;
156 let valid = segments.iter().all(|segment| {
157 let forward = segment[4] >= end;
158 end = segment[4];
159 forward && segment.iter().all(|n| n.is_finite())
160 }) && segments
161 .last()
162 .is_some_and(|last| last[4] == 1.0 && last[5] == 1.0);
163 if !valid {
164 return x;
165 }
166 let mut start = (0.0, 0.0);
167 for segment in segments {
168 if x <= segment[4] {
169 return bezier(x, start, *segment);
170 }
171 start = (segment[4], segment[5]);
172 }
173 1.0
174}
175
176fn bezier(x: f32, (x0, y0): (f32, f32), segment: [f32; 6]) -> f32 {
181 let [x1, y1, x2, y2, x3, y3] = segment;
182 let (x1, x2) = (x1.clamp(x0, x3), x2.clamp(x0, x3));
183 let at = |p0: f32, p1: f32, p2: f32, p3: f32, s: f32| {
184 let r = 1.0 - s;
185 r * r * r * p0 + 3.0 * r * r * s * p1 + 3.0 * r * s * s * p2 + s * s * s * p3
186 };
187 let (mut low, mut high) = (0.0_f32, 1.0_f32);
188 for _ in 0..32 {
189 let middle = (low + high) / 2.0;
190 if at(x0, x1, x2, x3, middle) < x {
191 low = middle;
192 } else {
193 high = middle;
194 }
195 }
196 at(y0, y1, y2, y3, (low + high) / 2.0)
197}
198
199#[derive(Clone, Copy, Debug, PartialEq)]
202pub struct Transition {
203 pub ms: u32,
205 pub easing: Easing,
207 pub delay: u32,
209 pub alternate: bool,
212}
213
214impl Transition {
215 pub const fn new(ms: u32) -> Transition {
217 Transition {
218 ms,
219 easing: Easing::OutCubic,
220 delay: 0,
221 alternate: false,
222 }
223 }
224
225 pub const fn easing(self, easing: Easing) -> Transition {
227 Transition { easing, ..self }
228 }
229
230 pub const fn delay(self, ms: u32) -> Transition {
232 Transition { delay: ms, ..self }
233 }
234
235 pub const fn alternate(self) -> Transition {
238 Transition {
239 alternate: true,
240 ..self
241 }
242 }
243
244 pub(crate) fn is_instant(self) -> bool {
246 self.ms == 0 && self.delay == 0
247 }
248
249 pub(crate) fn waiting(self, start: Instant, now: Instant) -> bool {
252 now.saturating_duration_since(start) < Duration::from_millis(self.delay.into())
253 }
254
255 pub(crate) fn progress(self, start: Instant, now: Instant) -> (f32, bool) {
258 let elapsed = now.saturating_duration_since(start).as_secs_f32() * 1000.0 - self.delay as f32;
259 if elapsed < 0.0 {
260 return (0.0, false);
261 }
262 if self.ms == 0 {
263 return (1.0, true);
264 }
265 let t = elapsed / self.ms as f32;
266 if t >= 1.0 {
267 (1.0, true)
268 } else {
269 (self.easing.value_at(t), false)
270 }
271 }
272}
273
274#[derive(Clone, Copy, Debug, PartialEq)]
283pub struct Spring {
284 pub stiffness: f32,
286 pub damping: f32,
288}
289
290impl Spring {
291 pub const fn new(stiffness: f32, damping: f32) -> Spring {
293 Spring { stiffness, damping }
294 }
295
296 fn is_valid(self) -> bool {
299 self.stiffness.is_finite()
300 && self.stiffness > 0.0
301 && self.damping.is_finite()
302 && self.damping >= 0.0
303 }
304
305 fn advance(self, x: &mut f64, v: &mut f64, goal: f64, seconds: f64) {
309 let k = f64::from(self.stiffness);
310 let c = 2.0 * f64::from(self.damping) * k.sqrt();
311 let steps = (seconds * 1000.0).ceil().max(1.0);
312 let dt = seconds / steps;
313 for _ in 0..steps as u32 {
314 *v = (*v - dt * k * (*x - goal)) / (1.0 + dt * c + dt * dt * k);
315 *x += dt * *v;
316 }
317 }
318}
319
320impl Default for Spring {
321 fn default() -> Spring {
322 Spring::new(700.0, 0.9)
323 }
324}
325
326const SPRING_MAX_STEP: f64 = 0.25;
329
330const SPRING_REST: (f64, f64) = (0.001, 0.01);
333
334pub trait Animate: Clone + PartialEq + 'static {
338 fn lerp(&self, to: &Self, t: f32) -> Self;
340}
341
342impl Animate for f32 {
343 fn lerp(&self, to: &f32, t: f32) -> f32 {
344 self + (to - self) * t
345 }
346}
347
348impl Animate for Point {
349 fn lerp(&self, to: &Point, t: f32) -> Point {
350 Point::new(self.x.lerp(&to.x, t), self.y.lerp(&to.y, t))
351 }
352}
353
354impl Animate for Color {
355 fn lerp(&self, to: &Color, t: f32) -> Color {
356 let channel = |a: u8, b: u8| {
357 f32::from(a)
358 .lerp(&f32::from(b), t)
359 .round()
360 .clamp(0.0, 255.0) as u8
361 };
362 Color::rgba(
363 channel(self.r, to.r),
364 channel(self.g, to.g),
365 channel(self.b, to.b),
366 channel(self.a, to.a),
367 )
368 }
369}
370
371type Step = Box<dyn FnMut(Instant) -> bool>;
374
375thread_local! {
376 static RUNNING: RefCell<Vec<(Rc<str>, Step)>> = const { RefCell::new(Vec::new()) };
379 static LAST_TICK: Cell<Option<Instant>> = const { Cell::new(None) };
381}
382
383pub(crate) fn start(label: Rc<str>, step: Step) {
386 RUNNING.with(|running| running.borrow_mut().push((label, step)));
387}
388
389pub(crate) fn start_time() -> Option<Instant> {
393 if animating() {
394 LAST_TICK.with(Cell::get)
395 } else {
396 None
397 }
398}
399
400pub fn animating() -> bool {
403 RUNNING.with(|running| !running.borrow().is_empty())
404}
405
406pub fn advance_animations(now: Instant) -> bool {
411 LAST_TICK.with(|last| last.set(Some(now)));
412 let steps = RUNNING.with(|running| std::mem::take(&mut *running.borrow_mut()));
413 let mut kept = Vec::with_capacity(steps.len());
414 for (label, mut step) in steps {
415 match panic::catch_unwind(AssertUnwindSafe(|| step(now))) {
416 Ok(true) => kept.push((label, step)),
417 Ok(false) => drop_isolated(&label, step),
418 Err(payload) => {
419 tracing::error!(
420 "animation {label} panicked and stops: {}",
421 panic_message(payload.as_ref())
422 );
423 drop_isolated(&label, step);
424 }
425 }
426 }
427 RUNNING.with(|running| {
428 let mut running = running.borrow_mut();
430 kept.append(&mut running);
431 *running = kept;
432 if running.is_empty() {
433 LAST_TICK.with(|last| last.set(None));
436 }
437 !running.is_empty()
438 })
439}
440
441fn drop_isolated(label: &str, step: Step) {
443 if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(move || drop(step))) {
444 tracing::error!(
445 "dropping animation {label} panicked: {}",
446 panic_message(payload.as_ref())
447 );
448 }
449}
450
451fn guarded<R>(label: &str, f: impl FnOnce() -> R) -> Option<R> {
454 match panic::catch_unwind(AssertUnwindSafe(f)) {
455 Ok(value) => Some(value),
456 Err(payload) => {
457 tracing::error!(
458 "animation {label} panicked and stops: {}",
459 panic_message(payload.as_ref())
460 );
461 None
462 }
463 }
464}
465
466fn set_changed<T: PartialEq + 'static>(signal: Signal<T>, value: T) {
469 if untrack(|| signal.try_with(|old| *old != value)) == Some(true) {
470 signal.try_set(value);
471 }
472}
473
474pub struct Animation<T: 'static> {
477 value: ReadSignal<T>,
478 running: ReadSignal<bool>,
479}
480
481impl<T> Clone for Animation<T> {
482 fn clone(&self) -> Self {
483 *self
484 }
485}
486
487impl<T> Copy for Animation<T> {}
488
489impl<T> Animation<T> {
490 pub fn value(&self) -> ReadSignal<T> {
492 self.value
493 }
494
495 pub fn running(&self) -> ReadSignal<bool> {
499 self.running
500 }
501}
502
503impl Cx {
504 #[track_caller]
520 pub fn animate<T: Animate>(
521 &self,
522 target: impl FnMut() -> T + 'static,
523 transition: Transition,
524 ) -> ReadSignal<T> {
525 self.follow("cx.animate", target, transition).value
526 }
527
528 #[track_caller]
547 pub fn animation<T: Animate>(
548 &self,
549 target: impl FnMut() -> T + 'static,
550 transition: Transition,
551 ) -> Animation<T> {
552 self.follow("cx.animation", target, transition)
553 }
554
555 #[track_caller]
557 fn follow<T: Animate>(
558 &self,
559 what: &str,
560 mut target: impl FnMut() -> T + 'static,
561 transition: Transition,
562 ) -> Animation<T> {
563 let label: Rc<str> = format!("{what} at {}", std::panic::Location::caller()).into();
564 let first = untrack(&mut target);
565 let value = self.signal(first.clone());
566 let running = self.signal(false);
567 let mut last = first;
568 let generation = Rc::new(Cell::new(0_u64));
570 let mut broken = false;
571 self.effect(move || {
572 if broken {
573 return;
574 }
575 let Some(to) = guarded(&label, &mut target) else {
576 broken = true;
577 generation.set(generation.get() + 1);
578 set_changed(running, false);
579 return;
580 };
581 if to == last {
582 return;
583 }
584 last = to.clone();
585 let Some(from) = untrack(|| value.try_get()) else {
586 return;
587 };
588 let current = generation.get() + 1;
589 generation.set(current);
590 if transition.is_instant() {
592 value.try_set(to);
593 set_changed(running, false);
594 return;
595 }
596 set_changed(running, true);
597 let generation = Rc::clone(&generation);
598 let mut started = start_time();
599 let step_label = Rc::clone(&label);
600 start(
601 Rc::clone(&label),
602 Box::new(move |now| {
603 if generation.get() != current {
604 return false;
605 }
606 let begun = *started.get_or_insert(now);
607 if transition.waiting(begun, now) {
608 return true;
610 }
611 let (t, ended) = transition.progress(begun, now);
612 let Some(next) = guarded(&step_label, || {
615 if ended { to.clone() } else { from.lerp(&to, t) }
616 }) else {
617 running.try_set(false);
618 return false;
619 };
620 let kept = value.try_set(next);
622 if ended {
623 running.try_set(false);
624 }
625 kept && !ended
626 }),
627 );
628 });
629 Animation {
630 value: value.read_only(),
631 running: running.read_only(),
632 }
633 }
634
635 #[track_caller]
657 pub fn cycle(
658 &self,
659 mut running: impl FnMut() -> bool + 'static,
660 transition: Transition,
661 ) -> ReadSignal<f32> {
662 let label: Rc<str> = format!("cx.cycle at {}", std::panic::Location::caller()).into();
663 let value = self.signal(0.0_f32);
664 let wanted = Rc::new(Cell::new(false));
666 let ticking = Rc::new(Cell::new(false));
667 let mut broken = false;
668 self.effect(move || {
669 let on = !broken
670 && guarded(&label, &mut running).unwrap_or_else(|| {
671 broken = true;
672 false
673 });
674 wanted.set(on);
675 if !on || ticking.get() || transition.ms == 0 {
676 return;
677 }
678 ticking.set(true);
679 let (wanted, ticking) = (Rc::clone(&wanted), Rc::clone(&ticking));
680 let leg = f64::from(transition.ms);
681 let period = if transition.alternate { 2.0 * leg } else { leg };
682 let mut started = start_time();
683 let mut round = 0_u64;
685 start(
686 Rc::clone(&label),
687 Box::new(move |now| {
688 let begun = *started.get_or_insert(now);
689 let elapsed = now.saturating_duration_since(begun).as_secs_f64() * 1000.0
690 - f64::from(transition.delay);
691 let now_round = if elapsed < 0.0 {
692 0
693 } else {
694 (elapsed / period) as u64 + 1
695 };
696 let ended = !wanted.get() && (now_round == 0 || now_round > round);
697 round = now_round;
698 if ended {
699 ticking.set(false);
700 value.try_set(0.0);
701 return false;
702 }
703 let phase = elapsed.max(0.0) % period / leg;
704 let t = if phase <= 1.0 { phase } else { 2.0 - phase };
705 value.try_set(transition.easing.value_at(t as f32))
706 }),
707 );
708 });
709 value.read_only()
710 }
711
712 #[track_caller]
728 pub fn frames(&self, mut running: impl FnMut() -> bool + 'static) -> ReadSignal<Duration> {
729 let label: Rc<str> = format!("cx.frames at {}", std::panic::Location::caller()).into();
730 let value = self.signal(Duration::ZERO);
731 let wanted = Rc::new(Cell::new(false));
732 let ticking = Rc::new(Cell::new(false));
733 let mut broken = false;
734 self.effect(move || {
735 let on = !broken
736 && guarded(&label, &mut running).unwrap_or_else(|| {
737 broken = true;
738 false
739 });
740 wanted.set(on);
741 if !on || ticking.get() {
742 return;
743 }
744 ticking.set(true);
745 let (wanted, ticking) = (Rc::clone(&wanted), Rc::clone(&ticking));
746 let mut started = start_time();
747 start(
748 Rc::clone(&label),
749 Box::new(move |now| {
750 if !wanted.get() {
751 ticking.set(false);
752 return false;
753 }
754 let begun = *started.get_or_insert(now);
755 value.try_set(now.saturating_duration_since(begun))
756 }),
757 );
758 });
759 value.read_only()
760 }
761
762 #[track_caller]
780 pub fn spring(
781 &self,
782 mut target: impl FnMut() -> f32 + 'static,
783 spring: Spring,
784 ) -> ReadSignal<f32> {
785 let label: Rc<str> = format!("cx.spring at {}", std::panic::Location::caller()).into();
786 let first = untrack(&mut target);
787 let value = self.signal(first);
788 let mut last = first;
789 let goal = Rc::new(Cell::new(first));
792 let ticking = Rc::new(Cell::new(false));
793 let mut broken = false;
794 self.effect(move || {
795 if broken {
796 return;
797 }
798 let Some(to) = guarded(&label, &mut target) else {
799 broken = true;
800 goal.set(f32::NAN);
801 return;
802 };
803 if to == last {
804 return;
805 }
806 last = to;
807 goal.set(to);
808 if !to.is_finite() || !spring.is_valid() {
809 value.try_set(to);
810 return;
811 }
812 if ticking.get() {
813 return;
814 }
815 let Some(from) = untrack(|| value.try_get()) else {
816 return;
817 };
818 ticking.set(true);
819 let (goal, ticking) = (Rc::clone(&goal), Rc::clone(&ticking));
820 let (mut x, mut v) = (f64::from(from), 0.0);
821 let mut previous = start_time();
822 start(
823 Rc::clone(&label),
824 Box::new(move |now| {
825 let to = f64::from(goal.get());
826 if !to.is_finite() {
827 ticking.set(false);
828 return false;
829 }
830 let seconds = previous.map_or(0.0, |previous| {
831 now
832 .saturating_duration_since(previous)
833 .as_secs_f64()
834 .min(SPRING_MAX_STEP)
835 });
836 previous = Some(now);
837 spring.advance(&mut x, &mut v, to, seconds);
838 let (near, slow) = SPRING_REST;
839 let rest = !(x.is_finite() && v.is_finite()) || ((x - to).abs() < near && v.abs() < slow);
841 if rest {
842 x = to;
843 }
844 let kept = value.try_set(x as f32);
845 if rest || !kept {
846 ticking.set(false);
847 }
848 kept && !rest
849 }),
850 );
851 });
852 value.read_only()
853 }
854}