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 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 c = ops::hypot(a, b);
283 let h = {
284 let h = ops::atan2(b.to_radians(), a.to_radians()).to_degrees();
285
286 if h < 0.0 {
287 h + 360.0
288 } else {
289 h
290 }
291 };
292
293 let chroma = c.clamp(0.0, 1.5);
294 let hue = h;
295
296 Lcha::new(lightness, chroma, hue, alpha)
297 }
298}
299
300impl From<Srgba> for Lcha {
303 fn from(value: Srgba) -> Self {
304 Laba::from(value).into()
305 }
306}
307
308impl From<Lcha> for Srgba {
309 fn from(value: Lcha) -> Self {
310 Laba::from(value).into()
311 }
312}
313
314impl From<LinearRgba> for Lcha {
315 fn from(value: LinearRgba) -> Self {
316 Laba::from(value).into()
317 }
318}
319
320impl From<Lcha> for LinearRgba {
321 fn from(value: Lcha) -> Self {
322 Laba::from(value).into()
323 }
324}
325
326impl From<Xyza> for Lcha {
327 fn from(value: Xyza) -> Self {
328 Laba::from(value).into()
329 }
330}
331
332impl From<Lcha> for Xyza {
333 fn from(value: Lcha) -> Self {
334 Laba::from(value).into()
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::{
342 color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
343 };
344
345 #[test]
346 fn test_to_from_srgba() {
347 for color in TEST_COLORS.iter() {
348 let rgb2: Srgba = (color.lch).into();
349 let lcha: Lcha = (color.rgb).into();
350 assert!(
351 color.rgb.distance(&rgb2) < 0.0001,
352 "{}: {:?} != {:?}",
353 color.name,
354 color.rgb,
355 rgb2
356 );
357 assert_approx_eq!(color.lch.lightness, lcha.lightness, 0.001);
358 if lcha.lightness > 0.01 {
359 assert_approx_eq!(color.lch.chroma, lcha.chroma, 0.1);
360 }
361 if lcha.lightness > 0.01 && lcha.chroma > 0.01 {
362 assert!(
363 (color.lch.hue - lcha.hue).abs() < 1.7,
364 "{:?} != {:?}",
365 color.lch,
366 lcha
367 );
368 }
369 assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001);
370 }
371 }
372
373 #[test]
374 fn test_to_from_linear() {
375 for color in TEST_COLORS.iter() {
376 let rgb2: LinearRgba = (color.lch).into();
377 let lcha: Lcha = (color.linear_rgb).into();
378 assert!(
379 color.linear_rgb.distance(&rgb2) < 0.0001,
380 "{}: {:?} != {:?}",
381 color.name,
382 color.linear_rgb,
383 rgb2
384 );
385 assert_approx_eq!(color.lch.lightness, lcha.lightness, 0.001);
386 if lcha.lightness > 0.01 {
387 assert_approx_eq!(color.lch.chroma, lcha.chroma, 0.1);
388 }
389 if lcha.lightness > 0.01 && lcha.chroma > 0.01 {
390 assert!(
391 (color.lch.hue - lcha.hue).abs() < 1.7,
392 "{:?} != {:?}",
393 color.lch,
394 lcha
395 );
396 }
397 assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001);
398 }
399 }
400}