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