use crate::types::SdfNode;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
pub enum Interpolation {
#[default]
Linear,
CubicBezier {
out_tangent: f32,
in_tangent: f32,
},
Step,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum LoopMode {
#[default]
Once,
Loop,
PingPong,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Keyframe {
pub time: f32,
pub value: f32,
pub interpolation: Interpolation,
}
impl Keyframe {
pub const fn new(time: f32, value: f32) -> Self {
Self {
time,
value,
interpolation: Interpolation::Linear,
}
}
pub const fn step(time: f32, value: f32) -> Self {
Self {
time,
value,
interpolation: Interpolation::Step,
}
}
pub const fn cubic(time: f32, value: f32, out_tangent: f32, in_tangent: f32) -> Self {
Self {
time,
value,
interpolation: Interpolation::CubicBezier {
out_tangent,
in_tangent,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Track {
pub name: String,
pub keyframes: Vec<Keyframe>,
pub loop_mode: LoopMode,
}
impl Track {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
keyframes: Vec::new(),
loop_mode: LoopMode::Once,
}
}
pub fn add_keyframe(&mut self, keyframe: Keyframe) {
let pos = self
.keyframes
.binary_search_by(|k| k.time.total_cmp(&keyframe.time))
.unwrap_or_else(|p| p);
self.keyframes.insert(pos, keyframe);
}
pub const fn with_loop(mut self, mode: LoopMode) -> Self {
self.loop_mode = mode;
self
}
pub fn duration(&self) -> f32 {
self.keyframes.last().map_or(0.0, |k| k.time)
}
pub fn evaluate(&self, time: f32) -> f32 {
if self.keyframes.is_empty() {
return 0.0;
}
let duration = self.duration();
if duration <= 0.0 {
return self.keyframes[0].value;
}
let t = match self.loop_mode {
LoopMode::Once => time.clamp(0.0, duration),
LoopMode::Loop => {
let t = time % duration;
if t < 0.0 {
t + duration
} else {
t
}
}
LoopMode::PingPong => {
let cycle = time / duration;
let phase = cycle % 2.0;
if phase < 1.0 {
phase * duration
} else {
(2.0 - phase) * duration
}
}
};
if t <= self.keyframes[0].time {
return self.keyframes[0].value;
}
if let Some(last) = self.keyframes.last() {
if t >= last.time {
return last.value;
}
}
let idx = self
.keyframes
.binary_search_by(|k| k.time.total_cmp(&t))
.unwrap_or_else(|p| p);
let idx = if idx > 0 { idx - 1 } else { 0 };
let k0 = &self.keyframes[idx];
let k1 = &self.keyframes[(idx + 1).min(self.keyframes.len() - 1)];
let span = k1.time - k0.time;
if span <= 0.0 {
return k0.value;
}
let alpha = (t - k0.time) / span;
match k0.interpolation {
Interpolation::Linear => (k1.value - k0.value).mul_add(alpha, k0.value),
Interpolation::Step => k0.value,
Interpolation::CubicBezier {
out_tangent,
in_tangent,
} => {
let t2 = alpha * alpha;
let t3 = t2 * alpha;
let h00 = 2.0f32.mul_add(t3, -(3.0 * t2)) + 1.0;
let h10 = 2.0f32.mul_add(-t2, t3) + alpha;
let h01 = (-2.0f32).mul_add(t3, 3.0 * t2);
let h11 = t3 - t2;
(h11 * span).mul_add(
in_tangent,
h00 * k0.value + h10 * span * out_tangent + h01 * k1.value,
)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Timeline {
pub name: String,
pub tracks: Vec<Track>,
pub speed: f32,
}
impl Timeline {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
tracks: Vec::new(),
speed: 1.0,
}
}
pub fn add_track(&mut self, track: Track) {
self.tracks.push(track);
}
pub fn duration(&self) -> f32 {
self.tracks
.iter()
.map(|t| t.duration())
.fold(0.0_f32, f32::max)
}
pub fn evaluate(&self, time: f32) -> Vec<(&str, f32)> {
let t = time * self.speed;
self.tracks
.iter()
.map(|track| (track.name.as_str(), track.evaluate(t)))
.collect()
}
pub fn get_value(&self, track_name: &str, time: f32) -> Option<f32> {
let t = time * self.speed;
self.tracks
.iter()
.find(|track| track.name == track_name)
.map(|track| track.evaluate(t))
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct AnimationParams {
pub translate_x: f32,
pub translate_y: f32,
pub translate_z: f32,
pub rotate_x: f32,
pub rotate_y: f32,
pub rotate_z: f32,
pub scale: f32,
pub twist: f32,
pub bend: f32,
}
impl AnimationParams {
#[inline]
pub fn has_translation(&self) -> bool {
self.translate_x.abs() > 1e-6
|| self.translate_y.abs() > 1e-6
|| self.translate_z.abs() > 1e-6
}
#[inline]
pub fn has_rotation(&self) -> bool {
self.rotate_x.abs() > 1e-6 || self.rotate_y.abs() > 1e-6 || self.rotate_z.abs() > 1e-6
}
#[inline]
pub fn has_scale(&self) -> bool {
(self.scale - 1.0).abs() > 1e-6
}
#[inline]
pub fn transform_point(&self, point: glam::Vec3) -> (glam::Vec3, f32) {
let mut p = point;
let mut scale_correction = 1.0_f32;
if self.has_translation() {
p -= glam::Vec3::new(self.translate_x, self.translate_y, self.translate_z);
}
if self.has_rotation() {
let quat = glam::Quat::from_euler(
glam::EulerRot::XYZ,
self.rotate_x,
self.rotate_y,
self.rotate_z,
);
p = quat.inverse() * p;
}
if self.has_scale() {
let inv_scale = 1.0 / self.scale;
p *= inv_scale;
scale_correction = self.scale;
}
(p, scale_correction)
}
}
#[derive(Debug, Clone)]
pub struct AnimatedSdf {
pub base: SdfNode,
pub timeline: Timeline,
}
impl AnimatedSdf {
pub const fn new(base: SdfNode, timeline: Timeline) -> Self {
Self { base, timeline }
}
pub fn evaluate_params(&self, time: f32) -> AnimationParams {
let values = self.timeline.evaluate(time);
let mut params = AnimationParams {
scale: 1.0,
..Default::default()
};
for (name, value) in &values {
match *name {
"translate.x" => params.translate_x = *value,
"translate.y" => params.translate_y = *value,
"translate.z" => params.translate_z = *value,
"rotate.x" => params.rotate_x = *value,
"rotate.y" => params.rotate_y = *value,
"rotate.z" => params.rotate_z = *value,
"scale" => params.scale = *value,
"twist" => params.twist = *value,
"bend" => params.bend = *value,
_ => {}
}
}
params
}
pub fn evaluate_at(&self, time: f32) -> SdfNode {
let values = self.timeline.evaluate(time);
let mut tx = 0.0_f32;
let mut ty = 0.0_f32;
let mut tz = 0.0_f32;
let mut rx = 0.0_f32;
let mut ry = 0.0_f32;
let mut rz = 0.0_f32;
let mut scale = 1.0_f32;
let mut twist = 0.0_f32;
let mut bend = 0.0_f32;
for (name, value) in &values {
match *name {
"translate.x" => tx = *value,
"translate.y" => ty = *value,
"translate.z" => tz = *value,
"rotate.x" => rx = *value,
"rotate.y" => ry = *value,
"rotate.z" => rz = *value,
"scale" => scale = *value,
"twist" => twist = *value,
"bend" => bend = *value,
_ => {}
}
}
let mut node = self.base.clone();
if twist.abs() > 1e-6 {
node = node.twist(twist);
}
if bend.abs() > 1e-6 {
node = node.bend(bend);
}
if (scale - 1.0).abs() > 1e-6 {
node = node.scale(scale);
}
if rx.abs() > 1e-6 || ry.abs() > 1e-6 || rz.abs() > 1e-6 {
node = node.rotate_euler(rx, ry, rz);
}
if tx.abs() > 1e-6 || ty.abs() > 1e-6 || tz.abs() > 1e-6 {
node = node.translate(tx, ty, tz);
}
node
}
}
pub fn morph(from: &SdfNode, to: &SdfNode, blend: f32) -> SdfNode {
let k = (1.0 - blend).max(0.01); SdfNode::SmoothUnion {
a: Arc::new(SdfNode::Scale {
child: Arc::new(from.clone()),
factor: 1.0,
}),
b: Arc::new(SdfNode::Scale {
child: Arc::new(to.clone()),
factor: 1.0,
}),
k,
}
}
pub fn eval_animated_compiled(
compiled: &crate::compiled::CompiledSdf,
params: &AnimationParams,
point: glam::Vec3,
) -> f32 {
let (transformed_point, scale_correction) = params.transform_point(point);
crate::compiled::eval_compiled(compiled, transformed_point) * scale_correction
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keyframe_linear() {
let mut track = Track::new("radius");
track.add_keyframe(Keyframe::new(0.0, 1.0));
track.add_keyframe(Keyframe::new(1.0, 3.0));
assert!((track.evaluate(0.0) - 1.0).abs() < 0.001);
assert!((track.evaluate(0.5) - 2.0).abs() < 0.001);
assert!((track.evaluate(1.0) - 3.0).abs() < 0.001);
}
#[test]
fn test_keyframe_step() {
let mut track = Track::new("state");
track.add_keyframe(Keyframe::step(0.0, 0.0));
track.add_keyframe(Keyframe::step(1.0, 1.0));
assert!((track.evaluate(0.0) - 0.0).abs() < 0.001);
assert!((track.evaluate(0.5) - 0.0).abs() < 0.001); assert!((track.evaluate(1.0) - 1.0).abs() < 0.001);
}
#[test]
fn test_loop_mode() {
let mut track = Track::new("loop_test").with_loop(LoopMode::Loop);
track.add_keyframe(Keyframe::new(0.0, 0.0));
track.add_keyframe(Keyframe::new(1.0, 1.0));
assert!((track.evaluate(0.0) - 0.0).abs() < 0.001);
assert!((track.evaluate(0.5) - 0.5).abs() < 0.001);
assert!((track.evaluate(1.5) - 0.5).abs() < 0.001);
}
#[test]
fn test_ping_pong() {
let mut track = Track::new("pp_test").with_loop(LoopMode::PingPong);
track.add_keyframe(Keyframe::new(0.0, 0.0));
track.add_keyframe(Keyframe::new(1.0, 1.0));
assert!((track.evaluate(0.5) - 0.5).abs() < 0.001);
assert!((track.evaluate(1.5) - 0.5).abs() < 0.001);
}
#[test]
fn test_timeline() {
let mut timeline = Timeline::new("bounce");
let mut tx = Track::new("translate.x");
tx.add_keyframe(Keyframe::new(0.0, 0.0));
tx.add_keyframe(Keyframe::new(1.0, 5.0));
timeline.add_track(tx);
let mut ty = Track::new("translate.y");
ty.add_keyframe(Keyframe::new(0.0, 0.0));
ty.add_keyframe(Keyframe::new(0.5, 3.0));
ty.add_keyframe(Keyframe::new(1.0, 0.0));
timeline.add_track(ty);
assert_eq!(timeline.duration(), 1.0);
let values = timeline.evaluate(0.5);
assert_eq!(values.len(), 2);
}
#[test]
fn test_animated_sdf() {
let sphere = SdfNode::sphere(1.0);
let mut timeline = Timeline::new("move");
let mut tx = Track::new("translate.x");
tx.add_keyframe(Keyframe::new(0.0, 0.0));
tx.add_keyframe(Keyframe::new(1.0, 5.0));
timeline.add_track(tx);
let animated = AnimatedSdf::new(sphere, timeline);
let node_t0 = animated.evaluate_at(0.0);
assert_eq!(node_t0.node_count(), 1);
let node_t1 = animated.evaluate_at(1.0);
assert!(node_t1.node_count() >= 2); }
#[test]
fn test_cubic_interpolation() {
let mut track = Track::new("smooth");
track.add_keyframe(Keyframe::cubic(0.0, 0.0, 1.0, 0.0));
track.add_keyframe(Keyframe::cubic(1.0, 1.0, 0.0, 1.0));
assert!((track.evaluate(0.0) - 0.0).abs() < 0.001);
assert!((track.evaluate(1.0) - 1.0).abs() < 0.001);
let mid = track.evaluate(0.5);
assert!((-0.1..=1.1).contains(&mid));
}
#[test]
fn test_animation_params_zero_alloc() {
let sphere = SdfNode::sphere(1.0);
let mut timeline = Timeline::new("move");
let mut tx = Track::new("translate.x");
tx.add_keyframe(Keyframe::new(0.0, 0.0));
tx.add_keyframe(Keyframe::new(1.0, 5.0));
timeline.add_track(tx);
let animated = AnimatedSdf::new(sphere, timeline);
let params = animated.evaluate_params(0.0);
assert!(params.translate_x.abs() < 0.001);
assert_eq!(params.scale, 1.0);
let params = animated.evaluate_params(1.0);
assert!((params.translate_x - 5.0).abs() < 0.001);
}
#[test]
fn test_eval_animated_compiled() {
let sphere = SdfNode::sphere(1.0);
let compiled = crate::compiled::CompiledSdf::compile(&sphere);
let mut timeline = Timeline::new("move");
let mut tx = Track::new("translate.x");
tx.add_keyframe(Keyframe::new(0.0, 0.0));
tx.add_keyframe(Keyframe::new(1.0, 5.0));
timeline.add_track(tx);
let animated = AnimatedSdf::new(sphere, timeline);
let params = animated.evaluate_params(0.0);
let d = eval_animated_compiled(&compiled, ¶ms, glam::Vec3::ZERO);
assert!((d + 1.0).abs() < 0.01);
let params = animated.evaluate_params(1.0);
let d = eval_animated_compiled(&compiled, ¶ms, glam::Vec3::ZERO);
assert!(d > 0.0);
}
#[test]
fn test_animation_params_size() {
assert_eq!(std::mem::size_of::<AnimationParams>(), 36);
}
#[test]
fn test_morph() {
let sphere = SdfNode::sphere(1.0);
let box3d = SdfNode::box3d(2.0, 2.0, 2.0);
let morphed = morph(&sphere, &box3d, 0.5);
assert!(morphed.node_count() >= 3);
}
#[test]
fn test_morph_midpoint() {
let sphere = SdfNode::sphere(1.0);
let box3d = SdfNode::box3d(1.0, 1.0, 1.0);
let p = glam::Vec3::new(2.0, 0.0, 0.0);
let ds = crate::eval::eval(&sphere, p);
let db = crate::eval::eval(&box3d, p);
let m_half = morph(&sphere, &box3d, 0.5);
let dm = crate::eval::eval(&m_half, p);
assert!(
dm >= ds.min(db) - 0.1 && dm <= ds.max(db) + 0.1,
"dm={} should be between ds={} and db={}",
dm,
ds,
db
);
}
#[test]
fn test_timeline_duration() {
let mut timeline = Timeline::new("test");
let mut track = Track::new("x");
track.add_keyframe(Keyframe::new(0.0, 0.0));
track.add_keyframe(Keyframe::new(2.5, 1.0));
timeline.add_track(track);
assert!((timeline.duration() - 2.5).abs() < 1e-5);
}
#[test]
fn test_track_empty() {
let track = Track::new("empty");
assert_eq!(track.evaluate(0.5), 0.0);
}
#[test]
fn test_track_single_keyframe() {
let mut track = Track::new("single");
track.add_keyframe(Keyframe::new(1.0, 42.0));
assert_eq!(track.evaluate(0.0), 42.0);
assert_eq!(track.evaluate(1.0), 42.0);
assert_eq!(track.evaluate(2.0), 42.0);
}
#[test]
fn test_timeline_evaluate_tracks() {
let mut timeline = Timeline::new("test");
let mut track = Track::new("translate.x");
track.add_keyframe(Keyframe::new(0.0, 0.0));
track.add_keyframe(Keyframe::new(1.0, 10.0));
timeline.add_track(track);
let values = timeline.evaluate(0.5);
assert_eq!(values.len(), 1);
let (name, val) = &values[0];
assert_eq!(*name, "translate.x");
assert!((val - 5.0).abs() < 0.1);
}
#[test]
fn test_timeline_speed() {
let mut timeline = Timeline::new("fast");
timeline.speed = 2.0;
let mut track = Track::new("translate.x");
track.add_keyframe(Keyframe::new(0.0, 0.0));
track.add_keyframe(Keyframe::new(1.0, 10.0));
timeline.add_track(track);
let values = timeline.evaluate(0.5);
let (_, val) = &values[0];
assert!((val - 10.0).abs() < 0.1, "val={}", val);
}
#[test]
fn test_animation_params_all_fields() {
let sphere = SdfNode::sphere(1.0);
let mut timeline = Timeline::new("all");
for name in [
"translate.x",
"translate.y",
"translate.z",
"rotate.x",
"rotate.y",
"rotate.z",
"scale",
"morph_t",
] {
let mut t = Track::new(name);
t.add_keyframe(Keyframe::new(0.0, 0.0));
t.add_keyframe(Keyframe::new(1.0, 1.0));
timeline.add_track(t);
}
let animated = AnimatedSdf::new(sphere, timeline);
let params = animated.evaluate_params(0.5);
assert!(params.translate_x > 0.0);
assert!(params.translate_y > 0.0);
assert!(params.translate_z > 0.0);
}
}