Skip to main content

shuyi_assets/
onebit.rs

1//! One-bit output by role (ADR-0008 §4): a fixed set of 8 × 8 [`Pattern`]s,
2//! and the pattern map (`palette@onebit.ron`, beside the palette) that
3//! gives each of a skin's palette roles a foreground and a background
4//! pattern, with the groups of roles that tell things apart and must stay
5//! distinct. As a format (ADR-0007): [`PatternMapRon`]. [`check`] is what
6//! `shuyi check --target` checks of a one-bit target; the renderer that
7//! draws with the map is `shuyi_ui::frame::onebit`.
8//!
9//! ```ron
10//! (
11//!     roles: {
12//!         "text": (fg: Black, bg: White),
13//!         "team.player": (fg: White),
14//!         "team.enemy": (fg: Dither50),
15//!         "team.ally": (fg: Inverted(DiagonalUp)),
16//!     },
17//!     distinct: [["team.player", "team.enemy", "team.ally"]],
18//!     text: ["text"],
19//! )
20//! ```
21use crate::prelude::*;
22
23use alloc::collections::{BTreeMap, BTreeSet};
24use core::fmt;
25
26use serde::{Deserialize, Deserializer, Serialize, Serializer};
27
28use crate::error::ContentError;
29use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, text};
30use crate::palette::{PaletteDef, format_hex};
31use crate::ron::parse_ron_implicit_some;
32
33/// The Bayer 8 × 8 order, by row then column: the dithers' thresholds. A
34/// dither with `k` black pixels is black where the order is below `k`.
35pub const BAYER: [[u8; 8]; 8] = [
36    [0, 32, 8, 40, 2, 34, 10, 42],
37    [48, 16, 56, 24, 50, 18, 58, 26],
38    [12, 44, 4, 36, 14, 46, 6, 38],
39    [60, 28, 52, 20, 62, 30, 54, 22],
40    [3, 35, 11, 43, 1, 33, 9, 41],
41    [51, 19, 59, 27, 49, 17, 57, 25],
42    [15, 47, 7, 39, 13, 45, 5, 37],
43    [63, 31, 55, 23, 61, 29, 53, 21],
44];
45
46/// The eight rows of a pattern: a row's highest bit is its left pixel, a
47/// set bit white, as in the Playdate's frame buffer.
48pub type Rows = [u8; 8];
49
50/// Bayer ordered dither of a grey `darkness` from 0 (white) to 255
51/// (black): black where `4 × order + 2 < darkness`, so 64 is
52/// [`Base::Dither25`], 128 [`Base::Dither50`] and 192
53/// [`Base::Dither75`] exactly, and the levels between fall evenly.
54pub fn dither(darkness: u8) -> Rows {
55    // At most 4 × 63 + 2 = 254: no overflow.
56    rows_where(|x, y| 4 * BAYER[y][x] + 2 < darkness)
57}
58
59/// The rows of a pattern black where `black(x, y)` says (each 0 to 7).
60fn rows_where(black: impl Fn(usize, usize) -> bool) -> Rows {
61    let mut rows = [0; 8];
62    for (y, row) in rows.iter_mut().enumerate() {
63        for x in 0..8 {
64            if !black(x, y) {
65                *row |= 0x80 >> x;
66            }
67        }
68    }
69    rows
70}
71
72/// One of the fixed patterns, before any inversion: black and white
73/// solids, three ordered dithers, and four kinds of line.
74#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
75pub enum Base {
76    /// Every pixel black.
77    Black,
78    /// Every pixel white.
79    White,
80    /// A quarter of the pixels black, Bayer ordered.
81    Dither25,
82    /// Half the pixels black, Bayer ordered: a one-pixel checker.
83    Dither50,
84    /// Three quarters of the pixels black, Bayer ordered.
85    Dither75,
86    /// Black lines rising to the right, one pixel in four.
87    DiagonalUp,
88    /// Black lines falling to the right, one pixel in four.
89    DiagonalDown,
90    /// Black rows, one in four.
91    HLines,
92    /// Black columns, one in four.
93    VLines,
94    /// A checker of 2 × 2 squares.
95    Checker2,
96}
97
98impl Base {
99    /// Every base pattern.
100    pub const ALL: [Self; 10] = [
101        Self::Black,
102        Self::White,
103        Self::Dither25,
104        Self::Dither50,
105        Self::Dither75,
106        Self::DiagonalUp,
107        Self::DiagonalDown,
108        Self::HLines,
109        Self::VLines,
110        Self::Checker2,
111    ];
112
113    /// Its name in a pattern map.
114    pub const fn name(self) -> &'static str {
115        match self {
116            Self::Black => "Black",
117            Self::White => "White",
118            Self::Dither25 => "Dither25",
119            Self::Dither50 => "Dither50",
120            Self::Dither75 => "Dither75",
121            Self::DiagonalUp => "DiagonalUp",
122            Self::DiagonalDown => "DiagonalDown",
123            Self::HLines => "HLines",
124            Self::VLines => "VLines",
125            Self::Checker2 => "Checker2",
126        }
127    }
128
129    /// How many of its 64 pixels are black.
130    pub const fn black_pixels(self) -> u32 {
131        match self {
132            Self::Black => 64,
133            Self::White => 0,
134            Self::Dither25
135            | Self::DiagonalUp
136            | Self::DiagonalDown
137            | Self::HLines
138            | Self::VLines => 16,
139            Self::Dither50 | Self::Checker2 => 32,
140            Self::Dither75 => 48,
141        }
142    }
143
144    /// Whether its pixel `(x, y)` (each 0 to 7) is black.
145    const fn is_black(self, x: usize, y: usize) -> bool {
146        match self {
147            Self::Black => true,
148            Self::White => false,
149            Self::Dither25 => BAYER[y][x] < 16,
150            Self::Dither50 => BAYER[y][x] < 32,
151            Self::Dither75 => BAYER[y][x] < 48,
152            Self::DiagonalUp => (x + y).is_multiple_of(4),
153            Self::DiagonalDown => (x + 8 - y).is_multiple_of(4),
154            Self::HLines => y.is_multiple_of(4),
155            Self::VLines => x.is_multiple_of(4),
156            Self::Checker2 => (x / 2 + y / 2).is_multiple_of(2),
157        }
158    }
159
160    /// Its rows.
161    pub fn rows(self) -> Rows {
162        rows_where(|x, y| self.is_black(x, y))
163    }
164}
165
166/// A pattern of 8 × 8 pixels: one of the [`Base`] patterns, or one of
167/// them inverted. A pattern map spells it `Dither50` or
168/// `Inverted(Dither50)`.
169#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
170pub struct Pattern {
171    base: Base,
172    inverted: bool,
173}
174
175impl Pattern {
176    /// Every pixel black.
177    pub const BLACK: Self = Self::new(Base::Black);
178    /// Every pixel white.
179    pub const WHITE: Self = Self::new(Base::White);
180
181    /// `base`, as it is.
182    pub const fn new(base: Base) -> Self {
183        Self {
184            base,
185            inverted: false,
186        }
187    }
188
189    /// The same pattern with black and white swapped.
190    #[must_use]
191    pub const fn inverted(self) -> Self {
192        Self {
193            base: self.base,
194            inverted: !self.inverted,
195        }
196    }
197
198    /// The base pattern.
199    pub const fn base(self) -> Base {
200        self.base
201    }
202
203    /// Whether it is its base inverted.
204    pub const fn is_inverted(self) -> bool {
205        self.inverted
206    }
207
208    /// Its rows, fixed to the screen: screen pixel `(x, y)` takes bit
209    /// `x % 8` of row `y % 8`.
210    pub fn rows(self) -> Rows {
211        let rows = self.base.rows();
212        if self.inverted {
213            rows.map(|row| !row)
214        } else {
215            rows
216        }
217    }
218
219    /// How many of its 64 pixels are black.
220    pub const fn black_pixels(self) -> u32 {
221        if self.inverted {
222            64 - self.base.black_pixels()
223        } else {
224            self.base.black_pixels()
225        }
226    }
227
228    /// Whether it is solid black or solid white.
229    pub const fn is_solid(self) -> bool {
230        matches!(self.base, Base::Black | Base::White)
231    }
232
233    /// Whether the two look the same on a screen: one's pixels are the
234    /// other's moved (`Dither50` and `Inverted(Dither50)` are both a
235    /// one-pixel checker, one pixel apart).
236    pub fn looks_like(self, other: Self) -> bool {
237        let (a, b) = (self.rows(), other.rows());
238        (0..8).any(|dy| (0..8u32).any(|dx| (0..8).all(|y| a[y] == b[(y + dy) % 8].rotate_left(dx))))
239    }
240}
241
242impl From<Base> for Pattern {
243    fn from(base: Base) -> Self {
244        Self::new(base)
245    }
246}
247
248impl fmt::Display for Pattern {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        if self.inverted {
251            write!(f, "Inverted({})", self.base.name())
252        } else {
253            f.write_str(self.base.name())
254        }
255    }
256}
257
258/// How a pattern map spells a pattern: a base, or `Inverted(…)` of one.
259#[derive(Serialize, Deserialize)]
260#[serde(rename = "Pattern")]
261enum Spelled {
262    Black,
263    White,
264    Dither25,
265    Dither50,
266    Dither75,
267    DiagonalUp,
268    DiagonalDown,
269    HLines,
270    VLines,
271    Checker2,
272    Inverted(Box<Spelled>),
273}
274
275impl Spelled {
276    fn of(pattern: Pattern) -> Self {
277        let plain = match pattern.base {
278            Base::Black => Self::Black,
279            Base::White => Self::White,
280            Base::Dither25 => Self::Dither25,
281            Base::Dither50 => Self::Dither50,
282            Base::Dither75 => Self::Dither75,
283            Base::DiagonalUp => Self::DiagonalUp,
284            Base::DiagonalDown => Self::DiagonalDown,
285            Base::HLines => Self::HLines,
286            Base::VLines => Self::VLines,
287            Base::Checker2 => Self::Checker2,
288        };
289        if pattern.inverted {
290            Self::Inverted(Box::new(plain))
291        } else {
292            plain
293        }
294    }
295
296    /// The pattern; inverting twice gives the base back.
297    fn pattern(self) -> Pattern {
298        let base = match self {
299            Self::Inverted(inner) => return inner.pattern().inverted(),
300            Self::Black => Base::Black,
301            Self::White => Base::White,
302            Self::Dither25 => Base::Dither25,
303            Self::Dither50 => Base::Dither50,
304            Self::Dither75 => Base::Dither75,
305            Self::DiagonalUp => Base::DiagonalUp,
306            Self::DiagonalDown => Base::DiagonalDown,
307            Self::HLines => Base::HLines,
308            Self::VLines => Base::VLines,
309            Self::Checker2 => Base::Checker2,
310        };
311        Pattern::new(base)
312    }
313}
314
315impl Serialize for Pattern {
316    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
317        Spelled::of(*self).serialize(serializer)
318    }
319}
320
321impl<'de> Deserialize<'de> for Pattern {
322    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
323        Spelled::deserialize(deserializer).map(Spelled::pattern)
324    }
325}
326
327/// A role's two patterns: `fg` for what is drawn in its colour (a glyph, a
328/// rectangle, a sprite painted in it), `bg` for a cell's background in it
329/// (`fg` again, if the map doesn't say).
330#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
331#[serde(deny_unknown_fields)]
332pub struct RolePatterns {
333    /// What is drawn in the role's colour.
334    pub fg: Pattern,
335    /// A cell's background in the role's colour.
336    #[serde(default)]
337    pub bg: Option<Pattern>,
338}
339
340impl RolePatterns {
341    /// `fg`, with `bg` behind.
342    pub const fn new(fg: Pattern, bg: Pattern) -> Self {
343        Self { fg, bg: Some(bg) }
344    }
345
346    /// The background pattern: `bg`, or else `fg`.
347    pub fn background(&self) -> Pattern {
348        self.bg.unwrap_or(self.fg)
349    }
350}
351
352impl AssetKind for PatternMapDef {
353    const NAME: &'static str = "pattern map";
354}
355
356/// A pattern map: each palette role's patterns on a one-bit target, the
357/// groups of roles that must stay distinct, and the roles text is drawn
358/// in.
359#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
360#[serde(deny_unknown_fields)]
361pub struct PatternMapDef {
362    /// Each role's patterns, by the palette's name for it.
363    pub roles: BTreeMap<String, RolePatterns>,
364    /// Groups of roles that tell things apart (`["team.player",
365    /// "team.enemy"]`): no two of a group may have foregrounds that look
366    /// alike.
367    #[serde(default)]
368    pub distinct: Vec<Vec<String>>,
369    /// The roles text is drawn in, which should be black on white or white
370    /// on black: text is never dithered.
371    #[serde(default)]
372    pub text: Vec<String>,
373}
374
375impl PatternMapDef {
376    /// The patterns of `role`, if the map has them.
377    pub fn get(&self, role: &str) -> Option<&RolePatterns> {
378        self.roles.get(role)
379    }
380
381    /// Parses pattern map `source`, attributing errors to `file`. Reports
382    /// every distinct group of fewer than two roles or with a role twice
383    /// in it. A map is checked against its palette by [`check`].
384    pub fn from_source(file: &str, source: &str) -> Result<Self, Vec<ContentError>> {
385        let map: Self = parse_ron_implicit_some(file, source).map_err(|e| vec![e])?;
386        let mut errors = Vec::new();
387        for (i, group) in map.distinct.iter().enumerate() {
388            let shown = group.join(", ");
389            if group.len() < 2 {
390                errors.push(ContentError::new(
391                    file,
392                    format!("distinct group {} [{shown}] needs two roles or more", i + 1),
393                ));
394            }
395            let mut seen = BTreeSet::new();
396            for role in group.iter().filter(|role| !seen.insert(role.as_str())) {
397                errors.push(ContentError::new(
398                    file,
399                    format!("distinct group {} names \"{role}\" twice", i + 1),
400                ));
401            }
402        }
403        if errors.is_empty() {
404            Ok(map)
405        } else {
406            Err(errors)
407        }
408    }
409}
410
411/// A pattern map file (`pattern-map`, `.ron`), read as
412/// [`PatternMapDef::from_source`] says.
413#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
414pub struct PatternMapRon;
415
416impl Format for PatternMapRon {
417    type Asset = PatternMapDef;
418
419    fn id(&self) -> &'static str {
420        "pattern-map"
421    }
422
423    fn extensions(&self) -> &'static [&'static str] {
424        &["ron"]
425    }
426
427    fn max_bytes(&self) -> usize {
428        TEXT_MAX_BYTES
429    }
430
431    fn decode(
432        &self,
433        file: &AssetPath,
434        bytes: &[u8],
435        _cx: &DecodeCx<'_>,
436    ) -> Result<PatternMapDef, Vec<ContentError>> {
437        PatternMapDef::from_source(file.shown(), text(file, bytes)?)
438    }
439}
440
441/// Where a palette's pattern map is: beside it, its name with `@onebit`
442/// (`skin/palette.ron` → `skin/palette@onebit.ron`).
443pub fn map_path(palette: &str) -> String {
444    let path = AssetPath::new(palette);
445    path.sibling(&format!("{}@onebit.ron", path.stem()))
446}
447
448/// How bad a [`Problem`] is.
449#[derive(Copy, Clone, Debug, PartialEq, Eq)]
450pub enum Severity {
451    /// The skin loses meaning on a one-bit target: the check fails.
452    Error,
453    /// It reads, short of what a one-bit target wants.
454    Warning,
455}
456
457/// Which rule a [`Problem`] breaks.
458#[derive(Copy, Clone, Debug, PartialEq, Eq)]
459pub enum Rule {
460    /// A role of the palette has no patterns.
461    Missing,
462    /// The map names a role the palette doesn't have.
463    Unknown,
464    /// Two roles of one colour have different patterns: the frame holds
465    /// colours, so it can't tell them apart.
466    SharedColour,
467    /// Two roles of a distinct group have foregrounds that look alike.
468    Distinct,
469    /// A text role isn't black on white or white on black.
470    Text,
471}
472
473/// What [`check`] found wrong with a pattern map.
474#[derive(Clone, Debug, PartialEq, Eq)]
475pub struct Problem {
476    /// An error or a warning.
477    pub severity: Severity,
478    /// The rule it breaks.
479    pub rule: Rule,
480    /// The roles it is about.
481    pub roles: Vec<String>,
482    /// What is wrong.
483    pub message: String,
484}
485
486impl Problem {
487    fn error(rule: Rule, roles: Vec<String>, message: String) -> Self {
488        Self {
489            severity: Severity::Error,
490            rule,
491            roles,
492            message,
493        }
494    }
495}
496
497/// What is wrong with `map` as the one-bit look of `palette` (ADR-0008
498/// §4), in this order:
499///
500/// - errors: a role of the palette with no patterns ([`Rule::Missing`]),
501///   a role of the map, a group or the text roles that the palette lacks
502///   ([`Rule::Unknown`]), two roles of one colour with different patterns
503///   ([`Rule::SharedColour`]), and two roles of a distinct group whose
504///   foregrounds look alike ([`Rule::Distinct`], [`Pattern::looks_like`]);
505/// - warnings: a text role that isn't `Black` on `White` or `White` on
506///   `Black` ([`Rule::Text`]).
507pub fn check(palette: &PaletteDef, map: &PatternMapDef) -> Vec<Problem> {
508    let mut out = Vec::new();
509    for role in palette
510        .colors
511        .keys()
512        .filter(|r| !map.roles.contains_key(*r))
513    {
514        out.push(Problem::error(
515            Rule::Missing,
516            vec![role.clone()],
517            format!("role \"{role}\" has no patterns"),
518        ));
519    }
520    let named = map.roles.keys().map(|role| (role, "the map"));
521    let grouped = map
522        .distinct
523        .iter()
524        .flatten()
525        .map(|role| (role, "a distinct group"));
526    let text = map.text.iter().map(|role| (role, "the text roles"));
527    let mut unknown = BTreeSet::new();
528    for (role, place) in named.chain(grouped).chain(text) {
529        if !palette.colors.contains_key(role) && unknown.insert((role, place)) {
530            out.push(Problem::error(
531                Rule::Unknown,
532                vec![role.clone()],
533                format!("{place} names role \"{role}\", which the palette doesn't have"),
534            ));
535        }
536    }
537    let mut by_colour: BTreeMap<[u8; 3], Vec<(&String, &RolePatterns)>> = BTreeMap::new();
538    for (role, rgb) in &palette.colors {
539        if let Some(patterns) = map.roles.get(role) {
540            by_colour.entry(*rgb).or_default().push((role, patterns));
541        }
542    }
543    for (rgb, roles) in &by_colour {
544        let Some(&(first, patterns)) = roles.first() else {
545            continue;
546        };
547        let drawn = |p: &RolePatterns| (p.fg, p.background());
548        for &(other, theirs) in &roles[1..] {
549            if drawn(theirs) != drawn(patterns) {
550                out.push(Problem::error(
551                    Rule::SharedColour,
552                    vec![first.clone(), other.clone()],
553                    format!(
554                        "roles \"{first}\" and \"{other}\" are both {} in the palette, so the \
555                         frame can't tell them apart, but their patterns differ: give them the \
556                         same patterns or different colours",
557                        format_hex(*rgb)
558                    ),
559                ));
560            }
561        }
562    }
563    for group in &map.distinct {
564        let known: Vec<(&String, Pattern)> = group
565            .iter()
566            .filter_map(|role| map.roles.get(role).map(|p| (role, p.fg)))
567            .collect();
568        for (i, &(a, pa)) in known.iter().enumerate() {
569            for &(b, pb) in &known[i + 1..] {
570                if pa.looks_like(pb) {
571                    let how = if pa == pb {
572                        format!("both {pa}")
573                    } else {
574                        format!("{pa} and {pb} are the same pattern moved")
575                    };
576                    out.push(Problem::error(
577                        Rule::Distinct,
578                        vec![a.clone(), b.clone()],
579                        format!(
580                            "roles \"{a}\" and \"{b}\" must stay distinct, but their foregrounds \
581                             look the same in one bit ({how})"
582                        ),
583                    ));
584                }
585            }
586        }
587    }
588    let solid_pair = |p: &RolePatterns| {
589        let pair = (p.fg, p.background());
590        pair == (Pattern::BLACK, Pattern::WHITE) || pair == (Pattern::WHITE, Pattern::BLACK)
591    };
592    for role in &map.text {
593        if let Some(patterns) = map.roles.get(role).filter(|p| !solid_pair(p)) {
594            out.push(Problem {
595                severity: Severity::Warning,
596                rule: Rule::Text,
597                roles: vec![role.clone()],
598                message: format!(
599                    "text role \"{role}\" is {} on {}: text reads best Black on White or White \
600                     on Black",
601                    patterns.fg,
602                    patterns.background()
603                ),
604            });
605        }
606    }
607    out
608}
609
610#[cfg(test)]
611mod tests;