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