From 5d268be2ed2d0f47487e900d1777d07542c0d435 Mon Sep 17 00:00:00 2001 From: Connor Skees Date: Sun, 4 Jul 2021 15:53:27 -0400 Subject: [PATCH] support hwb color space --- src/builtin/functions/color/hwb.rs | 136 +++++++++++++++++++++++++++++ src/builtin/functions/color/mod.rs | 1 + src/builtin/modules/color.rs | 4 + src/color/mod.rs | 60 +++++++++++++ tests/color_hwb.rs | 83 ++++++++++++++++++ 5 files changed, 284 insertions(+) create mode 100644 src/builtin/functions/color/hwb.rs create mode 100644 tests/color_hwb.rs diff --git a/src/builtin/functions/color/hwb.rs b/src/builtin/functions/color/hwb.rs new file mode 100644 index 0000000..0ebcc4b --- /dev/null +++ b/src/builtin/functions/color/hwb.rs @@ -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 { + 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 { + 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 { + 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, + )))) +} diff --git a/src/builtin/functions/color/mod.rs b/src/builtin/functions/color/mod.rs index 55c6425..815b9b5 100644 --- a/src/builtin/functions/color/mod.rs +++ b/src/builtin/functions/color/mod.rs @@ -1,6 +1,7 @@ use super::{Builtin, GlobalFunctionMap}; pub mod hsl; +pub mod hwb; pub mod opacity; pub mod other; pub mod rgb; diff --git a/src/builtin/modules/color.rs b/src/builtin/modules/color.rs index f2cd754..24c0616 100644 --- a/src/builtin/modules/color.rs +++ b/src/builtin/modules/color.rs @@ -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); } diff --git a/src/color/mod.rs b/src/color/mod.rs index e07034a..d37701b 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -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 { diff --git a/tests/color_hwb.rs b/tests/color_hwb.rs new file mode 100644 index 0000000..bd73b67 --- /dev/null +++ b/tests/color_hwb.rs @@ -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 \"%\"." +);