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