diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 15607ac..aceaad5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,27 +7,39 @@ jobs: steps: - uses: actions/checkout@v1 - name: Build - run: cargo build --verbose + run: | + cargo build --verbose + cargo build --verbose --features bezier - name: Test - run: cargo test --verbose + run: | + cargo test --verbose + cargo test --verbose --features bezier build-windows: runs-on: windows-latest steps: - uses: actions/checkout@v1 - name: Build - run: cargo build --verbose + run: | + cargo build --verbose + cargo build --verbose --features bezier - name: Test - run: cargo test --verbose + run: | + cargo test --verbose + cargo test --verbose --features bezier build-macosx: runs-on: macosx-latest steps: - uses: actions/checkout@v1 - name: Build - run: cargo build --verbose + run: | + cargo build --verbose + cargo build --verbose --features bezier - name: Test - run: cargo test --verbose + run: | + cargo test --verbose + cargo test --verbose --features bezier check-readme: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index e0fc228..3dae0ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "splines" -version = "1.0.0" +version = "1.1.0" license = "BSD-3-Clause" authors = ["Dimitri Sabadie "] description = "Spline interpolation made easy" @@ -21,6 +21,7 @@ maintenance = { status = "actively-developed" } [features] default = ["std"] +bezier = [] impl-cgmath = ["cgmath"] impl-nalgebra = ["alga", "nalgebra", "num-traits"] serialization = ["serde", "serde_derive"] diff --git a/README.md b/README.md index 6f3a378..8c713fe 100644 --- a/README.md +++ b/README.md @@ -84,19 +84,27 @@ not. It’s especially important to see how it copes with the documentation. So here’s a list of currently supported features and how to enable them: - **Serialization / deserialization.** - + This feature implements both the `Serialize` and `Deserialize` traits from `serde` for all + - This feature implements both the `Serialize` and `Deserialize` traits from `serde` for all types exported by this crate. - + Enable with the `"serialization"` feature. + - Enable with the `"serialization"` feature. - **[cgmath](https://crates.io/crates/cgmath) implementors.** - + Adds some useful implementations of `Interpolate` for some cgmath types. - + Enable with the `"impl-cgmath"` feature. + - Adds some useful implementations of `Interpolate` for some cgmath types. + - Enable with the `"impl-cgmath"` feature. - **[nalgebra](https://crates.io/crates/nalgebra) implementors.** - + Adds some useful implementations of `Interpolate` for some nalgebra types. - + Enable with the `"impl-nalgebra"` feature. + - Adds some useful implementations of `Interpolate` for some nalgebra types. + - Enable with the `"impl-nalgebra"` feature. - **Standard library / no standard library.** - + It’s possible to compile against the standard library or go on your own without it. - + Compiling with the standard library is enabled by default. - + Use `default-features = []` in your `Cargo.toml` to disable. - + Enable explicitly with the `"std"` feature. + - It’s possible to compile against the standard library or go on your own without it. + - Compiling with the standard library is enabled by default. + - Use `default-features = []` in your `Cargo.toml` to disable. + - Enable explicitly with the `"std"` feature. + - **Extra interpolation modes.** + - In order not to introduce breaking changes, some feature-gates are added to augment the + [`Interpolation`] enum. + - Those feature-gates will disappear on the next major release of the crate. + - The following lists all currently available: + - `"bezier"`: [Bézier curves](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). + +[`Interpolation`]: crate::interpolation::Interpolation diff --git a/src/interpolate.rs b/src/interpolate.rs index 05c9408..ed4a9ee 100644 --- a/src/interpolate.rs +++ b/src/interpolate.rs @@ -57,6 +57,12 @@ pub trait Interpolate: Sized + Copy { fn cubic_hermite(_: (Self, T), a: (Self, T), b: (Self, T), _: (Self, T), t: T) -> Self { Self::lerp(a.0, b.0, t) } + + /// Quadratic Bézier interpolation. + fn quadratic_bezier(a: Self, u: Self, b: Self, t: T) -> Self; + + /// Cubic Bézier interpolation. + fn cubic_bezier(a: Self, u: Self, v: Self, b: Self, t: T) -> Self; } /// Set of types that support additions and subtraction. @@ -212,6 +218,31 @@ where V: Linear, a.0.outer_mul(two_t3 - three_t2 + one_t) + m0.outer_mul(t3 - t2 * two_t + t) + b.0.outer_mul(three_t2 - two_t3) + m1.outer_mul(t3 - t2) } +/// Default implementation of [`Interpolate::quadratic_bezier`]. +/// +/// `V` is the value being interpolated. `T` is the sampling value (also sometimes called time). +pub fn quadratic_bezier_def(a: V, u: V, b: V, t: T) -> V +where V: Linear, + T: Additive + Mul + One { + let one_t = T::one() - t; + let one_t_2 = one_t * one_t; + u + (a - u).outer_mul(one_t_2) + (b - u).outer_mul(t * t) +} + +/// Default implementation of [`Interpolate::cubic_bezier`]. +/// +/// `V` is the value being interpolated. `T` is the sampling value (also sometimes called time). +pub fn cubic_bezier_def(a: V, u: V, v: V, b: V, t: T) -> V +where V: Linear, + T: Additive + Mul + One { + let one_t = T::one() - t; + let one_t_2 = one_t * one_t; + let one_t_3 = one_t_2 * one_t; + let three = T::one() + T::one() + T::one(); + + a.outer_mul(one_t_3) + u.outer_mul(three * one_t_2 * t) + v.outer_mul(three * one_t * t * t) + b.outer_mul(t * t * t) +} + macro_rules! impl_interpolate_simple { ($t:ty) => { impl Interpolate<$t> for $t { @@ -222,6 +253,14 @@ macro_rules! impl_interpolate_simple { fn cubic_hermite(x: (Self, $t), a: (Self, $t), b: (Self, $t), y: (Self, $t), t: $t) -> Self { cubic_hermite_def(x, a, b, y, t) } + + fn quadratic_bezier(a: Self, u: Self, b: Self, t: $t) -> Self { + quadratic_bezier_def(a, u, b, t) + } + + fn cubic_bezier(a: Self, u: Self, v: Self, b: Self, t: $t) -> Self { + cubic_bezier_def(a, u, v, b, t) + } } } } @@ -229,19 +268,27 @@ macro_rules! impl_interpolate_simple { impl_interpolate_simple!(f32); impl_interpolate_simple!(f64); -macro_rules! impl_interpolate_via { - ($t:ty, $v:ty) => { - impl Interpolate<$t> for $v { - fn lerp(a: Self, b: Self, t: $t) -> Self { - a * (1. - t as $v) + b * t as $v - } - - fn cubic_hermite((x, xt): (Self, $t), (a, at): (Self, $t), (b, bt): (Self, $t), (y, yt): (Self, $t), t: $t) -> Self { - cubic_hermite_def((x, xt as $v), (a, at as $v), (b, bt as $v), (y, yt as $v), t as $v) - } - } - } -} - -impl_interpolate_via!(f32, f64); -impl_interpolate_via!(f64, f32); +//macro_rules! impl_interpolate_via { +// ($t:ty, $v:ty) => { +// impl Interpolate<$t> for $v { +// fn lerp(a: Self, b: Self, t: $t) -> Self { +// a * (1. - t as $v) + b * t as $v +// } +// +// fn cubic_hermite((x, xt): (Self, $t), (a, at): (Self, $t), (b, bt): (Self, $t), (y, yt): (Self, $t), t: $t) -> Self { +// cubic_hermite_def((x, xt as $v), (a, at as $v), (b, bt as $v), (y, yt as $v), t as $v) +// } +// +// fn quadratic_bezier(a: Self, u: Self, b: Self, t: $t) -> Self { +// $t::quadratic_bezier(a as $t, u as $t, b as $t, t) +// } +// +// fn cubic_bezier(a: Self, u: Self, v: Self, b: Self, t: $t) -> Self { +// $t::cubic_bezier(a as $t, u as $t, v as $t, b as $t, t) +// } +// } +// } +//} +// +//impl_interpolate_via!(f32, f64); +//impl_interpolate_via!(f64, f32); diff --git a/src/interpolation.rs b/src/interpolation.rs index dd3eaf8..b02022e 100644 --- a/src/interpolation.rs +++ b/src/interpolation.rs @@ -8,7 +8,7 @@ #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serialization", derive(Deserialize, Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "snake_case"))] -pub enum Interpolation { +pub enum Interpolation { /// Hold a [`Key`] until the sampling value passes the normalized step threshold, in which /// case the next key is used. /// @@ -24,10 +24,29 @@ pub enum Interpolation { /// Cosine interpolation between a key and the next one. Cosine, /// Catmull-Rom interpolation, performing a cubic Hermite interpolation using four keys. - CatmullRom + CatmullRom, + /// Bézier interpolation. + /// + /// A control point that uses such an interpolation is associated with an extra point. The segmant + /// connecting both is called the _tangent_ of this point. The part of the spline defined between + /// this control point and the next one will be interpolated across with Bézier interpolation. Two + /// cases are possible: + /// + /// - The next control point also has a Bézier interpolation mode. In this case, its tangent is + /// used for the interpolation process. This is called _cubic Bézier interpolation_ and it + /// kicks ass. + /// - The next control point doesn’t have a Bézier interpolation mode set. In this case, the + /// tangent used for the next control point is defined as the segment connecting that control + /// point and the current control point’s associated point. This is called _quadratic Bézer + /// interpolation_ and it kicks ass too, but a bit less than cubic. + #[cfg(feature = "bezier")] + Bezier(V), + #[cfg(not(any(feature = "bezier")))] + #[doc(hidden)] + _V(std::marker::PhantomData), } -impl Default for Interpolation { +impl Default for Interpolation { /// [`Interpolation::Linear`] is the default. fn default() -> Self { Interpolation::Linear diff --git a/src/key.rs b/src/key.rs index bfba08a..76f09f6 100644 --- a/src/key.rs +++ b/src/key.rs @@ -26,12 +26,12 @@ pub struct Key { /// Carried value. pub value: V, /// Interpolation mode. - pub interpolation: Interpolation + pub interpolation: Interpolation } impl Key { /// Create a new key. - pub fn new(t: T, value: V, interpolation: Interpolation) -> Self { + pub fn new(t: T, value: V, interpolation: Interpolation) -> Self { Key { t, value, interpolation } } } diff --git a/src/lib.rs b/src/lib.rs index a567dd4..734eac8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,20 +85,28 @@ //! So here’s a list of currently supported features and how to enable them: //! //! - **Serialization / deserialization.** -//! + This feature implements both the `Serialize` and `Deserialize` traits from `serde` for all +//! - This feature implements both the `Serialize` and `Deserialize` traits from `serde` for all //! types exported by this crate. -//! + Enable with the `"serialization"` feature. +//! - Enable with the `"serialization"` feature. //! - **[cgmath](https://crates.io/crates/cgmath) implementors.** -//! + Adds some useful implementations of `Interpolate` for some cgmath types. -//! + Enable with the `"impl-cgmath"` feature. +//! - Adds some useful implementations of `Interpolate` for some cgmath types. +//! - Enable with the `"impl-cgmath"` feature. //! - **[nalgebra](https://crates.io/crates/nalgebra) implementors.** -//! + Adds some useful implementations of `Interpolate` for some nalgebra types. -//! + Enable with the `"impl-nalgebra"` feature. +//! - Adds some useful implementations of `Interpolate` for some nalgebra types. +//! - Enable with the `"impl-nalgebra"` feature. //! - **Standard library / no standard library.** -//! + It’s possible to compile against the standard library or go on your own without it. -//! + Compiling with the standard library is enabled by default. -//! + Use `default-features = []` in your `Cargo.toml` to disable. -//! + Enable explicitly with the `"std"` feature. +//! - It’s possible to compile against the standard library or go on your own without it. +//! - Compiling with the standard library is enabled by default. +//! - Use `default-features = []` in your `Cargo.toml` to disable. +//! - Enable explicitly with the `"std"` feature. +//! - **Extra interpolation modes.** +//! - In order not to introduce breaking changes, some feature-gates are added to augment the +//! [`Interpolation`] enum. +//! - Those feature-gates will disappear on the next major release of the crate. +//! - The following lists all currently available: +//! - `"bezier"`: [Bézier curves](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). +//! +//! [`Interpolation`]: crate::interpolation::Interpolation #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), feature(alloc))] diff --git a/src/spline.rs b/src/spline.rs index 5a6c4db..1e20e95 100644 --- a/src/spline.rs +++ b/src/spline.rs @@ -93,13 +93,13 @@ impl Spline { match cp0.interpolation { Interpolation::Step(threshold) => { - let cp1 = &keys[i+1]; + let cp1 = &keys[i + 1]; let nt = normalize_time(t, cp0, cp1); Some(if nt < threshold { cp0.value } else { cp1.value }) } Interpolation::Linear => { - let cp1 = &keys[i+1]; + let cp1 = &keys[i + 1]; let nt = normalize_time(t, cp0, cp1); Some(Interpolate::lerp(cp0.value, cp1.value, nt)) @@ -107,7 +107,7 @@ impl Spline { Interpolation::Cosine => { let two_t = T::one() + T::one(); - let cp1 = &keys[i+1]; + let cp1 = &keys[i + 1]; let nt = normalize_time(t, cp0, cp1); let cos_nt = (T::one() - (nt * T::pi()).cos()) / two_t; @@ -120,14 +120,35 @@ impl Spline { if i == 0 || i >= keys.len() - 2 { None } else { - let cp1 = &keys[i+1]; - let cpm0 = &keys[i-1]; - let cpm1 = &keys[i+2]; + let cp1 = &keys[i + 1]; + let cpm0 = &keys[i - 1]; + let cpm1 = &keys[i + 2]; let nt = normalize_time(t, cp0, cp1); Some(Interpolate::cubic_hermite((cpm0.value, cpm0.t), (cp0.value, cp0.t), (cp1.value, cp1.t), (cpm1.value, cpm1.t), nt)) } } + + #[cfg(feature = "bezier")] + Interpolation::Bezier(u) => { + // We need to check the next control point to see whether we want quadratic or cubic Bezier. + let cp1 = &keys[i + 1]; + let nt = normalize_time(t, cp0, cp1); + + if let Interpolation::Bezier(v) = cp1.interpolation { + Some(Interpolate::cubic_bezier(cp0.value, u, v, cp1.value, nt)) + //let one_nt = T::one() - nt; + //let one_nt_2 = one_nt * one_nt; + //let one_nt_3 = one_nt_2 * one_nt; + //let three_one_nt_2 = one_nt_2 + one_nt_2 + one_nt_2; // one_nt_2 * 3 + //let r = cp0.value * one_nt_3; + } else { + Some(Interpolate::quadratic_bezier(cp0.value, u, cp1.value, nt)) + } + } + + #[cfg(not(any(feature = "bezier")))] + Interpolation::_V(_) => unreachable!() } }