use std::cell::{Cell, RefCell};
use std::panic::{self, AssertUnwindSafe};
use std::rc::Rc;
use std::time::Instant;
use crabshell_reactive::{ReadSignal, untrack};
use crate::cx::Cx;
use crate::geometry::Point;
use crate::input::panic_message;
use crate::style::Color;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Easing {
Linear,
InQuad,
OutQuad,
InOutQuad,
InCubic,
#[default]
OutCubic,
InOutCubic,
Bezier(f32, f32, f32, f32),
}
impl Easing {
pub fn value_at(self, t: f32) -> f32 {
let t = if t.is_nan() { 1.0 } else { t.clamp(0.0, 1.0) };
match self {
Easing::Linear => t,
Easing::InQuad => t * t,
Easing::OutQuad => 1.0 - (1.0 - t) * (1.0 - t),
Easing::InOutQuad if t < 0.5 => 2.0 * t * t,
Easing::InOutQuad => 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0,
Easing::InCubic => t * t * t,
Easing::OutCubic => 1.0 - (1.0 - t).powi(3),
Easing::InOutCubic if t < 0.5 => 4.0 * t * t * t,
Easing::InOutCubic => 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0,
Easing::Bezier(x1, y1, x2, y2) => bezier(t, x1.clamp(0.0, 1.0), y1, x2.clamp(0.0, 1.0), y2),
}
}
}
fn bezier(x: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
let at = |a: f32, b: f32, s: f32| {
let r = 1.0 - s;
3.0 * r * r * s * a + 3.0 * r * s * s * b + s * s * s
};
let (mut low, mut high) = (0.0_f32, 1.0_f32);
for _ in 0..32 {
let middle = (low + high) / 2.0;
if at(x1, x2, middle) < x {
low = middle;
} else {
high = middle;
}
}
at(y1, y2, (low + high) / 2.0)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transition {
pub ms: u32,
pub easing: Easing,
}
impl Transition {
pub const fn new(ms: u32) -> Transition {
Transition {
ms,
easing: Easing::OutCubic,
}
}
pub const fn easing(self, easing: Easing) -> Transition {
Transition { easing, ..self }
}
pub(crate) fn progress(self, start: Instant, now: Instant) -> (f32, bool) {
if self.ms == 0 {
return (1.0, true);
}
let elapsed = now.saturating_duration_since(start).as_secs_f32() * 1000.0;
let t = elapsed / self.ms as f32;
if t >= 1.0 {
(1.0, true)
} else {
(self.easing.value_at(t), false)
}
}
}
pub trait Animate: Clone + PartialEq + 'static {
fn lerp(&self, to: &Self, t: f32) -> Self;
}
impl Animate for f32 {
fn lerp(&self, to: &f32, t: f32) -> f32 {
self + (to - self) * t
}
}
impl Animate for Point {
fn lerp(&self, to: &Point, t: f32) -> Point {
Point::new(self.x.lerp(&to.x, t), self.y.lerp(&to.y, t))
}
}
impl Animate for Color {
fn lerp(&self, to: &Color, t: f32) -> Color {
let channel = |a: u8, b: u8| {
f32::from(a)
.lerp(&f32::from(b), t)
.round()
.clamp(0.0, 255.0) as u8
};
Color::rgba(
channel(self.r, to.r),
channel(self.g, to.g),
channel(self.b, to.b),
channel(self.a, to.a),
)
}
}
type Step = Box<dyn FnMut(Instant) -> bool>;
thread_local! {
static RUNNING: RefCell<Vec<(Rc<str>, Step)>> = const { RefCell::new(Vec::new()) };
static LAST_TICK: Cell<Option<Instant>> = const { Cell::new(None) };
}
pub(crate) fn start(label: Rc<str>, step: Step) {
RUNNING.with(|running| running.borrow_mut().push((label, step)));
}
pub(crate) fn start_time() -> Option<Instant> {
if animating() {
LAST_TICK.with(Cell::get)
} else {
None
}
}
pub fn animating() -> bool {
RUNNING.with(|running| !running.borrow().is_empty())
}
pub fn advance_animations(now: Instant) -> bool {
LAST_TICK.with(|last| last.set(Some(now)));
let steps = RUNNING.with(|running| std::mem::take(&mut *running.borrow_mut()));
let mut kept = Vec::with_capacity(steps.len());
for (label, mut step) in steps {
match panic::catch_unwind(AssertUnwindSafe(|| step(now))) {
Ok(true) => kept.push((label, step)),
Ok(false) => drop_isolated(&label, step),
Err(payload) => {
tracing::error!(
"animation {label} panicked and stops: {}",
panic_message(payload.as_ref())
);
drop_isolated(&label, step);
}
}
}
RUNNING.with(|running| {
let mut running = running.borrow_mut();
kept.append(&mut running);
*running = kept;
if running.is_empty() {
LAST_TICK.with(|last| last.set(None));
}
!running.is_empty()
})
}
fn drop_isolated(label: &str, step: Step) {
if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(move || drop(step))) {
tracing::error!(
"dropping animation {label} panicked: {}",
panic_message(payload.as_ref())
);
}
}
impl Cx {
#[track_caller]
pub fn animate<T: Animate>(
&self,
mut target: impl FnMut() -> T + 'static,
transition: Transition,
) -> ReadSignal<T> {
let label: Rc<str> = format!("cx.animate at {}", std::panic::Location::caller()).into();
let first = untrack(&mut target);
let value = self.signal(first.clone());
let mut last = first;
let generation = Rc::new(Cell::new(0_u64));
self.effect(move || {
let to = target();
if to == last {
return;
}
last = to.clone();
let Some(from) = untrack(|| value.try_get()) else {
return;
};
let current = generation.get() + 1;
generation.set(current);
if transition.ms == 0 {
value.try_set(to);
return;
}
let generation = Rc::clone(&generation);
let mut started = start_time();
start(
Rc::clone(&label),
Box::new(move |now| {
if generation.get() != current {
return false;
}
let begun = *started.get_or_insert(now);
let (t, ended) = transition.progress(begun, now);
let next = if ended { to.clone() } else { from.lerp(&to, t) };
value.try_set(next) && !ended
}),
);
});
value.read_only()
}
}