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