diff --git a/src/builtin/color.rs b/src/builtin/color.rs index 39d335f..a80b075 100644 --- a/src/builtin/color.rs +++ b/src/builtin/color.rs @@ -1,7 +1,5 @@ use std::collections::BTreeMap; -use num_bigint::BigInt; - use super::Builtin; use crate::color::Color; use crate::common::QuoteKind; @@ -104,23 +102,41 @@ pub(crate) fn register(f: &mut BTreeMap) { 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)), + match arg!(args, 0, "color") { + Value::Color(c) => Some(Value::Dimension(Number::from(c.red()), Unit::None)), _ => todo!("non-color given to builtin function `red()`") } }); decl!(f "green", |args, _| { - match arg!(args, 0, "green") { - Value::Color(c) => Some(Value::Dimension(Number::from(BigInt::from(c.green())), Unit::None)), + match arg!(args, 0, "color") { + Value::Color(c) => Some(Value::Dimension(Number::from(c.green()), Unit::None)), _ => todo!("non-color given to builtin function `green()`") } }); decl!(f "blue", |args, _| { - match arg!(args, 0, "blue") { - Value::Color(c) => Some(Value::Dimension(Number::from(BigInt::from(c.blue())), Unit::None)), + match arg!(args, 0, "color") { + Value::Color(c) => Some(Value::Dimension(Number::from(c.blue()), Unit::None)), _ => todo!("non-color given to builtin function `blue()`") } }); + decl!(f "hue", |args, _| { + match arg!(args, 0, "color") { + Value::Color(c) => Some(Value::Dimension(c.hue(), Unit::Deg)), + _ => todo!("non-color given to builtin function `hue()`") + } + }); + decl!(f "saturation", |args, _| { + match arg!(args, 0, "color") { + Value::Color(c) => Some(Value::Dimension(c.saturation(), Unit::Percent)), + _ => todo!("non-color given to builtin function `saturation()`") + } + }); + decl!(f "lightness", |args, _| { + match arg!(args, 0, "color") { + Value::Color(c) => Some(Value::Dimension(c.lightness(), Unit::Percent)), + _ => todo!("non-color given to builtin function `lightness()`") + } + }); decl!(f "opacity", |args, _| { match arg!(args, 0, "color") { Value::Color(c) => Some(Value::Dimension(c.alpha() / Number::from(255), Unit::None)), diff --git a/src/color/mod.rs b/src/color/mod.rs index c777e20..8d1e6a2 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -40,6 +40,68 @@ impl Color { self.green } + /// Calculate hue from RGBA values + /// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ + pub fn hue(&self) -> Number { + let red = Number::ratio(self.red, 255); + let green = Number::ratio(self.green, 255); + let blue = Number::ratio(self.blue, 255); + let min = red.clone().min(green.clone().min(blue.clone())); + let max = red.clone().max(green.clone().max(blue.clone())); + if &min == &max { + return Number::from(0); + } + + let mut hue = if &red == &max { + (green - blue) / (max - min) + } else if &green == &max { + Number::from(2) + (blue - red) / (max - min) + } else { + Number::from(4) + (red - green) / (max - min) + }; + + if hue < Number::from(0) { + hue += Number::from(360); + } + + (hue * Number::from(60)).round() + } + + /// Calculate saturation from RGBA values + /// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ + pub fn saturation(&self) -> Number { + let red = Number::ratio(self.red, 255); + let green = Number::ratio(self.green, 255); + let blue = Number::ratio(self.blue, 255); + let mut min = red.clone().min(green.clone().min(blue.clone())); + min = Number::ratio((min * Number::from(100)).to_integer(), 100); + let mut max = red.max(green.max(blue)); + max = Number::ratio((max * Number::from(100)).to_integer(), 100); + let luminance = (min.clone() + max.clone()) / Number::from(2); + if &min == &max { + return Number::from(0); + } + + let saturation = if luminance < Number::ratio(1, 2) { + (max.clone() - min.clone()) / (max + min) + } else { + (max.clone() - min.clone()) / (Number::from(2) - max - min) + } * Number::from(100); + + saturation.round() + } + + /// Calculate luminance from RGBA values + /// Algorithm adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ + pub fn lightness(&self) -> Number { + let red = Number::ratio(self.red, 255); + let green = Number::ratio(self.green, 255); + let blue = Number::ratio(self.blue, 255); + let min = red.clone().min(green.clone().min(blue.clone())); + let max = red.max(green.max(blue)); + (((min + max) / Number::from(2)) * Number::from(100)).round() + } + pub fn alpha(&self) -> Number { self.alpha.clone() } @@ -77,7 +139,7 @@ impl Color { luminance.clone() + saturation.clone() - luminance.clone() * saturation.clone() }; let temporary_2 = Number::from(2) * luminance.clone() - temporary_1.clone(); - hue = hue / Number::from(360); + 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); diff --git a/src/value/number.rs b/src/value/number.rs index 0793a42..99bee31 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, AddAssign, Div, Mul, Sub, SubAssign}; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; use num_bigint::BigInt; use num_rational::BigRational; @@ -132,6 +132,12 @@ impl Mul for Number { } } +impl MulAssign for Number { + fn mul_assign(&mut self, other: Self) { + self.val *= other.val + } +} + impl Div for Number { type Output = Self; @@ -141,3 +147,9 @@ impl Div for Number { } } } + +impl DivAssign for Number { + fn div_assign(&mut self, other: Self) { + self.val /= other.val + } +} diff --git a/src/value/ops.rs b/src/value/ops.rs index 6f1130a..a08acaa 100644 --- a/src/value/ops.rs +++ b/src/value/ops.rs @@ -31,7 +31,7 @@ impl Add for Value { Self::Null => Value::Ident(c.to_string(), QuoteKind::None), Self::Color(..) => todo!("figure out if it's possible to add colors"), _ => Value::Ident(format!("{}{}", c, other), QuoteKind::None), - } + }, // Self::BinaryOp(..) => todo!(), // Self::Paren(..) => todo!(), Self::Ident(s1, quotes1) => match other { @@ -97,7 +97,7 @@ impl Sub for Value { } Self::Null => Value::Ident(format!("{}-", c), QuoteKind::None), Self::Dimension(..) => todo!("investigate adding numbers and colors"), - _ => Value::Ident(format!("{}-{}", c, other), QuoteKind::None) + _ => Value::Ident(format!("{}-{}", c, other), QuoteKind::None), }, // Self::BinaryOp(..) => todo!(), // Self::Paren(..) => todo!(), @@ -116,7 +116,11 @@ impl Sub for Value { QuoteKind::None, ) } - Self::Important | Self::True | Self::False | Self::Dimension(..) | Self::Color(..) => { + Self::Important + | Self::True + | Self::False + | Self::Dimension(..) + | Self::Color(..) => { let quotes = match quotes1 { QuoteKind::Double | QuoteKind::Single => QuoteKind::Double, QuoteKind::None => QuoteKind::None, diff --git a/tests/color.rs b/tests/color.rs index a5ed5f6..db913d2 100644 --- a/tests/color.rs +++ b/tests/color.rs @@ -156,3 +156,18 @@ test!( "a {\n color: foo - red;\n}\n", "a {\n color: foo-red;\n}\n" ); +test!( + hue, + "$a: hsl(193, 67%, 28%);\n\na {\n color: hue($a);\n}\n", + "a {\n color: 193deg;\n}\n" +); +test!( + saturation, + "$a: hsl(193, 67%, 28%);\n\na {\n color: saturation($a);\n}\n", + "a {\n color: 67%;\n}\n" +); +test!( + lightness, + "$a: hsl(193, 67%, 28%);\n\na {\n color: lightness($a);\n}\n", + "a {\n color: 28%;\n}\n" +);