use super::*;
use crate::geometry::{Constraints, Vec2};
use crate::phase::Phase;
use crate::raster::{PixelFormat, RasterComponent, RasterImage, Resolution};
use crate::render_context::{PassThrough, RenderContext};
use crate::time::{LocalTime, Time, TimelineTime};
use crate::timeline_container::{AudioFile, Sequence, Timeline};
#[derive(PartialEq, Hash)]
struct OpacityProbe {
alpha: u8,
}
impl OpacityProbe {
fn new(opacity: f32) -> Self {
Self {
alpha: (opacity.clamp(0.0, 1.0) * 255.0).round() as u8,
}
}
}
impl RasterComponent for OpacityProbe {
fn layout(&self, _c: Constraints) -> Vec2 {
Vec2(1.0, 1.0)
}
fn render(&self, _s: Vec2, t: Resolution, _ctx: &mut dyn RenderContext) -> RasterImage {
let count = (t.width as usize) * (t.height as usize);
let mut pixels = Vec::with_capacity(count * 4);
for _ in 0..count {
pixels.extend_from_slice(&[255, 255, 255, self.alpha]);
}
RasterImage::cpu(t.width, t.height, PixelFormat::Rgba8, pixels)
}
}
#[crate::component(timeline)]
fn Reveal(
#[clock] clock: Clock,
#[builder(into)] line: String,
event: Event,
) -> impl TimelineComponent {
let _ = line; let appear = event.phase(&clock, 0.0, 0.5);
OpacityProbe::new(appear.get())
}
#[crate::component(timeline)]
fn Dialogue(#[builder(into)] voice: AudioFile) -> impl TimelineComponent {
Timeline::builder().child(voice).build()
}
fn voice(seconds: f32) -> AudioFile {
AudioFile::builder()
.path("media/vo.wav")
.duration(seconds)
.build()
}
fn build_piece(reveal: Event, line1: f32, line2: f32, line3: f32) -> impl TimelineComponent {
Timeline::builder()
.child(
Sequence::builder()
.child(Dialogue::builder().voice(voice(line1)).build())
.child(
Dialogue::builder()
.voice(voice(line2))
.build()
.trigger_at_start(reveal),
)
.child(Dialogue::builder().voice(voice(line3)).build())
.build(),
)
.child(Reveal::builder().line("Chapter II").event(reveal).fill())
.build()
}
fn alpha_at(resolved: &ResolvedTimeline, t: f32) -> u8 {
let mut ctx = PassThrough;
let frame = resolved
.frame(TimelineTime::new(t), Resolution::new(2, 2), &mut ctx)
.expect("the reveal always contributes a frame");
frame.as_cpu().expect("cpu image").pixels[3]
}
#[test]
fn trigger_glued_to_resolved_start() {
let reveal = Event::new();
let resolved =
resolve(build_piece(reveal, 3.0, 4.0, 5.0)).expect("voices give the piece a length");
assert_eq!(resolved.triggers().get(reveal.id()).seconds(), 3.0);
assert_eq!(resolved.duration(), 12.0);
}
#[test]
fn reflow_reglues_trigger_to_new_start() {
let reveal = Event::new();
let resolved = resolve(build_piece(reveal, 3.0, 4.0, 5.0)).expect("not timeless");
assert_eq!(resolved.triggers().get(reveal.id()).seconds(), 3.0);
let reglued = resolve(build_piece(reveal, 5.0, 4.0, 5.0)).expect("not timeless");
assert_eq!(reglued.triggers().get(reveal.id()).seconds(), 5.0);
let earlier = resolve(build_piece(reveal, 1.5, 4.0, 5.0)).expect("not timeless");
assert_eq!(earlier.triggers().get(reveal.id()).seconds(), 1.5);
}
#[test]
fn per_frame_phase_ramps_off_the_resolved_trigger() {
let reveal = Event::new();
let resolved = resolve(build_piece(reveal, 3.0, 4.0, 5.0)).expect("not timeless");
assert_eq!(resolved.triggers().get(reveal.id()).seconds(), 3.0);
assert_eq!(alpha_at(&resolved, 2.9), 0, "before the trigger ⇒ 0");
assert_eq!(alpha_at(&resolved, 3.0), 0, "at the trigger ⇒ phase 0");
let mid = alpha_at(&resolved, 3.25);
assert!(
(120..=136).contains(&mid),
"mid-ramp opacity ~0.5, got {mid}"
);
assert_eq!(alpha_at(&resolved, 3.5), 255, "ramp end ⇒ 1.0");
assert_eq!(alpha_at(&resolved, 9.0), 255, "well after ⇒ stays 1.0");
}
#[test]
fn unfired_event_stays_zero_with_no_nan() {
let never = Event::new();
let root = Timeline::builder()
.child(OpacityProbe { alpha: 0 }.at(0.0..6.0))
.child(Reveal::builder().line("nope").event(never).fill())
.build();
let resolved = resolve(root).expect("the windowed sibling gives a length");
assert!(resolved.triggers().get(never.id()).seconds().is_infinite());
for &t in &[0.0_f32, 1.0, 3.0, 5.9] {
let a = alpha_at(&resolved, t);
assert_eq!(a, 0, "unfired event ⇒ opacity 0 at t={t}");
}
let table = TriggerTable::new();
let clock = Clock::new(TimelineTime::new(4.0), LocalTime::new(4.0), &table);
let p = never.phase(&clock, 0.0, 0.5);
assert_eq!(p, Phase::ZERO);
assert!(!p.get().is_nan());
}
#[test]
fn structural_clock_queries_do_not_panic() {
let reveal = Reveal::builder()
.line("Chapter II")
.event(Event::new())
.build();
assert_eq!(reveal.duration(), None, "the baked visual is timeless");
assert_eq!(reveal.measure(), None);
assert_eq!(reveal.arrangement(0.0).kind, NodeKind::Video);
assert!(reveal.cues(0.0).is_empty());
let mut ctx = ResolveCtx::new();
let len = reveal.resolve(0.0, &mut ctx);
assert_eq!(len, 0.0);
}
#[test]
fn container_positioned_child_triggers_at_its_resolved_start() {
let e = Event::new();
let seq = Sequence::builder()
.child(voice(2.5)) .child(voice(3.0).trigger_at_start(e)) .build();
let resolved = resolve(seq).expect("voices give a length");
assert_eq!(resolved.triggers().get(e.id()).seconds(), 2.5);
}
#[test]
fn triggered_over_placed_ignores_inner_offset() {
let e = Event::new();
let resolved =
resolve(OpacityProbe { alpha: 0 }.at(5.0..7.0).trigger_at_start(e)).expect("windowed");
assert_eq!(resolved.triggers().get(e.id()).seconds(), 0.0);
}
#[test]
fn placed_over_triggered_keeps_offset() {
let e = Event::new();
let resolved =
resolve(OpacityProbe { alpha: 0 }.trigger_at_start(e).at(5.0..7.0)).expect("windowed");
assert_eq!(resolved.triggers().get(e.id()).seconds(), 5.0);
}