Skip to content

Commit

Permalink
orb-ui: upgraded animations (#238)
Browse files Browse the repository at this point in the history
* orb-ui: upgraded animations

- 2 new animations: milkyway and simple-spinner
- new way to stop with a transition
- new transitions between animations
- only use r,g,b to pulse LEDs, not the dimming value

* orb-ui: use enum for transition_from result

explicit type

* orb-ui: remove clippy warnings allowances

* orb-ui: milky way animation: fix types

maximum value for an LED channel is 255. So a delta must be an i16 to be able to apply a delta that is larger than 127.

* orb-ui: remove useless logs

* orb-ui: derive Debug on struct SimpleSpinner

* orb-ui: [expect()] instead of [allow()]

* orb-ui: refactor `factor` to `scaling_factor`

more explicit

* orb-ui: remove useless logs

* orb-ui: remove silently failing

in case transition to stop operator pulse animation isn't supported.

* orb-ui: rollback changes to spinner animation

* orb-ui: fix clippy

* orb-ui: derive Eq on TransitionStatus

* orb-ui: Argb: add lerp function

* orb-ui: fix possible overflow in milky way animation
  • Loading branch information
fouge authored Oct 1, 2024
1 parent 7a28439 commit 9306f8a
Show file tree
Hide file tree
Showing 18 changed files with 943 additions and 213 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 29 additions & 27 deletions orb-ui/rgb/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
use std::ops;
use std::ops::Add;

/// RGB LED color.
#[derive(Eq, PartialEq, Copy, Clone, Default, Debug, Serialize, Deserialize)]
Expand All @@ -10,44 +11,45 @@ pub struct Argb(
pub u8,
);

impl Argb {
pub fn lerp(self, other: Self, t: f64) -> Self {
let t = t.clamp(0.0, 1.0);
self * (1.0 - t) + other * t
}
}
impl ops::Mul<f64> for Argb {
type Output = Self;

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn mul(self, rhs: f64) -> Self::Output {
// if intensity is led by the dimming value, use it
// otherwise, modify the color values
if let Some(dim) = self.0 {
Argb(
Some(((f64::from(dim) * rhs) as u8).clamp(0, Self::DIMMING_MAX_VALUE)),
self.1,
self.2,
self.3,
)
} else {
Argb(
None,
((f64::from(self.1) * rhs) as u8).clamp(0, 255),
((f64::from(self.2) * rhs) as u8).clamp(0, 255),
((f64::from(self.3) * rhs) as u8).clamp(0, 255),
)
}
Argb(
self.0,
((f64::from(self.1) * rhs) as u8).clamp(0, u8::MAX),
((f64::from(self.2) * rhs) as u8).clamp(0, u8::MAX),
((f64::from(self.3) * rhs) as u8).clamp(0, u8::MAX),
)
}
}

impl ops::MulAssign<f64> for Argb {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn mul_assign(&mut self, rhs: f64) {
// if intensity is led by the dimming value, use it
// otherwise, modify the color values
if let Some(dim) = self.0 {
self.0 =
Some(((f64::from(dim) * rhs) as u8).clamp(0, Self::DIMMING_MAX_VALUE));
} else {
self.1 = ((f64::from(self.1) * rhs) as u8).clamp(0, 255);
self.2 = ((f64::from(self.2) * rhs) as u8).clamp(0, 255);
self.3 = ((f64::from(self.3) * rhs) as u8).clamp(0, 255);
};
self.1 = ((f64::from(self.1) * rhs) as u8).clamp(0, u8::MAX);
self.2 = ((f64::from(self.2) * rhs) as u8).clamp(0, u8::MAX);
self.3 = ((f64::from(self.3) * rhs) as u8).clamp(0, u8::MAX);
}
}

impl Add for Argb {
type Output = Self;

fn add(self, rhs: Self) -> Self::Output {
Argb(
self.0,
self.1.saturating_add(rhs.1),
self.2.saturating_add(rhs.2),
self.3.saturating_add(rhs.3),
)
}
}

Expand Down
15 changes: 15 additions & 0 deletions orb-ui/src/engine/animations/alert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ pub struct Alert<const N: usize> {
phase: f64,
/// first edge from pattern\[0\] to pattern\[1\] has LEDs on
active_at_start: bool,
/// initial delay, in seconds, before starting the animation
initial_delay: f64,
}

impl<const N: usize> Alert<N> {
Expand All @@ -60,8 +62,15 @@ impl<const N: usize> Alert<N> {
blinks,
phase: 0.0,
active_at_start,
initial_delay: 0.0,
}
}

#[expect(dead_code)]
pub fn with_delay(mut self, delay: f64) -> Self {
self.initial_delay = delay;
self
}
}

