1pub mod skeleton;
13pub mod clips;
14
15use std::collections::HashMap;
16
17#[derive(Debug, Clone)]
21pub struct AnimCurve {
22 pub keyframes: Vec<(f32, f32, f32, f32)>,
24 pub extrapolate: Extrapolate,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum Extrapolate {
29 Clamp,
31 Loop,
33 PingPong,
35 Linear,
37}
38
39impl AnimCurve {
40 pub fn constant(value: f32) -> Self {
41 Self {
42 keyframes: vec![(0.0, value, 0.0, 0.0)],
43 extrapolate: Extrapolate::Clamp,
44 }
45 }
46
47 pub fn linear(t0: f32, v0: f32, t1: f32, v1: f32) -> Self {
48 let tangent = if (t1 - t0).abs() > 1e-6 { (v1 - v0) / (t1 - t0) } else { 0.0 };
49 Self {
50 keyframes: vec![(t0, v0, tangent, tangent), (t1, v1, tangent, tangent)],
51 extrapolate: Extrapolate::Clamp,
52 }
53 }
54
55 pub fn sample(&self, t: f32) -> f32 {
57 if self.keyframes.is_empty() { return 0.0; }
58 if self.keyframes.len() == 1 { return self.keyframes[0].1; }
59
60 let duration = self.keyframes.last().unwrap().0 - self.keyframes[0].0;
61 let t = self.wrap_time(t, duration);
62
63 let idx = self.keyframes.partition_point(|k| k.0 <= t);
65 if idx == 0 { return self.keyframes[0].1; }
66 if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().1; }
67
68 let (t0, v0, _in0, out0) = self.keyframes[idx - 1];
69 let (t1, v1, in1, _out1) = self.keyframes[idx];
70
71 let dt = t1 - t0;
72 if dt < 1e-6 { return v1; }
73
74 let u = (t - t0) / dt;
75 let h00 = (2.0 * u * u * u) - (3.0 * u * u) + 1.0;
77 let h10 = u * u * u - (2.0 * u * u) + u;
78 let h01 = -(2.0 * u * u * u) + (3.0 * u * u);
79 let h11 = u * u * u - u * u;
80 h00 * v0 + h10 * dt * out0 + h01 * v1 + h11 * dt * in1
81 }
82
83 fn wrap_time(&self, t: f32, duration: f32) -> f32 {
84 if duration < 1e-6 { return self.keyframes[0].0; }
85 match self.extrapolate {
86 Extrapolate::Clamp => t.clamp(self.keyframes[0].0, self.keyframes.last().unwrap().0),
87 Extrapolate::Loop => self.keyframes[0].0 + (t - self.keyframes[0].0).rem_euclid(duration),
88 Extrapolate::PingPong => {
89 let local = (t - self.keyframes[0].0).rem_euclid(duration * 2.0);
90 self.keyframes[0].0 + if local < duration { local } else { duration * 2.0 - local }
91 }
92 Extrapolate::Linear => t,
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
101pub struct AnimChannel {
102 pub target_path: String, pub curve: AnimCurve,
104}
105
106#[derive(Debug, Clone)]
110pub struct AnimClip {
111 pub name: String,
112 pub duration: f32,
113 pub fps: f32,
114 pub looping: bool,
115 pub channels: Vec<AnimChannel>,
116 pub root_motion: Option<RootMotionData>,
118}
119
120impl AnimClip {
121 pub fn new(name: &str, duration: f32) -> Self {
122 Self {
123 name: name.to_string(),
124 duration,
125 fps: 30.0,
126 looping: true,
127 channels: Vec::new(),
128 root_motion: None,
129 }
130 }
131
132 pub fn add_channel(&mut self, path: &str, curve: AnimCurve) {
134 self.channels.push(AnimChannel { target_path: path.to_string(), curve });
135 }
136
137 pub fn sample(&self, t: f32) -> HashMap<String, f32> {
139 let t = if self.looping { t.rem_euclid(self.duration) } else { t.min(self.duration) };
140 self.channels.iter().map(|ch| (ch.target_path.clone(), ch.curve.sample(t))).collect()
141 }
142
143 pub fn blend_samples(a: &HashMap<String, f32>, b: &HashMap<String, f32>, alpha: f32) -> HashMap<String, f32> {
145 let mut out = a.clone();
146 for (k, vb) in b {
147 let va = out.entry(k.clone()).or_insert(0.0);
148 *va = *va * (1.0 - alpha) + vb * alpha;
149 }
150 out
151 }
152}
153
154#[derive(Debug, Clone)]
158pub struct RootMotionData {
159 pub frames: Vec<(f32, f32, f32)>,
161}
162
163impl RootMotionData {
164 pub fn accumulate(&self, t0: f32, t1: f32) -> (f32, f32, f32) {
166 if self.frames.is_empty() { return (0.0, 0.0, 0.0); }
167 let n = self.frames.len();
168 let i0 = ((t0 * n as f32) as usize).min(n - 1);
169 let i1 = ((t1 * n as f32) as usize).min(n - 1);
170 let (mut dx, mut dy, mut dr) = (0.0_f32, 0.0_f32, 0.0_f32);
171 for i in i0..i1 {
172 dx += self.frames[i].0;
173 dy += self.frames[i].1;
174 dr += self.frames[i].2;
175 }
176 (dx, dy, dr)
177 }
178}
179
180#[derive(Debug, Clone)]
184pub struct AnimEvent {
185 pub normalized_time: f32,
187 pub name: String,
189 pub value: f32,
191}
192
193#[derive(Debug, Clone)]
197pub enum Condition {
198 BoolTrue(String),
199 BoolFalse(String),
200 IntEquals(String, i32),
201 IntGreater(String, i32),
202 IntLess(String, i32),
203 FloatGreater(String, f32),
204 FloatLess(String, f32),
205 Trigger(String),
206}
207
208impl Condition {
209 pub fn check(&self, params: &AnimParamSet) -> bool {
210 match self {
211 Condition::BoolTrue(n) => params.get_bool(n),
212 Condition::BoolFalse(n) => !params.get_bool(n),
213 Condition::IntEquals(n, v) => params.get_int(n) == *v,
214 Condition::IntGreater(n, v) => params.get_int(n) > *v,
215 Condition::IntLess(n, v) => params.get_int(n) < *v,
216 Condition::FloatGreater(n,v) => params.get_float(n) > *v,
217 Condition::FloatLess(n, v) => params.get_float(n) < *v,
218 Condition::Trigger(n) => params.consume_trigger(n),
219 }
220 }
221}
222
223#[derive(Debug, Clone, Default)]
227pub struct AnimParamSet {
228 floats: HashMap<String, f32>,
229 ints: HashMap<String, i32>,
230 bools: HashMap<String, bool>,
231 triggers: std::collections::HashSet<String>,
232 consumed: Vec<String>,
234}
235
236impl AnimParamSet {
237 pub fn set_float(&mut self, name: &str, v: f32) { self.floats.insert(name.to_string(), v); }
238 pub fn set_int (&mut self, name: &str, v: i32) { self.ints.insert(name.to_string(), v); }
239 pub fn set_bool (&mut self, name: &str, v: bool) { self.bools.insert(name.to_string(), v); }
240 pub fn set_trigger(&mut self, name: &str) { self.triggers.insert(name.to_string()); }
241
242 pub fn get_float(&self, name: &str) -> f32 { *self.floats.get(name).unwrap_or(&0.0) }
243 pub fn get_int (&self, name: &str) -> i32 { *self.ints.get(name).unwrap_or(&0) }
244 pub fn get_bool (&self, name: &str) -> bool { *self.bools.get(name).unwrap_or(&false) }
245
246 pub fn consume_trigger(&self, name: &str) -> bool {
247 self.triggers.contains(name)
248 }
249
250 pub fn flush_triggers(&mut self) {
252 for name in self.consumed.drain(..) {
253 self.triggers.remove(&name);
254 }
255 }
256
257 pub fn mark_trigger_consumed(&mut self, name: &str) {
258 self.consumed.push(name.to_string());
259 }
260}
261
262#[derive(Debug, Clone)]
266pub struct AnimTransition {
267 pub from_state: String,
268 pub to_state: String,
269 pub blend_duration: f32,
271 pub conditions: Vec<Condition>,
273 pub exit_time: Option<f32>,
275 pub can_interrupt: bool,
277 pub priority: i32,
279}
280
281impl AnimTransition {
282 pub fn new(from: &str, to: &str, blend_secs: f32) -> Self {
283 Self {
284 from_state: from.to_string(),
285 to_state: to.to_string(),
286 blend_duration: blend_secs,
287 conditions: Vec::new(),
288 exit_time: None,
289 can_interrupt: false,
290 priority: 0,
291 }
292 }
293
294 pub fn with_condition(mut self, c: Condition) -> Self {
295 self.conditions.push(c);
296 self
297 }
298
299 pub fn with_exit_time(mut self, t: f32) -> Self {
300 self.exit_time = Some(t);
301 self
302 }
303
304 pub fn interruptible(mut self) -> Self {
305 self.can_interrupt = true;
306 self
307 }
308
309 pub fn is_ready(&self, params: &AnimParamSet, normalized_time: f32) -> bool {
310 if let Some(et) = self.exit_time {
312 if normalized_time < et { return false; }
313 }
314 self.conditions.iter().all(|c| c.check(params))
316 }
317}
318
319#[derive(Debug, Clone)]
323pub enum BlendTree {
324 Clip { clip_name: String, speed: f32 },
326
327 Linear1D {
329 param: String,
330 children: Vec<(f32, BlendTree)>, },
332
333 Directional2D {
335 param_x: String,
336 param_y: String,
337 children: Vec<([f32; 2], BlendTree)>, },
339
340 Additive {
342 base: Box<BlendTree>,
343 additive: Box<BlendTree>,
344 weight_param: Option<String>,
345 weight: f32,
346 },
347
348 Override {
350 base: Box<BlendTree>,
351 overlay: Box<BlendTree>,
352 mask: Vec<String>, weight: f32,
354 },
355}
356
357impl BlendTree {
358 pub fn evaluate(
360 &self,
361 clips: &HashMap<String, AnimClip>,
362 params: &AnimParamSet,
363 time: f32,
364 ) -> HashMap<String, f32> {
365 match self {
366 BlendTree::Clip { clip_name, speed } => {
367 if let Some(clip) = clips.get(clip_name) {
368 clip.sample(time * speed)
369 } else {
370 HashMap::new()
371 }
372 }
373
374 BlendTree::Linear1D { param, children } => {
375 if children.is_empty() { return HashMap::new(); }
376 let v = params.get_float(param);
377
378 let idx = children.partition_point(|(t, _)| *t <= v);
380
381 if idx == 0 {
382 return children[0].1.evaluate(clips, params, time);
383 }
384 if idx >= children.len() {
385 return children.last().unwrap().1.evaluate(clips, params, time);
386 }
387
388 let (t0, sub0) = &children[idx - 1];
389 let (t1, sub1) = &children[idx];
390 let alpha = if (t1 - t0).abs() > 1e-6 { (v - t0) / (t1 - t0) } else { 0.0 };
391
392 let a = sub0.evaluate(clips, params, time);
393 let b = sub1.evaluate(clips, params, time);
394 AnimClip::blend_samples(&a, &b, alpha.clamp(0.0, 1.0))
395 }
396
397 BlendTree::Directional2D { param_x, param_y, children } => {
398 if children.is_empty() { return HashMap::new(); }
399 let vx = params.get_float(param_x);
400 let vy = params.get_float(param_y);
401
402 let mut dists: Vec<(f32, usize)> = children.iter().enumerate().map(|(i, (pos, _))| {
404 let dx = pos[0] - vx;
405 let dy = pos[1] - vy;
406 (dx * dx + dy * dy, i)
407 }).collect();
408 dists.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
409
410 let (d0, i0) = dists[0];
411 let (d1, i1) = if dists.len() > 1 { dists[1] } else { dists[0] };
412
413 let total = d0 + d1;
414 let alpha = if total < 1e-6 { 0.0 } else { d0 / total };
415
416 let a = children[i0].1.evaluate(clips, params, time);
417 let b = children[i1].1.evaluate(clips, params, time);
418 AnimClip::blend_samples(&a, &b, alpha)
419 }
420
421 BlendTree::Additive { base, additive, weight_param, weight } => {
422 let base_pose = base.evaluate(clips, params, time);
423 let add_pose = additive.evaluate(clips, params, time);
424 let w = weight_param.as_ref().map(|p| params.get_float(p)).unwrap_or(*weight);
425 let mut out = base_pose;
427 for (k, v) in &add_pose {
428 let entry = out.entry(k.clone()).or_insert(0.0);
429 *entry += v * w;
430 }
431 out
432 }
433
434 BlendTree::Override { base, overlay, mask, weight } => {
435 let base_pose = base.evaluate(clips, params, time);
436 let overlay_pose = overlay.evaluate(clips, params, time);
437 let mut out = base_pose;
438 for (k, v) in &overlay_pose {
439 if mask.iter().any(|m| k.starts_with(m.as_str())) {
440 let entry = out.entry(k.clone()).or_insert(0.0);
441 *entry = *entry * (1.0 - weight) + v * weight;
442 }
443 }
444 out
445 }
446 }
447 }
448}
449
450#[derive(Debug, Clone)]
454pub struct AnimState {
455 pub name: String,
456 pub motion: StateMotion,
457 pub speed: f32,
459 pub speed_param: Option<String>,
460 pub events: Vec<AnimEvent>,
462 pub mirror: bool,
464 pub cycle_offset: f32,
466}
467
468#[derive(Debug, Clone)]
470pub enum StateMotion {
471 Clip(String),
472 BlendTree(BlendTree),
473 SubStateMachine(Box<AnimStateMachine>),
474 Empty,
475}
476
477impl AnimState {
478 pub fn clip(name: &str, clip_name: &str) -> Self {
479 Self {
480 name: name.to_string(),
481 motion: StateMotion::Clip(clip_name.to_string()),
482 speed: 1.0,
483 speed_param: None,
484 events: Vec::new(),
485 mirror: false,
486 cycle_offset: 0.0,
487 }
488 }
489
490 pub fn blend_tree(name: &str, tree: BlendTree) -> Self {
491 Self {
492 name: name.to_string(),
493 motion: StateMotion::BlendTree(tree),
494 speed: 1.0,
495 speed_param: None,
496 events: Vec::new(),
497 mirror: false,
498 cycle_offset: 0.0,
499 }
500 }
501
502 pub fn effective_speed(&self, params: &AnimParamSet) -> f32 {
503 self.speed_param.as_ref().map(|p| params.get_float(p)).unwrap_or(self.speed)
504 }
505}
506
507#[derive(Debug, Clone)]
511pub struct AnimLayer {
512 pub name: String,
513 pub weight: f32,
514 pub blend_mode: LayerBlend,
515 pub mask: Vec<String>,
517 pub machine: AnimStateMachine,
519}
520
521#[derive(Debug, Clone, Copy, PartialEq)]
522pub enum LayerBlend {
523 Override,
524 Additive,
525}
526
527impl AnimLayer {
528 pub fn new(name: &str, machine: AnimStateMachine) -> Self {
529 Self {
530 name: name.to_string(),
531 weight: 1.0,
532 blend_mode: LayerBlend::Override,
533 mask: Vec::new(),
534 machine,
535 }
536 }
537
538 pub fn additive(mut self) -> Self {
539 self.blend_mode = LayerBlend::Additive;
540 self
541 }
542
543 pub fn with_mask(mut self, paths: Vec<&str>) -> Self {
544 self.mask = paths.into_iter().map(|s| s.to_string()).collect();
545 self
546 }
547}
548
549#[derive(Debug, Clone)]
553struct ActiveTransition {
554 to_state: String,
555 elapsed: f32,
556 duration: f32,
557 destination_time: f32,
558}
559
560#[derive(Debug, Clone)]
564pub struct AnimStateMachine {
565 pub name: String,
566 pub states: HashMap<String, AnimState>,
567 pub transitions: Vec<AnimTransition>,
568 pub entry_state: Option<String>,
569 pub any_state_transitions: Vec<AnimTransition>,
570
571 pub current_state: Option<String>,
573 state_time: f32,
574 normalized_time: f32,
575 active_transition: Option<ActiveTransition>,
576 last_clip_duration: f32,
577 fired_events: Vec<AnimEvent>,
578}
579
580impl AnimStateMachine {
581 pub fn new(name: &str) -> Self {
582 Self {
583 name: name.to_string(),
584 states: HashMap::new(),
585 transitions: Vec::new(),
586 entry_state: None,
587 any_state_transitions: Vec::new(),
588 current_state: None,
589 state_time: 0.0,
590 normalized_time: 0.0,
591 active_transition: None,
592 last_clip_duration: 1.0,
593 fired_events: Vec::new(),
594 }
595 }
596
597 pub fn add_state(&mut self, state: AnimState) {
598 if self.entry_state.is_none() {
599 self.entry_state = Some(state.name.clone());
600 }
601 self.states.insert(state.name.clone(), state);
602 }
603
604 pub fn add_transition(&mut self, t: AnimTransition) {
605 self.transitions.push(t);
606 }
607
608 pub fn add_any_transition(&mut self, t: AnimTransition) {
609 self.any_state_transitions.push(t);
610 }
611
612 pub fn enter(&mut self) {
614 self.current_state = self.entry_state.clone();
615 self.state_time = 0.0;
616 self.normalized_time = 0.0;
617 self.active_transition = None;
618 }
619
620 pub fn update(
623 &mut self,
624 dt: f32,
625 params: &mut AnimParamSet,
626 clips: &HashMap<String, AnimClip>,
627 ) -> HashMap<String, f32> {
628 if self.current_state.is_none() { self.enter(); }
630
631 let cur_name = match &self.current_state {
632 Some(n) => n.clone(),
633 None => return HashMap::new(),
634 };
635
636 let cur_state = match self.states.get(&cur_name) {
637 Some(s) => s.clone(),
638 None => return HashMap::new(),
639 };
640
641 let speed = cur_state.effective_speed(params);
642 self.state_time += dt * speed;
643
644 let clip_dur = match &cur_state.motion {
646 StateMotion::Clip(c) => clips.get(c).map(|cl| cl.duration).unwrap_or(1.0),
647 _ => 1.0,
648 };
649 self.last_clip_duration = clip_dur;
650 self.normalized_time = (self.state_time / clip_dur.max(1e-6)).fract();
651
652 self.check_events(&cur_state, self.normalized_time);
654
655 if let Some(ref mut at) = self.active_transition {
657 at.elapsed += dt;
658 at.destination_time += dt;
659 if at.elapsed >= at.duration {
660 let to = at.to_state.clone();
662 let dest_t = at.destination_time;
663 self.active_transition = None;
664 self.current_state = Some(to.clone());
665 self.state_time = dest_t;
666 self.normalized_time = (dest_t / clip_dur.max(1e-6)).fract();
667 }
668 }
669
670 if self.active_transition.is_none() {
672 let triggered = self.find_transition(&cur_name, params, self.normalized_time);
673 if let Some(t) = triggered {
674 let to = t.to_state.clone();
675 let dur = t.blend_duration;
676 for cond in &t.conditions {
678 if let Condition::Trigger(n) = cond {
679 params.mark_trigger_consumed(n);
680 }
681 }
682 if dur < 1e-6 {
683 self.current_state = Some(to);
685 self.state_time = 0.0;
686 self.normalized_time = 0.0;
687 } else {
688 self.active_transition = Some(ActiveTransition {
689 to_state: to,
690 elapsed: 0.0,
691 duration: dur,
692 destination_time: 0.0,
693 });
694 }
695 }
696 }
697 params.flush_triggers();
698
699 let current_pose = self.sample_state(&cur_state, clips, params, self.state_time);
701
702 if let Some(ref at) = self.active_transition {
704 let alpha = (at.elapsed / at.duration.max(1e-6)).clamp(0.0, 1.0);
705 let alpha = smooth_step(alpha);
706 if let Some(dest_state) = self.states.get(&at.to_state).cloned() {
707 let dest_pose = self.sample_state(&dest_state, clips, params, at.destination_time);
708 return AnimClip::blend_samples(¤t_pose, &dest_pose, alpha);
709 }
710 }
711
712 current_pose
713 }
714
715 fn sample_state(
716 &self,
717 state: &AnimState,
718 clips: &HashMap<String, AnimClip>,
719 params: &AnimParamSet,
720 time: f32,
721 ) -> HashMap<String, f32> {
722 match &state.motion {
723 StateMotion::Clip(c) => {
724 if let Some(clip) = clips.get(c) {
725 clip.sample(time)
726 } else {
727 HashMap::new()
728 }
729 }
730 StateMotion::BlendTree(tree) => tree.evaluate(clips, params, time),
731 StateMotion::SubStateMachine(_) => HashMap::new(), StateMotion::Empty => HashMap::new(),
733 }
734 }
735
736 fn find_transition<'a>(
737 &'a self,
738 from: &str,
739 params: &AnimParamSet,
740 normalized_time: f32,
741 ) -> Option<&'a AnimTransition> {
742 let mut candidates: Vec<&AnimTransition> = self.any_state_transitions.iter()
744 .filter(|t| t.to_state != *from && t.is_ready(params, normalized_time))
745 .collect();
746
747 candidates.extend(self.transitions.iter()
749 .filter(|t| t.from_state == *from && t.is_ready(params, normalized_time)));
750
751 candidates.sort_by(|a, b| b.priority.cmp(&a.priority));
752 candidates.into_iter().next()
753 }
754
755 fn check_events(&mut self, state: &AnimState, normalized_time: f32) {
756 for ev in &state.events {
757 if (ev.normalized_time - normalized_time).abs() < 0.02 {
759 self.fired_events.push(ev.clone());
760 }
761 }
762 }
763
764 pub fn drain_events(&mut self) -> Vec<AnimEvent> {
766 std::mem::take(&mut self.fired_events)
767 }
768
769 pub fn current_state_name(&self) -> Option<&str> {
770 self.current_state.as_deref()
771 }
772
773 pub fn normalized_time(&self) -> f32 { self.normalized_time }
774 pub fn state_time(&self) -> f32 { self.state_time }
775 pub fn is_transitioning(&self) -> bool { self.active_transition.is_some() }
776}
777
778pub struct Animator {
783 pub layers: Vec<AnimLayer>,
784 pub clips: HashMap<String, AnimClip>,
785 pub params: AnimParamSet,
786 root_motion: (f32, f32, f32),
788 pub use_root_motion: bool,
790}
791
792impl Animator {
793 pub fn new() -> Self {
794 Self {
795 layers: Vec::new(),
796 clips: HashMap::new(),
797 params: AnimParamSet::default(),
798 root_motion: (0.0, 0.0, 0.0),
799 use_root_motion: false,
800 }
801 }
802
803 pub fn add_clip(&mut self, clip: AnimClip) {
804 self.clips.insert(clip.name.clone(), clip);
805 }
806
807 pub fn add_layer(&mut self, layer: AnimLayer) {
808 self.layers.push(layer);
809 }
810
811 pub fn set_float(&mut self, n: &str, v: f32) { self.params.set_float(n, v); }
812 pub fn set_int (&mut self, n: &str, v: i32) { self.params.set_int(n, v); }
813 pub fn set_bool (&mut self, n: &str, v: bool) { self.params.set_bool(n, v); }
814 pub fn set_trigger(&mut self, n: &str) { self.params.set_trigger(n); }
815
816 pub fn update(&mut self, dt: f32) -> HashMap<String, f32> {
818 let mut final_pose: HashMap<String, f32> = HashMap::new();
819
820 for layer in &mut self.layers {
821 let pose = layer.machine.update(dt, &mut self.params, &self.clips);
822 let weight = layer.weight;
823
824 let masked_pose: HashMap<String, f32> = if layer.mask.is_empty() {
826 pose
827 } else {
828 pose.into_iter()
829 .filter(|(k, _)| layer.mask.iter().any(|m| k.starts_with(m.as_str())))
830 .collect()
831 };
832
833 match layer.blend_mode {
834 LayerBlend::Override => {
835 for (k, v) in masked_pose {
836 let entry = final_pose.entry(k).or_insert(0.0);
837 *entry = *entry * (1.0 - weight) + v * weight;
838 }
839 }
840 LayerBlend::Additive => {
841 for (k, v) in masked_pose {
842 let entry = final_pose.entry(k).or_insert(0.0);
843 *entry += v * weight;
844 }
845 }
846 }
847 }
848
849 final_pose
850 }
851
852 pub fn consume_root_motion(&mut self) -> (f32, f32, f32) {
854 std::mem::take(&mut self.root_motion)
855 }
856
857 pub fn drain_events(&mut self) -> Vec<AnimEvent> {
859 self.layers.iter_mut().flat_map(|l| l.machine.drain_events()).collect()
860 }
861}
862
863impl Default for Animator {
864 fn default() -> Self { Self::new() }
865}
866
867pub struct AnimatorBuilder {
871 animator: Animator,
872}
873
874impl AnimatorBuilder {
875 pub fn new() -> Self {
876 Self { animator: Animator::new() }
877 }
878
879 pub fn clip(mut self, clip: AnimClip) -> Self {
880 self.animator.add_clip(clip);
881 self
882 }
883
884 pub fn layer(mut self, layer: AnimLayer) -> Self {
885 self.animator.add_layer(layer);
886 self
887 }
888
889 pub fn root_motion(mut self) -> Self {
890 self.animator.use_root_motion = true;
891 self
892 }
893
894 pub fn build(self) -> Animator {
895 self.animator
896 }
897}
898
899pub struct AnimPresets;
903
904impl AnimPresets {
905 pub fn humanoid_locomotion() -> AnimStateMachine {
907 let mut sm = AnimStateMachine::new("locomotion");
908
909 sm.add_state(AnimState::clip("idle", "humanoid_idle"));
910 sm.add_state(AnimState::clip("walk", "humanoid_walk"));
911 sm.add_state(AnimState::blend_tree("locomotion_blend",
912 BlendTree::Linear1D {
913 param: "speed".to_string(),
914 children: vec![
915 (0.0, BlendTree::Clip { clip_name: "humanoid_idle".to_string(), speed: 1.0 }),
916 (0.5, BlendTree::Clip { clip_name: "humanoid_walk".to_string(), speed: 1.0 }),
917 (1.0, BlendTree::Clip { clip_name: "humanoid_run".to_string(), speed: 1.0 }),
918 ],
919 }
920 ));
921 sm.add_state(AnimState::clip("jump_rise", "humanoid_jump_rise"));
922 sm.add_state(AnimState::clip("jump_fall", "humanoid_jump_fall"));
923 sm.add_state(AnimState::clip("land", "humanoid_land"));
924
925 sm.add_transition(AnimTransition::new("locomotion_blend", "jump_rise", 0.1)
926 .with_condition(Condition::Trigger("jump".to_string())));
927 sm.add_transition(AnimTransition::new("jump_rise", "jump_fall", 0.15)
928 .with_condition(Condition::FloatLess("velocity_y".to_string(), 0.0)));
929 sm.add_transition(AnimTransition::new("jump_fall", "land", 0.05)
930 .with_condition(Condition::BoolTrue("grounded".to_string())));
931 sm.add_transition(AnimTransition::new("land", "locomotion_blend", 0.2)
932 .with_exit_time(0.7));
933
934 sm.entry_state = Some("locomotion_blend".to_string());
935 sm
936 }
937
938 pub fn combat_humanoid() -> AnimStateMachine {
940 let mut sm = AnimStateMachine::new("combat");
941
942 sm.add_state(AnimState::clip("idle_combat", "combat_idle"));
943 sm.add_state(AnimState::clip("attack_light", "combat_attack_light"));
944 sm.add_state(AnimState::clip("attack_heavy", "combat_attack_heavy"));
945 sm.add_state(AnimState::clip("attack_combo2", "combat_attack_combo2"));
946 sm.add_state(AnimState::clip("dodge", "combat_dodge"));
947 sm.add_state(AnimState::clip("block", "combat_block"));
948 sm.add_state(AnimState::clip("hurt", "combat_hurt"));
949 sm.add_state(AnimState::clip("death", "combat_death"));
950
951 sm.add_transition(AnimTransition::new("idle_combat", "attack_light", 0.1)
953 .with_condition(Condition::Trigger("attack_light".to_string())));
954 sm.add_transition(AnimTransition::new("attack_light", "attack_combo2", 0.1)
955 .with_condition(Condition::Trigger("attack_light".to_string()))
956 .with_exit_time(0.4));
957 sm.add_transition(AnimTransition::new("attack_light", "idle_combat", 0.2)
958 .with_exit_time(0.9));
959 sm.add_transition(AnimTransition::new("attack_combo2", "idle_combat", 0.2)
960 .with_exit_time(0.9));
961
962 sm.add_transition(AnimTransition::new("idle_combat", "attack_heavy", 0.1)
964 .with_condition(Condition::Trigger("attack_heavy".to_string())));
965 sm.add_transition(AnimTransition::new("attack_heavy", "idle_combat", 0.2)
966 .with_exit_time(0.9));
967
968 sm.add_transition(AnimTransition::new("idle_combat", "dodge", 0.05)
970 .with_condition(Condition::Trigger("dodge".to_string())));
971 sm.add_transition(AnimTransition::new("dodge", "idle_combat", 0.1)
972 .with_exit_time(0.85));
973
974 sm.add_transition(AnimTransition::new("idle_combat", "block", 0.1)
976 .with_condition(Condition::BoolTrue("blocking".to_string())));
977 sm.add_transition(AnimTransition::new("block", "idle_combat", 0.15)
978 .with_condition(Condition::BoolFalse("blocking".to_string())));
979
980 sm.add_any_transition(AnimTransition::new("", "hurt", 0.05)
982 .with_condition(Condition::Trigger("hurt".to_string())));
983 sm.add_transition(AnimTransition::new("hurt", "idle_combat", 0.15)
984 .with_exit_time(0.8));
985
986 let mut death_t = AnimTransition::new("", "death", 0.05);
988 death_t.conditions.push(Condition::Trigger("death".to_string()));
989 death_t.priority = 100;
990 sm.add_any_transition(death_t);
991
992 sm.entry_state = Some("idle_combat".to_string());
993 sm
994 }
995
996 pub fn flying_creature() -> AnimStateMachine {
998 let mut sm = AnimStateMachine::new("flying");
999
1000 sm.add_state(AnimState::clip("hover", "fly_hover"));
1001 sm.add_state(AnimState::clip("flap", "fly_flap"));
1002 sm.add_state(AnimState::clip("glide", "fly_glide"));
1003 sm.add_state(AnimState::clip("dive", "fly_dive"));
1004 sm.add_state(AnimState::clip("land", "fly_land"));
1005
1006 sm.add_transition(AnimTransition::new("hover", "flap", 0.2)
1007 .with_condition(Condition::FloatGreater("speed".to_string(), 0.3)));
1008 sm.add_transition(AnimTransition::new("flap", "glide", 0.3)
1009 .with_condition(Condition::FloatGreater("speed".to_string(), 0.8)));
1010 sm.add_transition(AnimTransition::new("glide", "flap", 0.2)
1011 .with_condition(Condition::FloatLess("speed".to_string(), 0.6)));
1012 sm.add_transition(AnimTransition::new("glide", "dive", 0.15)
1013 .with_condition(Condition::FloatLess("velocity_y".to_string(), -0.5)));
1014 sm.add_transition(AnimTransition::new("dive", "glide", 0.3)
1015 .with_condition(Condition::FloatGreater("velocity_y".to_string(), 0.0)));
1016 sm.add_any_transition(AnimTransition::new("", "land", 0.2)
1017 .with_condition(Condition::Trigger("land".to_string())));
1018 sm.add_transition(AnimTransition::new("land", "hover", 0.3)
1019 .with_exit_time(0.9));
1020
1021 sm.entry_state = Some("hover".to_string());
1022 sm
1023 }
1024}
1025
1026fn smooth_step(t: f32) -> f32 {
1029 let t = t.clamp(0.0, 1.0);
1030 t * t * (3.0 - 2.0 * t)
1031}
1032
1033#[cfg(test)]
1036mod tests {
1037 use super::*;
1038
1039 fn make_clip(name: &str, duration: f32) -> AnimClip {
1040 let mut clip = AnimClip::new(name, duration);
1041 clip.add_channel("pos_x", AnimCurve::linear(0.0, 0.0, duration, 1.0));
1042 clip
1043 }
1044
1045 #[test]
1046 fn test_anim_curve_sample() {
1047 let curve = AnimCurve::linear(0.0, 0.0, 1.0, 1.0);
1048 assert!((curve.sample(0.5) - 0.5).abs() < 0.01);
1049 assert!((curve.sample(0.0) - 0.0).abs() < 0.01);
1050 assert!((curve.sample(1.0) - 1.0).abs() < 0.01);
1051 }
1052
1053 #[test]
1054 fn test_anim_curve_clamp() {
1055 let curve = AnimCurve::linear(0.0, 5.0, 1.0, 10.0);
1056 assert!((curve.sample(-1.0) - 5.0).abs() < 0.01);
1057 assert!((curve.sample(2.0) - 10.0).abs() < 0.01);
1058 }
1059
1060 #[test]
1061 fn test_anim_clip_sample() {
1062 let clip = make_clip("test", 2.0);
1063 let pose = clip.sample(1.0);
1064 assert!(pose.contains_key("pos_x"));
1065 let v = pose["pos_x"];
1066 assert!(v > 0.4 && v < 0.6, "pos_x at t=1 of 2s clip should be ~0.5, got {}", v);
1067 }
1068
1069 #[test]
1070 fn test_blend_samples() {
1071 let mut a = HashMap::new(); a.insert("x".to_string(), 0.0_f32);
1072 let mut b = HashMap::new(); b.insert("x".to_string(), 1.0_f32);
1073 let blended = AnimClip::blend_samples(&a, &b, 0.5);
1074 assert!((blended["x"] - 0.5).abs() < 0.001);
1075 }
1076
1077 #[test]
1078 fn test_state_machine_transitions() {
1079 let mut sm = AnimStateMachine::new("test");
1080 sm.add_state(AnimState::clip("idle", "idle_clip"));
1081 sm.add_state(AnimState::clip("run", "run_clip"));
1082 sm.add_transition(AnimTransition::new("idle", "run", 0.1)
1083 .with_condition(Condition::Trigger("run".to_string())));
1084
1085 let mut clips = HashMap::new();
1086 clips.insert("idle_clip".to_string(), make_clip("idle_clip", 1.0));
1087 clips.insert("run_clip".to_string(), make_clip("run_clip", 1.0));
1088
1089 let mut params = AnimParamSet::default();
1090 sm.enter();
1091 sm.update(0.016, &mut params, &clips);
1092 assert_eq!(sm.current_state_name(), Some("idle"));
1093
1094 params.set_trigger("run");
1095 sm.update(0.016, &mut params, &clips);
1096 sm.update(0.15, &mut params, &clips);
1098 assert_eq!(sm.current_state_name(), Some("run"));
1099 }
1100
1101 #[test]
1102 fn test_blend_tree_linear() {
1103 let mut clips = HashMap::new();
1104 clips.insert("idle".to_string(), make_clip("idle", 1.0));
1105 clips.insert("walk".to_string(), make_clip("walk", 1.0));
1106 clips.insert("run".to_string(), make_clip("run", 1.0));
1107
1108 let tree = BlendTree::Linear1D {
1109 param: "speed".to_string(),
1110 children: vec![
1111 (0.0, BlendTree::Clip { clip_name: "idle".to_string(), speed: 1.0 }),
1112 (1.0, BlendTree::Clip { clip_name: "run".to_string(), speed: 1.0 }),
1113 ],
1114 };
1115
1116 let mut params = AnimParamSet::default();
1117 params.set_float("speed", 0.5);
1118 let pose = tree.evaluate(&clips, ¶ms, 0.5);
1119 let v = pose.get("pos_x").copied().unwrap_or(0.0);
1121 assert!(v > 0.0, "blend tree should produce non-zero values");
1122 }
1123
1124 #[test]
1125 fn test_animator_layers() {
1126 let mut animator = Animator::new();
1127 animator.add_clip(make_clip("idle_clip", 1.0));
1128
1129 let mut sm = AnimStateMachine::new("base");
1130 sm.add_state(AnimState::clip("idle", "idle_clip"));
1131
1132 animator.add_layer(AnimLayer::new("base", sm));
1133 let pose = animator.update(0.016);
1134 assert!(!pose.is_empty() || pose.is_empty(), "should not panic");
1135 }
1136
1137 #[test]
1138 fn test_anim_presets_locomotion() {
1139 let sm = AnimPresets::humanoid_locomotion();
1140 assert!(sm.states.contains_key("locomotion_blend"));
1141 assert!(sm.states.contains_key("jump_rise"));
1142 assert!(sm.transitions.len() >= 4);
1143 }
1144
1145 #[test]
1146 fn test_anim_presets_combat() {
1147 let sm = AnimPresets::combat_humanoid();
1148 assert!(sm.states.contains_key("attack_light"));
1149 assert!(sm.states.contains_key("death"));
1150 assert!(!sm.any_state_transitions.is_empty());
1151 }
1152
1153 #[test]
1154 fn test_smooth_step() {
1155 assert!((smooth_step(0.0) - 0.0).abs() < 1e-6);
1156 assert!((smooth_step(1.0) - 1.0).abs() < 1e-6);
1157 assert!((smooth_step(0.5) - 0.5).abs() < 1e-6);
1158 }
1159}