Skip to main content

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}