shuyi_ui/cinematic.rs
1//! Plays a cinematic (`shuyi_assets::cinematic`, ticket 1113; the game's
2//! game ADR-0060): a [`Timeline`] says which shot a time falls in and how far
3//! through it, and a [`CinematicPlayer`] has the shot draw itself there.
4//! What a shot shows is the game's: its shot kinds implement
5//! [`ShotPainter`].
6//!
7//! **Every frame is drawn from the time alone**: nothing is carried from
8//! one frame to the next, and no shot may keep state that an update
9//! advances; whatever moves in a shot is worked out from the time. So
10//! whoever owns the clock can follow the music, jump or loop, and a test
11//! can draw any moment. The player has no clock of its own.
12//!
13//! The engine also gives what most shots move by: an [`ease_in_out`] (and
14//! every curve of `shuyi_core::ease`, through
15//! [`ease_f32`](crate::ease::ease_f32)), and
16//! a [`Pan`] over a picture shown as the frame's backdrop (game ADR-0048,
17//! [`cinema::view`]), its frame ([`PanFrame`]) and its paint
18//! ([`paint_pan`]).
19
20use crate::prelude::*;
21use alloc::rc::Rc;
22
23use shuyi_assets::cinematic::CinematicDef;
24
25use crate::ctx::{Ctx, GameData};
26use crate::frame::cinema;
27use crate::frame::color::{EngineColor, Palette};
28use crate::frame::console::ConsoleSpec;
29use crate::frame::glyph_buffer::{Cell, GlyphBuffer, Rect};
30
31/// Where a time falls in a [`Timeline`].
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct ShotAt {
34 /// Which shot, counted from 0.
35 pub index: usize,
36 /// How far through the shot: 0 at its start, up to (never reaching) 1
37 /// at the next shot's start.
38 pub progress: f32,
39}
40
41/// Everything a shot is told about a moment of its cinematic.
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct Moment {
44 /// Which shot, counted from 0.
45 pub index: usize,
46 /// How far through the shot, as [`ShotAt::progress`].
47 pub progress: f32,
48 /// Milliseconds since the shot started.
49 pub into_ms: u32,
50 /// How long the shot lasts, in milliseconds ([`Timeline::shot_ms`]).
51 pub shot_ms: u32,
52 /// Milliseconds since the cinematic started, wrapped at its length.
53 pub t_ms: u32,
54}
55
56/// When each shot of a cinematic starts, and when the cinematic ends.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct Timeline {
59 /// Each shot's start, in milliseconds, in order.
60 starts_ms: Vec<u32>,
61 /// The length, in milliseconds; at least 1.
62 length_ms: u32,
63}
64
65impl Timeline {
66 /// A timeline of shots starting at `starts_ms` (in order, the first at
67 /// 0, as a loaded cinematic has them) over `length_ms` (0 counts as 1).
68 pub fn new(starts_ms: Vec<u32>, length_ms: u32) -> Self {
69 Self {
70 starts_ms,
71 length_ms: length_ms.max(1),
72 }
73 }
74
75 /// The timeline of `def`'s shots.
76 pub fn of<S>(def: &CinematicDef<S>) -> Self {
77 Self::new(def.shots.iter().map(|s| s.at_ms).collect(), def.length_ms)
78 }
79
80 /// The length, in milliseconds.
81 pub fn length_ms(&self) -> u32 {
82 self.length_ms
83 }
84
85 /// When the last shot starts, in milliseconds (0 with no shots).
86 pub fn last_start_ms(&self) -> u32 {
87 self.starts_ms.last().copied().unwrap_or(0)
88 }
89
90 /// Whether `t_ms` (wrapped at the length) falls in the last shot.
91 pub fn in_last_shot(&self, t_ms: u64) -> bool {
92 self.wrapped_ms(t_ms) >= self.last_start_ms()
93 }
94
95 /// How long shot `index` lasts, in milliseconds: until the next one
96 /// starts, the last until the end. 0 for a shot there isn't.
97 pub fn shot_ms(&self, index: usize) -> u32 {
98 let Some(&start) = self.starts_ms.get(index) else {
99 return 0;
100 };
101 let end = self.starts_ms.get(index + 1).copied();
102 end.unwrap_or(self.length_ms).saturating_sub(start)
103 }
104
105 /// `t_ms` wrapped at the length: the cinematic loops.
106 pub fn wrapped_ms(&self, t_ms: u64) -> u32 {
107 u32::try_from(t_ms % u64::from(self.length_ms)).unwrap_or(0)
108 }
109
110 /// The shot `t_ms` milliseconds after the start falls in, wrapped at
111 /// the length. A shot lasts until the next one starts, the last until
112 /// the end; time before the first shot's start is the first shot's.
113 pub fn at_ms(&self, t_ms: u64) -> ShotAt {
114 let (index, into) = self.into_shot(t_ms);
115 let start = self.wrapped_ms(t_ms) - into;
116 let end = self.starts_ms.get(index + 1).map_or(self.length_ms, |&s| s);
117 let span = end.saturating_sub(start).max(1);
118 #[allow(clippy::cast_precision_loss)] // a shot is far below 2^24 ms
119 let progress = (into as f32 / span as f32).clamp(0.0, 1.0);
120 ShotAt { index, progress }
121 }
122
123 /// The shot `t_ms` falls in ([`at_ms`](Self::at_ms)), counted from 0,
124 /// and how many milliseconds into it that is.
125 pub fn into_shot(&self, t_ms: u64) -> (usize, u32) {
126 let t = self.wrapped_ms(t_ms);
127 let index = self.starts_ms.iter().rposition(|&s| s <= t).unwrap_or(0);
128 let start = self.starts_ms.get(index).map_or(0, |&s| s.min(t));
129 (index, t - start)
130 }
131
132 /// [`at_ms`](Self::at_ms) for `t` seconds ([`millis`]).
133 pub fn at(&self, t: f32) -> ShotAt {
134 self.at_ms(millis(t))
135 }
136
137 /// Everything about the moment `t_ms` milliseconds after the start
138 /// (wrapped at the length) that a shot is told.
139 pub fn moment(&self, t_ms: u64) -> Moment {
140 let ShotAt { index, progress } = self.at_ms(t_ms);
141 Moment {
142 index,
143 progress,
144 into_ms: self.into_shot(t_ms).1,
145 shot_ms: self.shot_ms(index),
146 t_ms: self.wrapped_ms(t_ms),
147 }
148 }
149}
150
151/// `t` seconds in whole milliseconds. A time below 0, or one that isn't a
152/// number, counts as 0.
153pub fn millis(t: f32) -> u64 {
154 let ms = (f64::from(t) * 1000.0).round();
155 // The cast stops at 0 and at the biggest number by itself.
156 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
157 if ms.is_finite() { ms as u64 } else { 0 }
158}
159
160/// `p` (0 to 1; clamped, not a number counts as 0) eased in and out: slow
161/// at both ends, fastest in the middle. It is smoothstep, the curve of
162/// [`Ease::Smooth`](shuyi_core::Ease::Smooth), in `f32`; through
163/// [`ease_f32`](crate::ease::ease_f32) a skin has every other curve too.
164pub fn ease_in_out(p: f32) -> f32 {
165 let p = if p.is_finite() {
166 p.clamp(0.0, 1.0)
167 } else {
168 0.0
169 };
170 p * p * (3.0 - 2.0 * p)
171}
172
173/// A pan over a picture shown as the frame's backdrop (game ADR-0048): the
174/// picture's pixel `from` in the middle of the screen when the shot
175/// starts, `to` when it ends, eased in and out ([`ease_in_out`]), at one
176/// whole zoom for the whole shot.
177#[derive(Debug, Clone, Copy, PartialEq)]
178pub struct Pan {
179 /// The picture's pixel in the middle of the screen at the start.
180 pub from: (f32, f32),
181 /// The picture's pixel in the middle of the screen at the end.
182 pub to: (f32, f32),
183 /// Screen pixels per picture pixel.
184 pub zoom: u8,
185}
186
187impl Pan {
188 /// The picture's pixel in the middle of the screen `progress` (0 to 1)
189 /// through the shot.
190 pub fn centre(&self, progress: f32) -> (f32, f32) {
191 let eased = ease_in_out(progress);
192 let (from, to) = (self.from, self.to);
193 (
194 from.0 + (to.0 - from.0) * eased,
195 from.1 + (to.1 - from.1) * eased,
196 )
197 }
198
199 /// The picture's pixel at the top-left of a screen of `screen`'s cells
200 /// of `console` ([`cinema::view`]), `progress` through the shot, over a
201 /// picture `picture_px` big: the backdrop's `origin_px`.
202 pub fn origin_px(
203 &self,
204 console: ConsoleSpec,
205 picture_px: (u32, u32),
206 screen: Rect,
207 progress: f32,
208 ) -> (f32, f32) {
209 let px = |cells: i32, cell: u16| u32::try_from(cells).unwrap_or(0) * u32::from(cell);
210 let clip = (px(screen.w, console.cell_w), px(screen.h, console.cell_h));
211 cinema::view(picture_px, clip, self.centre(progress), self.zoom)
212 }
213
214 /// What the pan shows `progress` through the shot on a screen of
215 /// `screen`'s cells of `console`, over a picture `picture_px` big
216 /// ([`origin_px`](Self::origin_px)).
217 pub fn frame(
218 &self,
219 console: ConsoleSpec,
220 picture_px: (u32, u32),
221 screen: Rect,
222 progress: f32,
223 ) -> PanFrame {
224 PanFrame {
225 origin_px: self.origin_px(console, picture_px, screen, progress),
226 zoom: self.zoom,
227 }
228 }
229}
230
231/// A frame of a [`Pan`]: where its picture is behind the screen, as plain
232/// data.
233#[derive(Debug, Clone, Copy, PartialEq)]
234pub struct PanFrame {
235 /// The picture's pixel at the screen's top-left ([`Pan::origin_px`]).
236 pub origin_px: (f32, f32),
237 /// Screen pixels per picture pixel.
238 pub zoom: u8,
239}
240
241/// Paints `frame` over the whole of `buf`: `picture` (a whole map, say:
242/// [`MapPicture`](crate::grid::MapPicture)) as its backdrop, behind cells
243/// that are all holes, in `palette`'s text color.
244pub fn paint_pan(
245 palette: &Palette,
246 picture: Rc<GlyphBuffer>,
247 frame: PanFrame,
248 buf: &mut GlyphBuffer,
249) {
250 let hole = Cell::see_through(' ', palette.engine(EngineColor::Text));
251 buf.fill_rect(buf.bounds(), hole);
252 buf.set_backdrop(picture, buf.bounds(), frame.origin_px, frame.zoom);
253}
254
255/// A game's shot, built once to be drawn at any moment: one of its shot
256/// kinds with whatever it needs ready (a picture painted, a text laid
257/// out). Never changed by drawing.
258pub trait ShotPainter {
259 /// The game it is a shot of.
260 type Game: GameData;
261 /// What every shot of a player is drawn with, from whoever plays it
262 /// (`()` for nothing): the backdrop a title picked, say.
263 type Shared;
264 /// What a frame of the shot shows, as plain data: for tests of what
265 /// plays, and for [`paint`](Self::paint).
266 type Frame;
267
268 /// What the shot shows at `moment`, on a screen of `screen` cells.
269 fn frame(
270 &self,
271 ctx: &Ctx<Self::Game>,
272 shared: &Self::Shared,
273 moment: Moment,
274 screen: Rect,
275 ) -> Self::Frame;
276
277 /// Paints `frame` (the shot's frame at `moment`) over the whole of
278 /// `buf`, which is cleared to the background.
279 fn paint(
280 &self,
281 ctx: &Ctx<Self::Game>,
282 shared: &Self::Shared,
283 moment: Moment,
284 frame: Self::Frame,
285 buf: &mut GlyphBuffer,
286 );
287}
288
289/// Draws a cinematic of shots `S` at any time.
290#[derive(Debug, Clone, PartialEq)]
291pub struct CinematicPlayer<S> {
292 timeline: Timeline,
293 shots: Vec<S>,
294}
295
296impl<S> CinematicPlayer<S> {
297 /// A player of `shots` (one for each start of `timeline`).
298 pub fn new(timeline: Timeline, shots: Vec<S>) -> Self {
299 Self { timeline, shots }
300 }
301
302 /// A player of the cinematic `def`, each shot built once by `build`,
303 /// in order.
304 pub fn of<'d, D>(def: &'d CinematicDef<D>, build: impl FnMut(&'d D) -> S) -> Self {
305 let shots = def.shots.iter().map(|s| &s.shot).map(build).collect();
306 Self::new(Timeline::of(def), shots)
307 }
308
309 /// When its shots start and how long it is.
310 pub fn timeline(&self) -> &Timeline {
311 &self.timeline
312 }
313
314 /// Shot `index`, counted from 0.
315 pub fn shot(&self, index: usize) -> Option<&S> {
316 self.shots.get(index)
317 }
318
319 /// The moment `t` seconds after the start (wrapped at the length).
320 pub fn moment(&self, t: f32) -> Moment {
321 self.timeline.moment(millis(t))
322 }
323
324 /// The shot shown `t` seconds after the start (wrapped at the length),
325 /// and the moment of it; `None` with no shots.
326 pub fn at(&self, t: f32) -> Option<(&S, Moment)> {
327 let moment = self.moment(t);
328 self.shots.get(moment.index).map(|shot| (shot, moment))
329 }
330}
331
332impl<S: ShotPainter> CinematicPlayer<S> {
333 /// What is shown `t` seconds after the start (wrapped at the length),
334 /// on a screen of `screen` cells; `None` with no shots.
335 pub fn frame(
336 &self,
337 ctx: &Ctx<S::Game>,
338 shared: &S::Shared,
339 t: f32,
340 screen: Rect,
341 ) -> Option<S::Frame> {
342 let (shot, moment) = self.at(t)?;
343 Some(shot.frame(ctx, shared, moment, screen))
344 }
345
346 /// Draws the cinematic as it is `t` seconds after its start (wrapped
347 /// at the length) over the whole of `buf`, at `brightness` (1 = as it
348 /// is, 0 = black: [`GlyphBuffer::fade`]). The same `t` always draws
349 /// the same frame.
350 pub fn draw(
351 &self,
352 ctx: &Ctx<S::Game>,
353 shared: &S::Shared,
354 t: f32,
355 brightness: f32,
356 buf: &mut GlyphBuffer,
357 ) {
358 let c = |e| ctx.palette.engine(e);
359 let blank = Cell::new(' ', c(EngineColor::Text), c(EngineColor::Background));
360 buf.fill_rect(buf.bounds(), blank);
361 if let Some((shot, moment)) = self.at(t) {
362 let frame = shot.frame(ctx, shared, moment, buf.bounds());
363 shot.paint(ctx, shared, moment, frame, buf);
364 }
365 buf.fade(brightness);
366 }
367}
368
369#[cfg(test)]
370mod tests;