1use core::fmt;
4use core::iter::Sum;
5use core::ops::{Add, AddAssign, Mul};
6
7use euclid::{Vector3D, vec3};
8use ordered_float::NotNan;
9
10#[cfg(not(feature = "std"))]
12#[allow(unused_imports)]
13use num_traits::float::Float as _;
14
15use crate::math::{NotPositiveSign, NotZeroOne, PositiveSign, ZeroOne};
16
17#[macro_export]
27macro_rules! rgb_const {
28 ($r:literal, $g:literal, $b:literal) => {
29 const {
31 $crate::math::Rgb::new_ps(
32 $crate::math::PositiveSign::<f32>::new_strict($r),
33 $crate::math::PositiveSign::<f32>::new_strict($g),
34 $crate::math::PositiveSign::<f32>::new_strict($b),
35 )
36 }
37 };
38}
39
40#[macro_export]
43macro_rules! rgba_const {
44 ($r:literal, $g:literal, $b:literal, $a:literal) => {
45 const {
47 $crate::math::Rgba::new_ps(
48 $crate::math::PositiveSign::<f32>::new_strict($r),
49 $crate::math::PositiveSign::<f32>::new_strict($g),
50 $crate::math::PositiveSign::<f32>::new_strict($b),
51 $crate::math::ZeroOne::<f32>::new_strict($a),
52 )
53 }
54 };
55}
56
57#[macro_export]
72macro_rules! rgb01 {
73 ($r:literal, $g:literal, $b:literal) => {
74 const {
76 $crate::math::Rgb01::new_zo(
77 $crate::math::ZeroOne::<f32>::new_strict($r),
78 $crate::math::ZeroOne::<f32>::new_strict($g),
79 $crate::math::ZeroOne::<f32>::new_strict($b),
80 )
81 }
82 };
83}
84
85#[derive(Clone, Copy, Eq, Hash, PartialEq)]
107pub struct Rgb(Vector3D<PositiveSign<f32>, Intensity>);
108
109#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
128pub struct Rgb01(
129 Vector3D<
130 ZeroOne<f32>,
131 euclid::UnknownUnit,
134 >,
135);
136
137#[derive(Clone, Copy, Eq, Hash, PartialEq)]
160pub struct Rgba {
161 rgb: Rgb,
162 alpha: ZeroOne<f32>,
163}
164
165#[expect(clippy::exhaustive_enums)]
169#[derive(Debug, Eq, PartialEq)]
170pub enum Intensity {}
171
172const PS0: PositiveSign<f32> = <PositiveSign<f32> as num_traits::ConstZero>::ZERO;
176const PS1: PositiveSign<f32> = <PositiveSign<f32> as num_traits::ConstOne>::ONE;
177
178impl Rgb {
182 pub const ZERO: Rgb = Rgb(vec3(PS0, PS0, PS0));
184 pub const ONE: Rgb = Rgb(vec3(PS1, PS1, PS1));
188
189 #[inline]
194 #[track_caller]
195 pub const fn new(r: f32, g: f32, b: f32) -> Self {
196 match Self::try_new(vec3(r, g, b)) {
197 Ok(color) => color,
198 Err(_) => panic!("color component out of range"),
199 }
200 }
201
202 const fn try_new(value: Vector3D<f32, Intensity>) -> Result<Self, NotPositiveSign<f32>> {
203 match (
204 PositiveSign::<f32>::try_new(value.x),
205 PositiveSign::<f32>::try_new(value.y),
206 PositiveSign::<f32>::try_new(value.z),
207 ) {
208 (Ok(r), Ok(g), Ok(b)) => Ok(Self(vec3(r, g, b))),
209 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(e),
210 }
211 }
212
213 #[inline]
219 pub const fn new_ps(r: PositiveSign<f32>, g: PositiveSign<f32>, b: PositiveSign<f32>) -> Self {
220 Self(vec3(r, g, b))
221 }
222
223 #[inline]
226 #[track_caller]
227 pub const fn from_luminance(luminance: f32) -> Self {
228 Self::new(luminance, luminance, luminance)
229 }
230
231 #[inline]
233 pub const fn with_alpha(self, alpha: ZeroOne<f32>) -> Rgba {
234 Rgba { rgb: self, alpha }
235 }
236 #[inline]
238 pub const fn with_alpha_one(self) -> Rgba {
239 self.with_alpha(ZeroOne::ONE)
240 }
241
242 #[doc(hidden)]
245 #[inline]
246 #[must_use]
247 pub const fn with_alpha_one_if_has_no_alpha(self) -> Rgba {
248 self.with_alpha(ZeroOne::ONE)
249 }
250
251 #[inline]
253 pub const fn red(self) -> PositiveSign<f32> {
254 self.0.x
255 }
256 #[inline]
258 pub const fn green(self) -> PositiveSign<f32> {
259 self.0.y
260 }
261 #[inline]
263 pub const fn blue(self) -> PositiveSign<f32> {
264 self.0.z
265 }
266
267 #[inline]
286 pub fn luminance(self) -> f32 {
287 self.green().into_inner() * 0.7152
294 + (self.red().into_inner() * 0.2126 + self.blue().into_inner() * 0.0722)
295 }
296
297 #[inline]
299 pub const fn from_srgb8(rgb: [u8; 3]) -> Self {
300 Self(vec3(
301 component_from_srgb8_const(rgb[0]).into_ps(),
302 component_from_srgb8_const(rgb[1]).into_ps(),
303 component_from_srgb8_const(rgb[2]).into_ps(),
304 ))
305 }
306
307 #[inline]
309 #[must_use]
310 pub fn clamp(self, maximum: PositiveSign<f32>) -> Self {
311 Self(self.0.map(|c| c.clamp(PS0, maximum)))
312 }
313
314 #[inline]
316 #[must_use]
317 pub fn clamp_01(self) -> Rgb01 {
318 Rgb01(self.0.map(PositiveSign::clamp_01).cast_unit())
319 }
320
321 #[inline]
323 #[must_use]
324 pub fn saturating_sub(self, other: Self) -> Self {
325 Self(vec3(
326 self.red().saturating_sub(other.red()),
327 self.green().saturating_sub(other.green()),
328 self.blue().saturating_sub(other.blue()),
329 ))
330 }
331}
332
333impl Rgb01 {
334 pub const BLACK: Self = Self::new_zo(ZeroOne::ZERO, ZeroOne::ZERO, ZeroOne::ZERO);
336 pub const WHITE: Self = Self::new_zo(ZeroOne::ONE, ZeroOne::ONE, ZeroOne::ONE);
338
339 pub const UNIFORM_LUMINANCE_RED: Self = Rgb01::from_srgb8([0x9E, 0x00, 0x00]);
342 pub const UNIFORM_LUMINANCE_GREEN: Self = Rgb01::from_srgb8([0x00, 0x59, 0x00]);
345 pub const UNIFORM_LUMINANCE_BLUE: Self = Rgb01::from_srgb8([0x00, 0x00, 0xFF]);
349
350 #[inline]
352 #[track_caller]
353 pub const fn new(r: f32, g: f32, b: f32) -> Self {
354 match Self::try_new(vec3(r, g, b)) {
355 Ok(color) => color,
356 Err(_) => panic!("Rgb01 component out of range"),
357 }
358 }
359
360 const fn try_new(value: Vector3D<f32, Rgb01>) -> Result<Self, NotZeroOne<f32>> {
361 match (
362 ZeroOne::<f32>::try_new(value.x),
363 ZeroOne::<f32>::try_new(value.y),
364 ZeroOne::<f32>::try_new(value.z),
365 ) {
366 (Ok(r), Ok(g), Ok(b)) => Ok(Self(vec3(r, g, b))),
367 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(e),
368 }
369 }
370
371 #[inline]
377 pub const fn new_zo(r: ZeroOne<f32>, g: ZeroOne<f32>, b: ZeroOne<f32>) -> Self {
378 Self(vec3(r, g, b))
379 }
380
381 #[inline]
386 #[track_caller]
387 pub const fn from_luminance(luminance: ZeroOne<f32>) -> Self {
388 Self::new_zo(luminance, luminance, luminance)
389 }
390
391 #[inline]
393 pub const fn to_rgb(self) -> Rgb {
394 Rgb::new_ps(self.0.x.into_ps(), self.0.y.into_ps(), self.0.z.into_ps())
395 }
396
397 #[inline]
399 pub const fn with_alpha(self, alpha: ZeroOne<f32>) -> Rgba {
400 Rgba {
401 rgb: self.to_rgb(),
402 alpha,
403 }
404 }
405 #[inline]
407 pub const fn with_alpha_one(self) -> Rgba {
408 self.with_alpha(ZeroOne::ONE)
409 }
410
411 #[doc(hidden)]
414 #[inline]
415 #[must_use]
416 pub const fn with_alpha_one_if_has_no_alpha(self) -> Rgba {
417 self.with_alpha(ZeroOne::ONE)
418 }
419
420 #[inline]
422 pub const fn red(self) -> ZeroOne<f32> {
423 self.0.x
424 }
425 #[inline]
427 pub const fn green(self) -> ZeroOne<f32> {
428 self.0.y
429 }
430 #[inline]
432 pub const fn blue(self) -> ZeroOne<f32> {
433 self.0.z
434 }
435
436 #[inline]
458 pub fn luminance(self) -> f32 {
459 self.green().into_inner() * 0.7152
466 + (self.red().into_inner() * 0.2126 + self.blue().into_inner() * 0.0722)
467 }
468
469 #[inline]
472 pub fn to_srgb8(self) -> [u8; 3] {
473 [
474 component_to_srgb8(self.red().into_ps()),
475 component_to_srgb8(self.green().into_ps()),
476 component_to_srgb8(self.blue().into_ps()),
477 ]
478 }
479
480 #[inline]
482 pub const fn from_srgb8(rgb: [u8; 3]) -> Self {
483 Self(vec3(
484 component_from_srgb8_const(rgb[0]),
485 component_from_srgb8_const(rgb[1]),
486 component_from_srgb8_const(rgb[2]),
487 ))
488 }
489
490 #[inline]
492 #[must_use]
493 pub fn saturating_scale(self, scale: PositiveSign<f32>) -> Self {
494 Self(vec3(
495 (self.red() * scale).clamp_01(),
496 (self.green() * scale).clamp_01(),
497 (self.blue() * scale).clamp_01(),
498 ))
499 }
500}
501
502impl Rgba {
503 pub const TRANSPARENT: Rgba = Rgb::ZERO.with_alpha(ZeroOne::ZERO);
506 pub const BLACK: Rgba = Rgb::ZERO.with_alpha_one();
508 pub const WHITE: Rgba = Rgb::ONE.with_alpha_one();
510
511 #[inline]
514 #[track_caller]
515 pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
516 Rgb::new(r, g, b).with_alpha(ZeroOne::<f32>::new_strict(a))
517 }
518
519 #[inline]
525 pub const fn new_ps(
526 r: PositiveSign<f32>,
527 g: PositiveSign<f32>,
528 b: PositiveSign<f32>,
529 alpha: ZeroOne<f32>,
530 ) -> Self {
531 Self {
532 rgb: Rgb::new_ps(r, g, b),
533 alpha,
534 }
535 }
536
537 #[inline]
540 #[track_caller]
541 pub const fn from_luminance(luminance: f32) -> Self {
542 Rgb::new(luminance, luminance, luminance).with_alpha_one()
543 }
544
545 #[doc(hidden)]
548 #[inline]
549 #[must_use]
550 pub const fn with_alpha_one_if_has_no_alpha(self) -> Rgba {
551 self
552 }
553
554 #[inline]
556 pub const fn red(self) -> PositiveSign<f32> {
557 self.rgb.red()
558 }
559 #[inline]
561 pub const fn green(self) -> PositiveSign<f32> {
562 self.rgb.green()
563 }
564 #[inline]
566 pub const fn blue(self) -> PositiveSign<f32> {
567 self.rgb.blue()
568 }
569 #[inline]
573 pub const fn alpha(self) -> ZeroOne<f32> {
574 self.alpha
575 }
576
577 #[inline]
580 pub fn fully_transparent(self) -> bool {
581 self.alpha().is_zero()
582 }
583 #[inline]
586 pub fn fully_opaque(self) -> bool {
587 self.alpha().is_one()
588 }
589 #[inline]
593 pub fn opacity_category(self) -> OpacityCategory {
594 if self.fully_transparent() {
595 OpacityCategory::Invisible
596 } else if self.fully_opaque() {
597 OpacityCategory::Opaque
598 } else {
599 OpacityCategory::Partial
600 }
601 }
602
603 #[inline]
608 pub const fn to_rgb(self) -> Rgb {
609 self.rgb
610 }
611
612 #[must_use]
614 #[inline]
615 pub fn map_rgb(self, f: impl FnOnce(Rgb) -> Rgb) -> Self {
616 f(self.to_rgb()).with_alpha(self.alpha())
617 }
618
619 #[inline]
624 pub fn luminance(self) -> f32 {
625 self.to_rgb().luminance()
626 }
627
628 #[inline]
631 #[doc(hidden)] pub fn to_srgb_float(self) -> [f32; 4] {
633 [
634 component_to_srgb(self.red()),
635 component_to_srgb(self.green()),
636 component_to_srgb(self.blue()),
637 self.alpha.into_inner(),
638 ]
639 }
640
641 #[inline]
643 pub fn to_srgb8(self) -> [u8; 4] {
644 [
645 component_to_srgb8(self.red()),
646 component_to_srgb8(self.green()),
647 component_to_srgb8(self.blue()),
648 (self.alpha.into_inner() * 255.0).round() as u8,
649 ]
650 }
651
652 #[inline]
654 pub const fn from_srgb8(rgba: [u8; 4]) -> Self {
655 Self::new_ps(
656 component_from_srgb8_const(rgba[0]).into_ps(),
657 component_from_srgb8_const(rgba[1]).into_ps(),
658 component_from_srgb8_const(rgba[2]).into_ps(),
659 component_from_linear8(rgba[3]),
660 )
661 }
662
663 #[inline]
665 #[must_use]
666 pub fn clamp(self) -> Self {
667 Self {
668 rgb: self.rgb.clamp(PS1),
669 alpha: self.alpha,
670 }
671 }
672
673 #[inline]
681 #[doc(hidden)] pub fn reflect(self, illumination: Rgb) -> Rgb {
683 self.to_rgb() * illumination * self.alpha
684 }
685}
686
687impl From<Rgb01> for Rgb {
688 #[inline]
689 fn from(value: Rgb01) -> Self {
690 value.to_rgb()
691 }
692}
693
694impl From<Vector3D<PositiveSign<f32>, Intensity>> for Rgb {
695 #[inline]
696 fn from(value: Vector3D<PositiveSign<f32>, Intensity>) -> Self {
697 Self(value)
698 }
699}
700impl<U> From<Vector3D<ZeroOne<f32>, U>> for Rgb01 {
701 #[inline]
702 fn from(value: Vector3D<ZeroOne<f32>, U>) -> Self {
703 Self(value.cast_unit())
705 }
706}
707
708impl From<[PositiveSign<f32>; 3]> for Rgb {
709 #[inline]
710 fn from(value: [PositiveSign<f32>; 3]) -> Self {
711 Self(value.into())
712 }
713}
714impl From<[ZeroOne<f32>; 3]> for Rgb {
715 #[inline]
716 fn from(value: [ZeroOne<f32>; 3]) -> Self {
717 Self::from(value.map(PositiveSign::from))
718 }
719}
720impl From<[ZeroOne<f32>; 3]> for Rgb01 {
721 #[inline]
722 fn from(value: [ZeroOne<f32>; 3]) -> Self {
723 Self::from(Vector3D::<_, euclid::UnknownUnit>::from(value))
724 }
725}
726impl From<[ZeroOne<f32>; 4]> for Rgba {
727 #[inline]
728 fn from(value: [ZeroOne<f32>; 4]) -> Self {
729 let [r, g, b, alpha] = value;
730 Self {
731 rgb: Rgb::from([r, g, b]),
732 alpha,
733 }
734 }
735}
736impl
737 From<(
738 PositiveSign<f32>,
739 PositiveSign<f32>,
740 PositiveSign<f32>,
741 ZeroOne<f32>,
742 )> for Rgba
743{
744 #[inline]
745 fn from(
746 value: (
747 PositiveSign<f32>,
748 PositiveSign<f32>,
749 PositiveSign<f32>,
750 ZeroOne<f32>,
751 ),
752 ) -> Self {
753 let (r, g, b, alpha) = value;
754 Self {
755 rgb: Rgb::from([r, g, b]),
756 alpha,
757 }
758 }
759}
760
761impl From<Rgb> for Vector3D<f32, Intensity> {
762 #[inline]
763 fn from(value: Rgb) -> Self {
764 value.0.map(PositiveSign::<f32>::into_inner)
765 }
766}
767
768impl From<Rgb> for [PositiveSign<f32>; 3] {
769 #[inline]
770 fn from(value: Rgb) -> Self {
771 value.0.into()
772 }
773}
774impl From<Rgba> for [PositiveSign<f32>; 4] {
775 #[inline]
776 fn from(value: Rgba) -> Self {
777 let [r, g, b]: [PositiveSign<f32>; 3] = value.rgb.into();
778 [r, g, b, value.alpha.into()]
779 }
780}
781impl From<Rgba>
782 for (
783 PositiveSign<f32>,
784 PositiveSign<f32>,
785 PositiveSign<f32>,
786 ZeroOne<f32>,
787 )
788{
789 #[inline]
790 fn from(value: Rgba) -> Self {
791 let [r, g, b]: [PositiveSign<f32>; 3] = value.rgb.into();
792 (r, g, b, value.alpha)
793 }
794}
795
796impl From<Rgb> for [NotNan<f32>; 3] {
797 #[inline]
798 fn from(value: Rgb) -> Self {
799 value.0.map(PositiveSign::into).into()
800 }
801}
802impl From<Rgba> for [NotNan<f32>; 4] {
803 #[inline]
804 fn from(value: Rgba) -> Self {
805 let [r, g, b]: [NotNan<f32>; 3] = value.rgb.into();
806 [r, g, b, value.alpha.into()]
807 }
808}
809
810impl From<Rgb> for [f32; 3] {
811 #[inline]
812 fn from(value: Rgb) -> Self {
813 value.0.map(PositiveSign::<f32>::into_inner).into()
814 }
815}
816impl From<Rgba> for [f32; 4] {
817 #[inline]
818 fn from(value: Rgba) -> Self {
819 <[NotNan<f32>; 4]>::from(value).map(NotNan::into_inner)
820 }
821}
822
823impl TryFrom<Vector3D<f32, Intensity>> for Rgb {
824 type Error = NotPositiveSign<f32>;
825 #[inline]
826 fn try_from(value: Vector3D<f32, Intensity>) -> Result<Self, Self::Error> {
827 Ok(Self(vec3(
828 value.x.try_into()?,
829 value.y.try_into()?,
830 value.z.try_into()?,
831 )))
832 }
833}
834
835impl Add<Rgb> for Rgb {
836 type Output = Self;
837 #[inline]
838 fn add(self, other: Self) -> Self {
839 Self(self.0 + other.0)
840 }
841}
842impl AddAssign<Rgb> for Rgb {
843 #[inline]
844 fn add_assign(&mut self, other: Self) {
845 self.0 += other.0;
846 }
847}
848impl Mul<Rgb> for Rgb {
850 type Output = Self;
851 #[inline]
853 fn mul(self, other: Rgb) -> Self {
854 Self(self.0.component_mul(other.0))
855 }
856}
857impl Mul<PositiveSign<f32>> for Rgb {
859 type Output = Self;
860 #[inline]
862 fn mul(self, scalar: PositiveSign<f32>) -> Self {
863 Self(self.0 * scalar)
864 }
865}
866impl Mul<ZeroOne<f32>> for Rgb {
867 type Output = Self;
868 #[inline]
870 fn mul(self, scalar: ZeroOne<f32>) -> Self {
871 Self(self.0 * PositiveSign::from(scalar))
872 }
873}
874impl Mul<ZeroOne<f32>> for Rgb01 {
875 type Output = Self;
876 #[inline]
878 fn mul(self, scalar: ZeroOne<f32>) -> Self {
879 Self(self.0 * scalar)
880 }
881}
882impl Mul<f32> for Rgb {
887 type Output = Self;
888 #[inline]
892 fn mul(self, scalar: f32) -> Self {
893 Self(self.0 * PositiveSign::<f32>::new_clamped(scalar))
894 }
895}
896
897impl Sum for Rgb {
900 #[allow(clippy::missing_inline_in_public_items)]
901 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
902 Rgb::try_from(iter.fold(Vector3D::<f32, Intensity>::zero(), |accum, rgb| {
904 accum + Vector3D::<f32, Intensity>::from(rgb)
905 })).unwrap()
906 }
907}
908
909impl fmt::Debug for Rgb {
910 #[allow(clippy::missing_inline_in_public_items)]
911 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
912 write!(
913 fmt,
914 "Rgb({:?}, {:?}, {:?})",
915 self.red().into_inner(),
916 self.green().into_inner(),
917 self.blue().into_inner()
918 )
919 }
920}
921impl fmt::Debug for Rgba {
922 #[allow(clippy::missing_inline_in_public_items)]
923 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
924 write!(
925 fmt,
926 "Rgba({:?}, {:?}, {:?}, {:?})",
927 self.red().into_inner(),
928 self.green().into_inner(),
929 self.blue().into_inner(),
930 self.alpha().into_inner()
931 )
932 }
933}
934
935#[cfg(feature = "arbitrary")]
938#[mutants::skip]
939#[allow(clippy::missing_inline_in_public_items)]
940impl<'a> arbitrary::Arbitrary<'a> for Rgb {
941 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
942 Ok(Rgb::new_ps(u.arbitrary()?, u.arbitrary()?, u.arbitrary()?))
943 }
944
945 fn size_hint(_depth: usize) -> (usize, Option<usize>) {
946 <[PositiveSign<f32>; 3]>::size_hint(0) }
948}
949#[cfg(feature = "arbitrary")]
950#[mutants::skip]
951#[allow(clippy::missing_inline_in_public_items)]
952impl<'a> arbitrary::Arbitrary<'a> for Rgba {
953 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
954 Ok(Rgba::new_ps(
955 u.arbitrary()?,
956 u.arbitrary()?,
957 u.arbitrary()?,
958 u.arbitrary()?,
959 ))
960 }
961
962 fn size_hint(_depth: usize) -> (usize, Option<usize>) {
963 <[PositiveSign<f32>; 4]>::size_hint(0) }
965}
966#[cfg(feature = "arbitrary")]
967#[mutants::skip]
968#[allow(clippy::missing_inline_in_public_items)]
969impl<'a> arbitrary::Arbitrary<'a> for Rgb01 {
970 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
971 Ok(Rgb01::new_zo(
972 u.arbitrary()?,
973 u.arbitrary()?,
974 u.arbitrary()?,
975 ))
976 }
977
978 fn size_hint(_depth: usize) -> (usize, Option<usize>) {
979 <[ZeroOne<f32>; 3]>::size_hint(0) }
981}
982
983mod eg {
985 use embedded_graphics_core::pixelcolor::{self, RgbColor as _};
986
987 use super::*;
988 impl pixelcolor::PixelColor for Rgb01 {
989 type Raw = ();
990 }
991 impl pixelcolor::PixelColor for Rgba {
992 type Raw = ();
993 }
994 impl From<pixelcolor::Rgb888> for Rgb01 {
997 #[inline]
998 fn from(color: pixelcolor::Rgb888) -> Rgb01 {
999 Rgb01::from_srgb8([color.r(), color.g(), color.b()])
1000 }
1001 }
1002}
1003
1004#[cfg(feature = "rerun")]
1005mod rerun {
1006 use super::*;
1007 use re_types::datatypes;
1008
1009 impl From<Rgb> for datatypes::Rgba32 {
1010 #[inline]
1011 fn from(value: Rgb) -> Self {
1012 value.with_alpha_one().into()
1013 }
1014 }
1015 impl From<Rgba> for datatypes::Rgba32 {
1016 #[inline]
1017 fn from(value: Rgba) -> Self {
1018 let [r, g, b, a] = value.to_srgb8();
1019 datatypes::Rgba32::from_unmultiplied_rgba(r, g, b, a)
1020 }
1021 }
1022}
1023
1024#[inline]
1028fn component_to_srgb(c: PositiveSign<f32>) -> f32 {
1029 let c = c.into_inner();
1032 if c <= 0.0031308 {
1034 c * (323. / 25.)
1035 } else {
1036 (211. * c.powf(5. / 12.) - 11.) / 200.
1037 }
1038}
1039
1040#[inline]
1041fn component_to_srgb8(c: PositiveSign<f32>) -> u8 {
1042 (component_to_srgb(c) * 255.).round() as u8
1044}
1045
1046#[inline]
1047const fn component_from_linear8(c: u8) -> ZeroOne<f32> {
1048 unsafe { ZeroOne::new_unchecked(c as f32 / 255.0) }
1050}
1051
1052#[cfg(test)] fn component_from_srgb8_arithmetic(c: u8) -> f32 {
1059 let c = f32::from(c) / 255.0;
1062 if c <= 0.04045 {
1064 c * (25. / 323.)
1065 } else {
1066 libm::powf((200. * c + 11.) / 211., 12. / 5.)
1071 }
1072}
1073
1074#[inline]
1076const fn component_from_srgb8_const(c: u8) -> ZeroOne<f32> {
1077 unsafe { ZeroOne::new_unchecked(CONST_SRGB_LOOKUP_TABLE[c as usize]) }
1079}
1080
1081#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1088#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1089#[expect(clippy::exhaustive_enums)]
1090#[repr(u8)]
1091pub enum OpacityCategory {
1092 Invisible = 0,
1094 Partial = 1,
1096 Opaque = 2,
1098}
1099
1100#[rustfmt::skip]
1106static CONST_SRGB_LOOKUP_TABLE: &[f32; 256] = &[
1107 0.0, 0.000303527, 0.000607054, 0.000910581, 0.001214108, 0.001517635,
1108 0.001821162, 0.0021246888, 0.002428216, 0.002731743, 0.00303527, 0.003346535,
1109 0.003676507, 0.0040247166, 0.004391441, 0.0047769523, 0.005181516, 0.0056053908,
1110 0.0060488326, 0.00651209, 0.00699541, 0.0074990317, 0.008023192, 0.008568125,
1111 0.009134057, 0.009721216, 0.01032982, 0.010960094, 0.011612244, 0.012286487,
1112 0.012983031, 0.013702083, 0.014443844, 0.015208514, 0.015996292, 0.016807374,
1113 0.017641956, 0.018500218, 0.019382361, 0.02028856, 0.02121901, 0.022173883,
1114 0.023153365, 0.02415763, 0.025186857, 0.026241219, 0.027320892, 0.028426038,
1115 0.029556833, 0.03071344, 0.03189603, 0.033104762, 0.0343398, 0.03560131,
1116 0.036889456, 0.038204376, 0.039546236, 0.040915187, 0.0423114, 0.04373502,
1117 0.04518619, 0.046665072, 0.048171822, 0.049706563, 0.051269464, 0.052860655,
1118 0.054480284, 0.056128494, 0.057805434, 0.05951123, 0.061246056, 0.06301002,
1119 0.06480328, 0.06662594, 0.06847817, 0.070360094, 0.07227186, 0.074213564,
1120 0.076185375, 0.07818741, 0.08021983, 0.082282715, 0.084376216, 0.08650045,
1121 0.08865559, 0.09084171, 0.09305896, 0.09530747, 0.09758735, 0.099898726,
1122 0.102241725, 0.10461648, 0.10702311, 0.1094617, 0.111932434, 0.11443536,
1123 0.11697067, 0.11953841, 0.122138776, 0.124771796, 0.12743768, 0.13013647,
1124 0.13286832, 0.13563332, 0.13843161, 0.14126328, 0.14412846, 0.14702725,
1125 0.1499598, 0.15292613, 0.15592647, 0.15896082, 0.16202939, 0.16513216,
1126 0.1682694, 0.17144108, 0.17464739, 0.17788841, 0.18116423, 0.18447497,
1127 0.18782076, 0.19120166, 0.19461781, 0.1980693, 0.20155624, 0.20507872,
1128 0.20863685, 0.21223073, 0.21586053, 0.21952623, 0.22322798, 0.22696589,
1129 0.23074007, 0.2345506, 0.23839758, 0.24228114, 0.24620134, 0.25015828,
1130 0.2541521, 0.25818285, 0.26225066, 0.2663556, 0.2704978, 0.2746773,
1131 0.27889434, 0.28314874, 0.2874409, 0.29177064, 0.29613832, 0.30054379,
1132 0.30498737, 0.30946898, 0.31398875, 0.31854674, 0.32314324, 0.32777813,
1133 0.3324515, 0.33716366, 0.34191445, 0.34670407, 0.35153264, 0.35640007,
1134 0.36130688, 0.3662526, 0.3712377, 0.37626213, 0.38132593, 0.38642943,
1135 0.39157248, 0.39675522, 0.40197787, 0.4072402, 0.4125426, 0.41788507,
1136 0.42326772, 0.42869055, 0.43415362, 0.43965715, 0.44520125, 0.45078585,
1137 0.456411, 0.46207696, 0.46778384, 0.47353154, 0.47932023, 0.4851499,
1138 0.4910209, 0.49693304, 0.5028865, 0.5088813, 0.5149177, 0.5209956,
1139 0.52711517, 0.53327644, 0.5394795, 0.54572445, 0.55201143, 0.5583404,
1140 0.5647115, 0.5711248, 0.57758045, 0.58407843, 0.5906189, 0.59720176,
1141 0.6038273, 0.61049557, 0.61720663, 0.6239604, 0.6307571, 0.63759685,
1142 0.64447975, 0.6514057, 0.6583748, 0.6653873, 0.6724432, 0.67954254,
1143 0.6866853, 0.6938717, 0.7011019, 0.7083758, 0.71569353, 0.723055,
1144 0.73046076, 0.7379104, 0.74540424, 0.7529423, 0.7605245, 0.76815116,
1145 0.7758222, 0.78353786, 0.7912979, 0.7991027, 0.80695224, 0.8148465,
1146 0.82278585, 0.83076984, 0.838799, 0.84687316, 0.8549927, 0.8631573,
1147 0.87136704, 0.87962234, 0.8879232, 0.89626944, 0.90466106, 0.9130986,
1148 0.9215819, 0.9301109, 0.9386858, 0.94730645, 0.9559734, 0.9646863,
1149 0.9734453, 0.9822504, 0.9911021, 1.0,
1150];
1151
1152#[cfg(test)]
1155mod tests {
1156 use crate::math::zo32;
1157
1158 use super::*;
1159 use alloc::vec::Vec;
1160 use exhaust::Exhaust as _;
1161 use itertools::Itertools as _;
1162
1163 #[test]
1166 fn rgba_to_srgb8() {
1167 assert_eq!(
1168 Rgba::new(0.125, 0.25, 0.5, 0.75).to_srgb8(),
1169 [99, 137, 188, 191]
1170 );
1171
1172 assert_eq!(
1174 Rgba::new(0.5, -0.0, 10.0, 1.0).to_srgb8(),
1175 [188, 0, 255, 255]
1176 );
1177 }
1178
1179 #[test]
1180 fn rgb_rgba_debug() {
1181 assert_eq!(
1182 format!("{:#?}", Rgb::new(0.1, 0.2, 0.3)),
1183 "Rgb(0.1, 0.2, 0.3)"
1184 );
1185 assert_eq!(
1186 format!("{:#?}", Rgba::new(0.1, 0.2, 0.3, 0.4)),
1187 "Rgba(0.1, 0.2, 0.3, 0.4)"
1188 );
1189 }
1190
1191 #[test]
1193 fn srgb_round_trip() {
1194 let srgb_figures = [
1195 0x00, 0x05, 0x10, 0x22, 0x33, 0x44, 0x55, 0x77, 0x7f, 0xDD, 0xFF,
1196 ];
1197 let results = srgb_figures
1198 .iter()
1199 .cartesian_product(srgb_figures.iter())
1200 .map(|(&r, &a)| {
1201 let srgb = [r, 0, 0, a];
1202 let color = Rgba::from_srgb8(srgb);
1203 (srgb, color, color.to_srgb8())
1204 })
1205 .collect::<Vec<_>>();
1206 eprintln!("{results:#?}");
1208 let bad = results
1210 .into_iter()
1211 .filter(|&(o, _, r)| o.into_iter().zip(r).any(|(a, b)| a != b))
1212 .collect::<Vec<_>>();
1213 assert_eq!(bad, vec![]);
1214 }
1215
1216 #[test]
1217 fn srgb_float() {
1218 let color = Rgba::new(0.05, 0.1, 0.4, 0.5);
1219 let srgb_float = color.to_srgb_float();
1220 let srgb8 = color.to_srgb8();
1221 assert_eq!(
1222 srgb8,
1223 [
1224 (srgb_float[0] * 255.).round() as u8,
1225 (srgb_float[1] * 255.).round() as u8,
1226 (srgb_float[2] * 255.).round() as u8,
1227 (srgb_float[3] * 255.).round() as u8
1228 ]
1229 );
1230 }
1231
1232 #[test]
1233 fn check_const_srgb_table() {
1234 let generated_table: Vec<f32> =
1235 (0..=u8::MAX).map(component_from_srgb8_arithmetic).collect();
1236 print!("static CONST_SRGB_LOOKUP_TABLE: [f32; 256] = [");
1237 for i in 0..=u8::MAX {
1238 if i.is_multiple_of(6) {
1239 print!("\n {:?},", generated_table[i as usize]);
1240 } else {
1241 print!(" {:?},", generated_table[i as usize]);
1242 }
1243 }
1244 println!("\n];");
1245
1246 pretty_assertions::assert_eq!(CONST_SRGB_LOOKUP_TABLE.to_vec(), generated_table);
1247 }
1248
1249 #[test]
1251 fn check_component_from_linear8() {
1252 assert_eq!(component_from_linear8(0), zo32(0.0));
1253 assert_eq!(component_from_linear8(255), zo32(1.0));
1254 for u8_value in 1..=254 {
1255 let float_value = component_from_linear8(u8_value);
1256 assert!(
1257 float_value > zo32(0.0) && float_value < zo32(1.0),
1258 "component_from_linear8({u8_value}) out of range {float_value}"
1259 );
1260 }
1261 }
1262
1263 #[test]
1264 fn check_uniform_luminance() {
1265 fn optimize(channel: usize) -> [u8; 4] {
1266 let reference_luminance = Rgb01::UNIFORM_LUMINANCE_BLUE.luminance();
1269 let (_color, srgb, luminance_difference) = u8::exhaust()
1270 .map(|srgb_byte| {
1271 let mut srgb = [0, 0, 0, 255];
1272 srgb[channel] = srgb_byte;
1273 let color = Rgba::from_srgb8(srgb);
1274 (color, srgb, (color.luminance() - reference_luminance).abs())
1275 })
1276 .min_by(|a, b| a.2.total_cmp(&b.2))
1277 .unwrap();
1278 println!("best luminance difference = {luminance_difference}");
1279 srgb
1280 }
1281
1282 println!("red:");
1283 assert_eq!(
1284 Rgb01::UNIFORM_LUMINANCE_RED.with_alpha_one().to_srgb8(),
1285 optimize(0)
1286 );
1287 println!("green:");
1288 assert_eq!(
1289 Rgb01::UNIFORM_LUMINANCE_GREEN.with_alpha_one().to_srgb8(),
1290 optimize(1)
1291 );
1292 }
1293
1294 #[cfg(feature = "arbitrary")]
1295 #[test]
1296 fn arbitrary_size_hints() {
1297 use arbitrary::Arbitrary as _;
1298
1299 assert_eq!(Rgb::size_hint(0), (12, Some(12)));
1300 assert_eq!(Rgb::try_size_hint(0).unwrap(), (12, Some(12)));
1301 assert_eq!(Rgba::size_hint(0), (16, Some(16)));
1302 assert_eq!(Rgba::try_size_hint(0).unwrap(), (16, Some(16)));
1303 }
1304}