Improve color negation (#3500)

This commit is contained in:
frozolotl 2024-02-27 12:15:17 +01:00 committed by GitHub
parent 0aa9254356
commit 79615a01bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 42 additions and 45 deletions

View File

@ -4,10 +4,9 @@ use std::str::FromStr;
use ecow::{eco_format, EcoString, EcoVec}; use ecow::{eco_format, EcoString, EcoVec};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use palette::convert::FromColorUnclamped;
use palette::encoding::{self, Linear}; use palette::encoding::{self, Linear};
use palette::{ use palette::{
Darken, Desaturate, FromColor, Lighten, Okhsva, OklabHue, RgbHue, Saturate, ShiftHue, Darken, Desaturate, FromColor, Lighten, OklabHue, RgbHue, Saturate, ShiftHue,
}; };
use qcms::Profile; 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] #[func]
pub fn negate(self) -> Color { pub fn negate(
match self { 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::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( Self::Oklch(c) => Self::Oklch(Oklch::new(
c.l, 1.0 - c.l,
-c.chroma, c.chroma,
OklabHue::from_degrees(360.0 - c.hue.into_degrees()), OklabHue::from_degrees(c.hue.into_degrees() + 180.0),
c.alpha, c.alpha,
)), )),
Self::LinearRgb(c) => Self::LinearRgb(LinearRgb::new( 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::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( 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.saturation,
c.lightness, c.lightness,
c.alpha, c.alpha,
)), )),
Self::Hsv(c) => Self::Hsv(Hsv::new( 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.saturation,
c.value, c.value,
c.alpha, c.alpha,
)), )),
} };
result.to_space(self.space())
} }
/// Rotates the hue of the color by a given angle. /// Rotates the hue of the color by a given angle.
@ -1304,10 +1317,8 @@ impl Color {
pub fn to_luma(self) -> Self { pub fn to_luma(self) -> Self {
Self::Luma(match self { Self::Luma(match self {
Self::Luma(c) => c, Self::Luma(c) => c,
// Perform sRGB gamut mapping by converting to Okhsv first. Self::Oklab(c) => Luma::from_color(c),
// This yields better results than clamping. Self::Oklch(c) => Luma::from_color(c),
Self::Oklab(c) => Luma::from_color(Okhsva::from_color(c)),
Self::Oklch(c) => Luma::from_color(Okhsva::from_color(c)),
Self::Rgb(c) => Luma::from_color(c), Self::Rgb(c) => Luma::from_color(c),
Self::LinearRgb(c) => Luma::from_color(c), Self::LinearRgb(c) => Luma::from_color(c),
Self::Cmyk(c) => Luma::from_color(c.to_rgba()), Self::Cmyk(c) => Luma::from_color(c.to_rgba()),
@ -1320,9 +1331,7 @@ impl Color {
Self::Oklab(match self { Self::Oklab(match self {
Self::Luma(c) => Oklab::from_color(c), Self::Luma(c) => Oklab::from_color(c),
Self::Oklab(c) => c, Self::Oklab(c) => c,
// No clamping is necessary for this conversion because the Self::Oklch(c) => Oklab::from_color(c),
// lightness property is the same for both Oklab and Oklch.
Self::Oklch(c) => Oklab::from_color_unclamped(c),
Self::Rgb(c) => Oklab::from_color(c), Self::Rgb(c) => Oklab::from_color(c),
Self::LinearRgb(c) => Oklab::from_color(c), Self::LinearRgb(c) => Oklab::from_color(c),
Self::Cmyk(c) => Oklab::from_color(c.to_rgba()), Self::Cmyk(c) => Oklab::from_color(c.to_rgba()),
@ -1334,9 +1343,7 @@ impl Color {
pub fn to_oklch(self) -> Self { pub fn to_oklch(self) -> Self {
Self::Oklch(match self { Self::Oklch(match self {
Self::Luma(c) => Oklch::from_color(c), Self::Luma(c) => Oklch::from_color(c),
// No clamping is necessary for this conversion because the Self::Oklab(c) => Oklch::from_color(c),
// lightness property is the same for both Oklab and Oklch.
Self::Oklab(c) => Oklch::from_color_unclamped(c),
Self::Oklch(c) => c, Self::Oklch(c) => c,
Self::Rgb(c) => Oklch::from_color(c), Self::Rgb(c) => Oklch::from_color(c),
Self::LinearRgb(c) => Oklch::from_color(c), Self::LinearRgb(c) => Oklch::from_color(c),
@ -1349,10 +1356,8 @@ impl Color {
pub fn to_rgb(self) -> Self { pub fn to_rgb(self) -> Self {
Self::Rgb(match self { Self::Rgb(match self {
Self::Luma(c) => Rgb::from_color(c), Self::Luma(c) => Rgb::from_color(c),
// Perform sRGB gamut mapping by converting to Okhsv first. Self::Oklab(c) => Rgb::from_color(c),
// This yields better results than clamping. Self::Oklch(c) => Rgb::from_color(c),
Self::Oklab(c) => Rgb::from_color(Okhsva::from_color(c)),
Self::Oklch(c) => Rgb::from_color(Okhsva::from_color(c)),
Self::Rgb(c) => c, Self::Rgb(c) => c,
Self::LinearRgb(c) => Rgb::from_linear(c), Self::LinearRgb(c) => Rgb::from_linear(c),
Self::Cmyk(c) => Rgb::from_color(c.to_rgba()), Self::Cmyk(c) => Rgb::from_color(c.to_rgba()),
@ -1364,10 +1369,8 @@ impl Color {
pub fn to_linear_rgb(self) -> Self { pub fn to_linear_rgb(self) -> Self {
Self::LinearRgb(match self { Self::LinearRgb(match self {
Self::Luma(c) => LinearRgb::from_color(c), Self::Luma(c) => LinearRgb::from_color(c),
// Perform sRGB gamut mapping by converting to Okhsv first. Self::Oklab(c) => LinearRgb::from_color(c),
// This yields better results than clamping. Self::Oklch(c) => LinearRgb::from_color(c),
Self::Oklab(c) => LinearRgb::from_color(Okhsva::from_color(c)),
Self::Oklch(c) => LinearRgb::from_color(Okhsva::from_color(c)),
Self::Rgb(c) => LinearRgb::from_color(c), Self::Rgb(c) => LinearRgb::from_color(c),
Self::LinearRgb(c) => c, Self::LinearRgb(c) => c,
Self::Cmyk(c) => LinearRgb::from_color(c.to_rgba()), Self::Cmyk(c) => LinearRgb::from_color(c.to_rgba()),
@ -1379,10 +1382,8 @@ impl Color {
pub fn to_cmyk(self) -> Self { pub fn to_cmyk(self) -> Self {
Self::Cmyk(match self { Self::Cmyk(match self {
Self::Luma(c) => Cmyk::from_luma(c), Self::Luma(c) => Cmyk::from_luma(c),
// Perform sRGB gamut mapping by converting to Okhsv first. Self::Oklab(c) => Cmyk::from_rgba(Rgb::from_color(c)),
// This yields better results than clamping. Self::Oklch(c) => Cmyk::from_rgba(Rgb::from_color(c)),
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::Rgb(c) => Cmyk::from_rgba(c), Self::Rgb(c) => Cmyk::from_rgba(c),
Self::LinearRgb(c) => Cmyk::from_rgba(Rgb::from_linear(c)), Self::LinearRgb(c) => Cmyk::from_rgba(Rgb::from_linear(c)),
Self::Cmyk(c) => c, Self::Cmyk(c) => c,
@ -1394,10 +1395,8 @@ impl Color {
pub fn to_hsl(self) -> Self { pub fn to_hsl(self) -> Self {
Self::Hsl(match self { Self::Hsl(match self {
Self::Luma(c) => Hsl::from_color(c), Self::Luma(c) => Hsl::from_color(c),
// Perform sRGB gamut mapping by converting to Okhsv first. Self::Oklab(c) => Hsl::from_color(c),
// This yields better results than clamping. Self::Oklch(c) => Hsl::from_color(c),
Self::Oklab(c) => Hsl::from_color(Okhsva::from_color(c)),
Self::Oklch(c) => Hsl::from_color(Okhsva::from_color(c)),
Self::Rgb(c) => Hsl::from_color(c), Self::Rgb(c) => Hsl::from_color(c),
Self::LinearRgb(c) => Hsl::from_color(Rgb::from_linear(c)), Self::LinearRgb(c) => Hsl::from_color(Rgb::from_linear(c)),
Self::Cmyk(c) => Hsl::from_color(c.to_rgba()), Self::Cmyk(c) => Hsl::from_color(c.to_rgba()),
@ -1409,10 +1408,8 @@ impl Color {
pub fn to_hsv(self) -> Self { pub fn to_hsv(self) -> Self {
Self::Hsv(match self { Self::Hsv(match self {
Self::Luma(c) => Hsv::from_color(c), Self::Luma(c) => Hsv::from_color(c),
// Perform sRGB gamut mapping by converting to Okhsv first. Self::Oklab(c) => Hsv::from_color(c),
// This yields better results than clamping. Self::Oklch(c) => Hsv::from_color(c),
Self::Oklab(c) => Hsv::from_color(Okhsva::from_color(c)),
Self::Oklch(c) => Hsv::from_color(Okhsva::from_color(c)),
Self::Rgb(c) => Hsv::from_color(c), Self::Rgb(c) => Hsv::from_color(c),
Self::LinearRgb(c) => Hsv::from_color(Rgb::from_linear(c)), Self::LinearRgb(c) => Hsv::from_color(Rgb::from_linear(c)),
Self::Cmyk(c) => Hsv::from_color(c.to_rgba()), Self::Cmyk(c) => Hsv::from_color(c.to_rgba()),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -8,7 +8,7 @@
spacing: 1fr, spacing: 1fr,
rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%)), rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%)),
rect(width: 1cm, fill: c), 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) { #for x in range(0, 11) {
@ -82,4 +82,4 @@
// Ref: false // Ref: false
#test-repr(luma(20%).lighten(50%), luma(60%)) #test-repr(luma(20%).lighten(50%), luma(60%))
#test-repr(luma(80%).darken(20%), luma(64%)) #test-repr(luma(80%).darken(20%), luma(64%))
#test-repr(luma(80%).negate(), luma(20%)) #test-repr(luma(80%).negate(space: luma), luma(20%))

View File

@ -11,7 +11,7 @@
// Test color modification methods. // Test color modification methods.
#test(rgb(25, 35, 45).lighten(10%), rgb(48, 57, 66)) #test(rgb(25, 35, 45).lighten(10%), rgb(48, 57, 66))
#test(rgb(40, 30, 20).darken(10%), rgb(36, 27, 18)) #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) #test(white.lighten(100%), white)
// Color mixing, in Oklab space by default. // Color mixing, in Oklab space by default.