Skip to main content

blockworx_paint/
palette.rs

1use crate::Color;
2use serde::{Deserialize, Serialize};
3
4use blockworx_geom::WorldPx;
5
6/// One of the 16 base colors of a [`Palette`]. `B00..B07` are the monochrome
7/// shades (darkest background to lightest foreground); `B08..B0F` are the
8/// accent colors. This is the base16 convention.
9///
10/// `Base` is the canvas's color vocabulary: drawing code names a [`Swatch`]
11/// (a `Base` slot plus an alpha), and the renderer resolves it against the
12/// active [`Palette`]. Higher layers map their own semantics (app "roles") onto
13/// `Base`, but the canvas itself only ever sees palette slots — never an
14/// arbitrary `Color`.
15#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, strum::EnumIter)]
16pub enum Base {
17    B00,
18    B01,
19    B02,
20    B03,
21    B04,
22    B05,
23    B06,
24    B07,
25    B08,
26    B09,
27    B0A,
28    B0B,
29    B0C,
30    B0D,
31    B0E,
32    B0F,
33}
34
35/// A palette color: one of the 16 [`Base`] slots, dimmed by an `alpha`
36/// multiplier. `base == None` resolves to a fully transparent color (the
37/// canvas's "draw nothing" color). This is the single color type at the
38/// [`Renderer`](crate::Renderer) interface — it keeps the API tied to
39/// the palette instead of letting arbitrary `Color`s proliferate. Construct a
40/// fully-opaque swatch from a `Base` via `Base.into()`; use [`Swatch::faded`]
41/// for an overlay tint and [`Swatch::TRANSPARENT`] for nothing.
42#[derive(Clone, Copy, PartialEq)]
43pub struct Swatch {
44    pub base: Option<Base>,
45    pub alpha: f32,
46}
47
48impl Swatch {
49    /// A fully transparent swatch (draws nothing).
50    pub const TRANSPARENT: Swatch = Swatch {
51        base: None,
52        alpha: 1.0,
53    };
54
55    /// A fully opaque swatch on `base`.
56    pub const fn solid(base: Base) -> Self {
57        Self {
58            base: Some(base),
59            alpha: 1.0,
60        }
61    }
62
63    /// `base` dimmed to `alpha` (0.0..=1.0), for overlay tints (hover, marquee,
64    /// faded routes, grid lines).
65    pub const fn faded(base: Base, alpha: f32) -> Self {
66        Self {
67            base: Some(base),
68            alpha,
69        }
70    }
71}
72
73impl From<Base> for Swatch {
74    fn from(base: Base) -> Self {
75        Swatch::solid(base)
76    }
77}
78
79/// A stroke described by a world-space width plus a [`Swatch`] — a width and a
80/// colour, before either has a unit or a value. The renderer resolves the
81/// swatch to a color (and scales the width by zoom) at draw time. Construct from
82/// a `(width, Base)` or `(width, Swatch)` tuple, which wraps the literal width as
83/// a [`WorldPx`]; use [`PaletteStroke::NONE`] for no stroke.
84#[derive(Clone, Copy)]
85pub struct PaletteStroke {
86    pub width: WorldPx,
87    pub color: Swatch,
88}
89
90impl PaletteStroke {
91    /// A zero-width, transparent stroke (draws nothing).
92    pub const NONE: PaletteStroke = PaletteStroke {
93        width: WorldPx::ZERO,
94        color: Swatch::TRANSPARENT,
95    };
96}
97
98impl From<(f32, Base)> for PaletteStroke {
99    fn from((width, base): (f32, Base)) -> Self {
100        PaletteStroke {
101            width: WorldPx::new(width),
102            color: Swatch::solid(base),
103        }
104    }
105}
106
107impl From<(f32, Swatch)> for PaletteStroke {
108    fn from((width, color): (f32, Swatch)) -> Self {
109        PaletteStroke {
110            width: WorldPx::new(width),
111            color,
112        }
113    }
114}
115
116/// Which end of the light/dark axis a scheme sits on — the parameter form of
117/// `is_dark`, so a palette constructor reads `Luminance::Dark`, not `true`.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum Luminance {
120    Light,
121    Dark,
122}
123
124impl Luminance {
125    pub const fn is_dark(self) -> bool {
126        matches!(self, Luminance::Dark)
127    }
128}
129
130impl From<bool> for Luminance {
131    fn from(dark: bool) -> Self {
132        if dark {
133            Luminance::Dark
134        } else {
135            Luminance::Light
136        }
137    }
138}
139
140/// The 16 concrete colors a renderer draws from. Held by each
141/// [`Renderer`](crate::Renderer) backend so a [`Swatch`] → `Color`
142/// lookup can happen at draw time. Built by one of the named base16 scheme
143/// constructors; the active scheme is chosen by the user's preferences.
144#[derive(Clone, PartialEq, Serialize, Deserialize)]
145pub struct Palette {
146    colors: [Color; 16],
147    /// Whether this is a dark scheme (`B00` darker than `B07`). Read by the
148    /// shell to pick the host's own light/dark preset; ignored by the canvas
149    /// renderers.
150    is_dark: bool,
151}
152
153impl Palette {
154    /// The concrete color for a base slot.
155    pub fn get(&self, base: Base) -> Color {
156        self.colors[base as usize]
157    }
158
159    /// Resolve a [`Swatch`] to a concrete color: its base color from the palette
160    /// dimmed by its alpha. A swatch with no base resolves to fully transparent.
161    pub fn resolve(&self, swatch: Swatch) -> Color {
162        match swatch.base {
163            Some(base) => self.get(base).gamma_multiply(swatch.alpha),
164            None => Color::TRANSPARENT,
165        }
166    }
167
168    /// The *Tokyo Night Moon* scheme (base16). `B00` is the canvas/background,
169    /// `B05` the default foreground, `B08..B0F` the accents (red, orange,
170    /// yellow, green, cyan, blue, magenta, dark-red).
171    pub fn tokyo_night_moon() -> Self {
172        Self {
173            is_dark: true,
174            colors: [
175                rgb(0x22, 0x24, 0x36), // B00 background
176                rgb(0x2f, 0x33, 0x4d), // B01 lighter bg (block body)
177                rgb(0x3b, 0x42, 0x61), // B02 selection / subtle lines
178                rgb(0x54, 0x5c, 0x7e), // B03 muted
179                rgb(0x82, 0x8b, 0xb8), // B04 dark foreground
180                rgb(0xc8, 0xd3, 0xf5), // B05 default foreground
181                rgb(0xd5, 0xdb, 0xf5), // B06 light foreground
182                rgb(0xe6, 0xea, 0xfe), // B07 lightest
183                rgb(0xff, 0x75, 0x7f), // B08 red
184                rgb(0xff, 0x96, 0x6c), // B09 orange
185                rgb(0xff, 0xc7, 0x77), // B0A yellow
186                rgb(0xc3, 0xe8, 0x8d), // B0B green
187                rgb(0x86, 0xe1, 0xfc), // B0C cyan
188                rgb(0x82, 0xaa, 0xff), // B0D blue
189                rgb(0xc0, 0x99, 0xff), // B0E magenta
190                rgb(0xc5, 0x3b, 0x53), // B0F dark red
191            ],
192        }
193    }
194
195    /// Whether this is a dark scheme.
196    pub fn is_dark(&self) -> bool {
197        self.is_dark
198    }
199
200    /// This palette with `saturation` applied to every slot — the canvas's
201    /// half of the read-only signal.
202    #[must_use]
203    pub fn toned(&self, saturation: Saturation) -> Self {
204        Self {
205            colors: self.colors.map(|color| saturation.applied(color)),
206            is_dark: self.is_dark,
207        }
208    }
209}
210
211/// Whether the canvas draws in its own colors or drained of them.
212///
213/// The third of three redundant read-only signals, beside the viewing band
214/// and the dimmed tool band. A lens onto the past keeps its geometry
215/// exactly — only the color goes — so a reader can still see the drawing
216/// while never mistaking it for the live one.
217#[derive(Clone, Copy, PartialEq, Eq, Debug)]
218pub enum Saturation {
219    Full,
220    Drained,
221}
222
223impl Saturation {
224    /// How much of a color survives the drain. The mockup's
225    /// `filter: saturate(.35)`, which is enough to keep an accent
226    /// distinguishable from its neighbour and far too little to read as live.
227    const REMAINING: f32 = 0.35;
228
229    pub fn applied(self, color: Color) -> Color {
230        match self {
231            Saturation::Full => color,
232            Saturation::Drained => {
233                let grey = luma(color);
234                let mix = |channel: u8| {
235                    let kept = f32::from(channel) * Self::REMAINING;
236                    let lost = f32::from(grey) * (1.0 - Self::REMAINING);
237                    (kept + lost) as u8
238                };
239                Color::from_rgb(mix(color.r()), mix(color.g()), mix(color.b()))
240            }
241        }
242    }
243}
244
245/// Rec. 601 luma — the grey a color drains toward, so a drained palette keeps
246/// the light/dark contrast the scheme was built with.
247fn luma(color: Color) -> u8 {
248    (0.299 * f32::from(color.r()) + 0.587 * f32::from(color.g()) + 0.114 * f32::from(color.b()))
249        as u8
250}
251
252/// The six user-selectable base16 scheme families, each in a dark and a light
253/// variant. Values are the canonical [tinted-theming] base16 palettes; the base16
254/// slot order matches this module's [`Base`] (B00 background … B05 foreground;
255/// B08..B0F the accents red, orange, yellow, green, cyan, blue, magenta, brown).
256///
257/// [tinted-theming]: https://github.com/tinted-theming/schemes
258impl Palette {
259    /// Build a palette from 16 `0xRRGGBB` values in base16 slot order.
260    const fn base16(luminance: Luminance, hexes: [u32; 16]) -> Self {
261        let mut colors = [Color::TRANSPARENT; 16];
262        let mut i = 0;
263        while i < 16 {
264            let v = hexes[i];
265            colors[i] = Color::from_rgb((v >> 16) as u8, (v >> 8) as u8, v as u8);
266            i += 1;
267        }
268        Self {
269            colors,
270            is_dark: luminance.is_dark(),
271        }
272    }
273
274    pub fn catppuccin_mocha() -> Self {
275        Self::base16(
276            Luminance::Dark,
277            [
278                0x1e1e2e, 0x181825, 0x313244, 0x45475a, 0x585b70, 0xcdd6f4, 0xf5e0dc, 0xb4befe,
279                0xf38ba8, 0xfab387, 0xf9e2af, 0xa6e3a1, 0x94e2d5, 0x89b4fa, 0xcba6f7, 0xf2cdcd,
280            ],
281        )
282    }
283
284    pub fn catppuccin_latte() -> Self {
285        Self::base16(
286            Luminance::Light,
287            [
288                0xeff1f5, 0xe6e9ef, 0xccd0da, 0xbcc0cc, 0xacb0be, 0x4c4f69, 0xdc8a78, 0x7287fd,
289                0xd20f39, 0xfe640b, 0xdf8e1d, 0x40a02b, 0x179299, 0x1e66f5, 0x8839ef, 0xdd7878,
290            ],
291        )
292    }
293
294    pub fn gruvbox_dark() -> Self {
295        Self::base16(
296            Luminance::Dark,
297            [
298                0x1d2021, 0x3c3836, 0x504945, 0x665c54, 0xbdae93, 0xd5c4a1, 0xebdbb2, 0xfbf1c7,
299                0xfb4934, 0xfe8019, 0xfabd2f, 0xb8bb26, 0x8ec07c, 0x83a598, 0xd3869b, 0xd65d0e,
300            ],
301        )
302    }
303
304    pub fn gruvbox_light() -> Self {
305        Self::base16(
306            Luminance::Light,
307            [
308                0xf9f5d7, 0xebdbb2, 0xd5c4a1, 0xbdae93, 0x665c54, 0x504945, 0x3c3836, 0x282828,
309                0x9d0006, 0xaf3a03, 0xb57614, 0x79740e, 0x427b58, 0x076678, 0x8f3f71, 0xd65d0e,
310            ],
311        )
312    }
313
314    pub fn rose_pine() -> Self {
315        Self::base16(
316            Luminance::Dark,
317            [
318                0x191724, 0x1f1d2e, 0x26233a, 0x6e6a86, 0x908caa, 0xe0def4, 0xe0def4, 0x524f67,
319                0xeb6f92, 0xf6c177, 0xebbcba, 0x31748f, 0x9ccfd8, 0xc4a7e7, 0xf6c177, 0x524f67,
320            ],
321        )
322    }
323
324    pub fn rose_pine_dawn() -> Self {
325        Self::base16(
326            Luminance::Light,
327            [
328                0xfaf4ed, 0xfffaf3, 0xf2e9de, 0x9893a5, 0x797593, 0x575279, 0x575279, 0xcecacd,
329                0xb4637a, 0xea9d34, 0xd7827e, 0x286983, 0x56949f, 0x907aa9, 0xea9d34, 0xcecacd,
330            ],
331        )
332    }
333
334    pub fn everforest_dark() -> Self {
335        Self::base16(
336            Luminance::Dark,
337            [
338                0x272e33, 0x2e383c, 0x414b50, 0x859289, 0x9da9a0, 0xd3c6aa, 0xedeada, 0xfffbef,
339                0xe67e80, 0xe69875, 0xdbbc7f, 0xa7c080, 0x83c092, 0x7fbbb3, 0xd699b6, 0x9da9a0,
340            ],
341        )
342    }
343
344    pub fn everforest_light() -> Self {
345        Self::base16(
346            Luminance::Light,
347            [
348                0xfffbef, 0xf8f5e4, 0xedeada, 0x939f91, 0x829181, 0x5c6a72, 0x414b50, 0x272e33,
349                0xf85552, 0xf57d26, 0xdfa000, 0x8da101, 0x35a77c, 0x3a94c5, 0xdf69ba, 0x829181,
350            ],
351        )
352    }
353
354    pub fn ayu_dark() -> Self {
355        Self::base16(
356            Luminance::Dark,
357            [
358                0x0b0e14, 0x131721, 0x202229, 0x3e4b59, 0xbfbdb6, 0xe6e1cf, 0xece8db, 0xf2f0e7,
359                0xf07178, 0xff8f40, 0xffb454, 0xaad94c, 0x95e6cb, 0x59c2ff, 0xd2a6ff, 0xe6b450,
360            ],
361        )
362    }
363
364    pub fn ayu_light() -> Self {
365        Self::base16(
366            Luminance::Light,
367            [
368                0xf8f9fa, 0xedeff1, 0xd2d4d8, 0xa0a6ac, 0x8a9199, 0x5c6166, 0x4e5257, 0x404447,
369                0xf07171, 0xfa8d3e, 0xf2ae49, 0x6cbf49, 0x4cbf99, 0x399ee6, 0xa37acc, 0xe6ba7e,
370            ],
371        )
372    }
373
374    pub fn tokyo_night_dark() -> Self {
375        Self::base16(
376            Luminance::Dark,
377            [
378                0x1a1b26, 0x16161e, 0x2f3549, 0x444b6a, 0x787c99, 0xa9b1d6, 0xcbccd1, 0xd5d6db,
379                0xc0caf5, 0xa9b1d6, 0x0db9d7, 0x9ece6a, 0xb4f9f8, 0x2ac3de, 0xbb9af7, 0xf7768e,
380            ],
381        )
382    }
383
384    pub fn tokyo_night_light() -> Self {
385        Self::base16(
386            Luminance::Light,
387            [
388                0xd5d6db, 0xcbccd1, 0xdfe0e5, 0x9699a3, 0x4c505e, 0x343b59, 0x1a1b26, 0x1a1b26,
389                0x343b58, 0x965027, 0x166775, 0x485e30, 0x3e6968, 0x34548a, 0x5a4a78, 0x8c4351,
390            ],
391        )
392    }
393}
394
395const fn rgb(r: u8, g: u8, b: u8) -> Color {
396    Color::from_rgb(r, g, b)
397}