1pub mod build;
22pub mod compose;
24pub mod eval;
26pub mod node;
28pub mod sound;
30
31#[cfg(test)]
32mod tests {
33 use super::{
34 build::{IntoAnimNode, PlaybackExt, Unplaced},
35 compose::{AnimIterExt, lagged::LaggedFill, sequence::AnimSequence, stack::AnimStack},
36 eval::{DynOutput, Eval, EvalExt, OutputContext, Static, StaticAnim, pure::Pure},
37 node::{AnimNode, AnimationInfoKind, LeafContent, NodeContent},
38 };
39 use crate::{frame::EvaluatedFrame, lagged, seq, stack};
40
41 #[derive(Clone, Default)]
42 struct PositionX(f32);
43
44 struct ShiftX(f32);
46
47 impl Eval for ShiftX {
48 type Output = PositionX;
49
50 fn eval_alpha(&self, _alpha: f64) -> Self::Output {
51 PositionX(self.0)
52 }
53 }
54
55 struct ShiftAlpha(f32);
57
58 impl Eval for ShiftAlpha {
59 type Output = PositionX;
60
61 fn eval_alpha(&self, alpha: f64) -> Self::Output {
62 PositionX(self.0 + alpha as f32)
63 }
64 }
65
66 fn leaf(x: f32, duration: f64) -> impl Unplaced {
67 ShiftX(x).with_duration(duration)
68 }
69
70 fn progress_leaf(offset: f32) -> impl Unplaced {
71 ShiftAlpha(offset)
72 }
73
74 fn evaluated_xs(items: Vec<DynOutput>) -> Vec<f32> {
75 let mut frame = EvaluatedFrame::new();
76 let mut output = OutputContext::new(&mut frame, 0);
77 for value in items {
78 value.write(&mut output);
79 }
80 frame
81 .items::<PositionX>()
82 .iter()
83 .map(|item| item.value.0)
84 .collect()
85 }
86
87 fn sampled_xs(animation: &AnimNode, sec: f64) -> Vec<f32> {
88 let mut frame = EvaluatedFrame::new();
89 animation.eval_at(sec, &mut OutputContext::new(&mut frame, 0));
90 frame
91 .items::<PositionX>()
92 .iter()
93 .map(|item| item.value.0)
94 .collect()
95 }
96
97 #[test]
98 fn apply_alpha_to_writes_the_requested_progress_state() {
99 let mut item = PositionX::default();
100
101 let animation =
102 Pure::new(|alpha| ShiftAlpha(0.0).eval_alpha(alpha)).apply_alpha_to(&mut item, 0.25);
103 assert_eq!(item.0, 0.25);
104
105 let animation = animation.apply_to(&mut item);
106 assert_eq!(item.0, 1.0);
107 assert_eq!(animation.eval_alpha(0.5).0, 0.5);
108 }
109
110 #[test]
111 fn containers_lower_children_to_expected_timelines() {
112 let sequence = [leaf(1.0, 1.0), leaf(2.0, 2.0)].into_iter().into_seq();
113 let stack = [leaf(1.0, 1.0), leaf(2.0, 2.0)].into_iter().into_stack();
114 let lagged = [leaf(1.0, 1.0), leaf(2.0, 1.0)]
115 .into_iter()
116 .into_lagged(0.5);
117 for (node, second_range) in [
118 (sequence.into_anim_node(), 1.0..3.0),
119 (stack.into_anim_node(), 0.0..2.0),
120 (
121 lagged
122 .with_leading(LaggedFill::Empty)
123 .with_trailing(LaggedFill::Empty)
124 .into_anim_node(),
125 0.5..1.5,
126 ),
127 ] {
128 assert!(matches!(node.content, NodeContent::Children(_)));
129 let child = &node.children()[1];
131 let range = child.children().first().unwrap_or(child).time_range();
132 assert_eq!(range, second_range);
133 }
134 assert!(seq![].into_anim_node().children().is_empty());
135 assert!(stack![].into_anim_node().children().is_empty());
136 }
137
138 #[test]
139 fn runtime_children_evaluate_overlaps_in_order_and_keep_half_open_boundaries() {
140 let animation = stack![
141 progress_leaf(0.0).with_duration(2.0),
142 progress_leaf(10.0).with_duration(2.0).at(1.0),
143 leaf(100.0, 3.0).with_enabled(false),
144 ]
145 .into_anim_node();
146 assert!(matches!(animation.content, NodeContent::Children(_)));
147 assert_eq!(sampled_xs(&animation, 1.5), vec![0.75, 10.25]);
148 assert_eq!(sampled_xs(&animation, 2.0), vec![10.5]);
149 assert_eq!(sampled_xs(&animation, 3.0), vec![11.0]);
150 assert_eq!(sampled_xs(&animation, 3.1), vec![]);
151 let sequential = seq![leaf(1.0, 1.0), leaf(2.0, 1.0)].into_anim_node();
152 assert!(matches!(sequential.content, NodeContent::Children(_)));
153 assert_eq!(sampled_xs(&sequential, 1.0), vec![2.0]);
154 }
155
156 #[test]
157 fn offsets_shift_the_whole_container_timeline() {
158 let mut sequence = AnimSequence::new();
159 sequence
160 .push(leaf(1.0, 2.0))
161 .forward(1.0)
162 .push(leaf(2.0, 1.0));
163 let info = sequence.at(10.0).into_anim_node().animation_info();
164 assert_eq!(info.range, 10.0..14.0);
165 assert_eq!(info.children[0].range, 0.0..2.0);
166 assert_eq!(info.children[1].range, 3.0..4.0);
167 }
168
169 #[test]
170 fn parametrized_sequence_remaps_the_group_timeline() {
171 use crate::utils::rate_functions::ease_in_quad;
172
173 let animation = seq![progress_leaf(0.0), progress_leaf(10.0)]
174 .with_duration(4.0)
175 .with_rate_func(ease_in_quad);
176 let animation = animation.into_anim_node();
177
178 assert_eq!(animation.time_range(), 0.0..4.0);
179 assert_eq!(sampled_xs(&animation, 2.0), vec![0.5]);
180 let info = animation.animation_info();
181 assert_eq!(info.range, 0.0..4.0);
182 assert_eq!(info.content_duration_secs, 2.0);
183 assert_eq!(info.children.len(), 2);
184 assert_eq!(info.children[0].range, 0.0..1.0);
185 assert_eq!(info.children[1].range, 1.0..2.0);
186 }
187
188 #[test]
189 fn extend_appends_direct_children_and_preserves_local_gaps() {
190 let mut source = AnimSequence::new();
191 source
192 .push(leaf(2.0, 1.0))
193 .forward(2.0)
194 .push(leaf(3.0, 1.0));
195
196 let mut sequence = AnimSequence::new();
197 sequence.push(leaf(1.0, 2.0)).extend(source);
198 assert_eq!(sequence.cursor_sec(), 6.0);
199 assert_eq!(sequence.built_animations().len(), 3);
200 assert_eq!(sequence.built_animations()[0].time_range(), 0.0..2.0);
201 assert_eq!(sequence.built_animations()[1].time_range(), 2.0..3.0);
202 assert_eq!(sequence.built_animations()[2].time_range(), 5.0..6.0);
203
204 let source = stack![leaf(2.0, 1.0), leaf(3.0, 2.0).at(1.0)];
205 let mut stack = stack![leaf(1.0, 4.0)];
206 stack.extend(source);
207 assert_eq!(stack.duration_secs(), 4.0);
208 assert_eq!(stack.built_animations().len(), 3);
209 assert_eq!(stack.built_animations()[1].time_range(), 0.0..1.0);
210 assert_eq!(stack.built_animations()[2].time_range(), 1.0..3.0);
211 }
212
213 #[test]
214 fn holds_sample_active_items_and_repeat_without_nesting() {
215 let mut sequence = AnimSequence::new();
216 sequence
217 .push(stack![leaf(1.0, 1.0), leaf(2.0, 2.0), leaf(3.0, 2.0)])
218 .hold(1.0)
219 .hold(2.0);
220 assert_eq!(sequence.cursor_sec(), 5.0);
221 for (index, sec) in [(1, 2.5), (2, 4.0)] {
222 let held = &sequence.built_animations()[index];
223 let mut items = Vec::new();
224 held.capture_at(sec, &mut items);
225 assert_eq!(items.len(), 2);
227 assert_eq!(evaluated_xs(items), vec![2.0, 3.0]);
228 assert!(matches!(
229 held.content,
230 NodeContent::Leaf(LeafContent::Anim(_))
231 ));
232 }
233 }
234
235 #[test]
236 fn holds_use_final_evaluations_and_forward_does_not_hold() {
237 let shown = PositionX(7.0);
238 for (ending, expected) in [(shown.show(), vec![7.0]), (shown.hide(), vec![])] {
239 let mut outer = AnimSequence::new();
240 outer.push(seq![leaf(1.0, 1.0), ending]).hold(1.0);
241 assert_eq!(sampled_xs(&outer.into_anim_node(), 1.5), expected);
242 }
243 let mut gap = AnimSequence::new();
244 gap.push(leaf(4.0, 1.0)).forward(1.0).hold(1.0);
245 assert!(sampled_xs(&gap.into_anim_node(), 2.5).is_empty());
246 }
247
248 #[test]
249 fn sequence_lowering_removes_covered_points_after_holds_have_sampled_them() {
250 let first = PositionX(2.0);
251 let second = PositionX(3.0);
252 let mut sequence = AnimSequence::new();
253 sequence
254 .push(progress_leaf(0.0))
255 .push(first.show())
256 .push(second.show())
257 .hold(0.5)
258 .push(second.hide())
259 .hold(0.5);
260 let animation = sequence.into_anim_node();
261 assert_eq!(animation.children().len(), 2);
262 assert_eq!(sampled_xs(&animation, 0.5), vec![0.5]);
263 assert_eq!(sampled_xs(&animation, 1.0), vec![3.0]);
264 assert_eq!(sampled_xs(&animation, 1.25), vec![3.0]);
265 assert_eq!(sampled_xs(&animation, 1.5), vec![]);
266 assert_eq!(sampled_xs(&animation, 2.0), vec![]);
267 }
268
269 #[test]
270 fn sequence_final_points_replace_endpoints_without_affecting_stacks() {
271 let shown = PositionX(9.0);
272 let sequence = seq![progress_leaf(0.0), shown.show(), shown.hide(), shown.show()]
273 .with_duration(2.0)
274 .at(3.0)
275 .into_anim_node();
276 assert_eq!(sampled_xs(&sequence, 4.0), vec![0.5]);
277 assert_eq!(sampled_xs(&sequence, 5.0), vec![9.0]);
278 assert_eq!(sequence.children().len(), 2);
279 assert!(!sequence.children()[0].active_at(1.0));
280
281 let hidden = seq![progress_leaf(0.0), shown.hide()].into_anim_node();
282 assert_eq!(sampled_xs(&hidden, 0.5), vec![0.5]);
283 assert!(sampled_xs(&hidden, 1.0).is_empty());
284 let only_points = seq![shown.show(), shown.hide()].into_anim_node();
285 assert_eq!(only_points.children().len(), 1);
286 assert!(sampled_xs(&only_points, 0.0).is_empty());
287
288 let stack = stack![progress_leaf(0.0), shown.show().at(1.0)].into_anim_node();
290 assert_eq!(sampled_xs(&stack, 1.0), vec![1.0, 9.0]);
291 }
292
293 #[test]
294 fn lagged_staggers_and_fills_window_edges() {
295 let lagged = lagged![0.5; leaf(1.0, 1.0), leaf(2.0, 2.0), leaf(3.0, 1.0)];
297 assert_eq!(lagged.built_animations()[0].time_range(), 0.0..1.0);
298 assert_eq!(lagged.built_animations()[1].time_range(), 0.5..2.5);
299 assert_eq!(lagged.built_animations()[2].time_range(), 1.5..2.5);
300 assert_eq!(lagged.duration_secs(), 2.5);
301
302 let as_sequence = lagged![1.0; leaf(1.0, 1.0), leaf(2.0, 1.0)];
304 assert_eq!(as_sequence.built_animations()[1].time_range(), 1.0..2.0);
305 let as_stack = lagged![0.0; leaf(1.0, 1.0), leaf(2.0, 2.0)];
306 assert_eq!(as_stack.built_animations()[1].time_range(), 0.0..2.0);
307 assert_eq!(as_stack.duration_secs(), 2.0);
308
309 let animation = lagged![
311 0.5;
312 progress_leaf(0.0).with_duration(1.0),
313 progress_leaf(10.0).with_duration(1.0)
314 ]
315 .into_anim_node();
316 let info = animation.animation_info();
317 assert_eq!(info.kind, AnimationInfoKind::Children);
318 let track_ranges: Vec<_> = info
319 .children
320 .iter()
321 .map(|track| {
322 track
323 .children
324 .iter()
325 .map(|child| child.range.clone())
326 .collect::<Vec<_>>()
327 })
328 .collect();
329 assert_eq!(
330 track_ranges,
331 [vec![0.0..1.0, 1.0..1.5], vec![0.0..0.5, 0.5..1.5]]
332 );
333 assert_eq!(sampled_xs(&animation, 0.25), vec![0.25, 10.0]);
334 assert_eq!(sampled_xs(&animation, 1.0), vec![1.0, 10.5]);
335 assert_eq!(sampled_xs(&animation, 1.5), vec![1.0, 11.0]);
336
337 let empty_leading = lagged![
339 0.5;
340 progress_leaf(0.0).with_duration(1.0),
341 progress_leaf(10.0).with_duration(1.0)
342 ]
343 .with_leading(LaggedFill::Empty)
344 .into_anim_node();
345 assert_eq!(sampled_xs(&empty_leading, 0.25), vec![0.25]);
346 assert_eq!(sampled_xs(&empty_leading, 1.0), vec![1.0, 10.5]);
347
348 let shown = PositionX(2.0);
350 let hidden = lagged![0.5; seq![leaf(2.0, 1.0), shown.hide()]].into_anim_node();
351 assert_eq!(sampled_xs(&hidden, 0.5), vec![2.0]);
352 assert!(sampled_xs(&hidden, 1.0).is_empty());
353 }
354
355 use super::sound::Sound;
358 use crate::RanimScene;
359 use crate::audio::{AudioClip, MASTER_SAMPLE_RATE};
360 use crate::utils::rate_functions::ease_in_quad;
361
362 fn tone(secs: f64) -> AudioClip {
363 let pcm = vec![0.5f32; (secs * 48_000.0) as usize];
365 AudioClip::from_pcm(pcm, 48_000, 1)
366 }
367
368 fn audible_region(scene: RanimScene) -> (f64, f64, f64) {
372 let sealed = scene.seal();
373 let total = sealed.total_secs();
374 let evaluator = sealed.into_evaluator(120.0);
375 let buf = evaluator.mix_audio(total, MASTER_SAMPLE_RATE);
376 let first = buf
377 .iter()
378 .position(|s| s.abs() > 1e-4)
379 .expect("expected audible samples");
380 let last = buf.len()
381 - 1
382 - buf
383 .iter()
384 .rev()
385 .position(|s| s.abs() > 1e-4)
386 .expect("non-empty");
387 (
388 total,
389 first as f64 / 2.0 / MASTER_SAMPLE_RATE as f64,
390 last as f64 / 2.0 / MASTER_SAMPLE_RATE as f64,
391 )
392 }
393
394 #[test]
395 fn sound_windows_follow_nested_placement_and_duration_overrides() {
396 for (duration, expected_start, expected_end) in [(2.0, 2.0, 3.0), (1.0, 1.5, 2.0)] {
397 let mut scene = RanimScene::new();
398 scene.play(
399 seq![leaf(1.0, 1.0), Sound::new(tone(1.0))]
400 .with_duration(duration)
401 .at(1.0),
402 );
403 let (total, start, end) = audible_region(scene);
404 assert_eq!(total, expected_end);
405 for (actual, expected) in [(start, expected_start), (end, expected_end)] {
406 assert!((actual - expected).abs() < 0.01, "{actual} vs {expected}");
407 }
408 }
409 }
410
411 #[test]
412 fn audio_leaves_are_silent_when_disabled_and_emit_no_visual_items() {
413 let mut scene = RanimScene::new();
414 scene.play(stack![Sound::new(tone(1.0)).with_enabled(false)]);
415 let sealed = scene.seal();
416 let mut frame = EvaluatedFrame::new();
417 sealed.eval_at_sec(0.5, &mut frame);
418 assert!(frame.is_empty());
419 let evaluator = sealed.into_evaluator(120.0);
420 assert!(evaluator.has_audio());
421 assert!(
422 evaluator
423 .mix_audio(1.0, MASTER_SAMPLE_RATE)
424 .iter()
425 .all(|s| s.abs() < 1e-4)
426 );
427 let mut visual = RanimScene::new();
428 visual.play(leaf(1.0, 1.0));
429 assert!(!visual.seal().into_evaluator(120.0).has_audio());
430 let mut audio = RanimScene::new();
431 audio.play(Sound::new(tone(0.01)));
432 audio.seal().eval_at_sec(0.005, &mut frame);
433 assert!(frame.is_empty());
434 }
435
436 #[test]
437 fn container_rate_func_warps_the_sound_window() {
438 let inner = seq![leaf(1.0, 1.0), Sound::new(tone(1.0))];
442 let mut scene = RanimScene::new();
443 scene.play(inner.with_rate_func(ease_in_quad));
444
445 let (_, start, end) = audible_region(scene);
446 let expected_start = 2.0 * (0.5f64).sqrt();
447 assert!((start - expected_start).abs() < 0.01, "start {start}");
448 assert!((end - 2.0).abs() < 0.01, "end {end}");
449 }
450
451 #[test]
452 fn sound_rate_func_warps_its_content_progress() {
453 let clip = AudioClip::from_pcm(
456 (0..48_000).map(|i| i as f32 / 48_000.0).collect::<Vec<_>>(),
457 48_000,
458 1,
459 );
460 let mut scene = RanimScene::new();
461 scene.play(Sound::new(clip).with_rate_func(ease_in_quad));
462 let evaluator = scene.seal().into_evaluator(120.0);
463 let buf = evaluator.mix_audio(1.0, 48_000);
464 let mid = buf[(0.5 * 48_000.0) as usize * 2];
466 assert!((mid - 0.25).abs() < 0.01, "amplitude at 0.5s was {mid}");
467 }
468
469 #[test]
470 fn warped_container_of_warped_sounds_mixes_all_of_them() {
471 let clip = AudioClip::from_pcm(vec![0.25f32; 48_000], 48_000, 1);
475 let mut stack = AnimStack::new();
476 for _ in 0..10 {
477 stack.push(Sound::new(clip.clone()).with_rate_func(ease_in_quad));
478 }
479 let mut scene = RanimScene::new();
480 scene.play(stack.with_rate_func(ease_in_quad));
481 let buf = scene.seal().into_evaluator(120.0).mix_audio(0.5, 48_000);
482 for frame in [0usize, 12_000, 23_999] {
483 assert!(
484 (buf[frame * 2] - 2.5).abs() < 1e-3,
485 "frame {frame}: {} (expected 10 × 0.25)",
486 buf[frame * 2]
487 );
488 }
489 }
490
491 #[test]
492 fn sound_with_a_tail_extends_the_scene_to_its_window() {
493 let clip_len = 48_001; let clip = AudioClip::from_pcm(vec![0.5f32; clip_len], 48_000, 1);
499 let mut scene = RanimScene::new();
500 scene.play(stack![
501 Static(PositionX::default()).with_duration(1.0),
502 Sound::new(clip)
503 ]);
504 assert!((scene.seal().total_secs() - clip_len as f64 / 48_000.0).abs() < 1e-9);
505 }
506
507 #[test]
508 fn linear_rate_is_structural() {
509 let cells = [
515 AnimSequence::new().into_anim_node(),
516 AnimStack::new().into_anim_node(),
517 Sound::new(tone(0.01)).into_anim_node(),
518 leaf(1.0, 1.0).into_anim_node(),
519 ];
520 for cell in &cells {
521 assert!(
522 cell.rate_func.is_none(),
523 "a default-built cell must carry the structural linear rate"
524 );
525 }
526 let warped = leaf(1.0, 1.0).with_rate_func(ease_in_quad).into_anim_node();
527 assert!(warped.rate_func.is_some());
528 }
529
530 #[test]
531 fn baked_audio_matches_point_semantics() {
532 let ramp = |i: usize| 0.4 * i as f32 / 48_000.0;
539 let stereo = AudioClip::from_pcm(
540 (0..48_000)
541 .flat_map(|i| [ramp(i), ramp(i)])
542 .collect::<Vec<_>>(),
543 48_000,
544 2,
545 );
546 let warped = seq![
547 Static(PositionX::default()).with_duration(1.0),
548 Sound::new(tone(1.0))
549 ]
550 .with_rate_func(ease_in_quad);
551
552 let mut scene = RanimScene::new();
553 scene.play(stack![
554 seq![
555 Sound::new(AudioClip::sine(440.0, 1.0, 0.5))
556 .with_fade_in(0.25)
557 .with_gain(0.8),
558 Sound::new(stereo).with_duration(0.5),
560 ],
561 Sound::new(AudioClip::sine(880.0, 2.0, 0.3)).at(0.5),
562 seq![Sound::new(tone(2.0))].with_duration(1.7),
563 warped,
564 Sound::new(tone(1.0)).with_rate_func(ease_in_quad),
565 Static(PositionX::default()).with_duration(4.0),
566 ]);
567 let sealed = scene.seal();
568 let baked = sealed.audio().clone();
569 let evaluator = sealed.into_evaluator(120.0);
570
571 fn cell_at(cell: &AnimNode, x: f64) -> [f32; 2] {
572 if !cell.enabled || x < cell.time_range.start || x >= cell.time_range.end {
573 return [0.0; 2];
574 }
575 let raw = (x - cell.time_range.start) / cell.duration_secs();
576 let own = cell.internal_time_secs * cell.rate_func.map_or(raw, |rate| rate(raw));
577 match &cell.content {
578 NodeContent::Leaf(LeafContent::Audio(track)) => track.sample_at(own),
579 _ => {
580 let mut acc = [0.0f32; 2];
581 for child in cell.children() {
582 let [l, r] = cell_at(child, own);
583 acc[0] += l;
584 acc[1] += r;
585 }
586 acc
587 }
588 }
589 }
590
591 let frames = baked.len() / 2;
592 for frame in 0..frames {
593 let x = frame as f64 / MASTER_SAMPLE_RATE as f64;
594 let mut acc = [0.0f32; 2];
595 for cell in evaluator.cells() {
596 let [l, r] = cell_at(cell, x);
597 acc[0] += l;
598 acc[1] += r;
599 }
600 assert!(
601 (baked[frame * 2] - acc[0]).abs() < 1e-6
602 && (baked[frame * 2 + 1] - acc[1]).abs() < 1e-6,
603 "frame {frame}: baked [{}, {}] vs walk {acc:?}",
604 baked[frame * 2],
605 baked[frame * 2 + 1]
606 );
607 }
608 }
609}