diff --git a/crates/typst/src/visualize/color.rs b/crates/typst/src/visualize/color.rs index e22f668c8..29bd3fca1 100644 --- a/crates/typst/src/visualize/color.rs +++ b/crates/typst/src/visualize/color.rs @@ -4,10 +4,9 @@ use std::str::FromStr; use ecow::{eco_format, EcoString, EcoVec}; use once_cell::sync::Lazy; -use palette::convert::FromColorUnclamped; use palette::encoding::{self, Linear}; use palette::{ - Darken, Desaturate, FromColor, Lighten, Okhsva, OklabHue, RgbHue, Saturate, ShiftHue, + Darken, Desaturate, FromColor, Lighten, OklabHue, RgbHue, Saturate, ShiftHue, }; use qcms::Profile; @@ -1010,16 +1009,29 @@ impl Color { }) } - /// Produces the negative of the color. + /// Produces the complementary color using a provided color space. + /// You can think of it as the opposite side on a color wheel. + /// + /// ```example + /// #square(fill: yellow) + /// #square(fill: yellow.negate()) + /// #square(fill: yellow.negate(space: rgb)) + /// ``` #[func] - pub fn negate(self) -> Color { - match self { + pub fn negate( + self, + /// The color space used for the transformation. By default, a perceptual color space is used. + #[named] + #[default(ColorSpace::Oklab)] + space: ColorSpace, + ) -> Color { + let result = match self.to_space(space) { Self::Luma(c) => Self::Luma(Luma::new(1.0 - c.luma, c.alpha)), - Self::Oklab(c) => Self::Oklab(Oklab::new(c.l, -c.a, -c.b, c.alpha)), + Self::Oklab(c) => Self::Oklab(Oklab::new(1.0 - c.l, -c.a, -c.b, c.alpha)), Self::Oklch(c) => Self::Oklch(Oklch::new( - c.l, - -c.chroma, - OklabHue::from_degrees(360.0 - c.hue.into_degrees()), + 1.0 - c.l, + c.chroma, + OklabHue::from_degrees(c.hue.into_degrees() + 180.0), c.alpha, )), Self::LinearRgb(c) => Self::LinearRgb(LinearRgb::new( @@ -1033,18 +1045,19 @@ impl Color { } Self::Cmyk(c) => Self::Cmyk(Cmyk::new(1.0 - c.c, 1.0 - c.m, 1.0 - c.y, c.k)), Self::Hsl(c) => Self::Hsl(Hsl::new( - RgbHue::from_degrees(360.0 - c.hue.into_degrees()), + RgbHue::from_degrees(c.hue.into_degrees() + 180.0), c.saturation, c.lightness, c.alpha, )), Self::Hsv(c) => Self::Hsv(Hsv::new( - RgbHue::from_degrees(360.0 - c.hue.into_degrees()), + RgbHue::from_degrees(c.hue.into_degrees() + 180.0), c.saturation, c.value, c.alpha, )), - } + }; + result.to_space(self.space()) } /// Rotates the hue of the color by a given angle. @@ -1304,10 +1317,8 @@ impl Color { pub fn to_luma(self) -> Self { Self::Luma(match self { Self::Luma(c) => c, - // Perform sRGB gamut mapping by converting to Okhsv first. - // This yields better results than clamping. - Self::Oklab(c) => Luma::from_color(Okhsva::from_color(c)), - Self::Oklch(c) => Luma::from_color(Okhsva::from_color(c)), + Self::Oklab(c) => Luma::from_color(c), + Self::Oklch(c) => Luma::from_color(c), Self::Rgb(c) => Luma::from_color(c), Self::LinearRgb(c) => Luma::from_color(c), Self::Cmyk(c) => Luma::from_color(c.to_rgba()), @@ -1320,9 +1331,7 @@ impl Color { Self::Oklab(match self { Self::Luma(c) => Oklab::from_color(c), Self::Oklab(c) => c, - // No clamping is necessary for this conversion because the - // lightness property is the same for both Oklab and Oklch. - Self::Oklch(c) => Oklab::from_color_unclamped(c), + Self::Oklch(c) => Oklab::from_color(c), Self::Rgb(c) => Oklab::from_color(c), Self::LinearRgb(c) => Oklab::from_color(c), Self::Cmyk(c) => Oklab::from_color(c.to_rgba()), @@ -1334,9 +1343,7 @@ impl Color { pub fn to_oklch(self) -> Self { Self::Oklch(match self { Self::Luma(c) => Oklch::from_color(c), - // No clamping is necessary for this conversion because the - // lightness property is the same for both Oklab and Oklch. - Self::Oklab(c) => Oklch::from_color_unclamped(c), + Self::Oklab(c) => Oklch::from_color(c), Self::Oklch(c) => c, Self::Rgb(c) => Oklch::from_color(c), Self::LinearRgb(c) => Oklch::from_color(c), @@ -1349,10 +1356,8 @@ impl Color { pub fn to_rgb(self) -> Self { Self::Rgb(match self { Self::Luma(c) => Rgb::from_color(c), - // Perform sRGB gamut mapping by converting to Okhsv first. - // This yields better results than clamping. - Self::Oklab(c) => Rgb::from_color(Okhsva::from_color(c)), - Self::Oklch(c) => Rgb::from_color(Okhsva::from_color(c)), + Self::Oklab(c) => Rgb::from_color(c), + Self::Oklch(c) => Rgb::from_color(c), Self::Rgb(c) => c, Self::LinearRgb(c) => Rgb::from_linear(c), Self::Cmyk(c) => Rgb::from_color(c.to_rgba()), @@ -1364,10 +1369,8 @@ impl Color { pub fn to_linear_rgb(self) -> Self { Self::LinearRgb(match self { Self::Luma(c) => LinearRgb::from_color(c), - // Perform sRGB gamut mapping by converting to Okhsv first. - // This yields better results than clamping. - Self::Oklab(c) => LinearRgb::from_color(Okhsva::from_color(c)), - Self::Oklch(c) => LinearRgb::from_color(Okhsva::from_color(c)), + Self::Oklab(c) => LinearRgb::from_color(c), + Self::Oklch(c) => LinearRgb::from_color(c), Self::Rgb(c) => LinearRgb::from_color(c), Self::LinearRgb(c) => c, Self::Cmyk(c) => LinearRgb::from_color(c.to_rgba()), @@ -1379,10 +1382,8 @@ impl Color { pub fn to_cmyk(self) -> Self { Self::Cmyk(match self { Self::Luma(c) => Cmyk::from_luma(c), - // Perform sRGB gamut mapping by converting to Okhsv first. - // This yields better results than clamping. - Self::Oklab(c) => Cmyk::from_rgba(Rgb::from_color(Okhsva::from_color(c))), - Self::Oklch(c) => Cmyk::from_rgba(Rgb::from_color(Okhsva::from_color(c))), + Self::Oklab(c) => Cmyk::from_rgba(Rgb::from_color(c)), + Self::Oklch(c) => Cmyk::from_rgba(Rgb::from_color(c)), Self::Rgb(c) => Cmyk::from_rgba(c), Self::LinearRgb(c) => Cmyk::from_rgba(Rgb::from_linear(c)), Self::Cmyk(c) => c, @@ -1394,10 +1395,8 @@ impl Color { pub fn to_hsl(self) -> Self { Self::Hsl(match self { Self::Luma(c) => Hsl::from_color(c), - // Perform sRGB gamut mapping by converting to Okhsv first. - // This yields better results than clamping. - Self::Oklab(c) => Hsl::from_color(Okhsva::from_color(c)), - Self::Oklch(c) => Hsl::from_color(Okhsva::from_color(c)), + Self::Oklab(c) => Hsl::from_color(c), + Self::Oklch(c) => Hsl::from_color(c), Self::Rgb(c) => Hsl::from_color(c), Self::LinearRgb(c) => Hsl::from_color(Rgb::from_linear(c)), Self::Cmyk(c) => Hsl::from_color(c.to_rgba()), @@ -1409,10 +1408,8 @@ impl Color { pub fn to_hsv(self) -> Self { Self::Hsv(match self { Self::Luma(c) => Hsv::from_color(c), - // Perform sRGB gamut mapping by converting to Okhsv first. - // This yields better results than clamping. - Self::Oklab(c) => Hsv::from_color(Okhsva::from_color(c)), - Self::Oklch(c) => Hsv::from_color(Okhsva::from_color(c)), + Self::Oklab(c) => Hsv::from_color(c), + Self::Oklch(c) => Hsv::from_color(c), Self::Rgb(c) => Hsv::from_color(c), Self::LinearRgb(c) => Hsv::from_color(Rgb::from_linear(c)), Self::Cmyk(c) => Hsv::from_color(c.to_rgba()), diff --git a/tests/ref/compiler/color.png b/tests/ref/compiler/color.png index 69dbe5e84..2b416f641 100644 Binary files a/tests/ref/compiler/color.png and b/tests/ref/compiler/color.png differ diff --git a/tests/typ/compiler/color.typ b/tests/typ/compiler/color.typ index ec1f99024..f165ac0f4 100644 --- a/tests/typ/compiler/color.typ +++ b/tests/typ/compiler/color.typ @@ -8,7 +8,7 @@ spacing: 1fr, rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%)), rect(width: 1cm, fill: c), - rect(width: 1cm, fill: c.negate()), + rect(width: 1cm, fill: c.negate(space: cmyk)), ) #for x in range(0, 11) { @@ -82,4 +82,4 @@ // Ref: false #test-repr(luma(20%).lighten(50%), luma(60%)) #test-repr(luma(80%).darken(20%), luma(64%)) -#test-repr(luma(80%).negate(), luma(20%)) +#test-repr(luma(80%).negate(space: luma), luma(20%)) diff --git a/tests/typ/compute/construct.typ b/tests/typ/compute/construct.typ index dcf597558..e429efc58 100644 --- a/tests/typ/compute/construct.typ +++ b/tests/typ/compute/construct.typ @@ -11,7 +11,7 @@ // Test color modification methods. #test(rgb(25, 35, 45).lighten(10%), rgb(48, 57, 66)) #test(rgb(40, 30, 20).darken(10%), rgb(36, 27, 18)) -#test(rgb("#133337").negate(), rgb(236, 204, 200)) +#test(rgb("#133337").negate(space: rgb), rgb(236, 204, 200)) #test(white.lighten(100%), white) // Color mixing, in Oklab space by default.