summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Biedert <github@ericbiedert.de>2023-12-13 13:23:32 +0100
committerGitHub <noreply@github.com>2023-12-13 13:23:32 +0100
commit077d6b5c5442dc840327567944df64043e42e0a8 (patch)
tree051eafa07233064da216c11be15a9c16e95c1613
parent9cfe49e4ae63cd80a23ae0f32ac15dcc859fb640 (diff)
Adjust color mixing for hue-based spaces (#2931)
-rw-r--r--crates/typst/src/visualize/color.rs91
-rw-r--r--crates/typst/src/visualize/gradient.rs30
-rw-r--r--tests/typ/compute/construct.typ6
3 files changed, 84 insertions, 43 deletions
diff --git a/crates/typst/src/visualize/color.rs b/crates/typst/src/visualize/color.rs
index fe66cc1a..cb998160 100644
--- a/crates/typst/src/visualize/color.rs
+++ b/crates/typst/src/visualize/color.rs
@@ -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 {
diff --git a/crates/typst/src/visualize/gradient.rs b/crates/typst/src/visualize/gradient.rs
index e4878527..3848b499 100644
--- a/crates/typst/src/visualize/gradient.rs
+++ b/crates/typst/src/visualize/gradient.rs
@@ -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()
}
diff --git a/tests/typ/compute/construct.typ b/tests/typ/compute/construct.typ
index f6856c6f..f37669b5 100644
--- a/tests/typ/compute/construct.typ
+++ b/tests/typ/compute/construct.typ
@@ -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)