1use crate::*;
2
3pub fn round_dp(f: f64, d: u32) -> f64 {
5 let factor = 10u32.pow(d) as f64;
6 (f * factor).round() / factor
7}
8
9pub(crate) struct Number<T>(pub T);
10
11impl fmt::Display for Number<f64> {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 write!(f, "{}", round_css(self.0))
14 }
15}
16
17impl fmt::Display for Number<f32> {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 write!(f, "{}", round_css(self.0 as f64) as f32)
20 }
21}
22
23pub(crate) struct Alpha(pub f32);
24
25impl fmt::Display for Alpha {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 if self.0 < 100.0 { write!(f, " / {}", Number(self.0 as f64 / 100.0)) } else { Ok(()) }
28 }
29}
30
31fn round_css(value: f64) -> f64 {
32 let scaled = value * 1e6;
33 let floor = scaled.floor();
34 let rounded = if scaled - floor < 0.5 { floor } else { floor + 1.0 };
35 rounded / 1e6 + 0.0
36}
37
38pub trait PerceptualRound: Sized {
49 fn round(self) -> Self;
50}
51
52macro_rules! impl_perceptual_round {
53 ($ty:ident, $c1:ident: $dp1:expr, $c2:ident: $dp2:expr, $c3:ident: $dp3:expr) => {
54 impl PerceptualRound for $ty {
55 fn round(self) -> Self {
56 $ty::new(
57 round_dp(self.$c1 as f64, $dp1) as _,
58 round_dp(self.$c2 as f64, $dp2) as _,
59 round_dp(self.$c3 as f64, $dp3) as _,
60 round_dp(self.alpha as f64, 2) as f32,
61 )
62 }
63 }
64 };
65}
66
67impl_perceptual_round!(Oklch, lightness: 3, chroma: 3, hue: 1);
69impl_perceptual_round!(Oklab, lightness: 3, a: 3, b: 3);
70
71impl_perceptual_round!(Lab, lightness: 1, a: 1, b: 1);
73impl_perceptual_round!(Lch, lightness: 1, chroma: 1, hue: 1);
74
75impl_perceptual_round!(Srgb, red: 3, green: 3, blue: 3);
77impl_perceptual_round!(LinearRgb, red: 4, green: 4, blue: 4);
78impl_perceptual_round!(DisplayP3, red: 3, green: 3, blue: 3);
79impl_perceptual_round!(A98Rgb, red: 3, green: 3, blue: 3);
80impl_perceptual_round!(ProphotoRgb, red: 3, green: 3, blue: 3);
81impl_perceptual_round!(Rec2020, red: 3, green: 3, blue: 3);
82
83impl_perceptual_round!(XyzD50, x: 2, y: 2, z: 2);
85impl_perceptual_round!(XyzD65, x: 2, y: 2, z: 2);
86
87impl_perceptual_round!(Hsl, hue: 1, saturation: 1, lightness: 1);
89impl_perceptual_round!(Hwb, hue: 1, whiteness: 1, blackness: 1);
90
91macro_rules! impl_perceptual_round_noop {
92 ($($ty:ident),+) => {
93 $(impl PerceptualRound for $ty {
94 fn round(self) -> Self { self }
95 })+
96 };
97}
98
99impl_perceptual_round_noop!(Hex, Named, Hsv);
100
101impl PerceptualRound for Color {
102 fn round(self) -> Self {
103 match self {
104 Color::A98Rgb(c) => Color::A98Rgb(c.round()),
105 Color::DisplayP3(c) => Color::DisplayP3(c.round()),
106 Color::Hex(c) => Color::Hex(c.round()),
107 Color::Hsv(c) => Color::Hsv(c.round()),
108 Color::Hsl(c) => Color::Hsl(c.round()),
109 Color::Hwb(c) => Color::Hwb(c.round()),
110 Color::Lab(c) => Color::Lab(c.round()),
111 Color::Lch(c) => Color::Lch(c.round()),
112 Color::LinearRgb(c) => Color::LinearRgb(c.round()),
113 Color::Named(n) => Color::Named(n.round()),
114 Color::Oklab(c) => Color::Oklab(c.round()),
115 Color::Oklch(c) => Color::Oklch(c.round()),
116 Color::ProphotoRgb(c) => Color::ProphotoRgb(c.round()),
117 Color::Rec2020(c) => Color::Rec2020(c.round()),
118 Color::Srgb(c) => Color::Srgb(c.round()),
119 Color::XyzD50(c) => Color::XyzD50(c.round()),
120 Color::XyzD65(c) => Color::XyzD65(c.round()),
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn oklch_round() {
131 let rounded = Oklch::new(0.6593827, 0.30412345, 203.27412, 100.0).round();
133 assert_eq!(rounded, Oklch::new(0.659, 0.304, 203.3, 100.0));
134 }
135
136 #[test]
137 fn oklab_round() {
138 let rounded = Oklab::new(0.44027179, 0.08817676, -0.13386435, 100.0).round();
139 assert_eq!(rounded, Oklab::new(0.44, 0.088, -0.134, 100.0));
140 }
141
142 #[test]
143 fn lab_round() {
144 let rounded = Lab::new(32.39271642, 38.42945581, -47.68554267, 100.0).round();
146 assert_eq!(rounded, Lab::new(32.4, 38.4, -47.7, 100.0));
147 }
148
149 #[test]
150 fn lch_round() {
151 let rounded = Lch::new(61.23323694, 50.27335275, 273.48455139, 100.0).round();
152 assert_eq!(rounded, Lch::new(61.2, 50.3, 273.5, 100.0));
153 }
154
155 #[test]
156 fn hsl_round() {
157 let rounded = Hsl::new(218.54015, 79.19075, 66.07843, 100.0).round();
158 assert_eq!(rounded, Hsl::new(218.5, 79.2, 66.1, 100.0));
159 }
160
161 #[test]
162 fn display_p3_round() {
163 let rounded = DisplayP3::new(0.39189772, 0.57889666, 0.92721090, 100.0).round();
164 assert_eq!(rounded, DisplayP3::new(0.392, 0.579, 0.927, 100.0));
165 }
166
167 #[test]
168 fn already_clean_values_unchanged() {
169 let clean = Oklch::new(0.5, 0.2, 180.0, 100.0);
170 assert_eq!(clean.round(), clean);
171 }
172
173 #[test]
174 fn noop_types() {
175 assert_eq!(Srgb::from_u8(102, 51, 153, 100.0).round(), Srgb::from_u8(102, 51, 153, 100.0));
176 assert_eq!(Hex::new(0x663399FF).round(), Hex::new(0x663399FF));
177 assert_eq!(Named::Rebeccapurple.round(), Named::Rebeccapurple);
178 }
179
180 #[test]
181 fn alpha_is_rounded() {
182 assert_eq!(Oklch::new(0.5, 0.2, 180.0, 75.555).round().alpha, 75.56);
183 }
184
185 #[test]
186 fn color_enum_round() {
187 let rounded = Color::Oklch(Oklch::new(0.6593827, 0.30412345, 203.27412, 100.0)).round();
188 assert_eq!(rounded, Color::Oklch(Oklch::new(0.659, 0.304, 203.3, 100.0)));
189 }
190
191 #[test]
192 fn round_stays_perceptually_close() {
193 let colors = [
194 Color::Oklch(Oklch::new(0.659, 0.304, 203.274, 100.0)),
195 Color::Lab(Lab::new(50.0, 30.5, -20.123, 80.0)),
196 Color::Hsl(Hsl::new(218.54015, 79.19075, 66.07843, 100.0)),
197 Color::DisplayP3(DisplayP3::new(0.39189772, 0.57889666, 0.92721090, 100.0)),
198 Color::Oklab(Oklab::new(0.44027179, 0.08817676, -0.13386435, 100.0)),
199 Color::Lch(Lch::new(61.23323694, 50.27335275, 273.48455139, 100.0)),
200 ];
201 for color in &colors {
202 let rounded = color.round();
203 assert!(color.close_to(rounded, 1.0), "ΔE = {} for {color:?}", color.delta_e(rounded));
204 }
205 }
206}