use std::cell::Cell;
use crate::geometry::{Color, Rect};
thread_local! {
static REQUEST: Cell<bool> = const { Cell::new(false) };
static CLOCK_MS: Cell<u64> = const { Cell::new(0) };
static ENABLED: Cell<bool> = const { Cell::new(true) };
static PAINT_RECT: Cell<Option<Rect>> = const { Cell::new(None) };
static DAMAGE: Cell<Option<Rect>> = const { Cell::new(None) };
static DAMAGE_FULL: Cell<bool> = const { Cell::new(false) };
}
pub(crate) enum Damage {
None,
Rect(Rect),
Full,
}
pub fn request_repaint() {
REQUEST.with(|c| c.set(true));
match PAINT_RECT.with(|c| c.get()) {
Some(r) => DAMAGE.with(|d| {
let merged = match d.get() {
Some(cur) => cur.union(&r),
None => r,
};
d.set(Some(merged));
}),
None => DAMAGE_FULL.with(|c| c.set(true)),
}
}
pub(crate) fn set_paint_rect(r: Option<Rect>) {
PAINT_RECT.with(|c| {
debug_assert!(
r.is_none() || c.get().is_none(),
"set_paint_rect 嵌套泄漏:上一节点未清除"
);
c.set(r);
});
}
pub(crate) fn take_damage() -> Damage {
if DAMAGE_FULL.with(|c| c.get()) {
return Damage::Full;
}
match DAMAGE.with(|c| c.get()) {
Some(r) => Damage::Rect(r),
None => Damage::None,
}
}
pub fn clock_ms() -> u64 {
CLOCK_MS.with(|c| c.get())
}
pub fn enabled() -> bool {
ENABLED.with(|c| c.get())
}
pub fn set_enabled(on: bool) {
ENABLED.with(|c| c.set(on));
}
pub(crate) fn reset_request() {
REQUEST.with(|c| c.set(false));
DAMAGE.with(|c| c.set(None));
DAMAGE_FULL.with(|c| c.set(false));
PAINT_RECT.with(|c| c.set(None));
}
pub(crate) fn animation_requested() -> bool {
REQUEST.with(|c| c.get())
}
pub(crate) fn set_clock_ms(v: u64) {
CLOCK_MS.with(|c| c.set(v));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Easing {
Linear,
EaseIn,
EaseOut,
#[default]
EaseInOut,
}
impl Easing {
pub fn apply(self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
let r = match self {
Easing::Linear => t,
Easing::EaseIn => t * t * t,
Easing::EaseOut => {
let u = 1.0 - t;
1.0 - u * u * u
}
Easing::EaseInOut => {
if t < 0.5 {
4.0 * t * t * t
} else {
let u = -2.0 * t + 2.0;
1.0 - u * u * u / 2.0
}
}
};
r.clamp(0.0, 1.0)
}
}
pub trait Lerp: Copy {
fn lerp(self, to: Self, t: f32) -> Self;
}
impl Lerp for f32 {
fn lerp(self, to: f32, t: f32) -> f32 {
self + (to - self) * t
}
}
impl Lerp for Color {
fn lerp(self, to: Color, t: f32) -> Color {
let mix = |a: u8, b: u8| (a as f32).lerp(b as f32, t).round().clamp(0.0, 255.0) as u8;
Color::rgba(
mix(self.r, to.r),
mix(self.g, to.g),
mix(self.b, to.b),
mix(self.a, to.a),
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Transition<T: Lerp> {
from: T,
to: T,
start_ms: u64,
duration_ms: u32,
easing: Easing,
}
impl<T: Lerp> Transition<T> {
pub fn new(value: T) -> Self {
Self {
from: value,
to: value,
start_ms: 0,
duration_ms: 0,
easing: Easing::default(),
}
}
pub fn target(&self) -> T {
self.to
}
pub fn retarget(&mut self, to: T, duration_ms: u32, easing: Easing) {
let now = self.value();
self.from = now;
self.to = to;
self.start_ms = clock_ms();
self.duration_ms = if enabled() { duration_ms } else { 0 };
self.easing = easing;
}
pub fn value(&self) -> T {
if !enabled() || self.duration_ms == 0 {
return self.to;
}
let elapsed = clock_ms().saturating_sub(self.start_ms);
if elapsed >= self.duration_ms as u64 {
return self.to;
}
let t = elapsed as f32 / self.duration_ms as f32;
self.from.lerp(self.to, self.easing.apply(t))
}
pub fn is_active(&self) -> bool {
if !enabled() || self.duration_ms == 0 {
return false;
}
clock_ms().saturating_sub(self.start_ms) < self.duration_ms as u64
}
pub fn animate(&self) -> T {
let v = self.value();
if self.is_active() {
request_repaint();
}
v
}
}
#[cfg(test)]
mod tests {
use super::*;
fn set_clock(ms: u64) {
set_clock_ms(ms);
}
#[test]
fn easing_endpoints_are_fixed() {
for e in [
Easing::Linear,
Easing::EaseIn,
Easing::EaseOut,
Easing::EaseInOut,
] {
assert!((e.apply(0.0) - 0.0).abs() < 1e-6, "{e:?} 在 0 应为 0");
assert!((e.apply(1.0) - 1.0).abs() < 1e-6, "{e:?} 在 1 应为 1");
assert_eq!(e.apply(-1.0), e.apply(0.0));
assert_eq!(e.apply(2.0), e.apply(1.0));
}
}
#[test]
fn easing_is_monotonic_increasing() {
for e in [
Easing::Linear,
Easing::EaseIn,
Easing::EaseOut,
Easing::EaseInOut,
] {
let mut prev = e.apply(0.0);
for i in 1..=20 {
let v = e.apply(i as f32 / 20.0);
assert!(v >= prev - 1e-6, "{e:?} 应单调非减");
prev = v;
}
}
}
#[test]
fn f32_and_color_lerp() {
assert_eq!(0.0f32.lerp(10.0, 0.5), 5.0);
let c = Color::rgb(0, 0, 0).lerp(Color::rgb(255, 100, 50), 0.5);
assert_eq!((c.r, c.g, c.b), (128, 50, 25));
}
#[test]
fn transition_interpolates_and_settles() {
set_enabled(true);
set_clock(1000);
let mut tr = Transition::new(0.0f32);
tr.retarget(100.0, 200, Easing::Linear);
set_clock(1000); assert_eq!(tr.value(), 0.0);
set_clock(1100); assert!(
(tr.value() - 50.0).abs() < 1e-3,
"半程应约 50,实得 {}",
tr.value()
);
assert!(tr.is_active());
set_clock(1200); assert_eq!(tr.value(), 100.0);
assert!(!tr.is_active(), "结束后不应活跃");
set_clock(5000); assert_eq!(tr.value(), 100.0);
}
#[test]
fn retarget_midflight_starts_from_current() {
set_enabled(true);
set_clock(0);
let mut tr = Transition::new(0.0f32);
tr.retarget(100.0, 200, Easing::Linear);
set_clock(100); assert!((tr.value() - 50.0).abs() < 1e-3);
tr.retarget(0.0, 200, Easing::Linear); assert!((tr.value() - 50.0).abs() < 1e-3, "改向瞬间应保持当前值 50");
set_clock(200); assert!(
(tr.value() - 25.0).abs() < 1e-3,
"改向半程应约 25,实得 {}",
tr.value()
);
}
#[test]
fn disabled_snaps_instantly() {
set_enabled(false);
set_clock(0);
let mut tr = Transition::new(0.0f32);
tr.retarget(100.0, 200, Easing::EaseInOut);
set_clock(0);
assert_eq!(tr.value(), 100.0, "全局关闭应瞬时到目标");
assert!(!tr.is_active());
set_enabled(true); }
}