Adjust color mixing for hue-based spaces (#2931)

This commit is contained in:
Eric Biedert 2023-12-13 13:23:32 +01:00 committed by GitHub
parent 9cfe49e4ae
commit 077d6b5c54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 43 deletions

View File

@ -1047,6 +1047,10 @@ impl Color {
/// Create a color by mixing two or more colors.
///
/// In color spaces with a hue component (hsl, hsv, oklch), only two colors
/// can be mixed at once. Mixing more than two colors in such a space will
/// result in an error!
///
/// ```example
/// #set block(height: 20pt, width: 100%)
/// #block(fill: red.mix(blue))
@ -1076,27 +1080,70 @@ impl Color {
impl Color {
/// Same as [`Color::mix`], but takes an iterator instead of a vector.
pub fn mix_iter(
colors: impl IntoIterator<Item = WeightedColor>,
colors: impl IntoIterator<
Item = WeightedColor,
IntoIter = impl ExactSizeIterator<Item = WeightedColor>,
>,
space: ColorSpace,
) -> StrResult<Color> {
let mut total = 0.0;
let mut acc = [0.0; 4];
for WeightedColor { color, weight } in colors {
let weight = weight as f32;
let v = color.to_space(space).to_vec4();
acc[0] += weight * v[0];
acc[1] += weight * v[1];
acc[2] += weight * v[2];
acc[3] += weight * v[3];
total += weight;
let mut colors = colors.into_iter();
if space.hue_index().is_some() && colors.len() > 2 {
bail!("cannot mix more than two colors in a hue-based space");
}
if total <= 0.0 {
bail!("sum of weights must be positive");
}
let m = if space.hue_index().is_some() && colors.len() == 2 {
let mut m = [0.0; 4];
let WeightedColor { color: c0, weight: w0 } = colors.next().unwrap();
let WeightedColor { color: c1, weight: w1 } = colors.next().unwrap();
let c0 = c0.to_space(space).to_vec4();
let c1 = c1.to_space(space).to_vec4();
let w0 = w0 as f32;
let w1 = w1 as f32;
if w0 + w1 <= 0.0 {
bail!("sum of weights must be positive");
}
for i in 0..4 {
m[i] = (w0 * c0[i] + w1 * c1[i]) / (w0 + w1);
}
// Ensure that the hue circle is traversed in the short direction.
if let Some(index) = space.hue_index() {
if (c0[index] - c1[index]).abs() > 180.0 {
let (h0, h1) = if c0[index] < c1[index] {
(c0[index] + 360.0, c1[index])
} else {
(c0[index], c1[index] + 360.0)
};
m[index] = (w0 * h0 + w1 * h1) / (w0 + w1);
}
}
m
} else {
let mut total = 0.0;
let mut acc = [0.0; 4];
for WeightedColor { color, weight } in colors {
let weight = weight as f32;
let v = color.to_space(space).to_vec4();
acc[0] += weight * v[0];
acc[1] += weight * v[1];
acc[2] += weight * v[2];
acc[3] += weight * v[3];
total += weight;
}
if total <= 0.0 {
bail!("sum of weights must be positive");
}
acc.map(|v| v / total)
};
let m = acc.map(|v| v / total);
Ok(match space {
ColorSpace::Oklab => Color::Oklab(Oklab::new(m[0], m[1], m[2], m[3])),
ColorSpace::Oklch => Color::Oklch(Oklch::new(m[0], m[1], m[2], m[3])),
@ -1740,6 +1787,18 @@ pub enum ColorSpace {
Cmyk,
}
impl ColorSpace {
/// Returns the index of the hue component in this color space, if it has
/// one.
pub fn hue_index(&self) -> Option<usize> {
match self {
Self::Hsl | Self::Hsv => Some(0),
Self::Oklch => Some(2),
_ => None,
}
}
}
cast! {
ColorSpace,
self => match self {

View File

@ -12,7 +12,6 @@ use crate::foundations::{
};
use crate::layout::{Angle, Axes, Dir, Quadrant, Ratio};
use crate::syntax::{Span, Spanned};
use crate::visualize::color::{Hsl, Hsv};
use crate::visualize::{Color, ColorSpace, WeightedColor};
/// A color gradient.
@ -1234,37 +1233,14 @@ fn sample_stops(stops: &[(Color, Ratio)], mixing_space: ColorSpace, t: f64) -> C
if low == 0 {
low = 1;
}
let (col_0, pos_0) = stops[low - 1];
let (col_1, pos_1) = stops[low];
let t = (t - pos_0.get()) / (pos_1.get() - pos_0.get());
let out = Color::mix_iter(
Color::mix_iter(
[WeightedColor::new(col_0, 1.0 - t), WeightedColor::new(col_1, t)],
mixing_space,
)
.unwrap();
// Special case for handling multi-turn hue interpolation.
if mixing_space == ColorSpace::Hsl || mixing_space == ColorSpace::Hsv {
let hue_0 = col_0.to_space(mixing_space).to_vec4()[0];
let hue_1 = col_1.to_space(mixing_space).to_vec4()[0];
// Check if we need to interpolate over the 360° boundary.
if (hue_0 - hue_1).abs() > 180.0 {
let hue_0 = if hue_0 < hue_1 { hue_0 + 360.0 } else { hue_0 };
let hue_1 = if hue_1 < hue_0 { hue_1 + 360.0 } else { hue_1 };
let hue = hue_0 * (1.0 - t as f32) + hue_1 * t as f32;
if mixing_space == ColorSpace::Hsl {
let [_, saturation, lightness, alpha] = out.to_hsl().to_vec4();
return Color::Hsl(Hsl::new(hue, saturation, lightness, alpha));
} else if mixing_space == ColorSpace::Hsv {
let [_, saturation, value, alpha] = out.to_hsv().to_vec4();
return Color::Hsv(Hsv::new(hue, saturation, value, alpha));
}
}
}
out
.unwrap()
}

View File

@ -32,6 +32,12 @@
#test(color.mix((rgb("#aaff00"), 50%), (rgb("#aa00ff"), 50%), space: rgb), rgb("#aa8080"))
#test(color.mix((rgb("#aaff00"), 75%), (rgb("#aa00ff"), 25%), space: rgb), rgb("#aabf40"))
// Mix in hue-based space.
#test(rgb(color.mix(red, blue, space: color.hsl)), rgb("#c408ff"))
#test(rgb(color.mix((red, 50%), (blue, 100%), space: color.hsl)), rgb("#5100f8"))
// Error: 15-51 cannot mix more than two colors in a hue-based space
#rgb(color.mix(red, blue, white, space: color.hsl))
---
// Test color conversion method kinds
#test(rgb(rgb(10, 20, 30)).space(), rgb)