From 79615a01bd4266e6a5adb385650735768ea96d56 Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Tue, 27 Feb 2024 12:15:17 +0100 Subject: [PATCH] Improve color negation (#3500) --- crates/typst/src/visualize/color.rs | 81 ++++++++++++++-------------- tests/ref/compiler/color.png | Bin 1407 -> 1354 bytes tests/typ/compiler/color.typ | 4 +- tests/typ/compute/construct.typ | 2 +- 4 files changed, 42 insertions(+), 45 deletions(-) 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 69dbe5e844b4c05416a7947062495e17a690796e..2b416f6413aa7756a8c343b222390c3c9c55ba5a 100644 GIT binary patch delta 743 zcmey*b&6|(3di{XpAc6D28M~6(i7d))o+z)O>bP-uvoKxvF6TRhP0V8i+g$+8X9^^ zrZ2A5EN*CESigl~(Zh*Tb@?*`iWnLibc_6_X)gZH5Hazhx+}lQnRN^dEM}fAjv*Dd z-rhawC&I|!8kp|j5Ln2-=vwHMRyeQy|9>^%X?KZz=Y`Vc&{&P;oWJ4y=$rBi*fmn#cqLd>~qWrqepd|0n{cE6YW z<>y~iYkluu`u;`vug`v=$$>1wY>XVwOr@297Krd1Z&2`@q|$rleA;t9pff~-=WrZy zk}9n*?6s<~m6h2y`QN+QlNnh>L>m&~HXAx2*_+s-cXsjyMiJ(OxaP?d8AZ68uFv(D zq;m4L-yCb84q>i_f+|6^O9vlr=bYTgD8lqv3TUAymrbziBo)tjD<9nf1%*LNaKnZf z^On26EZj9UZ^`VJWxHm-TYi)U&38a=B{DQso;}6^)FdI+!+FT5YT~?Q>MxytO}V$E z{AF#`3^6pXNyt2qU%Sr15y{06Lw~rh?iB>ONSL?fKc6GeEmAY*pFJl#3Ft=)!Ir68 z=1jX(Gs}B(rkd~al_oRQ(2W%}P~#BJH1b{g(qwYl<(OIi-kFwbRVEiQ3bQc_oXIwG z12QE<5_9HX+%@akrK(#Vvoo#VF1fp9`nSmeEFy@YmjD^`J5&GdvbkI4nq78_^3TpR sg&4$M@A9E13=%=I>P?^slHGWJ$7A)lQg88{z~s!}>FVdQ&MBb@0M_Oek^lez literal 1407 zcmeAS@N?(olHy`uVBq!ia0y~yU}RxnU^>LX3>4w-kL3YUmjZl3T>t<7|M%bj7th{Z zzj*J&{tLU-9cK9dpZC)z$!E_PUj1J?cbnSHo5p9(IPBlQuz$_;>C=5TZ(db(??U#5 zyxtFy%a%RNDDIi4VxL3 zJxFV4(44VYb8%`_og2f3QnSUHkqr%v8U4Nu4Gs+r6|wEc4Gn4y4U!EFybTTi89Fip ziWnLibc_6_X)a!@nZDRWQ`3KOq;tHF=3*~%KWl9}J$XF^QAJ^H5jG|^21X{JCxPBt zec|YK1_qW4PZ!6Kid%2*ZqK`&z~C0R^!vTHDt_Jh4;|#PEyCKJ%UdVj1$X%-aGR0~Z-;%$X@waAwQ-Qh@S<7W*i@0WFQ(6xC zPt3pc{EKO=_x;QLUyT2H?Y}&q1=E_uDwgBfSp5+pnz+vq?y?BpgV)orSS+B>)4O5f zwAgz-r}gH0Sx?tr##qjTY0blW31Q~fo-gJ_~oA^R&za$U*1?^A?w91j2R&< zF9l`RBT@&tx(7om$~DJU$B)D+zXxEj`#5?$@*HYDXDqsa9MD