bevy_math/
cubic_splines.rs

1//! Provides types for building cubic splines for rendering curves and use with animation easing.
2
3use std::{fmt::Debug, iter::once};
4
5use crate::{Vec2, VectorSpace};
6
7use thiserror::Error;
8
9#[cfg(feature = "bevy_reflect")]
10use bevy_reflect::{std_traits::ReflectDefault, Reflect};
11
12/// A spline composed of a single cubic Bezier curve.
13///
14/// Useful for user-drawn curves with local control, or animation easing. See
15/// [`CubicSegment::new_bezier`] for use in easing.
16///
17/// ### Interpolation
18/// The curve only passes through the first and last control point in each set of four points. The curve
19/// is divided into "segments" by every fourth control point.
20///
21/// ### Tangency
22/// Tangents are manually defined by the two intermediate control points within each set of four points.
23/// You can think of the control points the curve passes through as "anchors", and as the intermediate
24/// control points as the anchors displaced along their tangent vectors
25///
26/// ### Continuity
27/// A Bezier curve is at minimum C0 continuous, meaning it has no holes or jumps. Each curve segment is
28/// C2, meaning the tangent vector changes smoothly between each set of four control points, but this
29/// doesn't hold at the control points between segments. Making the whole curve C1 or C2 requires moving
30/// the intermediate control points to align the tangent vectors between segments, and can result in a
31/// loss of local control.
32///
33/// ### Usage
34///
35/// ```
36/// # use bevy_math::{*, prelude::*};
37/// let points = [[
38///     vec2(-1.0, -20.0),
39///     vec2(3.0, 2.0),
40///     vec2(5.0, 3.0),
41///     vec2(9.0, 8.0),
42/// ]];
43/// let bezier = CubicBezier::new(points).to_curve();
44/// let positions: Vec<_> = bezier.iter_positions(100).collect();
45/// ```
46#[derive(Clone, Debug)]
47#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
48pub struct CubicBezier<P: VectorSpace> {
49    /// The control points of the Bezier curve
50    pub control_points: Vec<[P; 4]>,
51}
52
53impl<P: VectorSpace> CubicBezier<P> {
54    /// Create a new cubic Bezier curve from sets of control points.
55    pub fn new(control_points: impl Into<Vec<[P; 4]>>) -> Self {
56        Self {
57            control_points: control_points.into(),
58        }
59    }
60}
61impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {
62    #[inline]
63    fn to_curve(&self) -> CubicCurve<P> {
64        // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
65        // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
66        // See section 4.2 and equation 11.
67        let char_matrix = [
68            [1., 0., 0., 0.],
69            [-3., 3., 0., 0.],
70            [3., -6., 3., 0.],
71            [-1., 3., -3., 1.],
72        ];
73
74        let segments = self
75            .control_points
76            .iter()
77            .map(|p| CubicSegment::coefficients(*p, char_matrix))
78            .collect();
79
80        CubicCurve { segments }
81    }
82}
83
84/// A spline interpolated continuously between the nearest two control points, with the position and
85/// velocity of the curve specified at both control points. This curve passes through all control
86/// points, with the specified velocity which includes direction and parametric speed.
87///
88/// Useful for smooth interpolation when you know the position and velocity at two points in time,
89/// such as network prediction.
90///
91/// ### Interpolation
92/// The curve passes through every control point.
93///
94/// ### Tangency
95/// Tangents are explicitly defined at each control point.
96///
97/// ### Continuity
98/// The curve is at minimum C0 continuous, meaning it has no holes or jumps. It is also C1, meaning the
99/// tangent vector has no sudden jumps.
100///
101/// ### Usage
102///
103/// ```
104/// # use bevy_math::{*, prelude::*};
105/// let points = [
106///     vec2(-1.0, -20.0),
107///     vec2(3.0, 2.0),
108///     vec2(5.0, 3.0),
109///     vec2(9.0, 8.0),
110/// ];
111/// let tangents = [
112///     vec2(0.0, 1.0),
113///     vec2(0.0, 1.0),
114///     vec2(0.0, 1.0),
115///     vec2(0.0, 1.0),
116/// ];
117/// let hermite = CubicHermite::new(points, tangents).to_curve();
118/// let positions: Vec<_> = hermite.iter_positions(100).collect();
119/// ```
120#[derive(Clone, Debug)]
121#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
122pub struct CubicHermite<P: VectorSpace> {
123    /// The control points of the Hermite curve
124    pub control_points: Vec<(P, P)>,
125}
126impl<P: VectorSpace> CubicHermite<P> {
127    /// Create a new Hermite curve from sets of control points.
128    pub fn new(
129        control_points: impl IntoIterator<Item = P>,
130        tangents: impl IntoIterator<Item = P>,
131    ) -> Self {
132        Self {
133            control_points: control_points.into_iter().zip(tangents).collect(),
134        }
135    }
136}
137impl<P: VectorSpace> CubicGenerator<P> for CubicHermite<P> {
138    #[inline]
139    fn to_curve(&self) -> CubicCurve<P> {
140        let char_matrix = [
141            [1., 0., 0., 0.],
142            [0., 1., 0., 0.],
143            [-3., -2., 3., -1.],
144            [2., 1., -2., 1.],
145        ];
146
147        let segments = self
148            .control_points
149            .windows(2)
150            .map(|p| {
151                let (p0, v0, p1, v1) = (p[0].0, p[0].1, p[1].0, p[1].1);
152                CubicSegment::coefficients([p0, v0, p1, v1], char_matrix)
153            })
154            .collect();
155
156        CubicCurve { segments }
157    }
158}
159
160/// A spline interpolated continuously across the nearest four control points, with the position of
161/// the curve specified at every control point and the tangents computed automatically. The associated [`CubicCurve`]
162/// has one segment between each pair of adjacent control points.
163///
164/// **Note** the Catmull-Rom spline is a special case of Cardinal spline where the tension is 0.5.
165///
166/// ### Interpolation
167/// The curve passes through every control point.
168///
169/// ### Tangency
170/// Tangents are automatically computed based on the positions of control points.
171///
172/// ### Continuity
173/// The curve is at minimum C1, meaning that it is continuous (it has no holes or jumps), and its tangent
174/// vector is also well-defined everywhere, without sudden jumps.
175///
176/// ### Usage
177///
178/// ```
179/// # use bevy_math::{*, prelude::*};
180/// let points = [
181///     vec2(-1.0, -20.0),
182///     vec2(3.0, 2.0),
183///     vec2(5.0, 3.0),
184///     vec2(9.0, 8.0),
185/// ];
186/// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve();
187/// let positions: Vec<_> = cardinal.iter_positions(100).collect();
188/// ```
189#[derive(Clone, Debug)]
190#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
191pub struct CubicCardinalSpline<P: VectorSpace> {
192    /// Tension
193    pub tension: f32,
194    /// The control points of the Cardinal spline
195    pub control_points: Vec<P>,
196}
197
198impl<P: VectorSpace> CubicCardinalSpline<P> {
199    /// Build a new Cardinal spline.
200    pub fn new(tension: f32, control_points: impl Into<Vec<P>>) -> Self {
201        Self {
202            tension,
203            control_points: control_points.into(),
204        }
205    }
206
207    /// Build a new Catmull-Rom spline, the special case of a Cardinal spline where tension = 1/2.
208    pub fn new_catmull_rom(control_points: impl Into<Vec<P>>) -> Self {
209        Self {
210            tension: 0.5,
211            control_points: control_points.into(),
212        }
213    }
214}
215impl<P: VectorSpace> CubicGenerator<P> for CubicCardinalSpline<P> {
216    #[inline]
217    fn to_curve(&self) -> CubicCurve<P> {
218        let s = self.tension;
219        let char_matrix = [
220            [0., 1., 0., 0.],
221            [-s, 0., s, 0.],
222            [2. * s, s - 3., 3. - 2. * s, -s],
223            [-s, 2. - s, s - 2., s],
224        ];
225
226        let length = self.control_points.len();
227
228        // Early return to avoid accessing an invalid index
229        if length < 2 {
230            return CubicCurve { segments: vec![] };
231        }
232
233        // Extend the list of control points by mirroring the last second-to-last control points on each end;
234        // this allows tangents for the endpoints to be provided, and the overall effect is that the tangent
235        // at an endpoint is proportional to twice the vector between it and its adjacent control point.
236        //
237        // The expression used here is P_{-1} := P_0 - (P_1 - P_0) = 2P_0 - P_1. (Analogously at the other end.)
238        let mirrored_first = self.control_points[0] * 2. - self.control_points[1];
239        let mirrored_last = self.control_points[length - 1] * 2. - self.control_points[length - 2];
240        let extended_control_points = once(&mirrored_first)
241            .chain(self.control_points.iter())
242            .chain(once(&mirrored_last))
243            .collect::<Vec<_>>();
244
245        let segments = extended_control_points
246            .windows(4)
247            .map(|p| CubicSegment::coefficients([*p[0], *p[1], *p[2], *p[3]], char_matrix))
248            .collect();
249
250        CubicCurve { segments }
251    }
252}
253
254/// A spline interpolated continuously across the nearest four control points. The curve does not
255/// pass through any of the control points.
256///
257/// ### Interpolation
258/// The curve does not pass through control points.
259///
260/// ### Tangency
261/// Tangents are automatically computed based on the position of control points.
262///
263/// ### Continuity
264/// The curve is C2 continuous, meaning it has no holes or jumps, and the tangent vector changes smoothly along
265/// the entire curve length. The acceleration continuity of this spline makes it useful for camera paths.
266///
267/// ### Usage
268///
269/// ```
270/// # use bevy_math::{*, prelude::*};
271/// let points = [
272///     vec2(-1.0, -20.0),
273///     vec2(3.0, 2.0),
274///     vec2(5.0, 3.0),
275///     vec2(9.0, 8.0),
276/// ];
277/// let b_spline = CubicBSpline::new(points).to_curve();
278/// let positions: Vec<_> = b_spline.iter_positions(100).collect();
279/// ```
280#[derive(Clone, Debug)]
281#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
282pub struct CubicBSpline<P: VectorSpace> {
283    /// The control points of the spline
284    pub control_points: Vec<P>,
285}
286impl<P: VectorSpace> CubicBSpline<P> {
287    /// Build a new B-Spline.
288    pub fn new(control_points: impl Into<Vec<P>>) -> Self {
289        Self {
290            control_points: control_points.into(),
291        }
292    }
293}
294impl<P: VectorSpace> CubicGenerator<P> for CubicBSpline<P> {
295    #[inline]
296    fn to_curve(&self) -> CubicCurve<P> {
297        // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
298        // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
299        // See section 4.1 and equations 7 and 8.
300        let mut char_matrix = [
301            [1.0, 4.0, 1.0, 0.0],
302            [-3.0, 0.0, 3.0, 0.0],
303            [3.0, -6.0, 3.0, 0.0],
304            [-1.0, 3.0, -3.0, 1.0],
305        ];
306
307        char_matrix
308            .iter_mut()
309            .for_each(|r| r.iter_mut().for_each(|c| *c /= 6.0));
310
311        let segments = self
312            .control_points
313            .windows(4)
314            .map(|p| CubicSegment::coefficients([p[0], p[1], p[2], p[3]], char_matrix))
315            .collect();
316
317        CubicCurve { segments }
318    }
319}
320
321/// Error during construction of [`CubicNurbs`]
322#[derive(Clone, Debug, Error)]
323pub enum CubicNurbsError {
324    /// Provided the wrong number of knots.
325    #[error("Wrong number of knots: expected {expected}, provided {provided}")]
326    KnotsNumberMismatch {
327        /// Expected number of knots
328        expected: usize,
329        /// Provided number of knots
330        provided: usize,
331    },
332    /// The provided knots had a descending knot pair. Subsequent knots must
333    /// either increase or stay the same.
334    #[error("Invalid knots: contains descending knot pair")]
335    DescendingKnots,
336    /// The provided knots were all equal. Knots must contain at least one increasing pair.
337    #[error("Invalid knots: all knots are equal")]
338    ConstantKnots,
339    /// Provided a different number of weights and control points.
340    #[error("Incorrect number of weights: expected {expected}, provided {provided}")]
341    WeightsNumberMismatch {
342        /// Expected number of weights
343        expected: usize,
344        /// Provided number of weights
345        provided: usize,
346    },
347    /// The number of control points provided is less than 4.
348    #[error("Not enough control points, at least 4 are required, {provided} were provided")]
349    NotEnoughControlPoints {
350        /// The number of control points provided
351        provided: usize,
352    },
353}
354
355/// Non-uniform Rational B-Splines (NURBS) are a powerful generalization of the [`CubicBSpline`] which can
356/// represent a much more diverse class of curves (like perfect circles and ellipses).
357///
358/// ### Non-uniformity
359/// The 'NU' part of NURBS stands for "Non-Uniform". This has to do with a parameter called 'knots'.
360/// The knots are a non-decreasing sequence of floating point numbers. The first and last three pairs of
361/// knots control the behavior of the curve as it approaches its endpoints. The intermediate pairs
362/// each control the length of one segment of the curve. Multiple repeated knot values are called
363/// "knot multiplicity". Knot multiplicity in the intermediate knots causes a "zero-length" segment,
364/// and can create sharp corners.
365///
366/// ### Rationality
367/// The 'R' part of NURBS stands for "Rational". This has to do with NURBS allowing each control point to
368/// be assigned a weighting, which controls how much it affects the curve compared to the other points.
369///
370/// ### Interpolation
371/// The curve will not pass through the control points except where a knot has multiplicity four.
372///
373/// ### Tangency
374/// Tangents are automatically computed based on the position of control points.
375///
376/// ### Continuity
377/// When there is no knot multiplicity, the curve is C2 continuous, meaning it has no holes or jumps and the
378/// tangent vector changes smoothly along the entire curve length. Like the [`CubicBSpline`], the acceleration
379/// continuity makes it useful for camera paths. Knot multiplicity of 2 in intermediate knots reduces the
380/// continuity to C2, and knot multiplicity of 3 reduces the continuity to C0. The curve is always at least
381/// C0, meaning it has no jumps or holes.
382///
383/// ### Usage
384///
385/// ```
386/// # use bevy_math::{*, prelude::*};
387/// let points = [
388///     vec2(-1.0, -20.0),
389///     vec2(3.0, 2.0),
390///     vec2(5.0, 3.0),
391///     vec2(9.0, 8.0),
392/// ];
393/// let weights = [1.0, 1.0, 2.0, 1.0];
394/// let knots = [0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0];
395/// let nurbs = CubicNurbs::new(points, Some(weights), Some(knots))
396///     .expect("NURBS construction failed!")
397///     .to_curve();
398/// let positions: Vec<_> = nurbs.iter_positions(100).collect();
399/// ```
400#[derive(Clone, Debug)]
401#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
402pub struct CubicNurbs<P: VectorSpace> {
403    /// The control points of the NURBS
404    pub control_points: Vec<P>,
405    /// Weights
406    pub weights: Vec<f32>,
407    /// Knots
408    pub knots: Vec<f32>,
409}
410impl<P: VectorSpace> CubicNurbs<P> {
411    /// Build a Non-Uniform Rational B-Spline.
412    ///
413    /// If provided, weights must be the same length as the control points. Defaults to equal weights.
414    ///
415    /// If provided, the number of knots must be n + 4 elements, where n is the amount of control
416    /// points. Defaults to open uniform knots: [`Self::open_uniform_knots`]. Knots cannot
417    /// all be equal.
418    ///
419    /// At least 4 points must be provided, otherwise an error will be returned.
420    pub fn new(
421        control_points: impl Into<Vec<P>>,
422        weights: Option<impl Into<Vec<f32>>>,
423        knots: Option<impl Into<Vec<f32>>>,
424    ) -> Result<Self, CubicNurbsError> {
425        let mut control_points: Vec<P> = control_points.into();
426        let control_points_len = control_points.len();
427
428        if control_points_len < 4 {
429            return Err(CubicNurbsError::NotEnoughControlPoints {
430                provided: control_points_len,
431            });
432        }
433
434        let weights = weights
435            .map(Into::into)
436            .unwrap_or_else(|| vec![1.0; control_points_len]);
437
438        let mut knots: Vec<f32> = knots.map(Into::into).unwrap_or_else(|| {
439            Self::open_uniform_knots(control_points_len)
440                .expect("The amount of control points was checked")
441        });
442
443        let expected_knots_len = Self::knots_len(control_points_len);
444
445        // Check the number of knots is correct
446        if knots.len() != expected_knots_len {
447            return Err(CubicNurbsError::KnotsNumberMismatch {
448                expected: expected_knots_len,
449                provided: knots.len(),
450            });
451        }
452
453        // Ensure the knots are non-descending (previous element is less than or equal
454        // to the next)
455        if knots.windows(2).any(|win| win[0] > win[1]) {
456            return Err(CubicNurbsError::DescendingKnots);
457        }
458
459        // Ensure the knots are non-constant
460        if knots.windows(2).all(|win| win[0] == win[1]) {
461            return Err(CubicNurbsError::ConstantKnots);
462        }
463
464        // Check that the number of weights equals the number of control points
465        if weights.len() != control_points_len {
466            return Err(CubicNurbsError::WeightsNumberMismatch {
467                expected: control_points_len,
468                provided: weights.len(),
469            });
470        }
471
472        // To align the evaluation behavior of nurbs with the other splines,
473        // make the intervals between knots form an exact cover of [0, N], where N is
474        // the number of segments of the final curve.
475        let curve_length = (control_points.len() - 3) as f32;
476        let min = *knots.first().unwrap();
477        let max = *knots.last().unwrap();
478        let knot_delta = max - min;
479        knots = knots
480            .into_iter()
481            .map(|k| k - min)
482            .map(|k| k * curve_length / knot_delta)
483            .collect();
484
485        control_points
486            .iter_mut()
487            .zip(weights.iter())
488            .for_each(|(p, w)| *p = *p * *w);
489
490        Ok(Self {
491            control_points,
492            weights,
493            knots,
494        })
495    }
496
497    /// Generates uniform knots that will generate the same curve as [`CubicBSpline`].
498    ///
499    /// "Uniform" means that the difference between two subsequent knots is the same.
500    ///
501    /// Will return `None` if there are less than 4 control points.
502    pub fn uniform_knots(control_points: usize) -> Option<Vec<f32>> {
503        if control_points < 4 {
504            return None;
505        }
506        Some(
507            (0..Self::knots_len(control_points))
508                .map(|v| v as f32)
509                .collect(),
510        )
511    }
512
513    /// Generates open uniform knots, which makes the ends of the curve pass through the
514    /// start and end points.
515    ///
516    /// The start and end knots have multiplicity 4, and intermediate knots have multiplicity 0 and
517    /// difference of 1.
518    ///
519    /// Will return `None` if there are less than 4 control points.
520    pub fn open_uniform_knots(control_points: usize) -> Option<Vec<f32>> {
521        if control_points < 4 {
522            return None;
523        }
524        let last_knots_value = control_points - 3;
525        Some(
526            std::iter::repeat(0.0)
527                .take(4)
528                .chain((1..last_knots_value).map(|v| v as f32))
529                .chain(std::iter::repeat(last_knots_value as f32).take(4))
530                .collect(),
531        )
532    }
533
534    #[inline(always)]
535    const fn knots_len(control_points_len: usize) -> usize {
536        control_points_len + 4
537    }
538
539    /// Generates a non-uniform B-spline characteristic matrix from a sequence of six knots. Each six
540    /// knots describe the relationship between four successive control points. For padding reasons,
541    /// this takes a vector of 8 knots, but only six are actually used.
542    fn generate_matrix(knots: &[f32; 8]) -> [[f32; 4]; 4] {
543        // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
544        // <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
545        // See section 3.1.
546
547        let t = knots;
548        // In the notation of the paper:
549        // t[1] := t_i-2
550        // t[2] := t_i-1
551        // t[3] := t_i   (the lower extent of the current knot span)
552        // t[4] := t_i+1 (the upper extent of the current knot span)
553        // t[5] := t_i+2
554        // t[6] := t_i+3
555
556        let m00 = (t[4] - t[3]).powi(2) / ((t[4] - t[2]) * (t[4] - t[1]));
557        let m02 = (t[3] - t[2]).powi(2) / ((t[5] - t[2]) * (t[4] - t[2]));
558        let m12 = (3.0 * (t[4] - t[3]) * (t[3] - t[2])) / ((t[5] - t[2]) * (t[4] - t[2]));
559        let m22 = 3.0 * (t[4] - t[3]).powi(2) / ((t[5] - t[2]) * (t[4] - t[2]));
560        let m33 = (t[4] - t[3]).powi(2) / ((t[6] - t[3]) * (t[5] - t[3]));
561        let m32 = -m22 / 3.0 - m33 - (t[4] - t[3]).powi(2) / ((t[5] - t[3]) * (t[5] - t[2]));
562        [
563            [m00, 1.0 - m00 - m02, m02, 0.0],
564            [-3.0 * m00, 3.0 * m00 - m12, m12, 0.0],
565            [3.0 * m00, -3.0 * m00 - m22, m22, 0.0],
566            [-m00, m00 - m32 - m33, m32, m33],
567        ]
568    }
569}
570impl<P: VectorSpace> RationalGenerator<P> for CubicNurbs<P> {
571    #[inline]
572    fn to_curve(&self) -> RationalCurve<P> {
573        let segments = self
574            .control_points
575            .windows(4)
576            .zip(self.weights.windows(4))
577            .zip(self.knots.windows(8))
578            .filter(|(_, knots)| knots[4] - knots[3] > 0.0)
579            .map(|((points, weights), knots)| {
580                // This is curve segment i. It uses control points P_i, P_i+2, P_i+2 and P_i+3,
581                // It is associated with knot span i+3 (which is the interval between knots i+3
582                // and i+4) and its characteristic matrix uses knots i+1 through i+6 (because
583                // those define the two knot spans on either side).
584                let span = knots[4] - knots[3];
585                let coefficient_knots = knots.try_into().expect("Knot windows are of length 6");
586                let matrix = Self::generate_matrix(coefficient_knots);
587                RationalSegment::coefficients(
588                    points.try_into().expect("Point windows are of length 4"),
589                    weights.try_into().expect("Weight windows are of length 4"),
590                    span,
591                    matrix,
592                )
593            })
594            .collect();
595        RationalCurve { segments }
596    }
597}
598
599/// A spline interpolated linearly between the nearest 2 points.
600///
601/// ### Interpolation
602/// The curve passes through every control point.
603///
604/// ### Tangency
605/// The curve is not generally differentiable at control points.
606///
607/// ### Continuity
608/// The curve is C0 continuous, meaning it has no holes or jumps.
609#[derive(Clone, Debug)]
610#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
611pub struct LinearSpline<P: VectorSpace> {
612    /// The control points of the NURBS
613    pub points: Vec<P>,
614}
615impl<P: VectorSpace> LinearSpline<P> {
616    /// Create a new linear spline
617    pub fn new(points: impl Into<Vec<P>>) -> Self {
618        Self {
619            points: points.into(),
620        }
621    }
622}
623impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {
624    #[inline]
625    fn to_curve(&self) -> CubicCurve<P> {
626        let segments = self
627            .points
628            .windows(2)
629            .map(|points| {
630                let a = points[0];
631                let b = points[1];
632                CubicSegment {
633                    coeff: [a, b - a, P::default(), P::default()],
634                }
635            })
636            .collect();
637        CubicCurve { segments }
638    }
639}
640
641/// Implement this on cubic splines that can generate a cubic curve from their spline parameters.
642pub trait CubicGenerator<P: VectorSpace> {
643    /// Build a [`CubicCurve`] by computing the interpolation coefficients for each curve segment.
644    fn to_curve(&self) -> CubicCurve<P>;
645}
646
647/// A segment of a cubic curve, used to hold precomputed coefficients for fast interpolation.
648/// Can be evaluated as a parametric curve over the domain `[0, 1)`.
649///
650/// Segments can be chained together to form a longer compound curve.
651#[derive(Copy, Clone, Debug, Default, PartialEq)]
652#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))]
653pub struct CubicSegment<P: VectorSpace> {
654    /// Coefficients of the segment
655    pub coeff: [P; 4],
656}
657
658impl<P: VectorSpace> CubicSegment<P> {
659    /// Instantaneous position of a point at parametric value `t`.
660    #[inline]
661    pub fn position(&self, t: f32) -> P {
662        let [a, b, c, d] = self.coeff;
663        // Evaluate `a + bt + ct^2 + dt^3`, avoiding exponentiation
664        a + (b + (c + d * t) * t) * t
665    }
666
667    /// Instantaneous velocity of a point at parametric value `t`.
668    #[inline]
669    pub fn velocity(&self, t: f32) -> P {
670        let [_, b, c, d] = self.coeff;
671        // Evaluate the derivative, which is `b + 2ct + 3dt^2`, avoiding exponentiation
672        b + (c * 2.0 + d * 3.0 * t) * t
673    }
674
675    /// Instantaneous acceleration of a point at parametric value `t`.
676    #[inline]
677    pub fn acceleration(&self, t: f32) -> P {
678        let [_, _, c, d] = self.coeff;
679        // Evaluate the second derivative, which is `2c + 6dt`
680        c * 2.0 + d * 6.0 * t
681    }
682
683    /// Calculate polynomial coefficients for the cubic curve using a characteristic matrix.
684    #[inline]
685    fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self {
686        let [c0, c1, c2, c3] = char_matrix;
687        // These are the polynomial coefficients, computed by multiplying the characteristic
688        // matrix by the point matrix.
689        let coeff = [
690            p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
691            p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
692            p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
693            p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
694        ];
695        Self { coeff }
696    }
697}
698
699/// The `CubicSegment<Vec2>` can be used as a 2-dimensional easing curve for animation.
700///
701/// The x-axis of the curve is time, and the y-axis is the output value. This struct provides
702/// methods for extremely fast solves for y given x.
703impl CubicSegment<Vec2> {
704    /// Construct a cubic Bezier curve for animation easing, with control points `p1` and `p2`. A
705    /// cubic Bezier easing curve has control point `p0` at (0, 0) and `p3` at (1, 1), leaving only
706    /// `p1` and `p2` as the remaining degrees of freedom. The first and last control points are
707    /// fixed to ensure the animation begins at 0, and ends at 1.
708    ///
709    /// This is a very common tool for UI animations that accelerate and decelerate smoothly. For
710    /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`.
711    pub fn new_bezier(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> Self {
712        let (p0, p3) = (Vec2::ZERO, Vec2::ONE);
713        let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]]).to_curve();
714        bezier.segments[0]
715    }
716
717    /// Maximum allowable error for iterative Bezier solve
718    const MAX_ERROR: f32 = 1e-5;
719
720    /// Maximum number of iterations during Bezier solve
721    const MAX_ITERS: u8 = 8;
722
723    /// Given a `time` within `0..=1`, returns an eased value that follows the cubic curve instead
724    /// of a straight line. This eased result may be outside the range `0..=1`, however it will
725    /// always start at 0 and end at 1: `ease(0) = 0` and `ease(1) = 1`.
726    ///
727    /// ```
728    /// # use bevy_math::prelude::*;
729    /// let cubic_bezier = CubicSegment::new_bezier((0.25, 0.1), (0.25, 1.0));
730    /// assert_eq!(cubic_bezier.ease(0.0), 0.0);
731    /// assert_eq!(cubic_bezier.ease(1.0), 1.0);
732    /// ```
733    ///
734    /// # How cubic easing works
735    ///
736    /// Easing is generally accomplished with the help of "shaping functions". These are curves that
737    /// start at (0,0) and end at (1,1). The x-axis of this plot is the current `time` of the
738    /// animation, from 0 to 1. The y-axis is how far along the animation is, also from 0 to 1. You
739    /// can imagine that if the shaping function is a straight line, there is a 1:1 mapping between
740    /// the `time` and how far along your animation is. If the `time` = 0.5, the animation is
741    /// halfway through. This is known as linear interpolation, and results in objects animating
742    /// with a constant velocity, and no smooth acceleration or deceleration at the start or end.
743    ///
744    /// ```text
745    /// y
746    /// │         ●
747    /// │       ⬈
748    /// │     ⬈    
749    /// │   ⬈
750    /// │ ⬈
751    /// ●─────────── x (time)
752    /// ```
753    ///
754    /// Using cubic Beziers, we have a curve that starts at (0,0), ends at (1,1), and follows a path
755    /// determined by the two remaining control points (handles). These handles allow us to define a
756    /// smooth curve. As `time` (x-axis) progresses, we now follow the curve, and use the `y` value
757    /// to determine how far along the animation is.
758    ///
759    /// ```text
760    /// y
761    ///          ⬈➔●
762    /// │      ⬈   
763    /// │     ↑      
764    /// │     ↑
765    /// │    ⬈
766    /// ●➔⬈───────── x (time)
767    /// ```
768    ///
769    /// To accomplish this, we need to be able to find the position `y` on a curve, given the `x`
770    /// value. Cubic curves are implicit parametric functions like B(t) = (x,y). To find `y`, we
771    /// first solve for `t` that corresponds to the given `x` (`time`). We use the Newton-Raphson
772    /// root-finding method to quickly find a value of `t` that is very near the desired value of
773    /// `x`. Once we have this we can easily plug that `t` into our curve's `position` function, to
774    /// find the `y` component, which is how far along our animation should be. In other words:
775    ///
776    /// > Given `time` in `0..=1`
777    ///
778    /// > Use Newton's method to find a value of `t` that results in B(t) = (x,y) where `x == time`
779    ///
780    /// > Once a solution is found, use the resulting `y` value as the final result
781    #[inline]
782    pub fn ease(&self, time: f32) -> f32 {
783        let x = time.clamp(0.0, 1.0);
784        self.find_y_given_x(x)
785    }
786
787    /// Find the `y` value of the curve at the given `x` value using the Newton-Raphson method.
788    #[inline]
789    fn find_y_given_x(&self, x: f32) -> f32 {
790        let mut t_guess = x;
791        let mut pos_guess = Vec2::ZERO;
792        for _ in 0..Self::MAX_ITERS {
793            pos_guess = self.position(t_guess);
794            let error = pos_guess.x - x;
795            if error.abs() <= Self::MAX_ERROR {
796                break;
797            }
798            // Using Newton's method, use the tangent line to estimate a better guess value.
799            let slope = self.velocity(t_guess).x; // dx/dt
800            t_guess -= error / slope;
801        }
802        pos_guess.y
803    }
804}
805
806/// A collection of [`CubicSegment`]s chained into a single parametric curve. Has domain `[0, N)`
807/// where `N` is the number of attached segments.
808///
809/// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as
810/// [`CubicBezier`].
811#[derive(Clone, Debug, PartialEq)]
812#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
813pub struct CubicCurve<P: VectorSpace> {
814    /// Segments of the curve
815    pub segments: Vec<CubicSegment<P>>,
816}
817
818impl<P: VectorSpace> CubicCurve<P> {
819    /// Compute the position of a point on the cubic curve at the parametric value `t`.
820    ///
821    /// Note that `t` varies from `0..=(n_points - 3)`.
822    #[inline]
823    pub fn position(&self, t: f32) -> P {
824        let (segment, t) = self.segment(t);
825        segment.position(t)
826    }
827
828    /// Compute the first derivative with respect to t at `t`. This is the instantaneous velocity of
829    /// a point on the cubic curve at `t`.
830    ///
831    /// Note that `t` varies from `0..=(n_points - 3)`.
832    #[inline]
833    pub fn velocity(&self, t: f32) -> P {
834        let (segment, t) = self.segment(t);
835        segment.velocity(t)
836    }
837
838    /// Compute the second derivative with respect to t at `t`. This is the instantaneous
839    /// acceleration of a point on the cubic curve at `t`.
840    ///
841    /// Note that `t` varies from `0..=(n_points - 3)`.
842    #[inline]
843    pub fn acceleration(&self, t: f32) -> P {
844        let (segment, t) = self.segment(t);
845        segment.acceleration(t)
846    }
847
848    /// A flexible iterator used to sample curves with arbitrary functions.
849    ///
850    /// This splits the curve into `subdivisions` of evenly spaced `t` values across the
851    /// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,
852    /// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.
853    ///
854    /// For `subdivisions = 2`, this will split the curve into two lines, or three points, and
855    /// return an iterator with 3 items, the three points, one at the start, middle, and end.
856    #[inline]
857    pub fn iter_samples<'a, 'b: 'a>(
858        &'b self,
859        subdivisions: usize,
860        mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
861    ) -> impl Iterator<Item = P> + 'a {
862        self.iter_uniformly(subdivisions)
863            .map(move |t| sample_function(self, t))
864    }
865
866    /// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.
867    #[inline]
868    fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
869        let segments = self.segments.len() as f32;
870        let step = segments / subdivisions as f32;
871        (0..=subdivisions).map(move |i| i as f32 * step)
872    }
873
874    /// The list of segments contained in this `CubicCurve`.
875    ///
876    /// This spline's global `t` value is equal to how many segments it has.
877    ///
878    /// All method accepting `t` on `CubicCurve` depends on the global `t`.
879    /// When sampling over the entire curve, you should either use one of the
880    /// `iter_*` methods or account for the segment count using `curve.segments().len()`.
881    #[inline]
882    pub fn segments(&self) -> &[CubicSegment<P>] {
883        &self.segments
884    }
885
886    /// Iterate over the curve split into `subdivisions`, sampling the position at each step.
887    pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
888        self.iter_samples(subdivisions, Self::position)
889    }
890
891    /// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.
892    pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
893        self.iter_samples(subdivisions, Self::velocity)
894    }
895
896    /// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.
897    pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
898        self.iter_samples(subdivisions, Self::acceleration)
899    }
900
901    #[inline]
902    /// Adds a segment to the curve
903    pub fn push_segment(&mut self, segment: CubicSegment<P>) {
904        self.segments.push(segment);
905    }
906
907    /// Returns the [`CubicSegment`] and local `t` value given a spline's global `t` value.
908    #[inline]
909    fn segment(&self, t: f32) -> (&CubicSegment<P>, f32) {
910        if self.segments.len() == 1 {
911            (&self.segments[0], t)
912        } else {
913            let i = (t.floor() as usize).clamp(0, self.segments.len() - 1);
914            (&self.segments[i], t - i as f32)
915        }
916    }
917}
918
919impl<P: VectorSpace> Extend<CubicSegment<P>> for CubicCurve<P> {
920    fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) {
921        self.segments.extend(iter);
922    }
923}
924
925impl<P: VectorSpace> IntoIterator for CubicCurve<P> {
926    type IntoIter = <Vec<CubicSegment<P>> as IntoIterator>::IntoIter;
927
928    type Item = CubicSegment<P>;
929
930    fn into_iter(self) -> Self::IntoIter {
931        self.segments.into_iter()
932    }
933}
934
935/// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters.
936pub trait RationalGenerator<P: VectorSpace> {
937    /// Build a [`RationalCurve`] by computing the interpolation coefficients for each curve segment.
938    fn to_curve(&self) -> RationalCurve<P>;
939}
940
941/// A segment of a rational cubic curve, used to hold precomputed coefficients for fast interpolation.
942/// Can be evaluated as a parametric curve over the domain `[0, knot_span)`.
943///
944/// Segments can be chained together to form a longer compound curve.
945#[derive(Copy, Clone, Debug, Default, PartialEq)]
946#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))]
947pub struct RationalSegment<P: VectorSpace> {
948    /// The coefficients matrix of the cubic curve.
949    pub coeff: [P; 4],
950    /// The homogeneous weight coefficients.
951    pub weight_coeff: [f32; 4],
952    /// The width of the domain of this segment.
953    pub knot_span: f32,
954}
955
956impl<P: VectorSpace> RationalSegment<P> {
957    /// Instantaneous position of a point at parametric value `t` in `[0, knot_span)`.
958    #[inline]
959    pub fn position(&self, t: f32) -> P {
960        let [a, b, c, d] = self.coeff;
961        let [x, y, z, w] = self.weight_coeff;
962        // Compute a cubic polynomial for the control points
963        let numerator = a + (b + (c + d * t) * t) * t;
964        // Compute a cubic polynomial for the weights
965        let denominator = x + (y + (z + w * t) * t) * t;
966        numerator / denominator
967    }
968
969    /// Instantaneous velocity of a point at parametric value `t` in `[0, knot_span)`.
970    #[inline]
971    pub fn velocity(&self, t: f32) -> P {
972        // A derivation for the following equations can be found in "Matrix representation for NURBS
973        // curves and surfaces" by Choi et al. See equation 19.
974
975        let [a, b, c, d] = self.coeff;
976        let [x, y, z, w] = self.weight_coeff;
977        // Compute a cubic polynomial for the control points
978        let numerator = a + (b + (c + d * t) * t) * t;
979        // Compute a cubic polynomial for the weights
980        let denominator = x + (y + (z + w * t) * t) * t;
981
982        // Compute the derivative of the control point polynomial
983        let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;
984        // Compute the derivative of the weight polynomial
985        let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;
986
987        // Velocity is the first derivative (wrt to the parameter `t`)
988        // Position = N/D therefore
989        // Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^2
990        numerator_derivative / denominator
991            - numerator * (denominator_derivative / denominator.powi(2))
992    }
993
994    /// Instantaneous acceleration of a point at parametric value `t` in `[0, knot_span)`.
995    #[inline]
996    pub fn acceleration(&self, t: f32) -> P {
997        // A derivation for the following equations can be found in "Matrix representation for NURBS
998        // curves and surfaces" by Choi et al. See equation 20. Note: In come copies of this paper, equation 20
999        // is printed with the following two errors:
1000        // + The first term has incorrect sign.
1001        // + The second term uses R when it should use the first derivative.
1002
1003        let [a, b, c, d] = self.coeff;
1004        let [x, y, z, w] = self.weight_coeff;
1005        // Compute a cubic polynomial for the control points
1006        let numerator = a + (b + (c + d * t) * t) * t;
1007        // Compute a cubic polynomial for the weights
1008        let denominator = x + (y + (z + w * t) * t) * t;
1009
1010        // Compute the derivative of the control point polynomial
1011        let numerator_derivative = b + (c * 2.0 + d * 3.0 * t) * t;
1012        // Compute the derivative of the weight polynomial
1013        let denominator_derivative = y + (z * 2.0 + w * 3.0 * t) * t;
1014
1015        // Compute the second derivative of the control point polynomial
1016        let numerator_second_derivative = c * 2.0 + d * 6.0 * t;
1017        // Compute the second derivative of the weight polynomial
1018        let denominator_second_derivative = z * 2.0 + w * 6.0 * t;
1019
1020        // Velocity is the first derivative (wrt to the parameter `t`)
1021        // Position = N/D therefore
1022        // Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^2
1023        // Acceleration = (N/D)'' = ((N' * D - N * D')/D^2)' = N''/D + N' * (-2D'/D^2) + N * (-D''/D^2 + 2D'^2/D^3)
1024        numerator_second_derivative / denominator
1025            + numerator_derivative * (-2.0 * denominator_derivative / denominator.powi(2))
1026            + numerator
1027                * (-denominator_second_derivative / denominator.powi(2)
1028                    + 2.0 * denominator_derivative.powi(2) / denominator.powi(3))
1029    }
1030
1031    /// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix.
1032    #[inline]
1033    fn coefficients(
1034        control_points: [P; 4],
1035        weights: [f32; 4],
1036        knot_span: f32,
1037        char_matrix: [[f32; 4]; 4],
1038    ) -> Self {
1039        // An explanation of this use can be found in "Matrix representation for NURBS curves and surfaces"
1040        // by Choi et al. See section "Evaluation of NURB Curves and Surfaces", and equation 16.
1041
1042        let [c0, c1, c2, c3] = char_matrix;
1043        let p = control_points;
1044        let w = weights;
1045        // These are the control point polynomial coefficients, computed by multiplying the characteristic
1046        // matrix by the point matrix.
1047        let coeff = [
1048            p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
1049            p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
1050            p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
1051            p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
1052        ];
1053        // These are the weight polynomial coefficients, computed by multiplying the characteristic
1054        // matrix by the weight matrix.
1055        let weight_coeff = [
1056            w[0] * c0[0] + w[1] * c0[1] + w[2] * c0[2] + w[3] * c0[3],
1057            w[0] * c1[0] + w[1] * c1[1] + w[2] * c1[2] + w[3] * c1[3],
1058            w[0] * c2[0] + w[1] * c2[1] + w[2] * c2[2] + w[3] * c2[3],
1059            w[0] * c3[0] + w[1] * c3[1] + w[2] * c3[2] + w[3] * c3[3],
1060        ];
1061        Self {
1062            coeff,
1063            weight_coeff,
1064            knot_span,
1065        }
1066    }
1067}
1068
1069/// A collection of [`RationalSegment`]s chained into a single parametric curve.
1070///
1071/// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as
1072/// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`.
1073#[derive(Clone, Debug, PartialEq)]
1074#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
1075pub struct RationalCurve<P: VectorSpace> {
1076    /// The segments in the curve
1077    pub segments: Vec<RationalSegment<P>>,
1078}
1079
1080impl<P: VectorSpace> RationalCurve<P> {
1081    /// Compute the position of a point on the curve at the parametric value `t`.
1082    ///
1083    /// Note that `t` varies from `0..=(n_points - 3)`.
1084    #[inline]
1085    pub fn position(&self, t: f32) -> P {
1086        let (segment, t) = self.segment(t);
1087        segment.position(t)
1088    }
1089
1090    /// Compute the first derivative with respect to t at `t`. This is the instantaneous velocity of
1091    /// a point on the curve at `t`.
1092    ///
1093    /// Note that `t` varies from `0..=(n_points - 3)`.
1094    #[inline]
1095    pub fn velocity(&self, t: f32) -> P {
1096        let (segment, t) = self.segment(t);
1097        segment.velocity(t)
1098    }
1099
1100    /// Compute the second derivative with respect to t at `t`. This is the instantaneous
1101    /// acceleration of a point on the curve at `t`.
1102    ///
1103    /// Note that `t` varies from `0..=(n_points - 3)`.
1104    #[inline]
1105    pub fn acceleration(&self, t: f32) -> P {
1106        let (segment, t) = self.segment(t);
1107        segment.acceleration(t)
1108    }
1109
1110    /// A flexible iterator used to sample curves with arbitrary functions.
1111    ///
1112    /// This splits the curve into `subdivisions` of evenly spaced `t` values across the
1113    /// length of the curve from start (t = 0) to end (t = n), where `n = self.segment_count()`,
1114    /// returning an iterator evaluating the curve with the supplied `sample_function` at each `t`.
1115    ///
1116    /// For `subdivisions = 2`, this will split the curve into two lines, or three points, and
1117    /// return an iterator with 3 items, the three points, one at the start, middle, and end.
1118    #[inline]
1119    pub fn iter_samples<'a, 'b: 'a>(
1120        &'b self,
1121        subdivisions: usize,
1122        mut sample_function: impl FnMut(&Self, f32) -> P + 'a,
1123    ) -> impl Iterator<Item = P> + 'a {
1124        self.iter_uniformly(subdivisions)
1125            .map(move |t| sample_function(self, t))
1126    }
1127
1128    /// An iterator that returns values of `t` uniformly spaced over `0..=subdivisions`.
1129    #[inline]
1130    fn iter_uniformly(&self, subdivisions: usize) -> impl Iterator<Item = f32> {
1131        let domain = self.domain();
1132        let step = domain / subdivisions as f32;
1133        (0..=subdivisions).map(move |i| i as f32 * step)
1134    }
1135
1136    /// The list of segments contained in this `RationalCurve`.
1137    ///
1138    /// This spline's global `t` value is equal to how many segments it has.
1139    ///
1140    /// All method accepting `t` on `RationalCurve` depends on the global `t`.
1141    /// When sampling over the entire curve, you should either use one of the
1142    /// `iter_*` methods or account for the segment count using `curve.segments().len()`.
1143    #[inline]
1144    pub fn segments(&self) -> &[RationalSegment<P>] {
1145        &self.segments
1146    }
1147
1148    /// Iterate over the curve split into `subdivisions`, sampling the position at each step.
1149    pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1150        self.iter_samples(subdivisions, Self::position)
1151    }
1152
1153    /// Iterate over the curve split into `subdivisions`, sampling the velocity at each step.
1154    pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1155        self.iter_samples(subdivisions, Self::velocity)
1156    }
1157
1158    /// Iterate over the curve split into `subdivisions`, sampling the acceleration at each step.
1159    pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
1160        self.iter_samples(subdivisions, Self::acceleration)
1161    }
1162
1163    /// Adds a segment to the curve.
1164    #[inline]
1165    pub fn push_segment(&mut self, segment: RationalSegment<P>) {
1166        self.segments.push(segment);
1167    }
1168
1169    /// Returns the [`RationalSegment`] and local `t` value given a spline's global `t` value.
1170    /// Input `t` will be clamped to the domain of the curve. Returned value will be in `[0, 1]`.
1171    #[inline]
1172    fn segment(&self, mut t: f32) -> (&RationalSegment<P>, f32) {
1173        if t <= 0.0 {
1174            (&self.segments[0], 0.0)
1175        } else if self.segments.len() == 1 {
1176            (&self.segments[0], t / self.segments[0].knot_span)
1177        } else {
1178            // Try to fit t into each segment domain
1179            for segment in self.segments.iter() {
1180                if t < segment.knot_span {
1181                    // The division here makes t a normalized parameter in [0, 1] that can be properly
1182                    // evaluated against a cubic curve segment. See equations 6 & 16 from "Matrix representation
1183                    // of NURBS curves and surfaces" by Choi et al. or equation 3 from "General Matrix
1184                    // Representations for B-Splines" by Qin.
1185                    return (segment, t / segment.knot_span);
1186                }
1187                t -= segment.knot_span;
1188            }
1189            return (self.segments.last().unwrap(), 1.0);
1190        }
1191    }
1192
1193    /// Returns the length of of the domain of the parametric curve.
1194    #[inline]
1195    pub fn domain(&self) -> f32 {
1196        self.segments.iter().map(|segment| segment.knot_span).sum()
1197    }
1198}
1199
1200impl<P: VectorSpace> Extend<RationalSegment<P>> for RationalCurve<P> {
1201    fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) {
1202        self.segments.extend(iter);
1203    }
1204}
1205
1206impl<P: VectorSpace> IntoIterator for RationalCurve<P> {
1207    type IntoIter = <Vec<RationalSegment<P>> as IntoIterator>::IntoIter;
1208
1209    type Item = RationalSegment<P>;
1210
1211    fn into_iter(self) -> Self::IntoIter {
1212        self.segments.into_iter()
1213    }
1214}
1215
1216impl<P: VectorSpace> From<CubicSegment<P>> for RationalSegment<P> {
1217    fn from(value: CubicSegment<P>) -> Self {
1218        Self {
1219            coeff: value.coeff,
1220            weight_coeff: [1.0, 0.0, 0.0, 0.0],
1221            knot_span: 1.0, // Cubic curves are uniform, so every segment has domain [0, 1).
1222        }
1223    }
1224}
1225
1226impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> {
1227    fn from(value: CubicCurve<P>) -> Self {
1228        Self {
1229            segments: value.segments.into_iter().map(Into::into).collect(),
1230        }
1231    }
1232}
1233
1234#[cfg(test)]
1235mod tests {
1236    use glam::{vec2, Vec2};
1237
1238    use crate::cubic_splines::{
1239        CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve,
1240        RationalGenerator,
1241    };
1242
1243    /// How close two floats can be and still be considered equal
1244    const FLOAT_EQ: f32 = 1e-5;
1245
1246    /// Sweep along the full length of a 3D cubic Bezier, and manually check the position.
1247    #[test]
1248    fn cubic() {
1249        const N_SAMPLES: usize = 1000;
1250        let points = [[
1251            vec2(-1.0, -20.0),
1252            vec2(3.0, 2.0),
1253            vec2(5.0, 3.0),
1254            vec2(9.0, 8.0),
1255        ]];
1256        let bezier = CubicBezier::new(points).to_curve();
1257        for i in 0..=N_SAMPLES {
1258            let t = i as f32 / N_SAMPLES as f32; // Check along entire length
1259            assert!(bezier.position(t).distance(cubic_manual(t, points[0])) <= FLOAT_EQ);
1260        }
1261    }
1262
1263    /// Manual, hardcoded function for computing the position along a cubic bezier.
1264    fn cubic_manual(t: f32, points: [Vec2; 4]) -> Vec2 {
1265        let p = points;
1266        p[0] * (1.0 - t).powi(3)
1267            + 3.0 * p[1] * t * (1.0 - t).powi(2)
1268            + 3.0 * p[2] * t.powi(2) * (1.0 - t)
1269            + p[3] * t.powi(3)
1270    }
1271
1272    /// Basic cubic Bezier easing test to verify the shape of the curve.
1273    #[test]
1274    fn easing_simple() {
1275        // A curve similar to ease-in-out, but symmetric
1276        let bezier = CubicSegment::new_bezier([1.0, 0.0], [0.0, 1.0]);
1277        assert_eq!(bezier.ease(0.0), 0.0);
1278        assert!(bezier.ease(0.2) < 0.2); // tests curve
1279        assert_eq!(bezier.ease(0.5), 0.5); // true due to symmetry
1280        assert!(bezier.ease(0.8) > 0.8); // tests curve
1281        assert_eq!(bezier.ease(1.0), 1.0);
1282    }
1283
1284    /// A curve that forms an upside-down "U", that should extend below 0.0. Useful for animations
1285    /// that go beyond the start and end positions, e.g. bouncing.
1286    #[test]
1287    fn easing_overshoot() {
1288        // A curve that forms an upside-down "U", that should extend above 1.0
1289        let bezier = CubicSegment::new_bezier([0.0, 2.0], [1.0, 2.0]);
1290        assert_eq!(bezier.ease(0.0), 0.0);
1291        assert!(bezier.ease(0.5) > 1.5);
1292        assert_eq!(bezier.ease(1.0), 1.0);
1293    }
1294
1295    /// A curve that forms a "U", that should extend below 0.0. Useful for animations that go beyond
1296    /// the start and end positions, e.g. bouncing.
1297    #[test]
1298    fn easing_undershoot() {
1299        let bezier = CubicSegment::new_bezier([0.0, -2.0], [1.0, -2.0]);
1300        assert_eq!(bezier.ease(0.0), 0.0);
1301        assert!(bezier.ease(0.5) < -0.5);
1302        assert_eq!(bezier.ease(1.0), 1.0);
1303    }
1304
1305    /// Test that a simple cardinal spline passes through all of its control points with
1306    /// the correct tangents.
1307    #[test]
1308    fn cardinal_control_pts() {
1309        use super::CubicCardinalSpline;
1310
1311        let tension = 0.2;
1312        let [p0, p1, p2, p3] = [vec2(-1., -2.), vec2(0., 1.), vec2(1., 2.), vec2(-2., 1.)];
1313        let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3]).to_curve();
1314
1315        // Positions at segment endpoints
1316        assert!(curve.position(0.).abs_diff_eq(p0, FLOAT_EQ));
1317        assert!(curve.position(1.).abs_diff_eq(p1, FLOAT_EQ));
1318        assert!(curve.position(2.).abs_diff_eq(p2, FLOAT_EQ));
1319        assert!(curve.position(3.).abs_diff_eq(p3, FLOAT_EQ));
1320
1321        // Tangents at segment endpoints
1322        assert!(curve
1323            .velocity(0.)
1324            .abs_diff_eq((p1 - p0) * tension * 2., FLOAT_EQ));
1325        assert!(curve
1326            .velocity(1.)
1327            .abs_diff_eq((p2 - p0) * tension, FLOAT_EQ));
1328        assert!(curve
1329            .velocity(2.)
1330            .abs_diff_eq((p3 - p1) * tension, FLOAT_EQ));
1331        assert!(curve
1332            .velocity(3.)
1333            .abs_diff_eq((p3 - p2) * tension * 2., FLOAT_EQ));
1334    }
1335
1336    /// Test that [`RationalCurve`] properly generalizes [`CubicCurve`]. A Cubic upgraded to a rational
1337    /// should produce pretty much the same output.
1338    #[test]
1339    fn cubic_to_rational() {
1340        const EPSILON: f32 = 0.00001;
1341
1342        let points = [
1343            vec2(0.0, 0.0),
1344            vec2(1.0, 1.0),
1345            vec2(1.0, 1.0),
1346            vec2(2.0, -1.0),
1347            vec2(3.0, 1.0),
1348            vec2(0.0, 0.0),
1349        ];
1350
1351        let b_spline = CubicBSpline::new(points).to_curve();
1352        let rational_b_spline = RationalCurve::from(b_spline.clone());
1353
1354        /// Tests if two vectors of points are approximately the same
1355        fn compare_vectors(cubic_curve: Vec<Vec2>, rational_curve: Vec<Vec2>, name: &str) {
1356            assert_eq!(
1357                cubic_curve.len(),
1358                rational_curve.len(),
1359                "{name} vector lengths mismatch"
1360            );
1361            for (i, (a, b)) in cubic_curve.iter().zip(rational_curve.iter()).enumerate() {
1362                assert!(
1363                    a.distance(*b) < EPSILON,
1364                    "Mismatch at {name} value {i}. CubicCurve: {} Converted RationalCurve: {}",
1365                    a,
1366                    b
1367                );
1368            }
1369        }
1370
1371        // Both curves should yield the same values
1372        let cubic_positions: Vec<_> = b_spline.iter_positions(10).collect();
1373        let rational_positions: Vec<_> = rational_b_spline.iter_positions(10).collect();
1374        compare_vectors(cubic_positions, rational_positions, "position");
1375
1376        let cubic_velocities: Vec<_> = b_spline.iter_velocities(10).collect();
1377        let rational_velocities: Vec<_> = rational_b_spline.iter_velocities(10).collect();
1378        compare_vectors(cubic_velocities, rational_velocities, "velocity");
1379
1380        let cubic_accelerations: Vec<_> = b_spline.iter_accelerations(10).collect();
1381        let rational_accelerations: Vec<_> = rational_b_spline.iter_accelerations(10).collect();
1382        compare_vectors(cubic_accelerations, rational_accelerations, "acceleration");
1383    }
1384
1385    /// Test that a nurbs curve can approximate a portion of a circle.
1386    #[test]
1387    fn nurbs_circular_arc() {
1388        use std::f32::consts::FRAC_PI_2;
1389        const EPSILON: f32 = 0.0000001;
1390
1391        // The following NURBS parameters were determined by constraining the first two
1392        // points to the line y=1, the second two points to the line x=1, and the distance
1393        // between each pair of points to be equal. One can solve the weights by assuming the
1394        // first and last weights to be one, the intermediate weights to be equal, and
1395        // subjecting ones self to a lot of tedious matrix algebra.
1396
1397        let alpha = FRAC_PI_2;
1398        let leg = 2.0 * f32::sin(alpha / 2.0) / (1.0 + 2.0 * f32::cos(alpha / 2.0));
1399        let weight = (1.0 + 2.0 * f32::cos(alpha / 2.0)) / 3.0;
1400        let points = [
1401            vec2(1.0, 0.0),
1402            vec2(1.0, leg),
1403            vec2(leg, 1.0),
1404            vec2(0.0, 1.0),
1405        ];
1406        let weights = [1.0, weight, weight, 1.0];
1407        let knots = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
1408        let spline = CubicNurbs::new(points, Some(weights), Some(knots)).unwrap();
1409        let curve = spline.to_curve();
1410        for (i, point) in curve.iter_positions(10).enumerate() {
1411            assert!(
1412                f32::abs(point.length() - 1.0) < EPSILON,
1413                "Point {i} is not on the unit circle: {point:?} has length {}",
1414                point.length()
1415            );
1416        }
1417    }
1418}