1use crate::prelude::*;
11use alloc::collections::BTreeMap;
12use core::any::TypeId;
13
14use shuyi_assets::palette::PaletteDef;
15use shuyi_core::Lerp;
16
17#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
20pub struct Rgb {
21 pub r: u8,
23 pub g: u8,
25 pub b: u8,
27}
28
29impl Rgb {
30 pub const fn new(r: u8, g: u8, b: u8) -> Self {
32 Self { r, g, b }
33 }
34
35 #[must_use]
38 pub fn lerp(self, other: Rgb, t: f32) -> Rgb {
39 let t = if t.is_nan() { 0.0 } else { t.clamp(0.0, 1.0) };
40 let mix = |a: u8, b: u8| to_channel(f32::from(a) + (f32::from(b) - f32::from(a)) * t);
41 Rgb::new(
42 mix(self.r, other.r),
43 mix(self.g, other.g),
44 mix(self.b, other.b),
45 )
46 }
47
48 #[must_use]
51 pub fn scale(self, factor: f32) -> Rgb {
52 let f = if factor.is_nan() { 0.0 } else { factor };
53 let s = |c: u8| to_channel(f32::from(c) * f);
54 Rgb::new(s(self.r), s(self.g), s(self.b))
55 }
56
57 #[must_use]
62 pub fn dimmed(self) -> Rgb {
63 let [r, g, b] = [self.r, self.g, self.b].map(f32::from);
64 let grey = 0.299 * r + 0.587 * g + 0.114 * b;
65 let dim = |c: f32| to_channel((c + (grey - c) * DIM_GREY) * DIM_DARK);
66 Rgb::new(dim(r), dim(g), dim(b))
67 }
68
69 pub fn to_hex(self) -> String {
71 shuyi_assets::palette::format_hex([self.r, self.g, self.b])
72 }
73}
74
75impl Lerp for Rgb {
79 fn mix(from: Self, to: Self, p: i32) -> Self {
80 Rgb::new(
81 u8::mix(from.r, to.r, p),
82 u8::mix(from.g, to.g, p),
83 u8::mix(from.b, to.b, p),
84 )
85 }
86}
87
88impl From<[u8; 3]> for Rgb {
89 fn from([r, g, b]: [u8; 3]) -> Self {
90 Self::new(r, g, b)
91 }
92}
93
94impl From<Rgb> for [u8; 4] {
97 fn from(c: Rgb) -> Self {
98 [c.r, c.g, c.b, 255]
99 }
100}
101
102pub const DIM_GREY: f32 = 0.75;
105
106pub const DIM_DARK: f32 = 0.75;
110
111#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn to_channel(v: f32) -> u8 {
114 v.round().clamp(0.0, 255.0) as u8
115}
116
117#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
121pub enum EngineColor {
122 Text,
124 TextDim,
126 Panel,
128 Border,
130 Highlight,
132 Warning,
134 Focus,
136 Background,
138 Accent,
141 Bright,
143}
144
145impl EngineColor {
146 pub const ALL: &[EngineColor] = &[
148 EngineColor::Text,
149 EngineColor::TextDim,
150 EngineColor::Panel,
151 EngineColor::Border,
152 EngineColor::Highlight,
153 EngineColor::Warning,
154 EngineColor::Focus,
155 EngineColor::Background,
156 EngineColor::Accent,
157 EngineColor::Bright,
158 ];
159
160 pub const fn name(self) -> &'static str {
163 match self {
164 EngineColor::Text => "text",
165 EngineColor::TextDim => "text_dim",
166 EngineColor::Panel => "panel",
167 EngineColor::Border => "border",
168 EngineColor::Highlight => "highlight",
169 EngineColor::Warning => "warning",
170 EngineColor::Focus => "focus",
171 EngineColor::Background => "background",
172 EngineColor::Accent => "accent",
173 EngineColor::Bright => "bright",
174 }
175 }
176}
177
178pub trait ColorSet: Copy + 'static {
181 const ALL: &'static [Self];
183
184 fn name(self) -> &'static str;
186
187 fn index(self) -> usize;
189
190 fn engine_name(color: EngineColor) -> &'static str {
194 color.name()
195 }
196}
197
198impl ColorSet for EngineColor {
201 const ALL: &'static [Self] = EngineColor::ALL;
202
203 fn name(self) -> &'static str {
204 EngineColor::name(self)
205 }
206
207 fn index(self) -> usize {
208 self as usize
209 }
210}
211
212pub fn required_names<C: ColorSet>() -> Vec<&'static str> {
215 let mut names: Vec<&'static str> = C::ALL.iter().map(|c| c.name()).collect();
216 for &e in EngineColor::ALL {
217 let name = C::engine_name(e);
218 if !names.contains(&name) {
219 names.push(name);
220 }
221 }
222 names
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct Palette {
229 set: TypeId,
231 colors: Vec<Rgb>,
233 engine: Vec<Rgb>,
235 required: Vec<&'static str>,
238 named: BTreeMap<String, Rgb>,
240}
241
242impl Palette {
243 pub fn new<C: ColorSet>(def: &PaletteDef) -> Result<Self, Vec<&'static str>> {
247 let named: BTreeMap<String, Rgb> = def
248 .colors
249 .iter()
250 .map(|(name, &rgb)| (name.clone(), Rgb::from(rgb)))
251 .collect();
252 let required = required_names::<C>();
253 let missing: Vec<&'static str> = required
254 .iter()
255 .copied()
256 .filter(|n| !named.contains_key(*n))
257 .collect();
258 if !missing.is_empty() {
259 return Err(missing);
260 }
261 let colors = C::ALL.iter().map(|c| named[c.name()]).collect();
262 let engine = EngineColor::ALL
263 .iter()
264 .map(|&e| named[C::engine_name(e)])
265 .collect();
266 Ok(Self {
267 set: TypeId::of::<C>(),
268 colors,
269 engine,
270 required,
271 named,
272 })
273 }
274
275 pub fn get<C: ColorSet>(&self, color: C) -> Rgb {
279 if TypeId::of::<C>() == self.set {
280 self.colors[color.index()]
281 } else {
282 self.lookup(color.name()).unwrap_or(Rgb::new(0, 0, 0))
283 }
284 }
285
286 pub fn engine(&self, color: EngineColor) -> Rgb {
288 self.engine[color as usize]
289 }
290
291 pub fn lookup(&self, name: &str) -> Option<Rgb> {
293 self.named.get(name).copied()
294 }
295
296 pub fn iter(&self) -> impl Iterator<Item = (&str, Rgb)> {
298 self.named.iter().map(|(n, &c)| (n.as_str(), c))
299 }
300
301 pub fn name_of(&self, rgb: Rgb) -> Option<&str> {
305 self.required
306 .iter()
307 .copied()
308 .find(|&n| self.lookup(n) == Some(rgb))
309 .or_else(|| {
310 self.named
311 .iter()
312 .find(|&(_, &v)| v == rgb)
313 .map(|(n, _)| n.as_str())
314 })
315 }
316}
317
318#[cfg(test)]
319pub(crate) mod tests {
320 use super::*;
321
322 pub(crate) fn def(pairs: &[(&str, [u8; 3])]) -> PaletteDef {
324 PaletteDef {
325 colors: pairs.iter().map(|&(n, c)| (n.to_owned(), c)).collect(),
326 }
327 }
328
329 pub(crate) fn engine_palette() -> Palette {
332 let def = def(&[
333 ("text", [230, 230, 230]),
334 ("text_dim", [128, 128, 128]),
335 ("panel", [20, 24, 40]),
336 ("border", [90, 110, 160]),
337 ("highlight", [250, 210, 90]),
338 ("warning", [220, 60, 50]),
339 ("focus", [120, 150, 220]),
340 ("background", [0, 0, 0]),
341 ("accent", [80, 200, 240]),
342 ("bright", [255, 255, 255]),
343 ("grass", [40, 120, 50]),
344 ]);
345 Palette::new::<EngineColor>(&def).unwrap()
346 }
347
348 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
350 enum Toy {
351 Ink,
352 Hero,
353 Paper,
354 }
355
356 impl ColorSet for Toy {
357 const ALL: &'static [Self] = &[Toy::Ink, Toy::Hero, Toy::Paper];
358
359 fn name(self) -> &'static str {
360 match self {
361 Toy::Ink => "ink",
362 Toy::Hero => "hero",
363 Toy::Paper => "paper",
364 }
365 }
366
367 fn index(self) -> usize {
368 self as usize
369 }
370
371 fn engine_name(color: EngineColor) -> &'static str {
372 match color {
373 EngineColor::Text => "ink",
374 EngineColor::Panel => "paper",
375 other => other.name(),
376 }
377 }
378 }
379
380 fn toy_def() -> PaletteDef {
381 def(&[
382 ("ink", [1, 1, 1]),
383 ("hero", [2, 2, 2]),
384 ("paper", [3, 3, 3]),
385 ("text_dim", [4, 4, 4]),
386 ("border", [5, 5, 5]),
387 ("highlight", [6, 6, 6]),
388 ("warning", [7, 7, 7]),
389 ("text", [8, 8, 8]),
390 ("focus", [10, 10, 10]),
391 ("background", [11, 11, 11]),
392 ("accent", [12, 12, 12]),
393 ("bright", [13, 13, 13]),
394 ])
395 }
396
397 #[test]
398 fn a_colour_is_an_opaque_rgba_pixel() {
399 assert_eq!(<[u8; 4]>::from(Rgb::new(1, 2, 3)), [1, 2, 3, 255]);
400 }
401
402 #[test]
403 fn engine_color_names_and_indices() {
404 let names: Vec<&str> = EngineColor::ALL.iter().map(|c| c.name()).collect();
405 assert_eq!(
406 names,
407 [
408 "text",
409 "text_dim",
410 "panel",
411 "border",
412 "highlight",
413 "warning",
414 "focus",
415 "background",
416 "accent",
417 "bright"
418 ]
419 );
420 for (i, &c) in EngineColor::ALL.iter().enumerate() {
421 assert_eq!(ColorSet::index(c), i);
422 assert_eq!(ColorSet::name(c), c.name());
423 assert_eq!(<EngineColor as ColorSet>::engine_name(c), c.name());
424 }
425 assert_eq!(<EngineColor as ColorSet>::ALL, EngineColor::ALL);
426 }
427
428 #[test]
429 fn required_names_are_the_set_then_the_engine_names_it_lacks() {
430 assert_eq!(
431 required_names::<EngineColor>(),
432 [
433 "text",
434 "text_dim",
435 "panel",
436 "border",
437 "highlight",
438 "warning",
439 "focus",
440 "background",
441 "accent",
442 "bright"
443 ]
444 );
445 assert_eq!(
446 required_names::<Toy>(),
447 [
448 "ink",
449 "hero",
450 "paper",
451 "text_dim",
452 "border",
453 "highlight",
454 "warning",
455 "focus",
456 "background",
457 "accent",
458 "bright"
459 ]
460 );
461 }
462
463 #[test]
464 fn a_palette_gives_the_sets_colors_and_the_engines() {
465 let p = Palette::new::<Toy>(&toy_def()).unwrap();
466 assert_eq!(p.get(Toy::Ink), Rgb::new(1, 1, 1));
467 assert_eq!(p.get(Toy::Hero), Rgb::new(2, 2, 2));
468 assert_eq!(p.get(Toy::Paper), Rgb::new(3, 3, 3));
469 assert_eq!(p.engine(EngineColor::Text), Rgb::new(1, 1, 1));
471 assert_eq!(p.engine(EngineColor::Panel), Rgb::new(3, 3, 3));
472 assert_eq!(p.engine(EngineColor::TextDim), Rgb::new(4, 4, 4));
473 assert_eq!(p.engine(EngineColor::Warning), Rgb::new(7, 7, 7));
474 assert_eq!(p.get(EngineColor::Text), Rgb::new(8, 8, 8));
476 assert_eq!(p.get(EngineColor::Panel), Rgb::new(0, 0, 0));
477 let e = engine_palette();
478 assert_eq!(e.get(EngineColor::Highlight), Rgb::new(250, 210, 90));
479 assert_eq!(e.engine(EngineColor::Highlight), Rgb::new(250, 210, 90));
480 assert_eq!(e.get(Toy::Hero), Rgb::new(0, 0, 0));
481 }
482
483 #[test]
484 fn a_palette_lacking_names_says_which() {
485 assert_eq!(
486 Palette::new::<Toy>(&PaletteDef::default()),
487 Err(required_names::<Toy>())
488 );
489 let mut d = toy_def();
490 d.colors.remove("hero");
491 d.colors.remove("warning");
492 d.colors.remove("text");
494 assert_eq!(Palette::new::<Toy>(&d), Err(vec!["hero", "warning"]));
495 }
496
497 #[test]
498 fn lookup_and_iter() {
499 let p = engine_palette();
500 assert_eq!(p.lookup("grass"), Some(Rgb::new(40, 120, 50)));
501 assert_eq!(p.lookup("no_such_colour"), None);
502 let all: Vec<(&str, Rgb)> = p.iter().collect();
503 assert_eq!(all.len(), 11);
504 assert!(all.windows(2).all(|w| w[0].0 < w[1].0));
505 assert!(all.iter().all(|&(n, c)| p.lookup(n) == Some(c)));
506 }
507
508 #[test]
509 fn name_of_prefers_the_sets_names_then_alphabetical() {
510 let mut d = toy_def();
511 for (name, rgb) in [
512 ("zeta", [1, 2, 3]),
513 ("alpha", [1, 2, 3]),
514 ("aaa", [2, 2, 2]),
515 ("text", [9, 9, 9]),
516 ("border", [1, 1, 1]),
517 ] {
518 d.colors.insert(name.to_owned(), rgb);
519 }
520 let p = Palette::new::<Toy>(&d).unwrap();
521 assert_eq!(p.name_of(Rgb::new(2, 2, 2)), Some("hero"));
522 assert_eq!(p.name_of(Rgb::new(1, 1, 1)), Some("ink"));
524 assert_eq!(p.name_of(Rgb::new(1, 2, 3)), Some("alpha"));
525 assert_eq!(p.name_of(Rgb::new(9, 9, 9)), Some("text"));
526 assert_eq!(p.name_of(Rgb::new(7, 7, 7)), Some("warning"));
527 assert_eq!(p.name_of(Rgb::new(70, 7, 7)), None);
528 }
529
530 #[test]
531 fn a_dimmed_colour_is_greyer_and_darker() {
532 assert_eq!(Rgb::new(200, 200, 200).dimmed(), Rgb::new(150, 150, 150));
534 assert_eq!(Rgb::new(0, 0, 0).dimmed(), Rgb::new(0, 0, 0));
535 assert_eq!(Rgb::new(255, 255, 255).dimmed(), Rgb::new(191, 191, 191));
536 assert_eq!(Rgb::new(255, 0, 0).dimmed(), Rgb::new(91, 43, 43));
539 assert_eq!(Rgb::new(0, 255, 0).dimmed(), Rgb::new(84, 132, 84));
541 assert_eq!(Rgb::new(0, 0, 255).dimmed(), Rgb::new(16, 16, 64));
542 }
543
544 #[test]
545 fn lerp_endpoints_and_midpoint() {
546 let a = Rgb::new(0, 100, 255);
547 let b = Rgb::new(200, 0, 55);
548 assert_eq!(a.lerp(b, 0.0), a);
549 assert_eq!(a.lerp(b, 1.0), b);
550 assert_eq!(a.lerp(b, 0.5), Rgb::new(100, 50, 155));
551 assert_eq!(a.lerp(b, 0.25), Rgb::new(50, 75, 205));
552 }
553
554 #[test]
555 fn lerp_clamps_t() {
556 let a = Rgb::new(10, 20, 30);
557 let b = Rgb::new(40, 50, 60);
558 assert_eq!(a.lerp(b, -3.0), a);
559 assert_eq!(a.lerp(b, 7.0), b);
560 assert_eq!(a.lerp(b, f32::NAN), a);
561 }
562
563 #[test]
564 fn mix_is_lerp_in_integers_and_stops_at_the_channels_ends() {
565 use shuyi_core::ease::ONE;
566 let a = Rgb::new(0, 100, 255);
567 let b = Rgb::new(200, 0, 55);
568 assert_eq!(Rgb::mix(a, b, 0), a);
569 assert_eq!(Rgb::mix(a, b, ONE), b);
570 assert_eq!(Rgb::mix(a, b, ONE / 2), a.lerp(b, 0.5));
571 assert_eq!(Rgb::mix(a, b, ONE / 4), a.lerp(b, 0.25));
572 assert_eq!(Rgb::mix(a, b, -ONE / 10), Rgb::new(0, 110, 255));
574 assert_eq!(Rgb::mix(a, b, 11 * ONE / 10), Rgb::new(220, 0, 35));
575 }
576
577 #[test]
578 fn scale_dims_and_saturates() {
579 let c = Rgb::new(100, 200, 51);
580 assert_eq!(c.scale(0.5), Rgb::new(50, 100, 26));
581 assert_eq!(c.scale(1.0), c);
582 assert_eq!(c.scale(2.0), Rgb::new(200, 255, 102));
583 assert_eq!(c.scale(-1.0), Rgb::new(0, 0, 0));
584 assert_eq!(c.scale(f32::NAN), Rgb::new(0, 0, 0));
585 }
586
587 #[test]
588 fn to_hex_and_from_array() {
589 assert_eq!(Rgb::from([1, 0xab, 255]).to_hex(), "#01abff");
590 }
591}