Skip to main content

all_is_cubes_base/math/
color.rs

1//! Color data types. This module is private but reexported by its parent.
2
3use core::fmt;
4use core::iter::Sum;
5use core::ops::{Add, AddAssign, Mul};
6
7use euclid::{Vector3D, vec3};
8use ordered_float::NotNan;
9
10/// Acts as polyfill for float methods
11#[cfg(not(feature = "std"))]
12#[allow(unused_imports)]
13use num_traits::float::Float as _;
14
15use crate::math::{NotPositiveSign, NotZeroOne, PositiveSign, ZeroOne};
16
17// -------------------------------------------------------------------------------------------------
18
19// TODO: Rename these macros to not have `_const` suffixes because the point of having a macro
20// named after a type is to construct it
21
22/// Allows writing a constant [`Rgb`] color value, provided that its components are float
23/// literals.
24///
25/// TODO: examples
26#[macro_export]
27macro_rules! rgb_const {
28    ($r:literal, $g:literal, $b:literal) => {
29        // const block ensures all panics are compile-time
30        const {
31            $crate::math::Rgb::new_ps(
32                $crate::math::PositiveSign::<f32>::new_strict($r),
33                $crate::math::PositiveSign::<f32>::new_strict($g),
34                $crate::math::PositiveSign::<f32>::new_strict($b),
35            )
36        }
37    };
38}
39
40/// Allows writing a constant [`Rgba`] color value, provided that its components are float
41/// literals.
42#[macro_export]
43macro_rules! rgba_const {
44    ($r:literal, $g:literal, $b:literal, $a:literal) => {
45        // const block ensures all panics are compile-time
46        const {
47            $crate::math::Rgba::new_ps(
48                $crate::math::PositiveSign::<f32>::new_strict($r),
49                $crate::math::PositiveSign::<f32>::new_strict($g),
50                $crate::math::PositiveSign::<f32>::new_strict($b),
51                $crate::math::ZeroOne::<f32>::new_strict($a),
52            )
53        }
54    };
55}
56
57/// Allows writing a constant [`Rgb01`] color value,
58/// provided that its components are float literals.
59///
60/// ```rust
61/// # use all_is_cubes_base::rgb01;
62/// let red = rgb01!(1.0, 0.0, 0.0);
63/// ```
64///
65/// Any invalid value will result in a compilation error.
66///
67/// ```rust,compile_fail
68/// # use all_is_cubes_base::rgb01;
69/// let too_much_red = rgb01!(2.0, 0.0, 0.0);
70/// ```
71#[macro_export]
72macro_rules! rgb01 {
73    ($r:literal, $g:literal, $b:literal) => {
74        // const block ensures all panics are compile-time
75        const {
76            $crate::math::Rgb01::new_zo(
77                $crate::math::ZeroOne::<f32>::new_strict($r),
78                $crate::math::ZeroOne::<f32>::new_strict($g),
79                $crate::math::ZeroOne::<f32>::new_strict($b),
80            )
81        }
82    };
83}
84
85// -------------------------------------------------------------------------------------------------
86
87/// A floating-point RGB color value, unbounded.
88///
89/// * Represents a light intensity in unspecified units.
90/// * Each color component must have a nonnegative, non-NaN [`f32`] value.
91/// * Color components are linear (gamma = 1), but use the same RGB primaries as sRGB
92///   (Rec. 709).
93///
94/// For colors whose components do not exceed 1, use [`Rgb01`] instead.
95///
96/// # Suitability
97///
98/// The primary purpose of this color type and its relatives, and the reason for its enforced
99/// value restrictions, is to be able to be stored in game state data structures,
100/// with reliable comparison and serialization.
101/// It is not necessarily appropriate for storing the intermediate results of computations
102/// (though the operations do take care to avoid unnecessary re-validation).
103///
104/// Future versions of All is Cubes may choose to reduce the precision of this type to `f16`
105/// when Rust has built-in support for that data type.
106#[derive(Clone, Copy, Eq, Hash, PartialEq)]
107pub struct Rgb(Vector3D<PositiveSign<f32>, Intensity>);
108
109/// A floating-point RGB color value, between zero and one.
110///
111/// * Represents relative color values such as the reflectance of a surface.
112///   If an unrestricted “HDR” color is required, use [`Rgb`] instead.
113/// * Each color component must have a [`f32`] value between 0.0 and 1.0.
114/// * Color components are linear (gamma = 1), but use the same RGB primaries as sRGB
115///   (Rec. 709).
116///
117/// # Suitability
118///
119/// The primary purpose of this color type and its relatives, and the reason for its enforced
120/// value restrictions, is to be able to be stored in game state data structures,
121/// with reliable comparison and serialization.
122/// It is not necessarily appropriate for storing the intermediate results of computations
123/// (though the operations do take care to avoid unnecessary re-validation).
124///
125/// Future versions of All is Cubes may choose to reduce the precision of this type to `f16`
126/// when Rust has built-in support for that data type.
127#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
128pub struct Rgb01(
129    Vector3D<
130        ZeroOne<f32>,
131        // This isn't actually unknown, but *unitless*, but the “unknown” type plays the closest
132        // role availablein [`euclid`].
133        euclid::UnknownUnit,
134    >,
135);
136
137/// A floating-point RGBA color value.
138///
139/// * Each color component must have a nonnegative, non-NaN value.
140///   Depending on the application, they may be considered to have a nominal
141///   range of 0 to 1, or unbounded. (TODO: Split these use cases into different types.)
142/// * The alpha must have a non-NaN value.
143/// * Color components are linear (gamma = 1), but use the same RGB primaries as sRGB
144///   (Rec. 709).
145/// * The alpha is not premultiplied.
146/// * Alpha values less than zero and greater than one will usually be treated equivalently to
147///   zero and one, respectively, but are preserved rather than clipped.
148///
149/// # Suitability
150///
151/// The primary purpose of this color type and its relatives, and the reason for its enforced
152/// value restrictions, is to be able to be stored in game state data structures,
153/// with reliable comparison and serialization.
154/// It is not necessarily appropriate for storing the intermediate results of computations
155/// (though the operations do take care to avoid unnecessary re-validation).
156///
157/// Future versions of All is Cubes may choose to reduce the precision of this type to `f16`
158/// when Rust has built-in support for that data type.
159#[derive(Clone, Copy, Eq, Hash, PartialEq)]
160pub struct Rgba {
161    rgb: Rgb,
162    alpha: ZeroOne<f32>,
163}
164
165/// Unit-of-measure type for vectors that contain color channels.
166//---
167// TODO: replace this with formally accurate units.
168#[expect(clippy::exhaustive_enums)]
169#[derive(Debug, Eq, PartialEq)]
170pub enum Intensity {}
171
172// -------------------------------------------------------------------------------------------------
173
174// convenience alias
175const PS0: PositiveSign<f32> = <PositiveSign<f32> as num_traits::ConstZero>::ZERO;
176const PS1: PositiveSign<f32> = <PositiveSign<f32> as num_traits::ConstOne>::ONE;
177
178// Note: `Rgb`, `Rgb01`, and `Rgba` each have similar, but not identical, impl blocks, which
179// are all immediately below. They should be kept in sync insofar as that makes sense for each type.
180
181impl Rgb {
182    /// Black; the constant equal to `Rgb::new(0., 0., 0.).unwrap()`.
183    pub const ZERO: Rgb = Rgb(vec3(PS0, PS0, PS0));
184    /// Nominal white; the constant equal to `Rgb::new(1., 1., 1.).unwrap()`.
185    ///
186    /// Note that brighter values may exist; the color system “supports HDR”.
187    pub const ONE: Rgb = Rgb(vec3(PS1, PS1, PS1));
188
189    /// Constructs a color from components.
190    ///
191    /// Panics if any component is NaN.
192    /// Clamps any component that is negative.
193    #[inline]
194    #[track_caller]
195    pub const fn new(r: f32, g: f32, b: f32) -> Self {
196        match Self::try_new(vec3(r, g, b)) {
197            Ok(color) => color,
198            Err(_) => panic!("color component out of range"),
199        }
200    }
201
202    const fn try_new(value: Vector3D<f32, Intensity>) -> Result<Self, NotPositiveSign<f32>> {
203        match (
204            PositiveSign::<f32>::try_new(value.x),
205            PositiveSign::<f32>::try_new(value.y),
206            PositiveSign::<f32>::try_new(value.z),
207        ) {
208            (Ok(r), Ok(g), Ok(b)) => Ok(Self(vec3(r, g, b))),
209            (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(e),
210        }
211    }
212
213    /// Constructs a color from components that have already been checked for not being
214    /// NaN or negative.
215    ///
216    /// Note: This exists primarily to assist the [`rgb_const!`] macro and may be renamed
217    /// or replaced in future versions.
218    #[inline]
219    pub const fn new_ps(r: PositiveSign<f32>, g: PositiveSign<f32>, b: PositiveSign<f32>) -> Self {
220        Self(vec3(r, g, b))
221    }
222
223    /// Constructs a shade of gray (components all equal). Panics if any component is NaN.
224    /// No other range checks are performed.
225    #[inline]
226    #[track_caller]
227    pub const fn from_luminance(luminance: f32) -> Self {
228        Self::new(luminance, luminance, luminance)
229    }
230
231    /// Adds an alpha component to produce an [Rgba] color.
232    #[inline]
233    pub const fn with_alpha(self, alpha: ZeroOne<f32>) -> Rgba {
234        Rgba { rgb: self, alpha }
235    }
236    /// Adds an alpha component of `1.0` (fully opaque) to produce an [Rgba] color.
237    #[inline]
238    pub const fn with_alpha_one(self) -> Rgba {
239        self.with_alpha(ZeroOne::ONE)
240    }
241
242    /// Adds an alpha component of `1.0` (fully opaque) to produce an [Rgba] color.
243    /// This is for compile-time duck-typed use by the `block::from_color!` macro.
244    #[doc(hidden)]
245    #[inline]
246    #[must_use]
247    pub const fn with_alpha_one_if_has_no_alpha(self) -> Rgba {
248        self.with_alpha(ZeroOne::ONE)
249    }
250
251    /// Returns the red color component. Values are linear (gamma = 1).
252    #[inline]
253    pub const fn red(self) -> PositiveSign<f32> {
254        self.0.x
255    }
256    /// Returns the green color component. Values are linear (gamma = 1).
257    #[inline]
258    pub const fn green(self) -> PositiveSign<f32> {
259        self.0.y
260    }
261    /// Returns the blue color component. Values are linear (gamma = 1).
262    #[inline]
263    pub const fn blue(self) -> PositiveSign<f32> {
264        self.0.z
265    }
266
267    /// Combines the red, green, and blue components to obtain a [relative luminance]
268    /// (“grayscale”) value. This will be equal to 1 if all components are 1.
269    ///
270    /// ```
271    /// # extern crate all_is_cubes_base as all_is_cubes;
272    /// use all_is_cubes::math::Rgb;
273    ///
274    /// assert_eq!(0.0, Rgb::ZERO.luminance());
275    /// assert_eq!(0.5, (Rgb::ONE * 0.5).luminance());
276    /// assert_eq!(1.0, Rgb::ONE.luminance());
277    /// assert_eq!(2.0, (Rgb::ONE * 2.0).luminance());
278    ///
279    /// assert_eq!(0.2126, Rgb::new(1., 0., 0.).luminance());
280    /// assert_eq!(0.7152, Rgb::new(0., 1., 0.).luminance());
281    /// assert_eq!(0.0722, Rgb::new(0., 0., 1.).luminance());
282    /// ```
283    ///
284    /// [relative luminance]: https://en.wikipedia.org/wiki/Relative_luminance
285    #[inline]
286    pub fn luminance(self) -> f32 {
287        // Coefficients as per
288        // https://en.wikipedia.org/wiki/Relative_luminance
289        // Rec. ITU-R BT.709-6 https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
290        //
291        // Arithmetic operations ordered for minimum floating point error.
292        // (This probably doesn't matter at all.)
293        self.green().into_inner() * 0.7152
294            + (self.red().into_inner() * 0.2126 + self.blue().into_inner() * 0.0722)
295    }
296
297    /// Converts sRGB 8-bits-per-component color to the corresponding linear [`Rgba`] value.
298    #[inline]
299    pub const fn from_srgb8(rgb: [u8; 3]) -> Self {
300        Self(vec3(
301            component_from_srgb8_const(rgb[0]).into_ps(),
302            component_from_srgb8_const(rgb[1]).into_ps(),
303            component_from_srgb8_const(rgb[2]).into_ps(),
304        ))
305    }
306
307    /// Clamp each component to lie within the range 0 to `maximum`, inclusive.
308    #[inline]
309    #[must_use]
310    pub fn clamp(self, maximum: PositiveSign<f32>) -> Self {
311        Self(self.0.map(|c| c.clamp(PS0, maximum)))
312    }
313
314    /// Clamp each component to lie within the range 0 to `maximum`, inclusive.
315    #[inline]
316    #[must_use]
317    pub fn clamp_01(self) -> Rgb01 {
318        Rgb01(self.0.map(PositiveSign::clamp_01).cast_unit())
319    }
320
321    /// Subtract `other` from `self`; if any component would be negative, it is zero instead.
322    #[inline]
323    #[must_use]
324    pub fn saturating_sub(self, other: Self) -> Self {
325        Self(vec3(
326            self.red().saturating_sub(other.red()),
327            self.green().saturating_sub(other.green()),
328            self.blue().saturating_sub(other.blue()),
329        ))
330    }
331}
332
333impl Rgb01 {
334    /// Black; identical to `Rgb01::new(0.0, 0.0, 0.0)` except for being a constant.
335    pub const BLACK: Self = Self::new_zo(ZeroOne::ZERO, ZeroOne::ZERO, ZeroOne::ZERO);
336    /// White; identical to `Rgb01::new(1.0, 1.0, 1.0)` except for being a constant.
337    pub const WHITE: Self = Self::new_zo(ZeroOne::ONE, ZeroOne::ONE, ZeroOne::ONE);
338
339    /// Pure red that is as bright as it can be,
340    /// while being the same luminance as the other colors in this set.
341    pub const UNIFORM_LUMINANCE_RED: Self = Rgb01::from_srgb8([0x9E, 0x00, 0x00]);
342    /// Pure green that is as bright as it can be,
343    /// while being the same luminance as the other colors in this set.
344    pub const UNIFORM_LUMINANCE_GREEN: Self = Rgb01::from_srgb8([0x00, 0x59, 0x00]);
345    /// Pure blue that is as bright as it can be,
346    /// while being the same luminance as the other colors in this set.
347    /// (That turns out to be 100% blue, `#0000FF`.)
348    pub const UNIFORM_LUMINANCE_BLUE: Self = Rgb01::from_srgb8([0x00, 0x00, 0xFF]);
349
350    /// Constructs a color from components. Panics if any component is NaN, below 0, or above 1.
351    #[inline]
352    #[track_caller]
353    pub const fn new(r: f32, g: f32, b: f32) -> Self {
354        match Self::try_new(vec3(r, g, b)) {
355            Ok(color) => color,
356            Err(_) => panic!("Rgb01 component out of range"),
357        }
358    }
359
360    const fn try_new(value: Vector3D<f32, Rgb01>) -> Result<Self, NotZeroOne<f32>> {
361        match (
362            ZeroOne::<f32>::try_new(value.x),
363            ZeroOne::<f32>::try_new(value.y),
364            ZeroOne::<f32>::try_new(value.z),
365        ) {
366            (Ok(r), Ok(g), Ok(b)) => Ok(Self(vec3(r, g, b))),
367            (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(e),
368        }
369    }
370
371    /// Constructs a color from components that have already been checked for not being
372    /// NaN or out of range.
373    ///
374    /// Note: This exists primarily to assist the [`rgb_const!`] macro and may be renamed
375    /// or replaced in future versions.
376    #[inline]
377    pub const fn new_zo(r: ZeroOne<f32>, g: ZeroOne<f32>, b: ZeroOne<f32>) -> Self {
378        Self(vec3(r, g, b))
379    }
380
381    /// Constructs a shade of gray (components all equal) from [relative luminance].
382    /// Panics if any component is NaN. No other range checks are performed.
383    ///
384    /// [relative luminance]: https://en.wikipedia.org/wiki/Relative_luminance
385    #[inline]
386    #[track_caller]
387    pub const fn from_luminance(luminance: ZeroOne<f32>) -> Self {
388        Self::new_zo(luminance, luminance, luminance)
389    }
390
391    /// Converts this color to [`Rgb`] with unrestricted components.
392    #[inline]
393    pub const fn to_rgb(self) -> Rgb {
394        Rgb::new_ps(self.0.x.into_ps(), self.0.y.into_ps(), self.0.z.into_ps())
395    }
396
397    /// Adds an alpha component to produce a [`Rgba`].
398    #[inline]
399    pub const fn with_alpha(self, alpha: ZeroOne<f32>) -> Rgba {
400        Rgba {
401            rgb: self.to_rgb(),
402            alpha,
403        }
404    }
405    /// Adds an alpha component of `1.0` (fully opaque) to produce a [`Rgba`].
406    #[inline]
407    pub const fn with_alpha_one(self) -> Rgba {
408        self.with_alpha(ZeroOne::ONE)
409    }
410
411    /// Adds an alpha component of `1.0` (fully opaque) to produce a [`Rgba`].
412    /// This is for compile-time duck-typed use by the `block::from_color!` macro.
413    #[doc(hidden)]
414    #[inline]
415    #[must_use]
416    pub const fn with_alpha_one_if_has_no_alpha(self) -> Rgba {
417        self.with_alpha(ZeroOne::ONE)
418    }
419
420    /// Returns the red color component. Values are linear (gamma = 1).
421    #[inline]
422    pub const fn red(self) -> ZeroOne<f32> {
423        self.0.x
424    }
425    /// Returns the green color component. Values are linear (gamma = 1).
426    #[inline]
427    pub const fn green(self) -> ZeroOne<f32> {
428        self.0.y
429    }
430    /// Returns the blue color component. Values are linear (gamma = 1).
431    #[inline]
432    pub const fn blue(self) -> ZeroOne<f32> {
433        self.0.z
434    }
435
436    /// Combines the red, green, and blue components to obtain a [relative luminance]
437    /// (“grayscale”) value.
438    ///
439    /// If all components are equal, then the result will be approximately equal to that common value.
440    ///
441    /// ### Examples
442    ///
443    /// ```
444    /// # extern crate all_is_cubes_base as all_is_cubes;
445    /// use all_is_cubes::math::{Rgb01, zo32};
446    ///
447    /// assert_eq!(0.0, Rgb01::BLACK.luminance());
448    /// assert_eq!(0.5, (Rgb01::WHITE * zo32(0.5)).luminance());
449    /// assert_eq!(1.0, Rgb01::WHITE.luminance());
450    ///
451    /// assert_eq!(0.2126, Rgb01::new(1., 0., 0.).luminance());
452    /// assert_eq!(0.7152, Rgb01::new(0., 1., 0.).luminance());
453    /// assert_eq!(0.0722, Rgb01::new(0., 0., 1.).luminance());
454    /// ```
455    ///
456    /// [relative luminance]: https://en.wikipedia.org/wiki/Relative_luminance
457    #[inline]
458    pub fn luminance(self) -> f32 {
459        // Coefficients as per
460        // https://en.wikipedia.org/wiki/Relative_luminance
461        // Rec. ITU-R BT.709-6 https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
462        //
463        // Arithmetic operations ordered for minimum floating point error.
464        // (This probably doesn't matter at all.)
465        self.green().into_inner() * 0.7152
466            + (self.red().into_inner() * 0.2126 + self.blue().into_inner() * 0.0722)
467    }
468
469    /// Converts this color to sRGB 8-bits-per-component color, rounding to the nearest
470    /// representable value.
471    #[inline]
472    pub fn to_srgb8(self) -> [u8; 3] {
473        [
474            component_to_srgb8(self.red().into_ps()),
475            component_to_srgb8(self.green().into_ps()),
476            component_to_srgb8(self.blue().into_ps()),
477        ]
478    }
479
480    /// Converts sRGB 8-bits-per-component color to the corresponding linear [`Rgb01`] value.
481    #[inline]
482    pub const fn from_srgb8(rgb: [u8; 3]) -> Self {
483        Self(vec3(
484            component_from_srgb8_const(rgb[0]),
485            component_from_srgb8_const(rgb[1]),
486            component_from_srgb8_const(rgb[2]),
487        ))
488    }
489
490    /// Multiply `self` by `scale`, clamping out-of-range results to 1.0.
491    #[inline]
492    #[must_use]
493    pub fn saturating_scale(self, scale: PositiveSign<f32>) -> Self {
494        Self(vec3(
495            (self.red() * scale).clamp_01(),
496            (self.green() * scale).clamp_01(),
497            (self.blue() * scale).clamp_01(),
498        ))
499    }
500}
501
502impl Rgba {
503    /// Transparent black (all components zero); identical to
504    /// `Rgba::new(0.0, 0.0, 0.0, 0.0)` except for being a constant.
505    pub const TRANSPARENT: Rgba = Rgb::ZERO.with_alpha(ZeroOne::ZERO);
506    /// Black; identical to `Rgba::new(0.0, 0.0, 0.0, 1.0)` except for being a constant.
507    pub const BLACK: Rgba = Rgb::ZERO.with_alpha_one();
508    /// White; identical to `Rgba::new(1.0, 1.0, 1.0, 1.0)` except for being a constant.
509    pub const WHITE: Rgba = Rgb::ONE.with_alpha_one();
510
511    /// Constructs a color from components. Panics if any component is NaN or negative.
512    /// No other range checks are performed.
513    #[inline]
514    #[track_caller]
515    pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
516        Rgb::new(r, g, b).with_alpha(ZeroOne::<f32>::new_strict(a))
517    }
518
519    /// Constructs a color from components that have already been checked for not being
520    /// NaN or negative.
521    ///
522    /// Note: This exists primarily to assist the [`rgba_const!`] macro and may be renamed
523    /// or replaced in future versions.
524    #[inline]
525    pub const fn new_ps(
526        r: PositiveSign<f32>,
527        g: PositiveSign<f32>,
528        b: PositiveSign<f32>,
529        alpha: ZeroOne<f32>,
530    ) -> Self {
531        Self {
532            rgb: Rgb::new_ps(r, g, b),
533            alpha,
534        }
535    }
536
537    /// Constructs a shade of gray (components all equal). Panics if any component is NaN.
538    /// No other range checks are performed.
539    #[inline]
540    #[track_caller]
541    pub const fn from_luminance(luminance: f32) -> Self {
542        Rgb::new(luminance, luminance, luminance).with_alpha_one()
543    }
544
545    /// Returns the color unchanged.
546    /// This is for compile-time duck-typed use by the `block::from_color!` macro.
547    #[doc(hidden)]
548    #[inline]
549    #[must_use]
550    pub const fn with_alpha_one_if_has_no_alpha(self) -> Rgba {
551        self
552    }
553
554    /// Returns the red color component. Values are linear (gamma = 1) and not premultiplied.
555    #[inline]
556    pub const fn red(self) -> PositiveSign<f32> {
557        self.rgb.red()
558    }
559    /// Returns the green color component. Values are linear (gamma = 1) and not premultiplied.
560    #[inline]
561    pub const fn green(self) -> PositiveSign<f32> {
562        self.rgb.green()
563    }
564    /// Returns the blue color component. Values are linear (gamma = 1) and not premultiplied.
565    #[inline]
566    pub const fn blue(self) -> PositiveSign<f32> {
567        self.rgb.blue()
568    }
569    /// Returns the alpha component.
570    ///
571    /// Note that the RGB components are not premultiplied by alpha.
572    #[inline]
573    pub const fn alpha(self) -> ZeroOne<f32> {
574        self.alpha
575    }
576
577    /// Returns whether this color is fully transparent, or has an alpha component of
578    /// zero or less.
579    #[inline]
580    pub fn fully_transparent(self) -> bool {
581        self.alpha().is_zero()
582    }
583    /// Returns whether this color is fully opaque, or has an alpha component of
584    /// one or greater.
585    #[inline]
586    pub fn fully_opaque(self) -> bool {
587        self.alpha().is_one()
588    }
589    /// Returns the [`OpacityCategory`] which this color's alpha fits into.
590    /// This returns the same information as [`Rgba::fully_transparent`] combined with
591    /// [`Rgba::fully_opaque`].
592    #[inline]
593    pub fn opacity_category(self) -> OpacityCategory {
594        if self.fully_transparent() {
595            OpacityCategory::Invisible
596        } else if self.fully_opaque() {
597            OpacityCategory::Opaque
598        } else {
599            OpacityCategory::Partial
600        }
601    }
602
603    /// Discards the alpha component to produce an RGB color.
604    ///
605    /// Note that if alpha is 0 then the components could be any value and yet be “hidden”
606    /// by the transparency.
607    #[inline]
608    pub const fn to_rgb(self) -> Rgb {
609        self.rgb
610    }
611
612    /// Applies a function to the RGB portion of this color.
613    #[must_use]
614    #[inline]
615    pub fn map_rgb(self, f: impl FnOnce(Rgb) -> Rgb) -> Self {
616        f(self.to_rgb()).with_alpha(self.alpha())
617    }
618
619    /// Combines the red, green, and blue components to obtain a luminance (“grayscale”)
620    /// value. This will be equal to 1 if all components are 1.
621    ///
622    /// This is identical to [`Rgb::luminance`], ignoring the alpha component.
623    #[inline]
624    pub fn luminance(self) -> f32 {
625        self.to_rgb().luminance()
626    }
627
628    /// Converts this color to sRGB (nonlinear RGB components).
629    // TODO: decide whether to make this public and what to call it -- it is rarely needed
630    #[inline]
631    #[doc(hidden)] // used by all-is-cubes-gpu
632    pub fn to_srgb_float(self) -> [f32; 4] {
633        [
634            component_to_srgb(self.red()),
635            component_to_srgb(self.green()),
636            component_to_srgb(self.blue()),
637            self.alpha.into_inner(),
638        ]
639    }
640
641    /// Converts this color lossily to sRGB 8-bits-per-component color.
642    #[inline]
643    pub fn to_srgb8(self) -> [u8; 4] {
644        [
645            component_to_srgb8(self.red()),
646            component_to_srgb8(self.green()),
647            component_to_srgb8(self.blue()),
648            (self.alpha.into_inner() * 255.0).round() as u8,
649        ]
650    }
651
652    /// Converts sRGB 8-bits-per-component color to the corresponding linear [`Rgba`] value.
653    #[inline]
654    pub const fn from_srgb8(rgba: [u8; 4]) -> Self {
655        Self::new_ps(
656            component_from_srgb8_const(rgba[0]).into_ps(),
657            component_from_srgb8_const(rgba[1]).into_ps(),
658            component_from_srgb8_const(rgba[2]).into_ps(),
659            component_from_linear8(rgba[3]),
660        )
661    }
662
663    /// Clamp each component to lie within the range 0 to 1, inclusive.
664    #[inline]
665    #[must_use]
666    pub fn clamp(self) -> Self {
667        Self {
668            rgb: self.rgb.clamp(PS1),
669            alpha: self.alpha,
670        }
671    }
672
673    /// Compute the light reflected from a surface with this reflectance and alpha.
674    //---
675    // Design note: This method doesn't exist because it’s a terribly frequent pattern,
676    // but because when I experimented with some stronger typing of color values,
677    // it popped up 3 times as a needed operation, and I think there’s something notable
678    // there. Someday we might distinguish “RGB light” and “RGB fully-opaque surface colors”
679    // and then this will be important for working with definitely the former and not the latter.
680    #[inline]
681    #[doc(hidden)] // not sure if good public API
682    pub fn reflect(self, illumination: Rgb) -> Rgb {
683        self.to_rgb() * illumination * self.alpha
684    }
685}
686
687impl From<Rgb01> for Rgb {
688    #[inline]
689    fn from(value: Rgb01) -> Self {
690        value.to_rgb()
691    }
692}
693
694impl From<Vector3D<PositiveSign<f32>, Intensity>> for Rgb {
695    #[inline]
696    fn from(value: Vector3D<PositiveSign<f32>, Intensity>) -> Self {
697        Self(value)
698    }
699}
700impl<U> From<Vector3D<ZeroOne<f32>, U>> for Rgb01 {
701    #[inline]
702    fn from(value: Vector3D<ZeroOne<f32>, U>) -> Self {
703        // We discard the unit because
704        Self(value.cast_unit())
705    }
706}
707
708impl From<[PositiveSign<f32>; 3]> for Rgb {
709    #[inline]
710    fn from(value: [PositiveSign<f32>; 3]) -> Self {
711        Self(value.into())
712    }
713}
714impl From<[ZeroOne<f32>; 3]> for Rgb {
715    #[inline]
716    fn from(value: [ZeroOne<f32>; 3]) -> Self {
717        Self::from(value.map(PositiveSign::from))
718    }
719}
720impl From<[ZeroOne<f32>; 3]> for Rgb01 {
721    #[inline]
722    fn from(value: [ZeroOne<f32>; 3]) -> Self {
723        Self::from(Vector3D::<_, euclid::UnknownUnit>::from(value))
724    }
725}
726impl From<[ZeroOne<f32>; 4]> for Rgba {
727    #[inline]
728    fn from(value: [ZeroOne<f32>; 4]) -> Self {
729        let [r, g, b, alpha] = value;
730        Self {
731            rgb: Rgb::from([r, g, b]),
732            alpha,
733        }
734    }
735}
736impl
737    From<(
738        PositiveSign<f32>,
739        PositiveSign<f32>,
740        PositiveSign<f32>,
741        ZeroOne<f32>,
742    )> for Rgba
743{
744    #[inline]
745    fn from(
746        value: (
747            PositiveSign<f32>,
748            PositiveSign<f32>,
749            PositiveSign<f32>,
750            ZeroOne<f32>,
751        ),
752    ) -> Self {
753        let (r, g, b, alpha) = value;
754        Self {
755            rgb: Rgb::from([r, g, b]),
756            alpha,
757        }
758    }
759}
760
761impl From<Rgb> for Vector3D<f32, Intensity> {
762    #[inline]
763    fn from(value: Rgb) -> Self {
764        value.0.map(PositiveSign::<f32>::into_inner)
765    }
766}
767
768impl From<Rgb> for [PositiveSign<f32>; 3] {
769    #[inline]
770    fn from(value: Rgb) -> Self {
771        value.0.into()
772    }
773}
774impl From<Rgba> for [PositiveSign<f32>; 4] {
775    #[inline]
776    fn from(value: Rgba) -> Self {
777        let [r, g, b]: [PositiveSign<f32>; 3] = value.rgb.into();
778        [r, g, b, value.alpha.into()]
779    }
780}
781impl From<Rgba>
782    for (
783        PositiveSign<f32>,
784        PositiveSign<f32>,
785        PositiveSign<f32>,
786        ZeroOne<f32>,
787    )
788{
789    #[inline]
790    fn from(value: Rgba) -> Self {
791        let [r, g, b]: [PositiveSign<f32>; 3] = value.rgb.into();
792        (r, g, b, value.alpha)
793    }
794}
795
796impl From<Rgb> for [NotNan<f32>; 3] {
797    #[inline]
798    fn from(value: Rgb) -> Self {
799        value.0.map(PositiveSign::into).into()
800    }
801}
802impl From<Rgba> for [NotNan<f32>; 4] {
803    #[inline]
804    fn from(value: Rgba) -> Self {
805        let [r, g, b]: [NotNan<f32>; 3] = value.rgb.into();
806        [r, g, b, value.alpha.into()]
807    }
808}
809
810impl From<Rgb> for [f32; 3] {
811    #[inline]
812    fn from(value: Rgb) -> Self {
813        value.0.map(PositiveSign::<f32>::into_inner).into()
814    }
815}
816impl From<Rgba> for [f32; 4] {
817    #[inline]
818    fn from(value: Rgba) -> Self {
819        <[NotNan<f32>; 4]>::from(value).map(NotNan::into_inner)
820    }
821}
822
823impl TryFrom<Vector3D<f32, Intensity>> for Rgb {
824    type Error = NotPositiveSign<f32>;
825    #[inline]
826    fn try_from(value: Vector3D<f32, Intensity>) -> Result<Self, Self::Error> {
827        Ok(Self(vec3(
828            value.x.try_into()?,
829            value.y.try_into()?,
830            value.z.try_into()?,
831        )))
832    }
833}
834
835impl Add<Rgb> for Rgb {
836    type Output = Self;
837    #[inline]
838    fn add(self, other: Self) -> Self {
839        Self(self.0 + other.0)
840    }
841}
842impl AddAssign<Rgb> for Rgb {
843    #[inline]
844    fn add_assign(&mut self, other: Self) {
845        self.0 += other.0;
846    }
847}
848/// Multiplies two color values componentwise.
849impl Mul<Rgb> for Rgb {
850    type Output = Self;
851    /// Multiplies two color values componentwise.
852    #[inline]
853    fn mul(self, other: Rgb) -> Self {
854        Self(self.0.component_mul(other.0))
855    }
856}
857/// Multiplies this color value by a scalar.
858impl Mul<PositiveSign<f32>> for Rgb {
859    type Output = Self;
860    /// Multiplies this color value by a scalar.
861    #[inline]
862    fn mul(self, scalar: PositiveSign<f32>) -> Self {
863        Self(self.0 * scalar)
864    }
865}
866impl Mul<ZeroOne<f32>> for Rgb {
867    type Output = Self;
868    /// Multiplies this color value by a scalar.
869    #[inline]
870    fn mul(self, scalar: ZeroOne<f32>) -> Self {
871        Self(self.0 * PositiveSign::from(scalar))
872    }
873}
874impl Mul<ZeroOne<f32>> for Rgb01 {
875    type Output = Self;
876    /// Multiplies this color value by a scalar.
877    #[inline]
878    fn mul(self, scalar: ZeroOne<f32>) -> Self {
879        Self(self.0 * scalar)
880    }
881}
882/// Multiplies this color value by a scalar.
883///
884/// Panics if the scalar is NaN. Returns zero if the scalar is negative.
885// TODO: consider removing this panic risk
886impl Mul<f32> for Rgb {
887    type Output = Self;
888    /// Multiplies this color value by a scalar.
889    ///
890    /// Panics if the scalar is NaN. Returns zero if the scalar is negative.
891    #[inline]
892    fn mul(self, scalar: f32) -> Self {
893        Self(self.0 * PositiveSign::<f32>::new_clamped(scalar))
894    }
895}
896
897/// There is no corresponding `impl Sum for Rgba` because the alpha would
898/// not have a universally reasonable interpretation.
899impl Sum for Rgb {
900    #[allow(clippy::missing_inline_in_public_items)]
901    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
902        // Using Vector3 as the accumulator type avoids intermediate NaN checks.
903        Rgb::try_from(iter.fold(Vector3D::<f32, Intensity>::zero(), |accum, rgb| {
904            accum + Vector3D::<f32, Intensity>::from(rgb)
905        })).unwrap(/* impossible NaN */)
906    }
907}
908
909impl fmt::Debug for Rgb {
910    #[allow(clippy::missing_inline_in_public_items)]
911    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
912        write!(
913            fmt,
914            "Rgb({:?}, {:?}, {:?})",
915            self.red().into_inner(),
916            self.green().into_inner(),
917            self.blue().into_inner()
918        )
919    }
920}
921impl fmt::Debug for Rgba {
922    #[allow(clippy::missing_inline_in_public_items)]
923    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
924        write!(
925            fmt,
926            "Rgba({:?}, {:?}, {:?}, {:?})",
927            self.red().into_inner(),
928            self.green().into_inner(),
929            self.blue().into_inner(),
930            self.alpha().into_inner()
931        )
932    }
933}
934
935// Note: These currently cannot be derived implementations, because
936// `euclid`'s `Arbitrary` implementations don't implement size hints.
937#[cfg(feature = "arbitrary")]
938#[mutants::skip]
939#[allow(clippy::missing_inline_in_public_items)]
940impl<'a> arbitrary::Arbitrary<'a> for Rgb {
941    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
942        Ok(Rgb::new_ps(u.arbitrary()?, u.arbitrary()?, u.arbitrary()?))
943    }
944
945    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
946        <[PositiveSign<f32>; 3]>::size_hint(0) // non-recursive, so don't fail
947    }
948}
949#[cfg(feature = "arbitrary")]
950#[mutants::skip]
951#[allow(clippy::missing_inline_in_public_items)]
952impl<'a> arbitrary::Arbitrary<'a> for Rgba {
953    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
954        Ok(Rgba::new_ps(
955            u.arbitrary()?,
956            u.arbitrary()?,
957            u.arbitrary()?,
958            u.arbitrary()?,
959        ))
960    }
961
962    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
963        <[PositiveSign<f32>; 4]>::size_hint(0) // non-recursive, so don't fail
964    }
965}
966#[cfg(feature = "arbitrary")]
967#[mutants::skip]
968#[allow(clippy::missing_inline_in_public_items)]
969impl<'a> arbitrary::Arbitrary<'a> for Rgb01 {
970    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
971        Ok(Rgb01::new_zo(
972            u.arbitrary()?,
973            u.arbitrary()?,
974            u.arbitrary()?,
975        ))
976    }
977
978    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
979        <[ZeroOne<f32>; 3]>::size_hint(0) // non-recursive, so don't fail
980    }
981}
982
983/// Implementations necessary for `all_is_cubes::drawing` to be able to use these types
984mod eg {
985    use embedded_graphics_core::pixelcolor::{self, RgbColor as _};
986
987    use super::*;
988    impl pixelcolor::PixelColor for Rgb01 {
989        type Raw = ();
990    }
991    impl pixelcolor::PixelColor for Rgba {
992        type Raw = ();
993    }
994    /// Adapt `embedded_graphics`'s most general color type to ours.
995    // ^ can't be doc link because we don't depend on it
996    impl From<pixelcolor::Rgb888> for Rgb01 {
997        #[inline]
998        fn from(color: pixelcolor::Rgb888) -> Rgb01 {
999            Rgb01::from_srgb8([color.r(), color.g(), color.b()])
1000        }
1001    }
1002}
1003
1004#[cfg(feature = "rerun")]
1005mod rerun {
1006    use super::*;
1007    use re_types::datatypes;
1008
1009    impl From<Rgb> for datatypes::Rgba32 {
1010        #[inline]
1011        fn from(value: Rgb) -> Self {
1012            value.with_alpha_one().into()
1013        }
1014    }
1015    impl From<Rgba> for datatypes::Rgba32 {
1016        #[inline]
1017        fn from(value: Rgba) -> Self {
1018            let [r, g, b, a] = value.to_srgb8();
1019            datatypes::Rgba32::from_unmultiplied_rgba(r, g, b, a)
1020        }
1021    }
1022}
1023
1024// -------------------------------------------------------------------------------------------------
1025
1026/// Apply the sRGB encoding function. Do not use this on alpha values.
1027#[inline]
1028fn component_to_srgb(c: PositiveSign<f32>) -> f32 {
1029    // Source: <https://en.wikipedia.org/w/index.php?title=SRGB&oldid=1002296118#The_forward_transformation_(CIE_XYZ_to_sRGB)> (version as of Feb 3, 2020)
1030    // Strip wrapper
1031    let c = c.into_inner();
1032    // Apply sRGB gamma curve
1033    if c <= 0.0031308 {
1034        c * (323. / 25.)
1035    } else {
1036        (211. * c.powf(5. / 12.) - 11.) / 200.
1037    }
1038}
1039
1040#[inline]
1041fn component_to_srgb8(c: PositiveSign<f32>) -> u8 {
1042    // out of range values will be clamped by `as u8`
1043    (component_to_srgb(c) * 255.).round() as u8
1044}
1045
1046#[inline]
1047const fn component_from_linear8(c: u8) -> ZeroOne<f32> {
1048    // SAFETY: All possible `u8` values will be in range. This is verified by a test.
1049    unsafe { ZeroOne::new_unchecked(c as f32 / 255.0) }
1050}
1051
1052/// Implements sRGB decoding using the standard arithmetic.
1053///
1054/// This implementation is only used for testing because we want to be able to execute the
1055/// conversion in `const` contexts, and `libm::powf` is not `const fn` yet. If it ever is,
1056/// then we can discard the lookup table.
1057#[cfg(test)] // only used to validate the lookup tables
1058fn component_from_srgb8_arithmetic(c: u8) -> f32 {
1059    // Source: <https://en.wikipedia.org/w/index.php?title=SRGB&oldid=1002296118#The_reverse_transformation> (version as of Feb 3, 2020)
1060    // Convert to float
1061    let c = f32::from(c) / 255.0;
1062    // Apply sRGB gamma curve
1063    if c <= 0.04045 {
1064        c * (25. / 323.)
1065    } else {
1066        // Use pure-Rust implementation from `libm` to avoid platform-dependent rounding
1067        // which would make the application behavior inconsistent in a potentially suprising way,
1068        // and be inconsistent with our hardcoded lookup table. This function is supposed to
1069        // *define* what belongs in the lookup table.
1070        libm::powf((200. * c + 11.) / 211., 12. / 5.)
1071    }
1072}
1073
1074/// Implements sRGB decoding using a lookup table.
1075#[inline]
1076const fn component_from_srgb8_const(c: u8) -> ZeroOne<f32> {
1077    // Safety: the table may be inspected to contain no negative or NaN values.
1078    unsafe { ZeroOne::new_unchecked(CONST_SRGB_LOOKUP_TABLE[c as usize]) }
1079}
1080
1081/// Reduces alpha/opacity values to only three possibilities, by conflating all alphas
1082/// greater than zero and less than one.
1083///
1084/// This may be used in rendering algorithms to refer to whether something moved from
1085/// one category to another, and hence might need different treatment than in the previous
1086/// frame.
1087#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1088#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1089#[expect(clippy::exhaustive_enums)]
1090#[repr(u8)]
1091pub enum OpacityCategory {
1092    /// Alpha of zero; completely transparent; completely invisible; need not be drawn.
1093    Invisible = 0,
1094    /// Alpha greater than zero and less than one; requires blending.
1095    Partial = 1,
1096    /// Alpha of one; completely hides what is behind it and does not require blending.
1097    Opaque = 2,
1098}
1099
1100/// Precomputed lookup table of the results of [`component_from_srgb8_arithmetic()`].
1101/// This allows converting sRGB colors to [`Rgb`] linear colors in const evaluation
1102/// contexts.
1103/// 
1104/// This table is validated and can be regenerated using the test `check_const_srgb_table`.
1105#[rustfmt::skip]
1106static CONST_SRGB_LOOKUP_TABLE: &[f32; 256] = &[
1107    0.0, 0.000303527, 0.000607054, 0.000910581, 0.001214108, 0.001517635,
1108    0.001821162, 0.0021246888, 0.002428216, 0.002731743, 0.00303527, 0.003346535,
1109    0.003676507, 0.0040247166, 0.004391441, 0.0047769523, 0.005181516, 0.0056053908,
1110    0.0060488326, 0.00651209, 0.00699541, 0.0074990317, 0.008023192, 0.008568125,
1111    0.009134057, 0.009721216, 0.01032982, 0.010960094, 0.011612244, 0.012286487,
1112    0.012983031, 0.013702083, 0.014443844, 0.015208514, 0.015996292, 0.016807374,
1113    0.017641956, 0.018500218, 0.019382361, 0.02028856, 0.02121901, 0.022173883,
1114    0.023153365, 0.02415763, 0.025186857, 0.026241219, 0.027320892, 0.028426038,
1115    0.029556833, 0.03071344, 0.03189603, 0.033104762, 0.0343398, 0.03560131,
1116    0.036889456, 0.038204376, 0.039546236, 0.040915187, 0.0423114, 0.04373502,
1117    0.04518619, 0.046665072, 0.048171822, 0.049706563, 0.051269464, 0.052860655,
1118    0.054480284, 0.056128494, 0.057805434, 0.05951123, 0.061246056, 0.06301002,
1119    0.06480328, 0.06662594, 0.06847817, 0.070360094, 0.07227186, 0.074213564,
1120    0.076185375, 0.07818741, 0.08021983, 0.082282715, 0.084376216, 0.08650045,
1121    0.08865559, 0.09084171, 0.09305896, 0.09530747, 0.09758735, 0.099898726,
1122    0.102241725, 0.10461648, 0.10702311, 0.1094617, 0.111932434, 0.11443536,
1123    0.11697067, 0.11953841, 0.122138776, 0.124771796, 0.12743768, 0.13013647,
1124    0.13286832, 0.13563332, 0.13843161, 0.14126328, 0.14412846, 0.14702725,
1125    0.1499598, 0.15292613, 0.15592647, 0.15896082, 0.16202939, 0.16513216,
1126    0.1682694, 0.17144108, 0.17464739, 0.17788841, 0.18116423, 0.18447497,
1127    0.18782076, 0.19120166, 0.19461781, 0.1980693, 0.20155624, 0.20507872,
1128    0.20863685, 0.21223073, 0.21586053, 0.21952623, 0.22322798, 0.22696589,
1129    0.23074007, 0.2345506, 0.23839758, 0.24228114, 0.24620134, 0.25015828,
1130    0.2541521, 0.25818285, 0.26225066, 0.2663556, 0.2704978, 0.2746773,
1131    0.27889434, 0.28314874, 0.2874409, 0.29177064, 0.29613832, 0.30054379,
1132    0.30498737, 0.30946898, 0.31398875, 0.31854674, 0.32314324, 0.32777813,
1133    0.3324515, 0.33716366, 0.34191445, 0.34670407, 0.35153264, 0.35640007,
1134    0.36130688, 0.3662526, 0.3712377, 0.37626213, 0.38132593, 0.38642943,
1135    0.39157248, 0.39675522, 0.40197787, 0.4072402, 0.4125426, 0.41788507,
1136    0.42326772, 0.42869055, 0.43415362, 0.43965715, 0.44520125, 0.45078585,
1137    0.456411, 0.46207696, 0.46778384, 0.47353154, 0.47932023, 0.4851499,
1138    0.4910209, 0.49693304, 0.5028865, 0.5088813, 0.5149177, 0.5209956,
1139    0.52711517, 0.53327644, 0.5394795, 0.54572445, 0.55201143, 0.5583404,
1140    0.5647115, 0.5711248, 0.57758045, 0.58407843, 0.5906189, 0.59720176,
1141    0.6038273, 0.61049557, 0.61720663, 0.6239604, 0.6307571, 0.63759685,
1142    0.64447975, 0.6514057, 0.6583748, 0.6653873, 0.6724432, 0.67954254,
1143    0.6866853, 0.6938717, 0.7011019, 0.7083758, 0.71569353, 0.723055,
1144    0.73046076, 0.7379104, 0.74540424, 0.7529423, 0.7605245, 0.76815116,
1145    0.7758222, 0.78353786, 0.7912979, 0.7991027, 0.80695224, 0.8148465,
1146    0.82278585, 0.83076984, 0.838799, 0.84687316, 0.8549927, 0.8631573,
1147    0.87136704, 0.87962234, 0.8879232, 0.89626944, 0.90466106, 0.9130986,
1148    0.9215819, 0.9301109, 0.9386858, 0.94730645, 0.9559734, 0.9646863,
1149    0.9734453, 0.9822504, 0.9911021, 1.0,
1150];
1151
1152// -------------------------------------------------------------------------------------------------
1153
1154#[cfg(test)]
1155mod tests {
1156    use crate::math::zo32;
1157
1158    use super::*;
1159    use alloc::vec::Vec;
1160    use exhaust::Exhaust as _;
1161    use itertools::Itertools as _;
1162
1163    // TODO: Add tests of the color not-NaN mechanisms.
1164
1165    #[test]
1166    fn rgba_to_srgb8() {
1167        assert_eq!(
1168            Rgba::new(0.125, 0.25, 0.5, 0.75).to_srgb8(),
1169            [99, 137, 188, 191]
1170        );
1171
1172        // Test saturation
1173        assert_eq!(
1174            Rgba::new(0.5, -0.0, 10.0, 1.0).to_srgb8(),
1175            [188, 0, 255, 255]
1176        );
1177    }
1178
1179    #[test]
1180    fn rgb_rgba_debug() {
1181        assert_eq!(
1182            format!("{:#?}", Rgb::new(0.1, 0.2, 0.3)),
1183            "Rgb(0.1, 0.2, 0.3)"
1184        );
1185        assert_eq!(
1186            format!("{:#?}", Rgba::new(0.1, 0.2, 0.3, 0.4)),
1187            "Rgba(0.1, 0.2, 0.3, 0.4)"
1188        );
1189    }
1190
1191    /// Test that [`Rgba::from_srgb8`] agrees with [`Rgba::to_srgb8`].
1192    #[test]
1193    fn srgb_round_trip() {
1194        let srgb_figures = [
1195            0x00, 0x05, 0x10, 0x22, 0x33, 0x44, 0x55, 0x77, 0x7f, 0xDD, 0xFF,
1196        ];
1197        let results = srgb_figures
1198            .iter()
1199            .cartesian_product(srgb_figures.iter())
1200            .map(|(&r, &a)| {
1201                let srgb = [r, 0, 0, a];
1202                let color = Rgba::from_srgb8(srgb);
1203                (srgb, color, color.to_srgb8())
1204            })
1205            .collect::<Vec<_>>();
1206        // Print all the results before asserting
1207        eprintln!("{results:#?}");
1208        // Filter out correct roundtrip results.
1209        let bad = results
1210            .into_iter()
1211            .filter(|&(o, _, r)| o.into_iter().zip(r).any(|(a, b)| a != b))
1212            .collect::<Vec<_>>();
1213        assert_eq!(bad, vec![]);
1214    }
1215
1216    #[test]
1217    fn srgb_float() {
1218        let color = Rgba::new(0.05, 0.1, 0.4, 0.5);
1219        let srgb_float = color.to_srgb_float();
1220        let srgb8 = color.to_srgb8();
1221        assert_eq!(
1222            srgb8,
1223            [
1224                (srgb_float[0] * 255.).round() as u8,
1225                (srgb_float[1] * 255.).round() as u8,
1226                (srgb_float[2] * 255.).round() as u8,
1227                (srgb_float[3] * 255.).round() as u8
1228            ]
1229        );
1230    }
1231
1232    #[test]
1233    fn check_const_srgb_table() {
1234        let generated_table: Vec<f32> =
1235            (0..=u8::MAX).map(component_from_srgb8_arithmetic).collect();
1236        print!("static CONST_SRGB_LOOKUP_TABLE: [f32; 256] = [");
1237        for i in 0..=u8::MAX {
1238            if i.is_multiple_of(6) {
1239                print!("\n    {:?},", generated_table[i as usize]);
1240            } else {
1241                print!(" {:?},", generated_table[i as usize]);
1242            }
1243        }
1244        println!("\n];");
1245
1246        pretty_assertions::assert_eq!(CONST_SRGB_LOOKUP_TABLE.to_vec(), generated_table);
1247    }
1248
1249    /// Test exhaustively that [`component_from_linear8()`] doesn’t produce an invalid [`ZeroOne`].
1250    #[test]
1251    fn check_component_from_linear8() {
1252        assert_eq!(component_from_linear8(0), zo32(0.0));
1253        assert_eq!(component_from_linear8(255), zo32(1.0));
1254        for u8_value in 1..=254 {
1255            let float_value = component_from_linear8(u8_value);
1256            assert!(
1257                float_value > zo32(0.0) && float_value < zo32(1.0),
1258                "component_from_linear8({u8_value}) out of range {float_value}"
1259            );
1260        }
1261    }
1262
1263    #[test]
1264    fn check_uniform_luminance() {
1265        fn optimize(channel: usize) -> [u8; 4] {
1266            // Blue is the primary color whose maximum intensity is darkest;
1267            // therefore it is the standard by which we check the other.
1268            let reference_luminance = Rgb01::UNIFORM_LUMINANCE_BLUE.luminance();
1269            let (_color, srgb, luminance_difference) = u8::exhaust()
1270                .map(|srgb_byte| {
1271                    let mut srgb = [0, 0, 0, 255];
1272                    srgb[channel] = srgb_byte;
1273                    let color = Rgba::from_srgb8(srgb);
1274                    (color, srgb, (color.luminance() - reference_luminance).abs())
1275                })
1276                .min_by(|a, b| a.2.total_cmp(&b.2))
1277                .unwrap();
1278            println!("best luminance difference = {luminance_difference}");
1279            srgb
1280        }
1281
1282        println!("red:");
1283        assert_eq!(
1284            Rgb01::UNIFORM_LUMINANCE_RED.with_alpha_one().to_srgb8(),
1285            optimize(0)
1286        );
1287        println!("green:");
1288        assert_eq!(
1289            Rgb01::UNIFORM_LUMINANCE_GREEN.with_alpha_one().to_srgb8(),
1290            optimize(1)
1291        );
1292    }
1293
1294    #[cfg(feature = "arbitrary")]
1295    #[test]
1296    fn arbitrary_size_hints() {
1297        use arbitrary::Arbitrary as _;
1298
1299        assert_eq!(Rgb::size_hint(0), (12, Some(12)));
1300        assert_eq!(Rgb::try_size_hint(0).unwrap(), (12, Some(12)));
1301        assert_eq!(Rgba::size_hint(0), (16, Some(16)));
1302        assert_eq!(Rgba::try_size_hint(0).unwrap(), (16, Some(16)));
1303    }
1304}