moxcms/
luv.rs

1/*
2 * // Copyright 2024 (c) the Radzivon Bartoshyk. All rights reserved.
3 * //
4 * // Use of this source code is governed by a BSD-style
5 * // license that can be found in the LICENSE file.
6 */
7
8//! # Luv
9/// Struct representing a color in CIE LUV, a.k.a. L\*u\*v\*, color space
10#[repr(C)]
11#[derive(Debug, Copy, Clone, Default, PartialOrd)]
12pub struct Luv {
13    /// The L\* value (achromatic luminance) of the colour in 0–100 range.
14    pub l: f32,
15    /// The u\* value of the colour.
16    ///
17    /// Together with v\* value, it defines chromaticity of the colour.  The u\*
18    /// coordinate represents colour’s position on red-green axis with negative
19    /// values indicating more red and positive more green colour.  Typical
20    /// values are in -134–220 range (but exact range for ‘valid’ colours
21    /// depends on luminance and v\* value).
22    pub u: f32,
23    /// The u\* value of the colour.
24    ///
25    /// Together with u\* value, it defines chromaticity of the colour.  The v\*
26    /// coordinate represents colour’s position on blue-yellow axis with
27    /// negative values indicating more blue and positive more yellow colour.
28    /// Typical values are in -140–122 range (but exact range for ‘valid’
29    /// colours depends on luminance and u\* value).
30    pub v: f32,
31}
32
33/// Representing a color in cylindrical CIE LCh(uv) color space
34#[repr(C)]
35#[derive(Debug, Copy, Clone, Default, PartialOrd)]
36pub struct LCh {
37    /// The L\* value (achromatic luminance) of the colour in 0–100 range.
38    ///
39    /// This is the same value as in the [`Luv`] object.
40    pub l: f32,
41    /// The C\*_uv value (chroma) of the colour.
42    ///
43    /// Together with h_uv, it defines chromaticity of the colour.  The typical
44    /// values of the coordinate go from zero up to around 150 (but exact range
45    /// for ‘valid’ colours depends on luminance and hue).  Zero represents
46    /// shade of grey.
47    pub c: f32,
48    /// The h_uv value (hue) of the colour measured in radians.
49    ///
50    /// Together with C\*_uv, it defines chromaticity of the colour.  The value
51    /// represents an angle thus it wraps around τ.  Typically, the value will
52    /// be in the -π–π range.  The value is undefined if C\*_uv is zero.
53    pub h: f32,
54}
55
56use crate::mlaf::mlaf;
57use crate::{Chromaticity, Lab, Xyz};
58use num_traits::Pow;
59use pxfm::{f_atan2f, f_cbrtf, f_hypotf, f_powf, f_sincosf};
60use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
61
62pub(crate) const LUV_WHITE_U_PRIME: f32 = 4.0f32 * Chromaticity::D50.to_xyz().y
63    / (Chromaticity::D50.to_xyz().x
64        + 15.0 * Chromaticity::D50.to_xyz().y
65        + 3.0 * Chromaticity::D50.to_xyz().z);
66pub(crate) const LUV_WHITE_V_PRIME: f32 = 9.0f32 * Chromaticity::D50.to_xyz().y
67    / (Chromaticity::D50.to_xyz().x
68        + 15.0 * Chromaticity::D50.to_xyz().y
69        + 3.0 * Chromaticity::D50.to_xyz().z);
70
71pub(crate) const LUV_CUTOFF_FORWARD_Y: f32 = (6f32 / 29f32) * (6f32 / 29f32) * (6f32 / 29f32);
72pub(crate) const LUV_MULTIPLIER_FORWARD_Y: f32 = (29f32 / 3f32) * (29f32 / 3f32) * (29f32 / 3f32);
73pub(crate) const LUV_MULTIPLIER_INVERSE_Y: f32 = (3f32 / 29f32) * (3f32 / 29f32) * (3f32 / 29f32);
74impl Luv {
75    /// Converts CIE XYZ to CIE Luv using D50 white point
76    #[inline]
77    #[allow(clippy::manual_clamp)]
78    pub fn from_xyz(xyz: Xyz) -> Self {
79        let [x, y, z] = [xyz.x, xyz.y, xyz.z];
80        let den = mlaf(mlaf(x, 15.0, y), 3.0, z);
81
82        let l = (if y < LUV_CUTOFF_FORWARD_Y {
83            LUV_MULTIPLIER_FORWARD_Y * y
84        } else {
85            116. * f_cbrtf(y) - 16.
86        })
87        .min(100.)
88        .max(0.);
89        let (u, v);
90        if den != 0f32 {
91            let u_prime = 4. * x / den;
92            let v_prime = 9. * y / den;
93            u = 13. * l * (u_prime - LUV_WHITE_U_PRIME);
94            v = 13. * l * (v_prime - LUV_WHITE_V_PRIME);
95        } else {
96            u = 0.;
97            v = 0.;
98        }
99
100        Luv { l, u, v }
101    }
102
103    /// To [Xyz] using D50 colorimetry
104    #[inline]
105    pub fn to_xyz(&self) -> Xyz {
106        if self.l <= 0. {
107            return Xyz::new(0., 0., 0.);
108        }
109        let l13 = 1. / (13. * self.l);
110        let u = mlaf(LUV_WHITE_U_PRIME, self.u, l13);
111        let v = mlaf(LUV_WHITE_V_PRIME, self.v, l13);
112        let y = if self.l > 8. {
113            let jx = (self.l + 16.) / 116.;
114            jx * jx * jx
115        } else {
116            self.l * LUV_MULTIPLIER_INVERSE_Y
117        };
118        let (x, z);
119        if v != 0. {
120            let den = 1. / (4. * v);
121            x = y * 9. * u * den;
122            z = y * mlaf(mlaf(12.0, -3.0, u), -20., v) * den;
123        } else {
124            x = 0.;
125            z = 0.;
126        }
127
128        Xyz::new(x, y, z)
129    }
130
131    #[inline]
132    pub const fn new(l: f32, u: f32, v: f32) -> Luv {
133        Luv { l, u, v }
134    }
135}
136
137impl LCh {
138    #[inline]
139    pub const fn new(l: f32, c: f32, h: f32) -> Self {
140        LCh { l, c, h }
141    }
142
143    /// Converts Lab to LCh(uv)
144    #[inline]
145    pub fn from_luv(luv: Luv) -> Self {
146        LCh {
147            l: luv.l,
148            c: f_hypotf(luv.u, luv.v),
149            h: f_atan2f(luv.v, luv.u),
150        }
151    }
152
153    /// Converts Lab to LCh(ab)
154    #[inline]
155    pub fn from_lab(lab: Lab) -> Self {
156        LCh {
157            l: lab.l,
158            c: f_hypotf(lab.a, lab.b),
159            h: f_atan2f(lab.b, lab.a),
160        }
161    }
162
163    /// Computes LCh(uv)
164    #[inline]
165    pub fn from_xyz(xyz: Xyz) -> Self {
166        Self::from_luv(Luv::from_xyz(xyz))
167    }
168
169    /// Computes LCh(ab)
170    #[inline]
171    pub fn from_xyz_lab(xyz: Xyz) -> Self {
172        Self::from_lab(Lab::from_xyz(xyz))
173    }
174
175    /// Converts LCh(uv) to Luv
176    #[inline]
177    pub fn to_xyz(&self) -> Xyz {
178        self.to_luv().to_xyz()
179    }
180
181    /// Converts LCh(ab) to Lab
182    #[inline]
183    pub fn to_xyz_lab(&self) -> Xyz {
184        self.to_lab().to_xyz()
185    }
186
187    #[inline]
188    pub fn to_luv(&self) -> Luv {
189        let sincos = f_sincosf(self.h);
190        Luv {
191            l: self.l,
192            u: self.c * sincos.1,
193            v: self.c * sincos.0,
194        }
195    }
196
197    #[inline]
198    pub fn to_lab(&self) -> Lab {
199        let sincos = f_sincosf(self.h);
200        Lab {
201            l: self.l,
202            a: self.c * sincos.1,
203            b: self.c * sincos.0,
204        }
205    }
206}
207
208impl PartialEq<Luv> for Luv {
209    /// Compares two colours ignoring chromaticity if L\* is zero.
210    #[inline]
211    fn eq(&self, other: &Self) -> bool {
212        if self.l != other.l {
213            false
214        } else if self.l == 0.0 {
215            true
216        } else {
217            self.u == other.u && self.v == other.v
218        }
219    }
220}
221
222impl PartialEq<LCh> for LCh {
223    /// Compares two colours ignoring chromaticity if L\* is zero and hue if C\*
224    /// is zero.  Hues which are τ apart are compared equal.
225    #[inline]
226    fn eq(&self, other: &Self) -> bool {
227        if self.l != other.l {
228            false
229        } else if self.l == 0.0 {
230            true
231        } else if self.c != other.c {
232            false
233        } else if self.c == 0.0 {
234            true
235        } else {
236            use std::f32::consts::TAU;
237            self.h.rem_euclid(TAU) == other.h.rem_euclid(TAU)
238        }
239    }
240}
241
242impl Luv {
243    #[inline]
244    pub fn euclidean_distance(&self, other: Luv) -> f32 {
245        let dl = self.l - other.l;
246        let du = self.u - other.u;
247        let dv = self.v - other.v;
248        (dl * dl + du * du + dv * dv).sqrt()
249    }
250}
251
252impl LCh {
253    #[inline]
254    pub fn euclidean_distance(&self, other: LCh) -> f32 {
255        let dl = self.l - other.l;
256        let dc = self.c - other.c;
257        let dh = self.h - other.h;
258        (dl * dl + dc * dc + dh * dh).sqrt()
259    }
260}
261
262impl Luv {
263    #[inline]
264    pub const fn taxicab_distance(&self, other: Self) -> f32 {
265        let dl = self.l - other.l;
266        let du = self.u - other.u;
267        let dv = self.v - other.v;
268        dl.abs() + du.abs() + dv.abs()
269    }
270}
271
272impl LCh {
273    #[inline]
274    pub const fn taxicab_distance(&self, other: Self) -> f32 {
275        let dl = self.l - other.l;
276        let dc = self.c - other.c;
277        let dh = self.h - other.h;
278        dl.abs() + dc.abs() + dh.abs()
279    }
280}
281
282impl Add<Luv> for Luv {
283    type Output = Luv;
284
285    #[inline]
286    fn add(self, rhs: Luv) -> Luv {
287        Luv::new(self.l + rhs.l, self.u + rhs.u, self.v + rhs.v)
288    }
289}
290
291impl Add<LCh> for LCh {
292    type Output = LCh;
293
294    #[inline]
295    fn add(self, rhs: LCh) -> LCh {
296        LCh::new(self.l + rhs.l, self.c + rhs.c, self.h + rhs.h)
297    }
298}
299
300impl Sub<Luv> for Luv {
301    type Output = Luv;
302
303    #[inline]
304    fn sub(self, rhs: Luv) -> Luv {
305        Luv::new(self.l - rhs.l, self.u - rhs.u, self.v - rhs.v)
306    }
307}
308
309impl Sub<LCh> for LCh {
310    type Output = LCh;
311
312    #[inline]
313    fn sub(self, rhs: LCh) -> LCh {
314        LCh::new(self.l - rhs.l, self.c - rhs.c, self.h - rhs.h)
315    }
316}
317
318impl Mul<Luv> for Luv {
319    type Output = Luv;
320
321    #[inline]
322    fn mul(self, rhs: Luv) -> Luv {
323        Luv::new(self.l * rhs.l, self.u * rhs.u, self.v * rhs.v)
324    }
325}
326
327impl Mul<LCh> for LCh {
328    type Output = LCh;
329
330    #[inline]
331    fn mul(self, rhs: LCh) -> LCh {
332        LCh::new(self.l * rhs.l, self.c * rhs.c, self.h * rhs.h)
333    }
334}
335
336impl Div<Luv> for Luv {
337    type Output = Luv;
338
339    #[inline]
340    fn div(self, rhs: Luv) -> Luv {
341        Luv::new(self.l / rhs.l, self.u / rhs.u, self.v / rhs.v)
342    }
343}
344
345impl Div<LCh> for LCh {
346    type Output = LCh;
347
348    #[inline]
349    fn div(self, rhs: LCh) -> LCh {
350        LCh::new(self.l / rhs.l, self.c / rhs.c, self.h / rhs.h)
351    }
352}
353
354impl Add<f32> for Luv {
355    type Output = Luv;
356
357    #[inline]
358    fn add(self, rhs: f32) -> Self::Output {
359        Luv::new(self.l + rhs, self.u + rhs, self.v + rhs)
360    }
361}
362
363impl Add<f32> for LCh {
364    type Output = LCh;
365
366    #[inline]
367    fn add(self, rhs: f32) -> Self::Output {
368        LCh::new(self.l + rhs, self.c + rhs, self.h + rhs)
369    }
370}
371
372impl Sub<f32> for Luv {
373    type Output = Luv;
374
375    #[inline]
376    fn sub(self, rhs: f32) -> Self::Output {
377        Luv::new(self.l - rhs, self.u - rhs, self.v - rhs)
378    }
379}
380
381impl Sub<f32> for LCh {
382    type Output = LCh;
383
384    #[inline]
385    fn sub(self, rhs: f32) -> Self::Output {
386        LCh::new(self.l - rhs, self.c - rhs, self.h - rhs)
387    }
388}
389
390impl Mul<f32> for Luv {
391    type Output = Luv;
392
393    #[inline]
394    fn mul(self, rhs: f32) -> Self::Output {
395        Luv::new(self.l * rhs, self.u * rhs, self.v * rhs)
396    }
397}
398
399impl Mul<f32> for LCh {
400    type Output = LCh;
401
402    #[inline]
403    fn mul(self, rhs: f32) -> Self::Output {
404        LCh::new(self.l * rhs, self.c * rhs, self.h * rhs)
405    }
406}
407
408impl Div<f32> for Luv {
409    type Output = Luv;
410
411    #[inline]
412    fn div(self, rhs: f32) -> Self::Output {
413        Luv::new(self.l / rhs, self.u / rhs, self.v / rhs)
414    }
415}
416
417impl Div<f32> for LCh {
418    type Output = LCh;
419
420    #[inline]
421    fn div(self, rhs: f32) -> Self::Output {
422        LCh::new(self.l / rhs, self.c / rhs, self.h / rhs)
423    }
424}
425
426impl AddAssign<Luv> for Luv {
427    #[inline]
428    fn add_assign(&mut self, rhs: Luv) {
429        self.l += rhs.l;
430        self.u += rhs.u;
431        self.v += rhs.v;
432    }
433}
434
435impl AddAssign<LCh> for LCh {
436    #[inline]
437    fn add_assign(&mut self, rhs: LCh) {
438        self.l += rhs.l;
439        self.c += rhs.c;
440        self.h += rhs.h;
441    }
442}
443
444impl SubAssign<Luv> for Luv {
445    #[inline]
446    fn sub_assign(&mut self, rhs: Luv) {
447        self.l -= rhs.l;
448        self.u -= rhs.u;
449        self.v -= rhs.v;
450    }
451}
452
453impl SubAssign<LCh> for LCh {
454    #[inline]
455    fn sub_assign(&mut self, rhs: LCh) {
456        self.l -= rhs.l;
457        self.c -= rhs.c;
458        self.h -= rhs.h;
459    }
460}
461
462impl MulAssign<Luv> for Luv {
463    #[inline]
464    fn mul_assign(&mut self, rhs: Luv) {
465        self.l *= rhs.l;
466        self.u *= rhs.u;
467        self.v *= rhs.v;
468    }
469}
470
471impl MulAssign<LCh> for LCh {
472    #[inline]
473    fn mul_assign(&mut self, rhs: LCh) {
474        self.l *= rhs.l;
475        self.c *= rhs.c;
476        self.h *= rhs.h;
477    }
478}
479
480impl DivAssign<Luv> for Luv {
481    #[inline]
482    fn div_assign(&mut self, rhs: Luv) {
483        self.l /= rhs.l;
484        self.u /= rhs.u;
485        self.v /= rhs.v;
486    }
487}
488
489impl DivAssign<LCh> for LCh {
490    #[inline]
491    fn div_assign(&mut self, rhs: LCh) {
492        self.l /= rhs.l;
493        self.c /= rhs.c;
494        self.h /= rhs.h;
495    }
496}
497
498impl AddAssign<f32> for Luv {
499    #[inline]
500    fn add_assign(&mut self, rhs: f32) {
501        self.l += rhs;
502        self.u += rhs;
503        self.v += rhs;
504    }
505}
506
507impl AddAssign<f32> for LCh {
508    #[inline]
509    fn add_assign(&mut self, rhs: f32) {
510        self.l += rhs;
511        self.c += rhs;
512        self.h += rhs;
513    }
514}
515
516impl SubAssign<f32> for Luv {
517    #[inline]
518    fn sub_assign(&mut self, rhs: f32) {
519        self.l -= rhs;
520        self.u -= rhs;
521        self.v -= rhs;
522    }
523}
524
525impl SubAssign<f32> for LCh {
526    #[inline]
527    fn sub_assign(&mut self, rhs: f32) {
528        self.l -= rhs;
529        self.c -= rhs;
530        self.h -= rhs;
531    }
532}
533
534impl MulAssign<f32> for Luv {
535    #[inline]
536    fn mul_assign(&mut self, rhs: f32) {
537        self.l *= rhs;
538        self.u *= rhs;
539        self.v *= rhs;
540    }
541}
542
543impl MulAssign<f32> for LCh {
544    #[inline]
545    fn mul_assign(&mut self, rhs: f32) {
546        self.l *= rhs;
547        self.c *= rhs;
548        self.h *= rhs;
549    }
550}
551
552impl DivAssign<f32> for Luv {
553    #[inline]
554    fn div_assign(&mut self, rhs: f32) {
555        self.l /= rhs;
556        self.u /= rhs;
557        self.v /= rhs;
558    }
559}
560
561impl DivAssign<f32> for LCh {
562    #[inline]
563    fn div_assign(&mut self, rhs: f32) {
564        self.l /= rhs;
565        self.c /= rhs;
566        self.h /= rhs;
567    }
568}
569
570impl Neg for LCh {
571    type Output = LCh;
572
573    #[inline]
574    fn neg(self) -> Self::Output {
575        LCh::new(-self.l, -self.c, -self.h)
576    }
577}
578
579impl Neg for Luv {
580    type Output = Luv;
581
582    #[inline]
583    fn neg(self) -> Self::Output {
584        Luv::new(-self.l, -self.u, -self.v)
585    }
586}
587
588impl Pow<f32> for Luv {
589    type Output = Luv;
590
591    #[inline]
592    fn pow(self, rhs: f32) -> Self::Output {
593        Luv::new(
594            f_powf(self.l, rhs),
595            f_powf(self.u, rhs),
596            f_powf(self.v, rhs),
597        )
598    }
599}
600
601impl Pow<f32> for LCh {
602    type Output = LCh;
603
604    #[inline]
605    fn pow(self, rhs: f32) -> Self::Output {
606        LCh::new(
607            f_powf(self.l, rhs),
608            f_powf(self.c, rhs),
609            f_powf(self.h, rhs),
610        )
611    }
612}
613
614impl Pow<Luv> for Luv {
615    type Output = Luv;
616
617    #[inline]
618    fn pow(self, rhs: Luv) -> Self::Output {
619        Luv::new(
620            f_powf(self.l, rhs.l),
621            f_powf(self.u, rhs.u),
622            f_powf(self.v, rhs.v),
623        )
624    }
625}
626
627impl Pow<LCh> for LCh {
628    type Output = LCh;
629
630    #[inline]
631    fn pow(self, rhs: LCh) -> Self::Output {
632        LCh::new(
633            f_powf(self.l, rhs.l),
634            f_powf(self.c, rhs.c),
635            f_powf(self.h, rhs.h),
636        )
637    }
638}
639
640impl Luv {
641    #[inline]
642    pub fn sqrt(&self) -> Luv {
643        Luv::new(self.l.sqrt(), self.u.sqrt(), self.v.sqrt())
644    }
645
646    #[inline]
647    pub fn cbrt(&self) -> Luv {
648        Luv::new(f_cbrtf(self.l), f_cbrtf(self.u), f_cbrtf(self.v))
649    }
650}
651
652impl LCh {
653    #[inline]
654    pub fn sqrt(&self) -> LCh {
655        LCh::new(
656            if self.l < 0. { 0. } else { self.l.sqrt() },
657            if self.c < 0. { 0. } else { self.c.sqrt() },
658            if self.h < 0. { 0. } else { self.h.sqrt() },
659        )
660    }
661
662    #[inline]
663    pub fn cbrt(&self) -> LCh {
664        LCh::new(f_cbrtf(self.l), f_cbrtf(self.c), f_cbrtf(self.h))
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    #[test]
673    fn round_trip_luv() {
674        let xyz = Xyz::new(0.1, 0.2, 0.3);
675        let lab = Luv::from_xyz(xyz);
676        let rolled_back = lab.to_xyz();
677        let dx = (xyz.x - rolled_back.x).abs();
678        let dy = (xyz.y - rolled_back.y).abs();
679        let dz = (xyz.z - rolled_back.z).abs();
680        assert!(dx < 1e-5);
681        assert!(dy < 1e-5);
682        assert!(dz < 1e-5);
683    }
684
685    #[test]
686    fn round_trip_lch() {
687        let xyz = Xyz::new(0.1, 0.2, 0.3);
688        let luv = Luv::from_xyz(xyz);
689        let lab = LCh::from_luv(luv);
690        let rolled_back = lab.to_luv();
691        let dx = (luv.l - rolled_back.l).abs();
692        let dy = (luv.u - rolled_back.u).abs();
693        let dz = (luv.v - rolled_back.v).abs();
694        assert!(dx < 1e-4);
695        assert!(dy < 1e-4);
696        assert!(dz < 1e-4);
697    }
698}