1use crate::ecs::asset_id::AssetId;
8use alloc::vec::Vec;
9
10const FADE_HALF_SECS: f32 = 0.3;
11
12pub struct SceneFlow {
14 pub scenes: Vec<AssetId>,
16 pub current: AssetId,
18 pub fade: FadePhase,
20}
21
22pub enum FadePhase {
24 None,
26 ToBlack {
29 started_at: f32,
31 next: AssetId,
33 },
34 FromBlack {
36 started_at: f32,
38 },
39}
40
41pub trait SceneControl {
43 fn update_visibility(&mut self, draw_idx: usize, visible: bool);
45 fn set_fade(&mut self, fade: f32);
49}
50
51#[derive(Default)]
56pub struct SceneVisibility {
57 draws: Vec<usize>,
58 spans: Vec<(u32, u32)>,
59 scenes: Vec<Option<AssetId>>,
60}
61
62impl SceneVisibility {
63 pub fn clear(&mut self) {
65 self.draws.clear();
66 self.spans.clear();
67 self.scenes.clear();
68 }
69
70 pub fn begin_prop(&mut self, scene: Option<AssetId>) {
73 self.spans.push((self.draws.len() as u32, 0));
74 self.scenes.push(scene);
75 }
76
77 pub fn push_draw(&mut self, draw_idx: usize) {
79 debug_assert!(!self.spans.is_empty(), "push_draw before begin_prop");
80 self.draws.push(draw_idx);
81 if let Some(span) = self.spans.last_mut() {
82 span.1 += 1;
83 }
84 }
85
86 pub fn props(&self) -> impl Iterator<Item = (&[usize], Option<AssetId>)> + '_ {
88 self.spans
89 .iter()
90 .zip(self.scenes.iter())
91 .map(|(&(start, len), scene)| {
92 (&self.draws[start as usize..(start + len) as usize], *scene)
93 })
94 }
95}
96
97pub fn set_scene_visibility<B: SceneControl + ?Sized>(
100 visibility: &SceneVisibility,
101 active_scene: AssetId,
102 backend: &mut B,
103) {
104 for (draw_idxs, scene_opt) in visibility.props() {
105 let visible = match scene_opt {
106 None => true,
107 Some(s) => s == active_scene,
108 };
109 for &draw_idx in draw_idxs {
110 backend.update_visibility(draw_idx, visible);
111 }
112 }
113}
114
115pub fn tick_transitions<B: SceneControl + ?Sized>(
118 flow_opt: &mut Option<SceneFlow>,
119 visibility: &SceneVisibility,
120 elapsed: f32,
121 backend: &mut B,
122) {
123 let flow = match flow_opt {
124 Some(f) => f,
125 None => return,
126 };
127
128 match flow.fade {
129 FadePhase::ToBlack { started_at, next } => {
130 let t = ((elapsed - started_at) / FADE_HALF_SECS).clamp(0.0, 1.0);
131 backend.set_fade(t);
132 if t >= 1.0 {
133 flow.current = next;
134 flow.fade = FadePhase::FromBlack {
135 started_at: elapsed,
136 };
137 set_scene_visibility(visibility, next, backend);
138 }
139 }
140 FadePhase::FromBlack { started_at } => {
141 let t = ((elapsed - started_at) / FADE_HALF_SECS).clamp(0.0, 1.0);
142 backend.set_fade(1.0 - t);
143 if t >= 1.0 {
144 flow.fade = FadePhase::None;
145 }
146 }
147 FadePhase::None => {}
148 }
149}
150
151pub fn jump_to_scene<B: SceneControl + ?Sized>(
154 flow_opt: &mut Option<SceneFlow>,
155 visibility: &SceneVisibility,
156 elapsed: f32,
157 target_scene: AssetId,
158 transition: &str,
159 backend: &mut B,
160) {
161 let flow = match flow_opt {
162 Some(f) => f,
163 None => {
164 return;
165 }
166 };
167
168 if !flow.scenes.contains(&target_scene) {
169 return;
170 }
171
172 if target_scene == flow.current {
173 return;
174 }
175
176 match transition {
177 "FadeBlack" => {
178 flow.fade = FadePhase::ToBlack {
179 started_at: elapsed,
180 next: target_scene,
181 };
182 }
183 _ => {
184 flow.current = target_scene;
185 flow.fade = FadePhase::None;
186 set_scene_visibility(visibility, target_scene, backend);
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 use alloc::vec;
196 #[derive(Default)]
198 struct TestBackend {
199 visibility: Vec<(usize, bool)>,
200 fades: Vec<f32>,
201 }
202
203 impl SceneControl for TestBackend {
204 fn update_visibility(&mut self, draw_idx: usize, visible: bool) {
205 self.visibility.push((draw_idx, visible));
206 }
207 fn set_fade(&mut self, fade: f32) {
208 self.fades.push(fade);
209 }
210 }
211
212 fn make_flow(scenes: &[AssetId]) -> SceneFlow {
213 SceneFlow {
214 scenes: scenes.to_vec(),
215 current: scenes[0],
216 fade: FadePhase::None,
217 }
218 }
219
220 fn vis(props: &[(&[usize], Option<AssetId>)]) -> SceneVisibility {
221 let mut v = SceneVisibility::default();
222 for (draws, scene) in props {
223 v.begin_prop(*scene);
224 for &d in *draws {
225 v.push_draw(d);
226 }
227 }
228 v
229 }
230
231 #[test]
232 fn set_visibility_active_scene_visible_others_hidden() {
233 let visibility = vis(&[
235 (&[0], Some(AssetId(0))),
236 (&[1], None),
237 (&[2], Some(AssetId(1))),
238 ]);
239 let mut backend = TestBackend::default();
240 set_scene_visibility(&visibility, AssetId(0), &mut backend);
241
242 assert!(
243 backend.visibility.contains(&(0, true)),
244 "prop in 'a' should be visible"
245 );
246 assert!(
247 backend.visibility.contains(&(1, true)),
248 "scene-less prop always visible"
249 );
250 assert!(
251 backend.visibility.contains(&(2, false)),
252 "prop in 'b' should be hidden"
253 );
254 }
255
256 #[test]
257 fn set_visibility_no_scene_always_visible_regardless_of_active() {
258 let visibility = vis(&[(&[0], None)]);
259 let mut backend = TestBackend::default();
260 set_scene_visibility(&visibility, AssetId(99), &mut backend);
261 assert_eq!(backend.visibility, vec![(0, true)]);
262 }
263
264 #[test]
265 fn tick_without_fade_is_a_no_op() {
266 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
267 let mut backend = TestBackend::default();
268 tick_transitions(&mut opt, &SceneVisibility::default(), 999.0, &mut backend);
269 assert!(backend.visibility.is_empty());
270 assert!(backend.fades.is_empty());
271 assert_eq!(opt.as_ref().unwrap().current, AssetId(0));
272 }
273
274 #[test]
275 fn tick_fade_to_black_ramps_the_fade_up() {
276 let mut flow = make_flow(&[AssetId(0), AssetId(1)]);
277 flow.fade = FadePhase::ToBlack {
278 started_at: 0.0,
279 next: AssetId(1),
280 };
281 let mut opt = Some(flow);
282 let mut backend = TestBackend::default();
283 tick_transitions(
285 &mut opt,
286 &SceneVisibility::default(),
287 FADE_HALF_SECS * 0.5,
288 &mut backend,
289 );
290 assert_eq!(backend.fades.len(), 1);
291 assert!(
292 (backend.fades[0] - 0.5).abs() < 1e-5,
293 "half way to black at the midpoint of the first half"
294 );
295 assert!(matches!(
297 opt.as_ref().unwrap().fade,
298 FadePhase::ToBlack { .. }
299 ));
300 }
301
302 #[test]
303 fn tick_fade_to_black_completes_and_enters_from_black() {
304 let mut flow = make_flow(&[AssetId(0), AssetId(1)]);
305 flow.fade = FadePhase::ToBlack {
306 started_at: 0.0,
307 next: AssetId(1),
308 };
309 let mut opt = Some(flow);
310 let mut backend = TestBackend::default();
311 tick_transitions(
313 &mut opt,
314 &SceneVisibility::default(),
315 FADE_HALF_SECS,
316 &mut backend,
317 );
318 let f = opt.as_ref().unwrap();
319 assert_eq!(f.current, AssetId(1));
320 assert!(matches!(f.fade, FadePhase::FromBlack { .. }));
321 }
322
323 #[test]
324 fn tick_fade_from_black_clears_the_fade() {
325 let mut flow = make_flow(&[AssetId(0)]);
326 flow.fade = FadePhase::FromBlack { started_at: 0.0 };
327 let mut opt = Some(flow);
328 let mut backend = TestBackend::default();
329 tick_transitions(
331 &mut opt,
332 &SceneVisibility::default(),
333 FADE_HALF_SECS,
334 &mut backend,
335 );
336 assert!(matches!(opt.as_ref().unwrap().fade, FadePhase::None));
337 assert_eq!(*backend.fades.last().unwrap(), 0.0);
339 }
340
341 #[test]
344 fn tick_fade_from_black_ramps_the_fade_down() {
345 let mut flow = make_flow(&[AssetId(0)]);
346 flow.fade = FadePhase::FromBlack { started_at: 0.0 };
347 let mut opt = Some(flow);
348 let mut backend = TestBackend::default();
349 tick_transitions(
350 &mut opt,
351 &SceneVisibility::default(),
352 FADE_HALF_SECS * 0.25,
353 &mut backend,
354 );
355 assert_eq!(backend.fades.len(), 1);
356 assert!((backend.fades[0] - 0.75).abs() < 1e-5);
357 assert!(matches!(
358 opt.as_ref().unwrap().fade,
359 FadePhase::FromBlack { .. }
360 ));
361 }
362
363 #[test]
364 fn jump_to_scene_no_flow_is_no_op() {
365 let mut opt: Option<SceneFlow> = None;
366 let mut backend = TestBackend::default();
367 jump_to_scene(
368 &mut opt,
369 &SceneVisibility::default(),
370 0.0,
371 AssetId(99),
372 "Cut",
373 &mut backend,
374 );
375 assert!(backend.visibility.is_empty());
376 }
377
378 #[test]
379 fn jump_to_unknown_scene_is_no_op() {
380 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
381 let mut backend = TestBackend::default();
382 jump_to_scene(
383 &mut opt,
384 &SceneVisibility::default(),
385 0.0,
386 AssetId(99),
387 "Cut",
388 &mut backend,
389 );
390 assert_eq!(opt.as_ref().unwrap().current, AssetId(0));
391 assert!(backend.visibility.is_empty());
392 }
393
394 #[test]
395 fn jump_to_scene_same_scene_is_no_op() {
396 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
397 let mut backend = TestBackend::default();
398 jump_to_scene(
399 &mut opt,
400 &SceneVisibility::default(),
401 0.0,
402 AssetId(0),
403 "Cut",
404 &mut backend,
405 );
406 assert_eq!(opt.as_ref().unwrap().current, AssetId(0));
407 assert!(backend.visibility.is_empty());
408 }
409
410 #[test]
411 fn jump_to_scene_cut_switches_immediately() {
412 let visibility = vis(&[(&[0], Some(AssetId(0))), (&[1], Some(AssetId(1)))]);
413 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
414 let mut backend = TestBackend::default();
415 jump_to_scene(&mut opt, &visibility, 1.0, AssetId(1), "Cut", &mut backend);
416 assert_eq!(opt.as_ref().unwrap().current, AssetId(1));
417 assert!(matches!(opt.as_ref().unwrap().fade, FadePhase::None));
418 assert!(backend.visibility.contains(&(1, true)));
419 }
420
421 #[test]
422 fn jump_to_scene_fade_black_starts_to_black_phase() {
423 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
424 let mut backend = TestBackend::default();
425 jump_to_scene(
426 &mut opt,
427 &SceneVisibility::default(),
428 5.0,
429 AssetId(1),
430 "FadeBlack",
431 &mut backend,
432 );
433 assert_eq!(opt.as_ref().unwrap().current, AssetId(0));
435 assert!(matches!(
436 opt.as_ref().unwrap().fade,
437 FadePhase::ToBlack { started_at, next } if (started_at - 5.0).abs() < 1e-6 && next == AssetId(1)
438 ));
439 }
440}