1use heapless::Vec;
2
3#[cfg(not(feature = "std"))]
4use crate::math::F32Ext as _;
5use crate::{
6 Blend, BlendMode, Compositor, PixelRead,
7 animation::{Animation, Easing, apply_easing},
8 animation_timing::{self, timing_half_phase, timing_shutter_phase},
9 context::GuiContext,
10 geometry::Rect,
11 screen::{ScreenCommand, ScreenId, ScreenLifecycleEvent, ScreenStack, ScreenStackError},
12};
13use embedded_graphics_core::{
14 draw_target::DrawTarget,
15 pixelcolor::{Rgb565, RgbColor},
16};
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum ScreenTransitionEffect {
21 #[default]
22 None,
23 Fade,
24 SlideLeft,
25 SlideRight,
26 SlideUp,
27 SlideDown,
28 PushMoook,
30 PopMoook,
32 Zoom,
33 CircularReveal,
34 WipeLeft,
35 WipeRight,
36 WipeUp,
37 WipeDown,
38 ShutterLeft,
40 ShutterRight,
41 ShutterUp,
42 ShutterDown,
43 RoundFlipLeft,
45 RoundFlipRight,
46 PortHoleLeft,
48 PortHoleRight,
49 PortHoleUp,
50 PortHoleDown,
51 ModalSlideUp,
53 ModalSlideDown,
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum ScreenTransitionOrigin {
58 #[default]
59 Center,
60 TopLeft,
61 Top,
62 TopRight,
63 Left,
64 Right,
65 BottomLeft,
66 Bottom,
67 BottomRight,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct ScreenTransitionSpec {
72 pub effect: ScreenTransitionEffect,
73 pub duration_ms: u32,
74 pub origin: ScreenTransitionOrigin,
75 pub easing: Easing,
76}
77
78impl ScreenTransitionSpec {
79 pub const fn none() -> Self {
80 Self {
81 effect: ScreenTransitionEffect::None,
82 duration_ms: 0,
83 origin: ScreenTransitionOrigin::Center,
84 easing: Easing::InOutSine,
85 }
86 }
87
88 pub const fn fade(duration_ms: u32) -> Self {
89 Self {
90 effect: ScreenTransitionEffect::Fade,
91 duration_ms,
92 origin: ScreenTransitionOrigin::Center,
93 easing: Easing::InOutSine,
94 }
95 }
96
97 pub const fn slide_left(duration_ms: u32) -> Self {
98 Self {
99 effect: ScreenTransitionEffect::SlideLeft,
100 duration_ms,
101 origin: ScreenTransitionOrigin::Center,
102 easing: Easing::InOutSine,
103 }
104 }
105
106 pub const fn slide_right(duration_ms: u32) -> Self {
107 Self {
108 effect: ScreenTransitionEffect::SlideRight,
109 duration_ms,
110 origin: ScreenTransitionOrigin::Center,
111 easing: Easing::InOutSine,
112 }
113 }
114
115 pub const fn slide_up(duration_ms: u32) -> Self {
116 Self {
117 effect: ScreenTransitionEffect::SlideUp,
118 duration_ms,
119 origin: ScreenTransitionOrigin::Center,
120 easing: Easing::InOutSine,
121 }
122 }
123
124 pub const fn slide_down(duration_ms: u32) -> Self {
125 Self {
126 effect: ScreenTransitionEffect::SlideDown,
127 duration_ms,
128 origin: ScreenTransitionOrigin::Center,
129 easing: Easing::InOutSine,
130 }
131 }
132
133 pub const fn push_moook(duration_ms: u32) -> Self {
134 Self {
135 effect: ScreenTransitionEffect::PushMoook,
136 duration_ms,
137 origin: ScreenTransitionOrigin::Center,
138 easing: Easing::Moook,
139 }
140 }
141
142 pub const fn pop_moook(duration_ms: u32) -> Self {
143 Self {
144 effect: ScreenTransitionEffect::PopMoook,
145 duration_ms,
146 origin: ScreenTransitionOrigin::Center,
147 easing: Easing::Moook,
148 }
149 }
150
151 pub const fn shutter_left(duration_ms: u32) -> Self {
152 Self {
153 effect: ScreenTransitionEffect::ShutterLeft,
154 duration_ms,
155 origin: ScreenTransitionOrigin::Center,
156 easing: Easing::EaseInOut,
157 }
158 }
159
160 pub const fn shutter_right(duration_ms: u32) -> Self {
161 Self {
162 effect: ScreenTransitionEffect::ShutterRight,
163 duration_ms,
164 origin: ScreenTransitionOrigin::Center,
165 easing: Easing::EaseInOut,
166 }
167 }
168
169 pub const fn shutter_up(duration_ms: u32) -> Self {
170 Self {
171 effect: ScreenTransitionEffect::ShutterUp,
172 duration_ms,
173 origin: ScreenTransitionOrigin::Center,
174 easing: Easing::EaseInOut,
175 }
176 }
177
178 pub const fn shutter_down(duration_ms: u32) -> Self {
179 Self {
180 effect: ScreenTransitionEffect::ShutterDown,
181 duration_ms,
182 origin: ScreenTransitionOrigin::Center,
183 easing: Easing::EaseInOut,
184 }
185 }
186
187 pub const fn round_flip_left(duration_ms: u32) -> Self {
188 Self {
189 effect: ScreenTransitionEffect::RoundFlipLeft,
190 duration_ms,
191 origin: ScreenTransitionOrigin::Center,
192 easing: Easing::Linear,
193 }
194 }
195
196 pub const fn round_flip_right(duration_ms: u32) -> Self {
197 Self {
198 effect: ScreenTransitionEffect::RoundFlipRight,
199 duration_ms,
200 origin: ScreenTransitionOrigin::Center,
201 easing: Easing::Linear,
202 }
203 }
204
205 pub const fn port_hole_left(duration_ms: u32) -> Self {
206 Self {
207 effect: ScreenTransitionEffect::PortHoleLeft,
208 duration_ms,
209 origin: ScreenTransitionOrigin::Center,
210 easing: Easing::EaseInOut,
211 }
212 }
213
214 pub const fn port_hole_right(duration_ms: u32) -> Self {
215 Self {
216 effect: ScreenTransitionEffect::PortHoleRight,
217 duration_ms,
218 origin: ScreenTransitionOrigin::Center,
219 easing: Easing::EaseInOut,
220 }
221 }
222
223 pub const fn port_hole_up(duration_ms: u32) -> Self {
224 Self {
225 effect: ScreenTransitionEffect::PortHoleUp,
226 duration_ms,
227 origin: ScreenTransitionOrigin::Center,
228 easing: Easing::EaseInOut,
229 }
230 }
231
232 pub const fn port_hole_down(duration_ms: u32) -> Self {
233 Self {
234 effect: ScreenTransitionEffect::PortHoleDown,
235 duration_ms,
236 origin: ScreenTransitionOrigin::Center,
237 easing: Easing::EaseInOut,
238 }
239 }
240
241 pub const fn modal_slide_up(duration_ms: u32) -> Self {
242 Self {
243 effect: ScreenTransitionEffect::ModalSlideUp,
244 duration_ms,
245 origin: ScreenTransitionOrigin::Bottom,
246 easing: Easing::EaseOut,
247 }
248 }
249
250 pub const fn modal_slide_down(duration_ms: u32) -> Self {
251 Self {
252 effect: ScreenTransitionEffect::ModalSlideDown,
253 duration_ms,
254 origin: ScreenTransitionOrigin::Top,
255 easing: Easing::EaseOut,
256 }
257 }
258
259 pub const fn zoom(duration_ms: u32) -> Self {
260 Self {
261 effect: ScreenTransitionEffect::Zoom,
262 duration_ms,
263 origin: ScreenTransitionOrigin::Center,
264 easing: Easing::InOutSine,
265 }
266 }
267
268 pub const fn circular_reveal(duration_ms: u32) -> Self {
269 Self {
270 effect: ScreenTransitionEffect::CircularReveal,
271 duration_ms,
272 origin: ScreenTransitionOrigin::Center,
273 easing: Easing::InOutSine,
274 }
275 }
276
277 pub const fn wipe_left(duration_ms: u32) -> Self {
278 Self {
279 effect: ScreenTransitionEffect::WipeLeft,
280 duration_ms,
281 origin: ScreenTransitionOrigin::Center,
282 easing: Easing::InOutSine,
283 }
284 }
285
286 pub const fn wipe_right(duration_ms: u32) -> Self {
287 Self {
288 effect: ScreenTransitionEffect::WipeRight,
289 duration_ms,
290 origin: ScreenTransitionOrigin::Center,
291 easing: Easing::InOutSine,
292 }
293 }
294
295 pub const fn wipe_up(duration_ms: u32) -> Self {
296 Self {
297 effect: ScreenTransitionEffect::WipeUp,
298 duration_ms,
299 origin: ScreenTransitionOrigin::Center,
300 easing: Easing::InOutSine,
301 }
302 }
303
304 pub const fn wipe_down(duration_ms: u32) -> Self {
305 Self {
306 effect: ScreenTransitionEffect::WipeDown,
307 duration_ms,
308 origin: ScreenTransitionOrigin::Center,
309 easing: Easing::InOutSine,
310 }
311 }
312
313 pub const fn with_origin(mut self, origin: ScreenTransitionOrigin) -> Self {
314 self.origin = origin;
315 self
316 }
317
318 pub const fn with_easing(mut self, easing: Easing) -> Self {
319 self.easing = easing;
320 self
321 }
322}
323
324#[derive(Clone, Copy, Debug, PartialEq)]
325pub struct ActiveScreenTransition {
326 pub from: Option<ScreenId>,
327 pub to: Option<ScreenId>,
328 pub effect: ScreenTransitionEffect,
329 pub origin: ScreenTransitionOrigin,
330 pub progress: f32,
331}
332
333impl ActiveScreenTransition {
334 pub fn opacity_u8(&self) -> u8 {
335 (self.progress.clamp(0.0, 1.0) * 255.0) as u8
336 }
337
338 pub fn slide_offset_x(&self, width: u32) -> i32 {
339 let t = eased_progress(self.progress, self.effect);
340 let px = (width as f32 * t).round() as i32;
341 match self.effect {
342 ScreenTransitionEffect::SlideLeft
343 | ScreenTransitionEffect::ShutterLeft
344 | ScreenTransitionEffect::PortHoleLeft => -px,
345 ScreenTransitionEffect::SlideRight
346 | ScreenTransitionEffect::PushMoook
347 | ScreenTransitionEffect::ShutterRight
348 | ScreenTransitionEffect::PortHoleRight
349 | ScreenTransitionEffect::RoundFlipRight => px,
350 ScreenTransitionEffect::PopMoook => px,
351 _ => 0,
352 }
353 }
354
355 pub fn slide_offset_y(&self, height: u32) -> i32 {
356 let t = eased_progress(self.progress, self.effect);
357 let px = (height as f32 * t).round() as i32;
358 match self.effect {
359 ScreenTransitionEffect::SlideUp
360 | ScreenTransitionEffect::ShutterUp
361 | ScreenTransitionEffect::PortHoleUp
362 | ScreenTransitionEffect::ModalSlideUp => -px,
363 ScreenTransitionEffect::SlideDown
364 | ScreenTransitionEffect::ShutterDown
365 | ScreenTransitionEffect::PortHoleDown
366 | ScreenTransitionEffect::ModalSlideDown => px,
367 _ => 0,
368 }
369 }
370}
371
372#[derive(Clone, Copy, Debug, PartialEq, Eq)]
373pub struct ScreenTransitionSample {
374 pub outgoing_offset_x: i32,
375 pub outgoing_offset_y: i32,
376 pub incoming_offset_x: i32,
377 pub incoming_offset_y: i32,
378 pub outgoing_opacity: u8,
379 pub incoming_opacity: u8,
380 pub outgoing_clip: Option<Rect>,
381 pub incoming_clip: Option<Rect>,
382}
383
384fn eased_progress(progress: f32, effect: ScreenTransitionEffect) -> f32 {
385 match effect {
386 ScreenTransitionEffect::PushMoook | ScreenTransitionEffect::PopMoook => {
387 animation_timing::moook_curve(progress)
388 }
389 _ => progress.clamp(0.0, 1.0),
390 }
391}
392
393fn shutter_offset(progress: f32, viewport: u32, horizontal: bool, negative: bool) -> (i32, i32) {
394 let (phase_t, first_half) = timing_shutter_phase(progress);
395 let span = viewport as i32;
396 let sign = if negative { -1 } else { 1 };
397 if horizontal {
398 if first_half {
399 (sign * -((span as f32 * phase_t).round() as i32), 0)
400 } else {
401 (
402 sign * span,
403 sign * (span - (span as f32 * phase_t).round() as i32),
404 )
405 }
406 } else if first_half {
407 (0, sign * -((span as f32 * phase_t).round() as i32))
408 } else {
409 (0, sign * (span - (span as f32 * phase_t).round() as i32))
410 }
411}
412
413fn port_hole_offsets(
414 progress: f32,
415 viewport_w: u32,
416 viewport_h: u32,
417 horizontal: bool,
418 negative: bool,
419) -> (i32, i32, i32, i32) {
420 let viewport = if horizontal { viewport_w } else { viewport_h };
421 let (out, inc) = {
422 let (phase_t, first_half) = timing_half_phase(progress);
423 let gap = (viewport as f32 * 80.0 / 180.0).round() as i32;
424 let full = viewport as i32;
425 let sign = if negative { -1 } else { 1 };
426 if first_half {
427 (sign * (full - (gap as f32 * phase_t) as i32), sign * full)
428 } else {
429 (sign * gap, sign * ((gap as f32 * (1.0 - phase_t)) as i32))
430 }
431 };
432 if horizontal {
433 (out, 0, inc, 0)
434 } else {
435 (0, out, 0, inc)
436 }
437}
438
439impl ActiveScreenTransition {
440 pub fn sample(&self, viewport_w: u32, viewport_h: u32) -> ScreenTransitionSample {
441 match self.effect {
442 ScreenTransitionEffect::Fade => {
443 let incoming = self.opacity_u8();
444 ScreenTransitionSample {
445 outgoing_offset_x: 0,
446 outgoing_offset_y: 0,
447 incoming_offset_x: 0,
448 incoming_offset_y: 0,
449 outgoing_opacity: 255u8.saturating_sub(incoming),
450 incoming_opacity: incoming,
451 outgoing_clip: None,
452 incoming_clip: None,
453 }
454 }
455 ScreenTransitionEffect::SlideLeft | ScreenTransitionEffect::PushMoook => {
456 let out = self.slide_offset_x(viewport_w);
457 ScreenTransitionSample {
458 outgoing_offset_x: out,
459 outgoing_offset_y: 0,
460 incoming_offset_x: out + viewport_w as i32,
461 incoming_offset_y: 0,
462 outgoing_opacity: 255,
463 incoming_opacity: 255,
464 outgoing_clip: None,
465 incoming_clip: None,
466 }
467 }
468 ScreenTransitionEffect::SlideRight | ScreenTransitionEffect::PopMoook => {
469 let out = self.slide_offset_x(viewport_w);
470 ScreenTransitionSample {
471 outgoing_offset_x: out,
472 outgoing_offset_y: 0,
473 incoming_offset_x: out - viewport_w as i32,
474 incoming_offset_y: 0,
475 outgoing_opacity: 255,
476 incoming_opacity: 255,
477 outgoing_clip: None,
478 incoming_clip: None,
479 }
480 }
481 ScreenTransitionEffect::SlideUp | ScreenTransitionEffect::ModalSlideUp => {
482 let out = self.slide_offset_y(viewport_h);
483 ScreenTransitionSample {
484 outgoing_offset_x: 0,
485 outgoing_offset_y: out,
486 incoming_offset_x: 0,
487 incoming_offset_y: out + viewport_h as i32,
488 outgoing_opacity: 255,
489 incoming_opacity: 255,
490 outgoing_clip: None,
491 incoming_clip: None,
492 }
493 }
494 ScreenTransitionEffect::SlideDown | ScreenTransitionEffect::ModalSlideDown => {
495 let out = self.slide_offset_y(viewport_h);
496 ScreenTransitionSample {
497 outgoing_offset_x: 0,
498 outgoing_offset_y: out,
499 incoming_offset_x: 0,
500 incoming_offset_y: out - viewport_h as i32,
501 outgoing_opacity: 255,
502 incoming_opacity: 255,
503 outgoing_clip: None,
504 incoming_clip: None,
505 }
506 }
507 ScreenTransitionEffect::ShutterLeft => {
508 let (ox, ix) = shutter_offset(self.progress, viewport_w, true, true);
509 ScreenTransitionSample {
510 outgoing_offset_x: ox,
511 outgoing_offset_y: 0,
512 incoming_offset_x: ix,
513 incoming_offset_y: 0,
514 outgoing_opacity: 255,
515 incoming_opacity: 255,
516 outgoing_clip: None,
517 incoming_clip: None,
518 }
519 }
520 ScreenTransitionEffect::ShutterRight => {
521 let (ox, ix) = shutter_offset(self.progress, viewport_w, true, false);
522 ScreenTransitionSample {
523 outgoing_offset_x: ox,
524 outgoing_offset_y: 0,
525 incoming_offset_x: ix,
526 incoming_offset_y: 0,
527 outgoing_opacity: 255,
528 incoming_opacity: 255,
529 outgoing_clip: None,
530 incoming_clip: None,
531 }
532 }
533 ScreenTransitionEffect::ShutterUp => {
534 let (oy, iy) = shutter_offset(self.progress, viewport_h, false, true);
535 ScreenTransitionSample {
536 outgoing_offset_x: 0,
537 outgoing_offset_y: oy,
538 incoming_offset_x: 0,
539 incoming_offset_y: iy,
540 outgoing_opacity: 255,
541 incoming_opacity: 255,
542 outgoing_clip: None,
543 incoming_clip: None,
544 }
545 }
546 ScreenTransitionEffect::ShutterDown => {
547 let (oy, iy) = shutter_offset(self.progress, viewport_h, false, false);
548 ScreenTransitionSample {
549 outgoing_offset_x: 0,
550 outgoing_offset_y: oy,
551 incoming_offset_x: 0,
552 incoming_offset_y: iy,
553 outgoing_opacity: 255,
554 incoming_opacity: 255,
555 outgoing_clip: None,
556 incoming_clip: None,
557 }
558 }
559 ScreenTransitionEffect::PortHoleLeft => {
560 let (ox, oy, ix, iy) =
561 port_hole_offsets(self.progress, viewport_w, viewport_h, true, true);
562 ScreenTransitionSample {
563 outgoing_offset_x: ox,
564 outgoing_offset_y: oy,
565 incoming_offset_x: ix,
566 incoming_offset_y: iy,
567 outgoing_opacity: 255,
568 incoming_opacity: 255,
569 outgoing_clip: None,
570 incoming_clip: None,
571 }
572 }
573 ScreenTransitionEffect::PortHoleRight => {
574 let (ox, oy, ix, iy) =
575 port_hole_offsets(self.progress, viewport_w, viewport_h, true, false);
576 ScreenTransitionSample {
577 outgoing_offset_x: ox,
578 outgoing_offset_y: oy,
579 incoming_offset_x: ix,
580 incoming_offset_y: iy,
581 outgoing_opacity: 255,
582 incoming_opacity: 255,
583 outgoing_clip: None,
584 incoming_clip: None,
585 }
586 }
587 ScreenTransitionEffect::PortHoleUp => {
588 let (ox, oy, ix, iy) =
589 port_hole_offsets(self.progress, viewport_w, viewport_h, false, true);
590 ScreenTransitionSample {
591 outgoing_offset_x: ox,
592 outgoing_offset_y: oy,
593 incoming_offset_x: ix,
594 incoming_offset_y: iy,
595 outgoing_opacity: 255,
596 incoming_opacity: 255,
597 outgoing_clip: None,
598 incoming_clip: None,
599 }
600 }
601 ScreenTransitionEffect::PortHoleDown => {
602 let (ox, oy, ix, iy) =
603 port_hole_offsets(self.progress, viewport_w, viewport_h, false, false);
604 ScreenTransitionSample {
605 outgoing_offset_x: ox,
606 outgoing_offset_y: oy,
607 incoming_offset_x: ix,
608 incoming_offset_y: iy,
609 outgoing_opacity: 255,
610 incoming_opacity: 255,
611 outgoing_clip: None,
612 incoming_clip: None,
613 }
614 }
615 ScreenTransitionEffect::RoundFlipLeft | ScreenTransitionEffect::RoundFlipRight => {
616 let (out_clip, in_clip) = round_flip_clip(viewport_w, viewport_h, self.progress);
617 ScreenTransitionSample {
618 outgoing_offset_x: 0,
619 outgoing_offset_y: 0,
620 incoming_offset_x: 0,
621 incoming_offset_y: 0,
622 outgoing_opacity: if self.progress < 0.5 { 255 } else { 128 },
623 incoming_opacity: if self.progress < 0.5 { 128 } else { 255 },
624 outgoing_clip: out_clip,
625 incoming_clip: in_clip,
626 }
627 }
628 ScreenTransitionEffect::None => ScreenTransitionSample {
629 outgoing_offset_x: 0,
630 outgoing_offset_y: 0,
631 incoming_offset_x: 0,
632 incoming_offset_y: 0,
633 outgoing_opacity: 255,
634 incoming_opacity: 255,
635 outgoing_clip: None,
636 incoming_clip: None,
637 },
638 ScreenTransitionEffect::Zoom => {
639 let incoming = self.opacity_u8();
640 ScreenTransitionSample {
641 outgoing_offset_x: 0,
642 outgoing_offset_y: 0,
643 incoming_offset_x: 0,
644 incoming_offset_y: 0,
645 outgoing_opacity: 255u8.saturating_sub(incoming / 2),
646 incoming_opacity: incoming,
647 outgoing_clip: None,
648 incoming_clip: None,
649 }
650 }
651 ScreenTransitionEffect::CircularReveal => {
652 let incoming = self.opacity_u8();
653 let clip = reveal_clip(viewport_w, viewport_h, self.progress, self.origin);
654 ScreenTransitionSample {
655 outgoing_offset_x: 0,
656 outgoing_offset_y: 0,
657 incoming_offset_x: 0,
658 incoming_offset_y: 0,
659 outgoing_opacity: 255u8.saturating_sub(incoming / 4),
660 incoming_opacity: incoming,
661 outgoing_clip: None,
662 incoming_clip: Some(clip),
663 }
664 }
665 ScreenTransitionEffect::WipeLeft
666 | ScreenTransitionEffect::WipeRight
667 | ScreenTransitionEffect::WipeUp
668 | ScreenTransitionEffect::WipeDown => {
669 let clip = wipe_clip(viewport_w, viewport_h, self.progress, self.effect);
670 ScreenTransitionSample {
671 outgoing_offset_x: 0,
672 outgoing_offset_y: 0,
673 incoming_offset_x: 0,
674 incoming_offset_y: 0,
675 outgoing_opacity: 255,
676 incoming_opacity: 255,
677 outgoing_clip: None,
678 incoming_clip: Some(clip),
679 }
680 }
681 }
682 }
683}
684
685fn round_flip_clip(
686 viewport_w: u32,
687 viewport_h: u32,
688 progress: f32,
689) -> (Option<Rect>, Option<Rect>) {
690 let h = viewport_h as i32;
691 let mid = h / 2;
692 let scale = if progress < 0.5 {
693 1.0 - progress * 2.0
694 } else {
695 (progress - 0.5) * 2.0
696 };
697 let visible = ((h as f32 * scale).round() as i32).max(1);
698 let top = mid - visible / 2;
699 let clip = Rect::new(0, top.max(0), viewport_w, visible.max(1) as u32);
700 if progress < 0.5 {
701 (None, Some(clip))
702 } else {
703 (Some(clip), Some(clip))
704 }
705}
706
707fn reveal_clip(
708 viewport_w: u32,
709 viewport_h: u32,
710 progress: f32,
711 origin: ScreenTransitionOrigin,
712) -> Rect {
713 let (cx, cy) = origin_point(viewport_w, viewport_h, origin);
714 let max_radius = (((viewport_w as f32).hypot(viewport_h as f32)) * 0.5).ceil() as i32;
715 let radius = ((max_radius as f32) * progress.clamp(0.0, 1.0)).ceil() as i32;
716 let left = (cx - radius).clamp(0, viewport_w as i32);
717 let top = (cy - radius).clamp(0, viewport_h as i32);
718 let right = (cx + radius).clamp(0, viewport_w as i32);
719 let bottom = (cy + radius).clamp(0, viewport_h as i32);
720 Rect::new(
721 left,
722 top,
723 (right - left).max(0) as u32,
724 (bottom - top).max(0) as u32,
725 )
726}
727
728fn origin_point(viewport_w: u32, viewport_h: u32, origin: ScreenTransitionOrigin) -> (i32, i32) {
729 let mid_x = viewport_w as i32 / 2;
730 let mid_y = viewport_h as i32 / 2;
731 let max_x = viewport_w as i32;
732 let max_y = viewport_h as i32;
733 match origin {
734 ScreenTransitionOrigin::Center => (mid_x, mid_y),
735 ScreenTransitionOrigin::TopLeft => (0, 0),
736 ScreenTransitionOrigin::Top => (mid_x, 0),
737 ScreenTransitionOrigin::TopRight => (max_x, 0),
738 ScreenTransitionOrigin::Left => (0, mid_y),
739 ScreenTransitionOrigin::Right => (max_x, mid_y),
740 ScreenTransitionOrigin::BottomLeft => (0, max_y),
741 ScreenTransitionOrigin::Bottom => (mid_x, max_y),
742 ScreenTransitionOrigin::BottomRight => (max_x, max_y),
743 }
744}
745
746fn wipe_clip(
747 viewport_w: u32,
748 viewport_h: u32,
749 progress: f32,
750 effect: ScreenTransitionEffect,
751) -> Rect {
752 let w = viewport_w as i32;
753 let h = viewport_h as i32;
754 let p = progress.clamp(0.0, 1.0);
755 match effect {
756 ScreenTransitionEffect::WipeLeft => {
757 let visible = (w as f32 * p).round() as i32;
758 Rect::new(0, 0, visible.max(0) as u32, viewport_h)
759 }
760 ScreenTransitionEffect::WipeRight => {
761 let visible = (w as f32 * p).round() as i32;
762 Rect::new((w - visible).max(0), 0, visible.max(0) as u32, viewport_h)
763 }
764 ScreenTransitionEffect::WipeUp => {
765 let visible = (h as f32 * p).round() as i32;
766 Rect::new(0, 0, viewport_w, visible.max(0) as u32)
767 }
768 ScreenTransitionEffect::WipeDown => {
769 let visible = (h as f32 * p).round() as i32;
770 Rect::new(0, (h - visible).max(0), viewport_w, visible.max(0) as u32)
771 }
772 _ => Rect::new(0, 0, viewport_w, viewport_h),
773 }
774}
775
776#[derive(Clone, Copy, Debug, PartialEq)]
777pub struct ScreenTransitionRunner {
778 animation: Option<Animation>,
779 active: Option<ActiveScreenTransition>,
780}
781
782impl ScreenTransitionRunner {
783 pub const fn new() -> Self {
784 Self {
785 animation: None,
786 active: None,
787 }
788 }
789
790 pub fn apply<const N: usize, const M: usize>(
791 &mut self,
792 stack: &mut ScreenStack<N>,
793 command: ScreenCommand,
794 spec: ScreenTransitionSpec,
795 lifecycle_events: &mut Vec<ScreenLifecycleEvent, M>,
796 ) -> Result<(), ScreenStackError> {
797 let transition = stack.apply_lifecycle(command, lifecycle_events)?;
798 if spec.effect == ScreenTransitionEffect::None || spec.duration_ms == 0 {
799 self.animation = None;
800 self.active = None;
801 return Ok(());
802 }
803 let anim = Animation::new(0.0, 1.0, spec.duration_ms, spec.easing);
804 self.animation = Some(anim);
805 self.active = Some(ActiveScreenTransition {
806 from: transition.from,
807 to: transition.to,
808 effect: spec.effect,
809 origin: spec.origin,
810 progress: 0.0,
811 });
812 Ok(())
813 }
814
815 pub fn tick(&mut self, dt_ms: u32) {
816 let Some(animation) = self.animation.as_mut() else {
817 return;
818 };
819 animation.tick(dt_ms);
820 if let Some(active) = self.active.as_mut() {
821 active.progress = animation.value();
822 }
823 if animation.is_done() {
824 self.animation = None;
825 self.active = None;
826 }
827 }
828
829 pub fn active(&self) -> Option<ActiveScreenTransition> {
830 self.active
831 }
832}
833
834impl Default for ScreenTransitionRunner {
835 fn default() -> Self {
836 Self::new()
837 }
838}
839
840pub fn render_transition_pair<'a, D, const NODES: usize, const EVENTS: usize, const DIRTY: usize>(
841 target: &mut D,
842 outgoing: &GuiContext<'a, NODES, EVENTS, DIRTY>,
843 incoming: &GuiContext<'a, NODES, EVENTS, DIRTY>,
844 active: ActiveScreenTransition,
845 viewport_w: u32,
846 viewport_h: u32,
847) -> Result<(), D::Error>
848where
849 D: embedded_graphics_core::draw_target::DrawTarget<
850 Color = embedded_graphics_core::pixelcolor::Rgb565,
851 >,
852{
853 let sample = active.sample(viewport_w, viewport_h);
854 if let Some(clip) = sample.outgoing_clip {
855 outgoing.render_with_offset_opacity_and_clip(
856 target,
857 sample.outgoing_offset_x,
858 sample.outgoing_offset_y,
859 sample.outgoing_opacity,
860 clip,
861 )?;
862 } else {
863 outgoing.render_with_offset_and_opacity(
864 target,
865 sample.outgoing_offset_x,
866 sample.outgoing_offset_y,
867 sample.outgoing_opacity,
868 )?;
869 }
870 if let Some(clip) = sample.incoming_clip {
871 incoming.render_with_offset_opacity_and_clip(
872 target,
873 sample.incoming_offset_x,
874 sample.incoming_offset_y,
875 sample.incoming_opacity,
876 clip,
877 )?;
878 } else {
879 incoming.render_with_offset_and_opacity(
880 target,
881 sample.incoming_offset_x,
882 sample.incoming_offset_y,
883 sample.incoming_opacity,
884 )?;
885 }
886 Ok(())
887}
888
889pub fn fade_outgoing_opacity(progress: f32, easing: Easing) -> u8 {
891 let t = apply_easing(progress.clamp(0.0, 1.0), easing);
892 ((1.0 - t) * 255.0) as u8
893}
894
895pub fn composite_framebuffer_fade<D>(
898 target: &mut D,
899 outgoing: &[Rgb565],
900 width: usize,
901 height: usize,
902 outgoing_opacity: u8,
903) -> Result<(), D::Error>
904where
905 D: DrawTarget<Color = Rgb565> + PixelRead,
906{
907 if outgoing_opacity == 0 || outgoing.len() < width * height {
908 return Ok(());
909 }
910 for y in 0..height {
911 for x in 0..width {
912 let outgoing_px = outgoing[y * width + x];
913 let _ = Blend::plot(
914 target,
915 x as i32,
916 y as i32,
917 outgoing_px,
918 outgoing_opacity,
919 BlendMode::Normal,
920 Rgb565::BLACK,
921 );
922 }
923 }
924 Ok(())
925}