Adjust color mixing for hue-based spaces (#2931)
This commit is contained in:
parent
9cfe49e4ae
commit
077d6b5c54
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user