#![allow(missing_docs)]
#![allow(dead_code)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EaseKind {
#[default]
Linear,
EaseIn,
EaseOut,
EaseInOut,
Step,
}
impl EaseKind {
pub fn apply(self, t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
match self {
EaseKind::Linear => t,
EaseKind::EaseIn => t * t * t,
EaseKind::EaseOut => {
let u = 1.0 - t;
1.0 - u * u * u
}
EaseKind::EaseInOut => {
if t < 0.5 {
4.0 * t * t * t
} else {
let u = -2.0 * t + 2.0;
1.0 - u * u * u / 2.0
}
}
EaseKind::Step => {
if t < 1.0 {
0.0
} else {
1.0
}
}
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Vec3Keyframe {
pub time: f64,
pub value: [f64; 3],
pub ease: EaseKind,
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Vec3Track {
pub keyframes: Vec<Vec3Keyframe>,
pub looping: bool,
}
impl Vec3Track {
pub fn new(looping: bool) -> Self {
Self {
keyframes: Vec::new(),
looping,
}
}
pub fn push_keyframe(&mut self, time: f64, value: [f64; 3], ease: EaseKind) {
self.keyframes.push(Vec3Keyframe { time, value, ease });
}
pub fn duration(&self) -> f64 {
self.keyframes.last().map_or(0.0, |k| k.time)
}
pub fn len(&self) -> usize {
self.keyframes.len()
}
pub fn is_empty(&self) -> bool {
self.keyframes.is_empty()
}
pub fn sample(&self, mut t: f64) -> [f64; 3] {
if self.keyframes.is_empty() {
return [0.0; 3];
}
if self.keyframes.len() == 1 {
return self.keyframes[0].value;
}
let dur = self.duration();
if self.looping && dur > 0.0 {
t = t.rem_euclid(dur);
}
let first = self
.keyframes
.first()
.expect("at least two keyframes, checked above");
if t <= first.time {
return first.value;
}
let last = self
.keyframes
.last()
.expect("at least two keyframes, checked above");
if t >= last.time {
return last.value;
}
let idx = self
.keyframes
.partition_point(|k| k.time <= t)
.saturating_sub(1);
let a = &self.keyframes[idx];
let b = &self.keyframes[idx + 1];
let span = b.time - a.time;
let raw_t = if span > 1e-12 {
(t - a.time) / span
} else {
1.0
};
let et = a.ease.apply(raw_t);
[
a.value[0] + (b.value[0] - a.value[0]) * et,
a.value[1] + (b.value[1] - a.value[1]) * et,
a.value[2] + (b.value[2] - a.value[2]) * et,
]
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct QuatKeyframe {
pub time: f64,
pub value: [f64; 4],
pub ease: EaseKind,
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct QuatTrack {
pub keyframes: Vec<QuatKeyframe>,
pub looping: bool,
}
impl QuatTrack {
pub fn new(looping: bool) -> Self {
Self {
keyframes: Vec::new(),
looping,
}
}
pub fn push_keyframe(&mut self, time: f64, value: [f64; 4], ease: EaseKind) {
self.keyframes.push(QuatKeyframe { time, value, ease });
}
pub fn duration(&self) -> f64 {
self.keyframes.last().map_or(0.0, |k| k.time)
}
pub fn len(&self) -> usize {
self.keyframes.len()
}
pub fn is_empty(&self) -> bool {
self.keyframes.is_empty()
}
pub fn sample(&self, mut t: f64) -> [f64; 4] {
const IDENTITY: [f64; 4] = [0.0, 0.0, 0.0, 1.0];
if self.keyframes.is_empty() {
return IDENTITY;
}
if self.keyframes.len() == 1 {
return self.keyframes[0].value;
}
let dur = self.duration();
if self.looping && dur > 0.0 {
t = t.rem_euclid(dur);
}
let first = self
.keyframes
.first()
.expect("at least two keyframes, checked above");
if t <= first.time {
return first.value;
}
let last = self
.keyframes
.last()
.expect("at least two keyframes, checked above");
if t >= last.time {
return last.value;
}
let idx = self
.keyframes
.partition_point(|k| k.time <= t)
.saturating_sub(1);
let a = &self.keyframes[idx];
let b = &self.keyframes[idx + 1];
let span = b.time - a.time;
let raw_t = if span > 1e-12 {
(t - a.time) / span
} else {
1.0
};
let et = a.ease.apply(raw_t);
quat_slerp(a.value, b.value, et)
}
}
fn quat_slerp(a: [f64; 4], mut b: [f64; 4], t: f64) -> [f64; 4] {
let mut dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
if dot < 0.0 {
b = [-b[0], -b[1], -b[2], -b[3]];
dot = -dot;
}
const SLERP_THRESHOLD: f64 = 0.9995;
let raw = if dot > SLERP_THRESHOLD {
[
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2]),
a[3] + t * (b[3] - a[3]),
]
} else {
let theta_0 = dot.clamp(-1.0, 1.0).acos(); let theta = theta_0 * t;
let sin_theta = theta.sin();
let sin_theta_0 = theta_0.sin();
let s0 = theta.cos() - dot * sin_theta / sin_theta_0;
let s1 = sin_theta / sin_theta_0;
[
s0 * a[0] + s1 * b[0],
s0 * a[1] + s1 * b[1],
s0 * a[2] + s1 * b[2],
s0 * a[3] + s1 * b[3],
]
};
let len = (raw[0] * raw[0] + raw[1] * raw[1] + raw[2] * raw[2] + raw[3] * raw[3]).sqrt();
if len > 1e-12 {
[raw[0] / len, raw[1] / len, raw[2] / len, raw[3] / len]
} else {
[0.0, 0.0, 0.0, 1.0] }
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct BodyAnimation {
pub body_id: String,
pub position: Option<Vec3Track>,
pub rotation: Option<QuatTrack>,
pub scale: Option<Vec3Track>,
}
impl BodyAnimation {
pub fn new(body_id: impl Into<String>) -> Self {
Self {
body_id: body_id.into(),
position: None,
rotation: None,
scale: None,
}
}
pub fn with_position(mut self, track: Vec3Track) -> Self {
self.position = Some(track);
self
}
pub fn with_rotation(mut self, track: QuatTrack) -> Self {
self.rotation = Some(track);
self
}
pub fn with_scale(mut self, track: Vec3Track) -> Self {
self.scale = Some(track);
self
}
pub fn duration(&self) -> f64 {
let pd = self.position.as_ref().map_or(0.0, |t| t.duration());
let rd = self.rotation.as_ref().map_or(0.0, |t| t.duration());
let sd = self.scale.as_ref().map_or(0.0, |t| t.duration());
pd.max(rd).max(sd)
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct AnimationClip {
pub name: String,
pub body_animations: Vec<BodyAnimation>,
}
impl AnimationClip {
pub fn new(name: impl Into<String>, body_animations: Vec<BodyAnimation>) -> Self {
Self {
name: name.into(),
body_animations,
}
}
pub fn duration(&self) -> f64 {
self.body_animations
.iter()
.map(|ba| ba.duration())
.fold(0.0_f64, f64::max)
}
pub fn body_anim(&self, body_id: &str) -> Option<&BodyAnimation> {
self.body_animations.iter().find(|ba| ba.body_id == body_id)
}
pub fn body_count(&self) -> usize {
self.body_animations.len()
}
}
impl std::fmt::Display for AnimationClip {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"AnimationClip {{ name: {:?}, bodies: {}, duration: {:.3}s }}",
self.name,
self.body_animations.len(),
self.duration()
)
}
}
#[derive(Clone, Debug)]
pub struct AnimBodyState {
pub body_id: String,
pub position: Option<[f64; 3]>,
pub rotation: Option<[f64; 4]>,
pub scale: Option<[f64; 3]>,
}
#[derive(Clone, Debug)]
pub struct AnimationPlayer {
pub clip: AnimationClip,
pub time: f64,
pub speed: f64,
playing: bool,
pub looping: bool,
}
impl AnimationPlayer {
pub fn new(clip: AnimationClip) -> Self {
Self {
clip,
time: 0.0,
speed: 1.0,
playing: false,
looping: false,
}
}
pub fn play(&mut self) {
self.playing = true;
}
pub fn pause(&mut self) {
self.playing = false;
}
pub fn stop(&mut self) {
self.playing = false;
self.time = 0.0;
}
pub fn seek(&mut self, t: f64) {
self.time = t.clamp(0.0, self.clip.duration());
}
pub fn is_playing(&self) -> bool {
self.playing
}
pub fn progress(&self) -> f64 {
let dur = self.clip.duration();
if dur > 0.0 {
(self.time / dur).clamp(0.0, 1.0)
} else {
1.0
}
}
pub fn is_finished(&self) -> bool {
self.time >= self.clip.duration()
}
pub fn update(&mut self, dt: f64) -> Vec<AnimBodyState> {
if self.playing {
let dur = self.clip.duration();
self.time += dt * self.speed;
if self.looping && dur > 0.0 {
self.time = self.time.rem_euclid(dur);
} else {
self.time = self.time.min(dur);
if self.time >= dur {
self.playing = false;
}
}
}
self.sample_all()
}
fn sample_all(&self) -> Vec<AnimBodyState> {
self.clip
.body_animations
.iter()
.map(|ba| AnimBodyState {
body_id: ba.body_id.clone(),
position: ba.position.as_ref().map(|t| t.sample(self.time)),
rotation: ba.rotation.as_ref().map(|t| t.sample(self.time)),
scale: ba.scale.as_ref().map(|t| t.sample(self.time)),
})
.collect()
}
}
impl std::fmt::Display for AnimationPlayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"AnimationPlayer {{ clip: {:?}, time: {:.3}s/{:.3}s, playing: {}, speed: {} }}",
self.clip.name,
self.time,
self.clip.duration(),
self.playing,
self.speed
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ease_kind_boundary() {
assert!((EaseKind::Linear.apply(0.0)).abs() < 1e-9);
assert!((EaseKind::Linear.apply(1.0) - 1.0).abs() < 1e-9);
assert!((EaseKind::EaseInOut.apply(0.5) - 0.5).abs() < 1e-9);
assert!((EaseKind::Step.apply(0.99)).abs() < 1e-9);
assert!((EaseKind::Step.apply(1.0) - 1.0).abs() < 1e-9);
}
#[test]
fn vec3_track_clamp() {
let mut t = Vec3Track::new(false);
t.push_keyframe(1.0, [1.0, 0.0, 0.0], EaseKind::Linear);
t.push_keyframe(2.0, [2.0, 0.0, 0.0], EaseKind::Linear);
assert!((t.sample(0.0)[0] - 1.0).abs() < 1e-9);
assert!((t.sample(5.0)[0] - 2.0).abs() < 1e-9);
assert!((t.sample(1.5)[0] - 1.5).abs() < 1e-9);
}
#[test]
fn vec3_track_loop() {
let mut t = Vec3Track::new(true);
t.push_keyframe(0.0, [0.0, 0.0, 0.0], EaseKind::Linear);
t.push_keyframe(1.0, [1.0, 0.0, 0.0], EaseKind::Linear);
assert!((t.sample(1.5)[0] - 0.5).abs() < 1e-9);
}
#[test]
fn quat_slerp_identity() {
let id = [0.0_f64, 0.0, 0.0, 1.0];
let r = quat_slerp(id, id, 0.5);
assert!((r[3] - 1.0).abs() < 1e-6);
let len = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2] + r[3] * r[3]).sqrt();
assert!((len - 1.0).abs() < 1e-9);
}
#[test]
fn quat_slerp_short_path() {
let a = [0.0_f64, 0.0, 0.0, 1.0];
let b_neg = [0.0_f64, 0.0, 0.0, -1.0]; let r = quat_slerp(a, b_neg, 0.5);
let len = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2] + r[3] * r[3]).sqrt();
assert!((len - 1.0).abs() < 1e-9);
}
#[test]
fn animation_player_lifecycle() {
let mut pos = Vec3Track::new(false);
pos.push_keyframe(0.0, [0.0, 0.0, 0.0], EaseKind::Linear);
pos.push_keyframe(2.0, [10.0, 0.0, 0.0], EaseKind::Linear);
let ba = BodyAnimation::new("obj").with_position(pos);
let clip = AnimationClip::new("move", vec![ba]);
let mut player = AnimationPlayer::new(clip);
player.update(1.0);
assert!((player.time).abs() < 1e-9);
player.play();
let states = player.update(1.0);
assert_eq!(states.len(), 1);
assert!((states[0].position.unwrap()[0] - 5.0).abs() < 1e-9);
assert!(!player.is_finished());
player.update(1.0);
assert!(player.is_finished());
assert!(!player.is_playing());
}
}