1use crate::{MotionPx, MotionRect, MotionSpec, motion_point, motion_rect, motion_size};
4use std::{
5 collections::HashMap,
6 hash::Hash,
7 time::{Duration, Instant},
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MotionRunState {
13 Immediate,
15 Active,
17 Completed,
19 Cancelled,
21}
22
23impl MotionRunState {
24 pub const fn is_active(self) -> bool {
26 matches!(self, Self::Active)
27 }
28
29 pub const fn is_terminal(self) -> bool {
31 !self.is_active()
32 }
33
34 pub const fn reached_final_state(self) -> bool {
36 matches!(self, Self::Immediate | Self::Completed)
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct MotionTimelineSample {
43 state: MotionRunState,
44 elapsed: Duration,
45 raw_progress: f32,
46 progress: f32,
47}
48
49impl MotionTimelineSample {
50 pub const fn new(
52 state: MotionRunState,
53 elapsed: Duration,
54 raw_progress: f32,
55 progress: f32,
56 ) -> Self {
57 Self {
58 state,
59 elapsed,
60 raw_progress,
61 progress,
62 }
63 }
64
65 pub const fn state(self) -> MotionRunState {
67 self.state
68 }
69
70 pub const fn elapsed(self) -> Duration {
72 self.elapsed
73 }
74
75 pub const fn raw_progress(self) -> f32 {
77 self.raw_progress
78 }
79
80 pub const fn progress(self) -> f32 {
82 self.progress
83 }
84
85 pub const fn is_active(self) -> bool {
87 self.state.is_active()
88 }
89
90 pub const fn reached_final_state(self) -> bool {
92 self.state.reached_final_state()
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct MotionTimeline {
99 spec: MotionSpec,
100 started_at: Instant,
101 cancelled_at: Option<Instant>,
102}
103
104impl MotionTimeline {
105 pub const fn new(spec: MotionSpec, started_at: Instant) -> Self {
107 Self {
108 spec,
109 started_at,
110 cancelled_at: None,
111 }
112 }
113
114 pub const fn spec(self) -> MotionSpec {
116 self.spec
117 }
118
119 pub const fn started_at(self) -> Instant {
121 self.started_at
122 }
123
124 pub const fn cancelled_at(self) -> Option<Instant> {
126 self.cancelled_at
127 }
128
129 pub fn cancel_at(&mut self, cancelled_at: Instant) {
131 self.cancelled_at = Some(cancelled_at);
132 }
133
134 pub fn sample(self, now: Instant) -> MotionTimelineSample {
136 let effective_now = self.cancelled_at.unwrap_or(now);
137 let elapsed = effective_now.saturating_duration_since(self.started_at);
138 let mut sample = Self::sample_elapsed(self.spec, elapsed);
139 if self.cancelled_at.is_some() && !sample.reached_final_state() {
140 sample.state = MotionRunState::Cancelled;
141 }
142 sample
143 }
144
145 pub fn sample_elapsed(spec: MotionSpec, elapsed: Duration) -> MotionTimelineSample {
147 if spec.is_immediate() {
148 return MotionTimelineSample::new(MotionRunState::Immediate, elapsed, 1.0, 1.0);
149 }
150
151 let duration = spec.duration().as_duration();
152 if duration.is_zero() {
153 return MotionTimelineSample::new(MotionRunState::Immediate, elapsed, 1.0, 1.0);
154 }
155
156 let raw_progress = (elapsed.as_secs_f32() / duration.as_secs_f32()).clamp(0.0, 1.0);
157 let progress = spec.easing().sample(raw_progress);
158 let state = if raw_progress >= 1.0 {
159 MotionRunState::Completed
160 } else {
161 MotionRunState::Active
162 };
163 MotionTimelineSample::new(state, elapsed, raw_progress, progress)
164 }
165}
166
167#[derive(Debug, Clone, PartialEq)]
169pub struct MotionSnapshot<K, V> {
170 id: K,
171 value: V,
172}
173
174impl<K, V> MotionSnapshot<K, V> {
175 pub const fn new(id: K, value: V) -> Self {
177 Self { id, value }
178 }
179
180 pub const fn id(&self) -> &K {
182 &self.id
183 }
184
185 pub const fn value(&self) -> &V {
187 &self.value
188 }
189
190 pub fn into_parts(self) -> (K, V) {
192 (self.id, self.value)
193 }
194}
195
196#[derive(Debug, Clone, PartialEq)]
198pub struct MotionRetargetItem<K, S, T = S> {
199 id: K,
200 sampled: Option<S>,
201 target: T,
202}
203
204impl<K, S, T> MotionRetargetItem<K, S, T> {
205 pub const fn new(id: K, sampled: Option<S>, target: T) -> Self {
207 Self {
208 id,
209 sampled,
210 target,
211 }
212 }
213
214 pub const fn id(&self) -> &K {
216 &self.id
217 }
218
219 pub const fn sampled(&self) -> Option<&S> {
221 self.sampled.as_ref()
222 }
223
224 pub const fn target(&self) -> &T {
226 &self.target
227 }
228
229 pub fn into_parts(self) -> (K, Option<S>, T) {
231 (self.id, self.sampled, self.target)
232 }
233}
234
235#[derive(Debug, Clone, PartialEq)]
237pub struct MotionRetargetSet<K, S, T = S> {
238 targets: Vec<MotionRetargetItem<K, S, T>>,
239 leaving: Vec<MotionSnapshot<K, S>>,
240}
241
242impl<K, S, T> MotionRetargetSet<K, S, T> {
243 pub fn new(
245 targets: Vec<MotionRetargetItem<K, S, T>>,
246 leaving: Vec<MotionSnapshot<K, S>>,
247 ) -> Self {
248 Self { targets, leaving }
249 }
250
251 pub fn targets(&self) -> &[MotionRetargetItem<K, S, T>] {
253 &self.targets
254 }
255
256 pub fn leaving(&self) -> &[MotionSnapshot<K, S>] {
258 &self.leaving
259 }
260
261 pub fn into_parts(self) -> (Vec<MotionRetargetItem<K, S, T>>, Vec<MotionSnapshot<K, S>>) {
263 (self.targets, self.leaving)
264 }
265}
266
267pub fn retarget_motion_snapshots<K, S, T>(
272 sampled: impl IntoIterator<Item = MotionSnapshot<K, S>>,
273 targets: impl IntoIterator<Item = MotionSnapshot<K, T>>,
274) -> MotionRetargetSet<K, S, T>
275where
276 K: Clone + Eq + Hash,
277{
278 let mut sampled = sampled.into_iter().map(Some).collect::<Vec<_>>();
279 let mut sampled_indices = HashMap::new();
280 for (index, snapshot) in sampled.iter().enumerate() {
281 if let Some(snapshot) = snapshot {
282 sampled_indices.entry(snapshot.id.clone()).or_insert(index);
283 }
284 }
285
286 let targets = targets
287 .into_iter()
288 .map(|target| {
289 let (id, target_value) = target.into_parts();
290 let sampled_value = sampled_indices
291 .get(&id)
292 .and_then(|index| sampled[*index].take())
293 .map(|snapshot| snapshot.value);
294 MotionRetargetItem::new(id, sampled_value, target_value)
295 })
296 .collect::<Vec<_>>();
297 let leaving = sampled.into_iter().flatten().collect::<Vec<_>>();
298
299 MotionRetargetSet::new(targets, leaving)
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
304pub enum MotionEdge {
305 Left,
307 Right,
309 Top,
311 Bottom,
313}
314
315pub fn preferred_motion_edge(bounds: MotionRect, container: MotionRect) -> MotionEdge {
320 let left = (bounds.origin.x - container.origin.x).as_f32().abs();
321 let right = (rect_right(container) - rect_right(bounds)).abs();
322 let top = (bounds.origin.y - container.origin.y).as_f32().abs();
323 let bottom = (rect_bottom(container) - rect_bottom(bounds)).abs();
324 let touching_epsilon = 0.5_f32;
325
326 if left <= touching_epsilon {
327 return MotionEdge::Left;
328 }
329 if right <= touching_epsilon {
330 return MotionEdge::Right;
331 }
332 if top <= touching_epsilon {
333 return MotionEdge::Top;
334 }
335 if bottom <= touching_epsilon {
336 return MotionEdge::Bottom;
337 }
338
339 [
340 (MotionEdge::Left, left),
341 (MotionEdge::Right, right),
342 (MotionEdge::Top, top),
343 (MotionEdge::Bottom, bottom),
344 ]
345 .into_iter()
346 .min_by(|(_, a), (_, b)| a.total_cmp(b))
347 .map(|(edge, _)| edge)
348 .unwrap_or(MotionEdge::Left)
349}
350
351pub fn motion_source_rect(
353 edge: MotionEdge,
354 final_bounds: MotionRect,
355 container: MotionRect,
356) -> MotionRect {
357 let origin = match edge {
358 MotionEdge::Left => motion_point(
359 container.origin.x - final_bounds.size.width,
360 final_bounds.origin.y,
361 ),
362 MotionEdge::Right => {
363 motion_point(MotionPx::new(rect_right(container)), final_bounds.origin.y)
364 }
365 MotionEdge::Top => motion_point(
366 final_bounds.origin.x,
367 container.origin.y - final_bounds.size.height,
368 ),
369 MotionEdge::Bottom => {
370 motion_point(final_bounds.origin.x, MotionPx::new(rect_bottom(container)))
371 }
372 };
373 motion_rect(origin, final_bounds.size)
374}
375
376pub fn reveal_rect_from_edge(
378 final_bounds: MotionRect,
379 edge: MotionEdge,
380 progress: f32,
381) -> MotionRect {
382 let progress = progress.clamp(0.0, 1.0);
383 match edge {
384 MotionEdge::Left => {
385 let width = final_bounds.size.width * progress;
386 motion_rect(
387 final_bounds.origin,
388 motion_size(width, final_bounds.size.height),
389 )
390 }
391 MotionEdge::Right => {
392 let width = final_bounds.size.width * progress;
393 motion_rect(
394 motion_point(
395 MotionPx::new(rect_right(final_bounds)) - width,
396 final_bounds.origin.y,
397 ),
398 motion_size(width, final_bounds.size.height),
399 )
400 }
401 MotionEdge::Top => {
402 let height = final_bounds.size.height * progress;
403 motion_rect(
404 final_bounds.origin,
405 motion_size(final_bounds.size.width, height),
406 )
407 }
408 MotionEdge::Bottom => {
409 let height = final_bounds.size.height * progress;
410 motion_rect(
411 motion_point(
412 final_bounds.origin.x,
413 MotionPx::new(rect_bottom(final_bounds)) - height,
414 ),
415 motion_size(final_bounds.size.width, height),
416 )
417 }
418 }
419}
420
421pub fn lerp_rect(from: MotionRect, to: MotionRect, progress: f32) -> MotionRect {
423 let progress = progress.clamp(0.0, 1.0);
424 motion_rect(
425 motion_point(
426 lerp_px(from.origin.x, to.origin.x, progress),
427 lerp_px(from.origin.y, to.origin.y, progress),
428 ),
429 motion_size(
430 lerp_px(from.size.width, to.size.width, progress),
431 lerp_px(from.size.height, to.size.height, progress),
432 ),
433 )
434}
435
436fn lerp_px(from: MotionPx, to: MotionPx, progress: f32) -> MotionPx {
437 MotionPx::new(from.as_f32() + (to.as_f32() - from.as_f32()) * progress)
438}
439
440fn rect_right(bounds: MotionRect) -> f32 {
441 bounds.origin.x.as_f32() + bounds.size.width.as_f32()
442}
443
444fn rect_bottom(bounds: MotionRect) -> f32 {
445 bounds.origin.y.as_f32() + bounds.size.height.as_f32()
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use crate::{MotionDuration, MotionEasing, MotionPreference};
452
453 #[test]
454 fn timeline_samples_start_midpoint_and_completion() {
455 let started_at = Instant::now();
456 let spec = MotionSpec::new(
457 MotionPreference::Animated,
458 MotionDuration::Custom(Duration::from_millis(200)),
459 MotionEasing::Linear,
460 );
461 let timeline = MotionTimeline::new(spec, started_at);
462
463 let start = timeline.sample(started_at);
464 assert_eq!(start.state(), MotionRunState::Active);
465 assert_eq!(start.elapsed(), Duration::from_millis(0));
466 assert_eq!(start.raw_progress(), 0.0);
467 assert_eq!(start.progress(), 0.0);
468
469 let midpoint = timeline.sample(started_at + Duration::from_millis(100));
470 assert_eq!(midpoint.state(), MotionRunState::Active);
471 assert_eq!(midpoint.raw_progress(), 0.5);
472 assert_eq!(midpoint.progress(), 0.5);
473
474 let complete = timeline.sample(started_at + Duration::from_millis(250));
475 assert_eq!(complete.state(), MotionRunState::Completed);
476 assert_eq!(complete.raw_progress(), 1.0);
477 assert_eq!(complete.progress(), 1.0);
478 assert!(complete.reached_final_state());
479 }
480
481 #[test]
482 fn reduced_motion_samples_as_immediate() {
483 let sample = MotionTimeline::sample_elapsed(
484 MotionSpec::layout(MotionPreference::Reduced),
485 Duration::from_millis(0),
486 );
487
488 assert_eq!(sample.state(), MotionRunState::Immediate);
489 assert_eq!(sample.raw_progress(), 1.0);
490 assert_eq!(sample.progress(), 1.0);
491 assert!(sample.reached_final_state());
492 }
493
494 #[test]
495 fn cancelled_timeline_reports_cancelled_sampled_progress() {
496 let started_at = Instant::now();
497 let spec = MotionSpec::new(
498 MotionPreference::Animated,
499 MotionDuration::Custom(Duration::from_millis(200)),
500 MotionEasing::Linear,
501 );
502 let mut timeline = MotionTimeline::new(spec, started_at);
503
504 timeline.cancel_at(started_at + Duration::from_millis(80));
505 let sample = timeline.sample(started_at + Duration::from_millis(160));
506
507 assert_eq!(sample.state(), MotionRunState::Cancelled);
508 assert!((sample.raw_progress() - 0.4).abs() < f32::EPSILON);
509 assert!((sample.progress() - 0.4).abs() < f32::EPSILON);
510 assert!(!sample.reached_final_state());
511 }
512
513 #[test]
514 fn retarget_snapshots_match_by_identity_and_report_missing_items() {
515 let retarget = retarget_motion_snapshots(
516 [
517 MotionSnapshot::new("left", 0.25),
518 MotionSnapshot::new("center", 0.5),
519 MotionSnapshot::new("right", 0.25),
520 ],
521 [
522 MotionSnapshot::new("center", 0.7),
523 MotionSnapshot::new("inspector", 0.3),
524 ],
525 );
526
527 assert_eq!(retarget.targets().len(), 2);
528 assert_eq!(retarget.targets()[0].id(), &"center");
529 assert_eq!(retarget.targets()[0].sampled(), Some(&0.5));
530 assert_eq!(retarget.targets()[0].target(), &0.7);
531 assert_eq!(retarget.targets()[1].id(), &"inspector");
532 assert_eq!(retarget.targets()[1].sampled(), None);
533 assert_eq!(retarget.targets()[1].target(), &0.3);
534
535 let leaving_ids = retarget
536 .leaving()
537 .iter()
538 .map(MotionSnapshot::id)
539 .copied()
540 .collect::<Vec<_>>();
541 assert_eq!(leaving_ids, ["left", "right"]);
542 }
543
544 #[test]
545 fn preferred_motion_edge_prefers_touching_edge_before_distance() {
546 let container = motion_rect(
547 motion_point(MotionPx::ZERO, MotionPx::ZERO),
548 motion_size(MotionPx::new(400.0), MotionPx::new(240.0)),
549 );
550 let touching_top_but_closer_left = motion_rect(
551 motion_point(MotionPx::new(20.0), MotionPx::ZERO),
552 motion_size(MotionPx::new(80.0), MotionPx::new(80.0)),
553 );
554
555 assert_eq!(
556 preferred_motion_edge(touching_top_but_closer_left, container),
557 MotionEdge::Top
558 );
559 }
560
561 #[test]
562 fn motion_source_rect_places_rect_outside_container_edge() {
563 let container = motion_rect(
564 motion_point(MotionPx::ZERO, MotionPx::ZERO),
565 motion_size(MotionPx::new(400.0), MotionPx::new(240.0)),
566 );
567 let final_bounds = motion_rect(
568 motion_point(MotionPx::new(40.0), MotionPx::new(20.0)),
569 motion_size(MotionPx::new(80.0), MotionPx::new(60.0)),
570 );
571
572 assert_eq!(
573 motion_source_rect(MotionEdge::Left, final_bounds, container),
574 motion_rect(
575 motion_point(MotionPx::new(-80.0), MotionPx::new(20.0)),
576 final_bounds.size
577 )
578 );
579 assert_eq!(
580 motion_source_rect(MotionEdge::Bottom, final_bounds, container),
581 motion_rect(
582 motion_point(MotionPx::new(40.0), MotionPx::new(240.0)),
583 final_bounds.size
584 )
585 );
586 }
587
588 #[test]
589 fn reveal_and_lerp_rect_clamp_progress() {
590 let rect = motion_rect(
591 motion_point(MotionPx::new(10.0), MotionPx::new(20.0)),
592 motion_size(MotionPx::new(100.0), MotionPx::new(80.0)),
593 );
594
595 assert_eq!(
596 reveal_rect_from_edge(rect, MotionEdge::Right, 0.25),
597 motion_rect(
598 motion_point(MotionPx::new(85.0), MotionPx::new(20.0)),
599 motion_size(MotionPx::new(25.0), MotionPx::new(80.0))
600 )
601 );
602 assert_eq!(lerp_rect(rect, rect, 2.0), rect);
603 }
604}