Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge TaggedColor and CssColor #25

Merged
merged 3 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
72 changes: 32 additions & 40 deletions color/src/css.rs → color/src/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
}

Expand All @@ -27,36 +28,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 +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) {
Expand All @@ -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);
}
Expand All @@ -94,7 +88,7 @@ impl CssColor {
missing
}
} else {
Bitset::default()
Missing::default()
};
let mut result = Self {
cs,
Expand All @@ -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;
Expand Down Expand Up @@ -254,20 +248,18 @@ 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]),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bad merge, right?

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]),
}
}
}

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 +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,
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
24 changes: 14 additions & 10 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);
Comment on lines -6 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be clearer if this represented present (non-missing) components? 0 to represent "present" and 1 to represent "missing" seems somehow inverted. Presumably this is equivalent on a technical level?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps "missing" semantics make sense because the common case is "no missing components"? If so then perhaps the code could stay the same, but the method could be documented in terms of "which components are present (non-missing)" rather than (or in addition to) "which components are in the set".

e.g. for is_empty:

Returns `true` if the set contains no indices (and therefore all color components are present).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would also appreciate an/some example(s) of when components can be missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add some docs about missing components.

Inverting the boolean sense is equivalent at a technical level. I think the strongest case for this way ("missing") is that the default value of 0 makes sense.

Incidentally, I considered and rejected some other representations for this. In particular, color.js uses NaN. That works in pure Rust, but I have at least two problems with it: it complicates GPU interop, because NaN is almost always a bad thing), and it makes the test for "any missing components" more expensive, because you have to do a finiteness check on all the components, as opposed to comparison with 0 here. The current approach also has no memory cost, DynamicColor is 20 bytes either way.


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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

singleton seems like a confusing name for this. This usually implies what in Rust would be a static instance. I would prefer the old name of single. Or perhaps an idiomatic rusty name would be with_single_component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone back to single, at your suggestion.

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