shuyi_ui/frame/glyph_buffer/backdrop.rs
1//! A buffer's [`Backdrop`] (game ADR-0048): a second glyph picture, the *scene*,
2//! seen through a window of console cells at a pixel offset and a whole
3//! zoom. `app` draws it behind the cells marked
4//! [`Cell::see_through`](super::Cell::see_through); every other cell draws
5//! over it as usual.
6
7#[cfg(not(feature = "std"))]
8use crate::float::Float;
9use alloc::rc::Rc;
10
11use super::{GlyphBuffer, PxRect, Rect};
12use crate::frame::console::ConsoleSpec;
13
14/// The biggest zoom: whole steps 1× to 4× only
15/// (`docs/design/title-screen.md`, *Intro cinematic*).
16pub const MAX_ZOOM: u8 = 4;
17
18/// A scene shown through a window of a [`GlyphBuffer`]'s cells.
19#[derive(Clone, PartialEq, Debug)]
20pub struct Backdrop {
21 scene: Rc<GlyphBuffer>,
22 /// The console of the buffer it is shown in, whose cells `clip` counts.
23 console: ConsoleSpec,
24 clip: Rect,
25 origin_px: (f32, f32),
26 zoom: u8,
27}
28
29impl Backdrop {
30 /// `scene` behind the cells of `clip` of a `console` buffer. A zoom
31 /// outside 1 to [`MAX_ZOOM`] is clamped; an origin that isn't a number
32 /// counts as 0.
33 pub(super) fn new(
34 scene: Rc<GlyphBuffer>,
35 console: ConsoleSpec,
36 clip: Rect,
37 origin_px: (f32, f32),
38 zoom: u8,
39 ) -> Self {
40 let finite = |v: f32| if v.is_finite() { v } else { 0.0 };
41 Self {
42 scene,
43 console,
44 clip,
45 origin_px: (finite(origin_px.0), finite(origin_px.1)),
46 zoom: zoom.clamp(1, MAX_ZOOM),
47 }
48 }
49
50 /// Shows a copy of the scene [faded](GlyphBuffer::fade) to
51 /// `brightness` instead. The scene itself, which others may share, is
52 /// not touched.
53 pub(super) fn fade(&mut self, brightness: f32) {
54 let mut scene = (*self.scene).clone();
55 scene.fade(brightness);
56 self.scene = Rc::new(scene);
57 }
58
59 /// The picture shown: its cells, rectangles and sprites.
60 pub fn scene(&self) -> &GlyphBuffer {
61 &self.scene
62 }
63
64 /// The window, in console cells; inside the buffer.
65 pub fn clip(&self) -> Rect {
66 self.clip
67 }
68
69 /// The window in console pixels.
70 pub fn clip_px(&self) -> PxRect {
71 let (cw, ch) = self.console.cell_px();
72 let Rect { x, y, w, h } = self.clip;
73 Rect::new(x * cw, y * ch, w * cw, h * ch)
74 }
75
76 /// The scene pixel at the window's top-left. Fractions pan smoothly;
77 /// it may lie outside the scene (the clear colour shows there).
78 pub fn origin_px(&self) -> (f32, f32) {
79 self.origin_px
80 }
81
82 /// Console pixels per scene pixel: 1 to [`MAX_ZOOM`].
83 pub fn zoom(&self) -> u8 {
84 self.zoom
85 }
86
87 /// Where the scene's top-left pixel is drawn, in window pixels right
88 /// of and below the window's top-left, when a console pixel is `scale`
89 /// window pixels: the origin times `zoom × scale`, rounded to whole
90 /// window pixels so that glyphs stay sharp. Every renderer places the
91 /// scene with this, and draws a scene pixel `zoom × scale` pixels big.
92 pub fn scene_offset(&self, scale: u32) -> (i64, i64) {
93 #[allow(clippy::cast_precision_loss)] // zoom × scale is small
94 let per_px = (u32::from(self.zoom) * scale) as f32;
95 // Saturating: an origin far outside any scene shows nothing.
96 #[allow(clippy::cast_possible_truncation)]
97 let whole = |v: f32| (-v * per_px).round() as i64;
98 (whole(self.origin_px.0), whole(self.origin_px.1))
99 }
100}