Skip to content

Do not convert hsl/hwb to RGB at parse time to not loose precision #326

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

Merged
merged 1 commit into from
Mar 9, 2023
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
156 changes: 140 additions & 16 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,122 @@ impl ToCss for RGBA {
}
}

#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Hsl {
/// The hue component.
pub hue: f32,
/// The saturation component.
pub saturation: f32,
/// The lightness component.
pub lightness: f32,
/// The alpha component.
pub alpha: f32,
}

impl Hsl {
pub fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
Self {
hue,
saturation,
lightness,
alpha,
}
}
}

impl ToCss for Hsl {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
// HSL serializes to RGB, so we have to convert it.
let (red, green, blue) = hsl_to_rgb(
self.hue / 360.0, // Hue is expected in range [0..1].
self.saturation,
self.lightness,
);

RGBA::from_floats(red, green, blue, self.alpha).to_css(dest)
}
}

#[cfg(feature = "serde")]
impl Serialize for Hsl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
(self.hue, self.saturation, self.lightness, self.alpha).serialize(serializer)
}
}

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Hsl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?;
Ok(Self::new(lightness, a, b, alpha))
}
}

#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Hwb {
/// The hue component.
pub hue: f32,
/// The whiteness component.
pub whiteness: f32,
/// The blackness component.
pub blackness: f32,
/// The alpha component.
pub alpha: f32,
}

impl Hwb {
pub fn new(hue: f32, whiteness: f32, blackness: f32, alpha: f32) -> Self {
Self {
hue,
whiteness,
blackness,
alpha,
}
}
}

impl ToCss for Hwb {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
// HWB serializes to RGB, so we have to convert it.
let (red, green, blue) = hwb_to_rgb(self.hue / 360.0, self.whiteness, self.blackness);

RGBA::from_floats(red, green, blue, self.alpha).to_css(dest)
}
}

#[cfg(feature = "serde")]
impl Serialize for Hwb {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
(self.hue, self.whiteness, self.blackness, self.alpha).serialize(serializer)
}
}

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Hwb {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let (lightness, whiteness, blackness, alpha) = Deserialize::deserialize(deserializer)?;
Ok(Self::new(lightness, whiteness, blackness, alpha))
}
}

// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro,
// because it causes cbindgen to ignore them.

Expand Down Expand Up @@ -482,6 +598,10 @@ pub enum Color {
CurrentColor,
/// Specify sRGB colors directly by their red/green/blue/alpha chanels.
Rgba(RGBA),
/// Specifies a color in sRGB using hue, saturation and lightness components.
Hsl(Hsl),
/// Specifies a color in sRGB using hue, whiteness and blackness components.
Hwb(Hwb),
/// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue
/// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB
/// rectangular coordinate model.
Expand All @@ -508,6 +628,8 @@ impl ToCss for Color {
match *self {
Color::CurrentColor => dest.write_str("currentcolor"),
Color::Rgba(rgba) => rgba.to_css(dest),
Color::Hsl(hsl) => hsl.to_css(dest),
Color::Hwb(hwb) => hwb.to_css(dest),
Color::Lab(lab) => lab.to_css(dest),
Color::Lch(lch) => lch.to_css(dest),
Color::Oklab(lab) => lab.to_css(dest),
Expand Down Expand Up @@ -666,6 +788,12 @@ pub trait FromParsedColor {
/// Construct a new color from red, green, blue and alpha components.
fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self;

/// Construct a new color from hue, saturation, lightness and alpha components.
fn from_hsl(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self;

/// Construct a new color from hue, blackness, whiteness and alpha components.
fn from_hwb(hue: f32, whiteness: f32, blackness: f32, alpha: f32) -> Self;

/// Construct a new color from the `lab` notation.
fn from_lab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self;

Expand Down Expand Up @@ -725,6 +853,16 @@ impl FromParsedColor for Color {
Color::Rgba(RGBA::new(red, green, blue, alpha))
}

#[inline]
fn from_hsl(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
Color::Hsl(Hsl::new(hue, saturation, lightness, alpha))
}

#[inline]
fn from_hwb(hue: f32, whiteness: f32, blackness: f32, alpha: f32) -> Self {
Color::Hwb(Hwb::new(hue, whiteness, blackness, alpha))
}

#[inline]
fn from_lab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self {
Color::Lab(Lab::new(lightness, a, b, alpha))
Expand Down Expand Up @@ -1102,14 +1240,7 @@ where
let saturation = saturation.clamp(0.0, 1.0);
let lightness = lightness.clamp(0.0, 1.0);

let (red, green, blue) = hsl_to_rgb(hue / 360.0, saturation, lightness);

Ok(P::Output::from_rgba(
clamp_unit_f32(red),
clamp_unit_f32(green),
clamp_unit_f32(blue),
alpha,
))
Ok(P::Output::from_hsl(hue, saturation, lightness, alpha))
}

/// Parses hwb syntax.
Expand All @@ -1135,14 +1266,7 @@ where
let whiteness = whiteness.clamp(0.0, 1.0);
let blackness = blackness.clamp(0.0, 1.0);

let (red, green, blue) = hwb_to_rgb(hue / 360.0, whiteness, blackness);

Ok(P::Output::from_rgba(
clamp_unit_f32(red),
clamp_unit_f32(green),
clamp_unit_f32(blue),
alpha,
))
Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha))
}

/// https://drafts.csswg.org/css-color-4/#hwb-to-rgb
Expand Down
20 changes: 20 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,8 @@ impl ToJson for Color {
Color::Rgba(ref rgba) => {
json!([rgba.red, rgba.green, rgba.blue, rgba.alpha])
}
Color::Hsl(ref c) => json!([c.hue, c.saturation, c.lightness, c.alpha]),
Color::Hwb(ref c) => json!([c.hue, c.whiteness, c.blackness, c.alpha]),
Color::Lab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]),
Color::Lch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]),
Color::Oklab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]),
Expand Down Expand Up @@ -1517,6 +1519,8 @@ fn generic_parser() {
enum OutputType {
CurrentColor,
Rgba(u8, u8, u8, f32),
Hsl(f32, f32, f32, f32),
Hwb(f32, f32, f32, f32),
Lab(f32, f32, f32, f32),
Lch(f32, f32, f32, f32),
Oklab(f32, f32, f32, f32),
Expand All @@ -1533,6 +1537,14 @@ fn generic_parser() {
OutputType::Rgba(red, green, blue, alpha)
}

fn from_hsl(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
OutputType::Hsl(hue, saturation, lightness, alpha)
}

fn from_hwb(hue: f32, blackness: f32, whiteness: f32, alpha: f32) -> Self {
OutputType::Hwb(hue, blackness, whiteness, alpha)
}

fn from_lab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self {
OutputType::Lab(lightness, a, b, alpha)
}
Expand Down Expand Up @@ -1570,6 +1582,14 @@ fn generic_parser() {
("currentColor", OutputType::CurrentColor),
("rgb(1, 2, 3)", OutputType::Rgba(1, 2, 3, 1.0)),
("rgba(1, 2, 3, 0.4)", OutputType::Rgba(1, 2, 3, 0.4)),
(
"hsla(45deg, 20%, 30%, 0.4)",
OutputType::Hsl(45.0, 0.2, 0.3, 0.4),
),
(
"hwb(45deg 20% 30% / 0.4)",
OutputType::Hwb(45.0, 0.2, 0.3, 0.4),
),
(
"lab(100 20 30 / 0.4)",
OutputType::Lab(100.0, 20.0, 30.0, 0.4),
Expand Down