use std::cell::{Cell, RefCell};
use std::panic::{self, AssertUnwindSafe};
use std::rc::Rc;
use std::time::{Duration, Instant};
use crabshell_reactive::{ReadSignal, Signal, 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,
InSine,
OutSine,
InOutSine,
InQuart,
OutQuart,
InOutQuart,
InQuint,
OutQuint,
InOutQuint,
InExpo,
OutExpo,
InOutExpo,
InCirc,
OutCirc,
InOutCirc,
InBack,
OutBack,
InOutBack,
Bezier(f32, f32, f32, f32),
Spline(&'static [[f32; 6]]),
}
impl Easing {
pub fn value_at(self, t: f32) -> f32 {
use std::f32::consts::PI;
const C1: f32 = 1.70158;
const C2: f32 = C1 * 1.525;
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::InSine => 1.0 - (t * PI / 2.0).cos(),
Easing::OutSine => (t * PI / 2.0).sin(),
Easing::InOutSine => (1.0 - (PI * t).cos()) / 2.0,
Easing::InQuart => t.powi(4),
Easing::OutQuart => 1.0 - (1.0 - t).powi(4),
Easing::InOutQuart if t < 0.5 => 8.0 * t.powi(4),
Easing::InOutQuart => 1.0 - (-2.0 * t + 2.0).powi(4) / 2.0,
Easing::InQuint => t.powi(5),
Easing::OutQuint => 1.0 - (1.0 - t).powi(5),
Easing::InOutQuint if t < 0.5 => 16.0 * t.powi(5),
Easing::InOutQuint => 1.0 - (-2.0 * t + 2.0).powi(5) / 2.0,
Easing::InExpo | Easing::OutExpo | Easing::InOutExpo if t == 0.0 || t == 1.0 => t,
Easing::InExpo => 2_f32.powf(10.0 * t - 10.0),
Easing::OutExpo => 1.0 - 2_f32.powf(-10.0 * t),
Easing::InOutExpo if t < 0.5 => 2_f32.powf(20.0 * t - 10.0) / 2.0,
Easing::InOutExpo => (2.0 - 2_f32.powf(-20.0 * t + 10.0)) / 2.0,
Easing::InCirc => 1.0 - (1.0 - t * t).sqrt(),
Easing::OutCirc => (1.0 - (t - 1.0).powi(2)).sqrt(),
Easing::InOutCirc if t < 0.5 => (1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0,
Easing::InOutCirc => ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0,
Easing::InBack => (C1 + 1.0) * t.powi(3) - C1 * t * t,
Easing::OutBack => 1.0 + (C1 + 1.0) * (t - 1.0).powi(3) + C1 * (t - 1.0).powi(2),
Easing::InOutBack if t < 0.5 => (2.0 * t).powi(2) * ((C2 + 1.0) * 2.0 * t - C2) / 2.0,
Easing::InOutBack => {
((2.0 * t - 2.0).powi(2) * ((C2 + 1.0) * (2.0 * t - 2.0) + C2) + 2.0) / 2.0
}
Easing::Bezier(x1, y1, x2, y2) => bezier(t, (0.0, 0.0), [x1, y1, x2, y2, 1.0, 1.0]),
Easing::Spline(segments) => spline(t, segments),
}
}
}
fn spline(x: f32, segments: &[[f32; 6]]) -> f32 {
let mut end = 0.0;
let valid = segments.iter().all(|segment| {
let forward = segment[4] >= end;
end = segment[4];
forward && segment.iter().all(|n| n.is_finite())
}) && segments
.last()
.is_some_and(|last| last[4] == 1.0 && last[5] == 1.0);
if !valid {
return x;
}
let mut start = (0.0, 0.0);
for segment in segments {
if x <= segment[4] {
return bezier(x, start, *segment);
}
start = (segment[4], segment[5]);
}
1.0
}
fn bezier(x: f32, (x0, y0): (f32, f32), segment: [f32; 6]) -> f32 {
let [x1, y1, x2, y2, x3, y3] = segment;
let (x1, x2) = (x1.clamp(x0, x3), x2.clamp(x0, x3));
let at = |p0: f32, p1: f32, p2: f32, p3: f32, s: f32| {
let r = 1.0 - s;
r * r * r * p0 + 3.0 * r * r * s * p1 + 3.0 * r * s * s * p2 + s * s * s * p3
};
let (mut low, mut high) = (0.0_f32, 1.0_f32);
for _ in 0..32 {
let middle = (low + high) / 2.0;
if at(x0, x1, x2, x3, middle) < x {
low = middle;
} else {
high = middle;
}
}
at(y0, y1, y2, y3, (low + high) / 2.0)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transition {
pub ms: u32,
pub easing: Easing,
pub delay: u32,
pub alternate: bool,
}
impl Transition {
pub const fn new(ms: u32) -> Transition {
Transition {
ms,
easing: Easing::OutCubic,
delay: 0,
alternate: false,
}
}
pub const fn easing(self, easing: Easing) -> Transition {
Transition { easing, ..self }
}
pub const fn delay(self, ms: u32) -> Transition {
Transition { delay: ms, ..self }
}
pub const fn alternate(self) -> Transition {
Transition {
alternate: true,
..self
}
}
pub(crate) fn is_instant(self) -> bool {
self.ms == 0 && self.delay == 0
}
pub(crate) fn waiting(self, start: Instant, now: Instant) -> bool {
now.saturating_duration_since(start) < Duration::from_millis(self.delay.into())
}
pub(crate) fn progress(self, start: Instant, now: Instant) -> (f32, bool) {
let elapsed = now.saturating_duration_since(start).as_secs_f32() * 1000.0 - self.delay as f32;
if elapsed < 0.0 {
return (0.0, false);
}
if self.ms == 0 {
return (1.0, true);
}
let t = elapsed / self.ms as f32;
if t >= 1.0 {
(1.0, true)
} else {
(self.easing.value_at(t), false)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Spring {
pub stiffness: f32,
pub damping: f32,
}
impl Spring {
pub const fn new(stiffness: f32, damping: f32) -> Spring {
Spring { stiffness, damping }
}
fn is_valid(self) -> bool {
self.stiffness.is_finite()
&& self.stiffness > 0.0
&& self.damping.is_finite()
&& self.damping >= 0.0
}
fn advance(self, x: &mut f64, v: &mut f64, goal: f64, seconds: f64) {
let k = f64::from(self.stiffness);
let c = 2.0 * f64::from(self.damping) * k.sqrt();
let steps = (seconds * 1000.0).ceil().max(1.0);
let dt = seconds / steps;
for _ in 0..steps as u32 {
*v = (*v - dt * k * (*x - goal)) / (1.0 + dt * c + dt * dt * k);
*x += dt * *v;
}
}
}
impl Default for Spring {
fn default() -> Spring {
Spring::new(700.0, 0.9)
}
}
const SPRING_MAX_STEP: f64 = 0.25;
const SPRING_REST: (f64, f64) = (0.001, 0.01);
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())
);
}
}
fn guarded<R>(label: &str, f: impl FnOnce() -> R) -> Option<R> {
match panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(value) => Some(value),
Err(payload) => {
tracing::error!(
"animation {label} panicked and stops: {}",
panic_message(payload.as_ref())
);
None
}
}
}
fn set_changed<T: PartialEq + 'static>(signal: Signal<T>, value: T) {
if untrack(|| signal.try_with(|old| *old != value)) == Some(true) {
signal.try_set(value);
}
}
pub struct Animation<T: 'static> {
value: ReadSignal<T>,
running: ReadSignal<bool>,
}
impl<T> Clone for Animation<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Animation<T> {}
impl<T> Animation<T> {
pub fn value(&self) -> ReadSignal<T> {
self.value
}
pub fn running(&self) -> ReadSignal<bool> {
self.running
}
}
impl Cx {
#[track_caller]
pub fn animate<T: Animate>(
&self,
target: impl FnMut() -> T + 'static,
transition: Transition,
) -> ReadSignal<T> {
self.follow("cx.animate", target, transition).value
}
#[track_caller]
pub fn animation<T: Animate>(
&self,
target: impl FnMut() -> T + 'static,
transition: Transition,
) -> Animation<T> {
self.follow("cx.animation", target, transition)
}
#[track_caller]
fn follow<T: Animate>(
&self,
what: &str,
mut target: impl FnMut() -> T + 'static,
transition: Transition,
) -> Animation<T> {
let label: Rc<str> = format!("{what} at {}", std::panic::Location::caller()).into();
let first = untrack(&mut target);
let value = self.signal(first.clone());
let running = self.signal(false);
let mut last = first;
let generation = Rc::new(Cell::new(0_u64));
let mut broken = false;
self.effect(move || {
if broken {
return;
}
let Some(to) = guarded(&label, &mut target) else {
broken = true;
generation.set(generation.get() + 1);
set_changed(running, false);
return;
};
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.is_instant() {
value.try_set(to);
set_changed(running, false);
return;
}
set_changed(running, true);
let generation = Rc::clone(&generation);
let mut started = start_time();
let step_label = Rc::clone(&label);
start(
Rc::clone(&label),
Box::new(move |now| {
if generation.get() != current {
return false;
}
let begun = *started.get_or_insert(now);
if transition.waiting(begun, now) {
return true;
}
let (t, ended) = transition.progress(begun, now);
let Some(next) = guarded(&step_label, || {
if ended { to.clone() } else { from.lerp(&to, t) }
}) else {
running.try_set(false);
return false;
};
let kept = value.try_set(next);
if ended {
running.try_set(false);
}
kept && !ended
}),
);
});
Animation {
value: value.read_only(),
running: running.read_only(),
}
}
#[track_caller]
pub fn cycle(
&self,
mut running: impl FnMut() -> bool + 'static,
transition: Transition,
) -> ReadSignal<f32> {
let label: Rc<str> = format!("cx.cycle at {}", std::panic::Location::caller()).into();
let value = self.signal(0.0_f32);
let wanted = Rc::new(Cell::new(false));
let ticking = Rc::new(Cell::new(false));
let mut broken = false;
self.effect(move || {
let on = !broken
&& guarded(&label, &mut running).unwrap_or_else(|| {
broken = true;
false
});
wanted.set(on);
if !on || ticking.get() || transition.ms == 0 {
return;
}
ticking.set(true);
let (wanted, ticking) = (Rc::clone(&wanted), Rc::clone(&ticking));
let leg = f64::from(transition.ms);
let period = if transition.alternate { 2.0 * leg } else { leg };
let mut started = start_time();
let mut round = 0_u64;
start(
Rc::clone(&label),
Box::new(move |now| {
let begun = *started.get_or_insert(now);
let elapsed = now.saturating_duration_since(begun).as_secs_f64() * 1000.0
- f64::from(transition.delay);
let now_round = if elapsed < 0.0 {
0
} else {
(elapsed / period) as u64 + 1
};
let ended = !wanted.get() && (now_round == 0 || now_round > round);
round = now_round;
if ended {
ticking.set(false);
value.try_set(0.0);
return false;
}
let phase = elapsed.max(0.0) % period / leg;
let t = if phase <= 1.0 { phase } else { 2.0 - phase };
value.try_set(transition.easing.value_at(t as f32))
}),
);
});
value.read_only()
}
#[track_caller]
pub fn frames(&self, mut running: impl FnMut() -> bool + 'static) -> ReadSignal<Duration> {
let label: Rc<str> = format!("cx.frames at {}", std::panic::Location::caller()).into();
let value = self.signal(Duration::ZERO);
let wanted = Rc::new(Cell::new(false));
let ticking = Rc::new(Cell::new(false));
let mut broken = false;
self.effect(move || {
let on = !broken
&& guarded(&label, &mut running).unwrap_or_else(|| {
broken = true;
false
});
wanted.set(on);
if !on || ticking.get() {
return;
}
ticking.set(true);
let (wanted, ticking) = (Rc::clone(&wanted), Rc::clone(&ticking));
let mut started = start_time();
start(
Rc::clone(&label),
Box::new(move |now| {
if !wanted.get() {
ticking.set(false);
return false;
}
let begun = *started.get_or_insert(now);
value.try_set(now.saturating_duration_since(begun))
}),
);
});
value.read_only()
}
#[track_caller]
pub fn spring(
&self,
mut target: impl FnMut() -> f32 + 'static,
spring: Spring,
) -> ReadSignal<f32> {
let label: Rc<str> = format!("cx.spring at {}", std::panic::Location::caller()).into();
let first = untrack(&mut target);
let value = self.signal(first);
let mut last = first;
let goal = Rc::new(Cell::new(first));
let ticking = Rc::new(Cell::new(false));
let mut broken = false;
self.effect(move || {
if broken {
return;
}
let Some(to) = guarded(&label, &mut target) else {
broken = true;
goal.set(f32::NAN);
return;
};
if to == last {
return;
}
last = to;
goal.set(to);
if !to.is_finite() || !spring.is_valid() {
value.try_set(to);
return;
}
if ticking.get() {
return;
}
let Some(from) = untrack(|| value.try_get()) else {
return;
};
ticking.set(true);
let (goal, ticking) = (Rc::clone(&goal), Rc::clone(&ticking));
let (mut x, mut v) = (f64::from(from), 0.0);
let mut previous = start_time();
start(
Rc::clone(&label),
Box::new(move |now| {
let to = f64::from(goal.get());
if !to.is_finite() {
ticking.set(false);
return false;
}
let seconds = previous.map_or(0.0, |previous| {
now
.saturating_duration_since(previous)
.as_secs_f64()
.min(SPRING_MAX_STEP)
});
previous = Some(now);
spring.advance(&mut x, &mut v, to, seconds);
let (near, slow) = SPRING_REST;
let rest = !(x.is_finite() && v.is_finite()) || ((x - to).abs() < near && v.abs() < slow);
if rest {
x = to;
}
let kept = value.try_set(x as f32);
if rest || !kept {
ticking.set(false);
}
kept && !rest
}),
);
});
value.read_only()
}
}