Skip to main content

shuyi_ui/frame/onebit/
roles.rs

1//! A skin's one-bit look by role (ADR-0008 §4): [`PatternMap`] draws each
2//! colour of the palette in its role's patterns, [`to_one_bit`] draws a
3//! frame with it into a [`OneBitFrame`], and [`changed_rows`] says which
4//! rows of the screen two frames differ in, as the Playdate sends only
5//! those to its display. Integers only, so a frame is the same bits on
6//! every platform.
7
8use core::fmt;
9
10use alloc::collections::BTreeMap;
11
12use shuyi_assets::onebit::{PatternMapDef, dither};
13
14use super::{FRAME_BYTES, Font, Frame, HEIGHT, Images, ROW_BYTES, Shade, Shades, WIDTH};
15use crate::frame::color::Rgb;
16use crate::frame::glyph_buffer::GlyphBuffer;
17use crate::prelude::*;
18
19/// A colour's luminance, 0 (black) to 255 (white), in integer Rec. 601
20/// weights: `(77 r + 150 g + 29 b) >> 8`.
21#[allow(clippy::cast_possible_truncation)] // at most 255 × 256 >> 8
22pub const fn luminance(rgb: Rgb) -> u8 {
23    ((77 * rgb.r as u32 + 150 * rgb.g as u32 + 29 * rgb.b as u32) >> 8) as u8
24}
25
26/// A role's patterns, as the renderer draws them.
27#[derive(Clone, Debug, PartialEq, Eq)]
28struct Role {
29    name: String,
30    fg: Shade,
31    bg: Shade,
32}
33
34/// What each colour of a skin's palette is drawn in on a one-bit target:
35/// the patterns of its role, by the skin's pattern map. A colour that is
36/// no role's is dithered by its [`luminance`] (Bayer 8 × 8, as
37/// [`dither`] does); a glyph in it is white or black, whichever is
38/// nearer, as text is never dithered.
39#[derive(Clone, Debug, Default, PartialEq, Eq)]
40pub struct PatternMap {
41    /// Each colour's role, with its patterns.
42    roles: BTreeMap<Rgb, Role>,
43}
44
45impl PatternMap {
46    /// The map of the roles of `palette` (name and colour, as
47    /// [`Palette::iter`](crate::frame::Palette::iter) gives them) by
48    /// pattern map `map`. A role `map` lacks has no patterns, so its
49    /// colour is drawn as a colour of no role. When roles share a colour,
50    /// the first that `palette` gives with patterns is its role (the
51    /// frame holds colours, so it can't tell them apart:
52    /// `shuyi_assets::onebit::check` says so).
53    pub fn new<'a>(palette: impl IntoIterator<Item = (&'a str, Rgb)>, map: &PatternMapDef) -> Self {
54        let mut roles = BTreeMap::new();
55        for (name, rgb) in palette {
56            if let Some(patterns) = map.get(name) {
57                roles.entry(rgb).or_insert_with(|| Role {
58                    name: name.to_owned(),
59                    fg: patterns.fg.into(),
60                    bg: patterns.background().into(),
61                });
62            }
63        }
64        Self { roles }
65    }
66
67    /// The role `rgb` is drawn as, if it is one's.
68    pub fn role_of(&self, rgb: Rgb) -> Option<&str> {
69        self.roles.get(&rgb).map(|role| role.name.as_str())
70    }
71
72    /// `rgb` dithered by its luminance.
73    fn dithered(rgb: Rgb) -> Shade {
74        Shade::rows(dither(255 - luminance(rgb)))
75    }
76}
77
78impl Shades for PatternMap {
79    /// The role's background, or the colour dithered.
80    fn ground(&self, rgb: Rgb) -> Shade {
81        self.roles
82            .get(&rgb)
83            .map_or_else(|| Self::dithered(rgb), |role| role.bg)
84    }
85
86    /// The role's foreground, or solid white or black by the colour's
87    /// luminance.
88    fn glyph(&self, rgb: Rgb) -> Shade {
89        self.roles.get(&rgb).map_or_else(
90            || {
91                let solid = if luminance(rgb) >= 128 { u8::MAX } else { 0 };
92                Shade::rows([solid; 8])
93            },
94            |role| role.fg,
95        )
96    }
97
98    /// The role's foreground, or the colour dithered.
99    fn shape(&self, rgb: Rgb) -> Shade {
100        self.roles
101            .get(&rgb)
102            .map_or_else(|| Self::dithered(rgb), |role| role.fg)
103    }
104}
105
106/// A one-bit picture of the display, 400 × 240, in the Playdate's layout:
107/// each row [`ROW_BYTES`] bytes (padded to a whole number of 32-bit words,
108/// as its display takes them), a byte's highest bit its left pixel, a set
109/// bit white.
110#[derive(Clone, PartialEq, Eq)]
111pub struct OneBitFrame {
112    bytes: Box<Frame>,
113}
114
115impl OneBitFrame {
116    /// The display, all white.
117    pub fn white() -> Self {
118        Self {
119            bytes: Box::new([u8::MAX; FRAME_BYTES]),
120        }
121    }
122
123    /// Its width in pixels.
124    pub const fn width(&self) -> i32 {
125        WIDTH
126    }
127
128    /// Its height in pixels.
129    pub const fn height(&self) -> i32 {
130        HEIGHT
131    }
132
133    /// Its bytes, row after row.
134    pub fn bytes(&self) -> &Frame {
135        &self.bytes
136    }
137
138    /// Its bytes, to draw in.
139    pub fn bytes_mut(&mut self) -> &mut Frame {
140        &mut self.bytes
141    }
142
143    /// Whether pixel `(x, y)` is white; off the display, `false`.
144    pub fn is_white(&self, x: i32, y: i32) -> bool {
145        super::is_white(&self.bytes, x, y)
146    }
147
148    /// The rows it differs from `previous` in.
149    pub fn changed_rows(&self, previous: &Self) -> RowMask {
150        changed_rows(&previous.bytes, &self.bytes)
151    }
152}
153
154impl fmt::Debug for OneBitFrame {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "OneBitFrame({WIDTH} × {HEIGHT})")
157    }
158}
159
160/// Draws `frame` as a one-bit target shows it: its colours in their roles'
161/// patterns by `map`, glyphs from `font`, sprites' one-bit pictures from
162/// `images` ([`render_with`](super::render_with)). The frame is drawn
163/// from the display's top-left corner, and what lies past 400 × 240
164/// pixels is cut off.
165pub fn to_one_bit(
166    frame: &GlyphBuffer,
167    map: &PatternMap,
168    font: &Font<'_>,
169    images: &impl Images,
170) -> OneBitFrame {
171    let mut out = OneBitFrame::white();
172    super::render_with(frame, font, images, map, out.bytes_mut());
173    out
174}
175
176/// A set of the display's rows, a bit each.
177#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
178pub struct RowMask([u32; RowMask::WORDS]);
179
180impl RowMask {
181    /// Words of 32 rows, enough for the display's.
182    const WORDS: usize = (HEIGHT as usize).div_ceil(32);
183
184    /// Adds row `y` (a row off the display adds nothing).
185    #[allow(clippy::cast_sign_loss)] // checked first
186    pub fn insert(&mut self, y: i32) {
187        if (0..HEIGHT).contains(&y) {
188            self.0[y as usize / 32] |= 1 << (y % 32);
189        }
190    }
191
192    /// Whether it holds row `y`.
193    #[allow(clippy::cast_sign_loss)] // checked first
194    pub fn contains(&self, y: i32) -> bool {
195        (0..HEIGHT).contains(&y) && self.0[y as usize / 32] >> (y % 32) & 1 == 1
196    }
197
198    /// How many rows it holds.
199    pub fn count(&self) -> u32 {
200        self.0.iter().map(|word| word.count_ones()).sum()
201    }
202
203    /// Whether it holds none.
204    pub fn is_empty(&self) -> bool {
205        self.0.iter().all(|&word| word == 0)
206    }
207
208    /// The rows it holds, top to bottom.
209    pub fn rows(&self) -> impl Iterator<Item = i32> + '_ {
210        (0..HEIGHT).filter(|&y| self.contains(y))
211    }
212}
213
214/// The rows `new` differs from `old` in.
215pub fn changed_rows(old: &Frame, new: &Frame) -> RowMask {
216    let mut mask = RowMask::default();
217    let rows = old.as_chunks::<ROW_BYTES>().0.iter().zip(new.as_chunks().0);
218    for (y, (a, b)) in (0..).zip(rows) {
219        if a != b {
220            mask.insert(y);
221        }
222    }
223    mask
224}
225
226#[cfg(test)]
227mod tests;