1use crate::{
2 color_difference::EuclideanDistance, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba,
3 Laba, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza,
4};
5use bevy_math::{ops, FloatPow, 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 Oklcha {
26 pub lightness: f32,
28 pub chroma: f32,
30 pub hue: f32,
32 pub alpha: f32,
34}
35
36impl StandardColor for Oklcha {}
37
38impl Oklcha {
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 {
65 Self::new(lightness, chroma, hue, 1.0)
66 }
67
68 pub const fn with_lightness(self, lightness: f32) -> Self {
70 Self { lightness, ..self }
71 }
72
73 pub const fn with_chroma(self, chroma: f32) -> Self {
75 Self { chroma, ..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.1, hue)
105 }
106}
107
108impl Default for Oklcha {
109 fn default() -> Self {
110 Self::new(1., 0., 0., 1.)
111 }
112}
113
114impl Mix for Oklcha {
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 Oklcha {
128 const BLACK: Self = Self::new(0., 0., 0., 1.);
129 const WHITE: Self = Self::new(1.0, 0.000000059604645, 90.0, 1.0);
130}
131
132impl Alpha for Oklcha {
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 Oklcha {
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 Oklcha {
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 EuclideanDistance for Oklcha {
196 #[inline]
197 fn distance_squared(&self, other: &Self) -> f32 {
198 (self.lightness - other.lightness).squared()
199 + (self.chroma - other.chroma).squared()
200 + (self.hue - other.hue).squared()
201 }
202}
203
204impl ColorToComponents for Oklcha {
205 fn to_f32_array(self) -> [f32; 4] {
206 [self.lightness, self.chroma, self.hue, self.alpha]
207 }
208
209 fn to_f32_array_no_alpha(self) -> [f32; 3] {
210 [self.lightness, self.chroma, self.hue]
211 }
212
213 fn to_vec4(self) -> Vec4 {
214 Vec4::new(self.lightness, self.chroma, self.hue, self.alpha)
215 }
216
217 fn to_vec3(self) -> Vec3 {
218 Vec3::new(self.lightness, self.chroma, self.hue)
219 }
220
221 fn from_f32_array(color: [f32; 4]) -> Self {
222 Self {
223 lightness: color[0],
224 chroma: color[1],
225 hue: color[2],
226 alpha: color[3],
227 }
228 }
229
230 fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
231 Self {
232 lightness: color[0],
233 chroma: color[1],
234 hue: color[2],
235 alpha: 1.0,
236 }
237 }
238
239 fn from_vec4(color: Vec4) -> Self {
240 Self {
241 lightness: color[0],
242 chroma: color[1],
243 hue: color[2],
244 alpha: color[3],
245 }
246 }
247
248 fn from_vec3(color: Vec3) -> Self {
249 Self {
250 lightness: color[0],
251 chroma: color[1],
252 hue: color[2],
253 alpha: 1.0,
254 }
255 }
256}
257
258impl From<Oklaba> for Oklcha {
259 fn from(
260 Oklaba {
261 lightness,
262 a,
263 b,
264 alpha,
265 }: Oklaba,
266 ) -> Self {
267 let chroma = ops::hypot(a, b);
268 let hue = ops::atan2(b, a).to_degrees();
269
270 let hue = if hue < 0.0 { hue + 360.0 } else { hue };
271
272 Oklcha::new(lightness, chroma, hue, alpha)
273 }
274}
275
276impl From<Oklcha> for Oklaba {
277 fn from(
278 Oklcha {
279 lightness,
280 chroma,
281 hue,
282 alpha,
283 }: Oklcha,
284 ) -> Self {
285 let l = lightness;
286 let (sin, cos) = ops::sin_cos(hue.to_radians());
287 let a = chroma * cos;
288 let b = chroma * sin;
289
290 Oklaba::new(l, a, b, alpha)
291 }
292}
293
294impl From<Hsla> for Oklcha {
297 fn from(value: Hsla) -> Self {
298 Oklaba::from(value).into()
299 }
300}
301
302impl From<Oklcha> for Hsla {
303 fn from(value: Oklcha) -> Self {
304 Oklaba::from(value).into()
305 }
306}
307
308impl From<Hsva> for Oklcha {
309 fn from(value: Hsva) -> Self {
310 Oklaba::from(value).into()
311 }
312}
313
314impl From<Oklcha> for Hsva {
315 fn from(value: Oklcha) -> Self {
316 Oklaba::from(value).into()
317 }
318}
319
320impl From<Hwba> for Oklcha {
321 fn from(value: Hwba) -> Self {
322 Oklaba::from(value).into()
323 }
324}
325
326impl From<Oklcha> for Hwba {
327 fn from(value: Oklcha) -> Self {
328 Oklaba::from(value).into()
329 }
330}
331
332impl From<Laba> for Oklcha {
333 fn from(value: Laba) -> Self {
334 Oklaba::from(value).into()
335 }
336}
337
338impl From<Oklcha> for Laba {
339 fn from(value: Oklcha) -> Self {
340 Oklaba::from(value).into()
341 }
342}
343
344impl From<Lcha> for Oklcha {
345 fn from(value: Lcha) -> Self {
346 Oklaba::from(value).into()
347 }
348}
349
350impl From<Oklcha> for Lcha {
351 fn from(value: Oklcha) -> Self {
352 Oklaba::from(value).into()
353 }
354}
355
356impl From<LinearRgba> for Oklcha {
357 fn from(value: LinearRgba) -> Self {
358 Oklaba::from(value).into()
359 }
360}
361
362impl From<Oklcha> for LinearRgba {
363 fn from(value: Oklcha) -> Self {
364 Oklaba::from(value).into()
365 }
366}
367
368impl From<Srgba> for Oklcha {
369 fn from(value: Srgba) -> Self {
370 Oklaba::from(value).into()
371 }
372}
373
374impl From<Oklcha> for Srgba {
375 fn from(value: Oklcha) -> Self {
376 Oklaba::from(value).into()
377 }
378}
379
380impl From<Xyza> for Oklcha {
381 fn from(value: Xyza) -> Self {
382 Oklaba::from(value).into()
383 }
384}
385
386impl From<Oklcha> for Xyza {
387 fn from(value: Oklcha) -> Self {
388 Oklaba::from(value).into()
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use crate::{test_colors::TEST_COLORS, testing::assert_approx_eq};
396
397 #[test]
398 fn test_to_from_srgba() {
399 let oklcha = Oklcha::new(0.5, 0.5, 180.0, 1.0);
400 let srgba: Srgba = oklcha.into();
401 let oklcha2: Oklcha = srgba.into();
402 assert_approx_eq!(oklcha.lightness, oklcha2.lightness, 0.001);
403 assert_approx_eq!(oklcha.chroma, oklcha2.chroma, 0.001);
404 assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001);
405 assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001);
406 }
407
408 #[test]
409 fn test_to_from_srgba_2() {
410 for color in TEST_COLORS.iter() {
411 let rgb2: Srgba = (color.oklch).into();
412 let oklch: Oklcha = (color.rgb).into();
413 assert!(
414 color.rgb.distance(&rgb2) < 0.0001,
415 "{}: {:?} != {:?}",
416 color.name,
417 color.rgb,
418 rgb2
419 );
420 assert!(
421 color.oklch.distance(&oklch) < 0.0001,
422 "{}: {:?} != {:?}",
423 color.name,
424 color.oklch,
425 oklch
426 );
427 }
428 }
429
430 #[test]
431 fn test_to_from_linear() {
432 let oklcha = Oklcha::new(0.5, 0.5, 0.5, 1.0);
433 let linear: LinearRgba = oklcha.into();
434 let oklcha2: Oklcha = linear.into();
435 assert_approx_eq!(oklcha.lightness, oklcha2.lightness, 0.001);
436 assert_approx_eq!(oklcha.chroma, oklcha2.chroma, 0.001);
437 assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001);
438 assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001);
439 }
440}