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}