use core::any::TypeId;
use core::marker::PhantomData;
use crate::{
cache_key::{BitEq, BitHash},
ColorSpace, ColorSpaceLayout, ColorSpaceTag, Oklab, Oklch, PremulRgba8, Rgba8, Srgb,
};
#[cfg(all(not(feature = "std"), not(test)))]
use crate::floatfuncs::FloatFuncs;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[repr(transparent)]
pub struct OpaqueColor<CS> {
pub components: [f32; 3],
pub cs: PhantomData<CS>,
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[repr(transparent)]
pub struct AlphaColor<CS> {
pub components: [f32; 4],
pub cs: PhantomData<CS>,
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[repr(transparent)]
pub struct PremulColor<CS> {
pub components: [f32; 4],
pub cs: PhantomData<CS>,
}
#[derive(Clone, Copy, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum HueDirection {
#[default]
Shorter = 0,
Longer = 1,
Increasing = 2,
Decreasing = 3,
}
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub(crate) enum InterpolationAlphaSpace {
#[default]
Premultiplied = 0,
Unpremultiplied = 1,
}
impl InterpolationAlphaSpace {
pub(crate) const fn is_unpremultiplied(self) -> bool {
matches!(self, Self::Unpremultiplied)
}
}
fn fixup_hue(h1: f32, h2: &mut f32, direction: HueDirection) {
let dh = (*h2 - h1) * (1. / 360.);
match direction {
HueDirection::Shorter => {
*h2 -= 360. * ((dh.abs() - 0.25) - 0.25).ceil().copysign(dh);
}
HueDirection::Longer => {
let t = 2.0 * dh.abs().ceil() - (dh.abs() + 1.5).floor();
*h2 += 360.0 * (t.copysign(0.0 - dh));
}
HueDirection::Increasing => *h2 -= 360.0 * dh.floor(),
HueDirection::Decreasing => *h2 -= 360.0 * dh.ceil(),
}
}
pub(crate) fn fixup_hues_for_interpolate(
a: [f32; 3],
b: &mut [f32; 3],
layout: ColorSpaceLayout,
direction: HueDirection,
) {
if let Some(ix) = layout.hue_channel() {
fixup_hue(a[ix], &mut b[ix], direction);
}
}
impl<CS: ColorSpace> OpaqueColor<CS> {
pub const BLACK: Self = Self::new([0., 0., 0.]);
pub const WHITE: Self = Self::new(CS::WHITE_COMPONENTS);
pub const fn new(components: [f32; 3]) -> Self {
let cs = PhantomData;
Self { components, cs }
}
#[must_use]
pub fn convert<TargetCS: ColorSpace>(self) -> OpaqueColor<TargetCS> {
OpaqueColor::new(CS::convert::<TargetCS>(self.components))
}
#[must_use]
pub const fn with_alpha(self, alpha: f32) -> AlphaColor<CS> {
AlphaColor::new(add_alpha(self.components, alpha))
}
#[must_use]
pub fn difference(self, other: Self) -> f32 {
let d = (self - other).components;
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
}
#[must_use]
pub fn lerp_rect(self, other: Self, t: f32) -> Self {
self + t * (other - self)
}
pub fn fixup_hues(self, other: &mut Self, direction: HueDirection) {
fixup_hues_for_interpolate(
self.components,
&mut other.components,
CS::LAYOUT,
direction,
);
}
#[must_use]
pub fn lerp(self, mut other: Self, t: f32, direction: HueDirection) -> Self {
self.fixup_hues(&mut other, direction);
self.lerp_rect(other, t)
}
#[must_use]
pub fn scale_chroma(self, scale: f32) -> Self {
Self::new(CS::scale_chroma(self.components, scale))
}
#[must_use]
pub fn relative_luminance(self) -> f32 {
let [r, g, b] = CS::to_linear_srgb(self.components);
0.2126 * r + 0.7152 * g + 0.0722 * b
}
#[must_use]
pub fn map(self, f: impl Fn(f32, f32, f32) -> [f32; 3]) -> Self {
let [x, y, z] = self.components;
Self::new(f(x, y, z))
}
#[must_use]
pub fn map_in<TargetCS: ColorSpace>(self, f: impl Fn(f32, f32, f32) -> [f32; 3]) -> Self {
self.convert::<TargetCS>().map(f).convert()
}
#[must_use]
pub fn map_lightness(self, f: impl Fn(f32) -> f32) -> Self {
match CS::TAG {
Some(ColorSpaceTag::Lab) | Some(ColorSpaceTag::Lch) => {
self.map(|l, c1, c2| [100.0 * f(l * 0.01), c1, c2])
}
Some(ColorSpaceTag::Oklab) | Some(ColorSpaceTag::Oklch) => {
self.map(|l, c1, c2| [f(l), c1, c2])
}
Some(ColorSpaceTag::Hsl) => self.map(|h, s, l| [h, s, 100.0 * f(l * 0.01)]),
_ => self.map_in::<Oklab>(|l, a, b| [f(l), a, b]),
}
}
#[must_use]
pub fn map_hue(self, f: impl Fn(f32) -> f32) -> Self {
match CS::LAYOUT {
ColorSpaceLayout::HueFirst => self.map(|h, c1, c2| [f(h), c1, c2]),
ColorSpaceLayout::HueThird => self.map(|c0, c1, h| [c0, c1, f(h)]),
_ => self.map_in::<Oklch>(|l, c, h| [l, c, f(h)]),
}
}
#[must_use]
pub fn to_rgba8(self) -> Rgba8 {
self.with_alpha(1.0).to_rgba8()
}
}
pub(crate) const fn split_alpha([x, y, z, a]: [f32; 4]) -> ([f32; 3], f32) {
([x, y, z], a)
}
pub(crate) const fn add_alpha([x, y, z]: [f32; 3], a: f32) -> [f32; 4] {
[x, y, z, a]
}
impl<CS: ColorSpace> AlphaColor<CS> {
pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
pub const WHITE: Self = Self::new(add_alpha(CS::WHITE_COMPONENTS, 1.));
pub const fn new(components: [f32; 4]) -> Self {
let cs = PhantomData;
Self { components, cs }
}
#[must_use]
pub const fn split(self) -> (OpaqueColor<CS>, f32) {
let (opaque, alpha) = split_alpha(self.components);
(OpaqueColor::new(opaque), alpha)
}
#[must_use]
pub const fn with_alpha(self, alpha: f32) -> Self {
let (opaque, _alpha) = split_alpha(self.components);
Self::new(add_alpha(opaque, alpha))
}
#[must_use]
pub const fn discard_alpha(self) -> OpaqueColor<CS> {
self.split().0
}
#[must_use]
pub fn convert<TargetCs: ColorSpace>(self) -> AlphaColor<TargetCs> {
let (opaque, alpha) = split_alpha(self.components);
let components = CS::convert::<TargetCs>(opaque);
AlphaColor::new(add_alpha(components, alpha))
}
#[must_use]
pub const fn premultiply(self) -> PremulColor<CS> {
let (opaque, alpha) = split_alpha(self.components);
PremulColor::new(add_alpha(CS::LAYOUT.scale(opaque, alpha), alpha))
}
#[must_use]
pub(crate) fn difference(self, other: Self) -> f32 {
let d = (self - other).components;
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2] + d[3] * d[3]).sqrt()
}
#[must_use]
pub fn lerp_rect(self, other: Self, t: f32) -> Self {
self.premultiply()
.lerp_rect(other.premultiply(), t)
.un_premultiply()
}
#[must_use]
pub fn lerp(self, other: Self, t: f32, direction: HueDirection) -> Self {
self.premultiply()
.lerp(other.premultiply(), t, direction)
.un_premultiply()
}
#[must_use]
pub const fn multiply_alpha(self, rhs: f32) -> Self {
let (opaque, alpha) = split_alpha(self.components);
Self::new(add_alpha(opaque, alpha * rhs))
}
#[must_use]
pub fn scale_chroma(self, scale: f32) -> Self {
let (opaque, alpha) = split_alpha(self.components);
Self::new(add_alpha(CS::scale_chroma(opaque, scale), alpha))
}
#[must_use]
pub fn map(self, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self {
let [x, y, z, a] = self.components;
Self::new(f(x, y, z, a))
}
#[must_use]
pub fn map_in<TargetCS: ColorSpace>(self, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self {
self.convert::<TargetCS>().map(f).convert()
}
#[must_use]
pub fn map_lightness(self, f: impl Fn(f32) -> f32) -> Self {
match CS::TAG {
Some(ColorSpaceTag::Lab) | Some(ColorSpaceTag::Lch) => {
self.map(|l, c1, c2, a| [100.0 * f(l * 0.01), c1, c2, a])
}
Some(ColorSpaceTag::Oklab) | Some(ColorSpaceTag::Oklch) => {
self.map(|l, c1, c2, a| [f(l), c1, c2, a])
}
Some(ColorSpaceTag::Hsl) => self.map(|h, s, l, a| [h, s, 100.0 * f(l * 0.01), a]),
_ => self.map_in::<Oklab>(|l, a, b, alpha| [f(l), a, b, alpha]),
}
}
#[must_use]
pub fn map_hue(self, f: impl Fn(f32) -> f32) -> Self {
match CS::LAYOUT {
ColorSpaceLayout::HueFirst => self.map(|h, c1, c2, a| [f(h), c1, c2, a]),
ColorSpaceLayout::HueThird => self.map(|c0, c1, h, a| [c0, c1, f(h), a]),
_ => self.map_in::<Oklch>(|l, c, h, alpha| [l, c, f(h), alpha]),
}
}
#[must_use]
pub fn to_rgba8(self) -> Rgba8 {
let [r, g, b, a] = self
.convert::<Srgb>()
.components
.map(|x| fast_round_to_u8(x * 255.));
Rgba8 { r, g, b, a }
}
}
impl<CS: ColorSpace> PremulColor<CS> {
pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
pub const WHITE: Self = Self::new(add_alpha(CS::WHITE_COMPONENTS, 1.));
pub const fn new(components: [f32; 4]) -> Self {
let cs = PhantomData;
Self { components, cs }
}
#[must_use]
pub const fn discard_alpha(self) -> OpaqueColor<CS> {
self.un_premultiply().discard_alpha()
}
#[must_use]
pub fn convert<TargetCS: ColorSpace>(self) -> PremulColor<TargetCS> {
if TypeId::of::<CS>() == TypeId::of::<TargetCS>() {
PremulColor::new(self.components)
} else if TargetCS::IS_LINEAR && CS::IS_LINEAR {
let (multiplied, alpha) = split_alpha(self.components);
let components = CS::convert::<TargetCS>(multiplied);
PremulColor::new(add_alpha(components, alpha))
} else {
self.un_premultiply().convert().premultiply()
}
}
#[must_use]
pub const fn un_premultiply(self) -> AlphaColor<CS> {
let (multiplied, alpha) = split_alpha(self.components);
let scale = if alpha == 0.0 { 1.0 } else { 1.0 / alpha };
AlphaColor::new(add_alpha(CS::LAYOUT.scale(multiplied, scale), alpha))
}
#[must_use]
pub fn lerp_rect(self, other: Self, t: f32) -> Self {
self + t * (other - self)
}
pub fn fixup_hues(self, other: &mut Self, direction: HueDirection) {
if let Some(ix) = CS::LAYOUT.hue_channel() {
fixup_hue(self.components[ix], &mut other.components[ix], direction);
}
}
#[must_use]
pub fn lerp(self, mut other: Self, t: f32, direction: HueDirection) -> Self {
self.fixup_hues(&mut other, direction);
self.lerp_rect(other, t)
}
#[must_use]
pub const fn multiply_alpha(self, rhs: f32) -> Self {
let (multiplied, alpha) = split_alpha(self.components);
Self::new(add_alpha(CS::LAYOUT.scale(multiplied, rhs), alpha * rhs))
}
#[must_use]
pub fn difference(self, other: Self) -> f32 {
let d = (self - other).components;
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2] + d[3] * d[3]).sqrt()
}
#[must_use]
pub fn to_rgba8(self) -> PremulRgba8 {
let [r, g, b, a] = self
.convert::<Srgb>()
.components
.map(|x| fast_round_to_u8(x * 255.));
PremulRgba8 { r, g, b, a }
}
}
#[inline(always)]
#[expect(clippy::cast_possible_truncation, reason = "deliberate quantization")]
fn fast_round_to_u8(a: f32) -> u8 {
(a + 0.5) as u8
}
impl<CS: ColorSpace> From<OpaqueColor<CS>> for AlphaColor<CS> {
fn from(value: OpaqueColor<CS>) -> Self {
value.with_alpha(1.0)
}
}
impl<CS: ColorSpace> From<OpaqueColor<CS>> for PremulColor<CS> {
fn from(value: OpaqueColor<CS>) -> Self {
Self::new(add_alpha(value.components, 1.0))
}
}
impl<CS: ColorSpace> PartialEq for AlphaColor<CS> {
fn eq(&self, other: &Self) -> bool {
self.components == other.components
}
}
impl<CS: ColorSpace> PartialEq for OpaqueColor<CS> {
fn eq(&self, other: &Self) -> bool {
self.components == other.components
}
}
impl<CS: ColorSpace> PartialEq for PremulColor<CS> {
fn eq(&self, other: &Self) -> bool {
self.components == other.components
}
}
impl<CS: ColorSpace> core::ops::Mul<f32> for OpaqueColor<CS> {
type Output = Self;
fn mul(self, rhs: f32) -> Self {
Self::new(self.components.map(|x| x * rhs))
}
}
impl<CS: ColorSpace> core::ops::Mul<OpaqueColor<CS>> for f32 {
type Output = OpaqueColor<CS>;
fn mul(self, rhs: OpaqueColor<CS>) -> Self::Output {
rhs * self
}
}
impl<CS: ColorSpace> core::ops::Div<f32> for OpaqueColor<CS> {
type Output = Self;
#[expect(clippy::suspicious_arithmetic_impl, reason = "multiplicative inverse")]
fn div(self, rhs: f32) -> Self {
self * rhs.recip()
}
}
impl<CS: ColorSpace> core::ops::Add for OpaqueColor<CS> {
type Output = Self;
fn add(self, rhs: Self) -> Self {
let x = self.components;
let y = rhs.components;
Self::new([x[0] + y[0], x[1] + y[1], x[2] + y[2]])
}
}
impl<CS: ColorSpace> core::ops::Sub for OpaqueColor<CS> {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
let x = self.components;
let y = rhs.components;
Self::new([x[0] - y[0], x[1] - y[1], x[2] - y[2]])
}
}
impl<CS> BitEq for OpaqueColor<CS> {
fn bit_eq(&self, other: &Self) -> bool {
self.components.bit_eq(&other.components)
}
}
impl<CS> BitHash for OpaqueColor<CS> {
fn bit_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.components.bit_hash(state);
}
}
impl<CS: ColorSpace> core::ops::Mul<f32> for AlphaColor<CS> {
type Output = Self;
fn mul(self, rhs: f32) -> Self {
Self::new(self.components.map(|x| x * rhs))
}
}
impl<CS: ColorSpace> core::ops::Mul<AlphaColor<CS>> for f32 {
type Output = AlphaColor<CS>;
fn mul(self, rhs: AlphaColor<CS>) -> Self::Output {
rhs * self
}
}
impl<CS: ColorSpace> core::ops::Div<f32> for AlphaColor<CS> {
type Output = Self;
#[expect(clippy::suspicious_arithmetic_impl, reason = "multiplicative inverse")]
fn div(self, rhs: f32) -> Self {
self * rhs.recip()
}
}
impl<CS: ColorSpace> core::ops::Add for AlphaColor<CS> {
type Output = Self;
fn add(self, rhs: Self) -> Self {
let x = self.components;
let y = rhs.components;
Self::new([x[0] + y[0], x[1] + y[1], x[2] + y[2], x[3] + y[3]])
}
}
impl<CS: ColorSpace> core::ops::Sub for AlphaColor<CS> {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
let x = self.components;
let y = rhs.components;
Self::new([x[0] - y[0], x[1] - y[1], x[2] - y[2], x[3] - y[3]])
}
}
impl<CS> BitEq for AlphaColor<CS> {
fn bit_eq(&self, other: &Self) -> bool {
self.components.bit_eq(&other.components)
}
}
impl<CS> BitHash for AlphaColor<CS> {
fn bit_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.components.bit_hash(state);
}
}
impl<CS: ColorSpace> core::ops::Mul<f32> for PremulColor<CS> {
type Output = Self;
fn mul(self, rhs: f32) -> Self {
Self::new(self.components.map(|x| x * rhs))
}
}
impl<CS: ColorSpace> core::ops::Mul<PremulColor<CS>> for f32 {
type Output = PremulColor<CS>;
fn mul(self, rhs: PremulColor<CS>) -> Self::Output {
rhs * self
}
}
impl<CS: ColorSpace> core::ops::Div<f32> for PremulColor<CS> {
type Output = Self;
#[expect(clippy::suspicious_arithmetic_impl, reason = "multiplicative inverse")]
fn div(self, rhs: f32) -> Self {
self * rhs.recip()
}
}
impl<CS: ColorSpace> core::ops::Add for PremulColor<CS> {
type Output = Self;
fn add(self, rhs: Self) -> Self {
let x = self.components;
let y = rhs.components;
Self::new([x[0] + y[0], x[1] + y[1], x[2] + y[2], x[3] + y[3]])
}
}
impl<CS: ColorSpace> core::ops::Sub for PremulColor<CS> {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
let x = self.components;
let y = rhs.components;
Self::new([x[0] - y[0], x[1] - y[1], x[2] - y[2], x[3] - y[3]])
}
}
impl<CS> BitEq for PremulColor<CS> {
fn bit_eq(&self, other: &Self) -> bool {
self.components.bit_eq(&other.components)
}
}
impl<CS> BitHash for PremulColor<CS> {
fn bit_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.components.bit_hash(state);
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use super::{
fast_round_to_u8, fixup_hue, AlphaColor, HueDirection, PremulColor, PremulRgba8, Rgba8,
Srgb,
};
#[test]
fn to_rgba8_saturation() {
let (r, g, b, a) = (0, 0, 255, 255);
let ac = AlphaColor::<Srgb>::new([-1.01, -0.5, 1.01, 2.0]);
assert_eq!(ac.to_rgba8(), Rgba8 { r, g, b, a });
let pc = PremulColor::<Srgb>::new([-1.01, -0.5, 1.01, 2.0]);
assert_eq!(pc.to_rgba8(), PremulRgba8 { r, g, b, a });
}
#[test]
fn hue_fixup() {
for h1 in [0.0, 10.0, 180.0, 190.0, 350.0] {
for h2 in [0.0, 10.0, 180.0, 190.0, 350.0] {
let dh = h2 - h1;
{
let mut fixed_h2 = h2;
fixup_hue(h1, &mut fixed_h2, HueDirection::Shorter);
let (mut spec_h1, mut spec_h2) = (h1, h2);
if dh > 180.0 {
spec_h1 += 360.0;
} else if dh < -180.0 {
spec_h2 += 360.0;
}
assert_eq!(fixed_h2 - h1, spec_h2 - spec_h1);
}
{
let mut fixed_h2 = h2;
fixup_hue(h1, &mut fixed_h2, HueDirection::Longer);
let (mut spec_h1, mut spec_h2) = (h1, h2);
if 0.0 < dh && dh < 180.0 {
spec_h1 += 360.0;
} else if -180.0 < dh && dh <= 0.0 {
spec_h2 += 360.0;
}
assert_eq!(fixed_h2 - h1, spec_h2 - spec_h1);
}
{
let mut fixed_h2 = h2;
fixup_hue(h1, &mut fixed_h2, HueDirection::Increasing);
let (spec_h1, mut spec_h2) = (h1, h2);
if dh < 0.0 {
spec_h2 += 360.0;
}
assert_eq!(fixed_h2 - h1, spec_h2 - spec_h1);
}
{
let mut fixed_h2 = h2;
fixup_hue(h1, &mut fixed_h2, HueDirection::Decreasing);
let (mut spec_h1, spec_h2) = (h1, h2);
if dh > 0.0 {
spec_h1 += 360.0;
}
assert_eq!(fixed_h2 - h1, spec_h2 - spec_h1);
}
}
}
}
#[test]
fn fast_round() {
#[expect(clippy::cast_possible_truncation, reason = "deliberate quantization")]
fn real_round_to_u8(v: f32) -> u8 {
v.round() as u8
}
let mut failures = alloc::vec![];
let mut v = -1_f32;
while v <= 256. {
assert!(v.abs().fract() == 0. || v.abs().fract() == 0.5, "{v}");
let mut validate_rounding = |val: f32| {
if real_round_to_u8(val) != fast_round_to_u8(val) {
failures.push(val);
}
};
validate_rounding(v.next_down().next_down());
validate_rounding(v.next_down());
validate_rounding(v);
validate_rounding(v.next_up());
validate_rounding(v.next_up().next_up());
v += 0.5;
}
assert_eq!(&failures, &[0.49999997]);
}
#[test]
#[ignore = "Takes too long to execute."]
fn fast_round_full() {
#[expect(clippy::cast_possible_truncation, reason = "deliberate quantization")]
fn real_round_to_u8(v: f32) -> u8 {
v.round() as u8
}
let mut failures = alloc::vec![];
let mut v = -1_f32;
while v <= 256. {
if real_round_to_u8(v) != fast_round_to_u8(v) {
failures.push(v);
}
v = v.next_up();
}
assert_eq!(&failures, &[0.49999997]);
}
}