From 212391fc373fc95020547ff033071da7c00e3fec Mon Sep 17 00:00:00 2001 From: cepetr Date: Fri, 19 Apr 2024 09:38:06 +0200 Subject: [PATCH] feat(core): add pareen & easer crates [no changelog] --- core/embed/rust/Cargo.lock | 30 +- core/embed/rust/Cargo.toml | 15 +- rust/pareen/.gitignore | 13 + rust/pareen/CHANGELOG.md | 68 ++ rust/pareen/Cargo.toml | 34 + rust/pareen/LICENSE | 21 + rust/pareen/README.md | 81 +++ rust/pareen/examples/plots.rs | 96 +++ rust/pareen/images/plots.png | Bin 0 -> 32406 bytes rust/pareen/images/seq_ease_in_out.png | Bin 0 -> 12869 bytes rust/pareen/src/anim.rs | 841 +++++++++++++++++++++++++ rust/pareen/src/anim_box.rs | 51 ++ rust/pareen/src/anim_with_dur.rs | 191 ++++++ rust/pareen/src/arithmetic.rs | 169 +++++ rust/pareen/src/easer_combinators.rs | 233 +++++++ rust/pareen/src/lib.rs | 71 +++ rust/pareen/src/primitives.rs | 223 +++++++ rust/pareen/src/stats.rs | 133 ++++ 18 files changed, 2267 insertions(+), 3 deletions(-) create mode 100644 rust/pareen/.gitignore create mode 100644 rust/pareen/CHANGELOG.md create mode 100644 rust/pareen/Cargo.toml create mode 100644 rust/pareen/LICENSE create mode 100644 rust/pareen/README.md create mode 100644 rust/pareen/examples/plots.rs create mode 100644 rust/pareen/images/plots.png create mode 100644 rust/pareen/images/seq_ease_in_out.png create mode 100644 rust/pareen/src/anim.rs create mode 100644 rust/pareen/src/anim_box.rs create mode 100644 rust/pareen/src/anim_with_dur.rs create mode 100644 rust/pareen/src/arithmetic.rs create mode 100644 rust/pareen/src/easer_combinators.rs create mode 100644 rust/pareen/src/lib.rs create mode 100644 rust/pareen/src/primitives.rs create mode 100644 rust/pareen/src/stats.rs diff --git a/core/embed/rust/Cargo.lock b/core/embed/rust/Cargo.lock index de76974f67..114e08484e 100644 --- a/core/embed/rust/Cargo.lock +++ b/core/embed/rust/Cargo.lock @@ -94,6 +94,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "easer" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba524f8b83c9c5bde02c2bb1627de9d1f81980489a6d54168cdfd08c258f917" +dependencies = [ + "num-traits", +] + [[package]] name = "glob" version = "0.3.0" @@ -154,6 +163,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "memchr" version = "2.4.1" @@ -189,11 +204,20 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "pareen" +version = "0.3.3" +dependencies = [ + "easer", + "num-traits", ] [[package]] @@ -318,10 +342,12 @@ dependencies = [ "cc", "cstr_core", "cty", + "easer", "glob", "heapless", "num-derive", "num-traits", + "pareen", "qrcodegen", "serde_json", "spin", diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml index 26780a1fc8..30573b1af8 100644 --- a/core/embed/rust/Cargo.toml +++ b/core/embed/rust/Cargo.toml @@ -105,8 +105,9 @@ features = ["ufmt"] default_features = false [dependencies.num-traits] -version = "0.2.15" +version = "0.2.18" default_features = false +features = ["libm"] [dependencies.num-derive] version = "0.3.3" @@ -125,6 +126,18 @@ version = "0.2.2" [dependencies.unsize] version = "1.1.0" +[dependencies.pareen] +version = "0.3.3" +path = "../../../rust/pareen" +default-features = false +features = ["libm", "easer"] + +[dependencies.easer] +version = "0.3.0" +default-features = false +features = ["libm"] + + # Build dependencies [build-dependencies.bindgen] diff --git a/rust/pareen/.gitignore b/rust/pareen/.gitignore new file mode 100644 index 0000000000..bf05bc31f4 --- /dev/null +++ b/rust/pareen/.gitignore @@ -0,0 +1,13 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Vim +.*.swp diff --git a/rust/pareen/CHANGELOG.md b/rust/pareen/CHANGELOG.md new file mode 100644 index 0000000000..580dfd05fb --- /dev/null +++ b/rust/pareen/CHANGELOG.md @@ -0,0 +1,68 @@ +# Changelog + +## Version 0.3.3 (2023-08-01) + +- Add support for `no_std` + +## Version 0.3.2 (2023-06-03) + +- Bump version + +## Version 0.3.1 (2022-08-31) + +- Fix docs + +## Version 0.3.0 (2022-08-31) + +- Make operator overloading of `Mul`, `Add` and `Sub` more flexible. + This may break compilation in some cases, since types are more generic now. +- Add `AnimWithDur` for easier composition of animations that have a fixed duration +- Internal refactoring: split into multiple modules +- Implement `Anim::{fst,snd,copied}` +- Implement `AnimWithDur::{sum,mean}` and simple linear regression +- Implement `Anim::{into_fn,into_box_fn}` + +## Version 0.2.6 (2020-08-17) + +- Make exponential slowdown of compile times less likely +- Add `cycle` +- Add `quadratic` + +## Version 0.2.5 (2020-07-18) + +- Add `Anim::repeat`, `Anim::hold` and `Anim::seq_continue` +- Allow boxed animations +- Expose `easer` library + +## Version 0.2.4 (2020-07-16) + +Yanked. + +## Version 0.2.3 (2020-04-28) + +- Fix compilation issue on rustc 1.43.0 () + +## Version 0.2.1 (2020-01-18) + +- Implement `Anim::as_ref` + +## Version 0.2.0 (2020-01-13) + +- `squeeze` no longer switches to a default value outside of the given range. + Use `squeeze_and_surround` as a replacement. + +## Version 0.1.3 (2019-12-03) + +- Render README.md on crates.io + +## Version 0.1.2 (2019-12-02) + +- No change + +## Version 0.1.1 (2019-12-02) + +- Improve documentation + +## Version 0.1.0 (2019-12-02) + +- Initial version diff --git a/rust/pareen/Cargo.toml b/rust/pareen/Cargo.toml new file mode 100644 index 0000000000..59b265de36 --- /dev/null +++ b/rust/pareen/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "pareen" +version = "0.3.3" +authors = ["leod "] +edition = "2018" +license = "MIT" +description = "A small library for parameterized inbetweening" +homepage = "https://github.com/leod/pareen" +documentation = "https://docs.rs/pareen" +repository = "https://github.com/leod/pareen" +keywords = ["animation", "combinators", "tween", "easing"] +readme = "README.md" +exclude = [".github"] + +[features] +default = ["std"] +std = ["alloc", "num-traits/std"] +alloc = [] +libm = ["num-traits/libm"] + +[dependencies] +num-traits = { version = "0.2", default-features = false } +easer = { version = "0.3.0", default-features = false, optional = true } + +[dev-dependencies] +assert_approx_eq = "1.0" +gnuplot = "0.0.32" + +[package.metadata.docs.rs] +features = ["std", "alloc", "libm", "easer"] + +[[example]] +name = "plots" +required-features = ["std"] diff --git a/rust/pareen/LICENSE b/rust/pareen/LICENSE new file mode 100644 index 0000000000..e9dd81ba11 --- /dev/null +++ b/rust/pareen/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 leod + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/rust/pareen/README.md b/rust/pareen/README.md new file mode 100644 index 0000000000..13b88add2a --- /dev/null +++ b/rust/pareen/README.md @@ -0,0 +1,81 @@ +# Pareen +[![Docs Status](https://docs.rs/pareen/badge.svg)](https://docs.rs/pareen) +[![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/leod/pareen/blob/master/LICENSE) +[![Crates.io](https://img.shields.io/crates/v/pareen.svg)](https://crates.io/crates/pareen) + +Pareen is a small Rust library for *par*ameterized inbetw*een*ing. +The intended application is in game programming, where you sometimes have +two discrete game states between which you want to transition smoothly for +visualization purposes. + +Pareen gives you tools for composing animations that are parameterized by +time (i.e. mappings from time to some animated value) without constantly +having to pass around time variables; it hides the plumbing, so that you +need to provide time only once: when evaluating the animation. + +Animations are composed similarly to Rust's iterators, so no memory +allocations are necessary. The downside to this is that it is diffcult to store +pareen's animations. The recommended approach is to construct and evaluate +animations on the fly. + +## Current Status +I consider `pareen` to be an experimental approach, and I'm not sure if I'm +still happy with it. Anyway, the integration of easing functions could use +some love. Contributions are very much welcome! + +Unfortunately, it looks like heavily nested animations can cause an exponential +slowdown in compilation time. For now, boxing intermediate animations serves as +a workaround, but this comes with a decrease of both readability and efficiency. + +## Examples +```rust +// An animation returning a constant value +let anim1 = pareen::constant(1.0f64); + +// Animations can be evaluated at any time +let value = anim1.eval(0.5); + +// Animations can be played in sequence +let anim2 = anim1.seq(0.7, pareen::prop(0.25) + 0.5); + +// Animations can be composed and transformed in various ways +let anim3 = anim2 + .lerp(pareen::circle().cos()) + .scale_min_max(5.0, 10.0) + .backwards(1.0) + .squeeze(3.0, 0.5..=1.0); + +let anim4 = pareen::cubic(&[1.0, 2.0, 3.0, 4.0]) - anim3; + +let value = anim4.eval(1.0); +``` + +### Easer +Optionally, you can enable support for integrating easing functions from +[`easer`](https://docs.rs/easer/0.2.1/easer/index.html). + +```rust +let first_anim = pareen::constant(2.0); +let second_anim = pareen::prop(1.0f32); + +// Transition from first_anim to second_anim at time 0.5, applying cubic easing +// for 0.3 time units. +let anim = first_anim.seq_ease_in_out( + 0.5, + easer::functions::Cubic, + 0.3, + second_anim, +); +``` + +### Plots +There is an example that shows some animations as plots via +[RustGnuplot](https://github.com/SiegeLord/RustGnuplot) in +[examples/plots.rs](examples/plots.rs). Given that `gnuplot` has been +installed, it can be executed like this: +```bash +cargo run --example plots --feature easer +``` + +If everything works, you should see something like this: +![plots of the example/plots.rs animations](images/plots.png) diff --git a/rust/pareen/examples/plots.rs b/rust/pareen/examples/plots.rs new file mode 100644 index 0000000000..3f9743934b --- /dev/null +++ b/rust/pareen/examples/plots.rs @@ -0,0 +1,96 @@ +use gnuplot::{AutoOption, AxesCommon, Color, Figure, LineWidth}; + +fn main() { + let mut plots = Plots { plots: Vec::new() }; + + plots.add("id", pareen::id()); + plots.add("lerp between 2 and 4", pareen::lerp(2.0, 4.0)); + plots.add( + "dynamic lerp between sin^2 and cos", + pareen::circle().sin().powi(2).lerp(pareen::circle().cos()), + ); + plots.add( + "dynamic lerp, squeezed into [0.5 .. 1]", + pareen::circle() + .sin() + .powi(2) + .lerp(pareen::circle().cos()) + .squeeze_and_surround(0.5..=1.0, 0.0), + ); + plots.add( + "switch from 1 to 2 at time=0.5", + pareen::constant(1.0).switch(0.5, 2.0), + ); + + #[cfg(feature = "easer")] + plots.add( + "ease transition from 2 to a proportional anim", + pareen::constant(2.0).seq_ease_in_out( + 0.5, + easer::functions::Cubic, + 0.3, + pareen::prop(1.0f32), + ), + ); + + plots.show_gnuplot(); +} + +fn sample( + n: usize, + max_t: f32, + anim: pareen::Anim>, +) -> (Vec, Vec) { + let mut ts = Vec::new(); + let mut vs = Vec::new(); + + for i in 0..=n { + let time = i as f32 / n as f32 * max_t; + let value = anim.eval(time); + + ts.push(time); + vs.push(value); + } + + (ts, vs) +} + +struct Plot { + name: &'static str, + ts: Vec, + vs: Vec, +} + +struct Plots { + plots: Vec, +} + +impl Plots { + fn add(&mut self, name: &'static str, anim: pareen::Anim>) { + let (ts, vs) = sample(1000, 1.0, anim); + + self.plots.push(Plot { name, ts, vs }); + } + + fn show_gnuplot(&self) { + let mut figure = Figure::new(); + + // Show plots in a square rows/columns layout + let n_cols = (self.plots.len() as f32).sqrt() as u32; + let n_rows = (self.plots.len() as f32).sqrt().ceil() as u32; + + for (i, plot) in self.plots.iter().enumerate() { + figure + .axes2d() + .lines(&plot.ts, &plot.vs, &[Color("blue"), LineWidth(3.0)]) + .set_title(&plot.name, &[]) + .set_x_label("time", &[]) + .set_y_label("value", &[]) + .set_x_ticks(Some((AutoOption::Fix(0.5), 0)), &[], &[]) + .set_y_ticks(Some((AutoOption::Fix(0.5), 0)), &[], &[]) + .set_pos_grid(n_rows, n_cols, i as u32); + } + + figure.show().unwrap(); + } +} diff --git a/rust/pareen/images/plots.png b/rust/pareen/images/plots.png new file mode 100644 index 0000000000000000000000000000000000000000..8b0754b2c59fa909b48af619aa8255f991213cf9 GIT binary patch literal 32406 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 g(&XcUW1UO5ri=>x>&+njpy*fB%Hw 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 literal 0 HcmV?d00001 diff --git a/rust/pareen/images/seq_ease_in_out.png b/rust/pareen/images/seq_ease_in_out.png new file mode 100644 index 0000000000000000000000000000000000000000..96f847a0e69b106a4f3e1eb117403728b5b2bcd8 GIT binary patch literal 12869 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 za55bE V` +/// is so that the library works in stable rust. Having this type allows us to +/// implement e.g. `std::ops::Add` for [`Anim`](struct.Anim.html) where +/// `F: Fun`. Without this trait, it becomes difficult (impossible?) to provide +/// a name for `Add::Output`, unless you have the unstable feature +/// `type_alias_impl_trait` or `fn_traits`. +/// +/// In contrast to `std::ops::FnOnce`, both input _and_ output are associated +/// types of `Fun`. The main reason is that this makes types smaller for the +/// user of the library. I have not observed any downsides to this yet. +pub trait Fun { + /// The function's input type. Usually time. + type T; + + /// The function's output type. + type V; + + /// Evaluate the function at time `t`. + fn eval(&self, t: Self::T) -> Self::V; +} + +impl<'a, F> Fun for &'a F +where + F: Fun, +{ + type T = F::T; + type V = F::V; + + fn eval(&self, t: Self::T) -> Self::V { + (*self).eval(t) + } +} + +/// `Anim` is the main type provided by pareen. It is a wrapper around any type +/// implementing [`Fun`](trait.Fun.html). +/// +/// `Anim` provides methods that transform or compose animations, allowing +/// complex animations to be created out of simple pieces. +#[derive(Clone, Debug)] +pub struct Anim(pub F); + +impl Anim +where + F: Fun, +{ + /// Evaluate the animation at time `t`. + pub fn eval(&self, t: F::T) -> F::V { + self.0.eval(t) + } + + /// Transform an animation so that it applies a given function to its + /// values. + /// + /// # Example + /// + /// Turn `(2.0 * t)` into `(2.0 * t).sqrt() + 2.0 * t`: + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// let anim = pareen::prop(2.0f32).map(|value| value.sqrt() + value); + /// + /// assert_approx_eq!(anim.eval(1.0), 2.0f32.sqrt() + 2.0); + /// ``` + pub fn map(self, f: impl Fn(F::V) -> W) -> Anim> { + self.map_anim(fun(f)) + } + + /// Transform an animation so that it modifies time according to the given + /// function before evaluating the animation. + /// + /// # Example + /// Run an animation two times slower: + /// ``` + /// let anim = pareen::cubic(&[1.0, 1.0, 1.0, 1.0]); + /// let slower_anim = anim.map_time(|t: f32| t / 2.0); + /// ``` + pub fn map_time(self, f: impl Fn(S) -> F::T) -> Anim> { + fun(f).map_anim(self) + } + + /// Converts from `Anim` to `Anim<&F>`. + pub fn as_ref(&self) -> Anim<&F> { + Anim(&self.0) + } + + pub fn map_anim(self, anim: A) -> Anim> + where + G: Fun, + A: Into>, + { + // Nested closures result in exponential compilation time increase, and we + // expect map_anim to be used often. Thus, we avoid using `pareen::fun` here. + // For reference: https://github.com/rust-lang/rust/issues/72408 + Anim(MapClosure(self.0, anim.into().0)) + } + + pub fn map_time_anim(self, anim: A) -> Anim> + where + G: Fun, + A: Into>, + { + anim.into().map_anim(self) + } + + pub fn into_fn(self) -> impl Fn(F::T) -> F::V { + move |t| self.eval(t) + } +} + +impl Anim +where + F: Fun, + F::T: Clone + Mul, +{ + pub fn scale_time(self, t_scale: F::T) -> Anim> { + self.map_time(move |t| t * t_scale.clone()) + } +} + +impl Fun for Option +where + F: Fun, +{ + type T = F::T; + type V = Option; + + fn eval(&self, t: F::T) -> Option { + self.as_ref().map(|f| f.eval(t)) + } +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct MapClosure(F, G); + +impl Fun for MapClosure +where + F: Fun, + G: Fun, +{ + type T = F::T; + type V = G::V; + + fn eval(&self, t: F::T) -> G::V { + self.1.eval(self.0.eval(t)) + } +} + +impl Anim +where + F: Fun, +{ + pub fn fst(self) -> Anim> { + self.map(|(x, _)| x) + } + + pub fn snd(self) -> Anim> { + self.map(|(_, y)| y) + } +} + +impl Anim +where + F: Fun, + F::T: Clone, +{ + /// Combine two animations into one, yielding an animation having pairs as + /// the values. + pub fn zip(self, other: A) -> Anim> + where + G: Fun, + A: Into>, + { + // Nested closures result in exponential compilation time increase, and we + // expect zip to be used frequently. Thus, we avoid using `pareen::fun` here. + // For reference: https://github.com/rust-lang/rust/issues/72408 + Anim(ZipClosure(self.0, other.into().0)) + } + + pub fn bind(self, f: impl Fn(F::V) -> Anim) -> Anim> + where + G: Fun, + { + fun(move |t: F::T| f(self.eval(t.clone())).eval(t)) + } +} + +impl<'a, T, V, F> Anim +where + V: 'a + Copy, + F: Fun + 'a, +{ + pub fn copied(self) -> Anim + 'a> { + self.map(|x| *x) + } +} + +impl<'a, T, V, F> Anim +where + V: 'a + Clone, + F: Fun + 'a, +{ + pub fn cloned(self) -> Anim + 'a> { + self.map(|x| x.clone()) + } +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct ZipClosure(F, G); + +impl Fun for ZipClosure +where + F: Fun, + F::T: Clone, + G: Fun, +{ + type T = F::T; + type V = (F::V, G::V); + + fn eval(&self, t: F::T) -> Self::V { + (self.0.eval(t.clone()), self.1.eval(t)) + } +} + +impl Anim +where + F: Fun, + F::T: Copy + Sub, +{ + /// Shift an animation in time, so that it is moved to the right by `t_delay`. + pub fn shift_time(self, t_delay: F::T) -> Anim> { + (id::() - t_delay).map_anim(self) + } +} + +impl Anim +where + F: Fun, + F::T: Clone + PartialOrd, +{ + /// Concatenate `self` with another animation in time, using `self` until + /// time `self_end` (non-inclusive), and then switching to `next`. + /// + /// # Examples + /// Switch from one constant value to another: + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// let anim = pareen::constant(1.0f32).switch(0.5f32, 2.0); + /// + /// assert_approx_eq!(anim.eval(0.0), 1.0); + /// assert_approx_eq!(anim.eval(0.5), 2.0); + /// assert_approx_eq!(anim.eval(42.0), 2.0); + /// ``` + /// + /// Piecewise combinations of functions: + /// ``` + /// let cubic_1 = pareen::cubic(&[4.4034, 0.0, -4.5455e-2, 0.0]); + /// let cubic_2 = pareen::cubic(&[-1.2642e1, 2.0455e1, -8.1364, 1.0909]); + /// let cubic_3 = pareen::cubic(&[1.6477e1, -4.9432e1, 4.7773e1, -1.3818e1]); + /// + /// // Use cubic_1 for [0.0, 0.4), cubic_2 for [0.4, 0.8) and + /// // cubic_3 for [0.8, ..). + /// let anim = cubic_1.switch(0.4, cubic_2).switch(0.8, cubic_3); + /// ``` + pub fn switch(self, self_end: F::T, next: A) -> Anim> + where + G: Fun, + A: Into>, + { + // Nested closures result in exponential compilation time increase, and we + // expect switch to be used frequently. Thus, we avoid using `pareen::fun` here. + // For reference: https://github.com/rust-lang/rust/issues/72408 + cond(switch_cond(self_end), self, next) + } + + /// Play `self` in time range `range`, and `surround` outside of the time range. + /// + /// # Examples + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// let anim = pareen::constant(10.0f32).surround(2.0..=5.0, 20.0); + /// + /// assert_approx_eq!(anim.eval(0.0), 20.0); + /// assert_approx_eq!(anim.eval(2.0), 10.0); + /// assert_approx_eq!(anim.eval(4.0), 10.0); + /// assert_approx_eq!(anim.eval(5.0), 10.0); + /// assert_approx_eq!(anim.eval(6.0), 20.0); + /// ``` + pub fn surround( + self, + range: RangeInclusive, + surround: A, + ) -> Anim> + where + G: Fun, + A: Into>, + { + // Nested closures result in exponential compilation time increase, and we + // expect surround to be used frequently. Thus, we avoid using `pareen::fun` here. + // For reference: https://github.com/rust-lang/rust/issues/72408 + cond(surround_cond(range), self, surround) + } +} + +fn switch_cond(self_end: T) -> Anim> { + fun(move |t| t < self_end) +} + +fn surround_cond(range: RangeInclusive) -> Anim> { + fun(move |t| range.contains(&t)) +} + +impl Anim +where + F: Fun, + F::T: Clone + PartialOrd, + F::V: Clone, +{ + /// Play `self` until time `self_end`, then always return the value of + /// `self` at time `self_end`. + pub fn hold(self, self_end: F::T) -> Anim> { + let end_value = self.eval(self_end.clone()); + + self.switch(self_end, constant(end_value)) + } +} + +impl Anim +where + F: Fun, + F::T: Copy + PartialOrd + Sub, +{ + /// Play two animations in sequence, first playing `self` until time + /// `self_end` (non-inclusive), and then switching to `next`. Note that + /// `next` will see time starting at zero once it plays. + /// + /// # Example + /// Stay at value `5.0` for ten seconds, then increase value proportionally: + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// let anim_1 = pareen::constant(5.0f32); + /// let anim_2 = pareen::prop(2.0f32) + 5.0; + /// let anim = anim_1.seq(10.0, anim_2); + /// + /// assert_approx_eq!(anim.eval(0.0), 5.0); + /// assert_approx_eq!(anim.eval(10.0), 5.0); + /// assert_approx_eq!(anim.eval(11.0), 7.0); + /// ``` + pub fn seq(self, self_end: F::T, next: A) -> Anim> + where + G: Fun, + A: Into>, + { + self.switch(self_end.clone(), next.into().shift_time(self_end)) + } + + pub fn seq_continue( + self, + self_end: F::T, + next_fn: H, + ) -> Anim> + where + G: Fun, + A: Into>, + H: Fn(F::V) -> A, + { + let next = next_fn(self.eval(self_end.clone())).into(); + + self.seq(self_end, next) + } +} + +impl Anim +where + F: Fun, + F::T: Clone + Sub, +{ + /// Play an animation backwards, starting at time `end`. + /// + /// # Example + /// + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// let anim = pareen::prop(2.0f32).backwards(1.0); + /// + /// assert_approx_eq!(anim.eval(0.0f32), 2.0); + /// assert_approx_eq!(anim.eval(1.0f32), 0.0); + /// ``` + pub fn backwards(self, end: F::T) -> Anim> { + (constant(end) - id()).map_anim(self) + } +} + +impl Anim +where + F: Fun, + F::T: Copy, + F::V: Copy + Num, +{ + /// Given animation values in `[0.0 .. 1.0]`, this function transforms the + /// values so that they are in `[min .. max]`. + /// + /// # Example + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// let min = -3.0f32; + /// let max = 10.0; + /// let anim = pareen::id().scale_min_max(min, max); + /// + /// assert_approx_eq!(anim.eval(0.0f32), min); + /// assert_approx_eq!(anim.eval(1.0f32), max); + /// ``` + pub fn scale_min_max(self, min: F::V, max: F::V) -> Anim> { + self * (max.clone() - min) + min + } +} + +#[cfg(any(feature = "std", feature = "libm"))] +impl Anim +where + F: Fun, + F::V: Float, +{ + /// Apply `Float::sin` to the animation values. + pub fn sin(self) -> Anim> { + self.map(Float::sin) + } + + /// Apply `Float::cos` to the animation values. + pub fn cos(self) -> Anim> { + self.map(Float::cos) + } + + /// Apply `Float::powf` to the animation values. + pub fn powf(self, e: F::V) -> Anim> { + self.map(move |v| v.powf(e)) + } +} + +impl Anim +where + F: Fun, + F::V: FloatCore, +{ + /// Apply `FloatCore::abs` to the animation values. + pub fn abs(self) -> Anim> { + self.map(FloatCore::abs) + } + + /// Apply `FloatCore::powi` to the animation values. + pub fn powi(self, n: i32) -> Anim> { + self.map(move |v| v.powi(n)) + } +} + +impl Anim +where + F: Fun, + F::T: Copy + FloatCore, +{ + /// Transform an animation in time, so that its time `[0 .. 1]` is shifted + /// and scaled into the given `range`. + /// + /// In other words, this function can both delay and speed up or slow down a + /// given animation. + /// + /// # Example + /// + /// Go from zero to 2π in half a second: + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// // From zero to 2π in one second + /// let angle = pareen::circle::(); + /// + /// // From zero to 2π in time range [0.5 .. 1.0] + /// let anim = angle.squeeze(0.5..=1.0); + /// + /// assert_approx_eq!(anim.eval(0.5), 0.0); + /// assert_approx_eq!(anim.eval(1.0), std::f32::consts::PI * 2.0); + /// ``` + pub fn squeeze(self, range: RangeInclusive) -> Anim> { + let time_shift = *range.start(); + let time_scale = F::T::one() / (*range.end() - *range.start()); + + self.map_time(move |t| (t - time_shift) * time_scale) + } + + /// Transform an animation in time, so that its time `[0 .. 1]` is shifted + /// and scaled into the given `range`. + /// + /// In other words, this function can both delay and speed up or slow down a + /// given animation. + /// + /// For time outside of the given `range`, the `surround` animation is used + /// instead. + /// + /// # Example + /// + /// Go from zero to 2π in half a second: + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// // From zero to 2π in one second + /// let angle = pareen::circle(); + /// + /// // From zero to 2π in time range [0.5 .. 1.0] + /// let anim = angle.squeeze_and_surround(0.5..=1.0, 42.0); + /// + /// assert_approx_eq!(anim.eval(0.0f32), 42.0f32); + /// assert_approx_eq!(anim.eval(0.5), 0.0); + /// assert_approx_eq!(anim.eval(1.0), std::f32::consts::PI * 2.0); + /// assert_approx_eq!(anim.eval(1.1), 42.0); + /// ``` + pub fn squeeze_and_surround( + self, + range: RangeInclusive, + surround: A, + ) -> Anim> + where + G: Fun, + A: Into>, + { + self.squeeze(range.clone()).surround(range, surround) + } + + /// Play two animations in sequence, first playing `self` until time + /// `self_end` (non-inclusive), and then switching to `next`. The animations + /// are squeezed in time so that they fit into `[0 .. 1]` together. + /// + /// `self` is played in time `[0 .. self_end)`, and then `next` is played + /// in time [self_end .. 1]`. + pub fn seq_squeeze(self, self_end: F::T, next: A) -> Anim> + where + G: Fun, + A: Into>, + { + let first = self.squeeze(Zero::zero()..=self_end); + let second = next.into().squeeze(self_end..=One::one()); + + first.switch(self_end, second) + } + + /// Repeat an animation forever. + pub fn repeat(self, period: F::T) -> Anim> { + self.map_time(move |t: F::T| (t * period.recip()).fract() * period) + } +} + +impl Anim +where + F: Fun, + F::T: Copy + Mul, + F::V: Copy + Add + Sub, +{ + /// Linearly interpolate between two animations, starting at time zero and + /// finishing at time one. + /// + /// # Examples + /// + /// Linearly interpolate between two constant values: + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// let anim = pareen::constant(5.0f32).lerp(10.0); + /// + /// assert_approx_eq!(anim.eval(0.0f32), 5.0); + /// assert_approx_eq!(anim.eval(0.5), 7.5); + /// assert_approx_eq!(anim.eval(1.0), 10.0); + /// assert_approx_eq!(anim.eval(2.0), 15.0); + /// ``` + #[cfg_attr(any(feature = "std", feature = "libm"), doc = r##" +It is also possible to linearly interpolate between two non-constant +animations: +``` +let anim = pareen::circle().sin().lerp(pareen::circle().cos()); +let value: f32 = anim.eval(0.5f32); +``` + "##)] + pub fn lerp(self, other: A) -> Anim> + where + G: Fun, + A: Into>, + { + let other = other.into(); + + fun(move |t| { + let a = self.eval(t); + let b = other.eval(t); + + let delta = b - a; + + a + t * delta + }) + } +} + +impl Anim +where + F: Fun>, + F::T: Clone, +{ + /// Unwrap an animation of optional values. + /// + /// At any time, returns the animation value if it is not `None`, or the + /// given `default` value otherwise. + /// + /// # Examples + /// + /// ``` + /// let anim1 = pareen::constant(Some(42)).unwrap_or(-1); + /// assert_eq!(anim1.eval(2), 42); + /// assert_eq!(anim1.eval(3), 42); + /// ``` + /// + /// ``` + /// let cond = pareen::fun(|t| t % 2 == 0); + /// let anim1 = pareen::cond(cond, Some(42), None).unwrap_or(-1); + /// assert_eq!(anim1.eval(2), 42); + /// assert_eq!(anim1.eval(3), -1); + /// ``` + pub fn unwrap_or(self, default: A) -> Anim> + where + G: Fun, + A: Into>, + { + self.zip(default.into()) + .map(|(v, default)| v.unwrap_or(default)) + } + + /// Applies a function to the contained value (if any), or returns the + /// provided default (if not). + /// + /// Note that the function `f` itself returns an animation. + /// + /// # Example + /// + /// Animate a player's position offset if it is moving: + /// ``` + /// # use assert_approx_eq::assert_approx_eq; + /// fn my_offset_anim( + /// move_dir: Option, + /// ) -> pareen::Anim> { + /// let move_speed = 2.0f32; + /// + /// pareen::constant(move_dir).map_or( + /// 0.0, + /// move |move_dir| pareen::prop(move_dir) * move_speed, + /// ) + /// } + /// + /// let move_anim = my_offset_anim(Some(1.0)); + /// let stay_anim = my_offset_anim(None); + /// + /// assert_approx_eq!(move_anim.eval(0.5), 1.0); + /// assert_approx_eq!(stay_anim.eval(0.5), 0.0); + /// ``` + pub fn map_or( + self, + default: A, + f: impl Fn(V) -> Anim, + ) -> Anim> + where + G: Fun, + H: Fun, + A: Into>, + { + let default = default.into(); + + //self.bind(move |v| v.map_or(default, f)) + + fun(move |t: F::T| { + self.eval(t.clone()) + .map_or_else(|| default.eval(t.clone()), |v| f(v).eval(t.clone())) + }) + } +} + +/// Return the value of one of two animations depending on a condition. +/// +/// This allows returning animations of different types conditionally. +/// +/// Note that the condition `cond` may either be a value `true` and `false`, or +/// it may itself be a dynamic animation of type `bool`. +/// +/// For dynamic conditions, in many cases it suffices to use either +/// [`Anim::switch`](struct.Anim.html#method.switch) or +/// [`Anim::seq`](struct.Anim.html#method.seq) instead of this function. +/// +/// # Examples +/// ## Constant conditions +/// +/// The following example does _not_ compile, because the branches have +/// different types: +/// ```compile_fail +/// let cond = true; +/// let anim = if cond { pareen::constant(1) } else { pareen::id() }; +/// ``` +/// +/// However, this does compile: +/// ``` +/// let cond = true; +/// let anim = pareen::cond(cond, 1, pareen::id()); +/// +/// assert_eq!(anim.eval(2), 1); +/// ``` +/// +/// ## Dynamic conditions +/// +/// ``` +/// let cond = pareen::fun(|t| t * t <= 4); +/// let anim_1 = 1; +/// let anim_2 = pareen::id(); +/// let anim = pareen::cond(cond, anim_1, anim_2); +/// +/// assert_eq!(anim.eval(1), 1); // 1 * 1 <= 4 +/// assert_eq!(anim.eval(2), 1); // 2 * 2 <= 4 +/// assert_eq!(anim.eval(3), 3); // 3 * 3 > 4 +/// ``` +pub fn cond(cond: Cond, a: A, b: B) -> Anim> +where + F::T: Clone, + F: Fun, + G: Fun, + H: Fun, + Cond: Into>, + A: Into>, + B: Into>, +{ + // Nested closures result in exponential compilation time increase, and we + // expect cond to be used often. Thus, we avoid using `pareen::fun` here. + // For reference: https://github.com/rust-lang/rust/issues/72408 + Anim(CondClosure(cond.into().0, a.into().0, b.into().0)) +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct CondClosure(F, G, H); + +impl Fun for CondClosure +where + F::T: Clone, + F: Fun, + G: Fun, + H: Fun, +{ + type T = F::T; + type V = G::V; + + fn eval(&self, t: F::T) -> G::V { + if self.0.eval(t.clone()) { + self.1.eval(t) + } else { + self.2.eval(t) + } + } +} + +/// Linearly interpolate between two animations, starting at time zero and +/// finishing at time one. +/// +/// This is a wrapper around [`Anim::lerp`](struct.Anim.html#method.lerp) for +/// convenience, allowing automatic conversion into [`Anim`](struct.Anim.html) +/// for both `a` and `b`. +/// +/// # Example +/// +/// Linearly interpolate between two constant values: +/// +/// ``` +/// # use assert_approx_eq::assert_approx_eq; +/// let anim = pareen::lerp(5.0f32, 10.0); +/// +/// assert_approx_eq!(anim.eval(0.0f32), 5.0); +/// assert_approx_eq!(anim.eval(0.5), 7.5); +/// assert_approx_eq!(anim.eval(1.0), 10.0); +/// assert_approx_eq!(anim.eval(2.0), 15.0); +/// ``` +pub fn lerp(a: A, b: B) -> Anim> +where + T: Copy + Mul, + V: Copy + Add + Sub, + F: Fun, + G: Fun, + A: Into>, + B: Into>, +{ + a.into().lerp(b.into()) +} + +/// Build an animation that depends on matching some expression. +/// +/// Importantly, this macro allows returning animations of a different type in +/// each match arm, which is not possible with a normal `match` expression. +/// +/// # Example +/// ``` +/// # use assert_approx_eq::assert_approx_eq; +/// enum MyPlayerState { +/// Standing, +/// Running, +/// Jumping, +/// } +/// +/// fn my_anim(state: MyPlayerState) -> pareen::Anim> { +/// pareen::anim_match!(state; +/// MyPlayerState::Standing => pareen::constant(0.0), +/// MyPlayerState::Running => pareen::prop(1.0), +/// MyPlayerState::Jumping => pareen::id().powi(2), +/// ) +/// } +/// +/// assert_approx_eq!(my_anim(MyPlayerState::Standing).eval(2.0), 0.0); +/// assert_approx_eq!(my_anim(MyPlayerState::Running).eval(2.0), 2.0); +/// assert_approx_eq!(my_anim(MyPlayerState::Jumping).eval(2.0), 4.0); +/// ``` +#[macro_export] +macro_rules! anim_match { + ( + $expr:expr; + $($pat:pat => $value:expr $(,)?)* + ) => { + $crate::fun(move |t| match $expr { + $( + $pat => ($crate::Anim::from($value)).eval(t), + )* + }) + } +} diff --git a/rust/pareen/src/anim_box.rs b/rust/pareen/src/anim_box.rs new file mode 100644 index 0000000000..6a0461a087 --- /dev/null +++ b/rust/pareen/src/anim_box.rs @@ -0,0 +1,51 @@ +use core::ops::{Deref, Sub}; +extern crate alloc; +use alloc::boxed::Box; + +use crate::{Anim, Fun}; + +pub type AnimBox = Anim>>; + +impl Anim +where + F: Fun + 'static, +{ + /// Returns a boxed version of this animation. + /// + /// This may be used to reduce the compilation time of deeply nested + /// animations. + pub fn into_box(self) -> AnimBox { + Anim(Box::new(self.0)) + } + + pub fn into_box_fn(self) -> Box F::V> { + Box::new(self.into_fn()) + } +} + +// TODO: We need to get rid of the 'static requirements. +impl Anim +where + F: Fun + 'static, + F::T: Copy + PartialOrd + Sub + 'static, + F::V: 'static, +{ + pub fn seq_box(self, self_end: F::T, next: A) -> AnimBox + where + G: Fun + 'static, + A: Into>, + { + self.into_box() + .seq(self_end, next.into().into_box()) + .into_box() + } +} + +impl<'a, T, V> Fun for Box> { + type T = T; + type V = V; + + fn eval(&self, t: Self::T) -> Self::V { + self.deref().eval(t) + } +} diff --git a/rust/pareen/src/anim_with_dur.rs b/rust/pareen/src/anim_with_dur.rs new file mode 100644 index 0000000000..42a5a71a48 --- /dev/null +++ b/rust/pareen/src/anim_with_dur.rs @@ -0,0 +1,191 @@ +use core::ops::{Add, Div, Mul, Sub}; + +use num_traits::{float::FloatCore, One}; + +use crate::{Anim, Fun}; + +/// An `Anim` together with the duration that it is intended to be played for. +/// +/// Explicitly carrying the duration around makes it easier to sequentially +/// compose animations in some places. +#[derive(Clone, Debug)] +pub struct AnimWithDur(pub Anim, pub F::T); + +impl Anim +where + F: Fun, +{ + /// Tag this animation with the duration that it is intended to be played + /// for. + /// + /// Note that using this tagging is completely optional, but it may + /// make it easier to combine animations sometimes. + pub fn dur(self, t: F::T) -> AnimWithDur { + AnimWithDur(self, t) + } +} + +impl<'a, V> From<&'a [V]> for AnimWithDur> +where + V: Clone, +{ + fn from(slice: &'a [V]) -> Self { + AnimWithDur(Anim(SliceClosure(slice)), slice.len()) + } +} + +pub fn slice<'a, V>(slice: &'a [V]) -> AnimWithDur + 'a> +where + V: Clone + 'a, +{ + slice.into() +} + +#[doc(hidden)] +pub struct SliceClosure<'a, V>(&'a [V]); + +impl<'a, V> Fun for SliceClosure<'a, V> +where + V: Clone, +{ + type T = usize; + type V = V; + + fn eval(&self, t: Self::T) -> Self::V { + self.0[t].clone() + } +} + +impl Anim +where + F: Fun, + F::T: Clone + FloatCore, +{ + pub fn scale_to_dur(self, dur: F::T) -> AnimWithDur> { + self.scale_time(F::T::one() / dur).dur(dur) + } +} + +impl AnimWithDur +where + F: Fun, + F::T: Clone, +{ + pub fn as_ref(&self) -> AnimWithDur<&F> { + AnimWithDur(self.0.as_ref(), self.1.clone()) + } +} + +impl AnimWithDur +where + F: Fun, +{ + pub fn transform(self, h: H) -> AnimWithDur + where + G: Fun, + H: FnOnce(Anim) -> Anim, + { + AnimWithDur(h(self.0), self.1) + } + + pub fn map(self, f: impl Fn(F::V) -> W) -> AnimWithDur> { + self.transform(move |anim| anim.map(f)) + } + + pub fn dur(self, t: F::T) -> AnimWithDur { + AnimWithDur(self.0, t) + } +} + +impl<'a, T, X, Y, F> AnimWithDur +where + T: 'a + Clone, + X: 'a, + Y: 'a, + F: Fun, +{ + pub fn unzip( + &'a self, + ) -> ( + AnimWithDur + 'a>, + AnimWithDur + 'a>, + ) { + ( + self.as_ref().transform(|anim| anim.fst()), + self.as_ref().transform(|anim| anim.snd()), + ) + } +} + +impl AnimWithDur +where + F: Fun, + F::T: Copy + PartialOrd + Sub, +{ + pub fn seq(self, next: Anim) -> Anim> + where + G: Fun, + { + self.0.seq(self.1, next) + } +} + +impl AnimWithDur +where + F: Fun, + F::T: Copy + PartialOrd + Sub + Add, +{ + pub fn seq_with_dur(self, next: AnimWithDur) -> AnimWithDur> + where + G: Fun, + { + let dur = self.1.clone() + next.1; + AnimWithDur(self.seq(next.0), dur) + } +} + +impl AnimWithDur +where + F: Fun, + F::T: Clone + FloatCore, +{ + pub fn repeat(self) -> Anim> { + self.0.repeat(self.1) + } +} + +impl AnimWithDur +where + F: Fun, + F::T: Clone + Sub, +{ + pub fn backwards(self) -> AnimWithDur> { + AnimWithDur(self.0.backwards(self.1.clone()), self.1) + } +} + +impl AnimWithDur +where + F: Fun, + F::T: Clone + Mul + Div, +{ + pub fn scale_time(self, t_scale: F::T) -> AnimWithDur> { + AnimWithDur(self.0.scale_time(t_scale.clone()), self.1 / t_scale) + } +} + +#[macro_export] +macro_rules! seq_with_dur { + ( + $expr:expr $(,)? + ) => { + $expr + }; + + ( + $head:expr, + $($tail:expr $(,)?)+ + ) => { + $head.seq_with_dur($crate::seq_with_dur!($($tail,)*)) + } +} diff --git a/rust/pareen/src/arithmetic.rs b/rust/pareen/src/arithmetic.rs new file mode 100644 index 0000000000..ff952938f9 --- /dev/null +++ b/rust/pareen/src/arithmetic.rs @@ -0,0 +1,169 @@ +use core::ops::{Add, Mul, Neg, Sub}; + +use crate::{primitives::ConstantClosure, Anim, Fun}; + +impl Add> for Anim +where + F: Fun, + G: Fun, + F::V: Add, +{ + type Output = Anim>; + + fn add(self, rhs: Anim) -> Self::Output { + Anim(AddClosure(self.0, rhs.0)) + } +} + +impl Add for Anim +where + W: Copy, + F: Fun, + F::V: Add, +{ + type Output = Anim>>; + + fn add(self, rhs: W) -> Self::Output { + Anim(AddClosure(self.0, ConstantClosure::from(rhs))) + } +} + +impl Sub> for Anim +where + F: Fun, + G: Fun, + F::V: Sub, +{ + type Output = Anim>; + + fn sub(self, rhs: Anim) -> Self::Output { + Anim(SubClosure(self.0, rhs.0)) + } +} + +impl Sub for Anim +where + W: Copy, + F: Fun, + F::T: Copy, + F::V: Sub, +{ + type Output = Anim>>; + + fn sub(self, rhs: W) -> Self::Output { + Anim(SubClosure(self.0, ConstantClosure::from(rhs))) + } +} + +impl Mul> for Anim +where + F: Fun, + G: Fun, + F::T: Copy, + F::V: Mul, +{ + type Output = Anim>; + + fn mul(self, rhs: Anim) -> Self::Output { + Anim(MulClosure(self.0, rhs.0)) + } +} + +impl Mul for Anim +where + W: Copy, + F: Fun, + F::T: Copy, + F::V: Mul, +{ + type Output = Anim>>; + + fn mul(self, rhs: W) -> Self::Output { + Anim(MulClosure(self.0, ConstantClosure::from(rhs))) + } +} + +impl Neg for Anim +where + V: Copy, + F: Fun, +{ + type Output = Anim>; + + fn neg(self) -> Self::Output { + Anim(NegClosure(self.0)) + } +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct AddClosure(F, G); + +impl Fun for AddClosure +where + F: Fun, + G: Fun, + F::T: Clone, + F::V: Add, +{ + type T = F::T; + type V = >::Output; + + fn eval(&self, t: F::T) -> Self::V { + self.0.eval(t.clone()) + self.1.eval(t) + } +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct SubClosure(F, G); + +impl Fun for SubClosure +where + F: Fun, + G: Fun, + F::T: Clone, + F::V: Sub, +{ + type T = F::T; + type V = >::Output; + + fn eval(&self, t: F::T) -> Self::V { + self.0.eval(t.clone()) - self.1.eval(t) + } +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct MulClosure(F, G); + +impl Fun for MulClosure +where + F: Fun, + G: Fun, + F::T: Copy, + F::V: Mul, +{ + type T = F::T; + type V = >::Output; + + fn eval(&self, t: F::T) -> Self::V { + self.0.eval(t) * self.1.eval(t) + } +} + +#[doc(hidden)] +pub struct NegClosure(F); + +impl Fun for NegClosure +where + F: Fun, + F::V: Neg, +{ + type T = F::T; + type V = ::Output; + + fn eval(&self, t: F::T) -> Self::V { + -self.0.eval(t) + } +} diff --git a/rust/pareen/src/easer_combinators.rs b/rust/pareen/src/easer_combinators.rs new file mode 100644 index 0000000000..5c4312bb89 --- /dev/null +++ b/rust/pareen/src/easer_combinators.rs @@ -0,0 +1,233 @@ +use num_traits::Float; + +use easer::functions::Easing; + +use crate::{fun, Anim, Fun}; + +impl Anim +where + V: Float, + F: Fun, +{ + fn seq_ease( + self, + self_end: V, + ease: impl Fn(V, V, V) -> Anim, + ease_duration: V, + next: A, + ) -> Anim> + where + G: Fun, + H: Fun, + A: Into>, + { + let next = next.into(); + + let ease_start_value = self.eval(self_end); + let ease_end_value = next.eval(V::zero()); + let ease_delta = ease_end_value - ease_start_value; + let ease = ease(ease_start_value, ease_delta, ease_duration); + + self.seq(self_end, ease).seq(self_end + ease_duration, next) + } + + /// Play two animations in sequence, transitioning between them with an + /// easing-in function from + /// [`easer`](https://docs.rs/easer/0.2.1/easer/index.html). + /// + /// This is only available when enabling the `easer` feature for `pareen`. + /// + /// The values of `self` at `self_end` and of `next` at time zero are used + /// to determine the parameters of the easing function. + /// + /// Note that, as with [`seq`](struct.Anim.html#method.seq), the `next` + /// animation will see time starting at zero once it plays. + /// + /// # Arguments + /// + /// * `self_end` - Time at which the `self` animation is to stop. + /// * `_easing` - A struct implementing + /// [`easer::functions::Easing`](https://docs.rs/easer/0.2.1/easer/functions/trait.Easing.html). + /// This determines the easing function that will be used for the + /// transition. It is passed as a parameter here to simplify type + /// inference. + /// * `ease_duration` - The amount of time to use for transitioning to `next`. + /// * `next` - The animation to play after transitioning. + /// + /// # Example + /// + /// See [`seq_ease_in_out`](struct.Anim.html#method.seq_ease_in_out) for an example. + pub fn seq_ease_in( + self, + self_end: V, + _easing: E, + ease_duration: V, + next: A, + ) -> Anim> + where + E: Easing, + G: Fun, + A: Into>, + { + self.seq_ease(self_end, ease_in::, ease_duration, next) + } + + /// Play two animations in sequence, transitioning between them with an + /// easing-out function from + /// [`easer`](https://docs.rs/easer/0.2.1/easer/index.html). + /// + /// This is only available when enabling the `easer` feature for `pareen`. + /// + /// The values of `self` at `self_end` and of `next` at time zero are used + /// to determine the parameters of the easing function. + /// + /// Note that, as with [`seq`](struct.Anim.html#method.seq), the `next` + /// animation will see time starting at zero once it plays. + /// + /// # Arguments + /// + /// * `self_end` - Time at which the `self` animation is to stop. + /// * `_easing` - A struct implementing + /// [`easer::functions::Easing`](https://docs.rs/easer/0.2.1/easer/functions/trait.Easing.html). + /// This determines the easing function that will be used for the + /// transition. It is passed as a parameter here to simplify type + /// inference. + /// * `ease_duration` - The amount of time to use for transitioning to `next`. + /// * `next` - The animation to play after transitioning. + /// + /// # Example + /// + /// See [`seq_ease_in_out`](struct.Anim.html#method.seq_ease_in_out) for an example. + pub fn seq_ease_out( + self, + self_end: V, + _: E, + ease_duration: V, + next: A, + ) -> Anim> + where + E: Easing, + G: Fun, + A: Into>, + { + self.seq_ease(self_end, ease_out::, ease_duration, next) + } + + /// Play two animations in sequence, transitioning between them with an + /// easing-in-out function from + /// [`easer`](https://docs.rs/easer/0.2.1/easer/index.html). + /// + /// This is only available when enabling the `easer` feature for `pareen`. + /// + /// The values of `self` at `self_end` and of `next` at time zero are used + /// to determine the parameters of the easing function. + /// + /// Note that, as with [`seq`](struct.Anim.html#method.seq), the `next` + /// animation will see time starting at zero once it plays. + /// + /// # Arguments + /// + /// * `self_end` - Time at which the `self` animation is to stop. + /// * `_easing` - A struct implementing + /// [`easer::functions::Easing`](https://docs.rs/easer/0.2.1/easer/functions/trait.Easing.html). + /// This determines the easing function that will be used for the + /// transition. It is passed as a parameter here to simplify type + /// inference. + /// * `ease_duration` - The amount of time to use for transitioning to `next`. + /// * `next` - The animation to play after transitioning. + /// + /// # Example + /// + /// Play a constant value until time `0.5`, then transition for `0.3` + /// time units, using a cubic function, into a second animation: + /// ``` + /// let first_anim = pareen::constant(2.0); + /// let second_anim = pareen::prop(1.0f32); + /// let anim = first_anim.seq_ease_in_out( + /// 0.5, + /// easer::functions::Cubic, + /// 0.3, + /// second_anim, + /// ); + /// ``` + /// The animation will look like this: + /// + /// ![plot for seq_ease_in_out](https://raw.githubusercontent.com/leod/pareen/master/images/seq_ease_in_out.png) + pub fn seq_ease_in_out( + self, + self_end: V, + _: E, + ease_duration: V, + next: A, + ) -> Anim> + where + E: Easing, + G: Fun, + A: Into>, + { + self.seq_ease(self_end, ease_in_out::, ease_duration, next) + } +} + +/// Integrate an easing-in function from the +/// [`easer`](https://docs.rs/easer/0.2.1/easer/index.html) library. +/// +/// This is only available when enabling the `easer` feature for `pareen`. +/// +/// # Arguments +/// +/// * `start` - The start value for the easing function. +/// * `delta` - The change in the value from beginning to end time. +/// * `duration` - The total time between beginning and end. +/// +/// # See also +/// Documentation for [`easer::functions::Easing`](https://docs.rs/easer/0.2.1/easer/functions/trait.Easing.html). +pub fn ease_in(start: V, delta: V, duration: V) -> Anim> +where + V: Float, + E: Easing, +{ + fun(move |t| E::ease_in(t, start, delta, duration)) +} + +/// Integrate an easing-out function from the +/// [`easer`](https://docs.rs/easer/0.2.1/easer/index.html) library. +/// +/// This is only available when enabling the `easer` feature for `pareen`. +/// +/// # Arguments +/// +/// * `start` - The start value for the easing function. +/// * `delta` - The change in the value from beginning to end time. +/// * `duration` - The total time between beginning and end. +/// +/// # See also +/// Documentation for [`easer::functions::Easing`](https://docs.rs/easer/0.2.1/easer/functions/trait.Easing.html). +pub fn ease_out(start: V, delta: V, duration: V) -> Anim> +where + V: Float, + E: Easing, +{ + fun(move |t| E::ease_out(t, start, delta, duration)) +} + +/// Integrate an easing-in-out function from the +/// [`easer`](https://docs.rs/easer/0.2.1/easer/index.html) library. +/// +/// This is only available when enabling the `easer` feature for `pareen`. +/// +/// # Arguments +/// +/// * `start` - The start value for the easing function. +/// * `delta` - The change in the value from beginning to end time. +/// * `duration` - The total time between beginning and end. +/// +/// # See also +/// Documentation for [`easer::functions::Easing`](https://docs.rs/easer/0.2.1/easer/functions/trait.Easing.html). +pub fn ease_in_out(start: V, delta: V, duration: V) -> Anim> +where + V: Float, + E: Easing, +{ + fun(move |t| E::ease_in_out(t, start, delta, duration)) +} diff --git a/rust/pareen/src/lib.rs b/rust/pareen/src/lib.rs new file mode 100644 index 0000000000..e5f6e27f00 --- /dev/null +++ b/rust/pareen/src/lib.rs @@ -0,0 +1,71 @@ +//! Pareen is a small library for *par*ameterized inbetw*een*ing. +//! The intended application is in game programming, where you sometimes have +//! two discrete game states between which you want to transition smoothly +//! for visualization purposes. +//! +//! Pareen gives you tools for composing animations that are parameterized by +//! time (i.e. mappings from time to some animated value) without constantly +//! having to pass around time variables; it hides the plumbing, so that you +//! need to provide time only once: when evaluating the animation. +//! +//! Animations are composed similarly to Rust's iterators, so no memory +//! allocations are necessary. +//! ## Examples +//! +//! ```rust +//! # use assert_approx_eq::assert_approx_eq; +//! // An animation returning a constant value +//! let anim1 = pareen::constant(1.0f64); +//! +//! // Animations can be evaluated at any time +//! let value = anim1.eval(0.5); +//! +//! // Animations can be played in sequence +//! let anim2 = anim1.seq(0.7, pareen::prop(0.25) + 0.5); +//! +#![cfg_attr( + any(feature = "std", feature = "libm"), + doc = r##" +// Animations can be composed and transformed in various ways +let anim3 = anim2 + .lerp(pareen::circle().cos()) + .scale_min_max(5.0, 10.0) + .backwards(1.0) + .squeeze(0.5..=1.0); + +let anim4 = pareen::cubic(&[1.0, 2.0, 3.0, 4.0]) - anim3; + +let value = anim4.eval(1.0); +assert_approx_eq!(value, 0.0); +"## +)] +//! ``` + +#![no_std] + +mod anim; +#[cfg(feature = "alloc")] +mod anim_box; +mod anim_with_dur; +mod arithmetic; +mod primitives; + +pub mod stats; + +#[cfg(all(feature = "easer", any(feature = "std", feature = "libm")))] +mod easer_combinators; + +pub use anim::{cond, lerp, Anim, Fun}; +#[cfg(feature = "alloc")] +pub use anim_box::AnimBox; +pub use anim_with_dur::{slice, AnimWithDur}; +pub use primitives::{ + circle, constant, cubic, cycle, fun, half_circle, id, prop, quadratic, quarter_circle, +}; +pub use stats::{simple_linear_regression, simple_linear_regression_with_slope}; + +#[cfg(all(feature = "easer", any(feature = "std", feature = "libm")))] +pub use easer; + +#[cfg(all(feature = "easer", any(feature = "std", feature = "libm")))] +pub use easer_combinators::{ease_in, ease_in_out, ease_out}; diff --git a/rust/pareen/src/primitives.rs b/rust/pareen/src/primitives.rs new file mode 100644 index 0000000000..51c8bd9f68 --- /dev/null +++ b/rust/pareen/src/primitives.rs @@ -0,0 +1,223 @@ +use core::marker::PhantomData; +use core::ops::Mul; + +use crate::{Anim, Fun}; + +use num_traits::{FloatConst, float::FloatCore}; + +/// Turn any function `Fn(T) -> V` into an [`Anim`](struct.Anim.html). +/// +/// # Example +/// ``` +/// # use assert_approx_eq::assert_approx_eq; +/// fn my_crazy_function(t: f32) -> f32 { +/// 42.0 / t +/// } +/// +/// let anim = pareen::fun(my_crazy_function); +/// +/// assert_approx_eq!(anim.eval(1.0), 42.0); +/// assert_approx_eq!(anim.eval(2.0), 21.0); +/// ``` +pub fn fun(f: impl Fn(T) -> V) -> Anim> { + From::from(f) +} + +struct WrapFn V>(F, PhantomData<(T, V)>); + +impl From for Anim> +where + F: Fn(T) -> V, +{ + fn from(f: F) -> Self { + Anim(WrapFn(f, PhantomData)) + } +} + +impl Fun for WrapFn +where + F: Fn(T) -> V, +{ + type T = T; + type V = V; + + fn eval(&self, t: T) -> V { + self.0(t) + } +} + +/// A constant animation, always returning the same value. +/// +/// # Example +/// ``` +/// # use assert_approx_eq::assert_approx_eq; +/// let anim = pareen::constant(1.0f32); +/// +/// assert_approx_eq!(anim.eval(-10000.0f32), 1.0); +/// assert_approx_eq!(anim.eval(0.0), 1.0); +/// assert_approx_eq!(anim.eval(42.0), 1.0); +/// ``` +pub fn constant(c: V) -> Anim> { + fun(move |_| c.clone()) +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct ConstantClosure(V, PhantomData); + +impl Fun for ConstantClosure +where + V: Clone, +{ + type T = T; + type V = V; + + fn eval(&self, _: T) -> V { + self.0.clone() + } +} + +impl From for ConstantClosure +where + V: Clone, +{ + fn from(v: V) -> Self { + ConstantClosure(v, PhantomData) + } +} + +impl From for Anim> +where + V: Clone, +{ + fn from(v: V) -> Self { + Anim(ConstantClosure::from(v)) + } +} + +/// An animation that returns a value proportional to time. +/// +/// # Example +/// +/// Scale time with a factor of three: +/// ``` +/// # use assert_approx_eq::assert_approx_eq; +/// let anim = pareen::prop(3.0f32); +/// +/// assert_approx_eq!(anim.eval(0.0f32), 0.0); +/// assert_approx_eq!(anim.eval(3.0), 9.0); +/// ``` +pub fn prop(m: V) -> Anim> +where + V: Clone + Mul + From, +{ + fun(move |t| m.clone() * From::from(t)) +} + +/// An animation that returns time as its value. +/// +/// This is the same as [`prop`](fn.prop.html) with a factor of one. +/// +/// # Examples +/// ``` +/// let anim = pareen::id::(); +/// +/// assert_eq!(anim.eval(-100), -100); +/// assert_eq!(anim.eval(0), 0); +/// assert_eq!(anim.eval(100), 100); +/// ``` +/// ``` +/// # use assert_approx_eq::assert_approx_eq; +/// let anim = pareen::id::() * 3.0 + 4.0; +/// +/// assert_approx_eq!(anim.eval(0.0), 4.0); +/// assert_approx_eq!(anim.eval(100.0), 304.0); +/// ``` +pub fn id() -> Anim> +where + V: From, +{ + fun(From::from) +} + +/// Proportionally increase value from zero to 2π. +pub fn circle() -> Anim> +where + T: FloatCore, + V: FloatCore + FloatConst + From, +{ + prop(V::PI() * (V::one() + V::one())) +} + +/// Proportionally increase value from zero to π. +pub fn half_circle() -> Anim> +where + T: FloatCore, + V: FloatCore + FloatConst + From, +{ + prop(V::PI()) +} + +/// Proportionally increase value from zero to π/2. +pub fn quarter_circle() -> Anim> +where + T: FloatCore, + V: FloatCore + FloatConst + From, +{ + prop(V::PI() * (V::one() / (V::one() + V::one()))) +} + +/// Evaluate a quadratic polynomial in time. +pub fn quadratic(w: &[T; 3]) -> Anim + '_> +where + T: FloatCore, +{ + fun(move |t| { + let t2 = t * t; + + w[0] * t2 + w[1] * t + w[2] + }) +} + +/// Evaluate a cubic polynomial in time. +pub fn cubic(w: &[T; 4]) -> Anim + '_> +where + T: FloatCore, +{ + fun(move |t| { + let t2 = t * t; + let t3 = t2 * t; + + w[0] * t3 + w[1] * t2 + w[2] * t + w[3] + }) +} + +/// Count from 0 to `end` (non-inclusive) cyclically, at the given frames per +/// second rate. +/// +/// # Example +/// ``` +/// let anim = pareen::cycle(3, 5.0); +/// assert_eq!(anim.eval(0.0), 0); +/// assert_eq!(anim.eval(0.1), 0); +/// assert_eq!(anim.eval(0.3), 1); +/// assert_eq!(anim.eval(0.5), 2); +/// assert_eq!(anim.eval(0.65), 0); +/// +/// assert_eq!(anim.eval(-0.1), 2); +/// assert_eq!(anim.eval(-0.3), 1); +/// assert_eq!(anim.eval(-0.5), 0); +/// ``` +pub fn cycle(end: usize, fps: f32) -> Anim> { + fun(move |t: f32| { + if t < 0.0 { + let tau = (t.abs() * fps) as usize; + + end - 1 - tau % end + } else { + let tau = (t * fps) as usize; + + tau % end + } + }) +} diff --git a/rust/pareen/src/stats.rs b/rust/pareen/src/stats.rs new file mode 100644 index 0000000000..c55023545c --- /dev/null +++ b/rust/pareen/src/stats.rs @@ -0,0 +1,133 @@ +use core::ops::{Add, Div, Mul}; + +use num_traits::{float::FloatCore, AsPrimitive, Zero}; + +use crate::{Anim, AnimWithDur, Fun}; + +impl AnimWithDur +where + F: Fun, +{ + pub fn fold(&self, init: B, mut f: G) -> B + where + G: FnMut(B, F::V) -> B, + { + let mut b = init; + + for t in 0..self.1 { + b = f(b, self.0.eval(t)) + } + + b + } +} + +impl AnimWithDur +where + F: Fun, + F::V: Add + Zero, +{ + pub fn sum(&self) -> F::V { + self.fold(Zero::zero(), |a, b| a + b) + } +} + +impl AnimWithDur +where + F: Fun, + F::T: Clone, + F::V: 'static + Add + Div + Zero + Copy, + usize: AsPrimitive, +{ + pub fn mean(&self) -> F::V { + let len = self.1.clone().as_(); + self.sum() / len + } +} + +#[derive(Debug, Clone)] +pub struct Line { + pub y_intercept: V, + pub slope: V, +} + +impl Fun for Line +where + V: Add + Mul + Clone, +{ + type T = V; + type V = V; + + fn eval(&self, t: V) -> V { + self.y_intercept.clone() + self.slope.clone() * t + } +} + +pub fn simple_linear_regression_with_slope(slope: V, values: A) -> Anim> +where + V: 'static + FloatCore + Copy, + F: Fun, + A: Into>, + usize: AsPrimitive, +{ + // https://en.wikipedia.org/wiki/Simple_linear_regression#Fitting_the_regression_line + let values = values.into(); + let (x, y) = values.unzip(); + let x_mean = x.as_ref().mean(); + let y_mean = y.as_ref().mean(); + + let y_intercept = y_mean - slope * x_mean; + + Anim(Line { y_intercept, slope }) +} + +pub fn simple_linear_regression(values: A) -> Anim> +where + V: 'static + FloatCore + Copy, + F: Fun, + A: Into>, + usize: AsPrimitive, +{ + // https://en.wikipedia.org/wiki/Simple_linear_regression#Fitting_the_regression_line + let values = values.into(); + let (x, y) = values.unzip(); + let x_mean = x.as_ref().mean(); + let y_mean = y.as_ref().mean(); + let numerator = values + .as_ref() + .map(|(x, y)| (x - x_mean) * (y - y_mean)) + .sum(); + let denominator = x.as_ref().map(|x| (x - x_mean) * (x - x_mean)).sum(); + let slope = numerator / denominator; + + let y_intercept = y_mean - slope * x_mean; + + Anim(Line { y_intercept, slope }) +} + +#[cfg(all(test, feature = "alloc"))] +mod tests { + use assert_approx_eq::assert_approx_eq; + extern crate alloc; + use alloc::vec; + + use super::simple_linear_regression; + + #[test] + fn test_perfect_regression() { + let straight_line = vec![(1.0, 1.0), (2.0, 2.0)]; + let line = simple_linear_regression(straight_line.as_slice()); + assert_approx_eq!(line.eval(1.0), 1.0f64); + assert_approx_eq!(line.eval(10.0), 10.0); + assert_approx_eq!(line.eval(-10.0), -10.0); + } + + #[test] + fn test_negative_perfect_regression() { + let straight_line = vec![(1.0, -1.0), (2.0, -2.0)]; + let line = simple_linear_regression(straight_line.as_slice()); + assert_approx_eq!(line.eval(1.0), -1.0f64); + assert_approx_eq!(line.eval(10.0), -10.0); + assert_approx_eq!(line.eval(-10.0), 10.0); + } +}