Skip to main content

embedded_gui/
animation_timing.rs

1//! Animation timing helpers for embedded transitions.
2//!
3//! Provides interval remapping, table-based cubic easing samples, and the
4//! moook spatial interpolation curve used for stack push/pop motion.
5
6#[cfg(not(feature = "std"))]
7use crate::math::F32Ext as _;
8
9/// Normalized animation progress maximum (16-bit).
10pub const NORMALIZED_MAX: i32 = 65_535;
11
12/// Target frame interval at 30 Hz.
13pub const FRAME_INTERVAL_MS: u32 = 33;
14
15/// Default single-animation duration.
16pub const DEFAULT_DURATION_MS: u32 = 250;
17
18/// `PORT_HOLE_TRANSITION_DURATION_MS` / `ROUND_FLIP_ANIMATION_DURATION_MS`.
19pub const PORT_HOLE_DURATION_MS: u32 = 6 * FRAME_INTERVAL_MS;
20
21/// `SHUTTER_TRANSITION_DURATION_MS` (2 + 4 frames).
22pub const SHUTTER_DURATION_MS: u32 = 6 * FRAME_INTERVAL_MS;
23
24/// `interpolate_moook_duration()` (3 in + 4 out frames).
25pub const MOOOK_DURATION_MS: u32 =
26    (MOOOK_IN.len() as u32 + MOOOK_OUT.len() as u32) * FRAME_INTERVAL_MS;
27
28const MOOOK_IN: [i32; 3] = [0, 1, 20];
29const MOOOK_OUT: [i32; 4] = [4, 2, 1, 0];
30
31/// Remap normalized progress into `[interval_start, interval_end]`.
32#[inline]
33pub fn timing_scaled(time_normalized: i32, interval_start: i32, interval_end: i32) -> i32 {
34    if interval_end == interval_start {
35        return NORMALIZED_MAX;
36    }
37    let result = time_normalized - interval_start;
38    (result * NORMALIZED_MAX) / (interval_end - interval_start)
39}
40
41/// Clip normalized progress to `[0, NORMALIZED_MAX]`.
42#[inline]
43pub fn timing_clip(progress: i32) -> i32 {
44    progress.clamp(0, NORMALIZED_MAX)
45}
46
47/// Two-phase helper: first half / second half of a transition (port-hole, shutter, round window).
48#[inline]
49pub fn timing_half_phase(progress: f32) -> (f32, bool) {
50    if progress < 0.5 {
51        (progress * 2.0, true)
52    } else {
53        ((progress - 0.5) * 2.0, false)
54    }
55}
56
57/// Shutter timing: first 2/6 then 4/6 of total duration.
58#[inline]
59pub fn timing_shutter_phase(progress: f32) -> (f32, bool) {
60    const FIRST: f32 = 2.0 / 6.0;
61    if progress < FIRST {
62        (progress / FIRST, true)
63    } else {
64        ((progress - FIRST) / (1.0 - FIRST), false)
65    }
66}
67
68#[inline]
69pub fn moook_in_duration_ms() -> u32 {
70    MOOOK_IN.len() as u32 * FRAME_INTERVAL_MS
71}
72
73#[inline]
74pub fn moook_out_duration_ms() -> u32 {
75    MOOOK_OUT.len() as u32 * FRAME_INTERVAL_MS
76}
77
78#[inline]
79pub fn moook_duration_ms() -> u32 {
80    moook_in_duration_ms() + moook_out_duration_ms()
81}
82
83#[inline]
84pub fn moook_soft_duration_ms(mid_frames: i32) -> u32 {
85    moook_duration_ms() + mid_frames.max(0) as u32 * FRAME_INTERVAL_MS
86}
87
88fn interpolate_linear(normalized: i32, from: i64, to: i64) -> i64 {
89    from + (normalized as i64 * (to - from)) / NORMALIZED_MAX as i64
90}
91
92fn interpolate_moook_frames(
93    normalized: i32,
94    from: i64,
95    to: i64,
96    frames_in: &[i32],
97    frames_out: &[i32],
98    mid_frames: i32,
99    bounce_back: bool,
100) -> i64 {
101    let direction = if from == to {
102        0
103    } else if from < to {
104        1
105    } else {
106        -1
107    };
108    if direction == 0 {
109        return from;
110    }
111    let direction_out = if bounce_back { direction } else { -direction };
112    let num_in = frames_in.len() as i32;
113    let num_out = frames_out.len() as i32;
114    let num_total = num_in + mid_frames + num_out;
115    if num_total <= 0 {
116        return if normalized >= NORMALIZED_MAX {
117            to
118        } else {
119            from
120        };
121    }
122
123    let mut frame_idx = ((normalized as i64 * num_total as i64
124        + (NORMALIZED_MAX as i64 / (2 * num_total as i64)))
125        / NORMALIZED_MAX as i64) as i32;
126    frame_idx = frame_idx.clamp(0, num_total - 1);
127
128    if normalized >= NORMALIZED_MAX {
129        return to;
130    }
131    if frame_idx < 0 {
132        return from;
133    }
134    if frame_idx < num_in {
135        return from + direction as i64 * frames_in[frame_idx as usize] as i64;
136    }
137    if frame_idx < num_in + mid_frames && mid_frames > 0 {
138        let shifted =
139            normalized - ((num_in as i64 * NORMALIZED_MAX as i64) / num_total as i64) as i32;
140        let mid_normalized = ((num_total as i64 * shifted as i64) / mid_frames as i64) as i32;
141        let from_mid = from + direction as i64 * frames_in[(num_in - 1) as usize] as i64;
142        let to_mid = to + direction_out as i64 * frames_out[0] as i64;
143        return interpolate_linear(mid_normalized, from_mid, to_mid);
144    }
145    let out_idx = frame_idx - num_in - mid_frames;
146    to + direction_out as i64 * frames_out[out_idx as usize] as i64
147}
148
149/// Full moook spatial interpolation (`interpolate_moook`).
150pub fn interpolate_moook(normalized: i32, from: i64, to: i64) -> i64 {
151    interpolate_moook_frames(normalized, from, to, &MOOOK_IN, &MOOOK_OUT, 0, true)
152}
153
154/// Moook with linear middle segment (`interpolate_moook_soft`).
155pub fn interpolate_moook_soft(normalized: i32, from: i64, to: i64, mid_frames: i32) -> i64 {
156    interpolate_moook_frames(
157        normalized, from, to, &MOOOK_IN, &MOOOK_OUT, mid_frames, true,
158    )
159}
160
161/// Map linear progress `t` in `[0, 1]` through moook spatial easing to `[0, 1]` (may overshoot).
162pub fn moook_curve(t: f32) -> f32 {
163    let normalized = (t.clamp(0.0, 1.0) * NORMALIZED_MAX as f32).round() as i32;
164    let v = interpolate_moook(normalized, 0, NORMALIZED_MAX as i64);
165    v as f32 / NORMALIZED_MAX as f32
166}
167
168/// Table-based cubic ease-in sample (32-entry lookup).
169pub fn table_ease_in_sample(t: f32) -> f32 {
170    const TABLE: [u16; 33] = [
171        0, 64, 256, 576, 1024, 1600, 2304, 3136, 4096, 5184, 6400, 7744, 9216, 10816, 12544, 14400,
172        16384, 18496, 20736, 23104, 25600, 28224, 30976, 33856, 36864, 40000, 43264, 46656, 50176,
173        53824, 57600, 61504, 65535,
174    ];
175    ease_table_sample(t, &TABLE)
176}
177
178fn ease_table_sample(t: f32, table: &[u16]) -> f32 {
179    if table.is_empty() {
180        return t;
181    }
182    let progress = (t.clamp(0.0, 1.0) * NORMALIZED_MAX as f32).round() as i32;
183    if progress <= 0 {
184        return 0.0;
185    }
186    if progress >= NORMALIZED_MAX {
187        return 1.0;
188    }
189    let max_entry = table.len() - 1;
190    let stride = NORMALIZED_MAX / max_entry as i32;
191    let index = (progress * max_entry as i32) / NORMALIZED_MAX;
192    let from = table[index as usize] as i64;
193    let delta = table[(index + 1) as usize] as i64 - from;
194    let v = from + (delta * (progress - index * stride) as i64) / stride as i64;
195    v as f32 / NORMALIZED_MAX as f32
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn moook_reaches_endpoints() {
204        assert_eq!(interpolate_moook(0, 0, 100), 0);
205        assert_eq!(interpolate_moook(NORMALIZED_MAX, 0, 100), 100);
206    }
207
208    #[test]
209    fn timing_scaled_maps_interval() {
210        let mid = timing_scaled(NORMALIZED_MAX / 2, 0, NORMALIZED_MAX);
211        assert!((mid - NORMALIZED_MAX / 2).abs() <= 1);
212    }
213
214    #[test]
215    fn moook_curve_is_monotonic_overall() {
216        let a = moook_curve(0.0);
217        let b = moook_curve(1.0);
218        assert!((a - 0.0).abs() < 0.01);
219        assert!((b - 1.0).abs() < 0.01);
220    }
221}