impl<const N: usize> Animation for Alert<N> {
Expand All @@ -84,6 +93,12 @@ impl<const N: usize> Animation for Alert<N> {
let mut duration_acc = 0.0;
let mut color = Argb::OFF;

// initial delay
if self.initial_delay > 0.0 {
self.initial_delay -= dt;
return AnimationState::Running;
}

// sum up each edge duration and quit when the phase is in the current edge
for (i, &edge_duration) in self.blinks.0.iter().enumerate() {
duration_acc += edge_duration;
Expand Down
7 changes: 5 additions & 2 deletions orb-ui/src/engine/animations/arc_pulse.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::engine::animations::render_lines;
use crate::engine::{Animation, AnimationState, RingFrame};
use crate::engine::{Animation, AnimationState, RingFrame, TransitionStatus};
use orb_rgb::Argb;
use std::{any::Any, f64::consts::PI};

Expand Down Expand Up @@ -60,9 +60,12 @@ impl<const N: usize> Animation for ArcPulse<N> {
AnimationState::Running
}

fn transition_from(&mut self, superseded: &dyn Any) {
fn transition_from(&mut self, superseded: &dyn Any) -> TransitionStatus {
if let Some(other) = superseded.downcast_ref::<ArcPulse<N>>() {
self.shape = other.shape.clone();
TransitionStatus::Smooth
} else {
TransitionStatus::Sharp
}
}
}
Expand Down
2 changes: 0 additions & 2 deletions orb-ui/src/engine/animations/idle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,4 @@ impl<const N: usize> Animation for Idle<N> {
}
AnimationState::Running
}

fn transition_from(&mut self, _superseded: &dyn Any) {}
}
227 changes: 227 additions & 0 deletions orb-ui/src/engine/animations/milky_way.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
use crate::engine::{Animation, Transition};
use crate::engine::{AnimationState, RingFrame};
use eyre::eyre;
use orb_rgb::Argb;
use std::any::Any;
use std::f64::consts::PI;

/// Milky Way animation.
/// The animation is a randomized ring of LEDs, where each LED is a different color.
pub struct MilkyWay<const N: usize> {
phase: f64,
frame: RingFrame<N>,
config: MilkyWayConfig,
transition: Option<Transition>,
transition_time: f64,
}

#[derive(Debug, Eq, PartialEq, Clone)]
pub struct MilkyWayConfig {
/// initial background color from which the randomized ring is generated
/// color oscillates between this and background + *_delta values
pub background: Argb,
/// maximum delta in colors between two animated frames
pub fade_delta: i16,
/// delta in colors to generate the first frame
pub initial_delta: i16,
/// minimum and maximum values for each red channel
pub red_min_max: (u8, u8),
/// minimum and maximum values for each green channel
pub green_min_max: (u8, u8),
/// minimum and maximum values for each blue channel
pub blue_min_max: (u8, u8),
}

impl MilkyWayConfig {
pub fn default() -> Self {
MilkyWayConfig {
background: Argb(Some(10), 30, 20, 5),
fade_delta: 2,
initial_delta: 5,
red_min_max: (10, 40),
green_min_max: (1, 40),
blue_min_max: (1, 10),
}
}
}

fn rand_delta(delta_max: i16) -> i16 {
let sign = if rand::random::<i16>() % 2 == 0 {
1
} else {
-1
};
(rand::random::<i16>() % delta_max) * sign
}

fn generate_random(frame: &mut [Argb], config: &MilkyWayConfig) {
let new_color = |config: &MilkyWayConfig| {
Argb(
config.background.0,
(i16::from(config.background.1) + rand_delta(config.initial_delta)).clamp(
i16::from(config.red_min_max.0),
i16::from(config.red_min_max.1),
) as u8,
(i16::from(config.background.2) + rand_delta(config.initial_delta)).clamp(
i16::from(config.green_min_max.0),
i16::from(config.green_min_max.1),
) as u8,
(i16::from(config.background.3) + rand_delta(config.initial_delta)).clamp(
i16::from(config.blue_min_max.0),
i16::from(config.blue_min_max.1),
) as u8,
)
};

let mut c = new_color(config);
for (i, led) in frame.iter_mut().enumerate() {
if i % 2 == 0 {
c = new_color(config);
}
*led = c;
}
}

impl<const N: usize> MilkyWay<N> {
/// Create idle ring
#[expect(dead_code)]
#[must_use]
pub fn new(config: MilkyWayConfig) -> Self {
// generate initial randomized frame
let mut frame: [Argb; N] = [config.background; N];
generate_random(&mut frame, &config);

Self {
phase: 0.0,
transition: None,
transition_time: 1.5,
frame,
config,
}
}

#[expect(dead_code)]
pub fn fade_in(self, duration: f64) -> Self {
Self {
transition: Some(Transition::FadeIn(duration)),
transition_time: 0.0,
..self
}
}
}

impl<const N: usize> Default for MilkyWay<N> {
fn default() -> Self {
let mut frame: [Argb; N] = [MilkyWayConfig::default().background; N];
generate_random(&mut frame, &MilkyWayConfig::default());
Self {
phase: 0.0,
transition: None,
transition_time: 1.5,
frame,
config: MilkyWayConfig::default(),
}
}
}

impl<const N: usize> Animation for MilkyWay<N> {
type Frame = RingFrame<N>;

fn as_any(&self) -> &dyn Any {
self
}

fn as_any_mut(&mut self) -> &mut dyn Any {
self
}

#[allow(clippy::cast_precision_loss)]
fn animate(
&mut self,
frame: &mut RingFrame<N>,
dt: f64,
idle: bool,
) -> AnimationState {
match self.transition {
Some(Transition::ForceStop) => AnimationState::Finished,
Some(Transition::StartDelay(delay)) => {
self.transition_time += dt;
if self.transition_time >= delay {
self.transition = None;
}
AnimationState::Running
}
Some(Transition::FadeOut(duration)) => {
// apply sine wave to stop the animation smoothly
self.phase += dt;
let scaling_factor = (self.transition_time / duration * PI / 2.0).cos();
for (led, background_led) in frame.iter_mut().zip(&self.frame) {
*led = *background_led * scaling_factor;
}
if self.phase >= duration {
AnimationState::Finished
} else {
AnimationState::Running
}
}
Some(Transition::FadeIn(duration)) => {
// apply sine wave to start the animation smoothly
self.phase += dt;
let scaling_factor = (self.transition_time / duration * PI / 2.0).sin();
for (led, background_led) in frame.iter_mut().zip(&self.frame) {
*led = *background_led * scaling_factor;
}
if self.phase >= duration {
self.transition = None;
}
AnimationState::Running
}
_ => {
let mut color = self.frame[0];
for (i, led) in &mut self.frame.iter_mut().enumerate() {
if i % 2 == 0 {
color = Argb(
led.0,
(i16::from(led.1) + rand_delta(self.config.fade_delta))
.clamp(
i16::from(self.config.red_min_max.0),
i16::from(self.config.red_min_max.1),
) as u8,
(i16::from(led.2) + rand_delta(self.config.fade_delta))
.clamp(
i16::from(self.config.green_min_max.0),
i16::from(self.config.green_min_max.1),
) as u8,
(i16::from(led.3) + rand_delta(self.config.fade_delta))
.clamp(
i16::from(self.config.blue_min_max.0),
i16::from(self.config.blue_min_max.1),
) as u8,
);
}

*led = color;
}

if !idle {
frame.copy_from_slice(&self.frame);
}
AnimationState::Running
}
}
}

fn stop(&mut self, transition: Transition) -> eyre::Result<()> {
if transition == Transition::PlayOnce || transition == Transition::Shrink {
return Err(eyre!(
"Transition {:?} not supported for Milky Way animation",
transition
));
}

self.transition_time = 0.0;
self.transition = Some(transition);

Ok(())
}
}
Loading

0 comments on commit 9306f8a

Please sign in to comment.