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(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 Oklcha {
22 pub lightness: f32,
24 pub chroma: f32,
26 pub hue: f32,
28 pub alpha: f32,
30}
31
32impl StandardColor for Oklcha {}
33
34impl Oklcha {
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 {
61 Self::new(lightness, chroma, hue, 1.0)
62 }
63
64 pub const fn with_lightness(self, lightness: f32) -> Self {
66 Self { lightness, ..self }
67 }
68
69 pub const fn with_chroma(self, chroma: f32) -> Self {
71 Self { chroma, ..self }
72 }
73
74 pub fn sequential_dispersed(index: u32) -> Self {
92 const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; const RATIO_360: f32 = 360.0 / u32::MAX as f32;
94
95 let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360;
100 Self::lch(0.75, 0.1, hue)
101 }
102}
103
104impl Default for Oklcha {
105 fn default() -> Self {
106 Self::new(1., 0., 0., 1.)
107 }
108}
109
110impl Mix for Oklcha {
111 #[inline]
112 fn mix(&self, other: &Self, factor: f32) -> Self {
113 let n_factor = 1.0 - factor;
114 Self {
115 lightness: self.lightness * n_factor + other.lightness * factor,
116 chroma: self.chroma * n_factor + other.chroma * factor,
117 hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
118 alpha: self.alpha * n_factor + other.alpha * factor,
119 }
120 }
121}
122
123impl Gray for Oklcha {
124 const BLACK: Self = Self::new(0., 0., 0., 1.);
125 const WHITE: Self = Self::new(1.0, 0.000000059604645, 90.0, 1.0);
126}
127
128impl Alpha for Oklcha {
129 #[inline]
130 fn with_alpha(&self, alpha: f32) -> Self {
131 Self { alpha, ..*self }
132 }
133
134 #[inline]
135 fn alpha(&self) -> f32 {
136 self.alpha
137 }
138
139 #[inline]
140 fn set_alpha(&mut self, alpha: f32) {
141 self.alpha = alpha;
142 }
143}
144
145impl Hue for Oklcha {
146 #[inline]
147 fn with_hue(&self, hue: f32) -> Self {
148 Self { hue, ..*self }
149 }
150
151 #[inline]
152 fn hue(&self) -> f32 {
153 self.hue
154 }
155
156 #[inline]
157 fn set_hue(&mut self, hue: f32) {
158 self.hue = hue;
159 }
160}
161
162impl Luminance for Oklcha {
163 #[inline]
164 fn with_luminance(&self, lightness: f32) -> Self {
165 Self { lightness, ..*self }
166 }
167
168 fn luminance(&self) -> f32 {
169 self.lightness
170 }
171
172 fn darker(&self, amount: f32) -> Self {
173 Self::new(
174 (self.lightness - amount).max(0.),
175 self.chroma,
176 self.hue,
177 self.alpha,
178 )
179 }
180
181 fn lighter(&self, amount: f32) -> Self {
182 Self::new(
183 (self.lightness + amount).min(1.),
184 self.chroma,
185 self.hue,
186 self.alpha,
187 )
188 }
189}
190
191impl EuclideanDistance for Oklcha {
192 #[inline]
193 fn distance_squared(&self, other: &Self) -> f32 {
194 (self.lightness - other.lightness).squared()
195 + (self.chroma - other.chroma).squared()
196 + (self.hue - other.hue).squared()
197 }
198}
199
200impl ColorToComponents for Oklcha {
201 fn to_f32_array(self) -> [f32; 4] {
202 [self.lightness, self.chroma, self.hue, self.alpha]
203 }
204
205 fn to_f32_array_no_alpha(self) -> [f32; 3] {
206 [self.lightness, self.chroma, self.hue]
207 }
208
209 fn to_vec4(self) -> Vec4 {
210 Vec4::new(self.lightness, self.chroma, self.hue, self.alpha)
211 }
212
213 fn to_vec3(self) -> Vec3 {
214 Vec3::new(self.lightness, self.chroma, self.hue)
215 }
216
217 fn from_f32_array(color: [f32; 4]) -> Self {
218 Self {
219 lightness: color[0],
220 chroma: color[1],
221 hue: color[2],
222 alpha: color[3],
223 }
224 }
225
226 fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
227 Self {
228 lightness: color[0],
229 chroma: color[1],
230 hue: color[2],
231 alpha: 1.0,
232 }
233 }
234
235 fn from_vec4(color: Vec4) -> Self {
236 Self {
237 lightness: color[0],
238 chroma: color[1],
239 hue: color[2],
240 alpha: color[3],
241 }
242 }
243
244 fn from_vec3(color: Vec3) -> Self {
245 Self {
246 lightness: color[0],
247 chroma: color[1],
248 hue: color[2],
249 alpha: 1.0,
250 }
251 }
252}
253
254impl From<Oklaba> for Oklcha {
255 fn from(
256 Oklaba {
257 lightness,
258 a,
259 b,
260 alpha,
261 }: Oklaba,
262 ) -> Self {
263 let chroma = ops::hypot(a, b);
264 let hue = ops::atan2(b, a).to_degrees();
265
266 let hue = if hue < 0.0 { hue + 360.0 } else { hue };
267
268 Oklcha::new(lightness, chroma, hue, alpha)
269 }
270}
271
272impl From<Oklcha> for Oklaba {
273 fn from(
274 Oklcha {
275 lightness,
276 chroma,
277 hue,
278 alpha,
279 }: Oklcha,
280 ) -> Self {
281 let l = lightness;
282 let (sin, cos) = ops::sin_cos(hue.to_radians());
283 let a = chroma * cos;
284 let b = chroma * sin;
285
286 Oklaba::new(l, a, b, alpha)
287 }
288}
289
290impl From<Hsla> for Oklcha {
293 fn from(value: Hsla) -> Self {
294 Oklaba::from(value).into()
295 }
296}
297
298impl From<Oklcha> for Hsla {
299 fn from(value: Oklcha) -> Self {
300 Oklaba::from(value).into()
301 }
302}
303
304impl From<Hsva> for Oklcha {
305 fn from(value: Hsva) -> Self {
306 Oklaba::from(value).into()
307 }
308}
309
310impl From<Oklcha> for Hsva {
311 fn from(value: Oklcha) -> Self {
312 Oklaba::from(value).into()
313 }
314}
315
316impl From<Hwba> for Oklcha {
317 fn from(value: Hwba) -> Self {
318 Oklaba::from(value).into()
319 }
320}
321
322impl From<Oklcha> for Hwba {
323 fn from(value: Oklcha) -> Self {
324 Oklaba::from(value).into()
325 }
326}
327
328impl From<Laba> for Oklcha {
329 fn from(value: Laba) -> Self {
330 Oklaba::from(value).into()
331 }
332}
333
334impl From<Oklcha> for Laba {
335 fn from(value: Oklcha) -> Self {
336 Oklaba::from(value).into()
337 }
338}
339
340impl From<Lcha> for Oklcha {
341 fn from(value: Lcha) -> Self {
342 Oklaba::from(value).into()
343 }
344}
345
346impl From<Oklcha> for Lcha {
347 fn from(value: Oklcha) -> Self {
348 Oklaba::from(value).into()
349 }
350}
351
352impl From<LinearRgba> for Oklcha {
353 fn from(value: LinearRgba) -> Self {
354 Oklaba::from(value).into()
355 }
356}
357
358impl From<Oklcha> for LinearRgba {
359 fn from(value: Oklcha) -> Self {
360 Oklaba::from(value).into()
361 }
362}
363
364impl From<Srgba> for Oklcha {
365 fn from(value: Srgba) -> Self {
366 Oklaba::from(value).into()
367 }
368}
369
370impl From<Oklcha> for Srgba {
371 fn from(value: Oklcha) -> Self {
372 Oklaba::from(value).into()
373 }
374}
375
376impl From<Xyza> for Oklcha {
377 fn from(value: Xyza) -> Self {
378 Oklaba::from(value).into()
379 }
380}
381
382impl From<Oklcha> for Xyza {
383 fn from(value: Oklcha) -> Self {
384 Oklaba::from(value).into()
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::{test_colors::TEST_COLORS, testing::assert_approx_eq};
392
393 #[test]
394 fn test_to_from_srgba() {
395 let oklcha = Oklcha::new(0.5, 0.5, 180.0, 1.0);
396 let srgba: Srgba = oklcha.into();
397 let oklcha2: Oklcha = srgba.into();
398 assert_approx_eq!(oklcha.lightness, oklcha2.lightness, 0.001);
399 assert_approx_eq!(oklcha.chroma, oklcha2.chroma, 0.001);
400 assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001);
401 assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001);
402 }
403
404 #[test]
405 fn test_to_from_srgba_2() {
406 for color in TEST_COLORS.iter() {
407 let rgb2: Srgba = (color.oklch).into();
408 let oklch: Oklcha = (color.rgb).into();
409 assert!(
410 color.rgb.distance(&rgb2) < 0.0001,
411 "{}: {:?} != {:?}",
412 color.name,
413 color.rgb,
414 rgb2
415 );
416 assert!(
417 color.oklch.distance(&oklch) < 0.0001,
418 "{}: {:?} != {:?}",
419 color.name,
420 color.oklch,
421 oklch
422 );
423 }
424 }
425
426 #[test]
427 fn test_to_from_linear() {
428 let oklcha = Oklcha::new(0.5, 0.5, 0.5, 1.0);
429 let linear: LinearRgba = oklcha.into();
430 let oklcha2: Oklcha = linear.into();
431 assert_approx_eq!(oklcha.lightness, oklcha2.lightness, 0.001);
432 assert_approx_eq!(oklcha.chroma, oklcha2.chroma, 0.001);
433 assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001);
434 assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001);
435 }
436}