1use crate::BLACK;
4use crate::time::TimeDuration;
5use crate::types::{LoopCount, SequenceError, SequenceStep, TransitionStyle};
6use heapless::Vec;
7use palette::{Mix, Srgb};
8
9#[inline]
15fn apply_easing(t: f32, transition: TransitionStyle) -> f32 {
16 match transition {
17 TransitionStyle::Step => t,
18 TransitionStyle::Linear => t,
19 TransitionStyle::EaseIn => t * t,
20 TransitionStyle::EaseOut => t * (2.0 - t),
21 TransitionStyle::EaseInOut => {
22 if t < 0.5 {
23 2.0 * t * t
24 } else {
25 -1.0 + (4.0 - 2.0 * t) * t
26 }
27 }
28 TransitionStyle::EaseOutIn => {
29 if t < 0.5 {
30 let t2 = t * 2.0;
32 t2 * (2.0 - t2) * 0.5
33 } else {
34 let t2 = (t - 0.5) * 2.0;
36 0.5 + t2 * t2 * 0.5
37 }
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy)]
44pub struct StepPosition<D: TimeDuration> {
45 pub step_index: usize,
47 pub time_in_step: D,
49 pub time_until_step_end: D,
51 pub is_complete: bool,
53 pub current_loop: u32,
55}
56
57#[derive(Debug, Clone)]
59pub struct RgbSequence<D: TimeDuration, const N: usize> {
60 steps: Vec<SequenceStep<D>, N>,
61 loop_count: LoopCount,
62 start_color: Option<Srgb>,
63 landing_color: Option<Srgb>,
64 loop_duration: D,
65
66 color_fn: Option<fn(Srgb, D) -> Srgb>,
67 timing_fn: Option<fn(D) -> Option<D>>,
68}
69
70impl<D: TimeDuration, const N: usize> RgbSequence<D, N> {
71 pub fn builder() -> SequenceBuilder<D, N> {
73 SequenceBuilder::new()
74 }
75
76 pub fn from_function(
82 base_color: Srgb,
83 color_fn: fn(Srgb, D) -> Srgb,
84 timing_fn: fn(D) -> Option<D>,
85 ) -> Self {
86 Self {
87 steps: Vec::new(),
88 loop_count: LoopCount::Finite(1),
89 landing_color: None,
90 loop_duration: D::ZERO,
91 start_color: Some(base_color),
92 color_fn: Some(color_fn),
93 timing_fn: Some(timing_fn),
94 }
95 }
96
97 pub fn solid(color: Srgb) -> Result<Self, SequenceError> {
101 Self::builder()
102 .step(color, D::ZERO, TransitionStyle::Step)?
103 .build()
104 }
105
106 #[inline]
111 pub fn evaluate(&self, elapsed: D) -> (Srgb, Option<D>) {
112 if let (Some(color_fn), Some(timing_fn)) = (self.color_fn, self.timing_fn) {
114 let base = self.start_color.unwrap_or(BLACK);
115 return (color_fn(base, elapsed), timing_fn(elapsed));
116 }
117
118 if let Some(position) = self.find_step_position(elapsed) {
120 let color = self.color_at_position(&position);
121 let timing = self.next_service_time_from_position(&position);
122 (color, timing)
123 } else {
124 (BLACK, None)
126 }
127 }
128
129 #[inline]
131 fn is_complete_step_based(&self, elapsed: D) -> bool {
132 match self.loop_count {
133 LoopCount::Finite(count) => {
134 let loop_millis = self.loop_duration.as_millis();
135 if loop_millis == 0 {
136 elapsed.as_millis() > 0
137 } else {
138 let total_duration = loop_millis * (count as u64);
139 elapsed.as_millis() >= total_duration
140 }
141 }
142 LoopCount::Infinite => false,
143 }
144 }
145
146 #[inline]
148 fn handle_zero_duration_sequence(&self, elapsed: D) -> StepPosition<D> {
149 let is_complete = elapsed.as_millis() > 0;
150 let step_index = if is_complete { self.steps.len() - 1 } else { 0 };
151
152 StepPosition {
153 step_index,
154 time_in_step: D::ZERO,
155 time_until_step_end: D::ZERO,
156 is_complete,
157 current_loop: 0,
158 }
159 }
160
161 #[inline]
163 fn create_complete_position(&self) -> StepPosition<D> {
164 let last_index = self.steps.len() - 1;
165 let loop_count = match self.loop_count {
166 LoopCount::Finite(count) => count,
167 LoopCount::Infinite => 0,
168 };
169
170 StepPosition {
171 step_index: last_index,
172 time_in_step: self.steps[last_index].duration,
173 time_until_step_end: D::ZERO,
174 is_complete: true,
175 current_loop: loop_count.saturating_sub(1),
176 }
177 }
178
179 #[inline]
181 fn find_step_at_time(&self, time_in_loop: D, current_loop: u32) -> StepPosition<D> {
182 let mut accumulated_time = D::ZERO;
183
184 for (step_idx, step) in self.steps.iter().enumerate() {
185 let step_end_time =
186 D::from_millis(accumulated_time.as_millis() + step.duration.as_millis());
187
188 if time_in_loop.as_millis() < step_end_time.as_millis() {
189 let time_in_step =
190 D::from_millis(time_in_loop.as_millis() - accumulated_time.as_millis());
191 let time_until_end = step_end_time.saturating_sub(time_in_loop);
192
193 return StepPosition {
194 step_index: step_idx,
195 time_in_step,
196 time_until_step_end: time_until_end,
197 is_complete: false,
198 current_loop,
199 };
200 }
201
202 accumulated_time = step_end_time;
203 }
204
205 let last_index = self.steps.len() - 1;
206 StepPosition {
207 step_index: last_index,
208 time_in_step: self.steps[last_index].duration,
209 time_until_step_end: D::ZERO,
210 is_complete: false,
211 current_loop,
212 }
213 }
214
215 #[inline]
222 fn interpolate_color(&self, position: &StepPosition<D>, step: &SequenceStep<D>) -> Srgb {
223 let use_start_color = position.step_index == 0
225 && position.current_loop == 0
226 && self.start_color.is_some()
227 && matches!(
228 step.transition,
229 TransitionStyle::Linear
230 | TransitionStyle::EaseIn
231 | TransitionStyle::EaseOut
232 | TransitionStyle::EaseInOut
233 | TransitionStyle::EaseOutIn
234 );
235
236 let previous_color = if use_start_color {
237 self.start_color.unwrap()
238 } else if position.step_index == 0 {
239 self.steps.last().unwrap().color
240 } else {
241 self.steps[position.step_index - 1].color
242 };
243
244 let duration_millis = step.duration.as_millis();
245 if duration_millis == 0 {
246 return step.color;
247 }
248
249 let time_millis = position.time_in_step.as_millis();
250 let mut progress = (time_millis as f32) / (duration_millis as f32);
251 progress = progress.clamp(0.0, 1.0);
252
253 progress = apply_easing(progress, step.transition);
255
256 previous_color.mix(step.color, progress)
257 }
258
259 pub fn find_step_position(&self, elapsed: D) -> Option<StepPosition<D>> {
264 if self.steps.is_empty() {
265 return None;
266 }
267
268 let loop_millis = self.loop_duration.as_millis();
269
270 if loop_millis == 0 {
271 return Some(self.handle_zero_duration_sequence(elapsed));
272 }
273
274 if self.is_complete_step_based(elapsed) {
275 return Some(self.create_complete_position());
276 }
277
278 let elapsed_millis = elapsed.as_millis();
279 let current_loop = (elapsed_millis / loop_millis) as u32;
281 let time_in_loop = D::from_millis(elapsed_millis % loop_millis);
282
283 Some(self.find_step_at_time(time_in_loop, current_loop))
284 }
285
286 #[inline]
288 fn color_at_position(&self, position: &StepPosition<D>) -> Srgb {
289 if position.is_complete {
290 return self
291 .landing_color
292 .unwrap_or(self.steps.last().unwrap().color);
293 }
294
295 let step = &self.steps[position.step_index];
296
297 match step.transition {
298 TransitionStyle::Step => step.color,
299 TransitionStyle::Linear
300 | TransitionStyle::EaseIn
301 | TransitionStyle::EaseOut
302 | TransitionStyle::EaseInOut
303 | TransitionStyle::EaseOutIn => self.interpolate_color(position, step),
304 }
305 }
306
307 #[inline]
309 fn next_service_time_from_position(&self, position: &StepPosition<D>) -> Option<D> {
310 if position.is_complete {
311 return None;
312 }
313
314 let step = &self.steps[position.step_index];
315 match step.transition {
316 TransitionStyle::Linear
318 | TransitionStyle::EaseIn
319 | TransitionStyle::EaseOut
320 | TransitionStyle::EaseInOut
321 | TransitionStyle::EaseOutIn => Some(D::ZERO),
322 TransitionStyle::Step => Some(position.time_until_step_end),
324 }
325 }
326
327 #[inline]
329 pub fn has_completed(&self, elapsed: D) -> bool {
330 if let Some(timing_fn) = self.timing_fn {
331 timing_fn(elapsed).is_none()
332 } else {
333 self.is_complete_step_based(elapsed)
334 }
335 }
336
337 #[inline]
339 pub fn loop_duration(&self) -> D {
340 self.loop_duration
341 }
342
343 #[inline]
345 pub fn step_count(&self) -> usize {
346 self.steps.len()
347 }
348
349 #[inline]
351 pub fn loop_count(&self) -> LoopCount {
352 self.loop_count
353 }
354
355 #[inline]
357 pub fn landing_color(&self) -> Option<Srgb> {
358 self.landing_color
359 }
360
361 #[inline]
363 pub fn start_color(&self) -> Option<Srgb> {
364 self.start_color
365 }
366
367 #[inline]
369 pub fn get_step(&self, index: usize) -> Option<&SequenceStep<D>> {
370 self.steps.get(index)
371 }
372
373 #[inline]
375 pub fn is_function_based(&self) -> bool {
376 self.color_fn.is_some()
377 }
378}
379
380#[derive(Debug)]
382pub struct SequenceBuilder<D: TimeDuration, const N: usize> {
383 steps: Vec<SequenceStep<D>, N>,
384 loop_count: LoopCount,
385 landing_color: Option<Srgb>,
386 start_color: Option<Srgb>,
387}
388
389impl<D: TimeDuration, const N: usize> SequenceBuilder<D, N> {
390 pub fn new() -> Self {
392 Self {
393 steps: Vec::new(),
394 loop_count: LoopCount::default(),
395 landing_color: None,
396 start_color: None,
397 }
398 }
399
400 pub fn step(
404 mut self,
405 color: Srgb,
406 duration: D,
407 transition: TransitionStyle,
408 ) -> Result<Self, SequenceError> {
409 self.steps
410 .push(SequenceStep::new(color, duration, transition))
411 .map_err(|_| SequenceError::CapacityExceeded)?;
412 Ok(self)
413 }
414
415 pub fn loop_count(mut self, count: LoopCount) -> Self {
417 self.loop_count = count;
418 self
419 }
420
421 pub fn landing_color(mut self, color: Srgb) -> Self {
423 self.landing_color = Some(color);
424 self
425 }
426
427 pub fn start_color(mut self, color: Srgb) -> Self {
429 self.start_color = Some(color);
430 self
431 }
432
433 pub fn build(self) -> Result<RgbSequence<D, N>, SequenceError> {
441 if self.steps.is_empty() {
442 return Err(SequenceError::EmptySequence);
443 }
444
445 for step in &self.steps {
446 if step.duration.as_millis() == 0
447 && matches!(
448 step.transition,
449 TransitionStyle::Linear
450 | TransitionStyle::EaseIn
451 | TransitionStyle::EaseOut
452 | TransitionStyle::EaseInOut
453 | TransitionStyle::EaseOutIn
454 )
455 {
456 return Err(SequenceError::ZeroDurationWithInterpolation);
457 }
458 }
459
460 if self.start_color.is_some()
462 && let Some(first_step) = self.steps.first()
463 && matches!(first_step.transition, TransitionStyle::Step)
464 {
465 return Err(SequenceError::StartColorWithStepTransition);
466 }
467
468 if self.landing_color.is_some() && matches!(self.loop_count, LoopCount::Infinite) {
470 return Err(SequenceError::LandingColorWithInfiniteLoop);
471 }
472
473 let total_millis: u64 = self.steps.iter().map(|s| s.duration.as_millis()).sum();
475 let loop_duration = D::from_millis(total_millis);
476
477 Ok(RgbSequence {
478 steps: self.steps,
479 loop_count: self.loop_count,
480 landing_color: self.landing_color,
481 loop_duration,
482 start_color: self.start_color,
483 color_fn: None,
484 timing_fn: None,
485 })
486 }
487}
488
489impl<D: TimeDuration, const N: usize> Default for SequenceBuilder<D, N> {
490 fn default() -> Self {
492 Self::new()
493 }
494}