Skip to main content

motion_core/animation/
primitives.rs

1//! Clock-free analytic oscillators, seeded noise, path motion and mappings.
2
3use super::{Easing, FiniteAnimation, Interpolate, Signal};
4use crate::{Duration, Path2D, Point, Time, ValidationError, Vector2};
5use std::sync::Arc;
6
7/// Upper bound for oscillator frequency and phase so cycle counts stay finite.
8const MAX_MAGNITUDE: f64 = 1.0e290;
9
10/// Rounded-up bound on the seconds a signed tick count can express.
11const MAX_SECONDS: f64 = 2.0e10;
12
13/// Periodic waveform evaluated in turns.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15#[cfg_attr(
16    feature = "serde",
17    derive(serde::Serialize, serde::Deserialize),
18    serde(rename_all = "snake_case")
19)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21pub enum Waveform {
22    /// Smooth sine starting at zero.
23    #[default]
24    Sine,
25    /// Linear ramps starting at zero and rising to one.
26    Triangle,
27    /// Positive during the first half of each cycle, negative during the second.
28    Square,
29    /// Linear ramp from minus one to one with a discontinuity each cycle.
30    Sawtooth,
31}
32
33/// Infinite stateless periodic scalar animation.
34#[derive(Clone, Debug)]
35pub struct Oscillator {
36    waveform: Waveform,
37    frequency: f64,
38    amplitude: f64,
39    phase: f64,
40    offset: f64,
41}
42
43impl Oscillator {
44    /// Creates a unit-amplitude oscillator with zero phase and offset.
45    /// Use [`Oscillator::builder`] to configure those; `frequency_hertz` must be finite,
46    /// nonnegative and at most 1e290.
47    pub fn try_new(waveform: Waveform, frequency_hertz: f64) -> Result<Self, ValidationError> {
48        Self::builder(waveform, frequency_hertz).build()
49    }
50
51    /// Starts a builder for a waveform with the given frequency in hertz.
52    pub fn builder(waveform: Waveform, frequency_hertz: f64) -> OscillatorBuilder {
53        OscillatorBuilder {
54            waveform,
55            frequency: frequency_hertz,
56            amplitude: 1.0,
57            phase: 0.0,
58            offset: 0.0,
59        }
60    }
61}
62
63/// Unvalidated oscillator configuration; [`OscillatorBuilder::build`] reports invalid values.
64#[derive(Clone, Debug)]
65pub struct OscillatorBuilder {
66    waveform: Waveform,
67    frequency: f64,
68    amplitude: f64,
69    phase: f64,
70    offset: f64,
71}
72
73impl OscillatorBuilder {
74    /// Sets peak amplitude, which must be finite.
75    pub fn amplitude(mut self, amplitude: f64) -> Self {
76        self.amplitude = amplitude;
77        self
78    }
79
80    /// Sets a cycle offset in turns, which must be finite and at most 1e290.
81    pub fn phase(mut self, turns: f64) -> Self {
82        self.phase = turns;
83        self
84    }
85
86    /// Sets an output offset, which must be finite.
87    pub fn offset(mut self, offset: f64) -> Self {
88        self.offset = offset;
89        self
90    }
91
92    /// Validates the configuration. Frequency must be finite, nonnegative and at
93    /// most 1e290; the output range `amplitude * ±1 + offset` must
94    /// be finite, so sampling can never overflow.
95    pub fn build(self) -> Result<Oscillator, ValidationError> {
96        let error = |field, message| ValidationError::invalid("oscillator", field, message);
97        if !(self.frequency.is_finite() && (0.0..=MAX_MAGNITUDE).contains(&self.frequency)) {
98            return Err(error(
99                "frequency",
100                "frequency must be finite, nonnegative and bounded",
101            ));
102        }
103        if !(self.phase.is_finite() && self.phase.abs() <= MAX_MAGNITUDE) {
104            return Err(error("phase", "phase must be finite and bounded"));
105        }
106        if !(self.amplitude.is_finite()
107            && self.offset.is_finite()
108            && (self.amplitude.abs() + self.offset.abs()).is_finite())
109        {
110            return Err(error(
111                "amplitude",
112                "amplitude and offset must be finite and keep the output finite",
113            ));
114        }
115        Ok(Oscillator {
116            waveform: self.waveform,
117            frequency: self.frequency,
118            amplitude: self.amplitude,
119            phase: self.phase,
120            offset: self.offset,
121        })
122    }
123}
124
125impl Signal<f64> for Oscillator {
126    fn sample(&self, time: Time) -> f64 {
127        let cycles = time.as_seconds() * self.frequency + self.phase;
128        debug_assert!(
129            cycles.is_finite(),
130            "bounded parameters keep the phase finite"
131        );
132        let turns = cycles - libm::floor(cycles);
133        let value = match self.waveform {
134            Waveform::Sine => libm::sin(turns * std::f64::consts::TAU),
135            Waveform::Triangle if turns < 0.25 => turns * 4.0,
136            Waveform::Triangle if turns < 0.75 => 2.0 - turns * 4.0,
137            Waveform::Triangle => turns * 4.0 - 4.0,
138            Waveform::Square => {
139                if turns < 0.5 {
140                    1.0
141                } else {
142                    -1.0
143                }
144            }
145            Waveform::Sawtooth => turns * 2.0 - 1.0,
146        };
147        let output = value * self.amplitude + self.offset;
148        debug_assert!(
149            output.is_finite(),
150            "validated range keeps the output finite"
151        );
152        output
153    }
154}
155
156/// One-dimensional slice of [`crate::noise::value2_d`] along `y = 0`, with its derivative.
157///
158/// # Panics
159/// Panics for a nonfinite position; see [`try_value_noise`].
160pub fn value_noise(position: f64, seed: u32) -> (f64, f64) {
161    try_value_noise(position, seed).unwrap_or_else(|error| panic!("{}", error.message))
162}
163
164/// Evaluates [`value_noise`], reporting a nonfinite position.
165pub fn try_value_noise(position: f64, seed: u32) -> Result<(f64, f64), ValidationError> {
166    if !position.is_finite() {
167        return Err(ValidationError::invalid(
168            "noise",
169            "position",
170            "noise position must be finite",
171        ));
172    }
173    let (value, gradient) = crate::noise::value2_d([position, 0.0], seed);
174    Ok((value, gradient[0]))
175}
176
177/// Infinite deterministic multi-octave noise animation in minus one to one.
178#[derive(Clone, Debug)]
179pub struct Noise {
180    seed: u64,
181    frequency: f64,
182    octaves: u32,
183    lacunarity: f64,
184    gain: f64,
185}
186
187impl Noise {
188    /// Creates seeded one-octave noise with one lattice cell per second.
189    pub fn new(seed: u64) -> Self {
190        Self::builder(seed)
191            .build()
192            .unwrap_or_else(|error| unreachable!("default noise parameters are valid: {error}"))
193    }
194
195    /// Starts a builder with one octave and one lattice cell per second.
196    pub fn builder(seed: u64) -> NoiseBuilder {
197        NoiseBuilder {
198            seed,
199            frequency: 1.0,
200            octaves: 1,
201            lacunarity: 2.0,
202            gain: 0.5,
203        }
204    }
205
206    /// Creates independently seeded X/Y noise with the same spectral settings.
207    pub fn vector(self) -> impl Signal<Vector2> {
208        let mut vertical = self.clone();
209        vertical.seed ^= 0x9e3779b97f4a7c15;
210        move |time| Vector2::new(self.sample(time), vertical.sample(time))
211    }
212}
213
214/// Unvalidated noise configuration; [`NoiseBuilder::build`] reports invalid values.
215#[derive(Clone, Debug)]
216pub struct NoiseBuilder {
217    seed: u64,
218    frequency: f64,
219    octaves: u32,
220    lacunarity: f64,
221    gain: f64,
222}
223
224impl NoiseBuilder {
225    /// Sets lattice frequency, which must be finite and nonnegative.
226    pub fn frequency(mut self, hertz: f64) -> Self {
227        self.frequency = hertz;
228        self
229    }
230
231    /// Sets the octave count, from one through 32.
232    pub fn octaves(mut self, octaves: u32) -> Self {
233        self.octaves = octaves;
234        self
235    }
236
237    /// Sets octave frequency multiplication, which must be finite and positive.
238    pub fn lacunarity(mut self, lacunarity: f64) -> Self {
239        self.lacunarity = lacunarity;
240        self
241    }
242
243    /// Sets octave amplitude multiplication in zero to one.
244    pub fn gain(mut self, gain: f64) -> Self {
245        self.gain = gain;
246        self
247    }
248
249    /// Validates every setting at once. Beyond the individual ranges, the
250    /// highest octave position over the full time range must stay finite, so
251    /// sampling can never overflow.
252    pub fn build(self) -> Result<Noise, ValidationError> {
253        let error = |field, message| ValidationError::invalid("noise", field, message);
254        if !(self.frequency.is_finite() && self.frequency >= 0.0) {
255            return Err(error(
256                "frequency",
257                "frequency must be finite and nonnegative",
258            ));
259        }
260        if !(1..=32).contains(&self.octaves) {
261            return Err(error("octaves", "octaves must be from 1 through 32"));
262        }
263        if !(self.lacunarity.is_finite() && self.lacunarity > 0.0) {
264            return Err(error(
265                "lacunarity",
266                "lacunarity must be finite and positive",
267            ));
268        }
269        if !(self.gain.is_finite() && (0.0..=1.0).contains(&self.gain)) {
270            return Err(error("gain", "gain must be finite and in 0..=1"));
271        }
272        let reach = MAX_SECONDS * self.frequency * self.lacunarity.powi(self.octaves as i32 - 1);
273        if !reach.is_finite() {
274            return Err(error(
275                "frequency",
276                "frequency and lacunarity must keep octave positions finite",
277            ));
278        }
279        Ok(Noise {
280            seed: self.seed,
281            frequency: self.frequency,
282            octaves: self.octaves,
283            lacunarity: self.lacunarity,
284            gain: self.gain,
285        })
286    }
287}
288
289impl Signal<f64> for Noise {
290    fn sample(&self, time: Time) -> f64 {
291        let mut position = time.as_seconds() * self.frequency;
292        let mut amplitude = 1.0;
293        let mut value = 0.0;
294        let mut weight = 0.0;
295        for octave in 0..self.octaves {
296            let seed = (self.seed ^ (self.seed >> 32)) as u32;
297            value += value_noise(position, seed.wrapping_add(octave.wrapping_mul(0x9e3779b9))).0
298                * amplitude;
299            weight += amplitude;
300            position *= self.lacunarity;
301            amplitude *= self.gain;
302        }
303        value / weight
304    }
305}
306
307/// One arc-length path sample in local geometry coordinates.
308#[derive(Clone, Copy, Debug, PartialEq)]
309pub struct PathSample {
310    /// Position along the path.
311    pub position: Point,
312    /// Normalized local tangent, or zero for degenerate geometry.
313    pub tangent: Vector2,
314    /// Tangent angle in radians, zero for degenerate geometry.
315    pub angle: f64,
316}
317
318/// Finite path motion sampled by normalized arc length.
319#[derive(Clone, Debug)]
320pub struct FollowPath {
321    path: Arc<Path2D>,
322    duration: Duration,
323    easing: Easing,
324}
325
326impl FollowPath {
327    /// Creates path motion; a zero duration jumps to the end.
328    pub fn new(path: impl Into<Arc<Path2D>>, duration: Duration) -> Self {
329        Self {
330            path: path.into(),
331            duration,
332            easing: Easing::Linear,
333        }
334    }
335
336    /// Sets the normalized distance easing.
337    pub fn easing(mut self, easing: Easing) -> Self {
338        self.easing = easing;
339        self
340    }
341}
342
343impl Signal<PathSample> for FollowPath {
344    fn sample(&self, time: Time) -> PathSample {
345        let progress = if self.duration == Duration::ZERO {
346            if time < Time::ZERO { 0.0 } else { 1.0 }
347        } else {
348            (time.ticks() as f64 / self.duration.ticks() as f64).clamp(0.0, 1.0)
349        };
350        let (position, tangent) = self
351            .path
352            .point_at(self.easing.apply(progress).clamp(0.0, 1.0));
353        PathSample {
354            position,
355            tangent,
356            angle: libm::atan2(tangent.y, tangent.x),
357        }
358    }
359}
360
361impl FiniteAnimation<PathSample> for FollowPath {
362    fn duration(&self) -> Duration {
363        self.duration
364    }
365}
366
367/// Behavior outside a mapping's input interval.
368#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
369pub enum Extrapolate {
370    /// Keep the nearest endpoint.
371    #[default]
372    Clamp,
373    /// Continue the endpoint segment linearly.
374    Extend,
375    /// Repeat the half-open input interval.
376    Wrap,
377}
378
379fn mapping_error(field: &'static str, message: &'static str) -> ValidationError {
380    ValidationError::invalid("mapping", field, message)
381}
382
383fn progress(value: f64, from: [f64; 2], extrapolate: Extrapolate) -> Result<f64, ValidationError> {
384    if !(value.is_finite() && from.iter().all(|value| value.is_finite()) && from[0] != from[1]) {
385        return Err(mapping_error(
386            "from",
387            "mapping inputs must be finite with distinct endpoints",
388        ));
389    }
390    let scale = value
391        .abs()
392        .max(from[0].abs())
393        .max(from[1].abs())
394        .max(f64::MIN_POSITIVE);
395    let span = from[1] - from[0];
396    let distance = value - from[0];
397    let amount = if span.is_finite() && distance.is_finite() {
398        distance / span
399    } else {
400        (value / scale - from[0] / scale) / (from[1] / scale - from[0] / scale)
401    };
402    Ok(match extrapolate {
403        Extrapolate::Clamp => amount.clamp(0.0, 1.0),
404        Extrapolate::Extend => amount,
405        Extrapolate::Wrap => amount - libm::floor(amount),
406    })
407}
408
409/// Maps one finite scalar interval; reversed endpoints are supported.
410///
411/// # Panics
412/// Panics for invalid inputs or an overflowing output; see [`try_remap`].
413pub fn remap(value: f64, from: [f64; 2], to: [f64; 2], extrapolate: Extrapolate) -> f64 {
414    try_remap(value, from, to, extrapolate).unwrap_or_else(|error| panic!("{}", error.message))
415}
416
417/// Maps like [`remap`], reporting nonfinite inputs, equal input endpoints and overflow.
418pub fn try_remap(
419    value: f64,
420    from: [f64; 2],
421    to: [f64; 2],
422    extrapolate: Extrapolate,
423) -> Result<f64, ValidationError> {
424    if !to.iter().all(|value| value.is_finite()) {
425        return Err(mapping_error("to", "mapping outputs must be finite"));
426    }
427    let output = to[0].interpolate(&to[1], progress(value, from, extrapolate)?);
428    if !output.is_finite() {
429        return Err(mapping_error("output", "mapping output overflows"));
430    }
431    Ok(output)
432}
433
434/// Interpolates matching samples with finite strictly increasing input knots.
435///
436/// # Panics
437/// Panics for invalid knots or an invalid mapping; see [`try_piecewise`].
438pub fn piecewise<Value: Interpolate>(
439    value: f64,
440    inputs: &[f64],
441    outputs: &[Value],
442    easing: Easing,
443    extrapolate: Extrapolate,
444) -> Value {
445    try_piecewise(value, inputs, outputs, easing, extrapolate)
446        .unwrap_or_else(|error| panic!("{}", error.message))
447}
448
449/// Interpolates like [`piecewise`], reporting invalid knots, easing and mapping errors.
450pub fn try_piecewise<Value: Interpolate>(
451    value: f64,
452    inputs: &[f64],
453    outputs: &[Value],
454    easing: Easing,
455    extrapolate: Extrapolate,
456) -> Result<Value, ValidationError> {
457    if inputs.len() < 2 || inputs.len() != outputs.len() {
458        return Err(mapping_error(
459            "inputs",
460            "piecewise needs at least two knots and one output per knot",
461        ));
462    }
463    if !(inputs.iter().all(|value| value.is_finite())
464        && inputs.windows(2).all(|pair| pair[0] < pair[1]))
465    {
466        return Err(mapping_error(
467            "inputs",
468            "piecewise knots must be finite and strictly increasing",
469        ));
470    }
471    let mapped = try_remap(
472        value,
473        [inputs[0], inputs[inputs.len() - 1]],
474        [inputs[0], inputs[inputs.len() - 1]],
475        extrapolate,
476    )?;
477    let index = inputs
478        .partition_point(|input| *input <= mapped)
479        .saturating_sub(1)
480        .min(inputs.len() - 2);
481    let amount = progress(
482        mapped,
483        [inputs[index], inputs[index + 1]],
484        Extrapolate::Extend,
485    )?;
486    let amount = if !(0.0..=1.0).contains(&amount) && extrapolate == Extrapolate::Extend {
487        amount
488    } else {
489        easing.try_apply(amount)?
490    };
491    Ok(outputs[index].interpolate(&outputs[index + 1], amount))
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use crate::{Shape, TimeLiteral};
498
499    #[test]
500    fn oscillators_are_periodic_seek_independent_and_infinite() {
501        for waveform in [
502            Waveform::Sine,
503            Waveform::Triangle,
504            Waveform::Square,
505            Waveform::Sawtooth,
506        ] {
507            let value = Oscillator::builder(waveform, 2.0)
508                .amplitude(3.0)
509                .offset(7.0)
510                .build()
511                .unwrap();
512            assert_eq!(
513                value.sample(125.milliseconds().after_origin()),
514                value.sample(625.milliseconds().after_origin())
515            );
516            assert_eq!(
517                value.sample(Time::ZERO - 375.milliseconds()),
518                value.sample(125.milliseconds().after_origin())
519            );
520        }
521        assert_eq!(
522            Oscillator::builder(Waveform::Triangle, 1.0)
523                .build()
524                .unwrap()
525                .sample(250.milliseconds().after_origin()),
526            1.0
527        );
528    }
529
530    #[test]
531    fn noise_and_vector_replay_share_the_particle_noise_primitive() {
532        let noise = Noise::new(17);
533        for time in [
534            500.milliseconds().after_origin(),
535            Time::ZERO - 200.milliseconds(),
536            3.seconds().after_origin(),
537            500.milliseconds().after_origin(),
538        ] {
539            assert_eq!(noise.sample(time), value_noise(time.as_seconds(), 17).0);
540            assert!(
541                (-1.0..=1.0).contains(&Noise::builder(17).octaves(5).build().unwrap().sample(time))
542            );
543        }
544        let vector = noise.vector();
545        assert_eq!(
546            vector.sample(500.milliseconds().after_origin()),
547            vector.sample(500.milliseconds().after_origin())
548        );
549        assert_ne!(
550            vector.sample(500.milliseconds().after_origin()).x,
551            vector.sample(500.milliseconds().after_origin()).y
552        );
553    }
554
555    #[test]
556    fn path_motion_uses_arc_length_and_has_exact_endpoints() {
557        let path = crate::Line::new(Point::ORIGIN, Point::new(10.0, 0.0))
558            .unwrap()
559            .path()
560            .unwrap();
561        let motion = FollowPath::new(path, 2.seconds());
562        assert_eq!(motion.duration(), 2.seconds());
563        assert_eq!(
564            motion.sample(1.seconds().after_origin()).position,
565            Point::new(5.0, 0.0)
566        );
567        assert_eq!(
568            motion.sample(Time::ZERO - 1.seconds()).position,
569            Point::ORIGIN
570        );
571        assert_eq!(
572            motion.sample(3.seconds().after_origin()).position,
573            Point::new(10.0, 0.0)
574        );
575        assert_eq!(motion.sample(1.seconds().after_origin()).angle, 0.0);
576    }
577
578    #[test]
579    fn mappings_cover_clamp_wrap_extend_reversed_ranges_and_invalid_knots() {
580        assert_eq!(
581            remap(3.0, [0.0, 2.0], [0.0, 10.0], Extrapolate::Extend),
582            15.0
583        );
584        assert_eq!(remap(-1.0, [0.0, 2.0], [0.0, 10.0], Extrapolate::Wrap), 5.0);
585        assert_eq!(remap(3.0, [2.0, 0.0], [0.0, 10.0], Extrapolate::Clamp), 0.0);
586        assert_eq!(
587            piecewise(
588                1.5,
589                &[0.0, 1.0, 2.0],
590                &[0.0, 10.0, 30.0],
591                Easing::Linear,
592                Extrapolate::Clamp
593            ),
594            20.0
595        );
596        assert_eq!(
597            piecewise(
598                3.0,
599                &[0.0, 1.0, 2.0],
600                &[0.0, 10.0, 30.0],
601                Easing::Linear,
602                Extrapolate::Extend
603            ),
604            50.0
605        );
606        assert!(
607            try_piecewise(
608                0.0,
609                &[0.0, 0.0],
610                &[0.0, 1.0],
611                Easing::Linear,
612                Extrapolate::Clamp
613            )
614            .is_err()
615        );
616        assert!(try_remap(0.0, [1.0, 1.0], [0.0, 1.0], Extrapolate::Clamp).is_err());
617        assert!(
618            try_remap(
619                f64::MAX,
620                [0.0, 1.0],
621                [-f64::MAX, f64::MAX],
622                Extrapolate::Extend
623            )
624            .is_err()
625        );
626    }
627
628    #[test]
629    fn builders_report_invalid_and_overflowing_parameters() {
630        let oscillator = |frequency| Oscillator::builder(Waveform::Sine, frequency);
631        assert!(oscillator(f64::NAN).build().is_err());
632        assert!(oscillator(-1.0).build().is_err());
633        assert!(oscillator(f64::MAX).build().is_err());
634        assert!(oscillator(1.0).phase(f64::INFINITY).build().is_err());
635        assert!(
636            oscillator(1.0)
637                .amplitude(f64::MAX)
638                .offset(f64::MAX)
639                .build()
640                .is_err()
641        );
642        let extreme = oscillator(1.0e290).build().unwrap();
643        assert!(extreme.sample(Time::from_ticks(i64::MAX)).is_finite());
644        let noise = Noise::builder(1);
645        assert!(noise.clone().frequency(-1.0).build().is_err());
646        assert!(noise.clone().octaves(0).build().is_err());
647        assert!(noise.clone().octaves(33).build().is_err());
648        assert!(noise.clone().lacunarity(0.0).build().is_err());
649        assert!(noise.clone().gain(1.5).build().is_err());
650        assert!(noise.clone().frequency(1.0e300).build().is_err());
651        assert!(noise.octaves(32).lacunarity(1.0e10).build().is_err());
652        assert!(try_value_noise(f64::NAN, 0).is_err());
653    }
654}