support hwb color space
This commit is contained in:
parent
5c61f8ccaa
commit
5d268be2ed
136
src/builtin/functions/color/hwb.rs
Normal file
136
src/builtin/functions/color/hwb.rs
Normal 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,
|
||||
))))
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
|
||||
pub mod hsl;
|
||||
pub mod hwb;
|
||||
pub mod opacity;
|
||||
pub mod other;
|
||||
pub mod rgb;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
83
tests/color_hwb.rs
Normal 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 \"%\"."
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user