color/
color.rs

1// Copyright 2024 the Color Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Concrete types for colors.
5
6use core::any::TypeId;
7use core::marker::PhantomData;
8
9use crate::{
10    cache_key::{BitEq, BitHash},
11    ColorSpace, ColorSpaceLayout, ColorSpaceTag, Oklab, Oklch, PremulRgba8, Rgba8, Srgb,
12};
13
14#[cfg(all(not(feature = "std"), not(test)))]
15use crate::floatfuncs::FloatFuncs;
16
17/// An opaque color.
18///
19/// A color in a color space known at compile time, without transparency. Note
20/// that "opaque" refers to the color, not the representation; the components
21/// are publicly accessible.
22///
23/// Arithmetic traits are defined on this type, and operate component-wise. A
24/// major motivation for including these is to enable weighted sums, including
25/// for spline interpolation. For cylindrical color spaces, hue fixup should
26/// be applied before interpolation.
27#[derive(Clone, Copy, Debug)]
28#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
29#[repr(transparent)]
30pub struct OpaqueColor<CS> {
31    /// The components, which may be manipulated directly.
32    ///
33    /// The interpretation of the components depends on the color space.
34    pub components: [f32; 3],
35    /// The color space.
36    pub cs: PhantomData<CS>,
37}
38
39/// A color with an alpha channel.
40///
41/// A color in a color space known at compile time, with an alpha channel.
42///
43/// See [`OpaqueColor`] for a discussion of arithmetic traits and interpolation.
44#[derive(Clone, Copy, Debug)]
45#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
46#[repr(transparent)]
47pub struct AlphaColor<CS> {
48    /// The components, which may be manipulated directly.
49    ///
50    /// The interpretation of the first three components depends on the color
51    /// space. The fourth component is separate alpha.
52    pub components: [f32; 4],
53    /// The color space.
54    pub cs: PhantomData<CS>,
55}
56
57/// A color with premultiplied alpha.
58///
59/// A color in a color space known at compile time, with a premultiplied
60/// alpha channel.
61///
62/// Following the convention of CSS Color 4, in cylindrical color spaces
63/// the hue channel is not premultiplied. If it were, interpolation would
64/// give undesirable results.
65///
66/// See [`OpaqueColor`] for a discussion of arithmetic traits and interpolation.
67#[derive(Clone, Copy, Debug)]
68#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
69#[repr(transparent)]
70pub struct PremulColor<CS> {
71    /// The components, which may be manipulated directly.
72    ///
73    /// The interpretation of the first three components depends on the color
74    /// space, and are premultiplied with the alpha value. The fourth component
75    /// is alpha.
76    ///
77    /// Note that in cylindrical color spaces, the hue component is not
78    /// premultiplied, as specified in the CSS Color 4 spec. The methods on
79    /// this type take care of that for you, but if you're manipulating the
80    /// components yourself, be aware.
81    pub components: [f32; 4],
82    /// The color space.
83    pub cs: PhantomData<CS>,
84}
85
86/// The hue direction for interpolation.
87///
88/// This type corresponds to [`hue-interpolation-method`] in the CSS Color
89/// 4 spec.
90///
91/// [`hue-interpolation-method`]: https://developer.mozilla.org/en-US/docs/Web/CSS/hue-interpolation-method
92#[derive(Clone, Copy, Default, Debug, PartialEq)]
93#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
94#[non_exhaustive]
95#[repr(u8)]
96pub enum HueDirection {
97    /// Hue angles take the shorter of the two arcs between starting and ending values.
98    #[default]
99    Shorter = 0,
100    /// Hue angles take the longer of the two arcs between starting and ending values.
101    Longer = 1,
102    /// Hue angles increase as they are interpolated.
103    Increasing = 2,
104    /// Hue angles decrease as they are interpolated.
105    Decreasing = 3,
106    // It's possible we'll add "raw"; color.js has it.
107    // NOTICE: If a new value is added, be sure to modify `MAX_VALUE` in the bytemuck impl.
108}
109
110/// Fixup hue based on specified hue direction.
111///
112/// Reference: ยง12.4 of CSS Color 4 spec
113///
114/// Note that this technique has been tweaked to only modify the second hue.
115/// The rationale for this is to support multiple gradient stops, for example
116/// in a spline. Apply the fixup to successive adjacent pairs.
117///
118/// In addition, hues outside [0, 360) are supported, with a resulting hue
119/// difference always in [-360, 360].
120fn fixup_hue(h1: f32, h2: &mut f32, direction: HueDirection) {
121    let dh = (*h2 - h1) * (1. / 360.);
122    match direction {
123        HueDirection::Shorter => {
124            // Round, resolving ties toward zero. This tricky formula
125            // has been validated to yield the correct result for all
126            // bit values of f32.
127            *h2 -= 360. * ((dh.abs() - 0.25) - 0.25).ceil().copysign(dh);
128        }
129        HueDirection::Longer => {
130            let t = 2.0 * dh.abs().ceil() - (dh.abs() + 1.5).floor();
131            *h2 += 360.0 * (t.copysign(0.0 - dh));
132        }
133        HueDirection::Increasing => *h2 -= 360.0 * dh.floor(),
134        HueDirection::Decreasing => *h2 -= 360.0 * dh.ceil(),
135    }
136}
137
138pub(crate) fn fixup_hues_for_interpolate(
139    a: [f32; 3],
140    b: &mut [f32; 3],
141    layout: ColorSpaceLayout,
142    direction: HueDirection,
143) {
144    if let Some(ix) = layout.hue_channel() {
145        fixup_hue(a[ix], &mut b[ix], direction);
146    }
147}
148
149impl<CS: ColorSpace> OpaqueColor<CS> {
150    /// A black color.
151    ///
152    /// More comprehensive pre-defined colors are available
153    /// in the [`color::palette`](crate::palette) module.
154    pub const BLACK: Self = Self::new([0., 0., 0.]);
155
156    /// A white color.
157    ///
158    /// This value is specific to the color space.
159    ///
160    /// More comprehensive pre-defined colors are available
161    /// in the [`color::palette`](crate::palette) module.
162    pub const WHITE: Self = Self::new(CS::WHITE_COMPONENTS);
163
164    /// Create a new color from the given components.
165    pub const fn new(components: [f32; 3]) -> Self {
166        let cs = PhantomData;
167        Self { components, cs }
168    }
169
170    /// Convert a color into a different color space.
171    #[must_use]
172    pub fn convert<TargetCS: ColorSpace>(self) -> OpaqueColor<TargetCS> {
173        OpaqueColor::new(CS::convert::<TargetCS>(self.components))
174    }
175
176    /// Add an alpha channel.
177    ///
178    /// This function is the inverse of [`AlphaColor::split`].
179    #[must_use]
180    pub const fn with_alpha(self, alpha: f32) -> AlphaColor<CS> {
181        AlphaColor::new(add_alpha(self.components, alpha))
182    }
183
184    /// Difference between two colors by Euclidean metric.
185    #[must_use]
186    pub fn difference(self, other: Self) -> f32 {
187        let d = (self - other).components;
188        (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
189    }
190
191    /// Linearly interpolate colors, without hue fixup.
192    ///
193    /// This method produces meaningful results in rectangular color spaces,
194    /// or if hue fixup has been applied.
195    #[must_use]
196    pub fn lerp_rect(self, other: Self, t: f32) -> Self {
197        self + t * (other - self)
198    }
199
200    /// Apply hue fixup for interpolation.
201    ///
202    /// Adjust the hue angle of `other` so that linear interpolation results in
203    /// the expected hue direction.
204    pub fn fixup_hues(self, other: &mut Self, direction: HueDirection) {
205        fixup_hues_for_interpolate(
206            self.components,
207            &mut other.components,
208            CS::LAYOUT,
209            direction,
210        );
211    }
212
213    /// Linearly interpolate colors, with hue fixup if needed.
214    #[must_use]
215    pub fn lerp(self, mut other: Self, t: f32, direction: HueDirection) -> Self {
216        self.fixup_hues(&mut other, direction);
217        self.lerp_rect(other, t)
218    }
219
220    /// Scale the chroma by the given amount.
221    ///
222    /// See [`ColorSpace::scale_chroma`] for more details.
223    #[must_use]
224    pub fn scale_chroma(self, scale: f32) -> Self {
225        Self::new(CS::scale_chroma(self.components, scale))
226    }
227
228    /// Compute the relative luminance of the color.
229    ///
230    /// This can be useful for choosing contrasting colors, and follows the
231    /// [WCAG 2.1 spec].
232    ///
233    /// [WCAG 2.1 spec]: https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
234    #[must_use]
235    pub fn relative_luminance(self) -> f32 {
236        let [r, g, b] = CS::to_linear_srgb(self.components);
237        0.2126 * r + 0.7152 * g + 0.0722 * b
238    }
239
240    /// Map components.
241    #[must_use]
242    pub fn map(self, f: impl Fn(f32, f32, f32) -> [f32; 3]) -> Self {
243        let [x, y, z] = self.components;
244        Self::new(f(x, y, z))
245    }
246
247    /// Map components in a given color space.
248    #[must_use]
249    pub fn map_in<TargetCS: ColorSpace>(self, f: impl Fn(f32, f32, f32) -> [f32; 3]) -> Self {
250        self.convert::<TargetCS>().map(f).convert()
251    }
252
253    /// Map the lightness of the color.
254    ///
255    /// In a color space that naturally has a lightness component, map that value.
256    /// Otherwise, do the mapping in [Oklab]. The lightness range is normalized so
257    /// that 1.0 is white. That is the normal range for [Oklab] but differs from the
258    /// range in [Lab], [Lch], and [Hsl].
259    ///
260    /// # Examples
261    ///
262    /// ```rust
263    /// use color::{Lab, OpaqueColor};
264    ///
265    /// let color = OpaqueColor::<Lab>::new([40., 4., -17.]);
266    /// let lighter = color.map_lightness(|l| l + 0.2);
267    /// let expected = OpaqueColor::<Lab>::new([60., 4., -17.]);
268    ///
269    /// assert!(lighter.difference(expected) < 1e-4);
270    /// ```
271    ///
272    /// [Lab]: crate::Lab
273    /// [Lch]: crate::Lch
274    /// [Hsl]: crate::Hsl
275    #[must_use]
276    pub fn map_lightness(self, f: impl Fn(f32) -> f32) -> Self {
277        match CS::TAG {
278            Some(ColorSpaceTag::Lab) | Some(ColorSpaceTag::Lch) => {
279                self.map(|l, c1, c2| [100.0 * f(l * 0.01), c1, c2])
280            }
281            Some(ColorSpaceTag::Oklab) | Some(ColorSpaceTag::Oklch) => {
282                self.map(|l, c1, c2| [f(l), c1, c2])
283            }
284            Some(ColorSpaceTag::Hsl) => self.map(|h, s, l| [h, s, 100.0 * f(l * 0.01)]),
285            _ => self.map_in::<Oklab>(|l, a, b| [f(l), a, b]),
286        }
287    }
288
289    /// Map the hue of the color.
290    ///
291    /// In a color space that naturally has a hue component, map that value.
292    /// Otherwise, do the mapping in [Oklch]. The hue is in degrees.
293    ///
294    /// # Examples
295    ///
296    /// ```rust
297    /// use color::{Oklab, OpaqueColor};
298    ///
299    /// let color = OpaqueColor::<Oklab>::new([0.5, 0.2, -0.1]);
300    /// let complementary = color.map_hue(|h| (h + 180.) % 360.);
301    /// let expected = OpaqueColor::<Oklab>::new([0.5, -0.2, 0.1]);
302    ///
303    /// assert!(complementary.difference(expected) < 1e-4);
304    /// ```
305    #[must_use]
306    pub fn map_hue(self, f: impl Fn(f32) -> f32) -> Self {
307        match CS::LAYOUT {
308            ColorSpaceLayout::HueFirst => self.map(|h, c1, c2| [f(h), c1, c2]),
309            ColorSpaceLayout::HueThird => self.map(|c0, c1, h| [c0, c1, f(h)]),
310            _ => self.map_in::<Oklch>(|l, c, h| [l, c, f(h)]),
311        }
312    }
313
314    /// Convert the color to [sRGB][Srgb] if not already in sRGB, and pack into 8 bit per component
315    /// integer encoding.
316    ///
317    /// The RGB components are mapped from the floating point range of `0.0-1.0` to the integer
318    /// range of `0-255`. Component values outside of this range are saturated to 0 or 255. The
319    /// alpha component is set to 255.
320    ///
321    /// # Implementation note
322    ///
323    /// This performs almost-correct rounding, see the note on [`AlphaColor::to_rgba8`].
324    #[must_use]
325    pub fn to_rgba8(self) -> Rgba8 {
326        self.with_alpha(1.0).to_rgba8()
327    }
328}
329
330pub(crate) const fn split_alpha([x, y, z, a]: [f32; 4]) -> ([f32; 3], f32) {
331    ([x, y, z], a)
332}
333
334pub(crate) const fn add_alpha([x, y, z]: [f32; 3], a: f32) -> [f32; 4] {
335    [x, y, z, a]
336}
337
338impl<CS: ColorSpace> AlphaColor<CS> {
339    /// A black color.
340    ///
341    /// More comprehensive pre-defined colors are available
342    /// in the [`color::palette`](crate::palette) module.
343    pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
344
345    /// A transparent color.
346    ///
347    /// This is a black color with full alpha.
348    ///
349    /// More comprehensive pre-defined colors are available
350    /// in the [`color::palette`](crate::palette) module.
351    pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
352
353    /// A white color.
354    ///
355    /// This value is specific to the color space.
356    ///
357    /// More comprehensive pre-defined colors are available
358    /// in the [`color::palette`](crate::palette) module.
359    pub const WHITE: Self = Self::new(add_alpha(CS::WHITE_COMPONENTS, 1.));
360
361    /// Create a new color from the given components.
362    pub const fn new(components: [f32; 4]) -> Self {
363        let cs = PhantomData;
364        Self { components, cs }
365    }
366
367    /// Split into opaque and alpha components.
368    ///
369    /// This function is the inverse of [`OpaqueColor::with_alpha`].
370    #[must_use]
371    pub const fn split(self) -> (OpaqueColor<CS>, f32) {
372        let (opaque, alpha) = split_alpha(self.components);
373        (OpaqueColor::new(opaque), alpha)
374    }
375
376    /// Set the alpha channel.
377    ///
378    /// This replaces the existing alpha channel. To scale or
379    /// or otherwise modify the existing alpha channel, use
380    /// [`AlphaColor::multiply_alpha`] or [`AlphaColor::map`].
381    ///
382    /// ```
383    /// let c = color::palette::css::GOLDENROD.with_alpha(0.5);
384    /// assert_eq!(0.5, c.split().1);
385    /// ```
386    #[must_use]
387    pub const fn with_alpha(self, alpha: f32) -> Self {
388        let (opaque, _alpha) = split_alpha(self.components);
389        Self::new(add_alpha(opaque, alpha))
390    }
391
392    /// Split out the opaque components, discarding the alpha.
393    ///
394    /// This is a shorthand for calling [`split`](Self::split).
395    #[must_use]
396    pub const fn discard_alpha(self) -> OpaqueColor<CS> {
397        self.split().0
398    }
399
400    /// Convert a color into a different color space.
401    #[must_use]
402    pub fn convert<TargetCs: ColorSpace>(self) -> AlphaColor<TargetCs> {
403        let (opaque, alpha) = split_alpha(self.components);
404        let components = CS::convert::<TargetCs>(opaque);
405        AlphaColor::new(add_alpha(components, alpha))
406    }
407
408    /// Convert a color to the corresponding premultiplied form.
409    #[must_use]
410    pub const fn premultiply(self) -> PremulColor<CS> {
411        let (opaque, alpha) = split_alpha(self.components);
412        PremulColor::new(add_alpha(CS::LAYOUT.scale(opaque, alpha), alpha))
413    }
414
415    /// Linearly interpolate colors, without hue fixup.
416    ///
417    /// This method produces meaningful results in rectangular color spaces,
418    /// or if hue fixup has been applied.
419    #[must_use]
420    pub fn lerp_rect(self, other: Self, t: f32) -> Self {
421        self.premultiply()
422            .lerp_rect(other.premultiply(), t)
423            .un_premultiply()
424    }
425
426    /// Linearly interpolate colors, with hue fixup if needed.
427    #[must_use]
428    pub fn lerp(self, other: Self, t: f32, direction: HueDirection) -> Self {
429        self.premultiply()
430            .lerp(other.premultiply(), t, direction)
431            .un_premultiply()
432    }
433
434    /// Multiply alpha by the given factor.
435    #[must_use]
436    pub const fn multiply_alpha(self, rhs: f32) -> Self {
437        let (opaque, alpha) = split_alpha(self.components);
438        Self::new(add_alpha(opaque, alpha * rhs))
439    }
440
441    /// Scale the chroma by the given amount.
442    ///
443    /// See [`ColorSpace::scale_chroma`] for more details.
444    #[must_use]
445    pub fn scale_chroma(self, scale: f32) -> Self {
446        let (opaque, alpha) = split_alpha(self.components);
447        Self::new(add_alpha(CS::scale_chroma(opaque, scale), alpha))
448    }
449
450    /// Map components.
451    #[must_use]
452    pub fn map(self, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self {
453        let [x, y, z, a] = self.components;
454        Self::new(f(x, y, z, a))
455    }
456
457    /// Map components in a given color space.
458    #[must_use]
459    pub fn map_in<TargetCS: ColorSpace>(self, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self {
460        self.convert::<TargetCS>().map(f).convert()
461    }
462
463    /// Map the lightness of the color.
464    ///
465    /// In a color space that naturally has a lightness component, map that value.
466    /// Otherwise, do the mapping in [Oklab]. The lightness range is normalized so
467    /// that 1.0 is white. That is the normal range for [Oklab] but differs from the
468    /// range in [Lab], [Lch], and [Hsl].
469    ///
470    /// # Examples
471    ///
472    /// ```rust
473    /// use color::{AlphaColor, Lab};
474    ///
475    /// let color = AlphaColor::<Lab>::new([40., 4., -17., 1.]);
476    /// let lighter = color.map_lightness(|l| l + 0.2);
477    /// let expected = AlphaColor::<Lab>::new([60., 4., -17., 1.]);
478    ///
479    /// assert!(lighter.premultiply().difference(expected.premultiply()) < 1e-4);
480    /// ```
481    ///
482    /// [Lab]: crate::Lab
483    /// [Lch]: crate::Lch
484    /// [Hsl]: crate::Hsl
485    #[must_use]
486    pub fn map_lightness(self, f: impl Fn(f32) -> f32) -> Self {
487        match CS::TAG {
488            Some(ColorSpaceTag::Lab) | Some(ColorSpaceTag::Lch) => {
489                self.map(|l, c1, c2, a| [100.0 * f(l * 0.01), c1, c2, a])
490            }
491            Some(ColorSpaceTag::Oklab) | Some(ColorSpaceTag::Oklch) => {
492                self.map(|l, c1, c2, a| [f(l), c1, c2, a])
493            }
494            Some(ColorSpaceTag::Hsl) => self.map(|h, s, l, a| [h, s, 100.0 * f(l * 0.01), a]),
495            _ => self.map_in::<Oklab>(|l, a, b, alpha| [f(l), a, b, alpha]),
496        }
497    }
498
499    /// Map the hue of the color.
500    ///
501    /// In a color space that naturally has a hue component, map that value.
502    /// Otherwise, do the mapping in [Oklch]. The hue is in degrees.
503    ///
504    /// # Examples
505    ///
506    /// ```rust
507    /// use color::{AlphaColor, Oklab};
508    ///
509    /// let color = AlphaColor::<Oklab>::new([0.5, 0.2, -0.1, 1.]);
510    /// let complementary = color.map_hue(|h| (h + 180.) % 360.);
511    /// let expected = AlphaColor::<Oklab>::new([0.5, -0.2, 0.1, 1.]);
512    ///
513    /// assert!(complementary.premultiply().difference(expected.premultiply()) < 1e-4);
514    /// ```
515    #[must_use]
516    pub fn map_hue(self, f: impl Fn(f32) -> f32) -> Self {
517        match CS::LAYOUT {
518            ColorSpaceLayout::HueFirst => self.map(|h, c1, c2, a| [f(h), c1, c2, a]),
519            ColorSpaceLayout::HueThird => self.map(|c0, c1, h, a| [c0, c1, f(h), a]),
520            _ => self.map_in::<Oklch>(|l, c, h, alpha| [l, c, f(h), alpha]),
521        }
522    }
523
524    /// Convert the color to [sRGB][Srgb] if not already in sRGB, and pack into 8 bit per component
525    /// integer encoding.
526    ///
527    /// The RGBA components are mapped from the floating point range of `0.0-1.0` to the integer
528    /// range of `0-255`. Component values outside of this range are saturated to 0 or 255.
529    ///
530    /// # Implementation note
531    ///
532    /// This performs almost-correct rounding to be fast on both x86 and AArch64 hardware. Within the
533    /// saturated output range of this method, `0-255`, there is a single color component value
534    /// where results differ: `0.0019607842`. This method maps that component to integer value `1`;
535    /// it would more precisely be mapped to `0`.
536    #[must_use]
537    pub fn to_rgba8(self) -> Rgba8 {
538        let [r, g, b, a] = self
539            .convert::<Srgb>()
540            .components
541            .map(|x| fast_round_to_u8(x * 255.));
542        Rgba8 { r, g, b, a }
543    }
544}
545
546impl<CS: ColorSpace> PremulColor<CS> {
547    /// A black color.
548    ///
549    /// More comprehensive pre-defined colors are available
550    /// in the [`color::palette`](crate::palette) module.
551    pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
552
553    /// A transparent color.
554    ///
555    /// This is a black color with full alpha.
556    ///
557    /// More comprehensive pre-defined colors are available
558    /// in the [`color::palette`](crate::palette) module.
559    pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
560
561    /// A white color.
562    ///
563    /// This value is specific to the color space.
564    ///
565    /// More comprehensive pre-defined colors are available
566    /// in the [`color::palette`](crate::palette) module.
567    pub const WHITE: Self = Self::new(add_alpha(CS::WHITE_COMPONENTS, 1.));
568
569    /// Create a new color from the given components.
570    pub const fn new(components: [f32; 4]) -> Self {
571        let cs = PhantomData;
572        Self { components, cs }
573    }
574
575    /// Split out the opaque components, discarding the alpha.
576    ///
577    /// This is a shorthand for un-premultiplying the alpha and
578    /// calling [`AlphaColor::discard_alpha`].
579    ///
580    /// The result of calling this on a fully transparent color
581    /// will be the color black.
582    #[must_use]
583    pub const fn discard_alpha(self) -> OpaqueColor<CS> {
584        self.un_premultiply().discard_alpha()
585    }
586
587    /// Convert a color into a different color space.
588    #[must_use]
589    pub fn convert<TargetCS: ColorSpace>(self) -> PremulColor<TargetCS> {
590        if TypeId::of::<CS>() == TypeId::of::<TargetCS>() {
591            PremulColor::new(self.components)
592        } else if TargetCS::IS_LINEAR && CS::IS_LINEAR {
593            let (multiplied, alpha) = split_alpha(self.components);
594            let components = CS::convert::<TargetCS>(multiplied);
595            PremulColor::new(add_alpha(components, alpha))
596        } else {
597            self.un_premultiply().convert().premultiply()
598        }
599    }
600
601    /// Convert a color to the corresponding separate alpha form.
602    #[must_use]
603    pub const fn un_premultiply(self) -> AlphaColor<CS> {
604        let (multiplied, alpha) = split_alpha(self.components);
605        let scale = if alpha == 0.0 { 1.0 } else { 1.0 / alpha };
606        AlphaColor::new(add_alpha(CS::LAYOUT.scale(multiplied, scale), alpha))
607    }
608
609    /// Interpolate colors.
610    ///
611    /// Note: this function doesn't fix up hue in cylindrical spaces. It is
612    /// still useful if the hue angles are compatible, particularly if the
613    /// fixup has been applied.
614    #[must_use]
615    pub fn lerp_rect(self, other: Self, t: f32) -> Self {
616        self + t * (other - self)
617    }
618
619    /// Apply hue fixup for interpolation.
620    ///
621    /// Adjust the hue angle of `other` so that linear interpolation results in
622    /// the expected hue direction.
623    pub fn fixup_hues(self, other: &mut Self, direction: HueDirection) {
624        if let Some(ix) = CS::LAYOUT.hue_channel() {
625            fixup_hue(self.components[ix], &mut other.components[ix], direction);
626        }
627    }
628
629    /// Linearly interpolate colors, with hue fixup if needed.
630    #[must_use]
631    pub fn lerp(self, mut other: Self, t: f32, direction: HueDirection) -> Self {
632        self.fixup_hues(&mut other, direction);
633        self.lerp_rect(other, t)
634    }
635
636    /// Multiply alpha by the given factor.
637    #[must_use]
638    pub const fn multiply_alpha(self, rhs: f32) -> Self {
639        let (multiplied, alpha) = split_alpha(self.components);
640        Self::new(add_alpha(CS::LAYOUT.scale(multiplied, rhs), alpha * rhs))
641    }
642
643    /// Difference between two colors by Euclidean metric.
644    #[must_use]
645    pub fn difference(self, other: Self) -> f32 {
646        let d = (self - other).components;
647        (d[0] * d[0] + d[1] * d[1] + d[2] * d[2] + d[3] * d[3]).sqrt()
648    }
649
650    /// Convert the color to [sRGB][Srgb] if not already in sRGB, and pack into 8 bit per component
651    /// integer encoding.
652    ///
653    /// The RGBA components are mapped from the floating point range of `0.0-1.0` to the integer
654    /// range of `0-255`. Component values outside of this range are saturated to 0 or 255.
655    ///
656    /// # Implementation note
657    ///
658    /// This performs almost-correct rounding, see the note on [`AlphaColor::to_rgba8`].
659    #[must_use]
660    pub fn to_rgba8(self) -> PremulRgba8 {
661        let [r, g, b, a] = self
662            .convert::<Srgb>()
663            .components
664            .map(|x| fast_round_to_u8(x * 255.));
665        PremulRgba8 { r, g, b, a }
666    }
667}
668
669/// Fast rounding of `f32` to integer `u8`, rounding ties up.
670///
671/// Targeting x86, `f32::round` calls out to libc `roundf`. Even if that call were inlined, it is
672/// branchy, which would make it relatively slow. The following is faster, and on the range `0-255`
673/// almost correct*. AArch64 has dedicated rounding instructions so does not need this
674/// optimization, but the following is still fast.
675///
676/// * The only input where the output differs from `a.round() as u8` is `0.49999997`.
677#[inline(always)]
678#[expect(clippy::cast_possible_truncation, reason = "deliberate quantization")]
679fn fast_round_to_u8(a: f32) -> u8 {
680    // This does not need clamping as the behavior of a `f32` to `u8` cast in Rust is to saturate.
681    (a + 0.5) as u8
682}
683
684// Lossless conversion traits.
685
686impl<CS: ColorSpace> From<OpaqueColor<CS>> for AlphaColor<CS> {
687    fn from(value: OpaqueColor<CS>) -> Self {
688        value.with_alpha(1.0)
689    }
690}
691
692impl<CS: ColorSpace> From<OpaqueColor<CS>> for PremulColor<CS> {
693    fn from(value: OpaqueColor<CS>) -> Self {
694        Self::new(add_alpha(value.components, 1.0))
695    }
696}
697
698// Partial equality - Hand derive to avoid needing ColorSpace to be PartialEq
699
700impl<CS: ColorSpace> PartialEq for AlphaColor<CS> {
701    fn eq(&self, other: &Self) -> bool {
702        self.components == other.components
703    }
704}
705
706impl<CS: ColorSpace> PartialEq for OpaqueColor<CS> {
707    fn eq(&self, other: &Self) -> bool {
708        self.components == other.components
709    }
710}
711
712impl<CS: ColorSpace> PartialEq for PremulColor<CS> {
713    fn eq(&self, other: &Self) -> bool {
714        self.components == other.components
715    }
716}
717
718/// Multiply components by a scalar.
719impl<CS: ColorSpace> core::ops::Mul<f32> for OpaqueColor<CS> {
720    type Output = Self;
721
722    fn mul(self, rhs: f32) -> Self {
723        Self::new(self.components.map(|x| x * rhs))
724    }
725}
726
727/// Multiply components by a scalar.
728impl<CS: ColorSpace> core::ops::Mul<OpaqueColor<CS>> for f32 {
729    type Output = OpaqueColor<CS>;
730
731    fn mul(self, rhs: OpaqueColor<CS>) -> Self::Output {
732        rhs * self
733    }
734}
735
736/// Divide components by a scalar.
737impl<CS: ColorSpace> core::ops::Div<f32> for OpaqueColor<CS> {
738    type Output = Self;
739
740    // https://github.com/rust-lang/rust-clippy/issues/13652 has been filed
741    #[expect(clippy::suspicious_arithmetic_impl, reason = "multiplicative inverse")]
742    fn div(self, rhs: f32) -> Self {
743        self * rhs.recip()
744    }
745}
746
747/// Component-wise addition of components.
748impl<CS: ColorSpace> core::ops::Add for OpaqueColor<CS> {
749    type Output = Self;
750
751    fn add(self, rhs: Self) -> Self {
752        let x = self.components;
753        let y = rhs.components;
754        Self::new([x[0] + y[0], x[1] + y[1], x[2] + y[2]])
755    }
756}
757
758/// Component-wise subtraction of components.
759impl<CS: ColorSpace> core::ops::Sub for OpaqueColor<CS> {
760    type Output = Self;
761
762    fn sub(self, rhs: Self) -> Self {
763        let x = self.components;
764        let y = rhs.components;
765        Self::new([x[0] - y[0], x[1] - y[1], x[2] - y[2]])
766    }
767}
768
769impl<CS> BitEq for OpaqueColor<CS> {
770    fn bit_eq(&self, other: &Self) -> bool {
771        self.components.bit_eq(&other.components)
772    }
773}
774
775impl<CS> BitHash for OpaqueColor<CS> {
776    fn bit_hash<H: core::hash::Hasher>(&self, state: &mut H) {
777        self.components.bit_hash(state);
778    }
779}
780
781/// Multiply components by a scalar.
782impl<CS: ColorSpace> core::ops::Mul<f32> for AlphaColor<CS> {
783    type Output = Self;
784
785    fn mul(self, rhs: f32) -> Self {
786        Self::new(self.components.map(|x| x * rhs))
787    }
788}
789
790/// Multiply components by a scalar.
791impl<CS: ColorSpace> core::ops::Mul<AlphaColor<CS>> for f32 {
792    type Output = AlphaColor<CS>;
793
794    fn mul(self, rhs: AlphaColor<CS>) -> Self::Output {
795        rhs * self
796    }
797}
798
799/// Divide components by a scalar.
800impl<CS: ColorSpace> core::ops::Div<f32> for AlphaColor<CS> {
801    type Output = Self;
802
803    #[expect(clippy::suspicious_arithmetic_impl, reason = "multiplicative inverse")]
804    fn div(self, rhs: f32) -> Self {
805        self * rhs.recip()
806    }
807}
808
809/// Component-wise addition of components.
810impl<CS: ColorSpace> core::ops::Add for AlphaColor<CS> {
811    type Output = Self;
812
813    fn add(self, rhs: Self) -> Self {
814        let x = self.components;
815        let y = rhs.components;
816        Self::new([x[0] + y[0], x[1] + y[1], x[2] + y[2], x[3] + y[3]])
817    }
818}
819
820/// Component-wise subtraction of components.
821impl<CS: ColorSpace> core::ops::Sub for AlphaColor<CS> {
822    type Output = Self;
823
824    fn sub(self, rhs: Self) -> Self {
825        let x = self.components;
826        let y = rhs.components;
827        Self::new([x[0] - y[0], x[1] - y[1], x[2] - y[2], x[3] - y[3]])
828    }
829}
830
831impl<CS> BitEq for AlphaColor<CS> {
832    fn bit_eq(&self, other: &Self) -> bool {
833        self.components.bit_eq(&other.components)
834    }
835}
836
837impl<CS> BitHash for AlphaColor<CS> {
838    fn bit_hash<H: core::hash::Hasher>(&self, state: &mut H) {
839        self.components.bit_hash(state);
840    }
841}
842
843/// Multiply components by a scalar.
844///
845/// For rectangular color spaces, this is equivalent to multiplying
846/// alpha, but for cylindrical color spaces, [`PremulColor::multiply_alpha`]
847/// is the preferred method.
848impl<CS: ColorSpace> core::ops::Mul<f32> for PremulColor<CS> {
849    type Output = Self;
850
851    fn mul(self, rhs: f32) -> Self {
852        Self::new(self.components.map(|x| x * rhs))
853    }
854}
855
856/// Multiply components by a scalar.
857impl<CS: ColorSpace> core::ops::Mul<PremulColor<CS>> for f32 {
858    type Output = PremulColor<CS>;
859
860    fn mul(self, rhs: PremulColor<CS>) -> Self::Output {
861        rhs * self
862    }
863}
864
865/// Divide components by a scalar.
866impl<CS: ColorSpace> core::ops::Div<f32> for PremulColor<CS> {
867    type Output = Self;
868
869    #[expect(clippy::suspicious_arithmetic_impl, reason = "multiplicative inverse")]
870    fn div(self, rhs: f32) -> Self {
871        self * rhs.recip()
872    }
873}
874
875/// Component-wise addition of components.
876impl<CS: ColorSpace> core::ops::Add for PremulColor<CS> {
877    type Output = Self;
878
879    fn add(self, rhs: Self) -> Self {
880        let x = self.components;
881        let y = rhs.components;
882        Self::new([x[0] + y[0], x[1] + y[1], x[2] + y[2], x[3] + y[3]])
883    }
884}
885
886/// Component-wise subtraction of components.
887impl<CS: ColorSpace> core::ops::Sub for PremulColor<CS> {
888    type Output = Self;
889
890    fn sub(self, rhs: Self) -> Self {
891        let x = self.components;
892        let y = rhs.components;
893        Self::new([x[0] - y[0], x[1] - y[1], x[2] - y[2], x[3] - y[3]])
894    }
895}
896
897impl<CS> BitEq for PremulColor<CS> {
898    fn bit_eq(&self, other: &Self) -> bool {
899        self.components.bit_eq(&other.components)
900    }
901}
902
903impl<CS> BitHash for PremulColor<CS> {
904    fn bit_hash<H: core::hash::Hasher>(&self, state: &mut H) {
905        self.components.bit_hash(state);
906    }
907}
908
909#[cfg(test)]
910mod tests {
911    extern crate alloc;
912
913    use super::{
914        fast_round_to_u8, fixup_hue, AlphaColor, HueDirection, PremulColor, PremulRgba8, Rgba8,
915        Srgb,
916    };
917
918    #[test]
919    fn to_rgba8_saturation() {
920        // This is just testing the Rust compiler behavior described in
921        // <https://github.com/rust-lang/rust/issues/10184>.
922        let (r, g, b, a) = (0, 0, 255, 255);
923
924        let ac = AlphaColor::<Srgb>::new([-1.01, -0.5, 1.01, 2.0]);
925        assert_eq!(ac.to_rgba8(), Rgba8 { r, g, b, a });
926
927        let pc = PremulColor::<Srgb>::new([-1.01, -0.5, 1.01, 2.0]);
928        assert_eq!(pc.to_rgba8(), PremulRgba8 { r, g, b, a });
929    }
930
931    #[test]
932    fn hue_fixup() {
933        // Verify that the hue arc matches the spec for all hues specified
934        // within [0,360).
935        for h1 in [0.0, 10.0, 180.0, 190.0, 350.0] {
936            for h2 in [0.0, 10.0, 180.0, 190.0, 350.0] {
937                let dh = h2 - h1;
938                {
939                    let mut fixed_h2 = h2;
940                    fixup_hue(h1, &mut fixed_h2, HueDirection::Shorter);
941                    let (mut spec_h1, mut spec_h2) = (h1, h2);
942                    if dh > 180.0 {
943                        spec_h1 += 360.0;
944                    } else if dh < -180.0 {
945                        spec_h2 += 360.0;
946                    }
947                    assert_eq!(fixed_h2 - h1, spec_h2 - spec_h1);
948                }
949
950                {
951                    let mut fixed_h2 = h2;
952                    fixup_hue(h1, &mut fixed_h2, HueDirection::Longer);
953                    let (mut spec_h1, mut spec_h2) = (h1, h2);
954                    if 0.0 < dh && dh < 180.0 {
955                        spec_h1 += 360.0;
956                    } else if -180.0 < dh && dh <= 0.0 {
957                        spec_h2 += 360.0;
958                    }
959                    assert_eq!(fixed_h2 - h1, spec_h2 - spec_h1);
960                }
961
962                {
963                    let mut fixed_h2 = h2;
964                    fixup_hue(h1, &mut fixed_h2, HueDirection::Increasing);
965                    let (spec_h1, mut spec_h2) = (h1, h2);
966                    if dh < 0.0 {
967                        spec_h2 += 360.0;
968                    }
969                    assert_eq!(fixed_h2 - h1, spec_h2 - spec_h1);
970                }
971
972                {
973                    let mut fixed_h2 = h2;
974                    fixup_hue(h1, &mut fixed_h2, HueDirection::Decreasing);
975                    let (mut spec_h1, spec_h2) = (h1, h2);
976                    if dh > 0.0 {
977                        spec_h1 += 360.0;
978                    }
979                    assert_eq!(fixed_h2 - h1, spec_h2 - spec_h1);
980                }
981            }
982        }
983    }
984
985    /// Test the claim in [`super::fast_round_to_u8`] that the only rounding failure in the range
986    /// of interest occurs for `0.49999997`.
987    #[test]
988    fn fast_round() {
989        #[expect(clippy::cast_possible_truncation, reason = "deliberate quantization")]
990        fn real_round_to_u8(v: f32) -> u8 {
991            v.round() as u8
992        }
993
994        // Check the rounding behavior at integer and half integer values within (and near) the
995        // range 0-255, as well as one ULP up and down from those values.
996        let mut failures = alloc::vec![];
997        let mut v = -1_f32;
998
999        while v <= 256. {
1000            // Note we don't get accumulation of rounding errors by incrementing with 0.5: integers
1001            // and half integers are exactly representable in this range.
1002            assert!(v.abs().fract() == 0. || v.abs().fract() == 0.5, "{v}");
1003
1004            let mut validate_rounding = |val: f32| {
1005                if real_round_to_u8(val) != fast_round_to_u8(val) {
1006                    failures.push(val);
1007                }
1008            };
1009
1010            validate_rounding(v.next_down().next_down());
1011            validate_rounding(v.next_down());
1012            validate_rounding(v);
1013            validate_rounding(v.next_up());
1014            validate_rounding(v.next_up().next_up());
1015
1016            v += 0.5;
1017        }
1018
1019        assert_eq!(&failures, &[0.49999997]);
1020    }
1021
1022    /// A more thorough test than the one above: the one above only tests values that are likely to
1023    /// fail. This test runs through all floats in and near the range of interest (approximately
1024    /// 200 million floats), so can be somewhat slow (seconds rather than milliseconds). To run
1025    /// this test, use the `--ignored` flag.
1026    #[test]
1027    #[ignore = "Takes too long to execute."]
1028    fn fast_round_full() {
1029        #[expect(clippy::cast_possible_truncation, reason = "deliberate quantization")]
1030        fn real_round_to_u8(v: f32) -> u8 {
1031            v.round() as u8
1032        }
1033
1034        // Check the rounding behavior of all floating point values within (and near) the range
1035        // 0-255.
1036        let mut failures = alloc::vec![];
1037        let mut v = -1_f32;
1038
1039        while v <= 256. {
1040            if real_round_to_u8(v) != fast_round_to_u8(v) {
1041                failures.push(v);
1042            }
1043            v = v.next_up();
1044        }
1045
1046        assert_eq!(&failures, &[0.49999997]);
1047    }
1048}