alpha2_eval/frame.rs
1use crate::{EffectParamValue, sample_effect_param};
2use alpha2_media::SourceId;
3use alpha2_model::{
4 BlendSpace, ClipBlendMode, ClipEffect, ClipId, EffectResourceId, Generator, Matte, Rgb,
5 ShapeMask, TextStyle, TrackId, Vec2,
6};
7use alpha2_time::MediaTime;
8
9/// A timestamp-specific composition description with no GPU handles.
10///
11/// Identities and authored effects are borrowed from the [`crate::ActiveIndex`] (`'a`), so
12/// evaluating a frame copies none of them.
13#[derive(Clone, Debug, PartialEq)]
14pub struct FrameDesc<'a> {
15 /// Timestamp evaluated on the project timeline.
16 pub time: MediaTime,
17 /// Opaque color under every visual layer, or `None` for transparent black (alpha output).
18 pub background: Option<Rgb>,
19 /// The space in which blend modes are evaluated.
20 pub blend_space: BlendSpace,
21 /// Active visual clips in composition order.
22 pub visual: Vec<ActiveClip<'a>>,
23 /// Active audio clips in mix order.
24 pub audio: Vec<ActiveClip<'a>>,
25 /// Active two-input transitions in track order.
26 pub transitions: Vec<ActiveTransition<'a>>,
27 /// The cue each caption track shows, in track order.
28 pub captions: Vec<ActiveCaption<'a>>,
29}
30
31impl FrameDesc<'_> {
32 /// Whether the frame draws `clip`'s picture. A matte ([`ActiveClip::is_matte`]) is drawn
33 /// only while an active clip names it.
34 #[must_use]
35 pub fn draws(&self, clip: &ActiveClip<'_>) -> bool {
36 !clip.is_matte
37 || self.visual.iter().any(|other| {
38 other
39 .matte
40 .is_some_and(|matte| &matte.source == clip.clip_id)
41 })
42 }
43}
44
45/// The cue one caption track shows at a timestamp.
46#[derive(Clone, Copy, Debug, PartialEq)]
47pub struct ActiveCaption<'a> {
48 /// The caption track.
49 pub track_id: &'a TrackId,
50 /// The cue's text.
51 pub text: &'a alpha2_model::TextContent,
52 /// Time since the cue started, which keys its style's animators.
53 pub local_time: alpha2_time::MediaTime,
54 /// The track style with the cue's (or else its speaker's) named style patch applied.
55 pub style: &'a TextStyle,
56 /// How many [`FrameDesc::visual`] clips lie under the caption by track order, counting clips
57 /// the frame does not draw ([`FrameDesc::draws`]) so an undrawn clip never shifts a caption.
58 pub layers_below: usize,
59}
60
61/// A visible or audible clip mapped to its source timestamp.
62#[derive(Clone, Copy, Debug, PartialEq)]
63pub struct ActiveClip<'a> {
64 /// Active clip identity.
65 pub clip_id: &'a ClipId,
66 /// Media source needed for this clip.
67 pub source_id: &'a SourceId,
68 /// Track containing the clip.
69 pub track_id: &'a TrackId,
70 /// Source timestamp selected by the clip's time map.
71 pub source_time: MediaTime,
72 /// Evaluated visual presentation, present only for visual tracks.
73 pub visual: Option<EvaluatedVisual>,
74 /// The composition that builds the clip's picture; `None` shows the source as is.
75 pub composition: Option<crate::ActiveComposition<'a>>,
76 /// Artistic blend operation used by visual composition.
77 pub blend_mode: ClipBlendMode,
78 /// Whether a runtime may keep and reuse the clip's rendered picture.
79 pub cache: alpha2_model::CacheIntent,
80 /// Shape masks in order. Evaluate outlines with [`crate::evaluate_geometry`] and feathers with
81 /// [`crate::sample_scalar`] at the frame's time.
82 pub masks: &'a [ShapeMask],
83 /// The clip whose picture sets where this one is visible.
84 pub matte: Option<&'a Matte>,
85 /// Whether this clip is another clip's matte, drawn only into that matte.
86 pub is_matte: bool,
87 /// Generated source content, drawn at `source_time` with no host frame.
88 pub generator: Option<&'a Generator>,
89 /// Planned shapes or layout generator tree, to evaluate at `source_time`.
90 pub plan: Option<&'a crate::GeneratorPlan>,
91 /// For a composition source, its composition at the clip's source time (no host frame).
92 pub source_composition: Option<crate::ActiveComposition<'a>>,
93}
94
95impl ActiveClip<'_> {
96 /// Whether the clip draws its own host-supplied source frame.
97 #[must_use]
98 pub fn needs_source(&self) -> bool {
99 self.generator.is_none()
100 && self.source_composition.is_none()
101 && self
102 .composition
103 .is_none_or(|composition| composition.plan().uses_clip())
104 }
105}
106
107/// Visual properties evaluated at one timeline timestamp.
108#[derive(Copy, Clone, Debug, PartialEq)]
109pub struct EvaluatedVisual {
110 /// Normalized output translation.
111 pub position: Vec2,
112 /// Normalized source anchor.
113 pub anchor: Vec2,
114 /// Per-axis scale.
115 pub scale: Vec2,
116 /// Clockwise rotation in degrees.
117 pub rotation_degrees: f32,
118 /// Opacity from zero to one.
119 pub opacity: f32,
120}
121
122/// A source-frame request emitted for host prefetch and decode scheduling.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct FrameNeed {
125 /// Clip consumer that owns the sequential decoder cursor.
126 ///
127 /// Multiple clips may read the same source at unrelated timestamps. Hosts
128 /// must not make those clips share one seek position.
129 pub clip_id: ClipId,
130 /// The composition step reading the source, if not the clip itself: its
131 /// [`crate::Step::node`] path, prefixed with `/` for steps of a composition source. Each
132 /// (clip, reader) pair keeps its own decoder cursor.
133 pub reader: Option<alpha2_model::NodeId>,
134 /// Source to decode or retrieve.
135 pub source_id: SourceId,
136 /// Timestamp requested on the source timeline.
137 pub media_time: MediaTime,
138}
139
140/// A transition evaluated at one timeline timestamp.
141#[derive(Clone, Copy, Debug, PartialEq)]
142pub struct ActiveTransition<'a> {
143 /// Clip sampled at progress zero.
144 pub from_clip: &'a ClipId,
145 /// Clip sampled at progress one.
146 pub to_clip: &'a ClipId,
147 /// Versioned shader invocation, with parameters sampled at the frame's time.
148 pub effect: ActiveEffect<'a>,
149 /// Eased progress in the inclusive unit interval.
150 pub progress: f32,
151}
152
153/// One effect invocation at one timestamp (timeline time for transitions, composition time for
154/// composition steps).
155#[derive(Clone, Copy, Debug, PartialEq)]
156pub struct ActiveEffect<'a> {
157 effect: &'a ClipEffect,
158 time: MediaTime,
159}
160
161impl<'a> ActiveEffect<'a> {
162 /// The invocation `effect` sampled at `time`.
163 #[must_use]
164 pub const fn new(effect: &'a ClipEffect, time: MediaTime) -> Self {
165 Self { effect, time }
166 }
167
168 /// Stable effect definition identifier.
169 #[must_use]
170 pub fn id(&self) -> &'a str {
171 &self.effect.id
172 }
173
174 /// Exact effect definition version.
175 #[must_use]
176 pub const fn version(&self) -> u32 {
177 self.effect.version
178 }
179
180 /// Every authored parameter sampled at this timestamp. Omitted ones use definition defaults.
181 pub fn params(&self) -> impl Iterator<Item = (&'a str, EffectParamValue)> + 'a {
182 let time = self.time;
183 self.effect
184 .params
185 .iter()
186 .map(move |(name, animation)| (name.as_str(), sample_effect_param(animation, time)))
187 }
188
189 /// One parameter sampled at this timestamp, if the invocation sets it.
190 #[must_use]
191 pub fn param(&self, name: &str) -> Option<EffectParamValue> {
192 self.effect
193 .params
194 .get(name)
195 .map(|animation| sample_effect_param(animation, self.time))
196 }
197
198 /// Named external resources matched against the effect definition.
199 pub fn resources(&self) -> impl Iterator<Item = (&'a str, &'a EffectResourceId)> + 'a {
200 self.effect
201 .resources
202 .iter()
203 .map(|(name, id)| (name.as_str(), id))
204 }
205
206 /// The authored invocation, with its parameters unevaluated.
207 #[must_use]
208 pub const fn authored(&self) -> &'a ClipEffect {
209 self.effect
210 }
211}