1use crate::*;
2
3pub trait PerceptualRound: Sized {
14 fn round(self) -> Self;
15}
16
17macro_rules! impl_perceptual_round {
18 ($ty:ident, $c1:ident: $dp1:expr, $c2:ident: $dp2:expr, $c3:ident: $dp3:expr) => {
19 impl PerceptualRound for $ty {
20 fn round(self) -> Self {
21 $ty::new(
22 round_dp(self.$c1 as f64, $dp1) as _,
23 round_dp(self.$c2 as f64, $dp2) as _,
24 round_dp(self.$c3 as f64, $dp3) as _,
25 round_dp(self.alpha as f64, 2) as f32,
26 )
27 }
28 }
29 };
30}
31
32impl_perceptual_round!(Oklch, lightness: 3, chroma: 3, hue: 1);
34impl_perceptual_round!(Oklab, lightness: 3, a: 3, b: 3);
35
36impl_perceptual_round!(Lab, lightness: 1, a: 1, b: 1);
38impl_perceptual_round!(Lch, lightness: 1, chroma: 1, hue: 1);
39
40impl_perceptual_round!(LinearRgb, red: 4, green: 4, blue: 4);
42impl_perceptual_round!(DisplayP3, red: 3, green: 3, blue: 3);
43impl_perceptual_round!(A98Rgb, red: 3, green: 3, blue: 3);
44impl_perceptual_round!(ProphotoRgb, red: 3, green: 3, blue: 3);
45impl_perceptual_round!(Rec2020, red: 3, green: 3, blue: 3);
46
47impl_perceptual_round!(XyzD50, x: 2, y: 2, z: 2);
49impl_perceptual_round!(XyzD65, x: 2, y: 2, z: 2);
50
51impl_perceptual_round!(Hsl, hue: 1, saturation: 1, lightness: 1);
53impl_perceptual_round!(Hwb, hue: 1, whiteness: 1, blackness: 1);
54
55macro_rules! impl_perceptual_round_noop {
56 ($($ty:ident),+) => {
57 $(impl PerceptualRound for $ty {
58 fn round(self) -> Self { self }
59 })+
60 };
61}
62
63impl_perceptual_round_noop!(Srgb, Hex, Named, Hsv);
64
65impl PerceptualRound for Color {
66 fn round(self) -> Self {
67 match self {
68 Color::A98Rgb(c) => Color::A98Rgb(c.round()),
69 Color::DisplayP3(c) => Color::DisplayP3(c.round()),
70 Color::Hex(c) => Color::Hex(c.round()),
71 Color::Hsv(c) => Color::Hsv(c.round()),
72 Color::Hsl(c) => Color::Hsl(c.round()),
73 Color::Hwb(c) => Color::Hwb(c.round()),
74 Color::Lab(c) => Color::Lab(c.round()),
75 Color::Lch(c) => Color::Lch(c.round()),
76 Color::LinearRgb(c) => Color::LinearRgb(c.round()),
77 Color::Named(n) => Color::Named(n.round()),
78 Color::Oklab(c) => Color::Oklab(c.round()),
79 Color::Oklch(c) => Color::Oklch(c.round()),
80 Color::ProphotoRgb(c) => Color::ProphotoRgb(c.round()),
81 Color::Rec2020(c) => Color::Rec2020(c.round()),
82 Color::Srgb(c) => Color::Srgb(c.round()),
83 Color::XyzD50(c) => Color::XyzD50(c.round()),
84 Color::XyzD65(c) => Color::XyzD65(c.round()),
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn oklch_round() {
95 let rounded = Oklch::new(0.6593827, 0.30412345, 203.27412, 100.0).round();
97 assert_eq!(rounded, Oklch::new(0.659, 0.304, 203.3, 100.0));
98 }
99
100 #[test]
101 fn oklab_round() {
102 let rounded = Oklab::new(0.44027179, 0.08817676, -0.13386435, 100.0).round();
103 assert_eq!(rounded, Oklab::new(0.44, 0.088, -0.134, 100.0));
104 }
105
106 #[test]
107 fn lab_round() {
108 let rounded = Lab::new(32.39271642, 38.42945581, -47.68554267, 100.0).round();
110 assert_eq!(rounded, Lab::new(32.4, 38.4, -47.7, 100.0));
111 }
112
113 #[test]
114 fn lch_round() {
115 let rounded = Lch::new(61.23323694, 50.27335275, 273.48455139, 100.0).round();
116 assert_eq!(rounded, Lch::new(61.2, 50.3, 273.5, 100.0));
117 }
118
119 #[test]
120 fn hsl_round() {
121 let rounded = Hsl::new(218.54015, 79.19075, 66.07843, 100.0).round();
122 assert_eq!(rounded, Hsl::new(218.5, 79.2, 66.1, 100.0));
123 }
124
125 #[test]
126 fn display_p3_round() {
127 let rounded = DisplayP3::new(0.39189772, 0.57889666, 0.92721090, 100.0).round();
128 assert_eq!(rounded, DisplayP3::new(0.392, 0.579, 0.927, 100.0));
129 }
130
131 #[test]
132 fn already_clean_values_unchanged() {
133 let clean = Oklch::new(0.5, 0.2, 180.0, 100.0);
134 assert_eq!(clean.round(), clean);
135 }
136
137 #[test]
138 fn noop_types() {
139 assert_eq!(Srgb::new(102, 51, 153, 100.0).round(), Srgb::new(102, 51, 153, 100.0));
140 assert_eq!(Hex::new(0x663399FF).round(), Hex::new(0x663399FF));
141 assert_eq!(Named::Rebeccapurple.round(), Named::Rebeccapurple);
142 }
143
144 #[test]
145 fn alpha_is_rounded() {
146 assert_eq!(Oklch::new(0.5, 0.2, 180.0, 75.555).round().alpha, 75.56);
147 }
148
149 #[test]
150 fn color_enum_round() {
151 let rounded = Color::Oklch(Oklch::new(0.6593827, 0.30412345, 203.27412, 100.0)).round();
152 assert_eq!(rounded, Color::Oklch(Oklch::new(0.659, 0.304, 203.3, 100.0)));
153 }
154
155 #[test]
156 fn round_stays_perceptually_close() {
157 let colors = [
158 Color::Oklch(Oklch::new(0.659, 0.304, 203.274, 100.0)),
159 Color::Lab(Lab::new(50.0, 30.5, -20.123, 80.0)),
160 Color::Hsl(Hsl::new(218.54015, 79.19075, 66.07843, 100.0)),
161 Color::DisplayP3(DisplayP3::new(0.39189772, 0.57889666, 0.92721090, 100.0)),
162 Color::Oklab(Oklab::new(0.44027179, 0.08817676, -0.13386435, 100.0)),
163 Color::Lch(Lch::new(61.23323694, 50.27335275, 273.48455139, 100.0)),
164 ];
165 for color in &colors {
166 let rounded = color.round();
167 assert!(color.close_to(rounded, 1.0), "ΔE = {} for {color:?}", color.delta_e(rounded));
168 }
169 }
170}