1#![forbid(unsafe_code)]
2
3use std::rc::Rc;
4use std::time::{Duration, Instant};
5
6use sequoia_animation::Easing;
7use sequoia_core::{Color, Vec2};
8use sequoia_reactive::{Memo, Signal};
9
10use crate::clock::MAX_FRAME_STEP_SECS;
11
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
13pub struct Prop(pub u64);
14
15impl Prop {
16 pub const OPACITY: Self = Self(1);
17 pub const OFFSET: Self = Self(2);
18 pub const OFFSET_X: Self = Self(3);
19 pub const OFFSET_Y: Self = Self(4);
20 pub const BLUR: Self = Self(5);
21 pub const BACKGROUND: Self = Self(6);
22 pub const SHADOW: Self = Self(7);
23 pub const COLOR: Self = Self(8);
24 pub const RADIUS: Self = Self(9);
25
26 pub const SCALE: Self = Self(10);
27
28 pub const ROTATION: Self = Self(11);
29
30 pub const WIDTH: Self = Self(12);
31
32 pub const HEIGHT: Self = Self(13);
33
34 pub const fn custom(id: u64) -> Self {
35 Self(id)
36 }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct AnimSpec {
41 pub duration_secs: f32,
42 pub delay_secs: f32,
43 pub easing: Easing,
44}
45
46impl AnimSpec {
47 pub const FAST: Self = Self {
48 duration_secs: 0.12,
49 delay_secs: 0.0,
50 easing: Easing::Linear,
51 };
52
53 pub const SLOW: Self = Self {
54 duration_secs: 0.25,
55 delay_secs: 0.0,
56 easing: Easing::EaseOutCubic,
57 };
58
59 pub const SMOOTH: Self = Self {
60 duration_secs: 0.20,
61 delay_secs: 0.0,
62 easing: Easing::EaseOutCubic,
63 };
64
65 pub const SNAPPY: Self = Self {
66 duration_secs: 0.12,
67 delay_secs: 0.0,
68 easing: Easing::EaseOutCubic,
69 };
70
71 pub const GENTLE: Self = Self {
72 duration_secs: 0.35,
73 delay_secs: 0.0,
74 easing: Easing::EaseInOutCubic,
75 };
76
77 pub fn fast() -> Self {
78 Self::FAST
79 }
80 pub fn smooth() -> Self {
81 Self::SMOOTH
82 }
83 pub fn snappy() -> Self {
84 Self::SNAPPY
85 }
86 pub fn gentle() -> Self {
87 Self::GENTLE
88 }
89 pub fn slow() -> Self {
90 Self::SLOW
91 }
92
93 pub fn linear(duration: Duration) -> Self {
94 Self {
95 duration_secs: duration.as_secs_f32().max(0.0),
96 delay_secs: 0.0,
97 easing: Easing::Linear,
98 }
99 }
100
101 pub fn ease_out_cubic(duration: Duration) -> Self {
102 Self {
103 duration_secs: duration.as_secs_f32().max(0.0),
104 delay_secs: 0.0,
105 easing: Easing::EaseOutCubic,
106 }
107 }
108
109 pub fn ease_out(duration: Duration) -> Self {
110 Self::ease_out_cubic(duration)
111 }
112
113 pub fn ease_in_out(duration: Duration) -> Self {
114 Self {
115 duration_secs: duration.as_secs_f32().max(0.0),
116 delay_secs: 0.0,
117 easing: Easing::EaseInOutCubic,
118 }
119 }
120
121 pub fn from_duration(duration: Duration, easing: Easing) -> Self {
122 Self {
123 duration_secs: duration.as_secs_f32().max(0.0),
124 delay_secs: 0.0,
125 easing,
126 }
127 }
128
129 pub fn delay_duration(mut self, d: Duration) -> Self {
130 self.delay_secs = d.as_secs_f32().max(0.0);
131 self
132 }
133
134 pub fn sample(&self, from: f32, to: f32, elapsed_secs: f32) -> f32 {
135 let t = elapsed_secs.max(0.0);
136 if t < self.delay_secs {
137 return from;
138 }
139 if self.duration_secs <= 0.0 {
140 return to;
141 }
142 let k = ((t - self.delay_secs) / self.duration_secs).clamp(0.0, 1.0);
143 from + (to - from) * self.easing.apply(k)
144 }
145
146 pub fn is_running(&self, elapsed_secs: f32) -> bool {
147 elapsed_secs < self.delay_secs + self.duration_secs
148 }
149}
150
151impl From<Duration> for AnimSpec {
152 fn from(d: Duration) -> Self {
153 Self {
154 duration_secs: d.as_secs_f32().max(0.0),
155 delay_secs: 0.0,
156 easing: Easing::EaseOutCubic,
157 }
158 }
159}
160
161#[derive(Clone, Copy, Debug, PartialEq)]
162pub struct SpringSpec {
163 pub stiffness: f32,
164 pub damping: f32,
165 pub mass: f32,
166}
167
168impl SpringSpec {
169 pub const DEFAULT: Self = Self {
170 stiffness: 180.0,
171 damping: 20.0,
172 mass: 1.0,
173 };
174
175 pub const SNAPPY: Self = Self {
176 stiffness: 320.0,
177 damping: 22.0,
178 mass: 1.0,
179 };
180
181 pub const BOUNCY: Self = Self {
182 stiffness: 170.0,
183 damping: 11.0,
184 mass: 1.0,
185 };
186
187 pub const GENTLE: Self = Self {
188 stiffness: 90.0,
189 damping: 19.0,
190 mass: 1.0,
191 };
192
193 pub fn snappy() -> Self {
194 Self::SNAPPY
195 }
196 pub fn bouncy() -> Self {
197 Self::BOUNCY
198 }
199 pub fn gentle() -> Self {
200 Self::GENTLE
201 }
202
203 pub fn stiffness(mut self, k: f32) -> Self {
204 self.stiffness = k.max(0.0);
205 self
206 }
207
208 pub fn damping(mut self, c: f32) -> Self {
209 self.damping = c.max(0.0);
210 self
211 }
212
213 pub fn mass(mut self, m: f32) -> Self {
214 self.mass = m.max(0.0001);
215 self
216 }
217}
218
219impl Default for SpringSpec {
220 fn default() -> Self {
221 Self::DEFAULT
222 }
223}
224
225enum Source<T> {
226 Fixed(T),
227 Signal(Signal<T>),
228 Memo(Memo<T>),
229 Derived(Rc<dyn Fn() -> T>),
230}
231
232impl<T: Clone> Clone for Source<T> {
233 fn clone(&self) -> Self {
234 match self {
235 Source::Fixed(v) => Source::Fixed(v.clone()),
236 Source::Signal(s) => Source::Signal(s.clone()),
237 Source::Memo(m) => Source::Memo(m.clone()),
238 Source::Derived(f) => Source::Derived(Rc::clone(f)),
239 }
240 }
241}
242
243pub struct Live<T> {
244 source: Source<T>,
245 tween: Option<AnimSpec>,
246 spring: Option<SpringSpec>,
247}
248
249impl<T: Clone> Clone for Live<T> {
250 fn clone(&self) -> Self {
251 Self {
252 source: self.source.clone(),
253 tween: self.tween,
254 spring: self.spring,
255 }
256 }
257}
258
259impl<T: Clone + PartialEq + 'static> Live<T> {
260 pub fn fixed(value: T) -> Self {
261 Self {
262 source: Source::Fixed(value),
263 tween: None,
264 spring: None,
265 }
266 }
267
268 pub fn of_signal(signal: Signal<T>) -> Self {
269 Self {
270 source: Source::Signal(signal),
271 tween: None,
272 spring: None,
273 }
274 }
275
276 pub fn of_memo(memo: Memo<T>) -> Self {
277 Self {
278 source: Source::Memo(memo),
279 tween: None,
280 spring: None,
281 }
282 }
283
284 pub fn derived(f: impl Fn() -> T + 'static) -> Self {
285 Self {
286 source: Source::Derived(Rc::new(f)),
287 tween: None,
288 spring: None,
289 }
290 }
291
292 pub fn animate(mut self, spec: impl Into<AnimSpec>) -> Self {
293 self.tween = Some(spec.into());
294 self.spring = None;
295 self
296 }
297
298 pub fn spring(mut self, spec: SpringSpec) -> Self {
299 self.spring = Some(spec);
300 self.tween = None;
301 self
302 }
303
304 pub fn animate_mut(&mut self, spec: impl Into<AnimSpec>) {
305 self.tween = Some(spec.into());
306 self.spring = None;
307 }
308
309 pub fn spring_mut(&mut self, spec: SpringSpec) {
310 self.spring = Some(spec);
311 self.tween = None;
312 }
313
314 pub fn tween_spec(&self) -> Option<AnimSpec> {
315 self.tween
316 }
317
318 pub fn spring_spec(&self) -> Option<SpringSpec> {
319 self.spring
320 }
321
322 pub fn is_animated(&self) -> bool {
323 self.tween.is_some() || self.spring.is_some()
324 }
325
326 pub fn target(&self) -> T {
327 match &self.source {
328 Source::Fixed(v) => v.clone(),
329 Source::Signal(s) => s.get(),
330 Source::Memo(m) => m.get(),
331 Source::Derived(f) => f(),
332 }
333 }
334
335 pub fn target_untracked(&self) -> T
336 where
337 T: Clone,
338 {
339 match &self.source {
340 Source::Fixed(v) => v.clone(),
341 Source::Signal(s) => s.get_untracked(),
342 Source::Memo(m) => m.get_untracked(),
343 Source::Derived(f) => sequoia_reactive::untracked(|| f()),
344 }
345 }
346}
347
348impl<T: Clone + PartialEq + 'static> From<T> for Live<T> {
349 fn from(value: T) -> Self {
350 Self::fixed(value)
351 }
352}
353
354impl<T: Clone + PartialEq + 'static> From<Signal<T>> for Live<T> {
355 fn from(signal: Signal<T>) -> Self {
356 Self::of_signal(signal)
357 }
358}
359
360impl<T: Clone + PartialEq + 'static> From<Memo<T>> for Live<T> {
361 fn from(memo: Memo<T>) -> Self {
362 Self::of_memo(memo)
363 }
364}
365
366impl From<f64> for Live<f32> {
367 fn from(v: f64) -> Self {
368 Self::fixed(v as f32)
369 }
370}
371
372pub trait SignalLiveExt<T: Clone + PartialEq + 'static> {
373 fn live(self) -> Live<T>;
374 fn map_live<U: Clone + PartialEq + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Live<U>;
375 fn animate(self, spec: impl Into<AnimSpec>) -> Live<T>;
376 fn spring(self, spec: SpringSpec) -> Live<T>;
377}
378
379impl<T: Clone + PartialEq + 'static> SignalLiveExt<T> for Signal<T> {
380 fn live(self) -> Live<T> {
381 Live::of_signal(self)
382 }
383
384 fn map_live<U: Clone + PartialEq + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Live<U> {
385 let s = self.clone();
386 Live::derived(move || f(&s.get()))
387 }
388
389 fn animate(self, spec: impl Into<AnimSpec>) -> Live<T> {
390 Live::of_signal(self).animate(spec)
391 }
392
393 fn spring(self, spec: SpringSpec) -> Live<T> {
394 Live::of_signal(self).spring(spec)
395 }
396}
397
398pub trait MemoLiveExt<T: Clone + PartialEq + 'static> {
399 fn live(self) -> Live<T>;
400 fn map_live<U: Clone + PartialEq + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Live<U>;
401 fn animate(self, spec: impl Into<AnimSpec>) -> Live<T>;
402 fn spring(self, spec: SpringSpec) -> Live<T>;
403}
404
405impl<T: Clone + PartialEq + 'static> MemoLiveExt<T> for Memo<T> {
406 fn live(self) -> Live<T> {
407 Live::of_memo(self)
408 }
409
410 fn map_live<U: Clone + PartialEq + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Live<U> {
411 let m = self.clone();
412 Live::derived(move || f(&m.get()))
413 }
414
415 fn animate(self, spec: impl Into<AnimSpec>) -> Live<T> {
416 Live::of_memo(self).animate(spec)
417 }
418
419 fn spring(self, spec: SpringSpec) -> Live<T> {
420 Live::of_memo(self).spring(spec)
421 }
422}
423
424fn lerp_f32(a: f32, b: f32, t: f32) -> f32 {
425 a + (b - a) * t
426}
427
428fn lerp_color(a: Color, b: Color, t: f32) -> Color {
429 Color::rgba(
430 lerp_f32(a.r, b.r, t),
431 lerp_f32(a.g, b.g, t),
432 lerp_f32(a.b, b.b, t),
433 lerp_f32(a.a, b.a, t),
434 )
435}
436
437fn lerp_vec2(a: Vec2, b: Vec2, t: f32) -> Vec2 {
438 Vec2::new(lerp_f32(a.x, b.x, t), lerp_f32(a.y, b.y, t))
439}
440
441#[derive(Clone, Debug)]
442pub(crate) enum AnimState {
443 F32(F32State),
444 Color(ColorState),
445 Vec2(Vec2State),
446}
447
448#[derive(Clone, Debug)]
449pub(crate) struct F32State {
450 pub current: f32,
451 pub from: f32,
452 pub start: Instant,
453 pub last_target: f32,
454 pub tween: Option<AnimSpec>,
455 pub spring: Option<SpringSpec>,
456 pub velocity: f32,
457 pub last_tick: Instant,
458 pub initialized: bool,
459}
460
461#[derive(Clone, Debug)]
462pub(crate) struct ColorState {
463 pub current: Color,
464 pub from: Color,
465 pub start: Instant,
466 pub last_target: Color,
467 pub tween: Option<AnimSpec>,
468
469 pub smooth_rate: Option<f32>,
470 pub last_tick: Instant,
471 pub initialized: bool,
472}
473
474pub fn exp_blend(current: Color, target: Color, dt_secs: f32, rate: f32) -> Color {
475 let t = 1.0 - (-rate.max(0.0) * dt_secs.max(0.0)).exp();
476 Color::rgba(
477 current.r + (target.r - current.r) * t,
478 current.g + (target.g - current.g) * t,
479 current.b + (target.b - current.b) * t,
480 current.a + (target.a - current.a) * t,
481 )
482}
483
484pub const EXP_SETTLE_EPS: f32 = 0.004;
485
486pub fn exp_settled(current: Color, target: Color) -> bool {
487 (current.r - target.r).abs() < EXP_SETTLE_EPS
488 && (current.g - target.g).abs() < EXP_SETTLE_EPS
489 && (current.b - target.b).abs() < EXP_SETTLE_EPS
490 && (current.a - target.a).abs() < EXP_SETTLE_EPS
491}
492
493#[derive(Clone, Debug)]
494pub(crate) struct Vec2State {
495 pub current: Vec2,
496 pub from: Vec2,
497 pub start: Instant,
498 pub last_target: Vec2,
499 pub tween: Option<AnimSpec>,
500 pub spring: Option<SpringSpec>,
501 pub velocity: Vec2,
502 pub last_tick: Instant,
503 pub initialized: bool,
504}
505
506fn spring_step(current: f32, velocity: f32, target: f32, spec: &SpringSpec, dt: f32) -> (f32, f32) {
507 let m = spec.mass.max(0.0001);
508 let k = spec.stiffness.max(0.0);
509 let c = spec.damping.max(0.0);
510
511 let force = -k * (current - target) - c * velocity;
512 let vel = velocity + force / m * dt;
513 let pos = current + vel * dt;
514 (pos, vel)
515}
516
517impl AnimState {
518 pub(crate) fn sample_f32(
519 state: &mut F32State,
520 target: f32,
521 tween: Option<&AnimSpec>,
522 spring: Option<&SpringSpec>,
523 now: Instant,
524 ) -> (f32, bool) {
525 if !state.initialized {
526 state.current = target;
527 state.from = target;
528 state.last_target = target;
529 state.tween = tween.copied();
530 state.spring = spring.copied();
531 state.start = now;
532 state.last_tick = now;
533 state.velocity = 0.0;
534 state.initialized = true;
535 return (target, false);
536 }
537 let spec_changed = state.tween != tween.copied() || state.spring != spring.copied();
538 if target != state.last_target || spec_changed {
539 state.from = state.current;
540 state.last_target = target;
541 state.tween = tween.copied();
542 state.spring = spring.copied();
543 state.start = now;
544 state.last_tick = now;
545 if spring.is_some() {
546 } else {
547 state.velocity = 0.0;
548 }
549 }
550 if let Some(sp) = spring {
551 let dt = now
552 .duration_since(state.last_tick)
553 .as_secs_f32()
554 .clamp(0.0, MAX_FRAME_STEP_SECS);
555 state.last_tick = now;
556
557 let steps = ((dt * 120.0).ceil() as usize).max(1);
558 let h = dt / steps as f32;
559 for _ in 0..steps {
560 let (p, v) = spring_step(state.current, state.velocity, target, sp, h.max(0.0));
561 state.current = p;
562 state.velocity = v;
563 }
564 let settled = (state.current - target).abs() < 0.0005 && state.velocity.abs() < 0.0005;
565 if settled {
566 state.current = target;
567 state.velocity = 0.0;
568 return (target, false);
569 }
570 return (state.current, true);
571 }
572 if let Some(spec) = tween {
573 let elapsed = now.duration_since(state.start).as_secs_f32();
574 let v = spec.sample(state.from, target, elapsed);
575 state.current = v;
576 return (v, spec.is_running(elapsed));
577 }
578 state.current = target;
579 (target, false)
580 }
581
582 pub(crate) fn sample_color(
583 state: &mut ColorState,
584 target: Color,
585 tween: Option<&AnimSpec>,
586 smooth_rate: Option<f32>,
587 now: Instant,
588 ) -> (Color, bool) {
589 if !state.initialized {
590 state.current = target;
591 state.from = target;
592 state.last_target = target;
593 state.tween = tween.copied();
594 state.smooth_rate = smooth_rate;
595 state.start = now;
596 state.last_tick = now;
597 state.initialized = true;
598 return (target, false);
599 }
600 if target != state.last_target
601 || state.tween != tween.copied()
602 || state.smooth_rate != smooth_rate
603 {
604 state.from = state.current;
605 state.last_target = target;
606 state.tween = tween.copied();
607 state.smooth_rate = smooth_rate;
608 state.start = now;
609 state.last_tick = now;
610 }
611 if let Some(rate) = smooth_rate {
612 let dt = now
613 .duration_since(state.last_tick)
614 .as_secs_f32()
615 .clamp(0.0, MAX_FRAME_STEP_SECS);
616 state.last_tick = now;
617 if exp_settled(state.current, target) {
618 state.current = target;
619 return (target, false);
620 }
621 let next = exp_blend(state.current, target, dt, rate);
622 state.current = next;
623 if exp_settled(next, target) {
624 state.current = target;
625 return (target, false);
626 }
627 return (next, true);
628 }
629 if let Some(spec) = tween {
630 let elapsed = now.duration_since(state.start).as_secs_f32();
631 let k = if spec.duration_secs <= 0.0 || elapsed < spec.delay_secs {
632 if elapsed < spec.delay_secs { 0.0 } else { 1.0 }
633 } else {
634 spec.easing
635 .apply(((elapsed - spec.delay_secs) / spec.duration_secs).clamp(0.0, 1.0))
636 };
637 let v = lerp_color(state.from, target, k);
638 state.current = v;
639 return (v, spec.is_running(elapsed));
640 }
641 state.current = target;
642 (target, false)
643 }
644
645 pub(crate) fn sample_vec2(
646 state: &mut Vec2State,
647 target: Vec2,
648 tween: Option<&AnimSpec>,
649 spring: Option<&SpringSpec>,
650 now: Instant,
651 ) -> (Vec2, bool) {
652 if !state.initialized {
653 state.current = target;
654 state.from = target;
655 state.last_target = target;
656 state.tween = tween.copied();
657 state.spring = spring.copied();
658 state.start = now;
659 state.last_tick = now;
660 state.velocity = Vec2::ZERO;
661 state.initialized = true;
662 return (target, false);
663 }
664 let spec_changed = state.tween != tween.copied() || state.spring != spring.copied();
665 if target != state.last_target || spec_changed {
666 state.from = state.current;
667 state.last_target = target;
668 state.tween = tween.copied();
669 state.spring = spring.copied();
670 state.start = now;
671 state.last_tick = now;
672 }
673 if let Some(sp) = spring {
674 let dt = now
675 .duration_since(state.last_tick)
676 .as_secs_f32()
677 .clamp(0.0, MAX_FRAME_STEP_SECS);
678 state.last_tick = now;
679 let steps = ((dt * 120.0).ceil() as usize).max(1);
680 let h = dt / steps as f32;
681 for _ in 0..steps {
682 let (px, vx) =
683 spring_step(state.current.x, state.velocity.x, target.x, sp, h.max(0.0));
684 let (py, vy) =
685 spring_step(state.current.y, state.velocity.y, target.y, sp, h.max(0.0));
686 state.current = Vec2::new(px, py);
687 state.velocity = Vec2::new(vx, vy);
688 }
689 let settled = (state.current.x - target.x).abs() < 0.0005
690 && (state.current.y - target.y).abs() < 0.0005
691 && state.velocity.x.abs() < 0.0005
692 && state.velocity.y.abs() < 0.0005;
693 if settled {
694 state.current = target;
695 state.velocity = Vec2::ZERO;
696 return (target, false);
697 }
698 return (state.current, true);
699 }
700 if let Some(spec) = tween {
701 let elapsed = now.duration_since(state.start).as_secs_f32();
702 let k = if elapsed < spec.delay_secs {
703 0.0
704 } else if spec.duration_secs <= 0.0 {
705 1.0
706 } else {
707 spec.easing
708 .apply(((elapsed - spec.delay_secs) / spec.duration_secs).clamp(0.0, 1.0))
709 };
710 let v = lerp_vec2(state.from, target, k);
711 state.current = v;
712 return (v, spec.is_running(elapsed));
713 }
714 state.current = target;
715 (target, false)
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 #[test]
724 fn live_fixed_and_signal_targets() {
725 let a = Live::fixed(3.0f32);
726 assert_eq!(a.target_untracked(), 3.0);
727 assert!(!a.is_animated());
728 let s = Signal::new(2.0f32);
729 let b = s.clone().live().animate(AnimSpec::FAST);
730 assert!(b.is_animated());
731 assert_eq!(b.target_untracked(), 2.0);
732 s.set(5.0);
733 assert_eq!(b.target_untracked(), 5.0);
734 }
735
736 #[test]
737 fn signal_map_live_derives() {
738 let hovered = Signal::new(false);
739 let bg = hovered.map_live(|v| if *v { 20.0f32 } else { 8.0f32 });
740 assert_eq!(bg.target_untracked(), 8.0);
741 hovered.set(true);
742 assert_eq!(bg.target_untracked(), 20.0);
743 }
744
745 #[test]
746 fn tween_state_retargets_from_current() {
747 let now = Instant::now();
748 let spec = AnimSpec::FAST;
749 let mut st = F32State {
750 current: 0.0,
751 from: 0.0,
752 start: now,
753 last_target: 0.0,
754 tween: None,
755 spring: None,
756 velocity: 0.0,
757 last_tick: now,
758 initialized: false,
759 };
760
761 let (v0, r0) = AnimState::sample_f32(&mut st, 0.0, Some(&spec), None, now);
762 assert_eq!(v0, 0.0);
763 assert!(!r0, "first sample anchors without gear");
764
765 let (v1a, r1a) = AnimState::sample_f32(&mut st, 1.0, Some(&spec), None, now);
766 assert_eq!(v1a, 0.0, "retarget nie skacze w tej samej klatce");
767 assert!(r1a);
768 let later = now + Duration::from_millis(60);
769 let (v1, r1) = AnimState::sample_f32(&mut st, 1.0, Some(&spec), None, later);
770 assert!((v1 - 0.5).abs() < 0.05, "half way: {v1}");
771 assert!(r1);
772
773 let (v2, _) = AnimState::sample_f32(&mut st, 0.0, Some(&spec), None, later);
774 assert!((v2 - v1).abs() < 1e-5, "retarget nie skacze: {v2} vs {v1}");
775 }
776
777 #[test]
778 fn exp_color_matches_button_blend_math() {
779 use sequoia_core::Color;
780
781 let step = exp_blend(Color::BLACK, Color::WHITE, 1.0 / 60.0, 12.0);
782 assert!(step.r > 0.1 && step.r < 1.0, "pierwsza klatka: {}", step.r);
783 assert!(!exp_settled(Color::BLACK, Color::WHITE));
784 assert!(exp_settled(Color::WHITE, Color::WHITE));
785
786 let now = Instant::now();
787 let mut st = ColorState {
788 current: Color::BLACK,
789 from: Color::BLACK,
790 start: now,
791 last_target: Color::BLACK,
792 tween: None,
793 smooth_rate: None,
794 last_tick: now,
795 initialized: false,
796 };
797 let (v0, r0) = AnimState::sample_color(&mut st, Color::BLACK, None, Some(12.0), now);
798 assert_eq!(v0, Color::BLACK);
799 assert!(!r0);
800 let mut t = now;
801 let mut running = true;
802 for _ in 0..240 {
803 t += Duration::from_millis(16);
804 let (_, r) = AnimState::sample_color(&mut st, Color::WHITE, None, Some(12.0), t);
805 running = r;
806 if !running {
807 break;
808 }
809 }
810 assert!(!running, "240 klatek musi dobiec");
811 assert_eq!(st.current, Color::WHITE);
812 }
813
814 #[test]
815 fn spring_state_settles() {
816 let now = Instant::now();
817 let spec = SpringSpec::DEFAULT;
818 let mut st = F32State {
819 current: 0.0,
820 from: 0.0,
821 start: now,
822 last_target: 0.0,
823 tween: None,
824 spring: None,
825 velocity: 0.0,
826 last_tick: now,
827 initialized: false,
828 };
829 let mut t = now;
830 let _ = AnimState::sample_f32(&mut st, 100.0, None, Some(&spec), t);
831 for _ in 0..600 {
832 t += Duration::from_millis(16);
833 let _ = AnimState::sample_f32(&mut st, 100.0, None, Some(&spec), t);
834 }
835 assert!(
836 (st.current - 100.0).abs() < 1.0,
837 "spring reaches: {}",
838 st.current
839 );
840 }
841}