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 tracing::debug!("SceneFlow: switched to scene {}", next);
139 }
140 }
141 FadePhase::FromBlack { started_at } => {
142 let t = ((elapsed - started_at) / FADE_HALF_SECS).clamp(0.0, 1.0);
143 backend.set_fade(1.0 - t);
144 if t >= 1.0 {
145 flow.fade = FadePhase::None;
146 }
147 }
148 FadePhase::None => {}
149 }
150}
151
152pub fn jump_to_scene<B: SceneControl + ?Sized>(
155 flow_opt: &mut Option<SceneFlow>,
156 visibility: &SceneVisibility,
157 elapsed: f32,
158 target_scene: AssetId,
159 transition: &str,
160 backend: &mut B,
161) {
162 let flow = match flow_opt {
163 Some(f) => f,
164 None => {
165 tracing::warn!(
166 "SceneCommand: jump to {} ignored -- no Scene assets in world",
167 target_scene
168 );
169 return;
170 }
171 };
172
173 if !flow.scenes.contains(&target_scene) {
174 tracing::warn!(
175 "SceneCommand: jump to {} ignored -- no Scene with that name",
176 target_scene
177 );
178 return;
179 }
180
181 if target_scene == flow.current {
182 return;
183 }
184
185 match transition {
186 "FadeBlack" => {
187 flow.fade = FadePhase::ToBlack {
188 started_at: elapsed,
189 next: target_scene,
190 };
191 }
192 _ => {
193 flow.current = target_scene;
194 flow.fade = FadePhase::None;
195 set_scene_visibility(visibility, target_scene, backend);
196 tracing::debug!("SceneCommand: cut to scene {}", target_scene);
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 use alloc::vec;
206 #[derive(Default)]
208 struct TestBackend {
209 visibility: Vec<(usize, bool)>,
210 fades: Vec<f32>,
211 }
212
213 impl SceneControl for TestBackend {
214 fn update_visibility(&mut self, draw_idx: usize, visible: bool) {
215 self.visibility.push((draw_idx, visible));
216 }
217 fn set_fade(&mut self, fade: f32) {
218 self.fades.push(fade);
219 }
220 }
221
222 fn make_flow(scenes: &[AssetId]) -> SceneFlow {
223 SceneFlow {
224 scenes: scenes.to_vec(),
225 current: scenes[0],
226 fade: FadePhase::None,
227 }
228 }
229
230 fn vis(props: &[(&[usize], Option<AssetId>)]) -> SceneVisibility {
231 let mut v = SceneVisibility::default();
232 for (draws, scene) in props {
233 v.begin_prop(*scene);
234 for &d in *draws {
235 v.push_draw(d);
236 }
237 }
238 v
239 }
240
241 #[test]
242 fn set_visibility_active_scene_visible_others_hidden() {
243 let visibility = vis(&[
245 (&[0], Some(AssetId(0))),
246 (&[1], None),
247 (&[2], Some(AssetId(1))),
248 ]);
249 let mut backend = TestBackend::default();
250 set_scene_visibility(&visibility, AssetId(0), &mut backend);
251
252 assert!(
253 backend.visibility.contains(&(0, true)),
254 "prop in 'a' should be visible"
255 );
256 assert!(
257 backend.visibility.contains(&(1, true)),
258 "scene-less prop always visible"
259 );
260 assert!(
261 backend.visibility.contains(&(2, false)),
262 "prop in 'b' should be hidden"
263 );
264 }
265
266 #[test]
267 fn set_visibility_no_scene_always_visible_regardless_of_active() {
268 let visibility = vis(&[(&[0], None)]);
269 let mut backend = TestBackend::default();
270 set_scene_visibility(&visibility, AssetId(99), &mut backend);
271 assert_eq!(backend.visibility, vec![(0, true)]);
272 }
273
274 #[test]
275 fn tick_without_fade_is_a_no_op() {
276 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
277 let mut backend = TestBackend::default();
278 tick_transitions(&mut opt, &SceneVisibility::default(), 999.0, &mut backend);
279 assert!(backend.visibility.is_empty());
280 assert!(backend.fades.is_empty());
281 assert_eq!(opt.as_ref().unwrap().current, AssetId(0));
282 }
283
284 #[test]
285 fn tick_fade_to_black_ramps_the_fade_up() {
286 let mut flow = make_flow(&[AssetId(0), AssetId(1)]);
287 flow.fade = FadePhase::ToBlack {
288 started_at: 0.0,
289 next: AssetId(1),
290 };
291 let mut opt = Some(flow);
292 let mut backend = TestBackend::default();
293 tick_transitions(
295 &mut opt,
296 &SceneVisibility::default(),
297 FADE_HALF_SECS * 0.5,
298 &mut backend,
299 );
300 assert_eq!(backend.fades.len(), 1);
301 assert!(
302 (backend.fades[0] - 0.5).abs() < 1e-5,
303 "half way to black at the midpoint of the first half"
304 );
305 assert!(matches!(
307 opt.as_ref().unwrap().fade,
308 FadePhase::ToBlack { .. }
309 ));
310 }
311
312 #[test]
313 fn tick_fade_to_black_completes_and_enters_from_black() {
314 let mut flow = make_flow(&[AssetId(0), AssetId(1)]);
315 flow.fade = FadePhase::ToBlack {
316 started_at: 0.0,
317 next: AssetId(1),
318 };
319 let mut opt = Some(flow);
320 let mut backend = TestBackend::default();
321 tick_transitions(
323 &mut opt,
324 &SceneVisibility::default(),
325 FADE_HALF_SECS,
326 &mut backend,
327 );
328 let f = opt.as_ref().unwrap();
329 assert_eq!(f.current, AssetId(1));
330 assert!(matches!(f.fade, FadePhase::FromBlack { .. }));
331 }
332
333 #[test]
334 fn tick_fade_from_black_clears_the_fade() {
335 let mut flow = make_flow(&[AssetId(0)]);
336 flow.fade = FadePhase::FromBlack { started_at: 0.0 };
337 let mut opt = Some(flow);
338 let mut backend = TestBackend::default();
339 tick_transitions(
341 &mut opt,
342 &SceneVisibility::default(),
343 FADE_HALF_SECS,
344 &mut backend,
345 );
346 assert!(matches!(opt.as_ref().unwrap().fade, FadePhase::None));
347 assert_eq!(*backend.fades.last().unwrap(), 0.0);
349 }
350
351 #[test]
354 fn tick_fade_from_black_ramps_the_fade_down() {
355 let mut flow = make_flow(&[AssetId(0)]);
356 flow.fade = FadePhase::FromBlack { started_at: 0.0 };
357 let mut opt = Some(flow);
358 let mut backend = TestBackend::default();
359 tick_transitions(
360 &mut opt,
361 &SceneVisibility::default(),
362 FADE_HALF_SECS * 0.25,
363 &mut backend,
364 );
365 assert_eq!(backend.fades.len(), 1);
366 assert!((backend.fades[0] - 0.75).abs() < 1e-5);
367 assert!(matches!(
368 opt.as_ref().unwrap().fade,
369 FadePhase::FromBlack { .. }
370 ));
371 }
372
373 #[test]
374 fn jump_to_scene_no_flow_is_no_op() {
375 let mut opt: Option<SceneFlow> = None;
376 let mut backend = TestBackend::default();
377 jump_to_scene(
378 &mut opt,
379 &SceneVisibility::default(),
380 0.0,
381 AssetId(99),
382 "Cut",
383 &mut backend,
384 );
385 assert!(backend.visibility.is_empty());
386 }
387
388 #[test]
389 fn jump_to_unknown_scene_is_no_op() {
390 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
391 let mut backend = TestBackend::default();
392 jump_to_scene(
393 &mut opt,
394 &SceneVisibility::default(),
395 0.0,
396 AssetId(99),
397 "Cut",
398 &mut backend,
399 );
400 assert_eq!(opt.as_ref().unwrap().current, AssetId(0));
401 assert!(backend.visibility.is_empty());
402 }
403
404 #[test]
405 fn jump_to_scene_same_scene_is_no_op() {
406 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
407 let mut backend = TestBackend::default();
408 jump_to_scene(
409 &mut opt,
410 &SceneVisibility::default(),
411 0.0,
412 AssetId(0),
413 "Cut",
414 &mut backend,
415 );
416 assert_eq!(opt.as_ref().unwrap().current, AssetId(0));
417 assert!(backend.visibility.is_empty());
418 }
419
420 #[test]
421 fn jump_to_scene_cut_switches_immediately() {
422 let visibility = vis(&[(&[0], Some(AssetId(0))), (&[1], Some(AssetId(1)))]);
423 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
424 let mut backend = TestBackend::default();
425 jump_to_scene(&mut opt, &visibility, 1.0, AssetId(1), "Cut", &mut backend);
426 assert_eq!(opt.as_ref().unwrap().current, AssetId(1));
427 assert!(matches!(opt.as_ref().unwrap().fade, FadePhase::None));
428 assert!(backend.visibility.contains(&(1, true)));
429 }
430
431 #[test]
432 fn jump_to_scene_fade_black_starts_to_black_phase() {
433 let mut opt = Some(make_flow(&[AssetId(0), AssetId(1)]));
434 let mut backend = TestBackend::default();
435 jump_to_scene(
436 &mut opt,
437 &SceneVisibility::default(),
438 5.0,
439 AssetId(1),
440 "FadeBlack",
441 &mut backend,
442 );
443 assert_eq!(opt.as_ref().unwrap().current, AssetId(0));
445 assert!(matches!(
446 opt.as_ref().unwrap().fade,
447 FadePhase::ToBlack { started_at, next } if (started_at - 5.0).abs() < 1e-6 && next == AssetId(1)
448 ));
449 }
450}