Skip to main content

blockworx_canvas2d/
color.rs

1//! A resolved swatch as CSS writes it.
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::rc::Rc;
6
7use blockworx_paint::Color;
8
9/// `color` as a CSS `rgb()`/`rgba()` function. The display list carries
10/// premultiplied alpha and CSS does not, so the multiplication is undone here.
11#[must_use]
12pub fn css_color(color: Color) -> String {
13    let [r, g, b, a] = color.to_srgba_unmultiplied();
14    if a == 255 {
15        format!("rgb({r},{g},{b})")
16    } else {
17        format!("rgba({r},{g},{b},{:.4})", f32::from(a) / 255.0)
18    }
19}
20
21/// The CSS strings a frame's swatches were last written as.
22///
23/// A display list names a handful of colours thousands of times, and each
24/// mark would otherwise re-format the same six digits.
25#[derive(Default)]
26pub struct Swatches(RefCell<HashMap<Color, Rc<str>>>);
27
28impl Swatches {
29    pub fn css(&self, color: Color) -> Rc<str> {
30        if let Some(cached) = self.0.borrow().get(&color) {
31            return Rc::clone(cached);
32        }
33        let css: Rc<str> = Rc::from(css_color(color));
34        self.0.borrow_mut().insert(color, Rc::clone(&css));
35        css
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn an_opaque_colour_needs_no_alpha_channel() {
45        assert_eq!(css_color(Color::from_rgb(1, 2, 3)), "rgb(1,2,3)");
46        assert_eq!(css_color(Color::WHITE), "rgb(255,255,255)");
47    }
48
49    /// The same swatch is formatted once, however many marks name it.
50    #[test]
51    fn a_swatch_comes_back_from_the_cache() {
52        let swatches = Swatches::default();
53        let once = swatches.css(Color::GRAY);
54        let twice = swatches.css(Color::GRAY);
55        assert!(Rc::ptr_eq(&once, &twice), "the swatch was formatted twice");
56        assert_eq!(&*once, css_color(Color::GRAY));
57        assert_ne!(swatches.css(Color::WHITE), once);
58    }
59
60    #[test]
61    fn a_translucent_colour_is_unmultiplied_back_to_css() {
62        let half = Color::from_rgba_unmultiplied(200, 100, 50, 128);
63        assert!(
64            half.r() < 200,
65            "precondition: the display list holds it multiplied ({})",
66            half.r()
67        );
68        let css = css_color(half);
69        let channels: Vec<u32> = css
70            .trim_start_matches("rgba(")
71            .trim_end_matches(')')
72            .split(',')
73            .take(3)
74            .filter_map(|channel| channel.parse().ok())
75            .collect();
76        // Premultiplying and undoing it costs up to a unit a channel, which
77        // is the price of the display list holding colours the way both
78        // backends blend them.
79        assert_eq!(channels.len(), 3, "{css}");
80        for (written, asked) in channels.iter().zip([200, 100, 50]) {
81            assert!(written.abs_diff(asked) <= 1, "{css}");
82        }
83        assert!(css.ends_with(",0.5020)"), "{css}");
84        assert_eq!(css_color(Color::TRANSPARENT), "rgba(0,0,0,0.0000)");
85    }
86}