use bevy::prelude::*;
use crate::sequence::Shake;
const PHI: f32 = 1.618_034;
fn wobble(t: f32, hz: f32, phase: f32) -> f32 {
let x = t * hz * std::f32::consts::TAU + phase;
(x.sin() + (x * PHI).sin() * 0.5 + (x * PHI * PHI).sin() * 0.25) / 1.75
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct ShakeSample {
pub nudge: Vec3,
pub wobble: Quat,
pub fov_delta: f32,
pub focus_delta: f32,
}
pub(crate) fn shake_offset(shake: &Shake, t: f32) -> ShakeSample {
let seed = shake.seed as f32 * 12.9898;
let ramp = shake
.ramp
.as_ref()
.filter(|r| !r.keys.is_empty())
.map_or(1.0, |r| r.sample(t).max(0.0));
let amp = shake.amplitude_deg.to_radians() * ramp;
let hz = shake.frequency_hz.max(0.01);
let yaw = wobble(t, hz, seed) * amp;
let pitch = wobble(t, hz * 1.13, seed + 7.31) * amp;
let roll = wobble(t, hz * 0.87, seed + 3.77) * amp * 0.5;
let pos = Vec3::new(
wobble(t, hz * 0.71, seed + 1.13),
wobble(t, hz * 0.79, seed + 9.02),
0.0,
) * (shake.pos_amplitude * ramp);
let fov_delta = shake.fov_amplitude_deg.map_or(0.0, |amplitude| {
wobble(t, hz * 0.6, seed + 4.9) * amplitude.to_radians() * ramp
});
let focus_delta = shake.focus_amplitude.map_or(0.0, |amplitude| {
wobble(t, hz * 0.35, seed + 6.17) * amplitude * ramp
});
ShakeSample {
nudge: pos,
wobble: Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll),
fov_delta,
focus_delta,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sequence::ScalarTrack;
fn shake(seed: u32) -> Shake {
Shake {
amplitude_deg: 1.5,
frequency_hz: 9.0,
seed,
pos_amplitude: 0.02,
ramp: None,
fov_amplitude_deg: Some(0.8),
focus_amplitude: Some(0.5),
}
}
#[test]
fn same_seed_same_take() {
for t in [0.0, 0.37, 2.11, 8.4] {
assert_eq!(shake_offset(&shake(7), t), shake_offset(&shake(7), t));
}
}
#[test]
fn different_seeds_diverge_and_stay_bounded() {
let mut differs = false;
for i in 0..100 {
let t = i as f32 * 0.05;
let a = shake_offset(&shake(1), t);
let b = shake_offset(&shake(2), t);
differs |= a.nudge.distance(b.nudge) > 1e-4 || a.wobble.angle_between(b.wobble) > 1e-4;
assert!(a.nudge.length() <= 0.03, "position blew past its amplitude");
assert!(a.wobble.to_axis_angle().1 <= 3.0_f32.to_radians());
assert!(a.fov_delta.abs() <= 0.8_f32.to_radians());
assert!(a.focus_delta.abs() <= 0.5);
}
assert!(differs);
}
#[test]
fn ramp_zero_silences_every_channel() {
let mut zeroed = shake(3);
zeroed.ramp = Some(ScalarTrack::constant(0.0));
for t in [0.0, 0.5, 3.3] {
let sample = shake_offset(&zeroed, t);
assert_eq!(sample.nudge, Vec3::ZERO);
assert_eq!(sample.wobble, Quat::IDENTITY);
assert_eq!(sample.fov_delta, 0.0);
assert_eq!(sample.focus_delta, 0.0);
}
}
#[test]
fn ramp_scales_and_empty_ramp_reads_as_absent() {
let full = shake(5);
let mut half = shake(5);
half.ramp = Some(ScalarTrack::constant(0.5));
let mut empty = shake(5);
empty.ramp = Some(ScalarTrack::default());
let t = 1.23;
let f = shake_offset(&full, t);
let h = shake_offset(&half, t);
let e = shake_offset(&empty, t);
assert!((h.nudge - f.nudge * 0.5).length() < 1e-6);
assert!((h.fov_delta - f.fov_delta * 0.5).abs() < 1e-6);
assert!((h.focus_delta - f.focus_delta * 0.5).abs() < 1e-6);
assert_eq!(e, f);
}
}