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