use std::time::Duration;
use crate::anim::Easing;
use crate::render::Pixmap;
pub const THEME_FADE: Duration = Duration::from_millis(200);
#[derive(Debug, Default)]
pub(crate) struct CrossFade {
from: Option<Pixmap>,
start: Duration,
length: Duration,
mix: f64,
shown: bool,
}
impl CrossFade {
pub(crate) fn start(&mut self, from: Pixmap, now: Option<Duration>, length: Duration) {
let Some(now) = now.filter(|_| !length.is_zero()) else {
self.from = None;
return;
};
let from = match self.from.take() {
Some(old) if old.size() == from.size() => blend(&old, &from, self.mix),
_ => from,
};
*self = CrossFade {
from: Some(from),
start: now,
length,
mix: 0.0,
shown: self.shown,
};
self.advance(Some(now));
}
pub(crate) fn advance(&mut self, now: Option<Duration>) {
if self.from.is_none() {
return;
}
let Some(now) = now else {
self.from = None;
return;
};
let t = now
.saturating_sub(self.start)
.saturating_add(crate::platform::frame::interval());
let p = t.as_secs_f64() / self.length.as_secs_f64();
if p >= 1.0 {
self.from = None;
self.mix = 1.0;
} else {
self.mix = Easing::EaseOut.apply(p);
}
}
pub(crate) fn active(&self) -> bool {
self.from.is_some()
}
pub(crate) fn needs_full(&self) -> bool {
self.from.is_some() || self.shown
}
pub(crate) fn compose(&mut self, frame: &Pixmap) -> Option<Pixmap> {
let blended = match &self.from {
Some(from) if from.size() == frame.size() => Some(blend(from, frame, self.mix)),
_ => None,
};
if blended.is_none() {
self.from = None;
}
self.shown = blended.is_some();
blended
}
}
pub(crate) fn blend(from: &Pixmap, to: &Pixmap, mix: f64) -> Pixmap {
let w = (mix.clamp(0.0, 1.0) * 256.0).round() as u32;
let mut out = to.clone();
for (o, &a) in out.as_bytes_mut().iter_mut().zip(from.as_bytes()) {
let b = u32::from(*o);
*o = ((u32::from(a) * (256 - w) + b * w + 128) >> 8) as u8;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::PhysicalSize;
const MS: Duration = Duration::from_millis(1);
fn solid(rgba: [u8; 4]) -> Pixmap {
let size = PhysicalSize::new(4, 2);
Pixmap::from_rgba8(size, rgba.repeat(8))
}
#[test]
fn mid_fade_frames_blend_the_old_and_new_pixels() {
let (white, black) = (solid([255, 255, 255, 255]), solid([0, 0, 0, 255]));
let mut fade = CrossFade::default();
let t0 = Duration::from_secs(2);
fade.start(white, Some(t0), THEME_FADE);
assert!(fade.active() && fade.needs_full());
let lead = crate::platform::frame::interval();
fade.advance(Some(t0 + THEME_FADE / 2 - lead));
let mid = fade.compose(&black).expect("fading");
let expected = 64; assert_eq!(mid.pixel(1, 1), Some([expected, expected, expected, 255]));
fade.advance(Some(t0 + 20 * MS));
let early = fade.compose(&black).unwrap().pixel(0, 0).unwrap()[0];
assert!(early > expected && early < 255, "{early}");
fade.advance(Some(t0 + THEME_FADE));
assert!(!fade.active());
assert!(fade.needs_full(), "a blend is still on screen");
assert_eq!(fade.compose(&black), None);
assert!(!fade.needs_full());
}
#[test]
fn no_length_or_no_clock_snaps() {
let mut fade = CrossFade::default();
fade.start(solid([255; 4]), Some(Duration::ZERO), Duration::ZERO);
assert!(!fade.active());
fade.start(solid([255; 4]), None, THEME_FADE);
assert!(!fade.active());
assert_eq!(fade.compose(&solid([0; 4])), None);
}
#[test]
fn a_resize_ends_the_fade() {
let mut fade = CrossFade::default();
fade.start(solid([255; 4]), Some(Duration::ZERO), THEME_FADE);
let bigger = Pixmap::new(PhysicalSize::new(8, 8));
assert_eq!(fade.compose(&bigger), None);
assert!(!fade.active());
}
#[test]
fn a_second_change_fades_on_from_the_blend_on_screen() {
let mut fade = CrossFade::default();
let t0 = Duration::from_secs(1);
fade.start(solid([255, 255, 255, 255]), Some(t0), THEME_FADE);
let lead = crate::platform::frame::interval();
fade.advance(Some(t0 + THEME_FADE / 2 - lead));
let shown = fade.compose(&solid([0, 0, 0, 255])).unwrap();
let t1 = t0 + THEME_FADE / 2 - lead;
fade.start(solid([0, 0, 0, 255]), Some(t1), THEME_FADE);
assert_eq!(fade.from.as_ref(), Some(&shown));
}
}