1use renamite_animation::{Frame, LoopMode, Tween, ease_progress};
13use renamite_geometry::VectorPath;
14use renamite_model::{KeyframeData, NodeId, Overrides, PropPath, Value};
15use serde::{Deserialize, Serialize};
16use slotmap::SlotMap;
17use std::collections::HashMap;
18
19slotmap::new_key_type! { pub struct ClipId; pub struct MachineId; }
20pub type ClipMap = SlotMap<ClipId, Clip>;
21pub type MachineMap = SlotMap<MachineId, Machine>;
22
23#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
24pub struct Clip {
25 pub name: String,
26 pub range: (Frame, Frame),
27 pub tracks: Vec<Track>,
28 pub events: Vec<EventKey>,
30}
31
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33pub struct Track {
34 pub node: NodeId,
35 pub prop: PropPath,
36 pub keys: Vec<KeyframeData>,
38}
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct EventKey {
42 pub frame: Frame,
43 pub name: String,
44}
45
46pub fn value_tween(a: &Value, b: &Value, t: f64) -> Value {
48 use Value::*;
49 match (a, b) {
50 (F64(x), F64(y)) => F64(f64::tween(x, y, t)),
51 (DVec2(x), DVec2(y)) => DVec2(glam::DVec2::tween(x, y, t)),
52 (Angle(x), Angle(y)) => Angle(renamite_animation::Angle::tween(x, y, t)),
53 (Color(x), Color(y)) => Color(renamite_model::Color::tween(x, y, t)),
54 (Path(x), Path(y)) => Path(VectorPath::tween(x, y, t)),
55 _ => {
56 if t < 1.0 {
57 a.clone()
58 } else {
59 b.clone()
60 }
61 }
62 }
63}
64
65impl Track {
66 pub fn value_at(&self, frame: f64) -> Option<Value> {
67 let ks = &self.keys;
68 if ks.is_empty() {
69 return None;
70 }
71 if frame <= ks[0].frame.0 as f64 {
72 return Some(ks[0].value.clone());
73 }
74 let last = ks.len() - 1;
75 if frame >= ks[last].frame.0 as f64 {
76 return Some(ks[last].value.clone());
77 }
78 let i = ks.partition_point(|k| (k.frame.0 as f64) <= frame) - 1;
79 let (a, b) = (&ks[i], &ks[i + 1]);
80 let u = (frame - a.frame.0 as f64) / (b.frame.0 - a.frame.0) as f64;
81 let y = ease_progress(a.interpolation, a.ease_out, a.ease_in, u);
82 Some(value_tween(&a.value, &b.value, y))
83 }
84}
85
86impl Clip {
87 pub fn len_frames(&self) -> f64 {
88 (self.range.1.0 - self.range.0.0).max(1) as f64
89 }
90
91 pub fn local(&self, time: f64, loop_mode: LoopMode) -> (f64, f64) {
93 let (s, len) = (self.range.0.0 as f64, self.len_frames());
94 let t = match loop_mode {
95 LoopMode::Once => time.clamp(0.0, len),
96 LoopMode::Loop => time.rem_euclid(len),
97 LoopMode::PingPong => {
98 let c = time.rem_euclid(2.0 * len);
99 if c > len { 2.0 * len - c } else { c }
100 }
101 };
102 (s + t, t / len)
103 }
104
105 pub fn sample_into(&self, frame: f64, out: &mut HashMap<(NodeId, PropPath), Value>) {
106 for tr in &self.tracks {
107 if let Some(v) = tr.value_at(frame) {
108 out.insert((tr.node, tr.prop.clone()), v);
109 }
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
115pub struct Machine {
116 pub name: String,
117 pub inputs: Vec<InputDef>,
118 pub layers: Vec<MachineLayer>,
119 pub listeners: Vec<Listener>,
122}
123
124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
125pub struct InputDef {
126 pub name: String,
127 pub kind: InputKind,
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
131pub enum InputKind {
132 Bool { default: bool },
133 Number { default: f64 },
134 Trigger,
135}
136
137#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
138pub struct MachineLayer {
139 pub name: String,
140 pub states: Vec<State>,
141 pub entry: usize,
142 pub any_transitions: Vec<Transition>,
144}
145
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147pub struct State {
148 pub name: String,
149 pub kind: StateKind,
150 pub transitions: Vec<Transition>,
151 #[serde(default)]
152 pub graph_pos: Option<(f64, f64)>,
153}
154
155#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
156pub enum StateKind {
157 Clip {
159 clip: ClipId,
160 speed: f64,
161 loop_mode: LoopMode,
162 },
163 Blend1D {
165 input: usize,
166 children: Vec<BlendChild>,
167 },
168 Empty,
170}
171
172#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
173pub struct BlendChild {
174 pub threshold: f64,
175 pub clip: ClipId,
176}
177
178#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
179pub struct Transition {
180 pub to: usize,
181 pub duration: f64,
183 pub exit_time: Option<f64>,
185 pub conditions: Vec<Condition>,
187}
188
189#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
190pub enum Condition {
191 BoolIs { input: usize, value: bool },
192 NumberCmp { input: usize, op: CmpOp, value: f64 },
193 Triggered { input: usize },
194}
195
196#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
197pub enum CmpOp {
198 Eq,
199 Ne,
200 Lt,
201 Le,
202 Gt,
203 Ge,
204}
205
206#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
207pub struct Listener {
208 pub node: NodeId,
209 pub event: PointerEventKind,
210 pub action: ListenerAction,
211}
212
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
214pub enum PointerEventKind {
215 Down,
216 Up,
217 Click,
218 Enter,
219 Exit,
220}
221
222#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
223pub enum ListenerAction {
224 SetBool { input: usize, value: bool },
225 ToggleBool { input: usize },
226 SetNumber { input: usize, value: f64 },
227 FireTrigger { input: usize },
228}
229
230#[derive(Clone, Copy, Debug, PartialEq)]
231pub enum InputValue {
232 Bool(bool),
233 Number(f64),
234 Trigger { fired: bool },
235}
236
237#[derive(Clone, Debug)]
238struct LayerRt {
239 current: usize,
240 time: f64,
242 fade: Option<Fade>,
243}
244
245#[derive(Clone, Debug)]
246struct Fade {
247 from: usize,
248 from_time: f64,
249 t: f64,
250 duration: f64,
251}
252
253#[derive(Clone, Debug)]
254pub struct MachineInstance {
255 pub inputs: Vec<InputValue>,
256 layers: Vec<LayerRt>,
257}
258
259#[derive(Clone, Debug, Default)]
260pub struct TickOutput {
261 pub events: Vec<String>,
262}
263
264#[derive(Clone, Debug, thiserror::Error)]
265pub enum MachineError {
266 #[error("unknown input `{0}`")]
267 UnknownInput(String),
268 #[error("input type mismatch for `{0}`")]
269 InputType(String),
270}
271
272impl MachineInstance {
273 pub fn new(m: &Machine) -> Self {
274 Self {
275 inputs: m
276 .inputs
277 .iter()
278 .map(|i| match i.kind {
279 InputKind::Bool { default } => InputValue::Bool(default),
280 InputKind::Number { default } => InputValue::Number(default),
281 InputKind::Trigger => InputValue::Trigger { fired: false },
282 })
283 .collect(),
284 layers: m
285 .layers
286 .iter()
287 .map(|l| LayerRt {
288 current: l.entry.min(l.states.len().saturating_sub(1)),
289 time: 0.0,
290 fade: None,
291 })
292 .collect(),
293 }
294 }
295
296 pub fn input_index(m: &Machine, name: &str) -> Option<usize> {
297 m.inputs.iter().position(|i| i.name == name)
298 }
299 pub fn layer_states(&self) -> impl Iterator<Item = usize> + '_ {
301 self.layers.iter().map(|layer| layer.current)
302 }
303 pub fn set_bool(&mut self, idx: usize, v: bool) {
304 if let Some(InputValue::Bool(b)) = self.inputs.get_mut(idx) {
305 *b = v;
306 }
307 }
308 pub fn set_number(&mut self, idx: usize, v: f64) {
309 if let Some(InputValue::Number(n)) = self.inputs.get_mut(idx) {
310 *n = v;
311 }
312 }
313 pub fn fire(&mut self, idx: usize) {
314 if let Some(InputValue::Trigger { fired }) = self.inputs.get_mut(idx) {
315 *fired = true;
316 }
317 }
318
319 pub fn pointer_event(&mut self, m: &Machine, node: NodeId, kind: PointerEventKind) {
321 for l in m
322 .listeners
323 .iter()
324 .filter(|l| l.node == node && l.event == kind)
325 {
326 match l.action {
327 ListenerAction::SetBool { input, value } => self.set_bool(input, value),
328 ListenerAction::ToggleBool { input } => {
329 if let Some(InputValue::Bool(b)) = self.inputs.get_mut(input) {
330 *b = !*b;
331 }
332 }
333 ListenerAction::SetNumber { input, value } => self.set_number(input, value),
334 ListenerAction::FireTrigger { input } => self.fire(input),
335 }
336 }
337 }
338
339 pub fn tick(
342 &mut self,
343 m: &Machine,
344 clips: &ClipMap,
345 dt_frames: f64,
346 out: &mut Overrides,
347 ) -> TickOutput {
348 let mut output = TickOutput::default();
349 for (li, layer) in m.layers.iter().enumerate() {
350 let rt = &mut self.layers[li];
351 let prev_time = rt.time;
352 rt.time += dt_frames;
353 if let Some(f) = &mut rt.fade {
354 f.from_time += dt_frames;
355 f.t = if f.duration <= 0.0 {
356 1.0
357 } else {
358 (f.t + dt_frames / f.duration).min(1.0)
359 };
360 if f.t >= 1.0 {
361 rt.fade = None;
362 }
363 }
364
365 let state = &layer.states[rt.current];
367 let norm = normalized_time(state, clips, rt.time);
368 let fired = layer
369 .any_transitions
370 .iter()
371 .chain(state.transitions.iter())
372 .find(|tr| transition_ready(tr, &self.inputs, norm));
373 if let Some(tr) = fired.cloned() {
374 consume_triggers(&tr, &mut self.inputs);
375 rt.fade = (tr.duration > 0.0).then_some(Fade {
376 from: rt.current,
377 from_time: rt.time,
378 t: 0.0,
379 duration: tr.duration,
380 });
381 rt.current = tr.to.min(layer.states.len() - 1);
382 rt.time = 0.0;
383 }
384
385 let mut b = HashMap::new();
387 let mut evs = Vec::new();
388 sample_state(
389 &layer.states[rt.current],
390 clips,
391 &self.inputs,
392 prev_if_same(prev_time, rt.time),
393 rt.time,
394 &mut b,
395 &mut evs,
396 );
397 if let Some(f) = &rt.fade {
398 let mut a = HashMap::new();
399 sample_state(
400 &layer.states[f.from],
401 clips,
402 &self.inputs,
403 f.from_time,
404 f.from_time,
405 &mut a,
406 &mut Vec::new(),
407 );
408 for (k, va) in a {
409 let merged = match b.get(&k) {
410 Some(vb) => value_tween(&va, vb, f.t),
411 None => va,
412 };
413 b.insert(k, merged);
414 }
415 }
416 for (k, v) in b {
417 out.set(k.0, k.1, v);
418 }
419 output.events.append(&mut evs);
420 }
421 for i in &mut self.inputs {
423 if let InputValue::Trigger { fired } = i {
424 *fired = false;
425 }
426 }
427 output
428 }
429}
430
431fn prev_if_same(prev: f64, cur: f64) -> f64 {
432 if cur < prev { 0.0 } else { prev }
433}
434
435fn normalized_time(state: &State, clips: &ClipMap, time: f64) -> f64 {
436 match &state.kind {
437 StateKind::Clip {
438 clip,
439 speed,
440 loop_mode,
441 } => clips
442 .get(*clip)
443 .map(|c| c.local(time * speed.max(0.0), *loop_mode).1)
444 .unwrap_or(1.0),
445 StateKind::Blend1D { children, .. } => {
446 if children.is_empty() {
447 return 1.0;
448 }
449 children
450 .first()
451 .and_then(|ch| clips.get(ch.clip))
452 .map(|c| c.local(time, LoopMode::Loop).1)
453 .unwrap_or(1.0)
454 }
455 StateKind::Empty => 1.0,
456 }
457}
458
459fn transition_ready(tr: &Transition, inputs: &[InputValue], norm: f64) -> bool {
460 if let Some(et) = tr.exit_time
461 && norm < et
462 {
463 return false;
464 }
465 if tr.conditions.is_empty() && tr.exit_time.is_none() {
466 return false;
467 }
468 tr.conditions.iter().all(|c| match *c {
469 Condition::BoolIs { input, value } => {
470 matches!(inputs.get(input), Some(InputValue::Bool(b)) if *b == value)
471 }
472 Condition::NumberCmp { input, op, value } => match inputs.get(input) {
473 Some(InputValue::Number(n)) => match op {
474 CmpOp::Eq => (n - value).abs() < 1e-9,
475 CmpOp::Ne => (n - value).abs() >= 1e-9,
476 CmpOp::Lt => *n < value,
477 CmpOp::Le => *n <= value,
478 CmpOp::Gt => *n > value,
479 CmpOp::Ge => *n >= value,
480 },
481 _ => false,
482 },
483 Condition::Triggered { input } => {
484 matches!(inputs.get(input), Some(InputValue::Trigger { fired: true }))
485 }
486 })
487}
488
489fn consume_triggers(tr: &Transition, inputs: &mut [InputValue]) {
490 for c in &tr.conditions {
491 if let Condition::Triggered { input } = c
492 && let Some(InputValue::Trigger { fired }) = inputs.get_mut(*input)
493 {
494 *fired = false;
495 }
496 }
497}
498
499fn sample_state(
500 state: &State,
501 clips: &ClipMap,
502 inputs: &[InputValue],
503 prev_time: f64,
504 time: f64,
505 out: &mut HashMap<(NodeId, PropPath), Value>,
506 events: &mut Vec<String>,
507) {
508 match &state.kind {
509 StateKind::Empty => {}
510 StateKind::Clip {
511 clip,
512 speed,
513 loop_mode,
514 } => {
515 let Some(c) = clips.get(*clip) else { return };
516 let (frame, _) = c.local(time * speed.max(0.0), *loop_mode);
517 let (pframe, _) = c.local(prev_time * speed.max(0.0), *loop_mode);
518 c.sample_into(frame, out);
519 emit_events(c, pframe, frame, *loop_mode, events);
520 }
521 StateKind::Blend1D { input, children } => {
522 if children.is_empty() {
523 return;
524 }
525 let x = match inputs.get(*input) {
526 Some(InputValue::Number(n)) => *n,
527 _ => 0.0,
528 };
529 let (lo, hi, t) = bracket(children, x);
530 let mut a = HashMap::new();
531 if let Some(c) = clips.get(children[lo].clip) {
532 c.sample_into(c.local(time, LoopMode::Loop).0, &mut a);
533 }
534 if hi != lo {
535 let mut b = HashMap::new();
536 if let Some(c) = clips.get(children[hi].clip) {
537 c.sample_into(c.local(time, LoopMode::Loop).0, &mut b);
538 }
539 for (k, va) in a {
540 let v = match b.remove(&k) {
541 Some(vb) => value_tween(&va, &vb, t),
542 None => va,
543 };
544 out.insert(k, v);
545 }
546 out.extend(b);
547 } else {
548 out.extend(a);
549 }
550 }
551 }
552}
553
554fn bracket(children: &[BlendChild], x: f64) -> (usize, usize, f64) {
556 if children.is_empty() {
557 return (0, 0, 0.0);
558 }
559 if x <= children[0].threshold {
560 return (0, 0, 0.0);
561 }
562 let last = children.len() - 1;
563 if x >= children[last].threshold {
564 return (last, last, 0.0);
565 }
566 let hi = children.partition_point(|c| c.threshold <= x);
567 let (lo, hi) = (hi - 1, hi);
568 let span = children[hi].threshold - children[lo].threshold;
569 (
570 lo,
571 hi,
572 if span <= 0.0 {
573 0.0
574 } else {
575 (x - children[lo].threshold) / span
576 },
577 )
578}
579
580fn emit_events(c: &Clip, prev: f64, cur: f64, loop_mode: LoopMode, out: &mut Vec<String>) {
581 let hit = |a: f64, b: f64, out: &mut Vec<String>| {
582 for e in &c.events {
583 let f = e.frame.0 as f64;
584 if f > a && f <= b {
585 out.push(e.name.clone());
586 }
587 }
588 };
589 if cur >= prev {
590 hit(prev, cur, out);
591 } else if loop_mode == LoopMode::Loop {
592 hit(prev, c.range.1.0 as f64, out);
593 hit(c.range.0.0 as f64 - 1.0, cur, out);
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use renamite_animation::{EasingHandle, Interpolation};
601
602 fn key(f: i64, v: f64) -> KeyframeData {
603 KeyframeData {
604 frame: Frame(f),
605 value: Value::F64(v),
606 interpolation: Interpolation::Linear,
607 ease_out: EasingHandle::LINEAR_OUT,
608 ease_in: EasingHandle::LINEAR_IN,
609 }
610 }
611
612 fn world() -> (ClipMap, Machine, NodeId) {
613 let node = {
614 let mut doc = renamite_model::Document::empty();
615 doc.create_node(renamite_model::Node::new(
616 "n",
617 renamite_model::NodeKind::Group,
618 ))
619 };
620 let mut clips = ClipMap::default();
621 let up = clips.insert(Clip {
622 name: "up".into(),
623 range: (Frame(0), Frame(60)),
624 tracks: vec![Track {
625 node,
626 prop: PropPath::new("opacity"),
627 keys: vec![key(0, 0.0), key(60, 1.0)],
628 }],
629 events: vec![EventKey {
630 frame: Frame(30),
631 name: "half".into(),
632 }],
633 });
634 let down = clips.insert(Clip {
635 name: "down".into(),
636 range: (Frame(0), Frame(60)),
637 tracks: vec![Track {
638 node,
639 prop: PropPath::new("opacity"),
640 keys: vec![key(0, 1.0), key(60, 0.0)],
641 }],
642 events: vec![],
643 });
644 let m = Machine {
645 name: "hover".into(),
646 inputs: vec![InputDef {
647 name: "over".into(),
648 kind: InputKind::Bool { default: false },
649 }],
650 layers: vec![MachineLayer {
651 name: "base".into(),
652 entry: 0,
653 any_transitions: vec![],
654 states: vec![
655 State {
656 name: "Down".into(),
657 kind: StateKind::Clip {
658 clip: down,
659 speed: 1.0,
660 loop_mode: LoopMode::Once,
661 },
662 transitions: vec![Transition {
663 to: 1,
664 duration: 10.0,
665 exit_time: None,
666 conditions: vec![Condition::BoolIs {
667 input: 0,
668 value: true,
669 }],
670 }],
671 graph_pos: None,
672 },
673 State {
674 name: "Up".into(),
675 kind: StateKind::Clip {
676 clip: up,
677 speed: 1.0,
678 loop_mode: LoopMode::Once,
679 },
680 transitions: vec![Transition {
681 to: 0,
682 duration: 10.0,
683 exit_time: None,
684 conditions: vec![Condition::BoolIs {
685 input: 0,
686 value: false,
687 }],
688 }],
689 graph_pos: None,
690 },
691 ],
692 }],
693 listeners: vec![Listener {
694 node,
695 event: PointerEventKind::Enter,
696 action: ListenerAction::SetBool {
697 input: 0,
698 value: true,
699 },
700 }],
701 };
702 (clips, m, node)
703 }
704
705 #[test]
706 fn track_lerps() {
707 let (clips, _, node) = world();
708 let c = clips.values().find(|c| c.name == "up").unwrap();
709 let mut out = HashMap::new();
710 c.sample_into(30.0, &mut out);
711 assert_eq!(out[&(node, PropPath::new("opacity"))], Value::F64(0.5));
712 }
713
714 #[test]
715 fn bool_input_transitions_and_listener_sets_it() {
716 let (clips, m, node) = world();
717 let mut inst = MachineInstance::new(&m);
718 let mut ov = Overrides::default();
719 inst.tick(&m, &clips, 1.0, &mut ov);
720 assert_eq!(inst.layers[0].current, 0);
721 inst.pointer_event(&m, node, PointerEventKind::Enter);
722 inst.tick(&m, &clips, 1.0, &mut ov);
723 assert_eq!(inst.layers[0].current, 1);
724 assert!(inst.layers[0].fade.is_some());
725 }
726
727 #[test]
728 fn trigger_consumed_once() {
729 let (clips, mut m, _) = world();
730 m.inputs.push(InputDef {
731 name: "tap".into(),
732 kind: InputKind::Trigger,
733 });
734 m.layers[0].states[0].transitions[0].conditions = vec![Condition::Triggered { input: 1 }];
735 let mut inst = MachineInstance::new(&m);
736 let mut ov = Overrides::default();
737 inst.fire(1);
738 inst.tick(&m, &clips, 1.0, &mut ov);
739 assert_eq!(inst.layers[0].current, 1);
740 m.layers[0].states[1].transitions[0].conditions = vec![Condition::Triggered { input: 1 }];
741 inst.tick(&m, &clips, 1.0, &mut ov);
742 assert_eq!(inst.layers[0].current, 1);
743 }
744
745 #[test]
746 fn clip_event_crossing_fires_once() {
747 let (clips, m, _) = world();
748 let mut inst = MachineInstance::new(&m);
749 inst.set_bool(0, true);
750 let mut ov = Overrides::default();
751 inst.tick(&m, &clips, 1.0, &mut ov);
752 let mut names = Vec::new();
753 for _ in 0..40 {
754 let out = inst.tick(&m, &clips, 1.0, &mut ov);
755 names.extend(out.events);
756 }
757 assert_eq!(names.iter().filter(|n| *n == "half").count(), 1);
758 }
759
760 #[test]
761 fn crossfade_tweens_overlapping_props() {
762 let (clips, m, node) = world();
763 let mut inst = MachineInstance::new(&m);
764 let mut ov = Overrides::default();
765 inst.set_bool(0, true);
766 inst.tick(&m, &clips, 1.0, &mut ov);
768 assert!(inst.layers[0].fade.is_some());
769 ov.clear();
771 inst.tick(&m, &clips, 4.0, &mut ov);
772 let Some(Value::F64(op)) = ov.get(node, "opacity").cloned() else {
773 panic!("expected f64 opacity");
774 };
775 assert!(op > 0.0 && op < 1.0, "got {op}");
776 }
777}