Skip to main content

shuyi_core/
ease.rs

1//! Ease curves and tweens, in integers, drawn from the time alone.
2//!
3//! An ease curve ([`Ease`], or any [`Easing`]) bends progress: it takes how
4//! far through something is, in 1/65536 ([`ONE`] is the end), and gives how
5//! far along the value is. A [`Tween`] is a value going from one end to the
6//! other over a time, along a curve. [`Lerp`] is how a type goes between
7//! two of its values.
8//!
9//! ```
10//! use shuyi_core::{Ease, Family, Tween};
11//!
12//! // A score that pops: it rises 6 pixels in 300 milliseconds, overshoots
13//! // a little and settles.
14//! let pop = Tween::new(0, -6, 300).with_ease(Ease::Out(Family::Back));
15//! assert_eq!(pop.value_at(0), 0);
16//! assert_eq!(pop.value_at(200), -7); // past its end: the overshoot
17//! assert_eq!(pop.value_at(300), -6);
18//! assert_eq!(pop.value_at(5000), -6);
19//! ```
20//!
21//! # Drawn from the time alone
22//!
23//! A tween stores no progress. Its value is a function of its fields and
24//! the time since it started ([`Tween::value_at`]), so a tween never needs
25//! updating, and replays, saves and a test of any moment are exact. A tick
26//! simulation stores the tween and the step it started on; a view stores it
27//! and the millisecond.
28//!
29//! Its time is a plain `u32` whose unit the caller picks: steps in a tick
30//! simulation, turns in a turn game, whole milliseconds in a view. A screen
31//! gets each frame's time in seconds, so a view counts its own
32//! milliseconds. A caller with a `u64` clock passes the time since the
33//! start as `u32::try_from(now.saturating_sub(start)).unwrap_or(u32::MAX)`.
34//!
35//! "It landed" is [`Tween::finished_between`] the previous frame's or
36//! step's time and this one's, true once, with no flag kept.
37//!
38//! # The curves
39//!
40//! [`Ease`] names Robert Penner's curves as easings.net lists them: ten
41//! [`Family`]s, each eased in, out, in and out, or out and in, with
42//! [`Linear`](Ease::Linear), smoothstep ([`Smooth`](Ease::Smooth)) and
43//! [`Steps`](Ease::Steps). In RON a curve is written `Linear`,
44//! `InOut(Cubic)` or `Steps(4)`.
45//!
46//! - `Out(f)` is `ONE - In(f)(ONE - p)`, for every family.
47//! - `InOut(f)` is `In` over the first half and `Out` over the second,
48//!   except for `Back` and `Elastic`, whose `InOut` on easings.net has
49//!   constants of its own.
50//! - `OutIn(f)` is `Out` over the first half and `In` over the second.
51//! - `Back` and `Elastic` overshoot: they go below 0 or above [`ONE`] on
52//!   the way.
53//!
54//! Every curve gives exactly 0 at 0 and exactly [`ONE`] at [`ONE`], is
55//! within 1/65536 of its formula for `Linear`, `Smooth`, `Steps` and `Quad`
56//! to `Quint`, and within 2/65536 for the rest. The curves are computed in
57//! integers, so they are the same on every machine; a skin that works in
58//! floats eases through `shuyi_ui::ease::ease_f32`, which goes through
59//! these.
60//!
61//! The same curves in other engines, checked against their docs on
62//! 2026-11-04 (Godot 4.7's `Tween`, Bevy 0.20.1's `EaseFunction` in
63//! `bevy_curve`, Defold 1.14.0's `go.animate`, MDN's `<easing-function>`).
64//! A family and a way of easing make the name: `InOut(Cubic)` is
65//! easings.net's `easeInOutCubic`, Godot's `TRANS_CUBIC` with
66//! `EASE_IN_OUT`, Bevy's `CubicInOut` and Defold's `go.EASING_INOUTCUBIC`.
67//!
68//! | [`Family`] | easings.net | Godot | Bevy | Defold |
69//! | ---------- | ----------- | ----- | ---- | ------ |
70//! | `Sine` | `Sine` | `TRANS_SINE` | `Sine` | `SINE` |
71//! | `Quad` | `Quad` | `TRANS_QUAD` | `Quadratic` | `QUAD` |
72//! | `Cubic` | `Cubic` | `TRANS_CUBIC` | `Cubic` | `CUBIC` |
73//! | `Quart` | `Quart` | `TRANS_QUART` | `Quartic` | `QUART` |
74//! | `Quint` | `Quint` | `TRANS_QUINT` | `Quintic` | `QUINT` |
75//! | `Expo` | `Expo` | `TRANS_EXPO` (1) | `Exponential` (2) | `EXPO` |
76//! | `Circ` | `Circ` | `TRANS_CIRC` | `Circular` | `CIRC` |
77//! | `Back` | `Back` | `TRANS_BACK` | `Back` (3) | `BACK` |
78//! | `Elastic` | `Elastic` | `TRANS_ELASTIC` | `Elastic` | `ELASTIC` |
79//! | `Bounce` | `Bounce` | `TRANS_BOUNCE` | `Bounce` | `BOUNCE` |
80//!
81//! | [`Ease`] | easings.net | Godot | Bevy | Defold | CSS |
82//! | -------- | ----------- | ----- | ---- | ------ | --- |
83//! | `In(f)` | `easeIn…` | `EASE_IN` | `…In` | `IN…` | none |
84//! | `Out(f)` | `easeOut…` | `EASE_OUT` | `…Out` | `OUT…` | none |
85//! | `InOut(f)` | `easeInOut…` | `EASE_IN_OUT` | `…InOut` | `INOUT…` | none |
86//! | `OutIn(f)` | none | `EASE_OUT_IN` | none | `OUTIN…` | none |
87//! | `Linear` | none | `TRANS_LINEAR` | `Linear` | `LINEAR` | `linear` |
88//! | `Smooth` | none | none | `SmoothStep` | none | none |
89//! | `Steps(n)` | none | none | `Steps(n, JumpAt::End)` | none | `steps(n, jump-end)` |
90//!
91//! 1. Godot's `TRANS_EXPO` is Penner's first version, shifted by a
92//!    thousandth so it has no jump: within 1/1000 of `Expo`.
93//! 2. Bevy's `Exponential` curves are scaled to meet 0 and 1 without a
94//!    jump: within 1/1000 of `Expo`.
95//! 3. Bevy's `BackInOut` takes its constant as `1.70158 + 1.525`, not
96//!    `1.70158 * 1.525`, so it overshoots further than `InOut(Back)`.
97//!
98//! None here matches Godot's `TRANS_SPRING`, Bevy's `Elastic(omega)`,
99//! `SmoothStepIn`, `SmoothStepOut` and `SmootherStep` curves, or CSS's
100//! named curves (`ease`, `ease-in`, `ease-out`, `ease-in-out`), which are
101//! cubic Béziers. CSS's `step-end` is `Steps(1)`.
102//!
103//! # Curves and tweens are data
104//!
105//! [`Ease`], [`Tween`] and [`Repeat`] read and write with serde, inside a
106//! game's own RON files. A tween written with only `from`, `to` and
107//! `duration` takes the defaults for the rest:
108//!
109//! ```
110//! use shuyi_core::{Ease, Family, Tween};
111//!
112//! let tween: Tween<i32> = ron::from_str("(from: 0, to: 10, duration: 300)")?;
113//! assert_eq!(tween, Tween::new(0, 10, 300));
114//! let eased: Tween<i32> =
115//!     ron::from_str("(from: 0, to: 10, duration: 300, ease: InOut(Cubic))")?;
116//! assert_eq!(eased.ease, Ease::InOut(Family::Cubic));
117//! # Ok::<(), ron::error::SpannedError>(())
118//! ```
119
120use serde::{Deserialize, Serialize};
121
122mod tables;
123
124use tables::{EXP2, QUARTER_SINE};
125
126/// The whole way, in 1/65536: progress runs from 0 to `ONE`.
127pub const ONE: i32 = 65_536;
128
129/// [`ONE`], wide.
130const WIDE: i64 = 65_536;
131
132/// Half the way.
133const HALF: i64 = WIDE / 2;
134
135/// A turn, in the 1/2^24 an angle of `Elastic` is taken in.
136const TURN: i64 = 1 << 24;
137
138/// A quarter of a [`TURN`].
139const QUARTER: i64 = TURN / 4;
140
141/// `Back`'s constant on easings.net, `c1 = 1.70158`, as a fraction.
142const BACK: (i128, i128) = (170_158, 100_000);
143
144/// `Back`'s constant in and out, `c2 = c1 * 1.525`, as a fraction.
145const BACK_IN_OUT: (i128, i128) = (259_490_950, 100_000_000);
146
147/// A curve: how far along a value is, `p` of the way through.
148pub trait Easing {
149    /// The curve at `p`, in 1/65536. `p` below 0 counts as 0 and `p` above
150    /// [`ONE`] as `ONE`. The value may go past 0 and `ONE` on the way (an
151    /// overshoot), and should be 0 at 0 and `ONE` at `ONE`.
152    fn ease(&self, p: i32) -> i32;
153}
154
155/// The ten families of Penner's curves, as easings.net names them, from the
156/// gentlest to the ones that overshoot and bounce.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
158#[non_exhaustive]
159pub enum Family {
160    /// A quarter of a sine wave.
161    Sine,
162    /// `p²`.
163    Quad,
164    /// `p³`.
165    Cubic,
166    /// `p⁴`.
167    Quart,
168    /// `p⁵`.
169    Quint,
170    /// `2^(10p - 10)`: still, then fast.
171    Expo,
172    /// A quarter of a circle.
173    Circ,
174    /// Pulls back a little before it goes.
175    Back,
176    /// Springs back and forth.
177    Elastic,
178    /// Bounces off its end.
179    Bounce,
180}
181
182/// A named ease curve. The [module docs](self) name each beside other
183/// engines' names.
184#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
185#[non_exhaustive]
186pub enum Ease {
187    /// No easing: the value moves as the time does.
188    #[default]
189    Linear,
190    /// Smoothstep, `3p² - 2p³`: slow at both ends, fastest in the middle.
191    Smooth,
192    /// The family's curve: slow at the start.
193    In(Family),
194    /// The family's curve backwards: slow at the end.
195    Out(Family),
196    /// [`In`](Self::In) over the first half, [`Out`](Self::Out) over the
197    /// second: slow at both ends.
198    InOut(Family),
199    /// [`Out`](Self::Out) over the first half, [`In`](Self::In) over the
200    /// second: fast at both ends.
201    OutIn(Family),
202    /// `n` equal jumps, each at the end of its step, as CSS's
203    /// `steps(n, jump-end)`. `Steps(0)` acts as `Steps(1)`, which holds the
204    /// start until the end.
205    Steps(u16),
206}
207
208impl Easing for Ease {
209    fn ease(&self, p: i32) -> i32 {
210        let p = i64::from(p.clamp(0, ONE));
211        let wide = match *self {
212            Ease::Linear => i128::from(p),
213            Ease::Smooth => smooth(p).units(),
214            Ease::Steps(n) => steps(n, p).units(),
215            Ease::In(family) => rise(family, p, false).units(),
216            Ease::Out(family) => i128::from(WIDE) - rise(family, WIDE - p, false).units(),
217            // Both halves are one curve about the middle, `side` saying
218            // which half `p` is in (0 at the middle, where both give HALF).
219            Ease::InOut(family) => {
220                let (side, off) = (i128::from((p - HALF).signum()), (p - HALF).abs());
221                let near = rise(family, 2 * (HALF - off), true).half().units();
222                i128::from(HALF) + side * (i128::from(HALF) - near)
223            }
224            Ease::OutIn(family) => {
225                let (side, off) = (i128::from((p - HALF).signum()), (p - HALF).abs());
226                i128::from(HALF) + side * rise(family, 2 * off, false).half().units()
227            }
228        };
229        narrow(wide)
230    }
231}
232
233/// A curve's value, `wide`, as an `i32`. Every curve stays between -1 and 2
234/// of [`ONE`].
235#[expect(
236    clippy::cast_possible_truncation,
237    reason = "every curve stays between -65 536 and 131 072"
238)]
239const fn narrow(wide: i128) -> i32 {
240    wide as i32
241}
242
243/// A value as an exact fraction, `num / den` with `den` above 0, so that a
244/// curve rounds once, at the end.
245#[derive(Clone, Copy)]
246struct Frac {
247    /// The numerator.
248    num: i128,
249    /// The denominator, above 0.
250    den: i128,
251}
252
253impl Frac {
254    /// `num / den`.
255    const fn new(num: i128, den: i128) -> Self {
256        Self { num, den }
257    }
258
259    /// Half of it.
260    const fn half(self) -> Self {
261        Self::new(self.num, self.den * 2)
262    }
263
264    /// It in 1/65536, rounded to the nearest, halves up.
265    fn units(self) -> i128 {
266        round_div(self.num * i128::from(WIDE), self.den)
267    }
268}
269
270/// `num / den` rounded to the nearest, halves up, for `den` above 0.
271fn round_div(num: i128, den: i128) -> i128 {
272    (2 * num + den).div_euclid(2 * den)
273}
274
275/// Smoothstep at `p` (0 to [`ONE`]): `3p² - 2p³`.
276fn smooth(p: i64) -> Frac {
277    let (p, one) = (i128::from(p), i128::from(WIDE));
278    Frac::new(3 * p * p * one - 2 * p * p * p, one * one * one)
279}
280
281/// `n` steps at `p` (0 to [`ONE`]): the steps begun, of `n`.
282fn steps(n: u16, p: i64) -> Frac {
283    let n = i128::from(n.max(1));
284    Frac::new(i128::from(p) * n / i128::from(WIDE), n)
285}
286
287/// A family's `In` curve at `q` (0 to [`ONE`]). `in_out` takes the
288/// constants easings.net's `InOut` has for `Back` and `Elastic`.
289fn rise(family: Family, q: i64, in_out: bool) -> Frac {
290    let (x, one) = (i128::from(q), i128::from(WIDE));
291    match family {
292        // Easings.net sets both to 0 at 0, where their formulas aren't.
293        Family::Expo | Family::Elastic if q == 0 => Frac::new(0, 1),
294        // 1 - cos(x PI/2) = 1 - sin((1 - x) PI/2).
295        Family::Sine => {
296            let s = quarter_sine((WIDE - q) * (QUARTER / WIDE));
297            Frac::new(s.den - s.num, s.den)
298        }
299        Family::Quad => Frac::new(x.pow(2), one.pow(2)),
300        Family::Cubic => Frac::new(x.pow(3), one.pow(3)),
301        Family::Quart => Frac::new(x.pow(4), one.pow(4)),
302        Family::Quint => Frac::new(x.pow(5), one.pow(5)),
303        // 2^(10x - 10).
304        Family::Expo => exp2_down(10 * (WIDE - q)),
305        // 1 - sqrt(1 - x²), the root taken in 1/2^32.
306        Family::Circ => {
307            let under = (u128::from(1_u64 << 32) - u128::from(q.unsigned_abs().pow(2))) << 32;
308            let root = i128::try_from(under.isqrt()).unwrap_or(0);
309            Frac::new((1 << 32) - root, 1 << 32)
310        }
311        // (c + 1)x³ - c x².
312        Family::Back => {
313            let (c, d) = if in_out { BACK_IN_OUT } else { BACK };
314            Frac::new((d + c) * x.pow(3) - c * x.pow(2) * one, d * one.pow(3))
315        }
316        // -2^(10x - 10) sin((10x - 10.75) 2PI/3); in and out,
317        // sin((20x - 22.25) 2PI/9) in 1/2^24 of a turn. At 1 the angle is
318        // exactly a quarter turn back, so it is exactly 1 there.
319        Family::Elastic => {
320            let (k, start, per) = if in_out {
321                (20, 1_458_176, 9)
322            } else {
323                (10, 704_512, 3)
324            };
325            let angle = round_div(i128::from(k * q - start) * i128::from(TURN / WIDE), per);
326            let grow = exp2_down(10 * (WIDE - q));
327            let sine = sine_turn(i64::try_from(angle).unwrap_or(0));
328            Frac::new(-grow.num * sine.num, grow.den * sine.den)
329        }
330        // 1 - easeOutBounce(1 - x).
331        Family::Bounce => bounce_down(WIDE - q),
332    }
333}
334
335/// `1 - easeOutBounce(y)`, `y` being `u` (0 to [`ONE`]). With
336/// `7.5625 = 121/16` and `2.75 = 11/4`, each arc is
337/// `(22u - a)² / 64 + k` in 1/ONE², for `a` in `0, 12, 18, 21` ONE and `k`
338/// in `0, 3/4, 15/16, 63/64`.
339fn bounce_down(u: i64) -> Frac {
340    let (u, one) = (i128::from(u), i128::from(WIDE));
341    // The arcs start at 4/11, 8/11 and 10/11, which no `u` falls on.
342    let (a, k) = match 11 * u / one {
343        0..=3 => (0, 0),
344        4..=7 => (12, 48),
345        8 | 9 => (18, 60),
346        _ => (21, 63),
347    };
348    let arc = (22 * u - a * one).pow(2);
349    Frac::new((64 - k) * one * one - arc, 64 * one * one)
350}
351
352/// `2^(-e)`, `e` being `down` in 1/65536 (0 or more), from the table of
353/// `2^x`, in 1/2^24 before the halvings.
354fn exp2_down(down: i64) -> Frac {
355    let (whole, part) = (down >> 16, down & (WIDE - 1));
356    // 2^(-whole - part) = 2^(1 - part) / 2^(whole + 1).
357    let at = WIDE - part;
358    let index = usize::try_from(at >> 8).unwrap_or(0);
359    let between = at & 0xff;
360    let next = EXP2[(index + 1).min(256)];
361    let value = EXP2[index] * 256 + (next - EXP2[index]) * between;
362    Frac::new(i128::from(value), 1_i128 << (24 + whole + 1))
363}
364
365/// The sine of `angle`, in 1/2^24 of a turn, any angle.
366fn sine_turn(angle: i64) -> Frac {
367    let angle = angle.rem_euclid(TURN);
368    let into = angle % QUARTER;
369    // Up the quarter in the first and third quarters, down it in the others.
370    let s = if (angle / QUARTER) % 2 == 0 {
371        quarter_sine(into)
372    } else {
373        quarter_sine(QUARTER - into)
374    };
375    // Below 0 in the second half of the turn.
376    if angle >= 2 * QUARTER {
377        Frac::new(-s.num, s.den)
378    } else {
379        s
380    }
381}
382
383/// The sine of `angle`, 0 to a quarter of a turn in 1/2^24 of a turn
384/// (0 to 2^22), from the table, in 1/2^30.
385fn quarter_sine(angle: i64) -> Frac {
386    let index = usize::try_from(angle >> 14).unwrap_or(0);
387    let between = angle & 0x3fff;
388    let next = QUARTER_SINE[(index + 1).min(256)];
389    let value = QUARTER_SINE[index] * (1 << 14) + (next - QUARTER_SINE[index]) * between;
390    Frac::new(i128::from(value), 1 << 30)
391}
392
393/// A type that goes between two of its values.
394///
395/// `mix(from, to, p)` is `from + (to - from) * p / ONE`, rounded to the
396/// nearest with halves up: exactly `from` at 0 and exactly `to` at [`ONE`].
397/// Past them (an overshoot) it goes on in a line, and stops at the type's
398/// bounds: a `u8`, a colour's channel, stops at 0 and 255.
399///
400/// It is not called `lerp`, because `shuyi_ui`'s `Rgb` has an inherent
401/// `lerp` in `f32`.
402pub trait Lerp: Copy {
403    /// The value `p` (in 1/65536) of the way from `from` to `to`.
404    #[must_use]
405    fn mix(from: Self, to: Self, p: i32) -> Self;
406}
407
408/// `from + (to - from) * p / ONE`, rounded to the nearest with halves up.
409fn mix_wide(from: i128, to: i128, p: i32) -> i128 {
410    from + round_div((to - from) * i128::from(p), i128::from(WIDE))
411}
412
413/// `Lerp` for integers, stopping at their bounds.
414macro_rules! lerp_ints {
415    ($($int:ty),*) => {$(
416        impl Lerp for $int {
417            fn mix(from: Self, to: Self, p: i32) -> Self {
418                let wide = mix_wide(from.into(), to.into(), p);
419                <$int>::try_from(wide).unwrap_or(if wide < 0 { <$int>::MIN } else { <$int>::MAX })
420            }
421        }
422    )*};
423}
424
425lerp_ints!(i32, i64, u8, u16, u32);
426
427impl Lerp for (i32, i32) {
428    fn mix(from: Self, to: Self, p: i32) -> Self {
429        (i32::mix(from.0, to.0, p), i32::mix(from.1, to.1, p))
430    }
431}
432
433/// How many times a [`Tween`] plays, in all.
434#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
435#[non_exhaustive]
436pub enum Repeat {
437    /// Once.
438    #[default]
439    Once,
440    /// This many times in all; `Times(0)` acts as [`Once`](Self::Once).
441    Times(u16),
442    /// For ever.
443    Loop,
444}
445
446impl Repeat {
447    /// The plays in all, at least 1; `None` for [`Loop`](Self::Loop).
448    const fn plays(self) -> Option<u32> {
449        match self {
450            Repeat::Once | Repeat::Times(0) => Some(1),
451            Repeat::Times(n) => Some(n as u32),
452            Repeat::Loop => None,
453        }
454    }
455}
456
457/// A value going from `from` to `to` over `duration`, along a curve, after
458/// `delay`: drawn from the time since it started alone
459/// ([`value_at`](Self::value_at)). The [module docs](self) say how a
460/// simulation and a view keep one.
461///
462/// ```
463/// use shuyi_core::ease::Repeat;
464/// use shuyi_core::{Ease, Tween};
465///
466/// // A title that bobs 2 cells up and down for ever, 500 ms each way.
467/// const BOB: Tween<i32> = Tween::new(0, -2, 500)
468///     .with_ease(Ease::Smooth)
469///     .with_repeat(Repeat::Loop)
470///     .with_yoyo(true);
471/// assert_eq!(BOB.value_at(500), -2);
472/// assert_eq!(BOB.value_at(1000), 0);
473/// assert_eq!(BOB.value_at(1250), BOB.value_at(250));
474/// ```
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
476#[non_exhaustive]
477pub struct Tween<T, E = Ease> {
478    /// The value at the start.
479    pub from: T,
480    /// The value at the end.
481    pub to: T,
482    /// How long it holds `from` before its first play. 0 when left out.
483    #[serde(default)]
484    pub delay: u32,
485    /// How long one play lasts.
486    pub duration: u32,
487    /// Its curve. [`Ease::Linear`] when left out.
488    #[serde(default)]
489    pub ease: E,
490    /// How many times it plays. [`Repeat::Once`] when left out.
491    #[serde(default)]
492    pub repeat: Repeat,
493    /// Whether every second play runs from `to` back to `from`, its time
494    /// backwards, as CSS's `alternate`. `false` when left out.
495    #[serde(default)]
496    pub yoyo: bool,
497}
498
499impl<T: Copy> Tween<T> {
500    /// One play from `from` to `to` over `duration`, with no delay, along
501    /// [`Ease::Linear`].
502    pub const fn new(from: T, to: T, duration: u32) -> Self {
503        Self {
504            from,
505            to,
506            delay: 0,
507            duration,
508            ease: Ease::Linear,
509            repeat: Repeat::Once,
510            yoyo: false,
511        }
512    }
513
514    /// It along `ease`: an [`Ease`], or a curve of the game's own.
515    #[must_use]
516    pub const fn with_ease<F>(self, ease: F) -> Tween<T, F> {
517        Tween {
518            from: self.from,
519            to: self.to,
520            delay: self.delay,
521            duration: self.duration,
522            ease,
523            repeat: self.repeat,
524            yoyo: self.yoyo,
525        }
526    }
527}
528
529/// The builders are `const fn`s, so a game can keep a tween in a `const`.
530/// A `const fn` can't drop a part of a type that may need dropping, so they
531/// ask for `Copy`: every [`Lerp`] type and every [`Ease`] is. A tween along
532/// a curve that isn't `Copy` calls them before
533/// [`with_ease`](Tween::with_ease), or assigns its fields.
534impl<T: Copy, E: Copy> Tween<T, E> {
535    /// It after `delay`.
536    #[must_use]
537    pub const fn with_delay(self, delay: u32) -> Self {
538        Self { delay, ..self }
539    }
540
541    /// It played as `repeat` says.
542    #[must_use]
543    pub const fn with_repeat(self, repeat: Repeat) -> Self {
544        Self { repeat, ..self }
545    }
546
547    /// It back and forth.
548    #[must_use]
549    pub const fn with_yoyo(self, yoyo: bool) -> Self {
550        Self { yoyo, ..self }
551    }
552}
553
554impl<T, E> Tween<T, E> {
555    /// When its last play ends: `delay + duration * plays`, stopping at
556    /// `u32::MAX`. `None` for [`Repeat::Loop`].
557    pub const fn done_at(&self) -> Option<u32> {
558        match self.repeat.plays() {
559            Some(plays) => Some(
560                self.delay
561                    .saturating_add(self.duration.saturating_mul(plays)),
562            ),
563            None => None,
564        }
565    }
566
567    /// Whether it ended in the window after `prev` up to `now`: true when
568    /// `prev < d <= now`, `d` being [`done_at`](Self::done_at) and at least
569    /// 1, so a tween done at 0 ends in the first window that holds any time.
570    ///
571    /// A screen or a simulation passes the previous frame's or step's time
572    /// and this one's, starting from 0, and hears "it landed" once, with no
573    /// flag kept. Never true for [`Repeat::Loop`].
574    pub fn finished_between(&self, prev: u32, now: u32) -> bool {
575        self.done_at().is_some_and(|done| {
576            let done = done.max(1);
577            prev < done && done <= now
578        })
579    }
580}
581
582impl<T: Lerp, E: Easing> Tween<T, E> {
583    /// Its value `t` after it started: `from` before the delay, the eased
584    /// value during a play, and from [`done_at`](Self::done_at) on the
585    /// value its last play ended on: for every [`Ease`], `to`, or `from`
586    /// after an even number of plays with `yoyo`. With a `duration` of 0
587    /// it jumps to that value at the end of the delay; with
588    /// [`Repeat::Loop`] it then holds `to`.
589    pub fn value_at(&self, t: u32) -> T {
590        if t < self.delay {
591            return self.from;
592        }
593        if self.duration == 0 || self.done_at().is_some_and(|done| t >= done) {
594            return self.end();
595        }
596        let since = t - self.delay;
597        let play = since / self.duration;
598        let into = u64::from(since % self.duration);
599        let p = i32::try_from(into * u64::from(ONE.unsigned_abs()) / u64::from(self.duration))
600            .unwrap_or(ONE);
601        let p = if self.yoyo && play % 2 == 1 {
602            ONE - p
603        } else {
604            p
605        };
606        T::mix(self.from, self.to, self.ease.ease(p))
607    }
608
609    /// The value its last play ends on, through the curve: `to`, or `from`
610    /// after a play backwards, for every [`Ease`].
611    fn end(&self) -> T {
612        let p = match self.repeat.plays() {
613            Some(plays) if self.yoyo && plays % 2 == 0 => 0,
614            _ => ONE,
615        };
616        T::mix(self.from, self.to, self.ease.ease(p))
617    }
618}
619
620#[cfg(test)]
621mod tests;