Skip to main content

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