Skip to main content

oxihuman_export/
camera_shake_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Export camera shake parameters and keyframe data.
6
7use std::f32::consts::TAU;
8
9/// Camera shake channel type.
10#[allow(dead_code)]
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ShakeChannel {
13    TranslateX,
14    TranslateY,
15    TranslateZ,
16    RotateX,
17    RotateY,
18    RotateZ,
19}
20
21/// A single shake keyframe.
22#[allow(dead_code)]
23#[derive(Debug, Clone)]
24pub struct ShakeKeyframe {
25    pub time: f32,
26    pub amplitude: f32,
27    pub frequency: f32,
28    pub channel: ShakeChannel,
29}
30
31/// Camera shake export data.
32#[allow(dead_code)]
33#[derive(Debug, Clone, Default)]
34pub struct CameraShakeExport {
35    pub keyframes: Vec<ShakeKeyframe>,
36    pub duration: f32,
37    pub decay: f32,
38}
39
40/// Create a new camera shake export.
41#[allow(dead_code)]
42pub fn new_camera_shake(duration: f32, decay: f32) -> CameraShakeExport {
43    CameraShakeExport {
44        keyframes: vec![],
45        duration,
46        decay,
47    }
48}
49
50/// Add a keyframe.
51#[allow(dead_code)]
52pub fn add_shake_keyframe(
53    export: &mut CameraShakeExport,
54    time: f32,
55    amplitude: f32,
56    frequency: f32,
57    channel: ShakeChannel,
58) {
59    export.keyframes.push(ShakeKeyframe {
60        time,
61        amplitude,
62        frequency,
63        channel,
64    });
65}
66
67/// Evaluate the shake displacement at a given time using sine oscillation.
68#[allow(dead_code)]
69pub fn evaluate_shake(export: &CameraShakeExport, time: f32, channel: ShakeChannel) -> f32 {
70    export
71        .keyframes
72        .iter()
73        .filter(|k| k.channel == channel)
74        .map(|k| {
75            let envelope = (-export.decay * (time - k.time).max(0.0)).exp();
76            k.amplitude * (TAU * k.frequency * time).sin() * envelope
77        })
78        .sum()
79}
80
81/// Count keyframes on a specific channel.
82#[allow(dead_code)]
83pub fn channel_keyframe_count(export: &CameraShakeExport, channel: ShakeChannel) -> usize {
84    export
85        .keyframes
86        .iter()
87        .filter(|k| k.channel == channel)
88        .count()
89}
90
91/// Peak amplitude across all keyframes.
92#[allow(dead_code)]
93pub fn peak_amplitude(export: &CameraShakeExport) -> f32 {
94    export
95        .keyframes
96        .iter()
97        .map(|k| k.amplitude.abs())
98        .fold(0.0_f32, f32::max)
99}
100
101/// Serialise shake export to a flat buffer.
102#[allow(dead_code)]
103pub fn serialise_shake(export: &CameraShakeExport) -> Vec<f32> {
104    let mut buf = vec![export.duration, export.decay];
105    for k in &export.keyframes {
106        buf.push(k.time);
107        buf.push(k.amplitude);
108        buf.push(k.frequency);
109    }
110    buf
111}
112
113/// Check if shake has decayed below threshold at `time`.
114#[allow(dead_code)]
115pub fn is_decayed(export: &CameraShakeExport, time: f32, threshold: f32) -> bool {
116    if export.keyframes.is_empty() {
117        return true;
118    }
119    let max_env = (-export.decay * time).exp();
120    max_env * peak_amplitude(export) < threshold
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    fn basic_shake() -> CameraShakeExport {
128        let mut s = new_camera_shake(2.0, 1.0);
129        add_shake_keyframe(&mut s, 0.0, 0.5, 10.0, ShakeChannel::TranslateX);
130        s
131    }
132
133    #[test]
134    fn test_new_camera_shake() {
135        let s = new_camera_shake(1.5, 0.5);
136        assert!((s.duration - 1.5).abs() < 1e-6);
137    }
138
139    #[test]
140    fn test_add_shake_keyframe() {
141        let s = basic_shake();
142        assert_eq!(s.keyframes.len(), 1);
143    }
144
145    #[test]
146    fn test_channel_count() {
147        let s = basic_shake();
148        assert_eq!(channel_keyframe_count(&s, ShakeChannel::TranslateX), 1);
149        assert_eq!(channel_keyframe_count(&s, ShakeChannel::TranslateY), 0);
150    }
151
152    #[test]
153    fn test_peak_amplitude() {
154        let s = basic_shake();
155        assert!((peak_amplitude(&s) - 0.5).abs() < 1e-6);
156    }
157
158    #[test]
159    fn test_evaluate_zero_time() {
160        let s = basic_shake();
161        let v = evaluate_shake(&s, 0.0, ShakeChannel::TranslateX);
162        assert!(v.abs() < 1e-5); // sin(0) = 0
163    }
164
165    #[test]
166    fn test_is_decayed_far_future() {
167        let s = basic_shake();
168        assert!(is_decayed(&s, 100.0, 0.01));
169    }
170
171    #[test]
172    fn test_is_not_decayed_at_start() {
173        let s = basic_shake();
174        assert!(!is_decayed(&s, 0.0, 0.1));
175    }
176
177    #[test]
178    fn test_serialise_length() {
179        let s = basic_shake();
180        let buf = serialise_shake(&s);
181        assert_eq!(buf.len(), 2 + 3); // duration, decay + 3 per keyframe
182    }
183
184    #[test]
185    fn test_serialise_empty() {
186        let s = new_camera_shake(1.0, 1.0);
187        assert_eq!(serialise_shake(&s).len(), 2);
188    }
189}