Skip to main content

animato_tween/
waveform.rs

1//! Procedural waveform generators for animation values.
2
3use animato_core::math::sin;
4
5#[cfg(any(feature = "std", feature = "alloc"))]
6use crate::keyframe::{Keyframe, KeyframeTrack};
7#[cfg(any(feature = "std", feature = "alloc"))]
8use alloc::vec::Vec;
9#[cfg(any(feature = "std", feature = "alloc"))]
10use animato_core::Easing;
11#[cfg(any(feature = "std", feature = "alloc"))]
12use animato_core::math::ceil;
13
14const TAU: f32 = core::f32::consts::PI * 2.0;
15
16/// A procedural scalar waveform.
17#[derive(Clone, Copy, Debug, PartialEq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum Waveform {
20    /// Continuous sine wave.
21    Sine {
22        /// Cycles per second.
23        frequency: f32,
24        /// Peak absolute value.
25        amplitude: f32,
26        /// Phase offset in radians.
27        phase: f32,
28    },
29    /// Linear ramp from `-amplitude` to `amplitude`, then wraps.
30    Sawtooth {
31        /// Cycles per second.
32        frequency: f32,
33        /// Peak absolute value.
34        amplitude: f32,
35    },
36    /// Two-state square wave.
37    Square {
38        /// Cycles per second.
39        frequency: f32,
40        /// Peak absolute value.
41        amplitude: f32,
42        /// Fraction of the cycle spent at positive amplitude.
43        duty_cycle: f32,
44    },
45    /// Triangle wave with linear rise and fall.
46    Triangle {
47        /// Cycles per second.
48        frequency: f32,
49        /// Peak absolute value.
50        amplitude: f32,
51    },
52    /// Deterministic smoothed noise in `[-1, 1]`.
53    Noise {
54        /// Deterministic seed.
55        seed: u32,
56        /// Seconds between random control points.
57        smoothness: f32,
58    },
59}
60
61impl Waveform {
62    /// Evaluate the waveform at an absolute time in seconds.
63    pub fn sample(&self, time: f32) -> f32 {
64        let time = finite_or(time, 0.0);
65        match *self {
66            Self::Sine {
67                frequency,
68                amplitude,
69                phase,
70            } => finite_or(amplitude, 1.0) * sin(TAU * finite_or(frequency, 1.0) * time + phase),
71            Self::Sawtooth {
72                frequency,
73                amplitude,
74            } => {
75                let cycle = cycle(time, frequency);
76                finite_or(amplitude, 1.0) * (cycle * 2.0 - 1.0)
77            }
78            Self::Square {
79                frequency,
80                amplitude,
81                duty_cycle,
82            } => {
83                let cycle = cycle(time, frequency);
84                let duty = finite_or(duty_cycle, 0.5).clamp(0.0, 1.0);
85                let amp = finite_or(amplitude, 1.0);
86                if cycle < duty { amp } else { -amp }
87            }
88            Self::Triangle {
89                frequency,
90                amplitude,
91            } => {
92                let cycle = cycle(time, frequency);
93                finite_or(amplitude, 1.0) * (1.0 - 4.0 * (cycle - 0.5).abs())
94            }
95            Self::Noise { seed, smoothness } => {
96                let span = finite_or(smoothness, 0.25).max(f32::EPSILON);
97                let scaled = time.max(0.0) / span;
98                let index = scaled as u32;
99                let local = smoothstep(scaled - index as f32);
100                let a = hash_noise(seed, index);
101                let b = hash_noise(seed, index.saturating_add(1));
102                a + (b - a) * local
103            }
104        }
105    }
106
107    /// Convert this waveform into a scalar keyframe track.
108    ///
109    /// `sample_rate` is in samples per second and is clamped to at least `1`.
110    #[cfg(any(feature = "std", feature = "alloc"))]
111    pub fn to_keyframe_track(&self, duration: f32, sample_rate: f32) -> KeyframeTrack<f32> {
112        let duration = finite_or(duration, 0.0).max(0.0);
113        let sample_rate = finite_or(sample_rate, 60.0).max(1.0);
114        let samples = ceil(duration * sample_rate).max(1.0) as usize;
115        let mut frames = Vec::with_capacity(samples + 1);
116        for index in 0..=samples {
117            let t = duration * index as f32 / samples as f32;
118            frames.push(Keyframe::new(t, self.sample(t), Easing::Linear));
119        }
120        KeyframeTrack::from_sorted_frames(frames)
121    }
122}
123
124fn finite_or(value: f32, fallback: f32) -> f32 {
125    if value.is_finite() { value } else { fallback }
126}
127
128fn cycle(time: f32, frequency: f32) -> f32 {
129    let scaled = (time.max(0.0) * finite_or(frequency, 1.0).max(0.0)).max(0.0);
130    scaled - scaled as u32 as f32
131}
132
133fn smoothstep(t: f32) -> f32 {
134    let t = t.clamp(0.0, 1.0);
135    t * t * (3.0 - 2.0 * t)
136}
137
138fn hash_noise(seed: u32, index: u32) -> f32 {
139    let mut x = seed ^ index.wrapping_mul(0x9E37_79B9);
140    x ^= x >> 16;
141    x = x.wrapping_mul(0x7FEB_352D);
142    x ^= x >> 15;
143    x = x.wrapping_mul(0x846C_A68B);
144    x ^= x >> 16;
145    let unit = (x as f32) / (u32::MAX as f32);
146    unit * 2.0 - 1.0
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn sine_respects_frequency_amplitude_and_phase() {
155        let wave = Waveform::Sine {
156            frequency: 1.0,
157            amplitude: 2.0,
158            phase: 0.0,
159        };
160        assert!(wave.sample(0.0).abs() < 0.0001);
161        assert!((wave.sample(0.25) - 2.0).abs() < 0.0001);
162    }
163
164    #[test]
165    fn square_respects_duty_cycle() {
166        let wave = Waveform::Square {
167            frequency: 1.0,
168            amplitude: 3.0,
169            duty_cycle: 0.25,
170        };
171        assert_eq!(wave.sample(0.1), 3.0);
172        assert_eq!(wave.sample(0.3), -3.0);
173    }
174
175    #[test]
176    fn noise_is_deterministic_and_bounded() {
177        let wave = Waveform::Noise {
178            seed: 42,
179            smoothness: 0.25,
180        };
181        let a = wave.sample(0.123);
182        let b = wave.sample(0.123);
183        assert_eq!(a, b);
184        assert!((-1.0..=1.0).contains(&a));
185    }
186
187    #[cfg(any(feature = "std", feature = "alloc"))]
188    #[test]
189    fn waveform_converts_to_keyframes() {
190        let wave = Waveform::Triangle {
191            frequency: 1.0,
192            amplitude: 1.0,
193        };
194        let track = wave.to_keyframe_track(1.0, 4.0);
195        assert_eq!(track.frames().len(), 5);
196        assert_eq!(track.duration(), 1.0);
197    }
198}