diff --git a/color/examples/gradient.rs b/color/examples/gradient.rs index 4936adf..4a571ee 100644 --- a/color/examples/gradient.rs +++ b/color/examples/gradient.rs @@ -11,7 +11,7 @@ //! cargo run --example gradient 'oklab(0.5 0.2 0)' 'rgb(0, 200, 0, 0.8)' oklab //! ``` -use color::{gradient, ColorSpaceTag, CssColor, GradientIter, HueDirection, Srgb}; +use color::{gradient, ColorSpaceTag, DynamicColor, GradientIter, HueDirection, Srgb}; fn main() { let mut args = std::env::args().skip(1); @@ -33,7 +33,7 @@ fn main() { for (t, stop) in gradient { print!( ", {} {}%", - CssColor::from_alpha_color(stop.un_premultiply()), + DynamicColor::from_alpha_color(stop.un_premultiply()), t * 100.0 ); } diff --git a/color/examples/parse.rs b/color/examples/parse.rs index 3ebe1c7..52243df 100644 --- a/color/examples/parse.rs +++ b/color/examples/parse.rs @@ -11,7 +11,7 @@ //! cargo run --example parse 'oklab(0.5 0.2 0)' //! ``` -use color::{AlphaColor, CssColor, Lab, Srgb}; +use color::{AlphaColor, Lab, Srgb}; fn main() { let arg = std::env::args().nth(1).expect("give color as arg"); @@ -19,8 +19,7 @@ fn main() { Ok(color) => { println!("display: {color}"); println!("debug: {color:?}"); - let tagged = CssColor::to_tagged_color(color); - let srgba: AlphaColor = tagged.to_alpha_color(); + let srgba: AlphaColor = color.to_alpha_color(); println!("{srgba:?}"); let lab: AlphaColor = color.to_alpha_color(); println!("{lab:?}"); diff --git a/color/src/colorspace.rs b/color/src/colorspace.rs index 947d522..54c2a1d 100644 --- a/color/src/colorspace.rs +++ b/color/src/colorspace.rs @@ -3,7 +3,7 @@ use core::{any::TypeId, f32}; -use crate::{matmul, tagged::ColorSpaceTag}; +use crate::{matmul, tag::ColorSpaceTag}; #[cfg(all(not(feature = "std"), not(test)))] use crate::floatfuncs::FloatFuncs; diff --git a/color/src/css.rs b/color/src/dynamic.rs similarity index 86% rename from color/src/css.rs rename to color/src/dynamic.rs index 2a3b0c4..1e11b3e 100644 --- a/color/src/css.rs +++ b/color/src/dynamic.rs @@ -5,14 +5,15 @@ use crate::{ color::{add_alpha, fixup_hues_for_interpolate, split_alpha}, - AlphaColor, Bitset, ColorSpace, ColorSpaceLayout, ColorSpaceTag, HueDirection, TaggedColor, + AlphaColor, ColorSpace, ColorSpaceLayout, ColorSpaceTag, HueDirection, LinearSrgb, Missing, }; +/// A color with a color space tag decided at runtime. #[derive(Clone, Copy, Debug)] -pub struct CssColor { +pub struct DynamicColor { pub cs: ColorSpaceTag, /// A bitmask of missing components. - pub missing: Bitset, + pub missing: Missing, pub components: [f32; 4], } @@ -27,36 +28,30 @@ pub struct Interpolator { delta_premul: [f32; 3], alpha2: f32, cs: ColorSpaceTag, - missing: Bitset, + missing: Missing, } -impl From for CssColor { - fn from(value: TaggedColor) -> Self { - Self { - cs: value.cs, - missing: Bitset::default(), - components: value.components, - } - } -} - -impl CssColor { - #[must_use] - pub fn to_tagged_color(self) -> TaggedColor { - TaggedColor { - cs: self.cs, - components: self.components, - } - } - +impl DynamicColor { #[must_use] pub fn to_alpha_color(self) -> AlphaColor { - self.to_tagged_color().to_alpha_color() + if let Some(cs) = CS::TAG { + AlphaColor::new(self.convert(cs).components) + } else { + self.to_alpha_color::().convert() + } } #[must_use] pub fn from_alpha_color(color: AlphaColor) -> Self { - TaggedColor::from_alpha_color(color).into() + if let Some(cs) = CS::TAG { + Self { + cs, + missing: Missing::default(), + components: color.components, + } + } else { + Self::from_alpha_color(color.convert::()) + } } #[must_use] @@ -68,11 +63,10 @@ impl CssColor { // but Chrome and color.js don't seem do to that. self } else { - let tagged = self.to_tagged_color(); - let converted = tagged.convert(cs); - let mut components = converted.components; + let (opaque, alpha) = split_alpha(self.components); + let mut components = add_alpha(self.cs.convert(cs, opaque), alpha); // Reference: ยง12.2 of Color 4 spec - let missing = if self.missing.any() { + let missing = if !self.missing.is_empty() { if self.cs.same_analogous(cs) { for (i, component) in components.iter_mut().enumerate() { if self.missing.contains(i) { @@ -81,7 +75,7 @@ impl CssColor { } self.missing } else { - let mut missing = self.missing & Bitset::single(3); + let mut missing = self.missing & Missing::singleton(3); if self.cs.h_missing(self.missing) { cs.set_h_missing(&mut missing, &mut components); } @@ -94,7 +88,7 @@ impl CssColor { missing } } else { - Bitset::default() + Missing::default() }; let mut result = Self { cs, @@ -107,7 +101,7 @@ impl CssColor { } fn zero_missing_components(mut self) -> Self { - if self.missing.any() { + if !self.missing.is_empty() { for (i, component) in self.components.iter_mut().enumerate() { if self.missing.contains(i) { *component = 0.0; @@ -254,12 +248,10 @@ impl CssColor { #[must_use] pub fn map_lightness(self, f: impl Fn(f32) -> f32) -> Self { match self.cs { - ColorSpaceTag::Lab | ColorSpaceTag::Lch => { - self.map(|l, c1, c2, a| [100.0 * f(l * 0.01), c1, c2, a]) - } - ColorSpaceTag::Oklab | ColorSpaceTag::Oklch => { - self.map(|l, c1, c2, a| [f(l), c1, c2, a]) - } + ColorSpaceTag::Oklab + | ColorSpaceTag::Oklch + | ColorSpaceTag::Lab + | ColorSpaceTag::Lch => self.map(|l, c1, c2, a| [f(l), c1, c2, a]), ColorSpaceTag::Hsl => self.map(|h, s, l, a| [h, s, 100.0 * f(l * 0.01), a]), _ => self.map_in(ColorSpaceTag::Oklab, |l, a, b, alpha| [f(l), a, b, alpha]), } @@ -267,7 +259,7 @@ impl CssColor { } impl Interpolator { - pub fn eval(&self, t: f32) -> CssColor { + pub fn eval(&self, t: f32) -> DynamicColor { let premul = [ self.premul1[0] + t * self.delta_premul[0], self.premul1[1] + t * self.delta_premul[1], @@ -280,7 +272,7 @@ impl Interpolator { self.cs.layout().scale(premul, 1.0 / alpha) }; let components = add_alpha(opaque, alpha); - CssColor { + DynamicColor { cs: self.cs, missing: self.missing, components, diff --git a/color/src/gradient.rs b/color/src/gradient.rs index 620e1cf..42255be 100644 --- a/color/src/gradient.rs +++ b/color/src/gradient.rs @@ -1,7 +1,9 @@ // Copyright 2024 the Color Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::{ColorSpace, ColorSpaceTag, CssColor, HueDirection, Interpolator, Oklab, PremulColor}; +use crate::{ + ColorSpace, ColorSpaceTag, DynamicColor, HueDirection, Interpolator, Oklab, PremulColor, +}; #[expect(missing_debug_implementations, reason = "it's an iterator")] pub struct GradientIter { @@ -17,18 +19,18 @@ pub struct GradientIter { } pub fn gradient( - mut color0: CssColor, - mut color1: CssColor, + mut color0: DynamicColor, + mut color1: DynamicColor, interp_cs: ColorSpaceTag, direction: HueDirection, tolerance: f32, ) -> GradientIter { let interpolator = color0.interpolate(color1, interp_cs, direction); - if color0.missing.any() { + if !color0.missing.is_empty() { color0 = interpolator.eval(0.0); } let target0 = color0.to_alpha_color().premultiply(); - if color1.missing.any() { + if !color1.missing.is_empty() { color1 = interpolator.eval(1.0); } let target1 = color1.to_alpha_color().premultiply(); diff --git a/color/src/lib.rs b/color/src/lib.rs index 7a79d88..5b456de 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -20,29 +20,29 @@ //! TODO: need to write a treatise on the nature of color and how to model //! a reasonable fragment of it in the Rust type system. -mod bitset; mod color; mod colorspace; -mod css; mod gradient; +mod missing; // Note: this may become feature-gated; we'll decide this soon +mod dynamic; mod parse; mod serialize; -mod tagged; +mod tag; mod x11_colors; #[cfg(all(not(feature = "std"), not(test)))] mod floatfuncs; -pub use bitset::Bitset; pub use color::{AlphaColor, HueDirection, OpaqueColor, PremulColor}; pub use colorspace::{ ColorSpace, ColorSpaceLayout, DisplayP3, Lab, Lch, LinearSrgb, Oklab, Oklch, Srgb, XyzD65, }; -pub use css::{CssColor, Interpolator}; +pub use dynamic::{DynamicColor, Interpolator}; pub use gradient::{gradient, GradientIter}; +pub use missing::Missing; pub use parse::{parse_color, Error}; -pub use tagged::{ColorSpaceTag, TaggedColor}; +pub use tag::ColorSpaceTag; const fn u8_to_f32(x: u32) -> f32 { x as f32 * (1.0 / 255.0) diff --git a/color/src/bitset.rs b/color/src/missing.rs similarity index 52% rename from color/src/bitset.rs rename to color/src/missing.rs index 9b897f4..747c451 100644 --- a/color/src/bitset.rs +++ b/color/src/missing.rs @@ -3,29 +3,33 @@ //! A simple bitset. -/// A simple bitset, for representing missing components. +/// A simple bitset for representing missing components. #[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] -pub struct Bitset(u8); +pub struct Missing(u8); -impl Bitset { +impl Missing { + /// Returns `true` if the set contains the component index. pub fn contains(self, ix: usize) -> bool { (self.0 & (1 << ix)) != 0 } - pub fn set(&mut self, ix: usize) { + /// Adds a component index to the set. + pub fn insert(&mut self, ix: usize) { self.0 |= 1 << ix; } - pub fn single(ix: usize) -> Self { + /// The set containing a single component index. + pub fn singleton(ix: usize) -> Self { Self(1 << ix) } - pub fn any(self) -> bool { - self.0 != 0 + /// Returns `true` if the set contains no indices. + pub fn is_empty(self) -> bool { + self.0 == 0 } } -impl core::ops::BitAnd for Bitset { +impl core::ops::BitAnd for Missing { type Output = Self; fn bitand(self, rhs: Self) -> Self { @@ -33,7 +37,7 @@ impl core::ops::BitAnd for Bitset { } } -impl core::ops::BitOr for Bitset { +impl core::ops::BitOr for Missing { type Output = Self; fn bitor(self, rhs: Self) -> Self { @@ -41,7 +45,7 @@ impl core::ops::BitOr for Bitset { } } -impl core::ops::Not for Bitset { +impl core::ops::Not for Missing { type Output = Self; fn not(self) -> Self::Output { diff --git a/color/src/parse.rs b/color/src/parse.rs index 16dd2a5..4e26f6e 100644 --- a/color/src/parse.rs +++ b/color/src/parse.rs @@ -6,7 +6,7 @@ use core::f64; use core::str::FromStr; -use crate::{AlphaColor, Bitset, ColorSpaceTag, CssColor, Srgb}; +use crate::{AlphaColor, ColorSpaceTag, DynamicColor, Missing, Srgb}; // TODO: proper error type, maybe include string offset pub type Error = &'static str; @@ -30,14 +30,14 @@ enum Value<'a> { clippy::cast_possible_truncation, reason = "deliberate choice of f32 for colors" )] -fn color_from_components(components: [Option; 4], cs: ColorSpaceTag) -> CssColor { - let mut missing = Bitset::default(); +fn color_from_components(components: [Option; 4], cs: ColorSpaceTag) -> DynamicColor { + let mut missing = Missing::default(); for (i, component) in components.iter().enumerate() { if component.is_none() { - missing.set(i); + missing.insert(i); } } - CssColor { + DynamicColor { cs, missing, components: components.map(|x| x.unwrap_or(0.0) as f32), @@ -259,7 +259,7 @@ impl<'a> Parser<'a> { self.ch(if comma { b',' } else { b'/' }) } - fn rgb(&mut self) -> Result { + fn rgb(&mut self) -> Result { if !self.raw_ch(b'(') { return Err("expected arguments"); } @@ -298,7 +298,7 @@ impl<'a> Parser<'a> { Ok(alpha) } - fn lab(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result { + fn lab(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result { if !self.raw_ch(b'(') { return Err("expected arguments"); } @@ -314,7 +314,7 @@ impl<'a> Parser<'a> { Ok(color_from_components([l, a, b, alpha], tag)) } - fn lch(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result { + fn lch(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result { if !self.raw_ch(b'(') { return Err("expected arguments"); } @@ -330,7 +330,7 @@ impl<'a> Parser<'a> { Ok(color_from_components([l, c, h, alpha], tag)) } - fn color(&mut self) -> Result { + fn color(&mut self) -> Result { if !self.raw_ch(b'(') { return Err("expected arguments"); } @@ -363,10 +363,10 @@ impl<'a> Parser<'a> { /// /// Tries to return a suitable error for any invalid string, but may be /// a little lax on some details. -pub fn parse_color(s: &str) -> Result { +pub fn parse_color(s: &str) -> Result { if let Some(stripped) = s.strip_prefix('#') { let color = color_from_4bit_hex(get_4bit_hex_channels(stripped)?); - return Ok(CssColor::from_alpha_color(color)); + return Ok(DynamicColor::from_alpha_color(color)); } // TODO: the named x11 colors (steal from peniko) let mut parser = Parser::new(s); @@ -382,7 +382,7 @@ pub fn parse_color(s: &str) -> Result { _ => { if let Some([r, g, b, a]) = crate::x11_colors::lookup_palette(id) { let color = AlphaColor::from_rgba8(r, g, b, a); - Ok(CssColor::from_alpha_color(color)) + Ok(DynamicColor::from_alpha_color(color)) } else { Err("unknown color identifier") } @@ -452,11 +452,11 @@ impl FromStr for ColorSpaceTag { #[cfg(test)] mod tests { - use crate::CssColor; + use crate::DynamicColor; use super::parse_color; - fn assert_close_color(c1: CssColor, c2: CssColor) { + fn assert_close_color(c1: DynamicColor, c2: DynamicColor) { const EPSILON: f32 = 1e-4; assert_eq!(c1.cs, c2.cs); for i in 0..4 { diff --git a/color/src/serialize.rs b/color/src/serialize.rs index 5431f32..73e7c92 100644 --- a/color/src/serialize.rs +++ b/color/src/serialize.rs @@ -5,10 +5,10 @@ use core::fmt::{Formatter, Result}; -use crate::{ColorSpaceTag, CssColor}; +use crate::{ColorSpaceTag, DynamicColor}; fn write_scaled_component( - color: &CssColor, + color: &DynamicColor, ix: usize, f: &mut Formatter<'_>, scale: f32, @@ -24,7 +24,7 @@ fn write_scaled_component( } } -fn write_modern_function(color: &CssColor, name: &str, f: &mut Formatter<'_>) -> Result { +fn write_modern_function(color: &DynamicColor, name: &str, f: &mut Formatter<'_>) -> Result { write!(f, "{name}(")?; write_scaled_component(color, 0, f, 1.0)?; write!(f, " ")?; @@ -39,7 +39,7 @@ fn write_modern_function(color: &CssColor, name: &str, f: &mut Formatter<'_>) -> write!(f, ")") } -fn write_color_function(color: &CssColor, name: &str, f: &mut Formatter<'_>) -> Result { +fn write_color_function(color: &DynamicColor, name: &str, f: &mut Formatter<'_>) -> Result { write!(f, "color({name} ")?; write_scaled_component(color, 0, f, 1.0)?; write!(f, " ")?; @@ -54,7 +54,7 @@ fn write_color_function(color: &CssColor, name: &str, f: &mut Formatter<'_>) -> write!(f, ")") } -impl core::fmt::Display for CssColor { +impl core::fmt::Display for DynamicColor { fn fmt(&self, f: &mut Formatter<'_>) -> Result { match self.cs { ColorSpaceTag::Srgb => { diff --git a/color/src/tagged.rs b/color/src/tag.rs similarity index 76% rename from color/src/tagged.rs rename to color/src/tag.rs index cedfbdd..94c9384 100644 --- a/color/src/tagged.rs +++ b/color/src/tag.rs @@ -1,12 +1,11 @@ // Copyright 2024 the Color Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -//! Colors with runtime choice of color space. +//! The color space tag enum. use crate::{ - color::{add_alpha, split_alpha}, - AlphaColor, Bitset, ColorSpace, ColorSpaceLayout, DisplayP3, Lab, Lch, LinearSrgb, Oklab, - Oklch, Srgb, XyzD65, + ColorSpace, ColorSpaceLayout, DisplayP3, Lab, Lch, LinearSrgb, Missing, Oklab, Oklch, Srgb, + XyzD65, }; /// The color space tag for tagged colors. @@ -47,14 +46,6 @@ pub enum ColorSpaceTag { XyzD65, } -/// A color with a runtime color space tag. This type will likely get merged with -/// [`CssColor`][crate::css::CssColor]. -#[derive(Clone, Copy, Debug)] -pub struct TaggedColor { - pub cs: ColorSpaceTag, - pub components: [f32; 4], -} - impl ColorSpaceTag { pub(crate) fn layout(self) -> ColorSpaceLayout { match self { @@ -79,7 +70,7 @@ impl ColorSpaceTag { ) } - pub(crate) fn l_missing(self, missing: Bitset) -> bool { + pub(crate) fn l_missing(self, missing: Missing) -> bool { use ColorSpaceTag::*; match self { Lab | Lch | Oklab | Oklch => missing.contains(0), @@ -88,22 +79,22 @@ impl ColorSpaceTag { } } - pub(crate) fn set_l_missing(self, missing: &mut Bitset, components: &mut [f32; 4]) { + pub(crate) fn set_l_missing(self, missing: &mut Missing, components: &mut [f32; 4]) { use ColorSpaceTag::*; match self { Lab | Lch | Oklab | Oklch => { - missing.set(0); + missing.insert(0); components[0] = 0.0; } Hsl => { - missing.set(2); + missing.insert(2); components[2] = 0.0; } _ => (), } } - pub(crate) fn c_missing(self, missing: Bitset) -> bool { + pub(crate) fn c_missing(self, missing: Missing) -> bool { use ColorSpaceTag::*; match self { Lab | Lch | Oklab | Oklch | Hsl => missing.contains(1), @@ -111,26 +102,26 @@ impl ColorSpaceTag { } } - pub(crate) fn set_c_missing(self, missing: &mut Bitset, components: &mut [f32; 4]) { + pub(crate) fn set_c_missing(self, missing: &mut Missing, components: &mut [f32; 4]) { use ColorSpaceTag::*; match self { Lab | Lch | Oklab | Oklch | Hsl => { - missing.set(1); + missing.insert(1); components[1] = 0.0; } _ => (), } } - pub(crate) fn h_missing(self, missing: Bitset) -> bool { + pub(crate) fn h_missing(self, missing: Missing) -> bool { self.layout() .hue_channel() .is_some_and(|ix| missing.contains(ix)) } - pub(crate) fn set_h_missing(self, missing: &mut Bitset, components: &mut [f32; 4]) { + pub(crate) fn set_h_missing(self, missing: &mut Missing, components: &mut [f32; 4]) { if let Some(ix) = self.layout().hue_channel() { - missing.set(ix); + missing.insert(ix); components[ix] = 0.0; } } @@ -214,31 +205,3 @@ impl ColorSpaceTag { } } } - -impl TaggedColor { - #[must_use] - pub fn from_alpha_color(color: AlphaColor) -> Self { - if let Some(cs) = CS::TAG { - let components = color.components; - Self { cs, components } - } else { - Self::from_alpha_color(color.convert::()) - } - } - - #[must_use] - pub fn to_alpha_color(&self) -> AlphaColor { - if let Some(cs) = CS::TAG { - AlphaColor::new(self.convert(cs).components) - } else { - self.to_alpha_color::().convert() - } - } - - #[must_use] - pub fn convert(self, cs: ColorSpaceTag) -> Self { - let (opaque, alpha) = split_alpha(self.components); - let components = add_alpha(self.cs.convert(cs, opaque), alpha); - Self { components, cs } - } -}