Skip to main content

day_core/
tween.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! Tweening for self-drawn content (docs/tween.md): the pieces a canvas animation needs that
5//! native widgets get from their toolkit.
6//!
7//! [`with_animation`](crate::with_animation) hands native widgets *intent* and lets each toolkit
8//! animate on its own compositor. A canvas has no compositor to hand anything to: it re-records a
9//! display list, so something has to decide, frame by frame, what the in-between picture is. That
10//! something is built from four parts, each usable on its own:
11//!
12//! * [`Lerp`]: how a value moves between two states. Numbers, points, rects, tuples, arrays,
13//!   vectors, and colors (blended in OKLab, so a heat map's mid-transition cells are the colors in
14//!   between rather than a muddy detour through gray).
15//! * [`Timing`]: an [`AnimSpec`] sampled over elapsed seconds: delay, easing curve or spring,
16//!   repeats and autoreverse, and a definite end, so a driver knows when to stop asking for frames.
17//! * [`animate`]: a driver on the window's [`FrameClock`], calling a step
18//!   closure once per display frame until the timing completes, then releasing the clock.
19//! * [`Tweened`]: a reactive value that glides to each new target, starting from wherever it is when
20//!   retargeted, so an interrupted transition bends instead of jumping.
21//!
22//! [`stagger`] offsets one spec per item, for sequences (bars rising left to right).
23//!
24//! [`Pairs`] is the step before any of them when what changed is a *collection*: it matches the
25//! items of two states by key, gives the ones only one side has somewhere to come from or go to,
26//! and interpolates the lot, so bars that trade places slide and a new one grows out of its
27//! neighbor instead of the whole picture cross-fading.
28
29use std::cell::{Cell, RefCell};
30use std::collections::{HashMap, VecDeque};
31use std::hash::Hash;
32use std::ops::ControlFlow;
33use std::rc::Rc;
34use std::time::Duration;
35
36use day_spec::{AnimSpec, Color, Point, Rect, Size};
37
38use crate::frame::{FrameClock, FrameHandle};
39
40/// A value that can be interpolated: `a.lerp(&b, 0.0)` is `a`, `a.lerp(&b, 1.0)` is `b`.
41///
42/// `t` may leave `0..=1`: an under-damped spring overshoots, and a numeric value follows it past
43/// the target and back. Implementations that cannot extrapolate (a color's alpha, a count) clamp
44/// the parts that must stay in range.
45pub trait Lerp: Clone {
46    fn lerp(&self, to: &Self, t: f64) -> Self;
47}
48
49/// `a + (b - a) * t`, the scalar every other implementation is built from.
50#[inline]
51pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
52    a + (b - a) * t
53}
54
55impl Lerp for f64 {
56    fn lerp(&self, to: &Self, t: f64) -> Self {
57        lerp(*self, *to, t)
58    }
59}
60
61impl Lerp for f32 {
62    fn lerp(&self, to: &Self, t: f64) -> Self {
63        lerp(*self as f64, *to as f64, t) as f32
64    }
65}
66
67impl Lerp for Point {
68    fn lerp(&self, to: &Self, t: f64) -> Self {
69        Point::new(lerp(self.x, to.x, t), lerp(self.y, to.y, t))
70    }
71}
72
73impl Lerp for Size {
74    fn lerp(&self, to: &Self, t: f64) -> Self {
75        Size::new(
76            lerp(self.width, to.width, t).max(0.0),
77            lerp(self.height, to.height, t).max(0.0),
78        )
79    }
80}
81
82impl Lerp for Rect {
83    fn lerp(&self, to: &Self, t: f64) -> Self {
84        Rect {
85            origin: self.origin.lerp(&to.origin, t),
86            size: self.size.lerp(&to.size, t),
87        }
88    }
89}
90
91/// Colors blend in OKLab (Björn Ottosson's perceptual space), alpha linearly: the midpoint of blue
92/// and yellow is a clean mid-lightness color rather than sRGB's dim gray, and a sequential scale's
93/// in-between cells read as in-between values. The endpoints are exact.
94impl Lerp for Color {
95    fn lerp(&self, to: &Self, t: f64) -> Self {
96        mix_oklab(*self, *to, t)
97    }
98}
99
100impl<A: Lerp, B: Lerp> Lerp for (A, B) {
101    fn lerp(&self, to: &Self, t: f64) -> Self {
102        (self.0.lerp(&to.0, t), self.1.lerp(&to.1, t))
103    }
104}
105
106impl<A: Lerp, B: Lerp, C: Lerp> Lerp for (A, B, C) {
107    fn lerp(&self, to: &Self, t: f64) -> Self {
108        (
109            self.0.lerp(&to.0, t),
110            self.1.lerp(&to.1, t),
111            self.2.lerp(&to.2, t),
112        )
113    }
114}
115
116impl<T: Lerp, const N: usize> Lerp for [T; N] {
117    fn lerp(&self, to: &Self, t: f64) -> Self {
118        std::array::from_fn(|i| self[i].lerp(&to[i], t))
119    }
120}
121
122/// Element-wise over the common prefix; the target's extra elements (or the source's missing ones)
123/// take the target's values, so a series that grew draws its new tail where it is headed. A caller
124/// that wants entering elements to grow in matches them up first; `Vec` has no idea what they mean.
125impl<T: Lerp> Lerp for Vec<T> {
126    fn lerp(&self, to: &Self, t: f64) -> Self {
127        to.iter()
128            .enumerate()
129            .map(|(i, b)| match self.get(i) {
130                Some(a) => a.lerp(b, t),
131                None => b.clone(),
132            })
133            .collect()
134    }
135}
136
137/// Both present: interpolate. Otherwise there is nothing to move between, so the value switches at
138/// the midpoint.
139impl<T: Lerp> Lerp for Option<T> {
140    fn lerp(&self, to: &Self, t: f64) -> Self {
141        match (self, to) {
142            (Some(a), Some(b)) => Some(a.lerp(b, t)),
143            _ if t < 0.5 => self.clone(),
144            _ => to.clone(),
145        }
146    }
147}
148
149// --- OKLab ------------------------------------------------------------------------------------
150
151fn to_linear(c: f64) -> f64 {
152    if c <= 0.04045 {
153        c / 12.92
154    } else {
155        ((c + 0.055) / 1.055).powf(2.4)
156    }
157}
158
159fn to_srgb(c: f64) -> f64 {
160    if c <= 0.003_130_8 {
161        c * 12.92
162    } else {
163        1.055 * c.powf(1.0 / 2.4) - 0.055
164    }
165}
166
167fn oklab(c: Color) -> [f64; 3] {
168    let (r, g, b) = (to_linear(c.r), to_linear(c.g), to_linear(c.b));
169    let l = (0.412_221_470_8 * r + 0.536_332_536_3 * g + 0.051_445_992_9 * b).cbrt();
170    let m = (0.211_903_498_2 * r + 0.680_699_545_1 * g + 0.107_396_956_6 * b).cbrt();
171    let s = (0.088_302_461_9 * r + 0.281_718_837_6 * g + 0.629_978_700_5 * b).cbrt();
172    [
173        0.210_454_255_3 * l + 0.793_617_785_0 * m - 0.004_072_046_8 * s,
174        1.977_998_495_1 * l - 2.428_592_205_0 * m + 0.450_593_709_9 * s,
175        0.025_904_037_1 * l + 0.782_771_766_2 * m - 0.808_675_766_0 * s,
176    ]
177}
178
179fn from_oklab([l, a, b]: [f64; 3], alpha: f64) -> Color {
180    let l_ = l + 0.396_337_777_4 * a + 0.215_803_757_3 * b;
181    let m_ = l - 0.105_561_345_8 * a - 0.063_854_172_8 * b;
182    let s_ = l - 0.089_484_177_5 * a - 1.291_485_548_0 * b;
183    let (l3, m3, s3) = (l_ * l_ * l_, m_ * m_ * m_, s_ * s_ * s_);
184    let r = 4.076_741_662_1 * l3 - 3.307_711_591_3 * m3 + 0.230_969_929_2 * s3;
185    let g = -1.268_438_004_6 * l3 + 2.609_757_401_1 * m3 - 0.341_319_396_5 * s3;
186    let bl = -0.004_196_086_3 * l3 - 0.703_418_614_7 * m3 + 1.707_614_701_0 * s3;
187    let ch = |v: f64| to_srgb(v.max(0.0)).clamp(0.0, 1.0);
188    Color::rgba(ch(r), ch(g), ch(bl), alpha.clamp(0.0, 1.0))
189}
190
191/// Blend two colors in OKLab by `t`, alpha linearly. `t` outside `0..=1` extrapolates the
192/// lightness and hue axes and clamps the result into gamut.
193pub fn mix_oklab(a: Color, b: Color, t: f64) -> Color {
194    // The endpoints are the endpoints, not a round trip through two color spaces.
195    if t == 0.0 {
196        return a;
197    }
198    if t == 1.0 {
199        return b;
200    }
201    let (p, q) = (oklab(a), oklab(b));
202    from_oklab(
203        [
204            lerp(p[0], q[0], t),
205            lerp(p[1], q[1], t),
206            lerp(p[2], q[2], t),
207        ],
208        lerp(a.a, b.a, t),
209    )
210}
211
212// --- Timing -----------------------------------------------------------------------------------
213
214/// One sample of a [`Timing`]: how far the value has come, and whether it has arrived.
215#[derive(Clone, Copy, Debug, PartialEq)]
216pub struct Sample {
217    /// `0` at the start, `1` at the end. A spring may overshoot on the way; the final sample is
218    /// exactly the end value (`1`, or `0` for an autoreversed cycle count that ends where it began).
219    pub progress: f64,
220    /// Whether this sample is the last one. A driver stops requesting frames after it.
221    pub done: bool,
222}
223
224/// An [`AnimSpec`] as a function of elapsed seconds.
225///
226/// Easing curves run for `duration_ms`. A spring runs its analytic step response and ends at
227/// `duration_ms`, which `AnimSpec::spring` sets to the spring's `response`: that is how every Day
228/// backend times a spring, so a canvas transition and a native one started together land together.
229#[derive(Clone, Copy, Debug, PartialEq)]
230pub struct Timing {
231    pub spec: AnimSpec,
232}
233
234impl Timing {
235    pub fn new(spec: AnimSpec) -> Self {
236        Timing { spec }
237    }
238
239    /// One cycle, in seconds.
240    fn cycle(&self) -> f64 {
241        (self.spec.duration_ms as f64 / 1000.0).max(0.0)
242    }
243
244    /// The whole run including the delay, or `None` for a spec that repeats forever.
245    pub fn total(&self) -> Option<f64> {
246        if self.spec.repeat == u32::MAX {
247            return None;
248        }
249        Some(self.spec.delay_ms as f64 / 1000.0 + self.cycle() * (self.spec.repeat as f64 + 1.0))
250    }
251
252    /// Where the transition is `elapsed` seconds after it was started.
253    pub fn sample(&self, elapsed: f64) -> Sample {
254        let t = elapsed - self.spec.delay_ms as f64 / 1000.0;
255        if t < 0.0 || !t.is_finite() {
256            return Sample {
257                progress: 0.0,
258                done: false,
259            };
260        }
261        let one = self.cycle();
262        let reverses = self.spec.autoreverse;
263        let cycles = self.spec.repeat.saturating_add(1);
264        let forever = self.spec.repeat == u32::MAX;
265        if one <= 0.0 {
266            return Sample {
267                progress: 1.0,
268                done: true,
269            };
270        }
271        let index = (t / one).floor();
272        if !forever && index >= cycles as f64 {
273            // Arrived. An autoreversed run of an even number of cycles ends back at the start.
274            let back = reverses && cycles.is_multiple_of(2);
275            return Sample {
276                progress: if back { 0.0 } else { 1.0 },
277                done: true,
278            };
279        }
280        let mut local = t - index * one;
281        if reverses && (index as u64) % 2 == 1 {
282            local = one - local;
283        }
284        // A spring evaluates its own step response, which may overshoot; an easing curve its
285        // shape over the cycle. `Curve::fraction` is both.
286        Sample {
287            progress: self.spec.curve.fraction(local, one),
288            done: false,
289        }
290    }
291}
292
293/// `spec`, delayed for item `index` of `count` so the items start `spread_ms` apart in total:
294/// the first starts on time, the last `spread_ms` later. Bars rising left to right, points popping
295/// in along a line: a sequence is one spec per item, and this is the arithmetic.
296pub fn stagger(spec: AnimSpec, index: usize, count: usize, spread_ms: u32) -> AnimSpec {
297    let step = if count > 1 {
298        spread_ms as f64 / (count - 1) as f64
299    } else {
300        0.0
301    };
302    AnimSpec {
303        delay_ms: spec.delay_ms + (step * index as f64).round() as u32,
304        ..spec
305    }
306}
307
308// --- Drivers ----------------------------------------------------------------------------------
309
310/// Call `step(sample)` once per display frame of `clock` until `timing` is done, then stop.
311///
312/// Elapsed time is measured from the first frame delivered, not from this call, so a transition
313/// started while the app is busy begins on screen rather than part-way through. The final call has
314/// `sample.done == true` and the exact end value; after it the subscription ends and the window
315/// requests no more frames on this transition's behalf. Keep the handle, or call
316/// [`FrameHandle::in_scope`]: dropping it cancels the transition where it stands.
317pub fn animate(
318    clock: FrameClock,
319    timing: Timing,
320    mut step: impl FnMut(Sample) + 'static,
321) -> FrameHandle {
322    // Under reduced motion, finish finite decorative tweens on their first frame. Keep
323    // continuous animations and simulations running; they have no meaningful final state. The
324    // check is per frame, not per start, so a setting flipped mid-flight lands the tween too.
325    let finish = |timing: &Timing| {
326        (crate::reduce_motion_now() && timing.total().is_some()).then_some(Sample {
327            progress: if timing.spec.duration_ms > 0
328                && timing.spec.autoreverse
329                && timing.spec.repeat % 2 == 1
330            {
331                0.0
332            } else {
333                1.0
334            },
335            done: true,
336        })
337    };
338    let mut start: Option<Duration> = None;
339    clock.subscribe(move |frame| {
340        let t0 = *start.get_or_insert(frame.timestamp);
341        let elapsed = frame.timestamp.saturating_sub(t0).as_secs_f64();
342        let sample = finish(&timing).unwrap_or_else(|| timing.sample(elapsed));
343        step(sample);
344        if sample.done {
345            ControlFlow::Break(())
346        } else {
347            ControlFlow::Continue(())
348        }
349    })
350}
351
352struct Flight<T> {
353    from: T,
354    to: T,
355    handle: Option<FrameHandle>,
356}
357
358/// A value that animates toward whatever it is told to become.
359///
360/// Reads are reactive, so a canvas that reads a `Tweened` re-records every frame of its
361/// transition and not otherwise. Retargeting mid-flight starts the new transition from the value
362/// on screen, which keeps the motion continuous: a slider dragged back and forth bends the path
363/// instead of snapping it.
364///
365/// ```rust,ignore
366/// let height = Tweened::new(0.0);
367/// button("Grow").action(move || height.animate_to(120.0, AnimSpec::spring(0.4, 0.8)));
368/// canvas(move |d, _| d.fill(Shape::Rect(Rect::new(0.0, 0.0, 40.0, height.get())), BLUE));
369/// ```
370pub struct Tweened<T: Lerp + 'static> {
371    shown: day_reactive::Signal<T>,
372    flight: Rc<RefCell<Flight<T>>>,
373    clock: FrameClock,
374    moving: Rc<Cell<bool>>,
375}
376
377impl<T: Lerp + 'static> Clone for Tweened<T> {
378    fn clone(&self) -> Self {
379        Tweened {
380            shown: self.shown,
381            flight: self.flight.clone(),
382            clock: self.clock,
383            moving: self.moving.clone(),
384        }
385    }
386}
387
388impl<T: Lerp + 'static> Tweened<T> {
389    /// A value at `initial`, animating on the current page's window.
390    pub fn new(initial: T) -> Self {
391        Self::on(FrameClock::current(), initial)
392    }
393
394    /// A value at `initial`, animating on `clock`.
395    pub fn on(clock: FrameClock, initial: T) -> Self {
396        Tweened {
397            shown: day_reactive::Signal::new(initial.clone()),
398            flight: Rc::new(RefCell::new(Flight {
399                from: initial.clone(),
400                to: initial,
401                handle: None,
402            })),
403            clock,
404            moving: Rc::new(Cell::new(false)),
405        }
406    }
407
408    /// The value on screen now; tracked.
409    pub fn get(&self) -> T {
410        self.shown.get()
411    }
412
413    /// The value on screen now, without subscribing.
414    pub fn get_untracked(&self) -> T {
415        self.shown.get_untracked()
416    }
417
418    /// Where the value is headed (the value itself once it has arrived).
419    pub fn target(&self) -> T {
420        self.flight.borrow().to.clone()
421    }
422
423    /// Whether a transition is in flight.
424    pub fn is_animating(&self) -> bool {
425        self.moving.get()
426    }
427
428    /// Jump to `value`, cancelling any transition.
429    pub fn set(&self, value: T) {
430        let mut f = self.flight.borrow_mut();
431        if let Some(h) = f.handle.take() {
432            h.cancel();
433        }
434        f.from = value.clone();
435        f.to = value.clone();
436        drop(f);
437        self.moving.set(false);
438        self.shown.set(value);
439    }
440
441    /// Animate from the value on screen to `target` under `spec`.
442    pub fn animate_to(&self, target: T, spec: AnimSpec) {
443        // Reduced motion (docs/accessibility.md): a finite transition lands at once; an
444        // endless one (a spinner, a pulse that is the only sign of work) keeps going.
445        if crate::reduce_motion_now() && spec.repeat != u32::MAX {
446            self.set(target);
447            return;
448        }
449        let from = self.shown.get_untracked();
450        {
451            let mut f = self.flight.borrow_mut();
452            if let Some(h) = f.handle.take() {
453                h.cancel();
454            }
455            f.from = from;
456            f.to = target;
457        }
458        self.moving.set(true);
459        let (flight, shown, moving) = (self.flight.clone(), self.shown, self.moving.clone());
460        let handle = animate(self.clock, Timing::new(spec), move |s| {
461            let v = {
462                let f = flight.borrow();
463                if s.done {
464                    f.to.clone()
465                } else {
466                    f.from.lerp(&f.to, s.progress)
467                }
468            };
469            shown.set(v);
470            if s.done {
471                moving.set(false);
472            }
473        });
474        self.flight.borrow_mut().handle = Some(handle);
475    }
476}
477
478// ---------------------------------------------------------------------------
479// Pairs: matching two collections
480// ---------------------------------------------------------------------------
481
482/// Which side of a change an item of a [`Pairs`] is on.
483#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
484pub enum Change {
485    /// In both states: it moves from where it was to where it is.
486    Kept,
487    /// Only in the new state: it arrives from a stand-in the caller chose.
488    Entering,
489    /// Only in the old state: it departs toward a stand-in the caller chose, and is gone once the
490    /// transition ends.
491    Leaving,
492}
493
494/// One item of a transition between two collections: where it starts, where it ends, and why.
495#[derive(Clone, Debug, PartialEq)]
496pub struct Pair<T> {
497    pub from: T,
498    pub to: T,
499    pub change: Change,
500}
501
502impl<T: Lerp> Pair<T> {
503    /// The item `t` of the way along.
504    pub fn at(&self, t: f64) -> T {
505        self.from.lerp(&self.to, t)
506    }
507}
508
509/// Two states of a collection, matched item by item, ready to be sampled at any `t`.
510///
511/// Built once per change, sampled once per frame: matching is the part that costs something and
512/// it does not depend on `t`. [`Pairs::by_key`] matches by an identity the caller derives; a
513/// caller whose own rules already know what moved where (a sliding-tile game knows which tiles
514/// merged) collects its [`Pair`]s directly (`Pairs: FromIterator`).
515///
516/// ```
517/// use day_core::tween::{Change, Lerp, Pairs, lerp};
518///
519/// #[derive(Clone, Debug, PartialEq)]
520/// struct Bar { name: &'static str, height: f64 }
521///
522/// impl Lerp for Bar {
523///     fn lerp(&self, to: &Self, t: f64) -> Self {
524///         Bar { name: to.name, height: lerp(self.height, to.height, t) }
525///     }
526/// }
527///
528/// let bar = |name, height| Bar { name, height };
529/// // "b" shrinks, "c" grows out of nothing, "a" collapses away.
530/// let before = [bar("a", 3.0), bar("b", 5.0)];
531/// let after = [bar("b", 1.0), bar("c", 4.0)];
532/// let pairs = Pairs::by_key(&before, &after, |b| b.name, |b, _| bar(b.name, 0.0));
533/// assert_eq!(pairs.at(0.5), vec![bar("b", 3.0), bar("c", 2.0), bar("a", 1.5)]);
534/// assert_eq!(pairs[2].change, Change::Leaving);
535/// ```
536#[derive(Clone, Debug, PartialEq)]
537pub struct Pairs<T> {
538    items: Vec<Pair<T>>,
539}
540
541impl<T> Default for Pairs<T> {
542    fn default() -> Self {
543        Pairs { items: Vec::new() }
544    }
545}
546
547impl<T: Clone> Pairs<T> {
548    /// Match `from` with `to` by `key`.
549    ///
550    /// * An item whose key is on both sides is [`Change::Kept`]. Keys may repeat: the *n*th item
551    ///   with a key on one side matches the *n*th with that key on the other, so identical tokens
552    ///   (three red gems in a column) pair up in order rather than all with the first.
553    /// * An item only in `to` is [`Change::Entering`]; `absent(item, Change::Entering)` says where
554    ///   it starts. An item only in `from` is [`Change::Leaving`]; `absent(item, Change::Leaving)`
555    ///   says where it ends. That stand-in is the whole of what entering and leaving mean: grown
556    ///   from a baseline, faded out, slid in from a neighbor. The caller decides; the match does
557    ///   not.
558    ///
559    /// The result is in `to`'s order, then the leaving items in `from`'s order, so a renderer
560    /// drawing in sequence draws the arriving picture's order, with what is departing on top.
561    pub fn by_key<K: Eq + Hash>(
562        from: &[T],
563        to: &[T],
564        mut key: impl FnMut(&T) -> K,
565        mut absent: impl FnMut(&T, Change) -> T,
566    ) -> Self {
567        let mut waiting: HashMap<K, VecDeque<usize>> = HashMap::with_capacity(from.len());
568        for (i, item) in from.iter().enumerate() {
569            waiting.entry(key(item)).or_default().push_back(i);
570        }
571        let mut matched = vec![false; from.len()];
572        let mut items = Vec::with_capacity(from.len().max(to.len()));
573        for item in to {
574            let found = waiting.get_mut(&key(item)).and_then(VecDeque::pop_front);
575            items.push(match found {
576                Some(i) => {
577                    matched[i] = true;
578                    Pair {
579                        from: from[i].clone(),
580                        to: item.clone(),
581                        change: Change::Kept,
582                    }
583                }
584                None => Pair {
585                    from: absent(item, Change::Entering),
586                    to: item.clone(),
587                    change: Change::Entering,
588                },
589            });
590        }
591        for (item, _) in from.iter().zip(&matched).filter(|(_, m)| !**m) {
592            items.push(Pair {
593                from: item.clone(),
594                to: absent(item, Change::Leaving),
595                change: Change::Leaving,
596            });
597        }
598        Pairs { items }
599    }
600}
601
602impl<T> Pairs<T> {
603    /// Every item `t` of the way along, in order, through `f`: for items whose in-between depends
604    /// on more than the two ends (a chart mark needs both charts' scales to place itself).
605    pub fn at_with<U>(&self, t: f64, mut f: impl FnMut(&Pair<T>, f64) -> U) -> Vec<U> {
606        self.items.iter().map(|p| f(p, t)).collect()
607    }
608
609    /// Each item at its own progress: `progress(index, pair)` gives the `t` for that item, for
610    /// sequences that do not move in lockstep (cards dealt one after another, see [`stagger`]).
611    pub fn at_each<U>(
612        &self,
613        mut progress: impl FnMut(usize, &Pair<T>) -> f64,
614        mut f: impl FnMut(&Pair<T>, f64) -> U,
615    ) -> Vec<U> {
616        self.items
617            .iter()
618            .enumerate()
619            .map(|(i, p)| f(p, progress(i, p)))
620            .collect()
621    }
622
623    /// The arriving state: every item that is not leaving, at its end. What to draw once the
624    /// transition is over.
625    pub fn settled(&self) -> impl Iterator<Item = &T> {
626        self.items
627            .iter()
628            .filter(|p| p.change != Change::Leaving)
629            .map(|p| &p.to)
630    }
631
632    /// How many items of each kind of [`Change`] there are: `(kept, entering, leaving)`.
633    pub fn counts(&self) -> (usize, usize, usize) {
634        self.items
635            .iter()
636            .fold((0, 0, 0), |(k, e, l), p| match p.change {
637                Change::Kept => (k + 1, e, l),
638                Change::Entering => (k, e + 1, l),
639                Change::Leaving => (k, e, l + 1),
640            })
641    }
642
643    pub fn iter(&self) -> std::slice::Iter<'_, Pair<T>> {
644        self.items.iter()
645    }
646
647    pub fn len(&self) -> usize {
648        self.items.len()
649    }
650
651    pub fn is_empty(&self) -> bool {
652        self.items.is_empty()
653    }
654}
655
656impl<T: Lerp> Pairs<T> {
657    /// Every item `t` of the way along, in order. Leaving items are included until the caller
658    /// stops sampling: at `t = 1` they sit at their stand-ins.
659    pub fn at(&self, t: f64) -> Vec<T> {
660        self.items.iter().map(|p| p.at(t)).collect()
661    }
662}
663
664impl<T> std::ops::Index<usize> for Pairs<T> {
665    type Output = Pair<T>;
666    fn index(&self, i: usize) -> &Pair<T> {
667        &self.items[i]
668    }
669}
670
671impl<T> FromIterator<Pair<T>> for Pairs<T> {
672    fn from_iter<I: IntoIterator<Item = Pair<T>>>(iter: I) -> Self {
673        Pairs {
674            items: iter.into_iter().collect(),
675        }
676    }
677}
678
679impl<T> IntoIterator for Pairs<T> {
680    type Item = Pair<T>;
681    type IntoIter = std::vec::IntoIter<Pair<T>>;
682    fn into_iter(self) -> Self::IntoIter {
683        self.items.into_iter()
684    }
685}
686
687impl<'a, T> IntoIterator for &'a Pairs<T> {
688    type Item = &'a Pair<T>;
689    type IntoIter = std::slice::Iter<'a, Pair<T>>;
690    fn into_iter(self) -> Self::IntoIter {
691        self.items.iter()
692    }
693}
694
695#[cfg(test)]
696mod tests {
697    use super::*;
698
699    fn near(a: f64, b: f64) -> bool {
700        (a - b).abs() < 1e-9
701    }
702
703    #[test]
704    fn linear_timing_runs_its_duration_then_stops() {
705        let t = Timing::new(AnimSpec::linear(200));
706        assert_eq!(t.sample(0.0).progress, 0.0);
707        assert!(near(t.sample(0.1).progress, 0.5));
708        assert!(!t.sample(0.199).done);
709        let end = t.sample(0.2);
710        assert!(end.done && end.progress == 1.0);
711        assert_eq!(t.total(), Some(0.2));
712    }
713
714    #[test]
715    fn delay_holds_the_start_and_repeat_autoreverse_ends_where_it_began() {
716        let t = Timing::new(AnimSpec {
717            delay_ms: 100,
718            repeat: 1,
719            autoreverse: true,
720            ..AnimSpec::linear(100)
721        });
722        assert_eq!(t.sample(0.05).progress, 0.0);
723        assert!(near(t.sample(0.15).progress, 0.5)); // forward
724        assert!(near(t.sample(0.275).progress, 0.25)); // back
725        let end = t.sample(0.31);
726        assert!(end.done && end.progress == 0.0);
727        assert!(near(t.total().unwrap(), 0.3));
728        let forever = Timing::new(AnimSpec {
729            repeat: u32::MAX,
730            ..AnimSpec::linear(100)
731        });
732        assert!(!forever.sample(1000.0).done);
733        assert_eq!(forever.total(), None);
734    }
735
736    #[test]
737    fn a_bouncy_spring_overshoots_and_ends_exactly_on_target() {
738        let t = Timing::new(AnimSpec::spring(0.5, 0.4));
739        let peak = (1..50)
740            .map(|i| t.sample(i as f64 * 0.01).progress)
741            .fold(0.0, f64::max);
742        assert!(peak > 1.0, "an under-damped spring overshoots: peak {peak}");
743        let end = t.sample(0.5);
744        assert!(end.done && end.progress == 1.0);
745    }
746
747    #[test]
748    fn stagger_spreads_delays_across_the_items() {
749        let s = AnimSpec::linear(300);
750        assert_eq!(stagger(s, 0, 5, 400).delay_ms, 0);
751        assert_eq!(stagger(s, 2, 5, 400).delay_ms, 200);
752        assert_eq!(stagger(s, 4, 5, 400).delay_ms, 400);
753        assert_eq!(stagger(s, 0, 1, 400).delay_ms, 0);
754    }
755
756    #[test]
757    fn tweened_values_ride_the_frame_clock_and_release_it_on_arrival() {
758        use crate::tree::RNode;
759        use day_spec::FrameStamp;
760        use std::collections::VecDeque;
761        let pending: Rc<RefCell<VecDeque<day_spec::FrameCallback>>> = Rc::default();
762        let q = pending.clone();
763        crate::frame::install_frame_requester(move |_, cb| {
764            q.borrow_mut().push_back(cb);
765            Box::new(|| {})
766        });
767        let fire = |ts: f64| {
768            let cb = pending
769                .borrow_mut()
770                .pop_front()
771                .expect("a frame was requested");
772            cb(FrameStamp {
773                timestamp: ts,
774                target_timestamp: None,
775            });
776        };
777        let v = Tweened::on(FrameClock::for_root(RNode::default()), 0.0);
778        v.animate_to(100.0, AnimSpec::linear(100));
779        assert!(v.is_animating());
780        fire(5.0); // the first frame is the start: elapsed 0
781        assert_eq!(v.get_untracked(), 0.0);
782        fire(5.05);
783        assert!(near(v.get_untracked(), 50.0));
784        // Retargeted mid-flight: the new transition starts from the value on screen. The cancelled
785        // transition's request is still queued in this fake (a native source would drop it); the
786        // frame service ignores its delivery.
787        v.animate_to(0.0, AnimSpec::linear(100));
788        fire(5.06);
789        fire(5.07); // the new transition's first frame
790        assert!(near(v.get_untracked(), 50.0));
791        fire(5.12);
792        assert!(near(v.get_untracked(), 25.0));
793        fire(5.18);
794        assert_eq!(v.get_untracked(), 0.0);
795        assert!(!v.is_animating());
796        assert!(
797            pending.borrow().is_empty(),
798            "no frames requested after arrival"
799        );
800    }
801
802    #[test]
803    fn values_interpolate_and_colors_keep_their_endpoints() {
804        assert!(near(10.0.lerp(&20.0, 0.25), 12.5));
805        let r = Rect::new(0.0, 0.0, 10.0, 10.0).lerp(&Rect::new(10.0, 10.0, 30.0, 10.0), 0.5);
806        assert_eq!(r, Rect::new(5.0, 5.0, 20.0, 10.0));
807        let (blue, yellow) = (Color::rgb(0.0, 0.0, 1.0), Color::rgb(1.0, 1.0, 0.0));
808        let a = blue.lerp(&yellow, 0.0);
809        let b = blue.lerp(&yellow, 1.0);
810        assert_eq!(a, blue);
811        assert_eq!(b, yellow);
812        // The OKLab midpoint of blue and yellow is lighter than sRGB's gray (0.5, 0.5, 0.5).
813        let mid = blue.lerp(&yellow, 0.5);
814        assert!(mid.r + mid.g + mid.b > 1.6, "{mid:?}");
815        assert_eq!(
816            vec![0.0, 1.0].lerp(&vec![2.0, 3.0, 9.0], 0.5),
817            vec![1.0, 2.0, 9.0]
818        );
819        assert_eq!(Some(1.0).lerp(&None, 0.4), Some(1.0));
820        assert_eq!(Some(1.0).lerp(&None, 0.6), None);
821    }
822
823    // --- Pairs -----------------------------------------------------------------------------
824
825    /// A keyed value, the shape most callers pair: an identity and something that moves.
826    #[derive(Clone, Debug, PartialEq)]
827    struct Item {
828        key: &'static str,
829        at: f64,
830    }
831
832    impl Lerp for Item {
833        fn lerp(&self, to: &Self, t: f64) -> Self {
834            Item {
835                key: to.key,
836                at: lerp(self.at, to.at, t),
837            }
838        }
839    }
840
841    fn item(key: &'static str, at: f64) -> Item {
842        Item { key, at }
843    }
844
845    /// Stand-ins at zero, the way a bar grows from its baseline.
846    fn from_zero(i: &Item, _: Change) -> Item {
847        item(i.key, 0.0)
848    }
849
850    fn by_key(from: &[Item], to: &[Item]) -> Pairs<Item> {
851        Pairs::by_key(from, to, |i| i.key, from_zero)
852    }
853
854    fn changes(p: &Pairs<Item>) -> Vec<(&'static str, Change)> {
855        p.iter().map(|p| (p.to.key, p.change)).collect()
856    }
857
858    #[test]
859    fn items_are_kept_entering_or_leaving_in_arriving_order_then_leaving() {
860        let p = by_key(
861            &[item("a", 1.0), item("b", 2.0), item("c", 3.0)],
862            &[item("c", 30.0), item("d", 40.0), item("a", 10.0)],
863        );
864        assert_eq!(
865            changes(&p),
866            vec![
867                ("c", Change::Kept),
868                ("d", Change::Entering),
869                ("a", Change::Kept),
870                ("b", Change::Leaving),
871            ]
872        );
873        assert_eq!(p.counts(), (2, 1, 1));
874        // A kept item starts where it was, an entering one at its stand-in, a leaving one where
875        // it was and ends at its stand-in.
876        assert_eq!((p[0].from.at, p[0].to.at), (3.0, 30.0));
877        assert_eq!((p[1].from.at, p[1].to.at), (0.0, 40.0));
878        assert_eq!((p[2].from.at, p[2].to.at), (1.0, 10.0));
879        assert_eq!((p[3].from.at, p[3].to.at), (2.0, 0.0));
880    }
881
882    #[test]
883    fn the_stand_in_is_asked_for_with_the_item_and_its_side() {
884        let mut asked = Vec::new();
885        let p = Pairs::by_key(
886            &[item("gone", 5.0), item("stays", 1.0)],
887            &[item("stays", 2.0), item("new", 7.0)],
888            |i| i.key,
889            |i, change| {
890                asked.push((i.key, i.at, change));
891                // Entering items come from below their target, leaving ones go above.
892                match change {
893                    Change::Entering => item(i.key, i.at - 100.0),
894                    Change::Leaving => item(i.key, i.at + 100.0),
895                    Change::Kept => unreachable!("a kept item has both ends"),
896                }
897            },
898        );
899        assert_eq!(
900            asked,
901            vec![
902                ("new", 7.0, Change::Entering),
903                ("gone", 5.0, Change::Leaving)
904            ]
905        );
906        assert_eq!(p[1].from.at, -93.0);
907        assert_eq!(p[2].to.at, 105.0);
908    }
909
910    #[test]
911    fn repeated_keys_pair_in_order_and_the_surplus_enters_or_leaves() {
912        // Three red gems become two: the first two pair with the first two, the third leaves.
913        let p = by_key(
914            &[item("red", 1.0), item("red", 2.0), item("red", 3.0)],
915            &[item("red", 10.0), item("red", 20.0)],
916        );
917        assert_eq!(p.counts(), (2, 0, 1));
918        assert_eq!((p[0].from.at, p[1].from.at), (1.0, 2.0));
919        assert_eq!((p[2].from.at, p[2].change), (3.0, Change::Leaving));
920        // And the other way: the extra one enters.
921        let p = by_key(&[item("red", 1.0)], &[item("red", 10.0), item("red", 20.0)]);
922        assert_eq!(p.counts(), (1, 1, 0));
923        assert_eq!((p[0].from.at, p[1].change), (1.0, Change::Entering));
924    }
925
926    #[test]
927    fn an_empty_side_makes_everything_enter_or_leave() {
928        let some = [item("a", 1.0), item("b", 2.0)];
929        let arriving = by_key(&[], &some);
930        assert_eq!(arriving.counts(), (0, 2, 0));
931        assert_eq!(arriving.at(0.0), vec![item("a", 0.0), item("b", 0.0)]);
932        let departing = by_key(&some, &[]);
933        assert_eq!(departing.counts(), (0, 0, 2));
934        assert_eq!(departing.at(1.0), vec![item("a", 0.0), item("b", 0.0)]);
935        assert!(by_key(&[], &[]).is_empty());
936        assert!(Pairs::<Item>::default().is_empty());
937    }
938
939    #[test]
940    fn a_permutation_keeps_everything_and_follows_the_new_order() {
941        let from = [item("a", 0.0), item("b", 1.0), item("c", 2.0)];
942        let to = [item("c", 0.0), item("a", 1.0), item("b", 2.0)];
943        let p = by_key(&from, &to);
944        assert_eq!(p.counts(), (3, 0, 0));
945        assert_eq!(
946            p.at(0.0),
947            vec![item("c", 2.0), item("a", 0.0), item("b", 1.0)]
948        );
949        assert_eq!(p.at(1.0), to.to_vec());
950    }
951
952    #[test]
953    fn sampling_runs_from_the_old_state_to_the_new_one_and_past_it() {
954        let p = by_key(&[item("a", 0.0), item("b", 8.0)], &[item("a", 10.0)]);
955        assert_eq!(p.at(0.0), vec![item("a", 0.0), item("b", 8.0)]);
956        assert_eq!(p.at(0.25), vec![item("a", 2.5), item("b", 6.0)]);
957        // At the end the leaving item sits at its stand-in; the caller drops it when done.
958        assert_eq!(p.at(1.0), vec![item("a", 10.0), item("b", 0.0)]);
959        // A spring's overshoot carries the values past their targets.
960        assert_eq!(p.at(1.1)[0].at, 11.0);
961        assert_eq!(
962            p.settled().cloned().collect::<Vec<_>>(),
963            vec![item("a", 10.0)]
964        );
965    }
966
967    #[test]
968    fn at_with_hands_each_pair_and_t_to_the_caller() {
969        let p = by_key(&[item("a", 0.0)], &[item("a", 4.0), item("b", 2.0)]);
970        let got = p.at_with(0.5, |pair, t| (pair.change, pair.at(t).at));
971        assert_eq!(got, vec![(Change::Kept, 2.0), (Change::Entering, 1.0)]);
972    }
973
974    #[test]
975    fn at_each_gives_every_item_its_own_progress() {
976        // Dealt one after another: item i starts i quarters later.
977        let to: Vec<Item> = ["a", "b", "c"].iter().map(|k| item(k, 10.0)).collect();
978        let p = by_key(&[], &to);
979        let elapsed = 0.5;
980        let got = p.at_each(
981            |i, _| ((elapsed - i as f64 * 0.25) / 0.5).clamp(0.0, 1.0),
982            |pair, t| pair.at(t).at,
983        );
984        assert_eq!(got, vec![10.0, 5.0, 0.0]);
985    }
986
987    #[test]
988    fn pairs_collected_directly_may_share_a_destination() {
989        // Two tiles merging into one cell: the rules know it, and a key match could not say it.
990        let p: Pairs<Point> = [
991            (Point::new(0.0, 0.0), Point::new(3.0, 0.0)),
992            (Point::new(1.0, 0.0), Point::new(3.0, 0.0)),
993        ]
994        .into_iter()
995        .map(|(from, to)| Pair {
996            from,
997            to,
998            change: Change::Kept,
999        })
1000        .collect();
1001        assert_eq!(p.len(), 2);
1002        let mid = p.at(0.5);
1003        assert_eq!(mid, vec![Point::new(1.5, 0.0), Point::new(2.0, 0.0)]);
1004        assert_eq!(p.at(1.0), vec![Point::new(3.0, 0.0); 2]);
1005    }
1006
1007    #[test]
1008    fn pairs_of_library_types_interpolate_with_their_own_lerp() {
1009        let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0));
1010        let from = [(1u8, Rect::new(0.0, 0.0, 10.0, 10.0), red)];
1011        let to = [(1u8, Rect::new(20.0, 0.0, 10.0, 30.0), blue)];
1012        let p = Pairs::by_key(
1013            &from.map(|(_, r, c)| (r, c)),
1014            &to.map(|(_, r, c)| (r, c)),
1015            |_| 1u8,
1016            |x, _| *x,
1017        );
1018        let (rect, color) = p.at(0.5)[0];
1019        assert_eq!(rect, Rect::new(10.0, 0.0, 10.0, 20.0));
1020        assert!(color != red && color != blue);
1021        assert_eq!(p.at(1.0)[0].1, blue);
1022    }
1023
1024    #[test]
1025    fn keys_are_derived_once_per_item() {
1026        let calls = Cell::new(0);
1027        let from = [item("a", 0.0), item("b", 0.0), item("c", 0.0)];
1028        let to = [item("b", 0.0), item("d", 0.0)];
1029        let _ = Pairs::by_key(
1030            &from,
1031            &to,
1032            |i| {
1033                calls.set(calls.get() + 1);
1034                i.key
1035            },
1036            from_zero,
1037        );
1038        assert_eq!(calls.get(), from.len() + to.len());
1039    }
1040
1041    #[test]
1042    fn iteration_and_indexing_see_the_same_pairs() {
1043        let p = by_key(&[item("a", 1.0)], &[item("a", 2.0), item("b", 3.0)]);
1044        let by_ref: Vec<_> = (&p).into_iter().map(|p| p.to.key).collect();
1045        let by_iter: Vec<_> = p.iter().map(|p| p.to.key).collect();
1046        assert_eq!(by_ref, by_iter);
1047        assert_eq!(p[1].to.key, "b");
1048        let owned: Vec<Pair<Item>> = p.clone().into_iter().collect();
1049        assert_eq!(owned.len(), p.len());
1050        assert_eq!(owned[0], p[0]);
1051    }
1052
1053    /// For random collections with repeated keys, the match accounts for every item exactly
1054    /// once: each arriving item is the end of exactly one non-leaving pair (in its order), each
1055    /// departing item the start of exactly one kept or leaving pair, and the number kept is the
1056    /// size of the keys' multiset intersection.
1057    #[test]
1058    fn every_item_is_accounted_for_exactly_once() {
1059        const KEYS: [&str; 5] = ["a", "b", "c", "d", "e"];
1060        let mut seed = 0x9E37_79B9_7F4A_7C15u64;
1061        let mut next = move |n: usize| {
1062            seed ^= seed << 13;
1063            seed ^= seed >> 7;
1064            seed ^= seed << 17;
1065            (seed % n as u64) as usize
1066        };
1067        for round in 0..200 {
1068            let len_a = next(9);
1069            let len_b = next(9);
1070            // Distinct positions so each item can be recognized after matching.
1071            let from: Vec<Item> = (0..len_a)
1072                .map(|i| item(KEYS[next(KEYS.len())], i as f64))
1073                .collect();
1074            let to: Vec<Item> = (0..len_b)
1075                .map(|i| item(KEYS[next(KEYS.len())], 100.0 + i as f64))
1076                .collect();
1077            let p = Pairs::by_key(
1078                &from,
1079                &to,
1080                |i| i.key,
1081                |i, c| match c {
1082                    Change::Entering => item(i.key, -1.0),
1083                    _ => item(i.key, -2.0),
1084                },
1085            );
1086            let arriving: Vec<&Item> = p.settled().collect();
1087            assert_eq!(arriving, to.iter().collect::<Vec<_>>(), "round {round}");
1088            let mut starts: Vec<f64> = p
1089                .iter()
1090                .filter(|p| p.change != Change::Entering)
1091                .map(|p| p.from.at)
1092                .collect();
1093            starts.sort_by(f64::total_cmp);
1094            let all: Vec<f64> = from.iter().map(|i| i.at).collect();
1095            assert_eq!(starts, all, "round {round}");
1096            let kept = KEYS
1097                .iter()
1098                .map(|k| {
1099                    let a = from.iter().filter(|i| i.key == *k).count();
1100                    let b = to.iter().filter(|i| i.key == *k).count();
1101                    a.min(b)
1102                })
1103                .sum::<usize>();
1104            let (k, e, l) = p.counts();
1105            assert_eq!(
1106                (k, e, l),
1107                (kept, len_b - kept, len_a - kept),
1108                "round {round}"
1109            );
1110            for pair in &p {
1111                match pair.change {
1112                    Change::Kept => assert_eq!(pair.from.key, pair.to.key),
1113                    Change::Entering => assert_eq!(pair.from.at, -1.0),
1114                    Change::Leaving => assert_eq!(pair.to.at, -2.0),
1115                }
1116            }
1117        }
1118    }
1119}