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:
parent
5a2a7141c2
commit
6dff915e74
@ -5,6 +5,7 @@ use crate::{
|
||||
component::{Component, Event, EventCtx, Never},
|
||||
display::Color,
|
||||
geometry::{Offset, Rect},
|
||||
lerp::Lerp,
|
||||
shape::{self, Renderer},
|
||||
},
|
||||
};
|
||||
@ -17,8 +18,8 @@ use super::{
|
||||
/// 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.
|
||||
pub struct HoldToConfirmAnim {
|
||||
/// Duration of the animation
|
||||
duration: Duration,
|
||||
/// Intended total duration of Hold to Confirm animation
|
||||
total_duration: Duration,
|
||||
/// Screen border and header overlay color
|
||||
color: Color,
|
||||
/// Screen border shape
|
||||
@ -27,17 +28,47 @@ pub struct HoldToConfirmAnim {
|
||||
timer: Stopwatch,
|
||||
/// Header overlay text shown during the animation
|
||||
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 {
|
||||
/// 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 {
|
||||
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;
|
||||
Self {
|
||||
duration: theme::CONFIRM_HOLD_DURATION,
|
||||
total_duration: theme::CONFIRM_HOLD_DURATION,
|
||||
color: default_color,
|
||||
border: ScreenBorder::new(default_color),
|
||||
timer: Stopwatch::default(),
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@ -62,64 +99,19 @@ impl HoldToConfirmAnim {
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.rollback.timer = Stopwatch::new_started();
|
||||
self.rollback.duration = self.timer.elapsed();
|
||||
self.timer = Stopwatch::new_stopped();
|
||||
}
|
||||
|
||||
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>) {
|
||||
let ratio = self.timer.elapsed() / self.duration;
|
||||
|
||||
let bottom_width = self.border.bottom_width();
|
||||
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),
|
||||
)),
|
||||
)
|
||||
}
|
||||
fn is_rollback(&self) -> bool {
|
||||
self.rollback
|
||||
.timer
|
||||
.is_running_within(Self::ROLLBACK_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +124,7 @@ impl Component for HoldToConfirmAnim {
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
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_paint();
|
||||
}
|
||||
@ -141,37 +133,152 @@ impl Component for HoldToConfirmAnim {
|
||||
}
|
||||
|
||||
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() {
|
||||
// override header with custom text
|
||||
if let Some(text) = self.header_overlay {
|
||||
let font = fonts::FONT_SATOSHI_REGULAR_22;
|
||||
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);
|
||||
});
|
||||
// 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_opt) = self.get_clips();
|
||||
target.in_clip(in_clip, &|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)
|
||||
.with_bg(theme::BG)
|
||||
.with_fg(theme::BG)
|
||||
.render(target);
|
||||
}
|
||||
let (clip, top_gap) = self.get_clips(self.timer.elapsed());
|
||||
self.render_clipped_border(clip, top_gap, 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 {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ use super::{
|
||||
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);
|
||||
|
||||
// Color palette.
|
||||
|
Loading…
Reference in New Issue
Block a user