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;