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