1use crate::{
2 Alpha, ColorToComponents, Gray, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor,
3 Xyza,
4};
5use bevy_math::{Vec3, Vec4};
6use bevy_reflect::prelude::*;
7
8#[doc = include_str!("../docs/conversion.md")]
10#[doc = include_str!("../docs/diagrams/model_graph.svg")]
12#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
14#[reflect(PartialEq, Default)]
15#[cfg_attr(
16 feature = "serialize",
17 derive(serde::Serialize, serde::Deserialize),
18 reflect(Serialize, Deserialize)
19)]
20pub struct Lcha {
21 pub lightness: f32,
23 pub chroma: f32,
25 pub hue: f32,
27 pub alpha: f32,
29}
30
31impl StandardColor for Lcha {}
32
33impl Lcha {
34 pub const fn new(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self {
43 Self {
44 lightness,
45 chroma,
46 hue,
47 alpha,
48 }
49 }
50
51 pub const fn lch(lightness: f32, chroma: f32, hue: f32) -> Self {
59 Self {
60 lightness,
61 chroma,
62 hue,
63 alpha: 1.0,
64 }
65 }
66
67 pub const fn with_chroma(self, chroma: f32) -> Self {
69 Self { chroma, ..self }
70 }
71
72 pub const fn with_lightness(self, lightness: f32) -> Self {
74 Self { lightness, ..self }
75 }
76
77 pub fn sequential_dispersed(index: u32) -> Self {
95 const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; const RATIO_360: f32 = 360.0 / u32::MAX as f32;
97
98 let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360;
103 Self::lch(0.75, 0.35, hue)
104 }
105}
106
107impl Default for Lcha {
108 fn default() -> Self {
109 Self::new(1., 0., 0., 1.)
110 }
111}
112
113impl Mix for Lcha {
114 #[inline]
115 fn mix(&self, other: &Self, factor: f32) -> Self {
116 let n_factor = 1.0 - factor;
117 Self {
118 lightness: self.lightness * n_factor + other.lightness * factor,
119 chroma: self.chroma * n_factor + other.chroma * factor,
120 hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
121 alpha: self.alpha * n_factor + other.alpha * factor,
122 }
123 }
124}
125
126impl Gray for Lcha {
127 const BLACK: Self = Self::new(0.0, 0.0, 0.0000136603785, 1.0);
128 const WHITE: Self = Self::new(1.0, 0.0, 0.0000136603785, 1.0);
129}
130
131impl Alpha for Lcha {
132 #[inline]
133 fn with_alpha(&self, alpha: f32) -> Self {
134 Self { alpha, ..*self }
135 }
136
137 #[inline]
138 fn alpha(&self) -> f32 {
139 self.alpha
140 }
141
142 #[inline]
143 fn set_alpha(&mut self, alpha: f32) {
144 self.alpha = alpha;
145 }
146}
147
148impl Hue for Lcha {
149 #[inline]
150 fn with_hue(&self, hue: f32) -> Self {
151 Self { hue, ..*self }
152 }
153
154 #[inline]
155 fn hue(&self) -> f32 {
156 self.hue
157 }
158
159 #[inline]
160 fn set_hue(&mut self, hue: f32) {
161 self.hue = hue;
162 }
163}
164
165impl Luminance for Lcha {
166 #[inline]
167 fn with_luminance(&self, lightness: f32) -> Self {
168 Self { lightness, ..*self }
169 }
170
171 fn luminance(&self) -> f32 {
172 self.lightness
173 }
174
175 fn darker(&self, amount: f32) -> Self {
176 Self::new(
177 (self.lightness - amount).max(0.),
178 self.chroma,
179 self.hue,
180 self.alpha,
181 )
182 }
183
184 fn lighter(&self, amount: f32) -> Self {
185 Self::new(
186 (self.lightness + amount).min(1.),
187 self.chroma,
188 self.hue,
189 self.alpha,
190 )
191 }
192}
193
194impl ColorToComponents for Lcha {
195 fn to_f32_array(self) -> [f32; 4] {
196 [self.lightness, self.chroma, self.hue, self.alpha]
197 }
198
199 fn to_f32_array_no_alpha(self) -> [f32; 3] {
200 [self.lightness, self.chroma, self.hue]
201 }
202
203 fn to_vec4(self) -> Vec4 {
204 Vec4::new(self.lightness, self.chroma, self.hue, self.alpha)
205 }
206
207 fn to_vec3(self) -> Vec3 {
208 Vec3::new(self.lightness, self.chroma, self.hue)
209 }
210
211 fn from_f32_array(color: [f32; 4]) -> Self {
212 Self {
213 lightness: color[0],
214 chroma: color[1],
215 hue: color[2],
216 alpha: color[3],
217 }
218 }
219
220 fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
221 Self {
222 lightness: color[0],
223 chroma: color[1],
224 hue: color[2],
225 alpha: 1.0,
226 }
227 }
228
229 fn from_vec4(color: Vec4) -> Self {
230 Self {
231 lightness: color[0],
232 chroma: color[1],
233 hue: color[2],
234 alpha: color[3],
235 }
236 }
237
238 fn from_vec3(color: Vec3) -> Self {
239 Self {
240 lightness: color[0],
241 chroma: color[1],
242 hue: color[2],
243 alpha: 1.0,
244 }
245 }
246}
247
248impl From<Lcha> for Laba {
249 fn from(
250 Lcha {
251 lightness,
252 chroma,
253 hue,
254 alpha,
255 }: Lcha,
256 ) -> Self {
257 let l = lightness;
259 let a = chroma * hue.to_radians().cos();
260 let b = chroma * hue.to_radians().sin();
261
262 Laba::new(l, a, b, alpha)
263 }
264}
265
266impl From<Laba> for Lcha {
267 fn from(
268 Laba {
269 lightness,
270 a,
271 b,
272 alpha,
273 }: Laba,
274 ) -> Self {
275 let c = (a.powf(2.0) + b.powf(2.0)).sqrt();
277 let h = {
278 let h = b.to_radians().atan2(a.to_radians()).to_degrees();
279
280 if h < 0.0 {
281 h + 360.0
282 } else {
283 h
284 }
285 };
286
287 let chroma = c.clamp(0.0, 1.5);
288 let hue = h;
289
290 Lcha::new(lightness, chroma, hue, alpha)
291 }
292}
293
294impl From<Srgba> for Lcha {
297 fn from(value: Srgba) -> Self {
298 Laba::from(value).into()
299 }
300}
301
302impl From<Lcha> for Srgba {
303 fn from(value: Lcha) -> Self {
304 Laba::from(value).into()
305 }
306}
307
308impl From<LinearRgba> for Lcha {
309 fn from(value: LinearRgba) -> Self {
310 Laba::from(value).into()
311 }
312}
313
314impl From<Lcha> for LinearRgba {
315 fn from(value: Lcha) -> Self {
316 Laba::from(value).into()
317 }
318}
319
320impl From<Xyza> for Lcha {
321 fn from(value: Xyza) -> Self {
322 Laba::from(value).into()
323 }
324}
325
326impl From<Lcha> for Xyza {
327 fn from(value: Lcha) -> Self {
328 Laba::from(value).into()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::{
336 color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
337 };
338
339 #[test]
340 fn test_to_from_srgba() {
341 for color in TEST_COLORS.iter() {
342 let rgb2: Srgba = (color.lch).into();
343 let lcha: Lcha = (color.rgb).into();
344 assert!(
345 color.rgb.distance(&rgb2) < 0.0001,
346 "{}: {:?} != {:?}",
347 color.name,
348 color.rgb,
349 rgb2
350 );
351 assert_approx_eq!(color.lch.lightness, lcha.lightness, 0.001);
352 if lcha.lightness > 0.01 {
353 assert_approx_eq!(color.lch.chroma, lcha.chroma, 0.1);
354 }
355 if lcha.lightness > 0.01 && lcha.chroma > 0.01 {
356 assert!(
357 (color.lch.hue - lcha.hue).abs() < 1.7,
358 "{:?} != {:?}",
359 color.lch,
360 lcha
361 );
362 }
363 assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001);
364 }
365 }
366
367 #[test]
368 fn test_to_from_linear() {
369 for color in TEST_COLORS.iter() {
370 let rgb2: LinearRgba = (color.lch).into();
371 let lcha: Lcha = (color.linear_rgb).into();
372 assert!(
373 color.linear_rgb.distance(&rgb2) < 0.0001,
374 "{}: {:?} != {:?}",
375 color.name,
376 color.linear_rgb,
377 rgb2
378 );
379 assert_approx_eq!(color.lch.lightness, lcha.lightness, 0.001);
380 if lcha.lightness > 0.01 {
381 assert_approx_eq!(color.lch.chroma, lcha.chroma, 0.1);
382 }
383 if lcha.lightness > 0.01 && lcha.chroma > 0.01 {
384 assert!(
385 (color.lch.hue - lcha.hue).abs() < 1.7,
386 "{:?} != {:?}",
387 color.lch,
388 lcha
389 );
390 }
391 assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001);
392 }
393 }
394}