1use crate::Lab;
2
3pub trait ColorDistance<T>: Sized + Copy
8where
9 Lab: From<T> + From<Self>,
10{
11 fn close_to(&self, other: T, tolerance: f64) -> bool {
34 self.delta_e(other) <= tolerance
35 }
36
37 fn far_from(&self, other: T, tolerance: f64) -> bool {
60 self.delta_e(other) >= tolerance
61 }
62
63 fn delta_e(&self, other: T) -> f64 {
87 let lab1 = Lab::from(*self);
88 let lab2 = Lab::from(other);
89 let (l1, a1, b1) = (lab1.lightness, lab1.a, lab1.b);
91 let (l2, a2, b2) = (lab2.lightness, lab2.a, lab2.b);
92
93 let c1 = (a1 * a1 + b1 * b1).sqrt();
95 let c2 = (a2 * a2 + b2 * b2).sqrt();
96 let c_bar = (c1 + c2) / 2.0;
97
98 let c_bar_7 = c_bar.powf(7.0);
100 let g = 0.5 * (1.0 - (c_bar_7 / (c_bar_7 + 25.0_f64.powf(7.0))).sqrt());
101
102 let a1_prime = (1.0 + g) * a1;
104 let a2_prime = (1.0 + g) * a2;
105 let c1_prime = (a1_prime * a1_prime + b1 * b1).sqrt();
106 let c2_prime = (a2_prime * a2_prime + b2 * b2).sqrt();
107
108 let h1_prime =
110 if a1_prime == 0.0 && b1 == 0.0 { 0.0 } else { b1.atan2(a1_prime).to_degrees().rem_euclid(360.0) };
111 let h2_prime =
112 if a2_prime == 0.0 && b2 == 0.0 { 0.0 } else { b2.atan2(a2_prime).to_degrees().rem_euclid(360.0) };
113
114 let delta_l = l2 - l1;
116 let delta_c = c2_prime - c1_prime;
117 let delta_h = if c1_prime * c2_prime == 0.0 {
118 0.0
119 } else {
120 let diff = h2_prime - h1_prime;
121 if diff.abs() <= 180.0 {
122 diff
123 } else if diff > 180.0 {
124 diff - 360.0
125 } else {
126 diff + 360.0
127 }
128 };
129 let delta_h_big = 2.0 * (c1_prime * c2_prime).sqrt() * (delta_h.to_radians() / 2.0).sin();
130
131 let l_bar = (l1 + l2) / 2.0;
133 let c_prime_bar = (c1_prime + c2_prime) / 2.0;
134 let h_prime_bar = if c1_prime * c2_prime == 0.0 {
135 h1_prime + h2_prime
136 } else {
137 let sum = h1_prime + h2_prime;
138 let diff = (h1_prime - h2_prime).abs();
139 if diff <= 180.0 {
140 sum / 2.0
141 } else if sum < 360.0 {
142 (sum + 360.0) / 2.0
143 } else {
144 (sum - 360.0) / 2.0
145 }
146 };
147
148 let t = 1.0 - 0.17 * (h_prime_bar - 30.0).to_radians().cos()
150 + 0.24 * (2.0 * h_prime_bar).to_radians().cos()
151 + 0.32 * (3.0 * h_prime_bar + 6.0).to_radians().cos()
152 - 0.20 * (4.0 * h_prime_bar - 63.0).to_radians().cos();
153
154 let l_offset = l_bar - 50.0;
155 let sl = 1.0 + (0.015 * l_offset * l_offset) / (20.0 + l_offset * l_offset).sqrt();
156 let sc = 1.0 + 0.045 * c_prime_bar;
157 let sh = 1.0 + 0.015 * c_prime_bar * t;
158
159 let delta_theta = 30.0 * (-((h_prime_bar - 275.0) / 25.0).powf(2.0)).exp();
161 let c_prime_bar_7 = c_prime_bar.powf(7.0);
162 let rc = 2.0 * (c_prime_bar_7 / (c_prime_bar_7 + 25.0_f64.powf(7.0))).sqrt();
163 let rt = -rc * (2.0 * delta_theta).to_radians().sin();
164
165 let l_term = delta_l / sl;
167 let c_term = delta_c / sc;
168 let h_term = delta_h_big / sh;
169
170 (l_term * l_term + c_term * c_term + h_term * h_term + rt * c_term * h_term).sqrt()
171 }
172}
173
174impl<C, T> ColorDistance<T> for C
175where
176 C: Copy,
177 Lab: From<C> + From<T>,
178{
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::{Hsl, Srgb};
185
186 #[test]
187 fn test_identical_colors() {
188 let red = Srgb::new(255, 0, 0, 100.0);
189 assert!(red.close_to(red, 0.0));
190 assert_eq!(red.delta_e(red), 0.0);
191 }
192
193 #[test]
194 fn test_very_similar_colors() {
195 let red1 = Srgb::new(255, 0, 0, 100.0);
196 let red2 = Srgb::new(254, 1, 1, 100.0);
197
198 assert!(red1.close_to(red2, 2.0));
199 assert!(red1.delta_e(red2) < 2.0);
200 }
201
202 #[test]
203 fn test_different_colors() {
204 let red = Srgb::new(255, 0, 0, 100.0);
205 let blue = Srgb::new(0, 0, 255, 100.0);
206
207 assert!(!red.close_to(blue, 10.0));
208 assert!(red.delta_e(blue) > 40.0);
209 }
210
211 #[test]
212 fn test_cross_color_space_comparison() {
213 let red_srgb = Srgb::new(255, 0, 0, 100.0);
214 let red_hsl = Hsl::new(0.0, 100.0, 50.0, 100.0);
215
216 assert!(red_srgb.close_to(red_hsl, 2.0));
218 }
219
220 #[test]
221 fn test_tolerance_levels() {
222 let color1 = Srgb::new(100, 100, 100, 100.0);
223 let color2 = Srgb::new(110, 105, 95, 100.0);
224
225 let distance = color1.delta_e(color2);
226
227 assert!(!color1.close_to(color2, distance - 0.1));
229 assert!(color1.close_to(color2, distance + 0.1));
230 }
231
232 #[test]
233 fn test_alpha_ignored_in_distance() {
234 let color1 = Srgb::new(255, 0, 0, 100.0);
235 let color2 = Srgb::new(255, 0, 0, 50.0);
236
237 assert!(color1.close_to(color2, 0.1));
239 }
240}