Skip to main content

animato_core/
value.rs

1//! Interpolatable value wrappers for advanced animation targets.
2
3use crate::Interpolate;
4use crate::math::{acos, cos, sin, sqrt};
5
6const PI: f32 = core::f32::consts::PI;
7
8/// An angle stored in degrees.
9///
10/// Interpolation follows the shortest angular path, so `359deg` to `1deg`
11/// moves forward by two degrees instead of backward by 358 degrees.
12#[derive(Clone, Copy, Debug, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct Angle(pub f32);
15
16impl Angle {
17    /// Create an angle from degrees.
18    #[inline]
19    pub fn from_degrees(degrees: f32) -> Self {
20        Self(degrees)
21    }
22
23    /// Create an angle from radians.
24    #[inline]
25    pub fn from_radians(radians: f32) -> Self {
26        Self(radians * 180.0 / PI)
27    }
28
29    /// Return the angle in degrees.
30    #[inline]
31    pub fn degrees(self) -> f32 {
32        self.0
33    }
34
35    /// Return the angle in radians.
36    #[inline]
37    pub fn radians(self) -> f32 {
38        self.0 * PI / 180.0
39    }
40
41    /// Return an equivalent angle in `[0, 360)`.
42    pub fn normalized(self) -> Self {
43        let mut degrees = self.0 % 360.0;
44        if degrees < 0.0 {
45            degrees += 360.0;
46        }
47        Self(degrees)
48    }
49}
50
51impl Interpolate for Angle {
52    fn lerp(&self, other: &Self, t: f32) -> Self {
53        let delta = ((other.0 - self.0 + 540.0) % 360.0) - 180.0;
54        Self(self.0 + delta * t)
55    }
56}
57
58/// Unit quaternion used for 3D rotation interpolation.
59///
60/// The components are stored as `(x, y, z, w)`. [`Interpolate`] uses
61/// shortest-path spherical linear interpolation.
62#[derive(Clone, Copy, Debug, PartialEq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64pub struct Quaternion {
65    /// X component of the vector part.
66    pub x: f32,
67    /// Y component of the vector part.
68    pub y: f32,
69    /// Z component of the vector part.
70    pub z: f32,
71    /// Scalar part.
72    pub w: f32,
73}
74
75impl Quaternion {
76    /// Identity rotation.
77    pub const IDENTITY: Self = Self {
78        x: 0.0,
79        y: 0.0,
80        z: 0.0,
81        w: 1.0,
82    };
83
84    /// Create a quaternion from components.
85    pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
86        Self { x, y, z, w }.normalized()
87    }
88
89    /// Create a quaternion from an axis and angle in degrees.
90    pub fn from_axis_angle(axis: [f32; 3], angle: Angle) -> Self {
91        let len = sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
92        if len <= f32::EPSILON {
93            return Self::IDENTITY;
94        }
95        let half = angle.radians() * 0.5;
96        let s = sin(half) / len;
97        Self::new(axis[0] * s, axis[1] * s, axis[2] * s, cos(half))
98    }
99
100    /// Dot product between two quaternions.
101    #[inline]
102    pub fn dot(self, other: Self) -> f32 {
103        self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w
104    }
105
106    /// Magnitude of the quaternion.
107    #[inline]
108    pub fn length(self) -> f32 {
109        sqrt(self.dot(self))
110    }
111
112    /// Return a normalized quaternion, or identity when length is invalid.
113    pub fn normalized(self) -> Self {
114        let len = self.length();
115        if !len.is_finite() || len <= f32::EPSILON {
116            return Self::IDENTITY;
117        }
118        Self {
119            x: self.x / len,
120            y: self.y / len,
121            z: self.z / len,
122            w: self.w / len,
123        }
124    }
125
126    /// Return the negated representation of the same rotation.
127    #[inline]
128    pub fn negated(self) -> Self {
129        Self {
130            x: -self.x,
131            y: -self.y,
132            z: -self.z,
133            w: -self.w,
134        }
135    }
136
137    /// Spherical linear interpolation using the shortest rotation path.
138    pub fn slerp(self, other: Self, t: f32) -> Self {
139        let a = self.normalized();
140        let mut b = other.normalized();
141        let mut dot = a.dot(b);
142        if dot < 0.0 {
143            b = b.negated();
144            dot = -dot;
145        }
146
147        if dot > 0.9995 {
148            return Self {
149                x: a.x + (b.x - a.x) * t,
150                y: a.y + (b.y - a.y) * t,
151                z: a.z + (b.z - a.z) * t,
152                w: a.w + (b.w - a.w) * t,
153            }
154            .normalized();
155        }
156
157        let theta_0 = acos(dot.clamp(-1.0, 1.0));
158        let theta = theta_0 * t;
159        let sin_theta = sin(theta);
160        let sin_theta_0 = sin(theta_0);
161        if sin_theta_0.abs() <= f32::EPSILON {
162            return a;
163        }
164        let s0 = cos(theta) - dot * sin_theta / sin_theta_0;
165        let s1 = sin_theta / sin_theta_0;
166        Self {
167            x: a.x * s0 + b.x * s1,
168            y: a.y * s0 + b.y * s1,
169            z: a.z * s0 + b.z * s1,
170            w: a.w * s0 + b.w * s1,
171        }
172        .normalized()
173    }
174
175    /// Convert this rotation into a column-major 3x3 matrix.
176    pub fn to_mat3(self) -> [f32; 9] {
177        let q = self.normalized();
178        let xx = q.x * q.x;
179        let yy = q.y * q.y;
180        let zz = q.z * q.z;
181        let xy = q.x * q.y;
182        let xz = q.x * q.z;
183        let yz = q.y * q.z;
184        let wx = q.w * q.x;
185        let wy = q.w * q.y;
186        let wz = q.w * q.z;
187
188        [
189            1.0 - 2.0 * (yy + zz),
190            2.0 * (xy + wz),
191            2.0 * (xz - wy),
192            2.0 * (xy - wz),
193            1.0 - 2.0 * (xx + zz),
194            2.0 * (yz + wx),
195            2.0 * (xz + wy),
196            2.0 * (yz - wx),
197            1.0 - 2.0 * (xx + yy),
198        ]
199    }
200}
201
202impl Interpolate for Quaternion {
203    fn lerp(&self, other: &Self, t: f32) -> Self {
204        self.slerp(*other, t)
205    }
206}
207
208/// A column-major 4x4 affine transform matrix.
209///
210/// Interpolation decomposes translation, scale, and rotation, interpolates each
211/// component, then recomposes. Non-affine perspective terms are ignored.
212#[derive(Clone, Copy, Debug, PartialEq)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
214pub struct Mat4(pub [f32; 16]);
215
216impl Mat4 {
217    /// Identity matrix.
218    pub const IDENTITY: Self = Self([
219        1.0, 0.0, 0.0, 0.0, //
220        0.0, 1.0, 0.0, 0.0, //
221        0.0, 0.0, 1.0, 0.0, //
222        0.0, 0.0, 0.0, 1.0,
223    ]);
224
225    /// Create a matrix from translation, rotation, and scale.
226    pub fn from_translation_rotation_scale(
227        translation: [f32; 3],
228        rotation: Quaternion,
229        scale: [f32; 3],
230    ) -> Self {
231        let r = rotation.to_mat3();
232        Self([
233            r[0] * scale[0],
234            r[1] * scale[0],
235            r[2] * scale[0],
236            0.0,
237            r[3] * scale[1],
238            r[4] * scale[1],
239            r[5] * scale[1],
240            0.0,
241            r[6] * scale[2],
242            r[7] * scale[2],
243            r[8] * scale[2],
244            0.0,
245            translation[0],
246            translation[1],
247            translation[2],
248            1.0,
249        ])
250    }
251
252    /// Extract translation from the affine matrix.
253    #[inline]
254    pub fn translation(self) -> [f32; 3] {
255        [self.0[12], self.0[13], self.0[14]]
256    }
257
258    /// Extract approximate scale from the affine basis columns.
259    pub fn scale(self) -> [f32; 3] {
260        [
261            column_len(&self.0, 0),
262            column_len(&self.0, 1),
263            column_len(&self.0, 2),
264        ]
265    }
266
267    /// Decompose into translation, rotation, and scale.
268    pub fn decompose(self) -> ([f32; 3], Quaternion, [f32; 3]) {
269        let translation = self.translation();
270        let scale = self.scale();
271        let rotation = rotation_from_scaled_mat4(self.0, scale);
272        (translation, rotation, scale)
273    }
274}
275
276impl Interpolate for Mat4 {
277    fn lerp(&self, other: &Self, t: f32) -> Self {
278        let (ta, ra, sa) = self.decompose();
279        let (tb, rb, sb) = other.decompose();
280        Self::from_translation_rotation_scale(ta.lerp(&tb, t), ra.slerp(rb, t), sa.lerp(&sb, t))
281    }
282}
283
284/// A lightweight linear RGBA color value.
285///
286/// Perceptual color interpolation remains available through `animato-color`.
287#[derive(Clone, Copy, Debug, PartialEq)]
288#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
289pub struct Color(pub [f32; 4]);
290
291impl Color {
292    /// Create a linear RGBA color.
293    #[inline]
294    pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
295        Self([r, g, b, a])
296    }
297
298    /// Return the red component.
299    #[inline]
300    pub fn r(self) -> f32 {
301        self.0[0]
302    }
303
304    /// Return the green component.
305    #[inline]
306    pub fn g(self) -> f32 {
307        self.0[1]
308    }
309
310    /// Return the blue component.
311    #[inline]
312    pub fn b(self) -> f32 {
313        self.0[2]
314    }
315
316    /// Return the alpha component.
317    #[inline]
318    pub fn a(self) -> f32 {
319        self.0[3]
320    }
321}
322
323impl Interpolate for Color {
324    fn lerp(&self, other: &Self, t: f32) -> Self {
325        Self(self.0.lerp(&other.0, t))
326    }
327}
328
329fn column_len(m: &[f32; 16], col: usize) -> f32 {
330    let i = col * 4;
331    sqrt(m[i] * m[i] + m[i + 1] * m[i + 1] + m[i + 2] * m[i + 2])
332}
333
334fn rotation_from_scaled_mat4(m: [f32; 16], scale: [f32; 3]) -> Quaternion {
335    let sx = if scale[0].abs() <= f32::EPSILON {
336        1.0
337    } else {
338        scale[0]
339    };
340    let sy = if scale[1].abs() <= f32::EPSILON {
341        1.0
342    } else {
343        scale[1]
344    };
345    let sz = if scale[2].abs() <= f32::EPSILON {
346        1.0
347    } else {
348        scale[2]
349    };
350
351    let r00 = m[0] / sx;
352    let r01 = m[4] / sy;
353    let r02 = m[8] / sz;
354    let r10 = m[1] / sx;
355    let r11 = m[5] / sy;
356    let r12 = m[9] / sz;
357    let r20 = m[2] / sx;
358    let r21 = m[6] / sy;
359    let r22 = m[10] / sz;
360    let trace = r00 + r11 + r22;
361
362    if trace > 0.0 {
363        let s = sqrt(trace + 1.0) * 2.0;
364        return Quaternion::new((r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s, 0.25 * s);
365    }
366    if r00 > r11 && r00 > r22 {
367        let s = sqrt(1.0 + r00 - r11 - r22) * 2.0;
368        return Quaternion::new(0.25 * s, (r01 + r10) / s, (r02 + r20) / s, (r21 - r12) / s);
369    }
370    if r11 > r22 {
371        let s = sqrt(1.0 + r11 - r00 - r22) * 2.0;
372        return Quaternion::new((r01 + r10) / s, 0.25 * s, (r12 + r21) / s, (r02 - r20) / s);
373    }
374    let s = sqrt(1.0 + r22 - r00 - r11) * 2.0;
375    Quaternion::new((r02 + r20) / s, (r12 + r21) / s, 0.25 * s, (r10 - r01) / s)
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn angle_uses_shortest_path() {
384        let a = Angle::from_degrees(359.0);
385        let b = Angle::from_degrees(1.0);
386        assert_eq!(a.lerp(&b, 0.5).normalized().degrees(), 0.0);
387    }
388
389    #[test]
390    fn quaternion_slerp_midpoint_is_normalized() {
391        let a = Quaternion::IDENTITY;
392        let b = Quaternion::from_axis_angle([0.0, 0.0, 1.0], Angle::from_degrees(180.0));
393        let mid = a.lerp(&b, 0.5);
394        assert!((mid.length() - 1.0).abs() < 0.0001);
395        assert!((mid.w.abs() - 0.70710677).abs() < 0.0002);
396    }
397
398    #[test]
399    fn mat4_interpolates_translation_scale_and_rotation() {
400        let a = Mat4::IDENTITY;
401        let b = Mat4::from_translation_rotation_scale(
402            [10.0, 20.0, 30.0],
403            Quaternion::from_axis_angle([0.0, 0.0, 1.0], Angle::from_degrees(90.0)),
404            [2.0, 4.0, 6.0],
405        );
406        let mid = a.lerp(&b, 0.5);
407        let (translation, rotation, scale) = mid.decompose();
408        assert_eq!(translation, [5.0, 10.0, 15.0]);
409        assert!((rotation.length() - 1.0).abs() < 0.0001);
410        assert!((scale[0] - 1.5).abs() < 0.0001);
411        assert!((scale[1] - 2.5).abs() < 0.0001);
412        assert!((scale[2] - 3.5).abs() < 0.0001);
413    }
414
415    #[test]
416    fn color_lerps_components() {
417        let a = Color::rgba(0.0, 0.25, 0.5, 1.0);
418        let b = Color::rgba(1.0, 0.75, 0.0, 0.5);
419        assert_eq!(a.lerp(&b, 0.5), Color::rgba(0.5, 0.5, 0.25, 0.75));
420    }
421}