Implement builtin function mix()

This commit is contained in:
ConnorSkees 2020-02-14 16:47:23 -05:00
parent c04f83ddcf
commit af95658953
4 changed files with 83 additions and 22 deletions

View File

@ -67,4 +67,4 @@ pub(crate) fn register(f: &mut BTreeMap<String, Builtin>) {
};
Some(Value::Color(color.fade_out(amount)))
});
}
}

View File

@ -119,4 +119,22 @@ pub(crate) fn register(f: &mut BTreeMap<String, Builtin>) {
_ => todo!("non-color given to builtin function `blue()`")
}
});
}
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)))
});
}

View File

@ -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
/// <https://github.com/sass/dart-sass/blob/0d0270cb12a9ac5cce73a4d0785fecb00735feee/lib/src/functions/color.dart#L718>
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) {

View File

@ -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"
);