From 17072faa6522af68d96b765191c3a13917e92b0d Mon Sep 17 00:00:00 2001 From: tychovrahe Date: Thu, 9 May 2024 10:47:14 +0200 Subject: [PATCH] feat(core): animate status screen in mercury ui [no changelog] --- .../model_mercury/component/status_screen.rs | 118 ++++++++++++++++-- 1 file changed, 109 insertions(+), 9 deletions(-) diff --git a/core/embed/rust/src/ui/model_mercury/component/status_screen.rs b/core/embed/rust/src/ui/model_mercury/component/status_screen.rs index dde3d7ac3a..0ee73362d8 100644 --- a/core/embed/rust/src/ui/model_mercury/component/status_screen.rs +++ b/core/embed/rust/src/ui/model_mercury/component/status_screen.rs @@ -1,13 +1,87 @@ -use crate::ui::{ - component::{Component, Event, EventCtx, Swipe, SwipeDirection, Timeout}, - display::{Color, Icon}, - geometry::{Alignment2D, Rect}, - shape, - shape::Renderer, +use crate::{ + time::{Duration, Stopwatch}, + ui::{ + component::{Component, Event, EventCtx, Swipe, SwipeDirection, Timeout}, + constant::screen, + display::{Color, Icon}, + geometry::{Alignment2D, Insets, Rect}, + lerp::Lerp, + shape, + shape::Renderer, + util::animation_disabled, + }, }; use super::theme; +const TIMEOUT_MS: u32 = 2000; + +#[derive(Default, Clone)] +struct StatusAnimation { + pub timer: Stopwatch, +} + +impl StatusAnimation { + pub fn is_active(&self) -> bool { + self.timer + .is_running_within(Duration::from_millis(TIMEOUT_MS)) + } + + pub fn eval(&self) -> f32 { + if animation_disabled() { + return 1.0; + } + self.timer.elapsed().to_millis() as f32 / 1000.0 + } + + pub fn get_instruction_opacity(&self, t: f32) -> u8 { + let instruction_opacity = pareen::constant(0.0).seq_ease_in_out( + 0.0, + easer::functions::Cubic, + 0.42, + pareen::constant(1.0), + ); + u8::lerp(0, 255, instruction_opacity.eval(t)) + } + + pub fn get_content_opacity(&self, t: f32) -> u8 { + let content_opacity = pareen::constant(0.0).seq_ease_in_out( + 0.18, + easer::functions::Cubic, + 0.2, + pareen::constant(1.0), + ); + + u8::lerp(0, 255, content_opacity.eval(t)) + } + + pub fn get_circle_radius(&self, t: f32) -> i16 { + let circle_scale = pareen::constant(0.0).seq_ease_out( + 0.2, + easer::functions::Cubic, + 0.4, + pareen::constant(1.0), + ); + + const CIRCLE_DIAMETER_MAX: i16 = 170; + const CIRCLE_DIAMETER_MIN: i16 = 80; + + i16::lerp( + CIRCLE_DIAMETER_MAX / 2, + CIRCLE_DIAMETER_MIN / 2, + circle_scale.eval(t), + ) + } + + pub fn start(&mut self) { + self.timer.start(); + } + + pub fn reset(&mut self) { + self.timer = Stopwatch::new_stopped(); + } +} + /// Component showing status of an operation. Most typically embedded as a /// content of a Frame and showing success (checkmark with a circle around). #[derive(Clone)] @@ -17,6 +91,7 @@ pub struct StatusScreen { icon_color: Color, circle_color: Color, dismiss_type: DismissType, + anim: StatusAnimation, } #[derive(Clone)] @@ -25,8 +100,6 @@ enum DismissType { Timeout(Timeout), } -const TIMEOUT_MS: u32 = 2000; - impl StatusScreen { fn new(icon: Icon, icon_color: Color, circle_color: Color, dismiss_style: DismissType) -> Self { Self { @@ -35,6 +108,7 @@ impl StatusScreen { icon_color, circle_color, dismiss_type: dismiss_style, + anim: StatusAnimation::default(), } } @@ -87,6 +161,16 @@ impl Component for StatusScreen { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Event::Attach = event { + self.anim.start(); + ctx.request_paint(); + ctx.request_anim_frame(); + } + if self.anim.is_active() { + ctx.request_anim_frame(); + ctx.request_paint(); + } + match self.dismiss_type { DismissType::SwipeUp(ref mut swipe) => { let swipe_dir = swipe.event(ctx, event); @@ -109,7 +193,9 @@ impl Component for StatusScreen { } fn render<'s>(&self, target: &mut impl Renderer<'s>) { - shape::Circle::new(self.area.center(), 40) + let t = self.anim.eval(); + + shape::Circle::new(self.area.center(), self.anim.get_circle_radius(t)) .with_fg(self.circle_color) .with_bg(theme::BLACK) .with_thickness(2) @@ -118,6 +204,20 @@ impl Component for StatusScreen { .with_align(Alignment2D::CENTER) .with_fg(self.icon_color) .render(target); + + //content + header cover + shape::Bar::new(self.area.outset(Insets::top(self.area.y0))) + .with_fg(theme::BLACK) + .with_bg(theme::BLACK) + .with_alpha(255 - self.anim.get_content_opacity(t)) + .render(target); + + //instruction cover + shape::Bar::new(screen().inset(Insets::top(self.area.y1))) + .with_fg(theme::BLACK) + .with_bg(theme::BLACK) + .with_alpha(255 - self.anim.get_instruction_opacity(t)) + .render(target); } }