Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This release has an [MSRV][] of 1.82.
* `AlphaColor`, `OpaqueColor`, and `PremulColor` now impl `PartialEq`. ([#76][], [#86][] by [@waywardmonkeys][])
* `HueDirection` now impls `PartialEq`. ([#79][] by [@waywardmonkeys][])
* `ColorSpaceTag` and `HueDirection` now have bytemuck support. ([#81][] by [@waywardmonkeys][])
* A `DynamicColor` parsed from a named color or named color space function now serializes back to that name, as per the CSS Color Level 4 spec ([#39][] by [@tomcur][]).

### Changed

Expand All @@ -45,8 +46,10 @@ This is the initial release.

[@MightyBurger]: https://github.com/MightyBurger
[@raphlinus]: https://github.com/raphlinus
[@tomcur]: https://github.com/tomcur
[@waywardmonkeys]: https://github.com/waywardmonkeys

[#39]: https://github.com/linebender/color/pull/39
[#54]: https://github.com/linebender/color/pull/54
[#61]: https://github.com/linebender/color/pull/61
[#64]: https://github.com/linebender/color/pull/64
Expand Down
16 changes: 11 additions & 5 deletions color/make_x11_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,16 @@ def minimal_perfect_hash(d):
print("];")
print()

print(f"const NAMES: [&str; {n}] = [")
print(f"pub(crate) const NAMES: [&str; {n}] = [")
for (name, rgba) in keys:
print(f' "{name}",')
print("];")
print()
print(f"const COLORS: [[u8; 4]; {n}] = [")
print(f"""
/// RGBA8 color components of the named X11 colors, in the same order as [`NAMES`].
///
/// Use [`lookup_palette_index`] to efficiently find the color components for a given color name
/// string.
pub(crate) const COLORS: [[u8; 4]; {n}] = [""")
for (name, rgba) in keys:
print(f' {list(rgba)},')
print("];")
Expand All @@ -260,15 +264,17 @@ def minimal_perfect_hash(d):
(((y as u64) * (n as u64)) >> 32) as usize
}

pub(crate) fn lookup_palette(s: &str) -> Option<[u8; 4]> {
/// Given a named color (e.g., "red", "mediumorchid"), returns the index of that color into
/// [`COLORS`] and [`NAMES`].
pub(crate) fn lookup_palette_index(s: &str) -> Option<usize> {
let mut key = 0_u32;
for b in s.as_bytes() {
key = key.wrapping_mul(9).wrapping_add(*b as u32);
}
let salt = SALTS[weak_hash(key, 0, SALTS.len())] as u32;
let ix = weak_hash(key, salt, SALTS.len());
if s == NAMES[ix] {
Some(COLORS[ix])
Some(ix)
} else {
None
}
Expand Down
86 changes: 51 additions & 35 deletions color/src/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

use crate::{
color::{add_alpha, fixup_hues_for_interpolate, split_alpha},
AlphaColor, ColorSpace, ColorSpaceLayout, ColorSpaceTag, HueDirection, LinearSrgb, Missing,
AlphaColor, ColorSpace, ColorSpaceLayout, ColorSpaceTag, Flags, HueDirection, LinearSrgb,
Missing,
};
use core::hash::{Hash, Hasher};

Expand All @@ -31,8 +32,9 @@ use core::hash::{Hash, Hasher};
pub struct DynamicColor {
/// The color space.
pub cs: ColorSpaceTag,
/// A bitmask of missing components.
pub missing: Missing,
/// The state of this color, tracking whether it has missing components and how it was
/// constructed. See the documentation of [`Flags`] for more information.
pub flags: Flags,
/// The components.
///
/// The first three components are interpreted according to the
Expand Down Expand Up @@ -77,7 +79,7 @@ impl DynamicColor {
if let Some(cs) = CS::TAG {
Self {
cs,
missing: Missing::default(),
flags: Flags::default(),
components: color.components,
}
} else {
Expand All @@ -97,23 +99,23 @@ impl DynamicColor {
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.is_empty() {
let missing = if !self.flags.missing().is_empty() {
if self.cs.same_analogous(cs) {
for (i, component) in components.iter_mut().enumerate() {
if self.missing.contains(i) {
if self.flags.missing().contains(i) {
*component = 0.0;
}
}
self.missing
self.flags.missing()
} else {
let mut missing = self.missing & Missing::single(3);
if self.cs.h_missing(self.missing) {
let mut missing = self.flags.missing() & Missing::single(3);
if self.cs.h_missing(self.flags.missing()) {
cs.set_h_missing(&mut missing, &mut components);
}
if self.cs.c_missing(self.missing) {
if self.cs.c_missing(self.flags.missing()) {
cs.set_c_missing(&mut missing, &mut components);
}
if self.cs.l_missing(self.missing) {
if self.cs.l_missing(self.flags.missing()) {
cs.set_l_missing(&mut missing, &mut components);
}
missing
Expand All @@ -123,7 +125,7 @@ impl DynamicColor {
};
let mut result = Self {
cs,
missing,
flags: Flags::from_missing(missing),
components,
};
result.powerless_to_missing();
Expand All @@ -137,9 +139,9 @@ impl DynamicColor {
/// a corresponding component which is 0. This method restores that
/// invariant after manipulation which might invalidate it.
fn zero_missing_components(mut self) -> Self {
if !self.missing.is_empty() {
if !self.flags.missing().is_empty() {
for (i, component) in self.components.iter_mut().enumerate() {
if self.missing.contains(i) {
if self.flags.missing().contains(i) {
*component = 0.0;
}
}
Expand All @@ -153,13 +155,13 @@ impl DynamicColor {
/// will be ignored and the color returned unchanged.
#[must_use]
pub const fn multiply_alpha(self, rhs: f32) -> Self {
if self.missing.contains(3) {
if self.flags.missing().contains(3) {
self
} else {
let (opaque, alpha) = split_alpha(self.components);
Self {
cs: self.cs,
missing: self.missing,
flags: Flags::from_missing(self.flags.missing()),
components: add_alpha(opaque, alpha * rhs),
}
}
Expand All @@ -181,13 +183,13 @@ impl DynamicColor {
/// ```
#[must_use]
pub const fn with_alpha(self, alpha: f32) -> Self {
if self.missing.contains(3) {
if self.flags.missing().contains(3) {
self
} else {
let (opaque, _alpha) = split_alpha(self.components);
Self {
cs: self.cs,
missing: self.missing,
flags: Flags::from_missing(self.flags.missing()),
components: add_alpha(opaque, alpha),
}
}
Expand All @@ -200,9 +202,12 @@ impl DynamicColor {
pub fn scale_chroma(self, scale: f32) -> Self {
let (opaque, alpha) = split_alpha(self.components);
let components = self.cs.scale_chroma(opaque, scale);

let mut flags = self.flags;
flags.discard_name();
Self {
cs: self.cs,
missing: self.missing,
flags,
components: add_alpha(components, alpha),
}
.zero_missing_components()
Expand All @@ -219,15 +224,15 @@ impl DynamicColor {
let alpha = alpha.clamp(0., 1.);
Self {
cs: self.cs,
missing: self.missing,
flags: self.flags,
components: add_alpha(components, alpha),
}
}

fn premultiply_split(self) -> ([f32; 3], f32) {
// Reference: §12.3 of Color 4 spec
let (opaque, alpha) = split_alpha(self.components);
let premul = if alpha == 1.0 || self.missing.contains(3) {
let premul = if alpha == 1.0 || self.flags.missing().contains(3) {
opaque
} else {
self.cs.layout().scale(opaque, alpha)
Expand All @@ -243,8 +248,9 @@ impl DynamicColor {
if self.cs.layout() != ColorSpaceLayout::Rectangular
&& self.components[1] < POWERLESS_EPSILON
{
self.cs
.set_h_missing(&mut self.missing, &mut self.components);
let mut missing = self.flags.missing();
self.cs.set_h_missing(&mut missing, &mut self.components);
self.flags.set_missing(missing);
}
}

Expand All @@ -262,12 +268,14 @@ impl DynamicColor {
) -> Interpolator {
let mut a = self.convert(cs);
let mut b = other.convert(cs);
let missing = a.missing & b.missing;
if self.missing != other.missing {
let a_missing = a.flags.missing();
let b_missing = b.flags.missing();
let missing = a_missing & b_missing;
if a_missing != b_missing {
for i in 0..4 {
if (a.missing & !b.missing).contains(i) {
if (a_missing & !b_missing).contains(i) {
a.components[i] = b.components[i];
} else if (!a.missing & b.missing).contains(i) {
} else if (!a_missing & b_missing).contains(i) {
b.components[i] = a.components[i];
}
}
Expand Down Expand Up @@ -310,9 +318,12 @@ impl DynamicColor {
#[must_use]
pub fn map(self, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self {
let [x, y, z, a] = self.components;

let mut flags = self.flags;
flags.discard_name();
Self {
cs: self.cs,
missing: self.missing,
flags,
components: f(x, y, z, a),
}
.zero_missing_components()
Expand Down Expand Up @@ -371,7 +382,7 @@ impl Hash for DynamicColor {
/// match behavior for Rust float types.
fn hash<H: Hasher>(&self, state: &mut H) {
self.cs.hash(state);
self.missing.hash(state);
self.flags.hash(state);
for c in self.components {
c.to_bits().hash(state);
}
Expand All @@ -382,7 +393,7 @@ impl PartialEq for DynamicColor {
/// Equality is determined based on the bit representation.
fn eq(&self, other: &Self) -> bool {
self.cs == other.cs
&& self.missing == other.missing
&& self.flags == other.flags
&& self.components[0].to_bits() == other.components[0].to_bits()
&& self.components[1].to_bits() == other.components[1].to_bits()
&& self.components[2].to_bits() == other.components[2].to_bits()
Expand Down Expand Up @@ -410,29 +421,34 @@ impl Interpolator {
let components = add_alpha(opaque, alpha);
DynamicColor {
cs: self.cs,
missing: self.missing,
flags: Flags::from_missing(self.missing),
components,
}
}
}

#[cfg(test)]
mod tests {
use crate::{parse_color, Missing};
use crate::{parse_color, DynamicColor, Missing};

// `DynamicColor` was carefully packed. Ensure its size doesn't accidentally change.
const _: () = if size_of::<DynamicColor>() != 20 {
panic!("`DynamicColor` size changed");
};

#[test]
fn missing_alpha() {
let c = parse_color("oklab(0.5 0.2 0 / none)").unwrap();
assert_eq!(0., c.components[3]);
assert_eq!(Missing::single(3), c.missing);
assert_eq!(Missing::single(3), c.flags.missing());

// Alpha is missing, so we shouldn't be able to get an alpha added.
let c2 = c.with_alpha(0.5);
assert_eq!(0., c2.components[3]);
assert_eq!(Missing::single(3), c2.missing);
assert_eq!(Missing::single(3), c2.flags.missing());

let c3 = c.multiply_alpha(0.2);
assert_eq!(0., c3.components[3]);
assert_eq!(Missing::single(3), c3.missing);
assert_eq!(Missing::single(3), c3.flags.missing());
}
}
Loading
Loading