#![allow(dead_code)]
use std::collections::HashMap;
pub type ExprWeights = HashMap<String, f32>;
#[derive(Debug, Clone, PartialEq)]
pub enum EaseType {
Linear,
EaseIn,
EaseOut,
EaseInOut,
Step,
}
pub struct ExprKeyframe {
pub time: f32,
pub weights: ExprWeights,
pub ease_to_next: EaseType,
pub hold_duration: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SeqLoopMode {
Once,
Loop,
PingPong,
}
pub struct ExprTrack {
pub name: String,
pub keyframes: Vec<ExprKeyframe>,
pub loop_mode: SeqLoopMode,
}
pub struct ExprSequencer {
tracks: Vec<ExprTrack>,
}
pub fn ease_value(t: f32, ease: &EaseType) -> f32 {
let t = t.clamp(0.0, 1.0);
match ease {
EaseType::Linear => t,
EaseType::EaseIn => t * t,
EaseType::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
EaseType::EaseInOut => t * t * (3.0 - 2.0 * t),
EaseType::Step => {
if t < 1.0 {
0.0
} else {
1.0
}
}
}
}
pub fn lerp_weights(a: &ExprWeights, b: &ExprWeights, t: f32) -> ExprWeights {
let t = t.clamp(0.0, 1.0);
let mut out: ExprWeights = HashMap::new();
for (k, va) in a {
let vb = b.get(k).copied().unwrap_or(0.0);
out.insert(k.clone(), va + (vb - va) * t);
}
for (k, vb) in b {
if !out.contains_key(k) {
out.insert(k.clone(), vb * t);
}
}
out
}
pub fn blink_track(interval: f32, duration: f32) -> ExprTrack {
let interval = interval.max(duration + 0.01);
let close_t = duration * 0.3;
let open_t = duration;
let closed: ExprWeights = [("blink".to_string(), 1.0)].into();
let open: ExprWeights = [("blink".to_string(), 0.0)].into();
let mut track = ExprTrack::new("blink");
track.loop_mode = SeqLoopMode::Loop;
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: open.clone(),
ease_to_next: EaseType::EaseIn,
hold_duration: interval - duration,
});
track.add_keyframe(ExprKeyframe {
time: interval - duration,
weights: open.clone(),
ease_to_next: EaseType::EaseIn,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: interval - duration + close_t,
weights: closed,
ease_to_next: EaseType::EaseOut,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: interval - duration + open_t,
weights: open,
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
track
}
pub fn breathing_expr_track(rate: f32) -> ExprTrack {
let period = 60.0 / rate.max(1.0);
let inhale_end = period * 0.4;
let exhale_end = period;
let inhale: ExprWeights = [
("nostrils_flare".to_string(), 0.3),
("chest_expand".to_string(), 0.2),
]
.into();
let exhale: ExprWeights = [
("nostrils_flare".to_string(), 0.0),
("chest_expand".to_string(), 0.0),
]
.into();
let mut track = ExprTrack::new("breathing");
track.loop_mode = SeqLoopMode::Loop;
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: exhale.clone(),
ease_to_next: EaseType::EaseInOut,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: inhale_end,
weights: inhale,
ease_to_next: EaseType::EaseInOut,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: exhale_end,
weights: exhale,
ease_to_next: EaseType::EaseInOut,
hold_duration: 0.0,
});
track
}
impl ExprTrack {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
keyframes: Vec::new(),
loop_mode: SeqLoopMode::Once,
}
}
pub fn add_keyframe(&mut self, kf: ExprKeyframe) {
let pos = self.keyframes.partition_point(|k| k.time <= kf.time);
self.keyframes.insert(pos, kf);
}
pub fn remove_keyframe(&mut self, idx: usize) {
if idx < self.keyframes.len() {
self.keyframes.remove(idx);
}
}
pub fn duration(&self) -> f32 {
self.keyframes
.last()
.map(|kf| kf.time + kf.hold_duration)
.unwrap_or(0.0)
}
pub fn evaluate(&self, t_in: f32) -> ExprWeights {
if self.keyframes.is_empty() {
return HashMap::new();
}
if self.keyframes.len() == 1 {
return self.keyframes[0].weights.clone();
}
let dur = self.duration();
let t = self.remap_time(t_in, dur);
let next_idx = self.keyframes.partition_point(|kf| kf.time <= t);
if next_idx == 0 {
return self.keyframes[0].weights.clone();
}
if next_idx >= self.keyframes.len() {
return self.keyframes[self.keyframes.len() - 1].weights.clone();
}
let prev = &self.keyframes[next_idx - 1];
let next = &self.keyframes[next_idx];
let transition_start = prev.time + prev.hold_duration;
if t <= transition_start {
return prev.weights.clone();
}
let span = next.time - transition_start;
let local_t = if span > f32::EPSILON {
((t - transition_start) / span).clamp(0.0, 1.0)
} else {
1.0
};
let eased_t = ease_value(local_t, &prev.ease_to_next);
lerp_weights(&prev.weights, &next.weights, eased_t)
}
fn remap_time(&self, t: f32, dur: f32) -> f32 {
if dur <= 0.0 {
return 0.0;
}
match self.loop_mode {
SeqLoopMode::Once => t.clamp(0.0, dur),
SeqLoopMode::Loop => {
let wrapped = t % dur;
if wrapped < 0.0 {
wrapped + dur
} else {
wrapped
}
}
SeqLoopMode::PingPong => {
let cycle = dur * 2.0;
let pos = t % cycle;
let pos = if pos < 0.0 { pos + cycle } else { pos };
if pos <= dur {
pos
} else {
cycle - pos
}
}
}
}
}
impl ExprSequencer {
pub fn new() -> Self {
Self { tracks: Vec::new() }
}
pub fn add_track(&mut self, track: ExprTrack) {
self.tracks.push(track);
}
pub fn track_count(&self) -> usize {
self.tracks.len()
}
pub fn evaluate_all(&self, t: f32) -> ExprWeights {
let mut out: ExprWeights = HashMap::new();
for track in &self.tracks {
let w = track.evaluate(t);
for (k, v) in w {
*out.entry(k).or_insert(0.0) += v;
}
}
for v in out.values_mut() {
*v = v.clamp(0.0, 1.0);
}
out
}
}
impl Default for ExprSequencer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-5
}
fn single_weight(name: &str, v: f32) -> ExprWeights {
[(name.to_string(), v)].into()
}
#[test]
fn test_ease_linear() {
assert!(approx(ease_value(0.0, &EaseType::Linear), 0.0));
assert!(approx(ease_value(0.5, &EaseType::Linear), 0.5));
assert!(approx(ease_value(1.0, &EaseType::Linear), 1.0));
}
#[test]
fn test_ease_in() {
assert!(approx(ease_value(0.0, &EaseType::EaseIn), 0.0));
assert!(approx(ease_value(0.5, &EaseType::EaseIn), 0.25));
assert!(approx(ease_value(1.0, &EaseType::EaseIn), 1.0));
}
#[test]
fn test_ease_out() {
assert!(approx(ease_value(0.0, &EaseType::EaseOut), 0.0));
assert!(approx(ease_value(0.5, &EaseType::EaseOut), 0.75));
assert!(approx(ease_value(1.0, &EaseType::EaseOut), 1.0));
}
#[test]
fn test_ease_in_out_midpoint() {
assert!(approx(ease_value(0.5, &EaseType::EaseInOut), 0.5));
}
#[test]
fn test_ease_step() {
assert!(approx(ease_value(0.0, &EaseType::Step), 0.0));
assert!(approx(ease_value(0.5, &EaseType::Step), 0.0));
assert!(approx(ease_value(1.0, &EaseType::Step), 1.0));
}
#[test]
fn test_ease_clamps_input() {
assert!(approx(ease_value(-1.0, &EaseType::Linear), 0.0));
assert!(approx(ease_value(2.0, &EaseType::Linear), 1.0));
}
#[test]
fn test_lerp_weights_midpoint() {
let a = single_weight("smile", 0.0);
let b = single_weight("smile", 1.0);
let mid = lerp_weights(&a, &b, 0.5);
assert!(approx(*mid.get("smile").expect("should succeed"), 0.5));
}
#[test]
fn test_lerp_weights_missing_key_in_b() {
let a = single_weight("anger", 0.8);
let b: ExprWeights = HashMap::new();
let result = lerp_weights(&a, &b, 0.5);
assert!(approx(*result.get("anger").expect("should succeed"), 0.4));
}
#[test]
fn test_lerp_weights_missing_key_in_a() {
let a: ExprWeights = HashMap::new();
let b = single_weight("joy", 1.0);
let result = lerp_weights(&a, &b, 0.5);
assert!(approx(*result.get("joy").expect("should succeed"), 0.5));
}
#[test]
fn test_lerp_weights_t0_equals_a() {
let a = single_weight("fear", 0.6);
let b = single_weight("fear", 0.0);
let result = lerp_weights(&a, &b, 0.0);
assert!(approx(*result.get("fear").expect("should succeed"), 0.6));
}
#[test]
fn test_track_add_sorted() {
let mut track = ExprTrack::new("test");
track.add_keyframe(ExprKeyframe {
time: 2.0,
weights: single_weight("a", 1.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: single_weight("a", 0.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
assert!(approx(track.keyframes[0].time, 0.0));
assert!(approx(track.keyframes[1].time, 2.0));
}
#[test]
fn test_track_duration() {
let mut track = ExprTrack::new("dur_test");
track.add_keyframe(ExprKeyframe {
time: 3.0,
weights: HashMap::new(),
ease_to_next: EaseType::Linear,
hold_duration: 0.5,
});
assert!(approx(track.duration(), 3.5));
}
#[test]
fn test_track_remove_keyframe() {
let mut track = ExprTrack::new("rm");
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: HashMap::new(),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: 1.0,
weights: HashMap::new(),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
track.remove_keyframe(0);
assert_eq!(track.keyframes.len(), 1);
assert!(approx(track.keyframes[0].time, 1.0));
}
#[test]
fn test_track_evaluate_at_start() {
let mut track = ExprTrack::new("eval");
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: single_weight("smile", 0.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: 1.0,
weights: single_weight("smile", 1.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
let w = track.evaluate(0.0);
assert!(approx(*w.get("smile").expect("should succeed"), 0.0));
}
#[test]
fn test_track_evaluate_at_end() {
let mut track = ExprTrack::new("eval_end");
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: single_weight("smile", 0.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: 1.0,
weights: single_weight("smile", 1.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
let w = track.evaluate(1.0);
assert!(approx(*w.get("smile").expect("should succeed"), 1.0));
}
#[test]
fn test_track_evaluate_midpoint_linear() {
let mut track = ExprTrack::new("mid_linear");
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: single_weight("brow", 0.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: 2.0,
weights: single_weight("brow", 1.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
let w = track.evaluate(1.0);
assert!(approx(*w.get("brow").expect("should succeed"), 0.5));
}
#[test]
fn test_track_loop_mode() {
let mut track = ExprTrack::new("loop");
track.loop_mode = SeqLoopMode::Loop;
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: single_weight("x", 0.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: 1.0,
weights: single_weight("x", 1.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
let w = track.evaluate(1.5);
assert!(approx(*w.get("x").expect("should succeed"), 0.5));
}
#[test]
fn test_track_pingpong_mode() {
let mut track = ExprTrack::new("pp");
track.loop_mode = SeqLoopMode::PingPong;
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: single_weight("y", 0.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
track.add_keyframe(ExprKeyframe {
time: 1.0,
weights: single_weight("y", 1.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
let w = track.evaluate(1.5);
assert!(approx(*w.get("y").expect("should succeed"), 0.5));
}
#[test]
fn test_track_hold_duration() {
let mut track = ExprTrack::new("hold");
track.add_keyframe(ExprKeyframe {
time: 0.0,
weights: single_weight("sad", 1.0),
ease_to_next: EaseType::Linear,
hold_duration: 1.0, });
track.add_keyframe(ExprKeyframe {
time: 2.0,
weights: single_weight("sad", 0.0),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
let w = track.evaluate(0.5);
assert!(approx(*w.get("sad").expect("should succeed"), 1.0));
let w2 = track.evaluate(1.5);
assert!(approx(*w2.get("sad").expect("should succeed"), 0.5));
}
#[test]
fn test_sequencer_track_count() {
let mut seq = ExprSequencer::new();
seq.add_track(ExprTrack::new("a"));
seq.add_track(ExprTrack::new("b"));
assert_eq!(seq.track_count(), 2);
}
#[test]
fn test_sequencer_evaluate_all_additive_clamp() {
let mut seq = ExprSequencer::new();
let mut t1 = ExprTrack::new("t1");
t1.add_keyframe(ExprKeyframe {
time: 0.0,
weights: single_weight("smile", 0.8),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
let mut t2 = ExprTrack::new("t2");
t2.add_keyframe(ExprKeyframe {
time: 0.0,
weights: single_weight("smile", 0.6),
ease_to_next: EaseType::Linear,
hold_duration: 0.0,
});
seq.add_track(t1);
seq.add_track(t2);
let w = seq.evaluate_all(0.0);
assert!(approx(*w.get("smile").expect("should succeed"), 1.0));
}
#[test]
fn test_blink_track_loops() {
let track = blink_track(4.0, 0.2);
assert_eq!(track.loop_mode, SeqLoopMode::Loop);
}
#[test]
fn test_blink_track_has_keyframes() {
let track = blink_track(4.0, 0.2);
assert!(track.keyframes.len() >= 3);
}
#[test]
fn test_breathing_track_loops() {
let track = breathing_expr_track(15.0);
assert_eq!(track.loop_mode, SeqLoopMode::Loop);
}
#[test]
fn test_breathing_track_has_keyframes() {
let track = breathing_expr_track(15.0);
assert!(track.keyframes.len() >= 2);
}
#[test]
fn test_breathing_track_period() {
let rate = 12.0_f32;
let expected_period = 60.0 / rate;
let track = breathing_expr_track(rate);
assert!(approx(track.duration(), expected_period));
}
}