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 0000000000..8b0754b2c5 Binary files /dev/null and b/rust/pareen/images/plots.png differ 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 0000000000..96f847a0e6 Binary files /dev/null and b/rust/pareen/images/seq_ease_in_out.png differ diff --git a/rust/pareen/src/anim.rs b/rust/pareen/src/anim.rs new file mode 100644 index 0000000000..c695a238e6 --- /dev/null +++ b/rust/pareen/src/anim.rs @@ -0,0 +1,841 @@ +use core::ops::{Add, Mul, RangeInclusive, Sub}; + +#[cfg(any(feature = "std", feature = "libm"))] +use num_traits::Float; +use num_traits::{float::FloatCore, Num, One, Zero}; + +use crate::{constant, fun, id}; + +/// A `Fun` represents anything that maps from some type `T` to another +/// type `V`. +/// +/// `T` usually stands for time and `V` for some value that is parameterized by +/// time. +/// +/// ## Implementation details +/// The only reason that we define this trait instead of just using `Fn(T) -> 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); + } +}