Skip to content

Commit

Permalink
Merge TaggedColor and CssColor (#25)
Browse files Browse the repository at this point in the history
As per discussion, the new name is `DynamicColor`, following the
precedent of the `image` crate.

Also do some renames and add some docstrings.

Closes #21. The rename of Bitset closes #17.
  • Loading branch information
raphlinus authored Nov 10, 2024
1 parent 502dc41 commit 2ca51c9
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 131 deletions.
4 changes: 2 additions & 2 deletions color/examples/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
);
}
Expand Down
5 changes: 2 additions & 3 deletions color/examples/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@
//! 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");
match color::parse_color(&arg) {
Ok(color) => {
println!("display: {color}");
println!("debug: {color:?}");
let tagged = CssColor::to_tagged_color(color);
let srgba: AlphaColor<Srgb> = tagged.to_alpha_color();
let srgba: AlphaColor<Srgb> = color.to_alpha_color();
println!("{srgba:?}");
let lab: AlphaColor<Lab> = color.to_alpha_color();
println!("{lab:?}");
Expand Down
2 changes: 1 addition & 1 deletion color/src/colorspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
78 changes: 44 additions & 34 deletions color/src/css.rs → color/src/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,31 @@
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.
///
/// This type is roughly equivalent to [`AlphaColor`] except with a tag
/// for color space as opposed being determined at compile time. It can
/// also represent missing components, which are a feature of the CSS
/// Color 4 spec.
///
/// Missing components are mostly useful for interpolation, and in that
/// context take the value of the other color being interpolated. For
/// example, interpolating a color in [Oklch] with `oklch(none 0 none)`
/// fades the color saturation, ending in a gray with the same lightness.
///
/// In other contexts, missing colors are interpreted as a zero value.
/// When manipulating components directly, setting them nonzero when the
/// corresponding missing flag is set may yield unexpected results.
///
/// [Oklch]: crate::Oklch
#[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],
}

Expand All @@ -27,36 +44,30 @@ pub struct Interpolator {
delta_premul: [f32; 3],
alpha2: f32,
cs: ColorSpaceTag,
missing: Bitset,
missing: Missing,
}

impl From<TaggedColor> 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<CS: ColorSpace>(self) -> AlphaColor<CS> {
self.to_tagged_color().to_alpha_color()
if let Some(cs) = CS::TAG {
AlphaColor::new(self.convert(cs).components)
} else {
self.to_alpha_color::<LinearSrgb>().convert()
}
}

#[must_use]
pub fn from_alpha_color<CS: ColorSpace>(color: AlphaColor<CS>) -> 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::<LinearSrgb>())
}
}

#[must_use]
Expand All @@ -68,11 +79,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) {
Expand All @@ -81,7 +91,7 @@ impl CssColor {
}
self.missing
} else {
let mut missing = self.missing & Bitset::single(3);
let mut missing = self.missing & Missing::single(3);
if self.cs.h_missing(self.missing) {
cs.set_h_missing(&mut missing, &mut components);
}
Expand All @@ -94,7 +104,7 @@ impl CssColor {
missing
}
} else {
Bitset::default()
Missing::default()
};
let mut result = Self {
cs,
Expand All @@ -107,7 +117,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;
Expand Down Expand Up @@ -267,7 +277,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],
Expand All @@ -280,7 +290,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,
Expand Down
12 changes: 7 additions & 5 deletions color/src/gradient.rs
Original file line number Diff line number Diff line change
@@ -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<CS: ColorSpace> {
Expand All @@ -17,18 +19,18 @@ pub struct GradientIter<CS: ColorSpace> {
}

pub fn gradient<CS: ColorSpace>(
mut color0: CssColor,
mut color1: CssColor,
mut color0: DynamicColor,
mut color1: DynamicColor,
interp_cs: ColorSpaceTag,
direction: HueDirection,
tolerance: f32,
) -> GradientIter<CS> {
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();
Expand Down
12 changes: 6 additions & 6 deletions color/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 13 additions & 9 deletions color/src/bitset.rs → color/src/missing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,49 @@

//! 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;
}

/// The set containing a single component index.
pub fn single(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 {
Self(self.0 & rhs.0)
}
}

impl core::ops::BitOr for Bitset {
impl core::ops::BitOr for Missing {
type Output = Self;

fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}

impl core::ops::Not for Bitset {
impl core::ops::Not for Missing {
type Output = Self;

fn not(self) -> Self::Output {
Expand Down
Loading

0 comments on commit 2ca51c9

Please sign in to comment.