1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-24 21:32:03 +00:00

feat(eckhart): add easing and rollback to HtC anim

This commit is contained in:
obrusvit 2025-02-23 17:19:20 +01:00
parent 5a2a7141c2
commit 05f68e225c
2 changed files with 184 additions and 78 deletions

View File

@ -5,6 +5,7 @@ use crate::{
component::{Component, Event, EventCtx, Never}, component::{Component, Event, EventCtx, Never},
display::Color, display::Color,
geometry::{Offset, Rect}, geometry::{Offset, Rect},
lerp::Lerp,
shape::{self, Renderer}, shape::{self, Renderer},
}, },
}; };
@ -17,8 +18,8 @@ use super::{
/// A component that displays a border that grows from the bottom of the screen /// A component that displays a border that grows from the bottom of the screen
/// to the top. The animation is parametrizable by color and duration. /// to the top. The animation is parametrizable by color and duration.
pub struct HoldToConfirmAnim { pub struct HoldToConfirmAnim {
/// Duration of the animation /// Intended total duration of Hold to Confirm animation
duration: Duration, total_duration: Duration,
/// Screen border and header overlay color /// Screen border and header overlay color
color: Color, color: Color,
/// Screen border shape /// Screen border shape
@ -27,17 +28,33 @@ pub struct HoldToConfirmAnim {
timer: Stopwatch, timer: Stopwatch,
/// Header overlay text shown during the animation /// Header overlay text shown during the animation
header_overlay: Option<TString<'static>>, header_overlay: Option<TString<'static>>,
/// Rollback animation state
rollback: RollbackState,
}
/// State of the rollback animation, when `stop` is called.
struct RollbackState {
/// Timer for the rollback animation
timer: Stopwatch,
/// Point in time of the growth animation when the rollback was initiated
duration: Duration,
} }
impl HoldToConfirmAnim { impl HoldToConfirmAnim {
const ROLLBACK_DURATION: Duration = Duration::from_millis(600);
pub fn new() -> Self { pub fn new() -> Self {
let default_color = theme::GREEN_LIME; let default_color = theme::GREEN_LIME;
Self { Self {
duration: theme::CONFIRM_HOLD_DURATION, total_duration: theme::CONFIRM_HOLD_DURATION,
color: default_color, color: default_color,
border: ScreenBorder::new(default_color), border: ScreenBorder::new(default_color),
timer: Stopwatch::default(), timer: Stopwatch::default(),
header_overlay: None, header_overlay: None,
rollback: RollbackState {
timer: Stopwatch::default(),
duration: Duration::default(),
},
} }
} }
@ -48,7 +65,7 @@ impl HoldToConfirmAnim {
} }
pub fn with_duration(mut self, duration: Duration) -> Self { pub fn with_duration(mut self, duration: Duration) -> Self {
self.duration = duration; self.total_duration = duration;
self self
} }
@ -62,64 +79,118 @@ impl HoldToConfirmAnim {
} }
pub fn stop(&mut self) { pub fn stop(&mut self) {
self.rollback.timer = Stopwatch::new_started();
self.rollback.duration = self.timer.elapsed();
self.timer = Stopwatch::new_stopped(); self.timer = Stopwatch::new_stopped();
} }
fn is_active(&self) -> bool { fn is_active(&self) -> bool {
self.timer.is_running_within(self.duration) self.timer.is_running_within(self.total_duration)
} }
fn get_clips(&self) -> (Rect, Option<Rect>) { fn is_rollback(&self) -> bool {
let ratio = self.timer.elapsed() / self.duration; self.rollback
.timer
.is_running_within(Self::ROLLBACK_DURATION)
}
let bottom_width = self.border.bottom_width(); fn get_rollback_alpha(&self, elapsed: Duration) -> u8 {
let total_height = SCREEN.height(); let progress = (elapsed / Self::ROLLBACK_DURATION).clamp(0.0, 1.0);
let total_width = SCREEN.width(); let shift = pareen::constant(0.0).seq_ease_out(
0.0,
easer::functions::Cubic,
1.0,
pareen::constant(1.0),
);
let eased_progress = shift.eval(progress);
u8::lerp(u8::MAX, u8::MIN, shift.eval(progress))
}
let circumference = 2 * total_height + total_width + bottom_width; fn get_clip_top_rollback(&self, elapsed: Duration) -> Rect {
let bottom_ratio = bottom_width as f32 / circumference as f32; let progress = (elapsed / Self::ROLLBACK_DURATION).clamp(0.0, 1.0);
let vertical_ratio = (2 * total_height) as f32 / circumference as f32; let clip_width = (progress * SCREEN.width() as f32) as i16;
let upper_ratio = total_width as f32 / circumference as f32; Rect::from_center_and_size(
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
Offset::new(clip_width, ScreenBorder::WIDTH),
)
}
let vertical_cut = bottom_ratio + vertical_ratio; fn get_clips(&self, elapsed: Duration) -> (Rect, Rect) {
// Define segment-specific timings
let bottom_duration = Duration::from_millis(200);
let sides_duration = Duration::from_millis(800);
let top_duration = Duration::from_millis(600);
let bottom_duration_ratio = bottom_duration / self.total_duration;
let sides_duration_ratio = sides_duration / self.total_duration;
let top_duration_ratio = top_duration / self.total_duration;
let progress = (elapsed / self.total_duration).clamp(0.0, 1.0);
match progress {
// Bottom phase growing linearly
p if p < bottom_duration_ratio => {
let bottom_progress = p / bottom_duration_ratio;
let width = i16::lerp(0, SCREEN.width(), bottom_progress);
if ratio < bottom_ratio {
// Animate the bottom border growing horizontally.
let clip_width = ((ratio / bottom_ratio) * bottom_width as f32) as i16;
let clip_width = clip_width.clamp(0, bottom_width);
( (
Rect::from_center_and_size( Rect::from_center_and_size(
SCREEN SCREEN
.bottom_center() .bottom_center()
.ofs(Offset::y(-ScreenBorder::WIDTH / 2)), .ofs(Offset::y(-ScreenBorder::WIDTH / 2)),
Offset::new(clip_width, ScreenBorder::WIDTH), Offset::new(width, ScreenBorder::WIDTH),
), ),
None, Rect::zero(),
) )
} else if ratio < vertical_cut { }
// Animate the vertical border growing from the bottom up.
let progress = (ratio - bottom_ratio) / vertical_ratio; // Sides phase growing up linearly
let clip_height = (progress * total_height as f32) as i16; p if p < (bottom_duration_ratio + sides_duration_ratio) => {
let clip_height = clip_height.clamp(0, total_height - ScreenBorder::WIDTH); let sides_progress = (p - bottom_duration_ratio) / sides_duration_ratio;
let height = i16::lerp(ScreenBorder::WIDTH, SCREEN.height(), sides_progress);
( (
Rect::from_bottom_left_and_size( Rect::from_bottom_left_and_size(
SCREEN.bottom_left(), SCREEN.bottom_left(),
Offset::new(total_width, clip_height), Offset::new(SCREEN.width(), height),
), ),
None, Rect::zero(),
) )
} else { }
// Animate the top border growing horizontally towards center.
let progress = (ratio - vertical_cut) / upper_ratio; // Top phase
let clip_width = total_width - ((progress * total_width as f32) as i16); p if p < 1.0 => {
let top_progress =
(p - bottom_duration_ratio - sides_duration_ratio) / top_duration_ratio;
let ease = pareen::constant(0.0).seq_ease_out(
0.0,
easer::functions::Cubic,
1.0,
pareen::constant(1.0),
);
let eased_progress = ease.eval(top_progress.clamp(0.0, 1.0));
let width = i16::lerp(SCREEN.width(), 0, eased_progress);
( (
SCREEN, SCREEN,
Some(Rect::from_center_and_size( Rect::from_center_and_size(
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)), SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
Offset::new(clip_width, ScreenBorder::WIDTH), Offset::new(width, ScreenBorder::WIDTH),
)), ),
) )
} }
// Animation complete
_ => (
SCREEN,
Rect::from_center_and_size(
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
Offset::zero(),
),
),
}
} }
} }
@ -132,7 +203,7 @@ impl Component for HoldToConfirmAnim {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event { if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if self.is_active() { if self.is_active() || self.is_rollback() {
ctx.request_anim_frame(); ctx.request_anim_frame();
ctx.request_paint(); ctx.request_paint();
} }
@ -141,10 +212,41 @@ impl Component for HoldToConfirmAnim {
} }
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// Rollback & Fading out animation
if self.is_rollback() {
let rollback_elapsed = self.rollback.timer.elapsed();
let alpha = self.get_rollback_alpha(rollback_elapsed);
let rollback_duration_progressed = self
.rollback
.duration
.checked_add(rollback_elapsed)
.unwrap_or(Duration::default());
let (in_clip, out_clip) = self.get_clips(rollback_duration_progressed);
let out_clip_push_back = self.get_clip_top_rollback(rollback_elapsed);
let out_clip = out_clip.union(out_clip_push_back);
self.render_clipped_border(in_clip, out_clip, alpha, target);
}
// Growing animation
if self.is_active() { if self.is_active() {
// override header with custom text // override header with custom text
// TODO: is the delay desirable?
if self.timer.elapsed() > Duration::from_millis(300) {
self.render_header_overlay(target);
}
// growing border
let (in_clip, out_clip) = self.get_clips(self.timer.elapsed());
self.render_clipped_border(in_clip, out_clip, u8::MAX, target);
}
}
}
// Rendering helpers
impl HoldToConfirmAnim {
fn render_header_overlay<'s>(&'s self, target: &mut impl Renderer<'s>) {
if let Some(text) = self.header_overlay { if let Some(text) = self.header_overlay {
let font = fonts::FONT_SATOSHI_REGULAR_22; let font = theme::label_title_main().text_font;
let header_pad = Rect::from_top_left_and_size( let header_pad = Rect::from_top_left_and_size(
SCREEN.top_left(), SCREEN.top_left(),
Offset::new(SCREEN.width(), Header::HEADER_HEIGHT), Offset::new(SCREEN.width(), Header::HEADER_HEIGHT),
@ -160,18 +262,22 @@ impl Component for HoldToConfirmAnim {
.render(target); .render(target);
}); });
} }
// growing border }
let (in_clip, out_clip_opt) = self.get_clips();
fn render_clipped_border<'s>(
&'s self,
in_clip: Rect,
out_clip: Rect,
alpha: u8,
target: &mut impl Renderer<'s>,
) {
target.in_clip(in_clip, &|target| { target.in_clip(in_clip, &|target| {
self.border.render(target); self.border.render(alpha, target);
}); });
// optional out clip for upper line rendering // optional out clip for upper line rendering
if let Some(out_clip) = out_clip_opt {
shape::Bar::new(out_clip) shape::Bar::new(out_clip)
.with_bg(theme::BG) .with_bg(theme::BG)
.with_fg(theme::BG) .with_fg(theme::BG)
.render(target); .render(target);
} }
} }
}
}

View File

@ -12,7 +12,7 @@ use super::{
fonts, fonts,
}; };
pub const CONFIRM_HOLD_DURATION: Duration = Duration::from_millis(1500); pub const CONFIRM_HOLD_DURATION: Duration = Duration::from_millis(2500);
pub const ERASE_HOLD_DURATION: Duration = Duration::from_millis(1500); pub const ERASE_HOLD_DURATION: Duration = Duration::from_millis(1500);
// Color palette. // Color palette.