Skip to main content

motion_core/animation/
primitives.rs

1//! Clock-free analytic oscillators, seeded noise, path motion and mappings.
2
3use super::{Animation, Easing, Interpolate};
4use crate::{Duration, Path2D, Point, Time, Vector2};
5use std::sync::Arc;
6
7/// Periodic waveform evaluated in turns.
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9pub enum Waveform {
10    /// Smooth sine starting at zero.
11    #[default]
12    Sine,
13    /// Linear ramps starting at zero and rising to one.
14    Triangle,
15    /// Positive during the first half of each cycle, negative during the second.
16    Square,
17    /// Linear ramp from minus one to one with a discontinuity each cycle.
18    Sawtooth,
19}
20
21/// Infinite stateless periodic scalar animation.
22#[derive(Clone, Debug)]
23pub struct Oscillator {
24    waveform: Waveform,
25    frequency: f64,
26    amplitude: f64,
27    phase: f64,
28    offset: f64,
29}
30
31impl Oscillator {
32    /// Creates a waveform with finite nonnegative frequency in hertz.
33    pub fn new(waveform: Waveform, frequency_hertz: f64) -> Self {
34        assert!(
35            frequency_hertz.is_finite() && frequency_hertz >= 0.0,
36            "Frequency must be finite and nonnegative"
37        );
38        Self {
39            waveform,
40            frequency: frequency_hertz,
41            amplitude: 1.0,
42            phase: 0.0,
43            offset: 0.0,
44        }
45    }
46
47    /// Sets finite peak amplitude.
48    pub fn amplitude(mut self, amplitude: f64) -> Self {
49        assert!(amplitude.is_finite());
50        self.amplitude = amplitude;
51        self
52    }
53
54    /// Sets a finite cycle offset in turns.
55    pub fn phase(mut self, turns: f64) -> Self {
56        assert!(turns.is_finite());
57        self.phase = turns;
58        self
59    }
60
61    /// Sets a finite output offset.
62    pub fn offset(mut self, offset: f64) -> Self {
63        assert!(offset.is_finite());
64        self.offset = offset;
65        self
66    }
67}
68
69impl Animation<f64> for Oscillator {
70    fn sample(&self, time: Time) -> f64 {
71        let cycles = time.as_seconds() * self.frequency + self.phase;
72        assert!(cycles.is_finite(), "Oscillator phase overflow");
73        let turns = cycles - libm::floor(cycles);
74        let value = match self.waveform {
75            Waveform::Sine => libm::sin(turns * std::f64::consts::TAU),
76            Waveform::Triangle if turns < 0.25 => turns * 4.0,
77            Waveform::Triangle if turns < 0.75 => 2.0 - turns * 4.0,
78            Waveform::Triangle => turns * 4.0 - 4.0,
79            Waveform::Square => {
80                if turns < 0.5 {
81                    1.0
82                } else {
83                    -1.0
84                }
85            }
86            Waveform::Sawtooth => turns * 2.0 - 1.0,
87        };
88        let output = value * self.amplitude + self.offset;
89        assert!(output.is_finite(), "Oscillator output overflow");
90        output
91    }
92}
93
94/// One-dimensional slice of [`crate::noise::value2_d`] along `y = 0`, with its derivative.
95pub fn value_noise(position: f64, seed: u32) -> (f64, f64) {
96    assert!(position.is_finite(), "Noise position must be finite");
97    let (value, gradient) = crate::noise::value2_d([position, 0.0], seed);
98    (value, gradient[0])
99}
100
101/// Infinite deterministic multi-octave noise animation in minus one to one.
102#[derive(Clone, Debug)]
103pub struct Noise {
104    seed: u64,
105    frequency: f64,
106    octaves: u32,
107    lacunarity: f64,
108    gain: f64,
109}
110
111impl Noise {
112    /// Creates seeded one-octave noise with one lattice cell per second.
113    pub fn new(seed: u64) -> Self {
114        Self {
115            seed,
116            frequency: 1.0,
117            octaves: 1,
118            lacunarity: 2.0,
119            gain: 0.5,
120        }
121    }
122
123    /// Sets finite nonnegative lattice frequency.
124    pub fn frequency(mut self, hertz: f64) -> Self {
125        assert!(hertz.is_finite() && hertz >= 0.0);
126        self.frequency = hertz;
127        self
128    }
129
130    /// Sets the bounded octave count, from one through 32.
131    pub fn octaves(mut self, octaves: u32) -> Self {
132        assert!((1..=32).contains(&octaves));
133        self.octaves = octaves;
134        self
135    }
136
137    /// Sets finite positive octave frequency multiplication.
138    pub fn lacunarity(mut self, lacunarity: f64) -> Self {
139        assert!(lacunarity.is_finite() && lacunarity > 0.0);
140        self.lacunarity = lacunarity;
141        self
142    }
143
144    /// Sets finite octave amplitude multiplication in zero to one.
145    pub fn gain(mut self, gain: f64) -> Self {
146        assert!(gain.is_finite() && (0.0..=1.0).contains(&gain));
147        self.gain = gain;
148        self
149    }
150
151    /// Creates independently seeded X/Y noise with the same spectral settings.
152    pub fn vector(self) -> impl Animation<Vector2> {
153        let mut vertical = self.clone();
154        vertical.seed ^= 0x9e3779b97f4a7c15;
155        move |time| Vector2::new(self.sample(time), vertical.sample(time))
156    }
157}
158
159impl Animation<f64> for Noise {
160    fn sample(&self, time: Time) -> f64 {
161        let mut position = time.as_seconds() * self.frequency;
162        let mut amplitude = 1.0;
163        let mut value = 0.0;
164        let mut weight = 0.0;
165        for octave in 0..self.octaves {
166            let seed = (self.seed ^ (self.seed >> 32)) as u32;
167            value += value_noise(position, seed.wrapping_add(octave.wrapping_mul(0x9e3779b9))).0
168                * amplitude;
169            weight += amplitude;
170            position *= self.lacunarity;
171            amplitude *= self.gain;
172        }
173        value / weight
174    }
175}
176
177/// One arc-length path sample in local geometry coordinates.
178#[derive(Clone, Copy, Debug, PartialEq)]
179pub struct PathSample {
180    /// Position along the path.
181    pub position: Point,
182    /// Normalized local tangent, or zero for degenerate geometry.
183    pub tangent: Vector2,
184    /// Tangent angle in radians, zero for degenerate geometry.
185    pub angle: f64,
186}
187
188/// Finite path motion sampled by normalized arc length.
189#[derive(Clone, Debug)]
190pub struct FollowPath {
191    path: Arc<Path2D>,
192    duration: Duration,
193    easing: Easing,
194}
195
196impl FollowPath {
197    /// Creates path motion with a nonnegative duration; zero jumps to the end.
198    pub fn new(path: impl Into<Arc<Path2D>>, duration: Duration) -> Self {
199        assert!(duration >= Time::ZERO);
200        Self {
201            path: path.into(),
202            duration,
203            easing: Easing::Linear,
204        }
205    }
206
207    /// Sets the normalized distance easing.
208    pub fn easing(mut self, easing: Easing) -> Self {
209        easing.try_validate().expect("Valid path easing");
210        self.easing = easing;
211        self
212    }
213}
214
215impl Animation<PathSample> for FollowPath {
216    fn sample(&self, time: Time) -> PathSample {
217        let progress = if self.duration == Time::ZERO {
218            if time < Time::ZERO { 0.0 } else { 1.0 }
219        } else {
220            (time / self.duration).clamp(0.0, 1.0)
221        };
222        let (position, tangent) = self
223            .path
224            .point_at(self.easing.apply(progress).clamp(0.0, 1.0));
225        PathSample {
226            position,
227            tangent,
228            angle: libm::atan2(tangent.y, tangent.x),
229        }
230    }
231
232    fn duration(&self) -> Option<Duration> {
233        Some(self.duration)
234    }
235}
236
237/// Behavior outside a mapping's input interval.
238#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
239pub enum Extrapolate {
240    /// Keep the nearest endpoint.
241    #[default]
242    Clamp,
243    /// Continue the endpoint segment linearly.
244    Extend,
245    /// Repeat the half-open input interval.
246    Wrap,
247}
248
249fn progress(value: f64, from: [f64; 2], extrapolate: Extrapolate) -> f64 {
250    assert!(
251        value.is_finite() && from.iter().all(|value| value.is_finite()) && from[0] != from[1],
252        "Mapping inputs must be finite with distinct endpoints"
253    );
254    let scale = value
255        .abs()
256        .max(from[0].abs())
257        .max(from[1].abs())
258        .max(f64::MIN_POSITIVE);
259    let span = from[1] - from[0];
260    let distance = value - from[0];
261    let amount = if span.is_finite() && distance.is_finite() {
262        distance / span
263    } else {
264        (value / scale - from[0] / scale) / (from[1] / scale - from[0] / scale)
265    };
266    match extrapolate {
267        Extrapolate::Clamp => amount.clamp(0.0, 1.0),
268        Extrapolate::Extend => amount,
269        Extrapolate::Wrap => amount - libm::floor(amount),
270    }
271}
272
273/// Maps one finite scalar interval; reversed endpoints are supported.
274pub fn remap(value: f64, from: [f64; 2], to: [f64; 2], extrapolate: Extrapolate) -> f64 {
275    assert!(to.iter().all(|value| value.is_finite()));
276    let output = to[0].interpolate(&to[1], progress(value, from, extrapolate));
277    assert!(output.is_finite(), "Mapping output overflow");
278    output
279}
280
281/// Interpolates matching samples with finite strictly increasing input knots.
282pub fn piecewise<Value: Interpolate>(
283    value: f64,
284    inputs: &[f64],
285    outputs: &[Value],
286    easing: Easing,
287    extrapolate: Extrapolate,
288) -> Value {
289    assert!(inputs.len() >= 2 && inputs.len() == outputs.len());
290    assert!(
291        inputs.iter().all(|value| value.is_finite())
292            && inputs.windows(2).all(|pair| pair[0] < pair[1])
293    );
294    let mapped = remap(
295        value,
296        [inputs[0], inputs[inputs.len() - 1]],
297        [inputs[0], inputs[inputs.len() - 1]],
298        extrapolate,
299    );
300    let index = inputs
301        .partition_point(|input| *input <= mapped)
302        .saturating_sub(1)
303        .min(inputs.len() - 2);
304    let amount = progress(
305        mapped,
306        [inputs[index], inputs[index + 1]],
307        Extrapolate::Extend,
308    );
309    let amount = if !(0.0..=1.0).contains(&amount) && extrapolate == Extrapolate::Extend {
310        amount
311    } else {
312        easing.apply(amount)
313    };
314    outputs[index].interpolate(&outputs[index + 1], amount)
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::{Shape, TimeLiteral};
321
322    #[test]
323    fn oscillators_are_periodic_seek_independent_and_infinite() {
324        for waveform in [
325            Waveform::Sine,
326            Waveform::Triangle,
327            Waveform::Square,
328            Waveform::Sawtooth,
329        ] {
330            let value = Oscillator::new(waveform, 2.0).amplitude(3.0).offset(7.0);
331            assert_eq!(value.duration(), None);
332            assert_eq!(
333                value.sample(125.milliseconds()),
334                value.sample(625.milliseconds())
335            );
336            assert_eq!(
337                value.sample((-375).milliseconds()),
338                value.sample(125.milliseconds())
339            );
340        }
341        assert_eq!(
342            Oscillator::new(Waveform::Triangle, 1.0).sample(250.milliseconds()),
343            1.0
344        );
345    }
346
347    #[test]
348    fn noise_and_vector_replay_share_the_particle_noise_primitive() {
349        let noise = Noise::new(17);
350        for time in [
351            500.milliseconds(),
352            (-200).milliseconds(),
353            3.seconds(),
354            500.milliseconds(),
355        ] {
356            assert_eq!(noise.sample(time), value_noise(time.as_seconds(), 17).0);
357            assert!((-1.0..=1.0).contains(&noise.clone().octaves(5).sample(time)));
358        }
359        let vector = noise.vector();
360        assert_eq!(
361            vector.sample(500.milliseconds()),
362            vector.sample(500.milliseconds())
363        );
364        assert_ne!(
365            vector.sample(500.milliseconds()).x,
366            vector.sample(500.milliseconds()).y
367        );
368    }
369
370    #[test]
371    fn path_motion_uses_arc_length_and_has_exact_endpoints() {
372        let path = crate::Line::new(Point::ORIGIN, Point::new(10.0, 0.0)).path();
373        let motion = FollowPath::new(path, 2.seconds());
374        assert_eq!(motion.duration(), Some(2.seconds()));
375        assert_eq!(motion.sample(1.seconds()).position, Point::new(5.0, 0.0));
376        assert_eq!(motion.sample((-1).seconds()).position, Point::ORIGIN);
377        assert_eq!(motion.sample(3.seconds()).position, Point::new(10.0, 0.0));
378        assert_eq!(motion.sample(1.seconds()).angle, 0.0);
379    }
380
381    #[test]
382    fn mappings_cover_clamp_wrap_extend_reversed_ranges_and_invalid_knots() {
383        assert_eq!(
384            remap(3.0, [0.0, 2.0], [0.0, 10.0], Extrapolate::Extend),
385            15.0
386        );
387        assert_eq!(remap(-1.0, [0.0, 2.0], [0.0, 10.0], Extrapolate::Wrap), 5.0);
388        assert_eq!(remap(3.0, [2.0, 0.0], [0.0, 10.0], Extrapolate::Clamp), 0.0);
389        assert_eq!(
390            piecewise(
391                1.5,
392                &[0.0, 1.0, 2.0],
393                &[0.0, 10.0, 30.0],
394                Easing::Linear,
395                Extrapolate::Clamp
396            ),
397            20.0
398        );
399        assert_eq!(
400            piecewise(
401                3.0,
402                &[0.0, 1.0, 2.0],
403                &[0.0, 10.0, 30.0],
404                Easing::Linear,
405                Extrapolate::Extend
406            ),
407            50.0
408        );
409        assert!(
410            std::panic::catch_unwind(|| piecewise(
411                0.0,
412                &[0.0, 0.0],
413                &[0.0, 1.0],
414                Easing::Linear,
415                Extrapolate::Clamp
416            ))
417            .is_err()
418        );
419    }
420}