From 5cab99cd6ed79c9113168ec84cd9b01cf1f59a8a Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Sun, 9 Feb 2020 03:13:31 -0500 Subject: [PATCH] Implement `hsl()` and `hsla()` functions --- src/builtin/color.rs | 43 ++++++++++++++++++- src/color/mod.rs | 100 +++++++++++++++++++++++++++++++++++++++++++ src/value/number.rs | 30 ++++++++++++- tests/color.rs | 20 +++++++++ tests/strings.rs | 2 +- 5 files changed, 192 insertions(+), 3 deletions(-) diff --git a/src/builtin/color.rs b/src/builtin/color.rs index 06187b8..cccc478 100644 --- a/src/builtin/color.rs +++ b/src/builtin/color.rs @@ -34,9 +34,50 @@ pub(crate) fn register(f: &mut BTreeMap) { }; Some(Value::Color(Color::from_values(red, green, blue, alpha))) } else { - todo!("channels variable in `rgb`") + todo!("channels variable in `rgba`") } }); + decl!(f "hsl", |args, _| { + let hue = match arg!(args, 0, "hue").clone().eval() { + Value::Dimension(n, Unit::None) + | Value::Dimension(n, Unit::Percent) => n, + _ => todo!("expected either unitless or % number for alpha"), + }; + let saturation = match arg!(args, 1, "saturation").clone().eval() { + Value::Dimension(n, Unit::None) + | Value::Dimension(n, Unit::Percent) => n / Number::from(100), + _ => todo!("expected either unitless or % number for alpha"), + }; + let luminance = match arg!(args, 2, "luminance").clone().eval() { + Value::Dimension(n, Unit::None) + | Value::Dimension(n, Unit::Percent) => n / Number::from(100), + _ => todo!("expected either unitless or % number for alpha"), + }; + Some(Value::Color(Color::from_hsla(hue, saturation, luminance, Number::from(1)))) + }); + decl!(f "hsla", |args, _| { + let hue = match arg!(args, 0, "hue").clone().eval() { + Value::Dimension(n, Unit::None) + | Value::Dimension(n, Unit::Percent) => n, + _ => todo!("expected either unitless or % number for alpha"), + }; + let saturation = match arg!(args, 1, "saturation").clone().eval() { + Value::Dimension(n, Unit::None) + | Value::Dimension(n, Unit::Percent) => n / Number::from(100), + _ => todo!("expected either unitless or % number for alpha"), + }; + let luminance = match arg!(args, 2, "luminance").clone().eval() { + Value::Dimension(n, Unit::None) + | Value::Dimension(n, Unit::Percent) => n / Number::from(100), + _ => todo!("expected either unitless or % number for alpha"), + }; + let alpha = match arg!(args, 3, "alpha").clone().eval() { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + _ => todo!("expected either unitless or % number for alpha"), + }; + Some(Value::Color(Color::from_hsla(hue, saturation, luminance, alpha))) + }); decl!(f "red", |args, _| { match arg!(args, 0, "red") { Value::Color(c) => Some(Value::Dimension(Number::from(BigInt::from(c.red())), Unit::None)), diff --git a/src/color/mod.rs b/src/color/mod.rs index 6f2c65a..f0badc1 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -3,6 +3,8 @@ use std::fmt::{self, Display}; use crate::value::Number; pub(crate) use name::ColorName; +use num_traits::cast::ToPrimitive; + mod name; #[derive(Debug, Clone, Eq, PartialEq)] @@ -41,6 +43,104 @@ impl Color { self.alpha.clone() } + /// Create RGBA representation from HSLA values + /// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ + pub fn from_hsla( + mut hue: Number, + saturation: Number, + luminance: Number, + alpha: Number, + ) -> Self { + println!("{}-{}-{}", &hue, &saturation, &luminance); + if saturation.clone() == Number::from(0) { + let luminance = if luminance > Number::from(100) { + Number::from(100) + } else { + luminance + }; + let val = (luminance.clone() * Number::from(255)) + .to_integer() + .to_u16() + .unwrap(); + let repr = if alpha >= Number::from(1) { + format!("#{:0>2x}{:0>2x}{:0>2x}", val, val, val) + } else { + format!("rgba({}, {}, {}, {})", val, val, val, alpha) + }; + return Color { + red: val, + green: val, + blue: val, + alpha: Number::from(alpha), + repr, + }; + } + let temporary_1 = if luminance.clone() < Number::ratio(1, 2) { + luminance.clone() * (Number::from(1) + saturation) + } else { + luminance.clone() + saturation.clone() - luminance.clone() * saturation.clone() + }; + let temporary_2 = Number::from(2) * luminance.clone() - temporary_1.clone(); + hue = hue / Number::from(360); + let mut temporary_r = hue.clone() + Number::ratio(1, 3); + let mut temporary_g = hue.clone(); + let mut temporary_b = hue.clone() - Number::ratio(1, 3); + + macro_rules! clamp { + ($temp:ident) => { + if $temp > Number::from(1) { + $temp -= Number::from(1); + } else if $temp < Number::from(0) { + $temp += Number::from(1); + } + }; + } + + clamp!(temporary_r); + clamp!(temporary_g); + clamp!(temporary_b); + + macro_rules! channel { + ($name:ident, $temp:ident, $temp1:ident, $temp2:ident) => { + let $name = (if Number::from(6) * $temp.clone() < Number::from(1) { + $temp2.clone() + + ($temp1.clone() - $temp2.clone()) * Number::from(6) * $temp.clone() + } else if Number::from(2) * $temp.clone() < Number::from(1) { + $temp1.clone() + } else if Number::from(3) * $temp.clone() < Number::from(2) { + $temp2.clone() + + ($temp1.clone() - $temp2.clone()) + * (Number::ratio(2, 3) - $temp) + * Number::from(6) + } else { + $temp2.clone() + } * Number::from(255)) + .round() + .to_integer() + .to_u16() + .expect("expected channel to fit inside u16"); + }; + } + + channel!(red, temporary_r, temporary_1, temporary_2); + channel!(green, temporary_g, temporary_1, temporary_2); + channel!(blue, temporary_b, temporary_1, temporary_2); + + let repr = if alpha >= Number::from(1) { + format!("#{:0>2x}{:0>2x}{:0>2x}", red, green, blue) + } else { + dbg!("hi"); + format!("rgba({}, {}, {}, {})", red, green, blue, alpha) + }; + Color { + red, + green, + blue, + alpha, + repr, + } + } + pub fn from_values(red: u16, green: u16, blue: u16, alpha: Number) -> Self { let repr = if alpha >= Number::from(1) { format!("#{:0>2x}{:0>2x}{:0>2x}", red, green, blue) diff --git a/src/value/number.rs b/src/value/number.rs index 5d6d20c..0793a42 100644 --- a/src/value/number.rs +++ b/src/value/number.rs @@ -1,6 +1,6 @@ use std::convert::From; use std::fmt::{self, Display, Write}; -use std::ops::{Add, Div, Mul, Sub}; +use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; use num_bigint::BigInt; use num_rational::BigRational; @@ -20,6 +20,22 @@ impl Number { pub fn to_integer(&self) -> BigInt { self.val.to_integer() } + + pub fn ratio, B: Into>(a: A, b: B) -> Self { + Number::new(BigRational::new(a.into(), b.into())) + } + + pub fn round(self) -> Self { + Number { + val: self.val.round(), + } + } +} + +impl fmt::LowerHex for Number { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:0>2x}", self.val.to_integer()) + } } impl From for Number { @@ -84,6 +100,12 @@ impl Add for Number { } } +impl AddAssign for Number { + fn add_assign(&mut self, other: Self) { + self.val += other.val + } +} + impl Sub for Number { type Output = Self; @@ -94,6 +116,12 @@ impl Sub for Number { } } +impl SubAssign for Number { + fn sub_assign(&mut self, other: Self) { + self.val -= other.val + } +} + impl Mul for Number { type Output = Self; diff --git a/tests/color.rs b/tests/color.rs index 9d42f5a..d08176b 100644 --- a/tests/color.rs +++ b/tests/color.rs @@ -76,3 +76,23 @@ test!( "a {\n color: rgba(1, 2, 3, 50%);\n}\n", "a {\n color: rgba(1, 2, 3, 0.5);\n}\n" ); +test!( + hsl_basic, + "a {\n color: hsl(193, 67%, 99);\n}\n", + "a {\n color: #fbfdfe;\n}\n" +); +test!( + hsla_basic, + "a {\n color: hsla(193, 67%, 99, .6);\n}\n", + "a {\n color: rgba(251, 253, 254, 0.6);\n}\n" +); +test!( + hsl_named, + "a {\n color: hsl($hue: 193, $saturation: 67%, $luminance: 99);\n}\n", + "a {\n color: #fbfdfe;\n}\n" +); +test!( + hsla_named, + "a {\n color: hsla($hue: 193, $saturation: 67%, $luminance: 99, $alpha: .6);\n}\n", + "a {\n color: rgba(251, 253, 254, 0.6);\n}\n" +); diff --git a/tests/strings.rs b/tests/strings.rs index 79f7236..1306c90 100644 --- a/tests/strings.rs +++ b/tests/strings.rs @@ -32,4 +32,4 @@ test!( length_named_arg, "a {\n color: str-length($string: aBc123);\n}\n", "a {\n color: 6;\n}\n" -); \ No newline at end of file +);