Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_math/src/curve/mod.rs
6596 views
1
//! The [`Curve`] trait, providing a domain-agnostic description of curves.
2
//!
3
//! ## Overview
4
//!
5
//! At a high level, [`Curve`] is a trait that abstracts away the implementation details of curves,
6
//! which comprise any kind of data parametrized by a single continuous variable. For example, that
7
//! variable could represent time, in which case a curve would represent a value that changes over
8
//! time, as in animation; on the other hand, it could represent something like displacement or
9
//! distance, as in graphs, gradients, and curves in space.
10
//!
11
//! The trait itself has two fundamental components: a curve must have a [domain], which is a nonempty
12
//! range of `f32` values, and it must be able to be [sampled] on every one of those values, producing
13
//! output of some fixed type.
14
//!
15
//! A primary goal of the trait is to allow interfaces to simply accept `impl Curve<T>` as input
16
//! rather than requiring for input curves to be defined in data in any particular way. This is
17
//! supported by a number of interface methods which allow [changing parametrizations], [mapping output],
18
//! and [rasterization].
19
//!
20
//! ## Analogy with `Iterator`
21
//!
22
//! The `Curve` API behaves, in many ways, like a continuous counterpart to [`Iterator`]. The analogy
23
//! looks something like this with some of the common methods:
24
//!
25
//! | Iterators | Curves |
26
//! | :--------------- | :-------------- |
27
//! | `map` | `map` |
28
//! | `skip`/`step_by` | `reparametrize` |
29
//! | `enumerate` | `graph` |
30
//! | `chain` | `chain` |
31
//! | `zip` | `zip` |
32
//! | `rev` | `reverse` |
33
//! | `by_ref` | `by_ref` |
34
//!
35
//! Of course, there are very important differences, as well. For instance, the continuous nature of
36
//! curves means that many iterator methods make little sense in the context of curves, or at least
37
//! require numerical techniques. For example, the analogue of `sum` would be an integral, approximated
38
//! by something like Riemann summation.
39
//!
40
//! Furthermore, the two also differ greatly in their orientation to borrowing and mutation:
41
//! iterators are mutated by being iterated, and by contrast, all curve methods are immutable. More
42
//! information on the implications of this can be found [below](self#Ownership-and-borrowing).
43
//!
44
//! ## Defining curves
45
//!
46
//! Curves may be defined in a number of ways. The following are common:
47
//! - using [functions];
48
//! - using [sample interpolation];
49
//! - using [splines];
50
//! - using [easings].
51
//!
52
//! Among these, the first is the most versatile[^footnote]: the domain and the sampling output are just
53
//! specified directly in the construction. For this reason, function curves are a reliable go-to for
54
//! simple one-off constructions and procedural uses, where flexibility is desirable. For example:
55
//! ```rust
56
//! # use bevy_math::vec3;
57
//! # use bevy_math::curve::*;
58
//! // A sinusoid:
59
//! let sine_curve = FunctionCurve::new(Interval::EVERYWHERE, f32::sin);
60
//!
61
//! // A sawtooth wave:
62
//! let sawtooth_curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t % 1.0);
63
//!
64
//! // A helix:
65
//! let helix_curve = FunctionCurve::new(Interval::EVERYWHERE, |theta| vec3(theta.sin(), theta, theta.cos()));
66
//! ```
67
//!
68
//! Sample-interpolated curves commonly arises in both rasterization and in animation, and this library
69
//! has support for producing them in both fashions. See [below](self#Resampling-and-rasterization) for
70
//! more information about rasterization. Here is what an explicit sample-interpolated curve might look like:
71
//! ```rust
72
//! # use bevy_math::prelude::*;
73
//! # use std::f32::consts::FRAC_PI_2;
74
//! // A list of angles that we want to traverse:
75
//! let angles = [
76
//! 0.0,
77
//! -FRAC_PI_2,
78
//! 0.0,
79
//! FRAC_PI_2,
80
//! 0.0
81
//! ];
82
//!
83
//! // Make each angle into a rotation by that angle:
84
//! let rotations = angles.map(|angle| Rot2::radians(angle));
85
//!
86
//! // Interpolate these rotations with a `Rot2`-valued curve:
87
//! let rotation_curve = SampleAutoCurve::new(interval(0.0, 4.0).unwrap(), rotations).unwrap();
88
//! ```
89
//!
90
//! For more information on [spline curves] and [easing curves], see their respective modules.
91
//!
92
//! And, of course, you are also free to define curve types yourself, implementing the trait directly.
93
//! For custom sample-interpolated curves, the [`cores`] submodule provides machinery to avoid having to
94
//! reimplement interpolation logic yourself. In many other cases, implementing the trait directly is
95
//! often quite straightforward:
96
//! ```rust
97
//! # use bevy_math::prelude::*;
98
//! struct ExponentialCurve {
99
//! exponent: f32,
100
//! }
101
//!
102
//! impl Curve<f32> for ExponentialCurve {
103
//! fn domain(&self) -> Interval {
104
//! Interval::EVERYWHERE
105
//! }
106
//!
107
//! fn sample_unchecked(&self, t: f32) -> f32 {
108
//! f32::exp(self.exponent * t)
109
//! }
110
//!
111
//! // All other trait methods can be inferred from these.
112
//! }
113
//! ```
114
//!
115
//! ## Transforming curves
116
//!
117
//! The API provides a few key ways of transforming one curve into another. These are often useful when
118
//! you would like to make use of an interface that requires a curve that bears some logical relationship
119
//! to one that you already have access to, but with different requirements or expectations. For example,
120
//! the output type of the curves may differ, or the domain may be expected to be different. The `map`
121
//! and `reparametrize` methods can help address this.
122
//!
123
//! As a simple example of the kind of thing that arises in practice, let's imagine that we have a
124
//! `Curve<Vec2>` that we want to use to describe the motion of some object over time, but the interface
125
//! for animation expects a `Curve<Vec3>`, since the object will move in three dimensions:
126
//! ```rust
127
//! # use bevy_math::{vec2, prelude::*};
128
//! # use std::f32::consts::TAU;
129
//! // Our original curve, which may look something like this:
130
//! let ellipse_curve = FunctionCurve::new(
131
//! interval(0.0, TAU).unwrap(),
132
//! |t| vec2(t.cos(), t.sin() * 2.0)
133
//! );
134
//!
135
//! // Use `map` to situate this in 3D as a Curve<Vec3>; in this case, it will be in the xy-plane:
136
//! let ellipse_motion_curve = ellipse_curve.map(|pos| pos.extend(0.0));
137
//! ```
138
//!
139
//! We might imagine further still that the interface expects the curve to have domain `[0, 1]`. The
140
//! `reparametrize` methods can address this:
141
//! ```rust
142
//! # use bevy_math::{vec2, prelude::*};
143
//! # use std::f32::consts::TAU;
144
//! # let ellipse_curve = FunctionCurve::new(interval(0.0, TAU).unwrap(), |t| vec2(t.cos(), t.sin() * 2.0));
145
//! # let ellipse_motion_curve = ellipse_curve.map(|pos| pos.extend(0.0));
146
//! // Change the domain to `[0, 1]` instead of `[0, TAU]`:
147
//! let final_curve = ellipse_motion_curve.reparametrize_linear(Interval::UNIT).unwrap();
148
//! ```
149
//!
150
//! Of course, there are many other ways of using these methods. In general, `map` is used for transforming
151
//! the output and using it to drive something else, while `reparametrize` preserves the curve's shape but
152
//! changes the speed and direction in which it is traversed. For instance:
153
//! ```rust
154
//! # use bevy_math::{vec2, prelude::*};
155
//! // A line segment curve connecting two points in the plane:
156
//! let start = vec2(-1.0, 1.0);
157
//! let end = vec2(1.0, 1.0);
158
//! let segment = FunctionCurve::new(Interval::UNIT, |t| start.lerp(end, t));
159
//!
160
//! // Let's make a curve that goes back and forth along this line segment forever.
161
//! //
162
//! // Start by stretching the line segment in parameter space so that it travels along its length
163
//! // from `-1` to `1` instead of `0` to `1`:
164
//! let stretched_segment = segment.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap();
165
//!
166
//! // Now, the *output* of `f32::sin` in `[-1, 1]` corresponds to the *input* interval of
167
//! // `stretched_segment`; the sinusoid output is mapped to the input parameter and controls how
168
//! // far along the segment we are:
169
//! let back_and_forth_curve = stretched_segment.reparametrize(Interval::EVERYWHERE, f32::sin);
170
//! ```
171
//!
172
//! ## Combining curves
173
//!
174
//! Curves become more expressive when used together. For example, maybe you want to combine two
175
//! curves end-to-end:
176
//! ```rust
177
//! # use bevy_math::{vec2, prelude::*};
178
//! # use std::f32::consts::PI;
179
//! // A line segment connecting `(-1, 0)` to `(0, 0)`:
180
//! let line_curve = FunctionCurve::new(
181
//! Interval::UNIT,
182
//! |t| vec2(-1.0, 0.0).lerp(vec2(0.0, 0.0), t)
183
//! );
184
//!
185
//! // A half-circle curve starting at `(0, 0)`:
186
//! let half_circle_curve = FunctionCurve::new(
187
//! interval(0.0, PI).unwrap(),
188
//! |t| vec2(t.cos() * -1.0 + 1.0, t.sin())
189
//! );
190
//!
191
//! // A curve that traverses `line_curve` and then `half_circle_curve` over the interval
192
//! // from `0` to `PI + 1`:
193
//! let combined_curve = line_curve.chain(half_circle_curve).unwrap();
194
//! ```
195
//!
196
//! Or, instead, maybe you want to combine two curves the *other* way, producing a single curve
197
//! that combines their output in a tuple:
198
//! ```rust
199
//! # use bevy_math::{vec2, prelude::*};
200
//! // Some entity's position in 2D:
201
//! let position_curve = FunctionCurve::new(Interval::UNIT, |t| vec2(t.cos(), t.sin()));
202
//!
203
//! // The same entity's orientation, described as a rotation. (In this case it will be spinning.)
204
//! let orientation_curve = FunctionCurve::new(Interval::UNIT, |t| Rot2::radians(5.0 * t));
205
//!
206
//! // Both in one curve with `(Vec2, Rot2)` output:
207
//! let position_and_orientation = position_curve.zip(orientation_curve).unwrap();
208
//! ```
209
//!
210
//! See the documentation on [`chain`] and [`zip`] for more details on how these methods work.
211
//!
212
//! ## <a name="Resampling-and-rasterization"></a>Resampling and rasterization
213
//!
214
//! Sometimes, for reasons of portability, performance, or otherwise, it can be useful to ensure that
215
//! curves of various provenance all actually share the same concrete type. This is the purpose of the
216
//! [`resample`] family of functions: they allow a curve to be replaced by an approximate version of
217
//! itself defined by interpolation over samples from the original curve.
218
//!
219
//! In effect, this allows very different curves to be rasterized and treated uniformly. For example:
220
//! ```rust
221
//! # use bevy_math::{vec2, prelude::*};
222
//! // A curve that is not easily transported because it relies on evaluating a function:
223
//! let interesting_curve = FunctionCurve::new(Interval::UNIT, |t| vec2(t * 3.0, t.exp()));
224
//!
225
//! // A rasterized form of the preceding curve which is just a `SampleAutoCurve`. Inside, this
226
//! // just stores an `Interval` along with a buffer of sample data, so it's easy to serialize
227
//! // and deserialize:
228
//! let resampled_curve = interesting_curve.resample_auto(100).unwrap();
229
//!
230
//! // The rasterized form can be seamlessly used as a curve itself:
231
//! let some_value = resampled_curve.sample(0.5).unwrap();
232
//! ```
233
//!
234
//! ## <a name="Ownership-and-borrowing"></a>Ownership and borrowing
235
//!
236
//! It can be easy to get tripped up by how curves specifically interact with Rust's ownership semantics.
237
//! First of all, it's worth noting that the API never uses `&mut self` — every method either takes
238
//! ownership of the original curve or uses a shared reference.
239
//!
240
//! Because of the methods that take ownership, it is useful to be aware of the following:
241
//! - If `curve` is a curve, then `&curve` is also a curve with the same output. For convenience,
242
//! `&curve` can be written as `curve.by_ref()` for use in method chaining.
243
//! - However, `&curve` cannot outlive `curve`. In general, it is not `'static`.
244
//!
245
//! In other words, `&curve` can be used to perform temporary operations without consuming `curve` (for
246
//! example, to effectively pass `curve` into an API which expects an `impl Curve<T>`), but it *cannot*
247
//! be used in situations where persistence is necessary (e.g. when the curve itself must be stored
248
//! for later use).
249
//!
250
//! Here is a demonstration:
251
//! ```rust
252
//! # use bevy_math::prelude::*;
253
//! # let some_magic_constructor = || EasingCurve::new(0.0, 1.0, EaseFunction::ElasticInOut).graph();
254
//! //`my_curve` is obtained somehow. It is a `Curve<(f32, f32)>`.
255
//! let my_curve = some_magic_constructor();
256
//!
257
//! // Now, we want to sample a mapped version of `my_curve`.
258
//!
259
//! // let samples: Vec<f32> = my_curve.map(|(x, y)| y).samples(50).unwrap().collect();
260
//! // ^ This would work, but it would also invalidate `my_curve`, since `map` takes ownership.
261
//!
262
//! // Instead, we pass a borrowed version of `my_curve` to `map`. It lives long enough that we
263
//! // can extract samples:
264
//! let samples: Vec<f32> = my_curve.by_ref().map(|(x, y)| y).samples(50).unwrap().collect();
265
//!
266
//! // This way, we retain the ability to use `my_curve` later:
267
//! let new_curve = my_curve.map(|(x,y)| x + y);
268
//! ```
269
//!
270
//! [domain]: Curve::domain
271
//! [sampled]: Curve::sample
272
//! [changing parametrizations]: CurveExt::reparametrize
273
//! [mapping output]: CurveExt::map
274
//! [rasterization]: CurveResampleExt::resample
275
//! [functions]: FunctionCurve
276
//! [sample interpolation]: SampleCurve
277
//! [splines]: crate::cubic_splines
278
//! [easings]: easing
279
//! [spline curves]: crate::cubic_splines
280
//! [easing curves]: easing
281
//! [`chain`]: CurveExt::chain
282
//! [`zip`]: CurveExt::zip
283
//! [`resample`]: CurveResampleExt::resample
284
//!
285
//! [^footnote]: In fact, universal as well, in some sense: if `curve` is any curve, then `FunctionCurve::new
286
//! (curve.domain(), |t| curve.sample_unchecked(t))` is an equivalent function curve.
287
288
pub mod adaptors;
289
pub mod cores;
290
pub mod derivatives;
291
pub mod easing;
292
pub mod interval;
293
pub mod iterable;
294
295
#[cfg(feature = "alloc")]
296
pub mod sample_curves;
297
298
// bevy_math::curve re-exports all commonly-needed curve-related items.
299
pub use adaptors::*;
300
pub use easing::*;
301
pub use interval::{interval, Interval};
302
303
#[cfg(feature = "alloc")]
304
pub use {
305
cores::{EvenCore, UnevenCore},
306
sample_curves::*,
307
};
308
309
use crate::VectorSpace;
310
use core::{marker::PhantomData, ops::Deref};
311
use interval::InvalidIntervalError;
312
use thiserror::Error;
313
314
#[cfg(feature = "alloc")]
315
use {crate::StableInterpolate, itertools::Itertools};
316
317
/// A trait for a type that can represent values of type `T` parametrized over a fixed interval.
318
///
319
/// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds
320
/// of output data can be represented as well. See the [module-level documentation] for details.
321
///
322
/// [module-level documentation]: self
323
pub trait Curve<T> {
324
/// The interval over which this curve is parametrized.
325
///
326
/// This is the range of values of `t` where we can sample the curve and receive valid output.
327
fn domain(&self) -> Interval;
328
329
/// Sample a point on this curve at the parameter value `t`, extracting the associated value.
330
/// This is the unchecked version of sampling, which should only be used if the sample time `t`
331
/// is already known to lie within the curve's domain.
332
///
333
/// Values sampled from outside of a curve's domain are generally considered invalid; data which
334
/// is nonsensical or otherwise useless may be returned in such a circumstance, and extrapolation
335
/// beyond a curve's domain should not be relied upon.
336
fn sample_unchecked(&self, t: f32) -> T;
337
338
/// Sample a point on this curve at the parameter value `t`, returning `None` if the point is
339
/// outside of the curve's domain.
340
fn sample(&self, t: f32) -> Option<T> {
341
match self.domain().contains(t) {
342
true => Some(self.sample_unchecked(t)),
343
false => None,
344
}
345
}
346
347
/// Sample a point on this curve at the parameter value `t`, clamping `t` to lie inside the
348
/// domain of the curve.
349
fn sample_clamped(&self, t: f32) -> T {
350
let t = self.domain().clamp(t);
351
self.sample_unchecked(t)
352
}
353
}
354
355
impl<T, C, D> Curve<T> for D
356
where
357
C: Curve<T> + ?Sized,
358
D: Deref<Target = C>,
359
{
360
fn domain(&self) -> Interval {
361
<C as Curve<T>>::domain(self)
362
}
363
364
fn sample_unchecked(&self, t: f32) -> T {
365
<C as Curve<T>>::sample_unchecked(self, t)
366
}
367
}
368
369
/// Extension trait implemented by [curves], allowing access to a number of adaptors and
370
/// convenience methods.
371
///
372
/// This trait is automatically implemented for all curves that are `Sized`. In particular,
373
/// it is implemented for types like `Box<dyn Curve<T>>`. `CurveExt` is not dyn-compatible
374
/// itself.
375
///
376
/// For more information, see the [module-level documentation].
377
///
378
/// [curves]: Curve
379
/// [module-level documentation]: self
380
pub trait CurveExt<T>: Curve<T> + Sized {
381
/// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`,
382
/// returning `None` if the point is outside of the curve's domain.
383
///
384
/// The samples are returned in the same order as the parameter values `t_n` were provided and
385
/// will include all results. This leaves the responsibility for things like filtering and
386
/// sorting to the user for maximum flexibility.
387
fn sample_iter(&self, iter: impl IntoIterator<Item = f32>) -> impl Iterator<Item = Option<T>> {
388
iter.into_iter().map(|t| self.sample(t))
389
}
390
391
/// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`,
392
/// extracting the associated values. This is the unchecked version of sampling, which should
393
/// only be used if the sample times `t_n` are already known to lie within the curve's domain.
394
///
395
/// Values sampled from outside of a curve's domain are generally considered invalid; data
396
/// which is nonsensical or otherwise useless may be returned in such a circumstance, and
397
/// extrapolation beyond a curve's domain should not be relied upon.
398
///
399
/// The samples are returned in the same order as the parameter values `t_n` were provided and
400
/// will include all results. This leaves the responsibility for things like filtering and
401
/// sorting to the user for maximum flexibility.
402
fn sample_iter_unchecked(
403
&self,
404
iter: impl IntoIterator<Item = f32>,
405
) -> impl Iterator<Item = T> {
406
iter.into_iter().map(|t| self.sample_unchecked(t))
407
}
408
409
/// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`,
410
/// clamping `t_n` to lie inside the domain of the curve.
411
///
412
/// The samples are returned in the same order as the parameter values `t_n` were provided and
413
/// will include all results. This leaves the responsibility for things like filtering and
414
/// sorting to the user for maximum flexibility.
415
fn sample_iter_clamped(&self, iter: impl IntoIterator<Item = f32>) -> impl Iterator<Item = T> {
416
iter.into_iter().map(|t| self.sample_clamped(t))
417
}
418
419
/// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the
420
/// sample at time `t` for this curve is `x`, the value at time `t` on the new curve will be
421
/// `f(x)`.
422
#[must_use]
423
fn map<S, F>(self, f: F) -> MapCurve<T, S, Self, F>
424
where
425
F: Fn(T) -> S,
426
{
427
MapCurve {
428
preimage: self,
429
f,
430
_phantom: PhantomData,
431
}
432
}
433
434
/// Create a new [`Curve`] whose parameter space is related to the parameter space of this curve
435
/// by `f`. For each time `t`, the sample from the new curve at time `t` is the sample from
436
/// this curve at time `f(t)`. The given `domain` will be the domain of the new curve. The
437
/// function `f` is expected to take `domain` into `self.domain()`.
438
///
439
/// Note that this is the opposite of what one might expect intuitively; for example, if this
440
/// curve has a parameter domain of `[0, 1]`, then stretching the parameter domain to
441
/// `[0, 2]` would be performed as follows, dividing by what might be perceived as the scaling
442
/// factor rather than multiplying:
443
/// ```
444
/// # use bevy_math::curve::*;
445
/// let my_curve = ConstantCurve::new(Interval::UNIT, 1.0);
446
/// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0);
447
/// ```
448
/// This kind of linear remapping is provided by the convenience method
449
/// [`CurveExt::reparametrize_linear`], which requires only the desired domain for the new curve.
450
///
451
/// # Examples
452
/// ```
453
/// // Reverse a curve:
454
/// # use bevy_math::curve::*;
455
/// # use bevy_math::vec2;
456
/// let my_curve = ConstantCurve::new(Interval::UNIT, 1.0);
457
/// let domain = my_curve.domain();
458
/// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - (t - domain.start()));
459
///
460
/// // Take a segment of a curve:
461
/// # let my_curve = ConstantCurve::new(Interval::UNIT, 1.0);
462
/// let curve_segment = my_curve.reparametrize(interval(0.0, 0.5).unwrap(), |t| 0.5 + t);
463
/// ```
464
#[must_use]
465
fn reparametrize<F>(self, domain: Interval, f: F) -> ReparamCurve<T, Self, F>
466
where
467
F: Fn(f32) -> f32,
468
{
469
ReparamCurve {
470
domain,
471
base: self,
472
f,
473
_phantom: PhantomData,
474
}
475
}
476
477
/// Linearly reparametrize this [`Curve`], producing a new curve whose domain is the given
478
/// `domain` instead of the current one. This operation is only valid for curves with bounded
479
/// domains.
480
///
481
/// # Errors
482
///
483
/// If either this curve's domain or the given `domain` is unbounded, an error is returned.
484
fn reparametrize_linear(
485
self,
486
domain: Interval,
487
) -> Result<LinearReparamCurve<T, Self>, LinearReparamError> {
488
if !self.domain().is_bounded() {
489
return Err(LinearReparamError::SourceCurveUnbounded);
490
}
491
492
if !domain.is_bounded() {
493
return Err(LinearReparamError::TargetIntervalUnbounded);
494
}
495
496
Ok(LinearReparamCurve {
497
base: self,
498
new_domain: domain,
499
_phantom: PhantomData,
500
})
501
}
502
503
/// Reparametrize this [`Curve`] by sampling from another curve.
504
///
505
/// The resulting curve samples at time `t` by first sampling `other` at time `t`, which produces
506
/// another sample time `s` which is then used to sample this curve. The domain of the resulting
507
/// curve is the domain of `other`.
508
#[must_use]
509
fn reparametrize_by_curve<C>(self, other: C) -> CurveReparamCurve<T, Self, C>
510
where
511
C: Curve<f32>,
512
{
513
CurveReparamCurve {
514
base: self,
515
reparam_curve: other,
516
_phantom: PhantomData,
517
}
518
}
519
520
/// Create a new [`Curve`] which is the graph of this one; that is, its output echoes the sample
521
/// time as part of a tuple.
522
///
523
/// For example, if this curve outputs `x` at time `t`, then the produced curve will produce
524
/// `(t, x)` at time `t`. In particular, if this curve is a `Curve<T>`, the output of this method
525
/// is a `Curve<(f32, T)>`.
526
#[must_use]
527
fn graph(self) -> GraphCurve<T, Self> {
528
GraphCurve {
529
base: self,
530
_phantom: PhantomData,
531
}
532
}
533
534
/// Create a new [`Curve`] by zipping this curve together with another.
535
///
536
/// The sample at time `t` in the new curve is `(x, y)`, where `x` is the sample of `self` at
537
/// time `t` and `y` is the sample of `other` at time `t`. The domain of the new curve is the
538
/// intersection of the domains of its constituents.
539
///
540
/// # Errors
541
///
542
/// If the domain intersection would be empty, an error is returned instead.
543
fn zip<S, C>(self, other: C) -> Result<ZipCurve<T, S, Self, C>, InvalidIntervalError>
544
where
545
C: Curve<S> + Sized,
546
{
547
let domain = self.domain().intersect(other.domain())?;
548
Ok(ZipCurve {
549
domain,
550
first: self,
551
second: other,
552
_phantom: PhantomData,
553
})
554
}
555
556
/// Create a new [`Curve`] by composing this curve end-to-start with another, producing another curve
557
/// with outputs of the same type. The domain of the other curve is translated so that its start
558
/// coincides with where this curve ends.
559
///
560
/// # Errors
561
///
562
/// A [`ChainError`] is returned if this curve's domain doesn't have a finite end or if
563
/// `other`'s domain doesn't have a finite start.
564
fn chain<C>(self, other: C) -> Result<ChainCurve<T, Self, C>, ChainError>
565
where
566
C: Curve<T>,
567
{
568
if !self.domain().has_finite_end() {
569
return Err(ChainError::FirstEndInfinite);
570
}
571
if !other.domain().has_finite_start() {
572
return Err(ChainError::SecondStartInfinite);
573
}
574
Ok(ChainCurve {
575
first: self,
576
second: other,
577
_phantom: PhantomData,
578
})
579
}
580
581
/// Create a new [`Curve`] inverting this curve on the x-axis, producing another curve with
582
/// outputs of the same type, effectively playing backwards starting at `self.domain().end()`
583
/// and transitioning over to `self.domain().start()`. The domain of the new curve is still the
584
/// same.
585
///
586
/// # Errors
587
///
588
/// A [`ReverseError`] is returned if this curve's domain isn't bounded.
589
fn reverse(self) -> Result<ReverseCurve<T, Self>, ReverseError> {
590
self.domain()
591
.is_bounded()
592
.then(|| ReverseCurve {
593
curve: self,
594
_phantom: PhantomData,
595
})
596
.ok_or(ReverseError::SourceDomainEndInfinite)
597
}
598
599
/// Create a new [`Curve`] repeating this curve `N` times, producing another curve with outputs
600
/// of the same type. The domain of the new curve will be bigger by a factor of `n + 1`.
601
///
602
/// # Notes
603
///
604
/// - this doesn't guarantee a smooth transition from one occurrence of the curve to its next
605
/// iteration. The curve will make a jump if `self.domain().start() != self.domain().end()`!
606
/// - for `count == 0` the output of this adaptor is basically identical to the previous curve
607
/// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the
608
/// value at `domain.end()` in the original curve
609
///
610
/// # Errors
611
///
612
/// A [`RepeatError`] is returned if this curve's domain isn't bounded.
613
fn repeat(self, count: usize) -> Result<RepeatCurve<T, Self>, RepeatError> {
614
self.domain()
615
.is_bounded()
616
.then(|| {
617
// This unwrap always succeeds because `curve` has a valid Interval as its domain and the
618
// length of `curve` cannot be NAN. It's still fine if it's infinity.
619
let domain = Interval::new(
620
self.domain().start(),
621
self.domain().end() + self.domain().length() * count as f32,
622
)
623
.unwrap();
624
RepeatCurve {
625
domain,
626
curve: self,
627
_phantom: PhantomData,
628
}
629
})
630
.ok_or(RepeatError::SourceDomainUnbounded)
631
}
632
633
/// Create a new [`Curve`] repeating this curve forever, producing another curve with
634
/// outputs of the same type. The domain of the new curve will be unbounded.
635
///
636
/// # Notes
637
///
638
/// - this doesn't guarantee a smooth transition from one occurrence of the curve to its next
639
/// iteration. The curve will make a jump if `self.domain().start() != self.domain().end()`!
640
/// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the
641
/// value at `domain.end()` in the original curve
642
///
643
/// # Errors
644
///
645
/// A [`RepeatError`] is returned if this curve's domain isn't bounded.
646
fn forever(self) -> Result<ForeverCurve<T, Self>, RepeatError> {
647
self.domain()
648
.is_bounded()
649
.then(|| ForeverCurve {
650
curve: self,
651
_phantom: PhantomData,
652
})
653
.ok_or(RepeatError::SourceDomainUnbounded)
654
}
655
656
/// Create a new [`Curve`] chaining the original curve with its inverse, producing
657
/// another curve with outputs of the same type. The domain of the new curve will be twice as
658
/// long. The transition point is guaranteed to not make any jumps.
659
///
660
/// # Errors
661
///
662
/// A [`PingPongError`] is returned if this curve's domain isn't right-finite.
663
fn ping_pong(self) -> Result<PingPongCurve<T, Self>, PingPongError> {
664
self.domain()
665
.has_finite_end()
666
.then(|| PingPongCurve {
667
curve: self,
668
_phantom: PhantomData,
669
})
670
.ok_or(PingPongError::SourceDomainEndInfinite)
671
}
672
673
/// Create a new [`Curve`] by composing this curve end-to-start with another, producing another
674
/// curve with outputs of the same type. The domain of the other curve is translated so that
675
/// its start coincides with where this curve ends.
676
///
677
///
678
/// Additionally the transition of the samples is guaranteed to make no sudden jumps. This is
679
/// useful if you really just know about the shapes of your curves and don't want to deal with
680
/// stitching them together properly when it would just introduce useless complexity. It is
681
/// realized by translating the other curve so that its start sample point coincides with the
682
/// current curves' end sample point.
683
///
684
/// # Errors
685
///
686
/// A [`ChainError`] is returned if this curve's domain doesn't have a finite end or if
687
/// `other`'s domain doesn't have a finite start.
688
fn chain_continue<C>(self, other: C) -> Result<ContinuationCurve<T, Self, C>, ChainError>
689
where
690
T: VectorSpace,
691
C: Curve<T>,
692
{
693
if !self.domain().has_finite_end() {
694
return Err(ChainError::FirstEndInfinite);
695
}
696
if !other.domain().has_finite_start() {
697
return Err(ChainError::SecondStartInfinite);
698
}
699
700
let offset = self.sample_unchecked(self.domain().end())
701
- other.sample_unchecked(self.domain().start());
702
703
Ok(ContinuationCurve {
704
first: self,
705
second: other,
706
offset,
707
_phantom: PhantomData,
708
})
709
}
710
711
/// Extract an iterator over evenly-spaced samples from this curve.
712
///
713
/// # Errors
714
///
715
/// If `samples` is less than 2 or if this curve has unbounded domain, a [`ResamplingError`]
716
/// is returned.
717
fn samples(&self, samples: usize) -> Result<impl Iterator<Item = T>, ResamplingError> {
718
if samples < 2 {
719
return Err(ResamplingError::NotEnoughSamples(samples));
720
}
721
if !self.domain().is_bounded() {
722
return Err(ResamplingError::UnboundedDomain);
723
}
724
725
// Unwrap on `spaced_points` always succeeds because its error conditions are handled
726
// above.
727
Ok(self
728
.domain()
729
.spaced_points(samples)
730
.unwrap()
731
.map(|t| self.sample_unchecked(t)))
732
}
733
734
/// Borrow this curve rather than taking ownership of it. This is essentially an alias for a
735
/// prefix `&`; the point is that intermediate operations can be performed while retaining
736
/// access to the original curve.
737
///
738
/// # Example
739
/// ```
740
/// # use bevy_math::curve::*;
741
/// let my_curve = FunctionCurve::new(Interval::UNIT, |t| t * t + 1.0);
742
///
743
/// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes
744
/// // ownership of its input.
745
/// let samples = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap();
746
///
747
/// // Do something else with `my_curve` since we retained ownership:
748
/// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap();
749
/// ```
750
fn by_ref(&self) -> &Self {
751
self
752
}
753
754
/// Flip this curve so that its tuple output is arranged the other way.
755
#[must_use]
756
fn flip<U, V>(self) -> impl Curve<(V, U)>
757
where
758
Self: CurveExt<(U, V)>,
759
{
760
self.map(|(u, v)| (v, u))
761
}
762
}
763
764
impl<C, T> CurveExt<T> for C where C: Curve<T> {}
765
766
/// Extension trait implemented by [curves], allowing access to generic resampling methods as
767
/// well as those based on [stable interpolation].
768
///
769
/// This trait is automatically implemented for all curves.
770
///
771
/// For more information, see the [module-level documentation].
772
///
773
/// [curves]: Curve
774
/// [stable interpolation]: crate::StableInterpolate
775
/// [module-level documentation]: self
776
#[cfg(feature = "alloc")]
777
pub trait CurveResampleExt<T>: Curve<T> {
778
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
779
/// spaced sample values, using the provided `interpolation` to interpolate between adjacent samples.
780
/// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
781
/// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
782
/// the midpoint is taken as well, and so on.
783
///
784
/// The interpolation takes two values by reference together with a scalar parameter and
785
/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
786
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
787
///
788
/// # Errors
789
///
790
/// If `segments` is zero or if this curve has unbounded domain, then a [`ResamplingError`] is
791
/// returned.
792
///
793
/// # Example
794
/// ```
795
/// # use bevy_math::*;
796
/// # use bevy_math::curve::*;
797
/// let quarter_rotation = FunctionCurve::new(interval(0.0, 90.0).unwrap(), |t| Rot2::degrees(t));
798
/// // A curve which only stores three data points and uses `nlerp` to interpolate them:
799
/// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t));
800
/// ```
801
fn resample<I>(
802
&self,
803
segments: usize,
804
interpolation: I,
805
) -> Result<SampleCurve<T, I>, ResamplingError>
806
where
807
I: Fn(&T, &T, f32) -> T,
808
{
809
let samples = self.samples(segments + 1)?.collect_vec();
810
Ok(SampleCurve {
811
core: EvenCore {
812
domain: self.domain(),
813
samples,
814
},
815
interpolation,
816
})
817
}
818
819
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
820
/// spaced sample values, using [automatic interpolation] to interpolate between adjacent samples.
821
/// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
822
/// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
823
/// the midpoint is taken as well, and so on.
824
///
825
/// # Errors
826
///
827
/// If `segments` is zero or if this curve has unbounded domain, a [`ResamplingError`] is returned.
828
///
829
/// [automatic interpolation]: crate::common_traits::StableInterpolate
830
fn resample_auto(&self, segments: usize) -> Result<SampleAutoCurve<T>, ResamplingError>
831
where
832
T: StableInterpolate,
833
{
834
let samples = self.samples(segments + 1)?.collect_vec();
835
Ok(SampleAutoCurve {
836
core: EvenCore {
837
domain: self.domain(),
838
samples,
839
},
840
})
841
}
842
843
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples
844
/// taken at a given set of times. The given `interpolation` is used to interpolate adjacent
845
/// samples, and the `sample_times` are expected to contain at least two valid times within the
846
/// curve's domain interval.
847
///
848
/// Redundant sample times, non-finite sample times, and sample times outside of the domain
849
/// are filtered out. With an insufficient quantity of data, a [`ResamplingError`] is
850
/// returned.
851
///
852
/// The domain of the produced curve stretches between the first and last sample times of the
853
/// iterator.
854
///
855
/// The interpolation takes two values by reference together with a scalar parameter and
856
/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
857
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
858
///
859
/// # Errors
860
///
861
/// If `sample_times` doesn't contain at least two distinct times after filtering, a
862
/// [`ResamplingError`] is returned.
863
fn resample_uneven<I>(
864
&self,
865
sample_times: impl IntoIterator<Item = f32>,
866
interpolation: I,
867
) -> Result<UnevenSampleCurve<T, I>, ResamplingError>
868
where
869
I: Fn(&T, &T, f32) -> T,
870
{
871
let domain = self.domain();
872
let mut times = sample_times
873
.into_iter()
874
.filter(|t| t.is_finite() && domain.contains(*t))
875
.collect_vec();
876
times.sort_by(f32::total_cmp);
877
times.dedup();
878
if times.len() < 2 {
879
return Err(ResamplingError::NotEnoughSamples(times.len()));
880
}
881
let samples = times.iter().map(|t| self.sample_unchecked(*t)).collect();
882
Ok(UnevenSampleCurve {
883
core: UnevenCore { times, samples },
884
interpolation,
885
})
886
}
887
888
/// Resample this [`Curve`] to produce a new one that is defined by [automatic interpolation] over
889
/// samples taken at the given set of times. The given `sample_times` are expected to contain at least
890
/// two valid times within the curve's domain interval.
891
///
892
/// Redundant sample times, non-finite sample times, and sample times outside of the domain
893
/// are simply filtered out. With an insufficient quantity of data, a [`ResamplingError`] is
894
/// returned.
895
///
896
/// The domain of the produced [`UnevenSampleAutoCurve`] stretches between the first and last
897
/// sample times of the iterator.
898
///
899
/// # Errors
900
///
901
/// If `sample_times` doesn't contain at least two distinct times after filtering, a
902
/// [`ResamplingError`] is returned.
903
///
904
/// [automatic interpolation]: crate::common_traits::StableInterpolate
905
fn resample_uneven_auto(
906
&self,
907
sample_times: impl IntoIterator<Item = f32>,
908
) -> Result<UnevenSampleAutoCurve<T>, ResamplingError>
909
where
910
T: StableInterpolate,
911
{
912
let domain = self.domain();
913
let mut times = sample_times
914
.into_iter()
915
.filter(|t| t.is_finite() && domain.contains(*t))
916
.collect_vec();
917
times.sort_by(f32::total_cmp);
918
times.dedup();
919
if times.len() < 2 {
920
return Err(ResamplingError::NotEnoughSamples(times.len()));
921
}
922
let samples = times.iter().map(|t| self.sample_unchecked(*t)).collect();
923
Ok(UnevenSampleAutoCurve {
924
core: UnevenCore { times, samples },
925
})
926
}
927
}
928
929
#[cfg(feature = "alloc")]
930
impl<C, T> CurveResampleExt<T> for C where C: Curve<T> + ?Sized {}
931
932
/// An error indicating that a linear reparameterization couldn't be performed because of
933
/// malformed inputs.
934
#[derive(Debug, Error)]
935
#[error("Could not build a linear function to reparametrize this curve")]
936
pub enum LinearReparamError {
937
/// The source curve that was to be reparametrized had unbounded domain.
938
#[error("This curve has unbounded domain")]
939
SourceCurveUnbounded,
940
941
/// The target interval for reparameterization was unbounded.
942
#[error("The target interval for reparameterization is unbounded")]
943
TargetIntervalUnbounded,
944
}
945
946
/// An error indicating that a reversion of a curve couldn't be performed because of
947
/// malformed inputs.
948
#[derive(Debug, Error)]
949
#[error("Could not reverse this curve")]
950
pub enum ReverseError {
951
/// The source curve that was to be reversed had unbounded domain end.
952
#[error("This curve has an unbounded domain end")]
953
SourceDomainEndInfinite,
954
}
955
956
/// An error indicating that a repetition of a curve couldn't be performed because of malformed
957
/// inputs.
958
#[derive(Debug, Error)]
959
#[error("Could not repeat this curve")]
960
pub enum RepeatError {
961
/// The source curve that was to be repeated had unbounded domain.
962
#[error("This curve has an unbounded domain")]
963
SourceDomainUnbounded,
964
}
965
966
/// An error indicating that a ping ponging of a curve couldn't be performed because of
967
/// malformed inputs.
968
#[derive(Debug, Error)]
969
#[error("Could not ping pong this curve")]
970
pub enum PingPongError {
971
/// The source curve that was to be ping ponged had unbounded domain end.
972
#[error("This curve has an unbounded domain end")]
973
SourceDomainEndInfinite,
974
}
975
976
/// An error indicating that an end-to-end composition couldn't be performed because of
977
/// malformed inputs.
978
#[derive(Debug, Error)]
979
#[error("Could not compose these curves together")]
980
pub enum ChainError {
981
/// The right endpoint of the first curve was infinite.
982
#[error("The first curve's domain has an infinite end")]
983
FirstEndInfinite,
984
985
/// The left endpoint of the second curve was infinite.
986
#[error("The second curve's domain has an infinite start")]
987
SecondStartInfinite,
988
}
989
990
/// An error indicating that a resampling operation could not be performed because of
991
/// malformed inputs.
992
#[derive(Debug, Error)]
993
#[error("Could not resample from this curve because of bad inputs")]
994
pub enum ResamplingError {
995
/// This resampling operation was not provided with enough samples to have well-formed output.
996
#[error("Not enough unique samples to construct resampled curve")]
997
NotEnoughSamples(usize),
998
999
/// This resampling operation failed because of an unbounded interval.
1000
#[error("Could not resample because this curve has unbounded domain")]
1001
UnboundedDomain,
1002
}
1003
1004
#[cfg(test)]
1005
mod tests {
1006
use super::*;
1007
use crate::{ops, Quat};
1008
use alloc::vec::Vec;
1009
use approx::{assert_abs_diff_eq, AbsDiffEq};
1010
use core::f32::consts::TAU;
1011
use glam::*;
1012
1013
#[test]
1014
fn curve_can_be_made_into_an_object() {
1015
let curve = ConstantCurve::new(Interval::UNIT, 42.0);
1016
let curve: &dyn Curve<f64> = &curve;
1017
1018
assert_eq!(curve.sample(1.0), Some(42.0));
1019
assert_eq!(curve.sample(2.0), None);
1020
}
1021
1022
#[test]
1023
fn constant_curves() {
1024
let curve = ConstantCurve::new(Interval::EVERYWHERE, 5.0);
1025
assert!(curve.sample_unchecked(-35.0) == 5.0);
1026
1027
let curve = ConstantCurve::new(Interval::UNIT, true);
1028
assert!(curve.sample_unchecked(2.0));
1029
assert!(curve.sample(2.0).is_none());
1030
}
1031
1032
#[test]
1033
fn function_curves() {
1034
let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * t);
1035
assert!(curve.sample_unchecked(2.0).abs_diff_eq(&4.0, f32::EPSILON));
1036
assert!(curve.sample_unchecked(-3.0).abs_diff_eq(&9.0, f32::EPSILON));
1037
1038
let curve = FunctionCurve::new(interval(0.0, f32::INFINITY).unwrap(), ops::log2);
1039
assert_eq!(curve.sample_unchecked(3.5), ops::log2(3.5));
1040
assert!(curve.sample_unchecked(-1.0).is_nan());
1041
assert!(curve.sample(-1.0).is_none());
1042
}
1043
1044
#[test]
1045
fn linear_curve() {
1046
let start = Vec2::ZERO;
1047
let end = Vec2::new(1.0, 2.0);
1048
let curve = EasingCurve::new(start, end, EaseFunction::Linear);
1049
1050
let mid = (start + end) / 2.0;
1051
1052
[(0.0, start), (0.5, mid), (1.0, end)]
1053
.into_iter()
1054
.for_each(|(t, x)| {
1055
assert!(curve.sample_unchecked(t).abs_diff_eq(x, f32::EPSILON));
1056
});
1057
}
1058
1059
#[test]
1060
fn easing_curves_step() {
1061
let start = Vec2::ZERO;
1062
let end = Vec2::new(1.0, 2.0);
1063
1064
let curve = EasingCurve::new(start, end, EaseFunction::Steps(4, JumpAt::End));
1065
[
1066
(0.0, start),
1067
(0.249, start),
1068
(0.250, Vec2::new(0.25, 0.5)),
1069
(0.499, Vec2::new(0.25, 0.5)),
1070
(0.500, Vec2::new(0.5, 1.0)),
1071
(0.749, Vec2::new(0.5, 1.0)),
1072
(0.750, Vec2::new(0.75, 1.5)),
1073
(1.0, end),
1074
]
1075
.into_iter()
1076
.for_each(|(t, x)| {
1077
assert!(curve.sample_unchecked(t).abs_diff_eq(x, f32::EPSILON));
1078
});
1079
}
1080
1081
#[test]
1082
fn easing_curves_quadratic() {
1083
let start = Vec2::ZERO;
1084
let end = Vec2::new(1.0, 2.0);
1085
1086
let curve = EasingCurve::new(start, end, EaseFunction::QuadraticIn);
1087
[
1088
(0.0, start),
1089
(0.25, Vec2::new(0.0625, 0.125)),
1090
(0.5, Vec2::new(0.25, 0.5)),
1091
(1.0, end),
1092
]
1093
.into_iter()
1094
.for_each(|(t, x)| {
1095
assert!(curve.sample_unchecked(t).abs_diff_eq(x, f32::EPSILON),);
1096
});
1097
}
1098
1099
#[expect(
1100
clippy::neg_multiply,
1101
reason = "Clippy doesn't like this, but it's correct"
1102
)]
1103
#[test]
1104
fn mapping() {
1105
let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * 3.0 + 1.0);
1106
let mapped_curve = curve.map(|x| x / 7.0);
1107
assert_eq!(mapped_curve.sample_unchecked(3.5), (3.5 * 3.0 + 1.0) / 7.0);
1108
assert_eq!(
1109
mapped_curve.sample_unchecked(-1.0),
1110
(-1.0 * 3.0 + 1.0) / 7.0
1111
);
1112
assert_eq!(mapped_curve.domain(), Interval::EVERYWHERE);
1113
1114
let curve = FunctionCurve::new(Interval::UNIT, |t| t * TAU);
1115
let mapped_curve = curve.map(Quat::from_rotation_z);
1116
assert_eq!(mapped_curve.sample_unchecked(0.0), Quat::IDENTITY);
1117
assert!(mapped_curve.sample_unchecked(1.0).is_near_identity());
1118
assert_eq!(mapped_curve.domain(), Interval::UNIT);
1119
}
1120
1121
#[test]
1122
fn reverse() {
1123
let curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
1124
let rev_curve = curve.reverse().unwrap();
1125
assert_eq!(rev_curve.sample(-0.1), None);
1126
assert_eq!(rev_curve.sample(0.0), Some(1.0 * 3.0 + 1.0));
1127
assert_eq!(rev_curve.sample(0.5), Some(0.5 * 3.0 + 1.0));
1128
assert_eq!(rev_curve.sample(1.0), Some(0.0 * 3.0 + 1.0));
1129
assert_eq!(rev_curve.sample(1.1), None);
1130
1131
let curve = FunctionCurve::new(Interval::new(-2.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
1132
let rev_curve = curve.reverse().unwrap();
1133
assert_eq!(rev_curve.sample(-2.1), None);
1134
assert_eq!(rev_curve.sample(-2.0), Some(1.0 * 3.0 + 1.0));
1135
assert_eq!(rev_curve.sample(-0.5), Some(-0.5 * 3.0 + 1.0));
1136
assert_eq!(rev_curve.sample(1.0), Some(-2.0 * 3.0 + 1.0));
1137
assert_eq!(rev_curve.sample(1.1), None);
1138
}
1139
1140
#[test]
1141
fn repeat() {
1142
let curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
1143
let repeat_curve = curve.by_ref().repeat(1).unwrap();
1144
assert_eq!(repeat_curve.sample(-0.1), None);
1145
assert_eq!(repeat_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
1146
assert_eq!(repeat_curve.sample(0.5), Some(0.5 * 3.0 + 1.0));
1147
assert_eq!(repeat_curve.sample(0.99), Some(0.99 * 3.0 + 1.0));
1148
assert_eq!(repeat_curve.sample(1.0), Some(1.0 * 3.0 + 1.0));
1149
assert_eq!(repeat_curve.sample(1.01), Some(0.01 * 3.0 + 1.0));
1150
assert_eq!(repeat_curve.sample(1.5), Some(0.5 * 3.0 + 1.0));
1151
assert_eq!(repeat_curve.sample(1.99), Some(0.99 * 3.0 + 1.0));
1152
assert_eq!(repeat_curve.sample(2.0), Some(1.0 * 3.0 + 1.0));
1153
assert_eq!(repeat_curve.sample(2.01), None);
1154
1155
let repeat_curve = curve.by_ref().repeat(3).unwrap();
1156
assert_eq!(repeat_curve.sample(2.0), Some(1.0 * 3.0 + 1.0));
1157
assert_eq!(repeat_curve.sample(3.0), Some(1.0 * 3.0 + 1.0));
1158
assert_eq!(repeat_curve.sample(4.0), Some(1.0 * 3.0 + 1.0));
1159
assert_eq!(repeat_curve.sample(5.0), None);
1160
1161
let repeat_curve = curve.by_ref().forever().unwrap();
1162
assert_eq!(repeat_curve.sample(-1.0), Some(1.0 * 3.0 + 1.0));
1163
assert_eq!(repeat_curve.sample(2.0), Some(1.0 * 3.0 + 1.0));
1164
assert_eq!(repeat_curve.sample(3.0), Some(1.0 * 3.0 + 1.0));
1165
assert_eq!(repeat_curve.sample(4.0), Some(1.0 * 3.0 + 1.0));
1166
assert_eq!(repeat_curve.sample(5.0), Some(1.0 * 3.0 + 1.0));
1167
}
1168
1169
#[test]
1170
fn ping_pong() {
1171
let curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
1172
let ping_pong_curve = curve.ping_pong().unwrap();
1173
assert_eq!(ping_pong_curve.sample(-0.1), None);
1174
assert_eq!(ping_pong_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
1175
assert_eq!(ping_pong_curve.sample(0.5), Some(0.5 * 3.0 + 1.0));
1176
assert_eq!(ping_pong_curve.sample(1.0), Some(1.0 * 3.0 + 1.0));
1177
assert_eq!(ping_pong_curve.sample(1.5), Some(0.5 * 3.0 + 1.0));
1178
assert_eq!(ping_pong_curve.sample(2.0), Some(0.0 * 3.0 + 1.0));
1179
assert_eq!(ping_pong_curve.sample(2.1), None);
1180
1181
let curve = FunctionCurve::new(Interval::new(-2.0, 2.0).unwrap(), |t| t * 3.0 + 1.0);
1182
let ping_pong_curve = curve.ping_pong().unwrap();
1183
assert_eq!(ping_pong_curve.sample(-2.1), None);
1184
assert_eq!(ping_pong_curve.sample(-2.0), Some(-2.0 * 3.0 + 1.0));
1185
assert_eq!(ping_pong_curve.sample(-0.5), Some(-0.5 * 3.0 + 1.0));
1186
assert_eq!(ping_pong_curve.sample(2.0), Some(2.0 * 3.0 + 1.0));
1187
assert_eq!(ping_pong_curve.sample(4.5), Some(-0.5 * 3.0 + 1.0));
1188
assert_eq!(ping_pong_curve.sample(6.0), Some(-2.0 * 3.0 + 1.0));
1189
assert_eq!(ping_pong_curve.sample(6.1), None);
1190
}
1191
1192
#[test]
1193
fn continue_chain() {
1194
let first = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
1195
let second = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * t);
1196
let c0_chain_curve = first.chain_continue(second).unwrap();
1197
assert_eq!(c0_chain_curve.sample(-0.1), None);
1198
assert_eq!(c0_chain_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
1199
assert_eq!(c0_chain_curve.sample(0.5), Some(0.5 * 3.0 + 1.0));
1200
assert_eq!(c0_chain_curve.sample(1.0), Some(1.0 * 3.0 + 1.0));
1201
assert_eq!(c0_chain_curve.sample(1.5), Some(1.0 * 3.0 + 1.0 + 0.25));
1202
assert_eq!(c0_chain_curve.sample(2.0), Some(1.0 * 3.0 + 1.0 + 1.0));
1203
assert_eq!(c0_chain_curve.sample(2.1), None);
1204
}
1205
1206
#[test]
1207
fn reparameterization() {
1208
let curve = FunctionCurve::new(interval(1.0, f32::INFINITY).unwrap(), ops::log2);
1209
let reparametrized_curve = curve
1210
.by_ref()
1211
.reparametrize(interval(0.0, f32::INFINITY).unwrap(), ops::exp2);
1212
assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(3.5), 3.5);
1213
assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(100.0), 100.0);
1214
assert_eq!(
1215
reparametrized_curve.domain(),
1216
interval(0.0, f32::INFINITY).unwrap()
1217
);
1218
1219
let reparametrized_curve = curve.by_ref().reparametrize(Interval::UNIT, |t| t + 1.0);
1220
assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(0.0), 0.0);
1221
assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(1.0), 1.0);
1222
assert_eq!(reparametrized_curve.domain(), Interval::UNIT);
1223
}
1224
1225
#[test]
1226
fn multiple_maps() {
1227
// Make sure these actually happen in the right order.
1228
let curve = FunctionCurve::new(Interval::UNIT, ops::exp2);
1229
let first_mapped = curve.map(ops::log2);
1230
let second_mapped = first_mapped.map(|x| x * -2.0);
1231
assert_abs_diff_eq!(second_mapped.sample_unchecked(0.0), 0.0);
1232
assert_abs_diff_eq!(second_mapped.sample_unchecked(0.5), -1.0);
1233
assert_abs_diff_eq!(second_mapped.sample_unchecked(1.0), -2.0);
1234
}
1235
1236
#[test]
1237
fn multiple_reparams() {
1238
// Make sure these happen in the right order too.
1239
let curve = FunctionCurve::new(Interval::UNIT, ops::exp2);
1240
let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), ops::log2);
1241
let second_reparam = first_reparam.reparametrize(Interval::UNIT, |t| t + 1.0);
1242
assert_abs_diff_eq!(second_reparam.sample_unchecked(0.0), 1.0);
1243
assert_abs_diff_eq!(second_reparam.sample_unchecked(0.5), 1.5);
1244
assert_abs_diff_eq!(second_reparam.sample_unchecked(1.0), 2.0);
1245
}
1246
1247
#[test]
1248
fn resampling() {
1249
let curve = FunctionCurve::new(interval(1.0, 4.0).unwrap(), ops::log2);
1250
1251
// Need at least one segment to sample.
1252
let nice_try = curve.by_ref().resample_auto(0);
1253
assert!(nice_try.is_err());
1254
1255
// The values of a resampled curve should be very close at the sample points.
1256
// Because of denominators, it's not literally equal.
1257
// (This is a tradeoff against O(1) sampling.)
1258
let resampled_curve = curve.by_ref().resample_auto(100).unwrap();
1259
for test_pt in curve.domain().spaced_points(101).unwrap() {
1260
let expected = curve.sample_unchecked(test_pt);
1261
assert_abs_diff_eq!(
1262
resampled_curve.sample_unchecked(test_pt),
1263
expected,
1264
epsilon = 1e-6
1265
);
1266
}
1267
1268
// Another example.
1269
let curve = FunctionCurve::new(interval(0.0, TAU).unwrap(), ops::cos);
1270
let resampled_curve = curve.by_ref().resample_auto(1000).unwrap();
1271
for test_pt in curve.domain().spaced_points(1001).unwrap() {
1272
let expected = curve.sample_unchecked(test_pt);
1273
assert_abs_diff_eq!(
1274
resampled_curve.sample_unchecked(test_pt),
1275
expected,
1276
epsilon = 1e-6
1277
);
1278
}
1279
}
1280
1281
#[test]
1282
fn uneven_resampling() {
1283
let curve = FunctionCurve::new(interval(0.0, f32::INFINITY).unwrap(), ops::exp);
1284
1285
// Need at least two points to resample.
1286
let nice_try = curve.by_ref().resample_uneven_auto([1.0; 1]);
1287
assert!(nice_try.is_err());
1288
1289
// Uneven sampling should produce literal equality at the sample points.
1290
// (This is part of what you get in exchange for O(log(n)) sampling.)
1291
let sample_points = (0..100).map(|idx| idx as f32 * 0.1);
1292
let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap();
1293
for idx in 0..100 {
1294
let test_pt = idx as f32 * 0.1;
1295
let expected = curve.sample_unchecked(test_pt);
1296
assert_eq!(resampled_curve.sample_unchecked(test_pt), expected);
1297
}
1298
assert_abs_diff_eq!(resampled_curve.domain().start(), 0.0);
1299
assert_abs_diff_eq!(resampled_curve.domain().end(), 9.9, epsilon = 1e-6);
1300
1301
// Another example.
1302
let curve = FunctionCurve::new(interval(1.0, f32::INFINITY).unwrap(), ops::log2);
1303
let sample_points = (0..10).map(|idx| ops::exp2(idx as f32));
1304
let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap();
1305
for idx in 0..10 {
1306
let test_pt = ops::exp2(idx as f32);
1307
let expected = curve.sample_unchecked(test_pt);
1308
assert_eq!(resampled_curve.sample_unchecked(test_pt), expected);
1309
}
1310
assert_abs_diff_eq!(resampled_curve.domain().start(), 1.0);
1311
assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0);
1312
}
1313
1314
#[test]
1315
fn sample_iterators() {
1316
let times = [-0.5, 0.0, 0.5, 1.0, 1.5];
1317
1318
let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * 3.0 + 1.0);
1319
let samples = curve.sample_iter_unchecked(times).collect::<Vec<_>>();
1320
let [y0, y1, y2, y3, y4] = samples.try_into().unwrap();
1321
1322
assert_eq!(y0, -0.5 * 3.0 + 1.0);
1323
assert_eq!(y1, 0.0 * 3.0 + 1.0);
1324
assert_eq!(y2, 0.5 * 3.0 + 1.0);
1325
assert_eq!(y3, 1.0 * 3.0 + 1.0);
1326
assert_eq!(y4, 1.5 * 3.0 + 1.0);
1327
1328
let finite_curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
1329
let samples = finite_curve.sample_iter(times).collect::<Vec<_>>();
1330
let [y0, y1, y2, y3, y4] = samples.try_into().unwrap();
1331
1332
assert_eq!(y0, None);
1333
assert_eq!(y1, Some(0.0 * 3.0 + 1.0));
1334
assert_eq!(y2, Some(0.5 * 3.0 + 1.0));
1335
assert_eq!(y3, Some(1.0 * 3.0 + 1.0));
1336
assert_eq!(y4, None);
1337
1338
let samples = finite_curve.sample_iter_clamped(times).collect::<Vec<_>>();
1339
let [y0, y1, y2, y3, y4] = samples.try_into().unwrap();
1340
1341
assert_eq!(y0, 0.0 * 3.0 + 1.0);
1342
assert_eq!(y1, 0.0 * 3.0 + 1.0);
1343
assert_eq!(y2, 0.5 * 3.0 + 1.0);
1344
assert_eq!(y3, 1.0 * 3.0 + 1.0);
1345
assert_eq!(y4, 1.0 * 3.0 + 1.0);
1346
}
1347
}
1348
1349