1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12enum Phase {
13 Hidden,
14 Opening { start_tick: u64 },
15 Open,
16 Closing { start_tick: u64 },
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct TransitionOutput {
21 pub present: bool,
23 pub linear: f32,
25 pub progress: f32,
27 pub animating: bool,
29}
30
31#[derive(Debug, Clone, Copy)]
33pub struct TransitionTimeline {
34 open_ticks: u64,
35 close_ticks: u64,
36 phase: Phase,
37}
38
39impl Default for TransitionTimeline {
40 fn default() -> Self {
41 Self {
42 open_ticks: 4,
43 close_ticks: 4,
44 phase: Phase::Hidden,
45 }
46 }
47}
48
49impl TransitionTimeline {
50 pub fn open_ticks(&self) -> u64 {
51 self.open_ticks
52 }
53
54 pub fn close_ticks(&self) -> u64 {
55 self.close_ticks
56 }
57
58 pub fn set_open_ticks(&mut self, open_ticks: u64) {
59 self.open_ticks = open_ticks.max(1);
60 }
61
62 pub fn set_close_ticks(&mut self, close_ticks: u64) {
63 self.close_ticks = close_ticks.max(1);
64 }
65
66 pub fn set_durations(&mut self, open_ticks: u64, close_ticks: u64) {
67 self.open_ticks = open_ticks.max(1);
68 self.close_ticks = close_ticks.max(1);
69 }
70
71 pub fn update(&mut self, open: bool, tick: u64) -> TransitionOutput {
72 self.update_with_easing(open, tick, crate::easing::smoothstep)
73 }
74
75 pub fn update_with_easing(
76 &mut self,
77 open: bool,
78 tick: u64,
79 ease: fn(f32) -> f32,
80 ) -> TransitionOutput {
81 if open {
82 match self.phase {
83 Phase::Hidden | Phase::Closing { .. } => {
84 self.phase = Phase::Opening { start_tick: tick };
85 }
86 Phase::Opening { .. } | Phase::Open => {}
87 }
88 } else {
89 match self.phase {
90 Phase::Open | Phase::Opening { .. } => {
91 self.phase = Phase::Closing { start_tick: tick };
92 }
93 Phase::Closing { .. } | Phase::Hidden => {}
94 }
95 }
96
97 match self.phase {
98 Phase::Hidden => TransitionOutput {
99 present: false,
100 linear: 0.0,
101 progress: 0.0,
102 animating: false,
103 },
104 Phase::Open => TransitionOutput {
105 present: true,
106 linear: 1.0,
107 progress: 1.0,
108 animating: false,
109 },
110 Phase::Opening { start_tick } => {
111 let duration = self.open_ticks.max(1);
112 let elapsed = tick.saturating_sub(start_tick).saturating_add(1);
113 let t = (elapsed as f32 / duration as f32).clamp(0.0, 1.0);
114 let linear = t;
115 let progress = ease(linear).clamp(0.0, 1.0);
116 if t >= 1.0 {
117 self.phase = Phase::Open;
118 TransitionOutput {
119 present: true,
120 linear: 1.0,
121 progress: 1.0,
122 animating: false,
123 }
124 } else {
125 TransitionOutput {
126 present: true,
127 linear,
128 progress,
129 animating: true,
130 }
131 }
132 }
133 Phase::Closing { start_tick } => {
134 let duration = self.close_ticks.max(1);
135 let elapsed = tick.saturating_sub(start_tick).saturating_add(1);
136 let t = (elapsed as f32 / duration as f32).clamp(0.0, 1.0);
137 let linear = (1.0 - t).clamp(0.0, 1.0);
138 let progress = ease(linear).clamp(0.0, 1.0);
139 if t >= 1.0 {
140 self.phase = Phase::Hidden;
141 TransitionOutput {
142 present: false,
143 linear: 0.0,
144 progress: 0.0,
145 animating: false,
146 }
147 } else {
148 TransitionOutput {
149 present: true,
150 linear,
151 progress,
152 animating: true,
153 }
154 }
155 }
156 }
157 }
158
159 pub fn update_with_cubic_bezier(
160 &mut self,
161 open: bool,
162 tick: u64,
163 x1: f32,
164 y1: f32,
165 x2: f32,
166 y2: f32,
167 ) -> TransitionOutput {
168 if open {
169 match self.phase {
170 Phase::Hidden | Phase::Closing { .. } => {
171 self.phase = Phase::Opening { start_tick: tick };
172 }
173 Phase::Opening { .. } | Phase::Open => {}
174 }
175 } else {
176 match self.phase {
177 Phase::Open | Phase::Opening { .. } => {
178 self.phase = Phase::Closing { start_tick: tick };
179 }
180 Phase::Closing { .. } | Phase::Hidden => {}
181 }
182 }
183
184 match self.phase {
185 Phase::Hidden => TransitionOutput {
186 present: false,
187 linear: 0.0,
188 progress: 0.0,
189 animating: false,
190 },
191 Phase::Open => TransitionOutput {
192 present: true,
193 linear: 1.0,
194 progress: 1.0,
195 animating: false,
196 },
197 Phase::Opening { start_tick } => {
198 let duration = self.open_ticks.max(1);
199 let elapsed = tick.saturating_sub(start_tick).saturating_add(1);
200 let t = (elapsed as f32 / duration as f32).clamp(0.0, 1.0);
201 let linear = t;
202 let progress = cubic_bezier_ease(x1, y1, x2, y2, linear).clamp(0.0, 1.0);
203 if t >= 1.0 {
204 self.phase = Phase::Open;
205 TransitionOutput {
206 present: true,
207 linear: 1.0,
208 progress: 1.0,
209 animating: false,
210 }
211 } else {
212 TransitionOutput {
213 present: true,
214 linear,
215 progress,
216 animating: true,
217 }
218 }
219 }
220 Phase::Closing { start_tick } => {
221 let duration = self.close_ticks.max(1);
222 let elapsed = tick.saturating_sub(start_tick).saturating_add(1);
223 let t = (elapsed as f32 / duration as f32).clamp(0.0, 1.0);
224 let linear = (1.0 - t).clamp(0.0, 1.0);
225 let progress = cubic_bezier_ease(x1, y1, x2, y2, linear).clamp(0.0, 1.0);
226 if t >= 1.0 {
227 self.phase = Phase::Hidden;
228 TransitionOutput {
229 present: false,
230 linear: 0.0,
231 progress: 0.0,
232 animating: false,
233 }
234 } else {
235 TransitionOutput {
236 present: true,
237 linear,
238 progress,
239 animating: true,
240 }
241 }
242 }
243 }
244 }
245}
246
247fn cubic_bezier_ease(x1: f32, y1: f32, x2: f32, y2: f32, t: f32) -> f32 {
248 let t = t.clamp(0.0, 1.0);
249
250 if (x1, y1, x2, y2) == (0.0, 0.0, 1.0, 1.0) {
251 return t;
252 }
253
254 let mut u = t;
255 for _ in 0..8 {
256 let x = cubic_bezier_x(x1, x2, u);
257 let dx = cubic_bezier_x_derivative(x1, x2, u);
258 if dx.abs() < 1e-6 {
259 break;
260 }
261 u = (u - (x - t) / dx).clamp(0.0, 1.0);
262 }
263
264 let mut lo = 0.0;
265 let mut hi = 1.0;
266 for _ in 0..12 {
267 let x = cubic_bezier_x(x1, x2, u);
268 if (x - t).abs() < 1e-4 {
269 break;
270 }
271 if x < t {
272 lo = u;
273 } else {
274 hi = u;
275 }
276 u = (lo + hi) * 0.5;
277 }
278
279 cubic_bezier_y(y1, y2, u).clamp(0.0, 1.0)
280}
281
282fn cubic_bezier_x(p1: f32, p2: f32, u: f32) -> f32 {
283 cubic_bezier_component(u, p1, p2)
284}
285
286fn cubic_bezier_y(p1: f32, p2: f32, u: f32) -> f32 {
287 cubic_bezier_component(u, p1, p2)
288}
289
290fn cubic_bezier_x_derivative(p1: f32, p2: f32, u: f32) -> f32 {
291 cubic_bezier_component_derivative(u, p1, p2)
292}
293
294fn cubic_bezier_component(u: f32, p1: f32, p2: f32) -> f32 {
295 let inv = 1.0 - u;
296 3.0 * inv * inv * u * p1 + 3.0 * inv * u * u * p2 + u * u * u
297}
298
299fn cubic_bezier_component_derivative(u: f32, p1: f32, p2: f32) -> f32 {
300 let inv = 1.0 - u;
301 3.0 * inv * inv * p1 + 6.0 * inv * u * (p2 - p1) + 3.0 * u * u * (1.0 - p2)
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn opens_and_closes_with_present_window() {
310 let mut t = TransitionTimeline::default();
311 t.set_durations(3, 3);
312
313 let o0 = t.update(true, 0);
314 assert!(o0.present);
315 assert!(o0.animating);
316 assert!(o0.linear > 0.0 && o0.linear < 1.0);
317
318 let o2 = t.update(true, 2);
319 assert!(o2.present);
320
321 let o3 = t.update(true, 3);
322 assert!(o3.present);
323 assert!(!o3.animating);
324 assert_eq!(o3.linear, 1.0);
325
326 let c0 = t.update(false, 4);
327 assert!(c0.present);
328 assert!(c0.animating);
329 assert!(c0.linear < 1.0);
330
331 let c3 = t.update(false, 7);
332 assert!(!c3.present);
333 assert!(!c3.animating);
334 assert_eq!(c3.linear, 0.0);
335 }
336
337 #[test]
338 fn can_use_shadcn_cubic_bezier_easing() {
339 let mut t = TransitionTimeline::default();
340 t.set_durations(4, 4);
341 let out = t.update_with_easing(true, 0, |x| crate::easing::SHADCN_EASE.sample(x));
342 assert!(out.present);
343 assert!(out.animating);
344 assert!(out.progress >= 0.0 && out.progress <= 1.0);
345 }
346
347 #[test]
348 fn cubic_bezier_transition_matches_linear_for_linear_curve() {
349 let mut t = TransitionTimeline::default();
350 t.set_durations(4, 4);
351 let out = t.update_with_cubic_bezier(true, 0, 0.0, 0.0, 1.0, 1.0);
352 assert!(out.present);
353 assert!(out.animating);
354 assert!((out.progress - out.linear).abs() <= 1e-3);
355 }
356}