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:
parent
5a2a7141c2
commit
05f68e225c
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user