support hwb color space

This commit is contained in:
Connor Skees 2021-07-04 15:53:27 -04:00
parent 5c61f8ccaa
commit 5d268be2ed
5 changed files with 284 additions and 0 deletions

View File

@ -0,0 +1,136 @@
use num_traits::One;
use crate::{
args::CallArgs,
color::Color,
error::SassResult,
parse::Parser,
unit::Unit,
value::{Number, Value},
};
pub(crate) fn blackness(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?;
let color = match args.get_err(0, "color")? {
Value::Color(c) => c,
v => {
return Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let blackness =
Number::from(1) - (color.red().max(color.green()).max(color.blue()) / Number::from(255));
Ok(Value::Dimension(Some(blackness * 100), Unit::Percent, true))
}
pub(crate) fn whiteness(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?;
let color = match args.get_err(0, "color")? {
Value::Color(c) => c,
v => {
return Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let whiteness = color.red().min(color.green()).min(color.blue()) / Number::from(255);
Ok(Value::Dimension(Some(whiteness * 100), Unit::Percent, true))
}
pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(4)?;
if args.is_empty() {
return Err(("Missing argument $channels.", args.span()).into());
}
let hue = match args.get(0, "hue") {
Some(Ok(v)) => match v.node {
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!("$hue: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
},
Some(Err(e)) => return Err(e),
None => return Err(("Missing element $hue.", args.span()).into()),
};
let whiteness = match args.get(1, "whiteness") {
Some(Ok(v)) => match v.node {
Value::Dimension(Some(n), Unit::Percent, ..) => n,
v @ Value::Dimension(Some(..), ..) => {
return Err((
format!(
"$whiteness: Expected {} to have unit \"%\".",
v.inspect(args.span())?
),
args.span(),
)
.into())
}
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!("$whiteness: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
},
Some(Err(e)) => return Err(e),
None => return Err(("Missing element $whiteness.", args.span()).into()),
};
let blackness = match args.get(2, "blackness") {
Some(Ok(v)) => match v.node {
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!("$blackness: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
},
Some(Err(e)) => return Err(e),
None => return Err(("Missing element $blackness.", args.span()).into()),
};
let alpha = match args.get(3, "alpha") {
Some(Ok(v)) => match v.node {
Value::Dimension(Some(n), Unit::Percent, ..) => n / Number::from(100),
Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
},
Some(Err(e)) => return Err(e),
None => Number::one(),
};
Ok(Value::Color(Box::new(Color::from_hwb(
hue, whiteness, blackness, alpha,
))))
}

View File

@ -1,6 +1,7 @@
use super::{Builtin, GlobalFunctionMap};
pub mod hsl;
pub mod hwb;
pub mod opacity;
pub mod other;
pub mod rgb;

View File

@ -1,6 +1,7 @@
use crate::builtin::{
color::{
hsl::{complement, grayscale, hue, invert, lightness, saturation},
hwb::{blackness, hwb, whiteness},
opacity::alpha,
other::{adjust_color, change_color, ie_hex_str, scale_color},
rgb::{blue, green, mix, red},
@ -24,4 +25,7 @@ pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("red", red);
f.insert_builtin("saturation", saturation);
f.insert_builtin("scale", scale_color);
f.insert_builtin("blackness", blackness);
f.insert_builtin("whiteness", whiteness);
f.insert_builtin("hwb", hwb);
}

View File

@ -514,6 +514,66 @@ impl Color {
}
}
/// HWB color functions
impl Color {
pub fn from_hwb(
mut hue: Number,
mut white: Number,
mut black: Number,
mut alpha: Number,
) -> Color {
hue %= Number::from(360);
hue /= Number::from(360);
white /= Number::from(100);
black /= Number::from(100);
alpha = alpha.clamp(Number::zero(), Number::one());
let white_black_sum = white.clone() + black.clone();
if white_black_sum > Number::one() {
white /= white_black_sum.clone();
black /= white_black_sum;
}
let factor = Number::one() - white.clone() - black;
fn channel(m1: Number, m2: Number, mut hue: Number) -> Number {
if hue < Number::zero() {
hue += Number::one();
}
if hue > Number::one() {
hue -= Number::one();
}
if hue < Number::small_ratio(1, 6) {
return m1.clone() + (m2 - m1) * hue * Number::from(6);
} else if hue < Number::small_ratio(1, 2) {
return m2;
} else if hue < Number::small_ratio(2, 3) {
return m1.clone()
+ (m2 - m1) * (Number::small_ratio(2, 3) - hue) * Number::from(6);
} else {
return m1;
}
}
let to_rgb = |hue: Number| -> Number {
let channel =
channel(Number::zero(), Number::one(), hue) * factor.clone() + white.clone();
channel * Number::from(255)
};
let red = to_rgb(hue.clone() + Number::small_ratio(1, 3));
let green = to_rgb(hue.clone());
let blue = to_rgb(hue - Number::small_ratio(1, 3));
let repr = repr(&red, &green, &blue, &alpha);
Color::new_rgba(red, green, blue, alpha, repr)
}
}
/// Get the proper representation from RGBA values
fn repr(red: &Number, green: &Number, blue: &Number, alpha: &Number) -> String {
fn into_u8(channel: &Number) -> u8 {

83
tests/color_hwb.rs Normal file
View File

@ -0,0 +1,83 @@
#[macro_use]
mod macros;
test!(
blackness_black,
"@use \"sass:color\";\na {\n color: color.blackness(black);\n}\n",
"a {\n color: 100%;\n}\n"
);
test!(
blackness_white,
"@use \"sass:color\";\na {\n color: color.blackness(white);\n}\n",
"a {\n color: 0%;\n}\n"
);
test!(
blackness_approx_50_pct,
"@use \"sass:color\";\na {\n color: color.blackness(color.hwb(0, 0%, 50%));\n}\n",
"a {\n color: 49.8039215686%;\n}\n"
);
test!(
blackness_approx_50_pct_and_whiteness,
"@use \"sass:color\";\na {\n color: color.blackness(color.hwb(0, 50%, 50%));\n}\n",
"a {\n color: 49.8039215686%;\n}\n"
);
test!(
blackness_approx_70_pct_and_whiteness,
"@use \"sass:color\";\na {\n color: color.blackness(color.hwb(0, 70%, 70%));\n}\n",
"a {\n color: 49.8039215686%;\n}\n"
);
test!(
blackness_approx_half_pct,
"@use \"sass:color\";\na {\n color: color.blackness(color.hwb(0, 0%, 0.5%));\n}\n",
"a {\n color: 0.3921568627%;\n}\n"
);
test!(
hwb_half_blackness,
"@use \"sass:color\";\na {\n color: color.hwb(0, 0%, 50%);\n}\n",
"a {\n color: maroon;\n}\n"
);
test!(
hwb_equal_white_black_50,
"@use \"sass:color\";\na {\n color: color.hwb(0, 50%, 50%);\n}\n",
"a {\n color: gray;\n}\n"
);
test!(
hwb_equal_white_black_70,
"@use \"sass:color\";\na {\n color: color.hwb(0, 70%, 70%);\n}\n",
"a {\n color: gray;\n}\n"
);
test!(
hwb_half_percent_black,
"@use \"sass:color\";\na {\n color: color.hwb(0, 0%, 0.5%);\n}\n",
"a {\n color: #fe0000;\n}\n"
);
test!(
hwb_black_100,
"@use \"sass:color\";\na {\n color: color.hwb(0, 0%, 100%);\n}\n",
"a {\n color: black;\n}\n"
);
test!(
blackness_named,
"@use \"sass:color\";\na {\n color: color.blackness($color: color.hwb(0, 0%, 42%));\n}\n",
"a {\n color: 41.9607843137%;\n}\n"
);
test!(
hwb_alpha_unitless,
"@use \"sass:color\";\na {\n color: color.hwb(0, 0%, 100%, 0.04);\n}\n",
"a {\n color: rgba(0, 0, 0, 0.04);\n}\n"
);
test!(
hwb_alpha_unit_percent,
"@use \"sass:color\";\na {\n color: color.hwb(0, 0%, 100%, 0.04%);\n}\n",
"a {\n color: rgba(0, 0, 0, 0.0004);\n}\n"
);
test!(
hwb_negative_alpha,
"@use \"sass:color\";\na {\n color: color.hwb(0, 0%, 100%, -0.5);\n}\n",
"a {\n color: rgba(0, 0, 0, 0);\n}\n"
);
error!(
hwb_whiteness_missing_pct,
"@use \"sass:color\";\na {\n color: color.hwb(0, 0, 100);\n}\n",
"Error: $whiteness: Expected 0 to have unit \"%\"."
);