1use std::cell::RefCell;
2use std::rc::Rc;
3use std::time::Instant;
4
5use sequoia_core::{Color, Point};
6
7use crate::MAX_FRAME_DT;
8use crate::animation::{Animation, LoopMode};
9use crate::machine::StateMachine;
10use crate::spring_machine::SpringMachine;
11use crate::value::Value;
12
13#[derive(Debug)]
14struct Inner {
15 clip: Option<Animation>,
16 machine: Option<StateMachine>,
17 spring: Option<SpringMachine>,
18 autoplay: bool,
19 playing: bool,
20
21 elapsed: f64,
22
23 last_frame: Option<Instant>,
24
25 offset: Point,
26 width: Option<f32>,
27 height: Option<f32>,
28 background: Option<Color>,
29 opacity: Option<f32>,
30}
31
32#[derive(Clone, Debug)]
33pub struct Animator {
34 inner: Rc<RefCell<Inner>>,
35}
36
37impl Animator {
38 pub fn new() -> Self {
39 Self {
40 inner: Rc::new(RefCell::new(Inner {
41 clip: None,
42 machine: None,
43 spring: None,
44 autoplay: true,
45 playing: true,
46 elapsed: 0.0,
47 last_frame: None,
48 offset: Point::default(),
49 width: None,
50 height: None,
51 background: None,
52 opacity: None,
53 })),
54 }
55 }
56
57 pub fn with_clip(self, clip: Animation) -> Self {
58 self.inner.borrow_mut().clip = Some(clip);
59 self
60 }
61
62 pub fn with_machine(self, machine: StateMachine) -> Self {
63 self.inner.borrow_mut().machine = Some(machine);
64 self
65 }
66
67 pub fn with_spring(self, spring: SpringMachine) -> Self {
68 self.inner.borrow_mut().spring = Some(spring);
69 self
70 }
71
72 pub fn with_autoplay(self, on: bool) -> Self {
73 {
74 let mut inner = self.inner.borrow_mut();
75 inner.autoplay = on;
76 inner.playing = on;
77 }
78 self
79 }
80
81 pub fn set_clip(&mut self, clip: Animation) {
82 let mut inner = self.inner.borrow_mut();
83 inner.clip = Some(clip);
84 inner.elapsed = 0.0;
85 }
86
87 pub fn play(&mut self) {
88 let mut inner = self.inner.borrow_mut();
89 inner.playing = true;
90 inner.elapsed = 0.0;
91 inner.last_frame = Some(Instant::now());
92 }
93
94 pub fn pause(&mut self) {
95 self.inner.borrow_mut().playing = false;
96 }
97
98 pub fn toggle_play(&mut self) {
99 let playing = self.inner.borrow().playing;
100 if playing {
101 self.pause();
102 } else {
103 self.play();
104 }
105 }
106
107 pub fn fire(&mut self, trigger: impl Into<String>) {
108 if let Some(m) = self.inner.borrow_mut().machine.as_mut() {
109 m.fire(trigger);
110 }
111 }
112
113 pub fn set_boolean(&mut self, name: impl Into<String>, value: bool) {
114 if let Some(m) = self.inner.borrow_mut().machine.as_mut() {
115 m.set_boolean(name, value);
116 }
117 }
118
119 pub fn set_number(&mut self, name: impl Into<String>, value: f32) {
120 if let Some(m) = self.inner.borrow_mut().machine.as_mut() {
121 m.set_number(name, value);
122 }
123 }
124
125 pub fn advance(&mut self, dt: f64) -> bool {
126 let dt = dt.max(0.0);
127
128 let clip = self.inner.borrow().clip.clone();
129 let mut inner = self.inner.borrow_mut();
130 inner.offset = Point::default();
131 inner.width = None;
132 inner.height = None;
133 inner.background = None;
134 inner.opacity = None;
135 let mut running = false;
136
137 if let Some(spring) = inner.spring.as_mut() {
138 running |= spring.advance(dt);
139 let p = spring.offset();
140 inner.offset.x += p.x;
141 inner.offset.y += p.y;
142 }
143
144 if let Some(machine) = inner.machine.as_mut() {
145 running |= machine.advance(dt);
146 let frame = machine.frame().clone();
147 apply_frame_additive(&mut inner, &frame);
148 }
149
150 let Some(clip) = clip.as_ref() else {
151 return running;
152 };
153 if inner.playing {
154 inner.elapsed += dt;
155 }
156 let t = inner.elapsed * f64::from(clip.speed());
157 let frame = clip.sample(t);
158 apply_clip_frame(&mut inner, &frame);
159 if !inner.playing {
160 return running;
161 }
162 running
163 || match clip.loop_mode() {
164 LoopMode::Once => !clip.is_finished_at(t),
165
166 LoopMode::Loop | LoopMode::PingPong => true,
167 }
168 }
169
170 pub fn finish(&mut self) -> bool {
175 let end = {
176 let inner = self.inner.borrow();
177 inner.clip.as_ref().map(|clip| {
178 let speed = f64::from(clip.speed());
179 if speed > 0.0 {
180 clip.duration() / speed
181 } else {
182 0.0
183 }
184 })
185 };
186 {
187 let mut inner = self.inner.borrow_mut();
188 if let Some(end) = end {
189 inner.elapsed = end;
190 }
191 inner.playing = false;
192 }
193 let _ = self.advance(0.0);
194 false
195 }
196
197 pub fn advance_frame(&mut self) -> bool {
198 let now = Instant::now();
199 let dt = {
200 let mut inner = self.inner.borrow_mut();
201 let dt = inner
202 .last_frame
203 .map(|last| now.duration_since(last).as_secs_f64())
204 .unwrap_or(0.0)
205 .clamp(0.0, f64::from(MAX_FRAME_DT));
206 inner.last_frame = Some(now);
207 dt
208 };
209 self.advance(dt)
210 }
211
212 pub fn offset(&self) -> Point {
213 self.inner.borrow().offset
214 }
215
216 pub fn width(&self) -> Option<f32> {
217 self.inner.borrow().width
218 }
219
220 pub fn height(&self) -> Option<f32> {
221 self.inner.borrow().height
222 }
223
224 pub fn background(&self) -> Option<Color> {
225 self.inner.borrow().background
226 }
227
228 pub fn opacity(&self) -> Option<f32> {
229 self.inner.borrow().opacity
230 }
231
232 pub fn elapsed(&self) -> f64 {
233 self.inner.borrow().elapsed
234 }
235
236 pub fn is_playing(&self) -> bool {
237 self.inner.borrow().playing
238 }
239
240 pub fn state_name(&self) -> String {
241 self.inner
242 .borrow()
243 .machine
244 .as_ref()
245 .map(|m| m.current_state().to_string())
246 .unwrap_or_default()
247 }
248}
249
250impl Default for Animator {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256fn apply_clip_frame(inner: &mut Inner, frame: &crate::AnimationFrame) {
257 if let Some(v) = frame.get("x").and_then(Value::as_px) {
258 inner.offset.x += v;
259 }
260 if let Some(v) = frame.get("y").and_then(Value::as_px) {
261 inner.offset.y += v;
262 }
263 if let Some(v) = frame.get("width").and_then(Value::as_px) {
264 inner.width = Some(v);
265 }
266 if let Some(v) = frame.get("height").and_then(Value::as_px) {
267 inner.height = Some(v);
268 }
269 if let Some(v) = frame
270 .get("background")
271 .or_else(|| frame.get("color"))
272 .and_then(Value::as_color)
273 {
274 inner.background = Some(v);
275 }
276 if let Some(v) = frame.get("opacity").and_then(Value::as_f32) {
277 inner.opacity = Some(v.clamp(0.0, 1.0));
278 }
279}
280
281fn apply_frame_additive(inner: &mut Inner, frame: &crate::AnimationFrame) {
282 apply_clip_frame(inner, frame);
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::{Keyframe, Track};
289
290 fn slide(dur: f64, to: f32) -> Animation {
291 Animation::new("slide", dur).with_track(
292 "x",
293 Track::try_new(vec![
294 Keyframe::new(0.0, Value::px(0.0)),
295 Keyframe::new(dur, Value::px(to)),
296 ])
297 .unwrap(),
298 )
299 }
300
301 #[test]
302 fn first_frame_starts_at_first_keyframe() {
303 let mut a = Animator::new().with_clip(slide(1.0, 100.0));
304 assert!(a.advance(0.0), "klip Once leci od t = 0");
305 assert_eq!(a.offset(), Point::new(0.0, 0.0));
306 }
307
308 #[test]
309 fn once_clip_travels_then_holds_last_frame() {
310 let mut a = Animator::new().with_clip(slide(0.4, 100.0));
311 assert!(a.advance(0.1));
312 let x1 = a.offset().x;
313 assert!(x1 > 0.0 && x1 < 100.0, "center of flight: {x1}");
314 assert!(a.advance(0.1));
315 let x2 = a.offset().x;
316 assert!(x2 > x1, "leci do przodu ({x1} -> {x2})");
317 assert!(a.advance(0.15));
318 assert!(
319 (a.offset().x - 87.5).abs() < 1e-4,
320 "t=0.35: {}",
321 a.offset().x
322 );
323 assert!(!a.advance(0.1));
324 assert_eq!(a.offset().x, 100.0, "holds the last frame");
325 assert!(!a.advance(0.016), "at the end of Once stands");
326 }
327
328 #[test]
329 fn loop_clip_wraps_and_never_settles() {
330 let clip = Animation::new("pulse", 0.2)
331 .with_loop(LoopMode::Loop)
332 .with_track(
333 "x",
334 Track::try_new(vec![
335 Keyframe::new(0.0, Value::px(0.0)),
336 Keyframe::new(0.2, Value::px(50.0)),
337 ])
338 .unwrap(),
339 );
340 let mut a = Animator::new().with_clip(clip);
341 assert!(a.advance(0.25));
342 assert!((a.offset().x - 12.5).abs() < 1e-4, "wrap: {}", a.offset().x);
343 assert!(a.advance(10.0), "Loop nigdy nie staje");
344 }
345
346 #[test]
347 fn pingpong_comes_back() {
348 let clip = Animation::new("pp", 1.0)
349 .with_loop(LoopMode::PingPong)
350 .with_track(
351 "x",
352 Track::try_new(vec![
353 Keyframe::new(0.0, Value::px(0.0)),
354 Keyframe::new(1.0, Value::px(100.0)),
355 ])
356 .unwrap(),
357 );
358 let mut a = Animator::new().with_clip(clip);
359 a.advance(0.25);
360 assert!((a.offset().x - 25.0).abs() < 1e-4);
361 a.advance(1.25);
362 assert!(
363 (a.offset().x - 50.0).abs() < 1e-4,
364 "ping-pong: {}",
365 a.offset().x
366 );
367 }
368
369 #[test]
370 fn autoplay_false_freezes_until_play() {
371 let mut a = Animator::new()
372 .with_clip(slide(0.4, 100.0))
373 .with_autoplay(false);
374 assert!(!a.is_playing());
375 assert!(!a.advance(0.1), "zapauzowany nie gra");
376 assert_eq!(a.offset().x, 0.0);
377 a.play();
378 assert!(a.is_playing());
379 assert!(a.advance(0.1));
380 assert!(a.offset().x > 0.0, "po play leci");
381 a.pause();
382 let frozen = a.offset().x;
383 assert!(!a.advance(0.1));
384 assert_eq!(a.offset().x, frozen, "pause freezes sample");
385 }
386
387 #[test]
388 fn clones_share_playback_state() {
389 let player = Animator::new().with_clip(slide(1.0, 100.0));
390 let mut view = player.clone();
391 assert!(view.advance(0.25));
392 assert!((player.offset().x - 25.0).abs() < 1e-4, "klon dzieli stan");
393 let mut other = player.clone();
394 other.pause();
395 assert!(!player.is_playing(), "pauza widoczna we wszystkich klonach");
396 }
397
398 #[test]
399 fn width_height_background_opacity_tracks() {
400 let clip = Animation::new("grow", 1.0)
401 .with_track(
402 "width",
403 Track::try_new(vec![Keyframe::new(0.0, Value::px(10.0))]).unwrap(),
404 )
405 .with_track(
406 "background",
407 Track::try_new(vec![Keyframe::new(0.0, Value::color(Color::GREEN))]).unwrap(),
408 )
409 .with_track(
410 "opacity",
411 Track::try_new(vec![
412 Keyframe::new(0.0, Value::f32(1.0)),
413 Keyframe::new(1.0, Value::f32(0.0)),
414 ])
415 .unwrap(),
416 );
417 let mut a = Animator::new().with_clip(clip);
418 a.advance(0.0);
419 assert_eq!(a.width(), Some(10.0));
420 assert_eq!(a.background(), Some(Color::GREEN));
421 assert_eq!(a.opacity(), Some(1.0));
422 a.advance(0.5);
423 assert_eq!(a.opacity(), Some(0.5));
424 }
425
426 #[test]
427 fn opacity_is_clamped() {
428 let clip = Animation::new("o", 1.0).with_track(
429 "opacity",
430 Track::try_new(vec![Keyframe::new(0.0, Value::f32(42.0))]).unwrap(),
431 );
432 let mut a = Animator::new().with_clip(clip);
433 a.advance(0.0);
434 assert_eq!(a.opacity(), Some(1.0));
435 }
436
437 #[test]
438 fn color_alias_track_works() {
439 let clip = Animation::new("c", 1.0).with_track(
440 "color",
441 Track::try_new(vec![Keyframe::new(0.0, Value::color(Color::RED))]).unwrap(),
442 );
443 let mut a = Animator::new().with_clip(clip);
444 a.advance(0.0);
445 assert_eq!(a.background(), Some(Color::RED));
446 }
447
448 #[test]
449 fn spring_offset_adds_to_clip() {
450 use crate::{AnimValue, SpringMachine, StateDef};
451 let spring = SpringMachine::new(
452 vec![
453 StateDef::new("a", AnimValue::static_px(10.0), AnimValue::static_px(0.0)),
454 StateDef::new("b", AnimValue::static_px(10.0), AnimValue::static_px(0.0)),
455 ],
456 vec![],
457 );
458 let mut a = Animator::new()
459 .with_clip(slide(1.0, 100.0))
460 .with_spring(spring);
461 a.advance(0.5);
462 assert!((a.offset().x - 60.0).abs() < 1e-4, "suma: {}", a.offset().x);
463 }
464
465 #[test]
466 fn full_machine_trigger_drives_offset() {
467 use crate::{Condition, FromState, MachineState, ToState, Transition};
468 use std::collections::HashMap;
469 let idle = Animation::new("idle", 1.0).with_track(
470 "x",
471 Track::try_new(vec![
472 Keyframe::new(0.0, Value::px(0.0)),
473 Keyframe::new(1.0, Value::px(0.0)),
474 ])
475 .unwrap(),
476 );
477 let wave = Animation::new("wave", 1.0).with_track(
478 "x",
479 Track::try_new(vec![
480 Keyframe::new(0.0, Value::px(0.0)),
481 Keyframe::new(1.0, Value::px(100.0)),
482 ])
483 .unwrap(),
484 );
485 let sm = StateMachine::try_new(
486 vec![
487 MachineState::entry(),
488 MachineState::animation("idle", "idle"),
489 MachineState::animation("wave", "wave"),
490 ],
491 vec![
492 Transition::new(FromState::Entry, ToState::State("idle".into()), vec![]),
493 Transition::new(
494 FromState::State("idle".into()),
495 ToState::State("wave".into()),
496 vec![Condition::Trigger("go".into())],
497 )
498 .with_mix(0.5),
499 ],
500 HashMap::from([("idle".into(), idle), ("wave".into(), wave)]),
501 )
502 .unwrap();
503 let mut a = Animator::new().with_machine(sm);
504 a.advance(0.0);
505 assert_eq!(a.state_name(), "idle");
506 a.fire("go");
507 a.advance(0.0);
508 assert!(a.advance(0.25));
509 let x = a.offset().x;
510 assert!(x > 0.0 && x < 100.0, "blend w locie: {x}");
511 }
512
513 #[test]
514 fn empty_animator_is_idle() {
515 let mut a = Animator::new();
516 assert!(!a.advance(0.016));
517 assert_eq!(a.offset(), Point::ZERO);
518 assert_eq!(a.width(), None);
519 assert_eq!(a.opacity(), None);
520 }
521
522 #[test]
523 fn negative_dt_is_clamped() {
524 let mut a = Animator::new().with_clip(slide(1.0, 100.0));
525 assert!(a.advance(-5.0));
526 assert_eq!(a.offset().x, 0.0);
527 }
528
529 #[test]
530 fn set_clip_restarts_time() {
531 let mut a = Animator::new().with_clip(slide(1.0, 100.0));
532 a.advance(0.75);
533 assert!((a.offset().x - 75.0).abs() < 1e-4);
534 a.set_clip(slide(1.0, 200.0));
535 assert_eq!(a.elapsed(), 0.0);
536 a.advance(0.5);
537 assert!((a.offset().x - 100.0).abs() < 1e-4);
538 }
539
540 #[test]
541 fn advance_frame_uses_wall_clock_without_teleport() {
542 let mut a = Animator::new().with_clip(slide(60.0, 100.0));
543 assert!(a.advance_frame());
544 assert!(a.offset().x < 1.0, "the first frame barely moved");
545 }
546}