1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-24 13:22:05 +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 6dff915e74
2 changed files with 192 additions and 85 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,47 @@ 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 {
/// Growth duration of the bottom part of the border
const BOTTOM_DURATION: Duration = Duration::from_millis(200);
/// Growth duration of the side parts of the border
const SIDES_DURATION: Duration = Duration::from_millis(800);
/// Growth duration of the top part of the border
const TOP_DURATION: Duration = Duration::from_millis(600);
/// Duration of the rollback animation
const ROLLBACK_DURATION: Duration = Duration::from_millis(600);
pub fn new() -> Self { pub fn new() -> Self {
debug_assert!(
theme::CONFIRM_HOLD_DURATION.to_millis()
> (Self::BOTTOM_DURATION.to_millis()
+ Self::SIDES_DURATION.to_millis()
+ Self::TOP_DURATION.to_millis())
);
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 +79,13 @@ impl HoldToConfirmAnim {
} }
pub fn with_duration(mut self, duration: Duration) -> Self { pub fn with_duration(mut self, duration: Duration) -> Self {
self.duration = duration; debug_assert!(
duration.to_millis()
> (Self::BOTTOM_DURATION.to_millis()
+ Self::SIDES_DURATION.to_millis()
+ Self::TOP_DURATION.to_millis())
);
self.total_duration = duration;
self self
} }
@ -62,64 +99,19 @@ 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
let bottom_width = self.border.bottom_width(); .is_running_within(Self::ROLLBACK_DURATION)
let total_height = SCREEN.height();
let total_width = SCREEN.width();
let circumference = 2 * total_height + total_width + bottom_width;
let bottom_ratio = bottom_width as f32 / circumference as f32;
let vertical_ratio = (2 * total_height) as f32 / circumference as f32;
let upper_ratio = total_width as f32 / circumference as f32;
let vertical_cut = bottom_ratio + vertical_ratio;
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(
SCREEN
.bottom_center()
.ofs(Offset::y(-ScreenBorder::WIDTH / 2)),
Offset::new(clip_width, ScreenBorder::WIDTH),
),
None,
)
} else if ratio < vertical_cut {
// Animate the vertical border growing from the bottom up.
let progress = (ratio - bottom_ratio) / vertical_ratio;
let clip_height = (progress * total_height as f32) as i16;
let clip_height = clip_height.clamp(0, total_height - ScreenBorder::WIDTH);
(
Rect::from_bottom_left_and_size(
SCREEN.bottom_left(),
Offset::new(total_width, clip_height),
),
None,
)
} else {
// Animate the top border growing horizontally towards center.
let progress = (ratio - vertical_cut) / upper_ratio;
let clip_width = total_width - ((progress * total_width as f32) as i16);
(
SCREEN,
Some(Rect::from_center_and_size(
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
Offset::new(clip_width, ScreenBorder::WIDTH),
)),
)
}
} }
} }
@ -132,7 +124,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,37 +133,152 @@ 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 (clip, top_gap) = self.get_clips(rollback_duration_progressed);
let top_back_rollback = self.get_top_gap_rollback(rollback_elapsed);
let top_gap = top_gap.union(top_back_rollback);
self.render_clipped_border(clip, top_gap, alpha, target);
}
// Growing animation
if self.is_active() { if self.is_active() {
// override header with custom text // override header with custom text
if let Some(text) = self.header_overlay { // TODO: is the delay desirable?
let font = fonts::FONT_SATOSHI_REGULAR_22; if self.timer.elapsed() > Duration::from_millis(300) {
let header_pad = Rect::from_top_left_and_size( self.render_header_overlay(target);
SCREEN.top_left(),
Offset::new(SCREEN.width(), Header::HEADER_HEIGHT),
);
shape::Bar::new(header_pad)
.with_bg(theme::BG)
.render(target);
text.map(|text| {
let text_pos = header_pad.top_left()
+ Offset::new(24, font.vert_center(0, Header::HEADER_HEIGHT, text));
shape::Text::new(text_pos, text, font)
.with_fg(self.color)
.render(target);
});
} }
// growing border // growing border
let (in_clip, out_clip_opt) = self.get_clips(); let (clip, top_gap) = self.get_clips(self.timer.elapsed());
target.in_clip(in_clip, &|target| { self.render_clipped_border(clip, top_gap, u8::MAX, target);
self.border.render(target); }
}); }
// optional out clip for upper line rendering }
if let Some(out_clip) = out_clip_opt {
shape::Bar::new(out_clip) // Rendering helpers
.with_bg(theme::BG) impl HoldToConfirmAnim {
.with_fg(theme::BG) fn render_header_overlay<'s>(&'s self, target: &mut impl Renderer<'s>) {
.render(target); if let Some(text) = self.header_overlay {
} let font = theme::label_title_main().text_font;
let header_pad = Rect::from_top_left_and_size(
SCREEN.top_left(),
Offset::new(SCREEN.width(), Header::HEADER_HEIGHT),
);
shape::Bar::new(header_pad)
.with_bg(theme::BG)
.render(target);
text.map(|text| {
let text_pos = header_pad.top_left()
+ Offset::new(24, font.vert_center(0, Header::HEADER_HEIGHT, text));
shape::Text::new(text_pos, text, font)
.with_fg(self.color)
.render(target);
});
}
}
fn render_clipped_border<'s>(
&'s self,
clip: Rect,
top_gap: Rect,
alpha: u8,
target: &mut impl Renderer<'s>,
) {
target.in_clip(clip, &|target| {
self.border.render(alpha, target);
});
// optional out clip for upper line rendering
shape::Bar::new(top_gap)
.with_bg(theme::BG)
.with_fg(theme::BG)
.render(target);
}
fn get_rollback_alpha(&self, elapsed: Duration) -> u8 {
let progress = (elapsed / Self::ROLLBACK_DURATION).clamp(0.0, 1.0);
let shift = pareen::constant(0.0).seq_ease_out(
0.0,
easer::functions::Cubic,
1.0,
pareen::constant(1.0),
);
u8::lerp(u8::MAX, u8::MIN, shift.eval(progress))
}
fn get_top_gap_rollback(&self, elapsed: Duration) -> Rect {
let progress = (elapsed / Self::ROLLBACK_DURATION).clamp(0.0, 1.0);
let clip_width = (progress * SCREEN.width() as f32) as i16;
Rect::from_center_and_size(
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
Offset::new(clip_width, ScreenBorder::WIDTH),
)
}
fn get_clips(&self, elapsed: Duration) -> (Rect, Rect) {
// Define segment-specific timings
let bottom_dur_ratio = Self::BOTTOM_DURATION / self.total_duration;
let sides_dur_ratio = Self::SIDES_DURATION / self.total_duration;
let top_dur_ratio = Self::TOP_DURATION / self.total_duration;
let progress = (elapsed / self.total_duration).clamp(0.0, 1.0);
const TOP_GAP_ZERO: Rect = Rect::from_center_and_size(
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
Offset::zero(),
);
match progress {
// Bottom phase growing linearly
p if p < bottom_dur_ratio => {
let bottom_progress = (p / bottom_dur_ratio).clamp(0.0, 1.0);
let width = i16::lerp(0, SCREEN.width(), bottom_progress);
let clip = Rect::from_center_and_size(
SCREEN
.bottom_center()
.ofs(Offset::y(-ScreenBorder::WIDTH / 2)),
Offset::new(width, ScreenBorder::WIDTH),
);
(clip, TOP_GAP_ZERO)
}
// Sides phase growing up linearly
p if p < (bottom_dur_ratio + sides_dur_ratio) => {
let sides_progress = ((p - bottom_dur_ratio) / sides_dur_ratio).clamp(0.0, 1.0);
let height = i16::lerp(ScreenBorder::WIDTH, SCREEN.height(), sides_progress);
let clip = Rect::from_bottom_left_and_size(
SCREEN.bottom_left(),
Offset::new(SCREEN.width(), height),
);
(clip, TOP_GAP_ZERO)
}
// Top phase
p if p < 1.0 => {
let top_progress =
((p - bottom_dur_ratio - sides_dur_ratio) / top_dur_ratio).clamp(0.0, 1.0);
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);
let width = i16::lerp(SCREEN.width(), 0, eased_progress);
let top_gap = Rect::from_center_and_size(
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
Offset::new(width, ScreenBorder::WIDTH),
);
(SCREEN, top_gap)
}
// Animation complete
_ => (SCREEN, TOP_GAP_ZERO),
} }
} }
} }

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.