diff --git a/orb-ui/cone/examples/cone-simulation.rs b/orb-ui/cone/examples/cone-simulation.rs index 99e04628..fcb845d1 100644 --- a/orb-ui/cone/examples/cone-simulation.rs +++ b/orb-ui/cone/examples/cone-simulation.rs @@ -54,11 +54,11 @@ async fn simulation_task(cone: &mut Cone) -> eyre::Result<()> { let state_res = match counter { SimulationState::Idle => { for pixel in pixels.iter_mut() { - *pixel = Argb::DIAMOND_USER_IDLE; + *pixel = Argb::DIAMOND_CONE_AMBER; } cone.lcd .tx() - .try_send(LcdCommand::try_from(Argb::DIAMOND_USER_IDLE)?) + .try_send(LcdCommand::try_from(Argb::DIAMOND_CONE_AMBER)?) .wrap_err("unable to send DIAMOND_USER_IDLE to lcd") } SimulationState::Red => { @@ -134,7 +134,7 @@ async fn simulation_task(cone: &mut Cone) -> eyre::Result<()> { } SimulationState::QrCode => { for pixel in pixels.iter_mut() { - *pixel = Argb::DIAMOND_USER_AMBER; + *pixel = Argb::DIAMOND_SHROUD_SUMMON_USER_AMBER; } let cmd = diff --git a/orb-ui/rgb/src/lib.rs b/orb-ui/rgb/src/lib.rs index b76fb60e..b0c1974a 100644 --- a/orb-ui/rgb/src/lib.rs +++ b/orb-ui/rgb/src/lib.rs @@ -79,16 +79,21 @@ impl Argb { Argb(Some(Self::DIMMING_MAX_VALUE), 128, 128, 0); pub const DIAMOND_OPERATOR_VERSIONS_OUTDATED: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 255, 0, 0); - pub const DIAMOND_USER_AMBER: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 20, 16, 1); - #[allow(dead_code)] - pub const DIAMOND_USER_IDLE: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 18, 23, 18); - pub const DIAMOND_USER_QR_SCAN: Argb = - Argb(Some(Self::DIMMING_MAX_VALUE), 24, 29, 24); - pub const DIAMOND_USER_SIGNUP: Argb = - Argb(Some(Self::DIMMING_MAX_VALUE), 32, 26, 1); - pub const DIAMOND_USER_FLASH: Argb = - Argb(Some(Self::DIMMING_MAX_VALUE), 255, 255, 255); + /// Outer-ring color during operator QR scans + pub const DIAMOND_RING_OPERATOR_QR_SCAN: Argb = Argb(Some(4), 55, 10, 0); + pub const DIAMOND_RING_OPERATOR_QR_SCAN_SPINNER: Argb = Argb(Some(7), 80, 50, 30); + /// Outer-ring color during user QR scans + pub const DIAMOND_RING_USER_QR_SCAN: Argb = Argb(Some(4), 50, 40, 3); + pub const DIAMOND_RING_USER_QR_SCAN_SPINNER: Argb = Argb(Some(7), 80, 60, 40); + /// Shroud color to invite user to scan / reposition in front of the orb + pub const DIAMOND_SHROUD_SUMMON_USER_AMBER: Argb = Argb(Some(3), 95, 40, 3); + /// Shroud color during user scan/capture (in progress) + pub const DIAMOND_SHROUD_USER_CAPTURE: Argb = Argb(Some(3), 118, 51, 3); + /// Outer-ring color during user scan/capture (in progress) + pub const DIAMOND_RING_USER_CAPTURE: Argb = Argb(Some(10), 100, 80, 3); pub const DIAMOND_CONE_AMBER: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 25, 18, 1); + /// Error color for outer ring + pub const DIAMOND_RING_ERROR_SALMON: Argb = Argb(Some(3), 127, 20, 0); pub const FULL_RED: Argb = Argb(None, 255, 0, 0); pub const FULL_GREEN: Argb = Argb(None, 0, 255, 0); diff --git a/orb-ui/src/engine/animations/arc_dash.rs b/orb-ui/src/engine/animations/arc_dash.rs index 3b57bc9d..b848f87a 100644 --- a/orb-ui/src/engine/animations/arc_dash.rs +++ b/orb-ui/src/engine/animations/arc_dash.rs @@ -85,7 +85,7 @@ impl Animation for ArcDash { if N == PEARL_RING_LED_COUNT { current_color = Argb::PEARL_USER_FLASH; } else { - current_color = Argb::DIAMOND_USER_FLASH; + current_color = Argb::DIAMOND_RING_USER_CAPTURE; } *phase += dt; if *phase >= FLASH_ON_TIME { diff --git a/orb-ui/src/engine/animations/mod.rs b/orb-ui/src/engine/animations/mod.rs index e2ae18c4..171984c7 100644 --- a/orb-ui/src/engine/animations/mod.rs +++ b/orb-ui/src/engine/animations/mod.rs @@ -18,6 +18,7 @@ pub use self::milky_way::MilkyWay; pub use self::progress::Progress; pub use self::r#static::Static; pub use self::simple_spinner::SimpleSpinner; +pub use self::slider::Slider; pub use self::spinner::Spinner; pub use self::wave::Wave; use crate::engine::{RingFrame, DIAMOND_RING_LED_COUNT, GAMMA}; diff --git a/orb-ui/src/engine/animations/simple_spinner.rs b/orb-ui/src/engine/animations/simple_spinner.rs index 58616a82..ff1fbbc3 100644 --- a/orb-ui/src/engine/animations/simple_spinner.rs +++ b/orb-ui/src/engine/animations/simple_spinner.rs @@ -25,7 +25,6 @@ pub struct SimpleSpinner { impl SimpleSpinner { /// Creates a new [`SimpleSpinner`] with one arc. - #[expect(dead_code)] #[must_use] pub fn new(color: Argb, background: Option) -> Self { Self { @@ -40,7 +39,6 @@ impl SimpleSpinner { } /// Set the speed of the spinner in radians per second. - #[expect(dead_code)] pub fn speed(self, speed: f64) -> Self { Self { speed, ..self } } @@ -51,7 +49,6 @@ impl SimpleSpinner { self.phase } - #[expect(dead_code)] pub fn fade_in(self, duration: f64) -> Self { Self { transition: Some(Transition::FadeIn(duration)), diff --git a/orb-ui/src/engine/animations/slider.rs b/orb-ui/src/engine/animations/slider.rs index 5ed1dd33..fcd93493 100644 --- a/orb-ui/src/engine/animations/slider.rs +++ b/orb-ui/src/engine/animations/slider.rs @@ -33,7 +33,6 @@ pub struct Shape { impl Slider { /// Creates a new [`Slider`]. #[must_use] - #[expect(dead_code)] pub fn new(progress: f64, color: Argb) -> Self { Self { color, @@ -60,7 +59,6 @@ impl Slider { /// Enable pulsing #[must_use] - #[expect(dead_code)] pub fn with_pulsing(mut self) -> Self { self.shape.pulse_phase = Some(0.0); self diff --git a/orb-ui/src/engine/animations/static.rs b/orb-ui/src/engine/animations/static.rs index bb516e20..89a3bf94 100644 --- a/orb-ui/src/engine/animations/static.rs +++ b/orb-ui/src/engine/animations/static.rs @@ -29,7 +29,6 @@ impl Static { } } - #[expect(dead_code)] pub fn fade_in(self, duration: f64) -> Self { Self { transition: Some(Transition::FadeIn(duration)), diff --git a/orb-ui/src/engine/animations/wave.rs b/orb-ui/src/engine/animations/wave.rs index 83d74dba..d5b02b62 100644 --- a/orb-ui/src/engine/animations/wave.rs +++ b/orb-ui/src/engine/animations/wave.rs @@ -39,7 +39,6 @@ impl Wave { } } - #[expect(dead_code)] pub fn with_delay(mut self, delay: f64) -> Self { self.transition = Some(Transition::StartDelay(delay)); self diff --git a/orb-ui/src/engine/diamond.rs b/orb-ui/src/engine/diamond.rs index 2b065f86..22d8daea 100644 --- a/orb-ui/src/engine/diamond.rs +++ b/orb-ui/src/engine/diamond.rs @@ -192,6 +192,7 @@ impl Runner { self.ring_animations_stack.set(level, Box::new(animation)); } + #[expect(dead_code)] fn set_cone( &mut self, level: u8, @@ -210,18 +211,19 @@ impl Runner { self.center_animations_stack.set(level, Box::new(animation)); } - fn stop_ring(&mut self, level: u8, force: bool) { - self.ring_animations_stack.stop(level, force); + fn stop_ring(&mut self, level: u8, transition: Transition) { + self.ring_animations_stack.stop(level, transition); } - fn stop_cone(&mut self, level: u8, force: bool) { + #[expect(dead_code)] + fn stop_cone(&mut self, level: u8, transition: Transition) { if let Some(animations) = &mut self.cone_animations_stack { - animations.stop(level, force); + animations.stop(level, transition); } } - fn stop_center(&mut self, level: u8, force: bool) { - self.center_animations_stack.stop(level, force); + fn stop_center(&mut self, level: u8, transition: Transition) { + self.center_animations_stack.stop(level, transition); } } @@ -229,12 +231,11 @@ impl Runner { impl EventHandler for Runner { #[allow(clippy::too_many_lines)] fn event(&mut self, event: &Event) -> Result<()> { - tracing::trace!("UI event: {}", serde_json::to_string(event)?.as_str()); + tracing::debug!("UI event: {}", serde_json::to_string(event)?.as_str()); match event { Event::Bootup => { - self.stop_ring(LEVEL_NOTICE, true); - self.stop_center(LEVEL_NOTICE, true); - self.stop_cone(LEVEL_NOTICE, true); + self.stop_ring(LEVEL_NOTICE, Transition::ForceStop); + self.stop_center(LEVEL_NOTICE, Transition::ForceStop); self.set_ring( LEVEL_BACKGROUND, animations::Idle::::default(), @@ -253,23 +254,21 @@ impl EventHandler for Runner { self.operator_pulse.stop(Transition::PlayOnce)?; self.operator_idle.api_mode(*api_mode); self.is_api_mode = *api_mode; - } - Event::Shutdown { requested } => { - // overwrite any existing animation by setting notice-level animation - // as the last animation before shutdown + + // make sure we set the background to off self.set_center( - LEVEL_NOTICE, - animations::Alert::::new( - if *requested { - Argb::DIAMOND_USER_QR_SCAN - } else { - Argb::DIAMOND_USER_AMBER - }, - BlinkDurations::from(vec![0.0, 0.3, 0.45, 0.3, 0.45, 0.45]), + LEVEL_BACKGROUND, + animations::Static::::new( + Argb::OFF, None, - false, ), ); + self.set_ring( + LEVEL_BACKGROUND, + animations::Static::::new(Argb::OFF, None), + ); + } + Event::Shutdown { requested: _ } => { self.sound .queue(sound::Type::Melody(sound::Melody::PoweringDown), None)?; self.set_ring( @@ -279,7 +278,7 @@ impl EventHandler for Runner { self.operator_action .trigger(1.0, Argb::OFF, true, false, true); } - Event::SignupStart => { + Event::SignupStartOperator => { self.capture_sound.reset(); self.sound .queue(sound::Type::Melody(sound::Melody::StartSignup), None)?; @@ -303,45 +302,25 @@ impl EventHandler for Runner { None, ), ); - self.stop_ring(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_NOTICE, true); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); + self.stop_center(LEVEL_NOTICE, Transition::ForceStop); self.set_ring( LEVEL_BACKGROUND, - animations::Static::::new( - Argb::DIAMOND_USER_QR_SCAN, - None, - ), - ); - self.set_ring( - LEVEL_NOTICE, - animations::Alert::::new( - Argb::DIAMOND_USER_QR_SCAN, - BlinkDurations::from(vec![0.0, 0.3, 0.3]), - None, - false, - ), - ); - self.set_cone( - LEVEL_NOTICE, - animations::Alert::::new( - Argb::DIAMOND_USER_AMBER, - BlinkDurations::from(vec![0.0, 0.5, 1.0]), - None, - false, - ), + animations::Static::::new(Argb::OFF, None), ); } Event::QrScanStart { schema } => { + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); match schema { QrScanSchema::Operator => { self.set_ring( LEVEL_FOREGROUND, - animations::Static::::new( - Argb::DIAMOND_USER_QR_SCAN, - None, - ), + animations::SimpleSpinner::new( + Argb::DIAMOND_RING_OPERATOR_QR_SCAN_SPINNER, + Some(Argb::DIAMOND_RING_OPERATOR_QR_SCAN), + ) + .fade_in(1.5), ); self.operator_signup_phase.operator_qr_code_ok(); } @@ -354,56 +333,44 @@ impl EventHandler for Runner { } QrScanSchema::User => { self.operator_signup_phase.user_qr_code_ok(); - self.set_center( + self.set_ring( LEVEL_FOREGROUND, - animations::Static::::new( - Argb::DIAMOND_USER_AMBER, - None, - ), + animations::SimpleSpinner::new( + Argb::DIAMOND_RING_USER_QR_SCAN_SPINNER, + Some(Argb::DIAMOND_RING_USER_QR_SCAN), + ) + .fade_in(1.5), ); } }; } Event::QrScanCapture => { - self.stop_center(LEVEL_FOREGROUND, true); self.sound .queue(sound::Type::Melody(sound::Melody::QrCodeCapture), None)?; } Event::QrScanCompleted { schema } => { - self.stop_ring(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_FOREGROUND, true); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); // reset ring background to black/off so that it's turned off in next animations self.set_ring( LEVEL_BACKGROUND, animations::Static::::new(Argb::OFF, None), ); match schema { - QrScanSchema::Operator => { - self.set_ring( - LEVEL_FOREGROUND, - animations::Alert::::new( - Argb::DIAMOND_USER_QR_SCAN, - BlinkDurations::from(vec![0.0, 0.5, 0.5]), - None, - false, - ), - ); - } - QrScanSchema::User => { - self.set_center( - LEVEL_FOREGROUND, - animations::Alert::::new( - Argb::DIAMOND_USER_AMBER, - BlinkDurations::from(vec![0.0, 0.5, 0.5]), - None, - false, - ), - ); - } + QrScanSchema::Operator => {} + QrScanSchema::User => {} QrScanSchema::Wifi => {} } } Event::QrScanUnexpected { schema, reason } => { + self.set_ring( + LEVEL_NOTICE, + animations::Alert::::new( + Argb::DIAMOND_RING_ERROR_SALMON, + BlinkDurations::from(vec![0.0, 2.0, 4.0]), + Some(vec![1.0, 1.5]), + true, + ), + ); match reason { QrScanUnexpectedReason::Invalid => { self.sound.queue( @@ -427,27 +394,25 @@ impl EventHandler for Runner { } QrScanSchema::Wifi => {} } - self.stop_center(LEVEL_FOREGROUND, true); } Event::QrScanFail { schema } => { self.sound .queue(sound::Type::Melody(sound::Melody::SoundError), None)?; match schema { QrScanSchema::User | QrScanSchema::Operator => { - self.stop_ring(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_FOREGROUND, true); - self.set_center( - LEVEL_FOREGROUND, - animations::Static::::new( - Argb::OFF, - None, + self.operator_signup_phase.failure(); + self.set_ring( + LEVEL_NOTICE, + animations::Alert::::new( + Argb::DIAMOND_RING_ERROR_SALMON, + BlinkDurations::from(vec![0.0, 2.0, 4.0]), + Some(vec![1.0, 1.5]), + true, ), ); - self.operator_signup_phase.failure(); } QrScanSchema::Wifi => {} } - self.stop_ring(LEVEL_FOREGROUND, true); } Event::QrScanSuccess { schema } => match schema { QrScanSchema::Operator => { @@ -458,31 +423,19 @@ impl EventHandler for Runner { self.operator_signup_phase.operator_qr_captured(); } QrScanSchema::User => { - self.operator_signup_phase.user_qr_captured(); - self.set_center( - LEVEL_NOTICE, - animations::Alert::::new( - Argb::DIAMOND_USER_AMBER, - BlinkDurations::from(vec![0.0, 0.5, 0.5]), - None, - false, - ), - ); - // wave center LEDs to transition to biometric capture - self.set_center( - LEVEL_FOREGROUND, - animations::Wave::::new( - Argb::DIAMOND_USER_AMBER, - 4.0, - 0.0, - false, - ), - ); self.sound.queue( sound::Type::Melody(sound::Melody::UserQrLoadSuccess), None, )?; - self.stop_cone(LEVEL_FOREGROUND, true); + self.operator_signup_phase.user_qr_captured(); + self.set_ring( + LEVEL_FOREGROUND, + animations::SimpleSpinner::new( + Argb::DIAMOND_RING_USER_QR_SCAN_SPINNER, + Some(Argb::DIAMOND_RING_USER_QR_SCAN), + ) + .speed(2.0 * PI / 7.0), // 7 seconds per turn + ); } QrScanSchema::Wifi => { self.sound.queue( @@ -496,8 +449,7 @@ impl EventHandler for Runner { .queue(sound::Type::Voice(sound::Voice::Timeout), None)?; match schema { QrScanSchema::User | QrScanSchema::Operator => { - self.stop_ring(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_FOREGROUND, true); + self.stop_center(LEVEL_FOREGROUND, Transition::FadeOut(1.0)); self.set_center( LEVEL_FOREGROUND, animations::Static::::new( @@ -506,10 +458,23 @@ impl EventHandler for Runner { ), ); self.operator_signup_phase.failure(); + + // show error animation + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); + self.set_ring( + LEVEL_NOTICE, + animations::Alert::::new( + Argb::DIAMOND_RING_ERROR_SALMON, + BlinkDurations::from(vec![0.0, 2.0, 4.0]), + Some(vec![1.0, 1.5]), + true, + ), + ); + } + QrScanSchema::Wifi => { + self.stop_ring(LEVEL_FOREGROUND, Transition::FadeOut(1.0)); } - QrScanSchema::Wifi => {} } - self.stop_ring(LEVEL_FOREGROUND, true); } Event::MagicQrActionCompleted { success } => { let melody = if *success { @@ -522,6 +487,28 @@ impl EventHandler for Runner { // to inform the operator to press the button. self.operator_signup_phase.failure(); } + Event::SignupStart => { + self.capture_sound.reset(); + self.stop_ring(LEVEL_FOREGROUND, Transition::FadeOut(2.0)); + // if not self-serve, the animations to transition + // to biometric capture are already set in `QrScanSuccess` + self.sound.queue( + sound::Type::Melody(sound::Melody::UserStartCapture), + None, + )?; + // pulsing wave animation displayed + // while we wait for the user to be in position + self.set_center( + LEVEL_FOREGROUND, + animations::Wave::::new( + Argb::DIAMOND_SHROUD_SUMMON_USER_AMBER, + 3.0, + 0.0, + true, + ) + .with_delay(1.5), + ); + } Event::BiometricCaptureHalfObjectivesCompleted => { // do nothing } @@ -529,44 +516,8 @@ impl EventHandler for Runner { self.operator_signup_phase.irises_captured(); } Event::BiometricCaptureProgress { progress } => { - if self - .ring_animations_stack - .stack - .get_mut(&LEVEL_NOTICE) - .and_then(|RunningAnimation { animation, .. }| { - animation - .as_any_mut() - .downcast_mut::>() - }) - .is_none() - { - // in case animation not yet initialized, initialize - self.set_ring( - LEVEL_NOTICE, - animations::Progress::::new( - 0.0, - None, - Argb::DIAMOND_USER_SIGNUP, - ), - ); - } - let ring_progress = self - .ring_animations_stack - .stack - .get_mut(&LEVEL_NOTICE) - .and_then(|RunningAnimation { animation, .. }| { - animation - .as_any_mut() - .downcast_mut::>() - }); - if let Some(ring_progress) = ring_progress { - ring_progress.set_progress(*progress, None); - } - } - Event::BiometricCaptureOcclusion { occlusion_detected } => { - // don't set a new wave animation if already waving - // to not interrupt the current animation - let waving = self + // set progress but wait for shroud to finish breathing + let shroud_breathing = self .center_animations_stack .stack .get_mut(&LEVEL_FOREGROUND) @@ -577,35 +528,60 @@ impl EventHandler for Runner { ) }) .is_some(); - if *occlusion_detected { - if !waving { - self.stop_center(LEVEL_FOREGROUND, true); - // wave center LEDs - self.set_center( - LEVEL_FOREGROUND, - animations::Wave::::new( - Argb::DIAMOND_USER_AMBER, - 4.0, + if !shroud_breathing { + if self + .ring_animations_stack + .stack + .get_mut(&LEVEL_NOTICE) + .and_then(|RunningAnimation { animation, .. }| { + animation + .as_any_mut() + .downcast_mut::>() + }) + .is_none() || *progress <= 0.01 + { + // in case animation not yet initialized, initialize + self.set_ring( + LEVEL_NOTICE, + animations::Progress::::new( 0.0, - false, + None, + Argb::DIAMOND_RING_USER_CAPTURE, ), ); } + let ring_progress = self + .ring_animations_stack + .stack + .get_mut(&LEVEL_NOTICE) + .and_then(|RunningAnimation { animation, .. }| { + animation + .as_any_mut() + .downcast_mut::>() + }); + if let Some(ring_progress) = ring_progress { + ring_progress.set_progress(*progress, None); + } + } + } + Event::BiometricCaptureOcclusion { occlusion_detected } => { + if *occlusion_detected { self.operator_signup_phase.capture_occlusion_issue(); } else { - self.stop_center(LEVEL_FOREGROUND, true); - self.set_center( - LEVEL_FOREGROUND, - animations::Static::::new( - Argb::DIAMOND_USER_AMBER, - None, - ), - ); self.operator_signup_phase.capture_occlusion_ok(); } } Event::BiometricCaptureDistance { in_range } => { - let waving = self + // show correct user position to operator with operator leds + if *in_range { + self.operator_signup_phase.capture_distance_ok(); + } else { + self.operator_signup_phase.capture_distance_issue(); + } + + // show correct position to user by playing sounds but + // only once shroud stops breathing + let shround_breathing = self .center_animations_stack .stack .get_mut(&LEVEL_FOREGROUND) @@ -616,36 +592,23 @@ impl EventHandler for Runner { ) }) .is_some(); - if *in_range { - self.operator_signup_phase.capture_distance_ok(); - if let Some(melody) = self.capture_sound.peekable().peek() { - if self.sound.try_queue(sound::Type::Melody(*melody))? { - self.capture_sound.next(); - } - } - self.stop_center(LEVEL_FOREGROUND, true); + if shround_breathing && *in_range { + // stop any ongoing breathing animation and transition to static self.set_center( LEVEL_FOREGROUND, animations::Static::::new( - Argb::DIAMOND_USER_AMBER, + Argb::DIAMOND_SHROUD_SUMMON_USER_AMBER, None, - ), + ) + .fade_in(1.5), ); - } else { - if !waving { - self.stop_center(LEVEL_FOREGROUND, true); - // wave center LEDs - self.set_center( - LEVEL_FOREGROUND, - animations::Wave::::new( - Argb::DIAMOND_USER_AMBER, - 4.0, - 0.0, - false, - ), - ); + } else if *in_range { + if let Some(melody) = self.capture_sound.peekable().peek() { + if self.sound.try_queue(sound::Type::Melody(*melody))? { + self.capture_sound.next(); + } } - self.operator_signup_phase.capture_distance_issue(); + } else { self.capture_sound = sound::capture::CaptureLoopSound::default(); let _ = self .sound @@ -658,32 +621,41 @@ impl EventHandler for Runner { // custom alert animation on ring // a bit off for 500ms then on with fade out animation // twice: first faster than the other + self.stop_center(LEVEL_FOREGROUND, Transition::FadeOut(0.5)); + // in case nothing is running on center, make sure we set the background to off + self.set_center( + LEVEL_BACKGROUND, + animations::Static::::new( + Argb::OFF, + None, + ), + ); self.set_ring( LEVEL_NOTICE, animations::Alert::::new( - Argb::DIAMOND_USER_SIGNUP, - BlinkDurations::from(vec![0.0, 0.5, 0.75, 0.2, 1.5, 0.2]), - Some(vec![0.49, 0.4, 0.19, 0.75, 0.2]), + Argb::DIAMOND_RING_USER_CAPTURE, + BlinkDurations::from(vec![ + 0.1, 0.5, 0.75, 0.2, 1.5, 0.4, 3.0, 0.2, + ]), + Some(vec![0.49, 0.4, 0.19, 0.75, 0.2, 0.2, 1.0]), true, ), ); - self.stop_center(LEVEL_FOREGROUND, false); - self.stop_ring(LEVEL_NOTICE, false); - - // preparing animation for biometric pipeline progress - self.set_ring( - LEVEL_FOREGROUND, - animations::Progress::::new( - 0.0, - None, - Argb::DIAMOND_USER_SIGNUP, - ), - ); self.operator_signup_phase.iris_scan_complete(); } Event::BiometricPipelineProgress { progress } => { - let ring_animation = self + if *progress <= 0.01 { + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); + self.set_ring( + LEVEL_FOREGROUND, + animations::Progress::::new( + 0.0, + None, + Argb::DIAMOND_RING_USER_CAPTURE, + ), + ); + } else if let Some(ring_animation) = self .ring_animations_stack .stack .get_mut(&LEVEL_FOREGROUND) @@ -691,12 +663,16 @@ impl EventHandler for Runner { animation .as_any_mut() .downcast_mut::>() - }); - if let Some(ring_animation) = ring_animation { + }) { ring_animation.set_progress(*progress, None); } else { - tracing::warn!( - "BiometricPipelineProgress: ring animation not found" + self.set_ring( + LEVEL_FOREGROUND, + animations::Progress::::new( + 0.0, + None, + Argb::DIAMOND_RING_USER_CAPTURE, + ), ); } @@ -743,6 +719,11 @@ impl EventHandler for Runner { self.operator_signup_phase.biometric_pipeline_successful(); } Event::SignupFail { reason } => { + // replace background + self.set_ring( + LEVEL_BACKGROUND, + animations::Static::::new(Argb::OFF, None), + ); self.sound.queue( sound::Type::Melody(sound::Melody::SoundError), Some(Duration::from_millis(2000)), @@ -791,8 +772,8 @@ impl EventHandler for Runner { self.operator_signup_phase.failure(); // turn off center - self.stop_center(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_NOTICE, true); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); + self.stop_center(LEVEL_NOTICE, Transition::ForceStop); // close biometric capture progress if let Some(progress) = self @@ -807,7 +788,6 @@ impl EventHandler for Runner { { progress.set_progress(2.0, None); } - self.stop_ring(LEVEL_NOTICE, false); // close biometric pipeline progress if let Some(progress) = self @@ -822,7 +802,18 @@ impl EventHandler for Runner { { progress.set_progress(2.0, None); } - self.stop_ring(LEVEL_FOREGROUND, false); + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); + + // show error animation + self.set_ring( + LEVEL_NOTICE, + animations::Alert::::new( + Argb::DIAMOND_RING_ERROR_SALMON, + BlinkDurations::from(vec![0.0, 2.0, 4.0]), + Some(vec![1.0, 1.5]), + true, + ), + ); } Event::SignupSuccess => { self.sound @@ -830,11 +821,16 @@ impl EventHandler for Runner { self.operator_signup_phase.signup_successful(); + // replace background + self.set_ring( + LEVEL_BACKGROUND, + animations::Static::::new(Argb::OFF, None), + ); // alert with ring self.set_ring( LEVEL_NOTICE, animations::Alert::::new( - Argb::DIAMOND_USER_SIGNUP, + Argb::DIAMOND_RING_USER_CAPTURE, BlinkDurations::from(vec![0.0, 0.6, 3.6]), None, false, @@ -842,13 +838,27 @@ impl EventHandler for Runner { ); } Event::Idle => { - self.stop_ring(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_FOREGROUND, true); - self.stop_cone(LEVEL_FOREGROUND, true); - self.stop_ring(LEVEL_NOTICE, false); - self.stop_center(LEVEL_NOTICE, false); - self.stop_cone(LEVEL_NOTICE, false); + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); + self.stop_ring(LEVEL_NOTICE, Transition::FadeOut(0.5)); + self.stop_center(LEVEL_NOTICE, Transition::FadeOut(0.5)); + self.operator_signup_phase.idle(); + self.set_center( + LEVEL_BACKGROUND, + animations::Static::::new( + Argb::OFF, + None, + ), + ); + self.set_ring( + LEVEL_FOREGROUND, + animations::Static::::new( + Argb::DIAMOND_RING_USER_QR_SCAN, + None, + ) + .fade_in(1.5), + ); } Event::GoodInternet => { self.operator_idle.good_internet(); @@ -900,7 +910,7 @@ impl EventHandler for Runner { self.set_ring( LEVEL_NOTICE, animations::Spinner::::triple( - Argb::DIAMOND_USER_AMBER, + Argb::DIAMOND_SHROUD_SUMMON_USER_AMBER, None, ), ); diff --git a/orb-ui/src/engine/mod.rs b/orb-ui/src/engine/mod.rs index 6eeaaea1..f6240977 100644 --- a/orb-ui/src/engine/mod.rs +++ b/orb-ui/src/engine/mod.rs @@ -188,8 +188,8 @@ event_enum! { #[event_enum(method = boot_complete)] BootComplete { api_mode: bool }, /// Start of the signup phase, triggered on button press - #[event_enum(method = signup_start)] - SignupStart, + #[event_enum(method = signup_start_operator)] + SignupStartOperator, /// Start of QR scan. #[event_enum(method = qr_scan_start)] QrScanStart { @@ -232,6 +232,9 @@ event_enum! { /// Network connection successful #[event_enum(method = network_connection_success)] NetworkConnectionSuccess, + /// Biometric capture start. Triggered on app button press (app-based self-serve flow), or orb button press (operator-based self-serve flow). + #[event_enum(method = signup_start)] + SignupStart, /// Biometric capture half of the objectives completed. #[event_enum(method = biometric_capture_half_objectives_completed)] BiometricCaptureHalfObjectivesCompleted, @@ -386,7 +389,6 @@ pub enum Transition { /// immediately stop the animation ForceStop, /// fade out the animation with a duration. - #[expect(dead_code)] FadeOut(f64), /// play the animation one last time PlayOnce, @@ -551,10 +553,14 @@ impl AnimationsStack { } } - fn stop(&mut self, level: u8, force: bool) { + fn stop(&mut self, level: u8, transition: Transition) { if let Some(RunningAnimation { animation, kill }) = self.stack.get_mut(&level) { - let _ = animation.stop(Transition::ForceStop); - *kill = *kill || force; + if let Transition::ForceStop = transition { + *kill = true; + } else if let Err(e) = animation.stop(transition) { + tracing::error!("Failed to stop animation: {}", e); + *kill = true; + } } } diff --git a/orb-ui/src/engine/pearl.rs b/orb-ui/src/engine/pearl.rs index 6375f2bf..736ec676 100644 --- a/orb-ui/src/engine/pearl.rs +++ b/orb-ui/src/engine/pearl.rs @@ -175,12 +175,12 @@ impl Runner { self.center_animations_stack.set(level, Box::new(animation)); } - fn stop_ring(&mut self, level: u8, force: bool) { - self.ring_animations_stack.stop(level, force); + fn stop_ring(&mut self, level: u8, transition: Transition) { + self.ring_animations_stack.stop(level, transition); } - fn stop_center(&mut self, level: u8, force: bool) { - self.center_animations_stack.stop(level, force); + fn stop_center(&mut self, level: u8, transition: Transition) { + self.center_animations_stack.stop(level, transition); } } @@ -188,11 +188,11 @@ impl Runner { impl EventHandler for Runner { #[allow(clippy::too_many_lines)] fn event(&mut self, event: &Event) -> Result<()> { - tracing::trace!("UI event: {}", serde_json::to_string(event)?.as_str()); + tracing::debug!("UI event: {}", serde_json::to_string(event)?.as_str()); match event { Event::Bootup => { - self.stop_ring(LEVEL_NOTICE, true); - self.stop_center(LEVEL_NOTICE, true); + self.stop_ring(LEVEL_NOTICE, Transition::ForceStop); + self.stop_center(LEVEL_NOTICE, Transition::ForceStop); self.set_ring( LEVEL_BACKGROUND, animations::Idle::::default(), @@ -237,7 +237,7 @@ impl EventHandler for Runner { self.operator_action .trigger(1.0, Argb::OFF, true, false, true); } - Event::SignupStart => { + Event::SignupStartOperator => { self.capture_sound.reset(); self.sound .queue(sound::Type::Melody(sound::Melody::StartSignup), None)?; @@ -258,9 +258,9 @@ impl EventHandler for Runner { LEVEL_BACKGROUND, animations::Static::::new(Argb::OFF, None), ); - self.stop_ring(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_NOTICE, true); + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); + self.stop_center(LEVEL_NOTICE, Transition::ForceStop); // reset ring background to black/off so that it's turned off in next animations self.set_ring( @@ -295,9 +295,8 @@ impl EventHandler for Runner { // initialize ring with short segment to invite user to scan QR self.set_ring( LEVEL_FOREGROUND, - animations::Progress::::new( + animations::Slider::::new( 0.0, - None, Argb::PEARL_USER_SIGNUP, ), ); @@ -306,13 +305,13 @@ impl EventHandler for Runner { } Event::QrScanCapture => { // stop wave (foreground) & show alert/blinks (notice) - self.stop_center(LEVEL_FOREGROUND, true); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); self.sound .queue(sound::Type::Melody(sound::Melody::QrCodeCapture), None)?; } Event::QrScanCompleted { schema } => { // stop wave (foreground) & show alert/blinks (notice) - self.stop_center(LEVEL_FOREGROUND, true); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); self.set_center( LEVEL_NOTICE, animations::Alert::::new( @@ -347,7 +346,7 @@ impl EventHandler for Runner { match schema { QrScanSchema::User => { // remove short segment from ring - self.stop_ring(LEVEL_FOREGROUND, true); + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); self.operator_signup_phase.user_qr_code_issue(); } QrScanSchema::Operator => { @@ -356,7 +355,7 @@ impl EventHandler for Runner { QrScanSchema::Wifi => {} } // stop wave - self.stop_center(LEVEL_FOREGROUND, true); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); } Event::QrScanFail { schema } => { self.sound @@ -364,13 +363,13 @@ impl EventHandler for Runner { match schema { QrScanSchema::User | QrScanSchema::Operator => { // in case schema is user qr - self.stop_ring(LEVEL_FOREGROUND, true); + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); self.operator_signup_phase.failure(); } QrScanSchema::Wifi => {} } // stop wave - self.stop_center(LEVEL_FOREGROUND, true); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); } Event::QrScanSuccess { schema } => { match schema { @@ -383,26 +382,7 @@ impl EventHandler for Runner { } QrScanSchema::User => { self.operator_signup_phase.user_qr_captured(); - // initialize ring with animated short segment to invite user to start iris capture - self.set_ring( - LEVEL_NOTICE, - animations::Progress::::new( - 0.0, - None, - Argb::PEARL_USER_SIGNUP, - ), - ); - // remove short segment from ring (foreground, superseded by notice level above) - self.stop_ring(LEVEL_FOREGROUND, false); - // off background for biometric-capture, which relies on LEVEL_NOTICE animations - self.stop_center(LEVEL_FOREGROUND, true); - self.set_center( - LEVEL_FOREGROUND, - animations::Static::::new( - Argb::OFF, - None, - ), - ); + // see `Event::BiometricCaptureStart } QrScanSchema::Wifi => { self.sound.queue( @@ -418,13 +398,13 @@ impl EventHandler for Runner { match schema { QrScanSchema::User | QrScanSchema::Operator => { // in case schema is user qr - self.stop_ring(LEVEL_FOREGROUND, true); + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); self.operator_signup_phase.failure(); } QrScanSchema::Wifi => {} } // stop wave - self.stop_center(LEVEL_FOREGROUND, true); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); } Event::MagicQrActionCompleted { success } => { let melody = if *success { @@ -437,6 +417,27 @@ impl EventHandler for Runner { // to inform the operator to press the button. self.operator_signup_phase.failure(); } + Event::SignupStart => { + self.sound.queue( + sound::Type::Melody(sound::Melody::UserQrLoadSuccess), + None, + )?; + // initialize ring with animated short segment to invite user to start iris capture + self.set_ring( + LEVEL_NOTICE, + animations::Slider::::new( + 0.0, + Argb::PEARL_USER_SIGNUP, + ) + .with_pulsing(), + ); + // off background for biometric-capture, which relies on LEVEL_NOTICE animations + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); + self.set_center( + LEVEL_FOREGROUND, + animations::Static::::new(Argb::OFF, None), + ); + } Event::BiometricCaptureHalfObjectivesCompleted => { // do nothing } @@ -522,8 +523,8 @@ impl EventHandler for Runner { .map(|x| { x.set_progress(2.0, None); }); - self.stop_center(LEVEL_NOTICE, false); - self.stop_ring(LEVEL_NOTICE, false); + self.stop_center(LEVEL_NOTICE, Transition::PlayOnce); + self.stop_ring(LEVEL_NOTICE, Transition::PlayOnce); // preparing animation for biometric pipeline progress self.set_ring( @@ -653,9 +654,9 @@ impl EventHandler for Runner { if let Some(slider) = slider { slider.set_progress(2.0, None); } - self.stop_ring(LEVEL_FOREGROUND, false); - self.stop_ring(LEVEL_NOTICE, true); - self.stop_center(LEVEL_FOREGROUND, true); + self.stop_ring(LEVEL_FOREGROUND, Transition::PlayOnce); + self.stop_ring(LEVEL_NOTICE, Transition::ForceStop); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); self.set_ring( LEVEL_FOREGROUND, animations::Idle::::new( @@ -683,9 +684,9 @@ impl EventHandler for Runner { if let Some(slider) = slider { slider.set_progress(2.0, None); } - self.stop_ring(LEVEL_FOREGROUND, false); - self.stop_ring(LEVEL_NOTICE, true); - self.stop_center(LEVEL_FOREGROUND, true); + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); + self.stop_ring(LEVEL_NOTICE, Transition::ForceStop); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); self.set_ring( LEVEL_FOREGROUND, animations::Idle::::new( @@ -695,10 +696,18 @@ impl EventHandler for Runner { ); } Event::Idle => { - self.stop_ring(LEVEL_FOREGROUND, false); - self.stop_ring(LEVEL_NOTICE, false); - self.stop_center(LEVEL_FOREGROUND, false); - self.stop_center(LEVEL_NOTICE, false); + self.set_ring( + LEVEL_BACKGROUND, + animations::Static::::new(Argb::OFF, None), + ); + self.set_center( + LEVEL_BACKGROUND, + animations::Static::::new(Argb::OFF, None), + ); + self.stop_ring(LEVEL_FOREGROUND, Transition::ForceStop); + self.stop_ring(LEVEL_NOTICE, Transition::ForceStop); + self.stop_center(LEVEL_FOREGROUND, Transition::ForceStop); + self.stop_center(LEVEL_NOTICE, Transition::ForceStop); self.operator_signup_phase.idle(); } Event::GoodInternet => {