use std::time::Duration;
use frust::Curve;
use frust::authoring::Size;
use frust::motion::patterns::{PatternLayer, TransitionPattern};
const GLYPH_SLIDE_DISTANCE_DP: f64 = 16.0;
pub const GLYPH_SLIDE_ENTER: Duration = Duration::from_millis(340);
pub const GLYPH_SLIDE_EXIT: Duration = Duration::from_millis(150);
const GLYPH_SLIDE_SPATIAL: Curve = Curve::Cubic(0.05, 0.7, 0.1, 1.0);
const GLYPH_SLIDE_EFFECTS: Curve = Curve::Cubic(0.2, 0.0, 0.0, 1.0);
const GLYPH_SLIDE_EXIT_CURVE: Curve = Curve::Cubic(0.3, 0.0, 1.0, 1.0);
const GLYPH_SLIDE_EXIT_FRACTION: f64 = 150.0 / 340.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SlideDirection {
Up,
Down,
Left,
Right,
}
impl SlideDirection {
fn unit(self) -> (f64, f64) {
match self {
SlideDirection::Up => (0.0, -1.0),
SlideDirection::Down => (0.0, 1.0),
SlideDirection::Left => (-1.0, 0.0),
SlideDirection::Right => (1.0, 0.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GlyphSlide {
pub direction: SlideDirection,
}
impl GlyphSlide {
pub const fn new(direction: SlideDirection) -> Self {
Self { direction }
}
}
impl TransitionPattern for GlyphSlide {
fn resolve(&self, p: f64, reverse: bool, _size: Size) -> (PatternLayer, PatternLayer) {
let pc = p.clamp(0.0, 1.0);
let (mut ux, mut uy) = self.direction.unit();
if reverse {
ux = -ux;
uy = -uy;
}
let enter = GLYPH_SLIDE_SPATIAL.transform(pc);
let in_off = (1.0 - enter) * GLYPH_SLIDE_DISTANCE_DP;
let incoming = PatternLayer {
dx: -ux * in_off,
dy: -uy * in_off,
alpha: GLYPH_SLIDE_EFFECTS.transform(pc) as f32,
scale: 1.0,
};
let exit_local = (pc / GLYPH_SLIDE_EXIT_FRACTION).min(1.0);
let exit = GLYPH_SLIDE_EXIT_CURVE.transform(exit_local);
let out_off = exit * GLYPH_SLIDE_DISTANCE_DP;
let exiting = PatternLayer {
dx: ux * out_off,
dy: uy * out_off,
alpha: (1.0 - exit) as f32,
scale: 1.0,
};
(incoming, exiting)
}
}
const GLYPH_STAGGER_ITEM_MS: f64 = 150.0;
const GLYPH_STAGGER_DELAY_MS: f64 = 90.0;
const GLYPH_STAGGER_CURVE: Curve = Curve::Cubic(0.2, 0.0, 0.0, 1.0);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlyphStagger {
pub per_item_delay: f64,
pub item_duration: f64,
pub item_curve: Curve,
}
impl GlyphStagger {
pub const fn glyph() -> Self {
Self {
per_item_delay: GLYPH_STAGGER_DELAY_MS,
item_duration: GLYPH_STAGGER_ITEM_MS,
item_curve: GLYPH_STAGGER_CURVE,
}
}
pub fn total_duration(&self, n: usize) -> f64 {
if n == 0 {
return 0.0;
}
(n as f64 - 1.0) * self.per_item_delay + self.item_duration
}
pub fn item_progress(&self, overall: f64, i: usize, n: usize, reduce_motion: bool) -> f64 {
if reduce_motion {
return overall.clamp(0.0, 1.0);
}
let total = self.total_duration(n);
if total <= 0.0 || i >= n {
return 1.0;
}
let overall = overall.clamp(0.0, 1.0);
let elapsed = overall * total;
let item_start = i as f64 * self.per_item_delay;
let item_end = item_start + self.item_duration;
if self.item_duration <= 0.0 {
return if elapsed < item_start { 0.0 } else { 1.0 };
}
if elapsed <= item_start {
0.0
} else if elapsed >= item_end {
1.0
} else {
let local = (elapsed - item_start) / self.item_duration;
self.item_curve.transform(local)
}
}
pub fn item_layer(
&self,
overall: f64,
i: usize,
n: usize,
reduce_motion: bool,
) -> PatternLayer {
PatternLayer {
alpha: self.item_progress(overall, i, n, reduce_motion) as f32,
..PatternLayer::IDENTITY
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const SIZE: Size = Size::new(400.0, 800.0);
#[test]
fn glyph_slide_up_offsets_and_alphas_forward_and_reverse() {
let gs = GlyphSlide::new(SlideDirection::Up);
let (inc, out) = gs.resolve(0.0, false, SIZE);
assert!(
(inc.dy - GLYPH_SLIDE_DISTANCE_DP).abs() < 1e-9,
"enters from below"
);
assert_eq!(inc.dx, 0.0);
assert_eq!(inc.alpha, 0.0);
assert_eq!(out.dy, 0.0);
assert_eq!(out.alpha, 1.0);
let (inc, out) = gs.resolve(1.0, false, SIZE);
assert!(inc.dy.abs() < 1e-9);
assert!((inc.alpha - 1.0).abs() < 1e-6);
assert!(
(out.dy + GLYPH_SLIDE_DISTANCE_DP).abs() < 1e-9,
"exits to the top"
);
assert_eq!(out.alpha, 0.0);
let (inc, out) = gs.resolve(0.5, false, SIZE);
let enter = GLYPH_SLIDE_SPATIAL.transform(0.5);
assert!((inc.dy - (1.0 - enter) * GLYPH_SLIDE_DISTANCE_DP).abs() < 1e-9);
assert!(inc.alpha > 0.0 && inc.alpha < 1.0);
assert!(out.alpha.abs() < 1e-6, "faster exit completes before t=0.5");
assert!((out.dy + GLYPH_SLIDE_DISTANCE_DP).abs() < 1e-9);
let (inc_r, _out_r) = gs.resolve(0.0, true, SIZE);
assert!(
(inc_r.dy + GLYPH_SLIDE_DISTANCE_DP).abs() < 1e-9,
"reverse enters from above"
);
}
#[test]
fn glyph_slide_horizontal_moves_on_x_only() {
let (inc, out) = GlyphSlide::new(SlideDirection::Left).resolve(0.0, false, SIZE);
assert_eq!(inc.dy, 0.0);
assert_eq!(out.dy, 0.0);
assert!((inc.dx - GLYPH_SLIDE_DISTANCE_DP).abs() < 1e-9);
}
#[test]
fn glyph_slide_exit_is_faster_than_enter() {
assert_eq!(GLYPH_SLIDE_ENTER, Duration::from_millis(340));
assert_eq!(GLYPH_SLIDE_EXIT, Duration::from_millis(150));
assert!(GLYPH_SLIDE_EXIT < GLYPH_SLIDE_ENTER);
assert!((GLYPH_SLIDE_EXIT_FRACTION - 150.0 / 340.0).abs() < 1e-12);
}
#[test]
fn glyph_stagger_item_progress_matches_the_stagger_math_for_five_items() {
let gs = GlyphStagger::glyph();
let n = 5;
let total = (n as f64 - 1.0) * GLYPH_STAGGER_DELAY_MS + GLYPH_STAGGER_ITEM_MS;
for &overall in &[0.0, 0.2, 0.5, 0.8, 1.0] {
for i in 0..n {
let elapsed = overall * total;
let start = i as f64 * GLYPH_STAGGER_DELAY_MS;
let expected = if elapsed <= start {
0.0
} else if elapsed >= start + GLYPH_STAGGER_ITEM_MS {
1.0
} else {
GLYPH_STAGGER_CURVE.transform((elapsed - start) / GLYPH_STAGGER_ITEM_MS)
};
assert!(
(gs.item_progress(overall, i, n, false) - expected).abs() < 1e-12,
"GlyphStagger reveal math at overall={overall}, i={i}"
);
}
}
assert!((gs.total_duration(5) - 510.0).abs() < 1e-9);
let eased = GLYPH_STAGGER_CURVE.transform(0.5);
assert!((gs.item_progress(0.5, 2, 5, false) - eased).abs() < 1e-9);
assert_eq!(gs.item_progress(0.5, 0, 5, false), 1.0);
assert_eq!(gs.item_progress(0.5, 4, 5, false), 0.0);
}
#[test]
fn glyph_stagger_reduce_motion_collapses_to_simultaneous_fade() {
let gs = GlyphStagger::glyph();
for i in 0..5 {
assert!((gs.item_progress(0.5, i, 5, true) - 0.5).abs() < 1e-12);
}
assert_eq!(gs.item_progress(0.5, 4, 5, false), 0.0);
assert!((gs.item_progress(0.5, 4, 5, true) - 0.5).abs() < 1e-12);
let layer = gs.item_layer(0.5, 4, 5, true);
assert!((layer.alpha - 0.5).abs() < 1e-6);
assert_eq!(layer.dx, 0.0);
assert_eq!(layer.scale, 1.0);
}
#[test]
fn glyph_stagger_degenerate_counts_have_nothing_left_to_reveal() {
let gs = GlyphStagger::glyph();
assert_eq!(gs.total_duration(0), 0.0);
assert_eq!(gs.item_progress(0.5, 0, 0, false), 1.0);
assert_eq!(gs.item_progress(0.5, 3, 3, false), 1.0);
}
}