diff --git a/src/builtin/color/opacity.rs b/src/builtin/color/opacity.rs index 419bacc..ba466af 100644 --- a/src/builtin/color/opacity.rs +++ b/src/builtin/color/opacity.rs @@ -67,4 +67,4 @@ pub(crate) fn register(f: &mut BTreeMap) { }; Some(Value::Color(color.fade_out(amount))) }); -} \ No newline at end of file +} diff --git a/src/builtin/color/rgb.rs b/src/builtin/color/rgb.rs index 5ee68d1..23b11bd 100644 --- a/src/builtin/color/rgb.rs +++ b/src/builtin/color/rgb.rs @@ -119,4 +119,22 @@ pub(crate) fn register(f: &mut BTreeMap) { _ => todo!("non-color given to builtin function `blue()`") } }); -} \ No newline at end of file + decl!(f "mix", |args, _| { + let color1 = match arg!(args, 0, "color1").eval() { + Value::Color(c) => c, + _ => todo!("non-color given to builtin function `mix()`") + }; + + let color2 = match arg!(args, 1, "color2").eval() { + Value::Color(c) => c, + _ => todo!("non-color given to builtin function `mix()`") + }; + + let weight = match arg!(args, 2, "weight"=Value::Dimension(Number::ratio(1, 2), Unit::None)) { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + _ => todo!("expected either unitless or % number for $weight") + }; + Some(Value::Color(color1.mix(color2, weight))) + }); +} diff --git a/src/color/mod.rs b/src/color/mod.rs index 10dabcc..e8f368a 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -8,6 +8,18 @@ use num_traits::cast::ToPrimitive; mod name; +macro_rules! clamp { + ($c:expr, $min:literal, $max:literal) => { + if $c > Number::from($max) { + Number::from($max) + } else if $c < Number::from($min) { + Number::from($min) + } else { + $c + } + }; +} + #[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct Color { red: Number, @@ -77,6 +89,32 @@ impl Color { pub fn green(&self) -> Number { self.green.clone() } + + /// Mix two colors together with weight + /// Algorithm adapted from + /// + pub fn mix(self, other: Color, weight: Number) -> Self { + let weight = clamp!(weight, 0, 100); + let normalized_weight = weight.clone() * Number::from(2) - Number::from(1); + let alpha_distance = self.alpha.clone() - other.alpha.clone(); + + let combined_weight1 = + if normalized_weight.clone() * alpha_distance.clone() == Number::from(-1) { + normalized_weight + } else { + (normalized_weight.clone() + alpha_distance.clone()) + / (Number::from(1) + normalized_weight * alpha_distance) + }; + let weight1 = (combined_weight1 + Number::from(1)) / Number::from(2); + let weight2 = Number::from(1) - weight1.clone(); + + Color::from_rgba( + self.red * weight1.clone() + other.red * weight2.clone(), + self.green * weight1.clone() + other.green * weight2.clone(), + self.blue * weight1.clone() + other.blue * weight2, + self.alpha * weight.clone() + other.alpha * (Number::from(1) - weight), + ) + } } /// HSLA color functions @@ -207,26 +245,11 @@ impl Color { } /// Create RGBA representation from HSLA values - pub fn from_hsla( - mut hue: Number, - mut saturation: Number, - mut luminance: Number, - mut alpha: Number, - ) -> Self { - macro_rules! clamp { - ($c:ident, $min:literal, $max:literal) => { - if $c > Number::from($max) { - $c = Number::from($max) - } else if $c < Number::from($min) { - $c = Number::from($min) - } - }; - } - - clamp!(hue, 0, 360); - clamp!(saturation, 0, 1); - clamp!(luminance, 0, 1); - clamp!(alpha, 0, 1); + pub fn from_hsla(hue: Number, saturation: Number, luminance: Number, alpha: Number) -> Self { + let mut hue = clamp!(hue, 0, 360); + let saturation = clamp!(saturation, 0, 1); + let luminance = clamp!(luminance, 0, 1); + let alpha = clamp!(alpha, 0, 1); if saturation.clone() == Number::from(0) { let luminance = if luminance > Number::from(100) { diff --git a/tests/color.rs b/tests/color.rs index b47c5b1..5c8aead 100644 --- a/tests/color.rs +++ b/tests/color.rs @@ -356,3 +356,23 @@ test!( "a {\n color: complement(red);\n}\n", "a {\n color: aqua;\n}\n" ); +test!( + mix_no_weight, + "a {\n color: mix(#f00, #00f);\n}\n", + "a {\n color: purple;\n}\n" +); +test!( + mix_weight_25, + "a {\n color: mix(#f00, #00f, 25%);\n}\n", + "a {\n color: #4000bf;\n}\n" +); +test!( + mix_opacity, + "a {\n color: mix(rgba(255, 0, 0, 0.5), #00f);\n}\n", + "a {\n color: rgba(64, 0, 191, 0.75);\n}\n" +); +test!( + mix_sanity_check, + "a {\n color: mix(black, white);\n}\n", + "a {\n color: gray;\n}\n" +);