diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index c9a625d1c..fd83b8c9e 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -1223,6 +1223,11 @@ impl<'a> CompletionContext<'a> { "oklab(${l}, ${a}, ${b}, ${alpha})", "A custom Oklab color.", ); + self.snippet_completion( + "oklch()", + "oklch(${l}, ${chroma}, ${hue}, ${alpha})", + "A custom Oklch color.", + ); self.snippet_completion( "color.linear-rgb()", "color.linear-rgb(${r}, ${g}, ${b}, ${a})", diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 554e7bcbc..212debb05 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -65,6 +65,7 @@ fn prelude(global: &mut Scope) { global.define("lime", Color::LIME); global.define("luma", Color::luma_data()); global.define("oklab", Color::oklab_data()); + global.define("oklch", Color::oklch_data()); global.define("rgb", Color::rgb_data()); global.define("cmyk", Color::cmyk_data()); global.define("range", Array::range_data()); diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs index 999f604e2..0de023071 100644 --- a/crates/typst-pdf/src/color.rs +++ b/crates/typst-pdf/src/color.rs @@ -107,6 +107,7 @@ impl ColorSpaces { oklab.tint_ref(self.oklab(alloc)); oklab.attrs().subtype(DeviceNSubtype::DeviceN); } + ColorSpace::Oklch => self.write(ColorSpace::Oklab, writer, alloc), ColorSpace::Srgb => writer.icc_based(self.srgb(alloc)), ColorSpace::D65Gray => writer.icc_based(self.d65_gray(alloc)), ColorSpace::LinearRgb => { @@ -266,6 +267,9 @@ impl ColorEncode for ColorSpace { let [h, s, v, _] = color.to_hsv().to_vec4(); [h / 360.0, s, v, 0.0] } + ColorSpace::Oklch => { + unimplemented!("Oklch is always converted to Oklab first") + } _ => color.to_vec4(), } } @@ -306,7 +310,8 @@ impl PaintEncode for Color { let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); ctx.content.set_fill_color([l]); } - Color::Oklab(_) => { + // Oklch is converted to Oklab. + Color::Oklab(_) | Color::Oklch(_) => { ctx.parent.colors.oklab(&mut ctx.parent.alloc); ctx.set_fill_color_space(OKLAB); @@ -359,7 +364,8 @@ impl PaintEncode for Color { let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); ctx.content.set_stroke_color([l]); } - Color::Oklab(_) => { + // Oklch is converted to Oklab. + Color::Oklab(_) | Color::Oklch(_) => { ctx.parent.colors.oklab(&mut ctx.parent.alloc); ctx.set_stroke_color_space(OKLAB); diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index c057566d6..6095baded 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -1275,6 +1275,24 @@ impl ColorEncode for Color { ) } } + Color::Oklch(oklch) => { + if oklch.alpha != 1.0 { + eco_format!( + "oklch({:.3}% {:.5} {:.3}deg / {:.3})", + oklch.l * 100.0, + oklch.chroma, + oklch.hue.into_degrees(), + oklch.alpha + ) + } else { + eco_format!( + "oklch({:.3}% {:.5} {:.3}deg)", + oklch.l * 100.0, + oklch.chroma, + oklch.hue.into_degrees(), + ) + } + } Color::Hsl(hsl) => { if hsl.alpha != 1.0 { eco_format!( diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/geom/color.rs index 3a02a8c27..ab827c190 100644 --- a/crates/typst/src/geom/color.rs +++ b/crates/typst/src/geom/color.rs @@ -3,7 +3,9 @@ use std::str::FromStr; use ecow::EcoVec; use once_cell::sync::Lazy; use palette::encoding::{self, Linear}; -use palette::{Darken, Desaturate, FromColor, Lighten, RgbHue, Saturate, ShiftHue}; +use palette::{ + Darken, Desaturate, FromColor, Lighten, OklabHue, RgbHue, Saturate, ShiftHue, +}; use super::*; use crate::diag::{bail, error, At, SourceResult}; @@ -12,6 +14,7 @@ use crate::syntax::{Span, Spanned}; // Type aliases for `palette` internal types in f32. pub type Oklab = palette::oklab::Oklaba; +pub type Oklch = palette::oklch::Oklcha; pub type LinearRgba = palette::rgb::Rgba, f32>; pub type Rgba = palette::rgb::Rgba; pub type Hsl = palette::hsl::Hsla; @@ -28,6 +31,7 @@ const ANGLE_EPSILON: f32 = 1e-5; /// - Device CMYK through [`cmyk` function]($color.cmyk) /// - D65 Gray through the [`luma` function]($color.luma) /// - Oklab through the [`oklab` function]($color.oklab) +/// - Oklch through the [`oklch` function]($color.oklch) /// - Linear RGB through the [`color.linear-rgb` function]($color.linear-rgb) /// - HSL through the [`color.hsl` function]($color.hsl) /// - HSV through the [`color.hsv` function]($color.hsv) @@ -153,8 +157,10 @@ const ANGLE_EPSILON: f32 = 1e-5; pub enum Color { /// A 32-bit luma color. Luma(Luma), - /// A 32-bit L*a*b* color in the Oklab color space. + /// A 32-bit L\*a\*b\* color in the Oklab color space. Oklab(Oklab), + /// A 32-bit LCh color in the Oklab color space. + Oklch(Oklch), /// A 32-bit RGBA color. Rgba(Rgba), /// A 32-bit linear RGB color. @@ -179,6 +185,12 @@ impl From for Color { } } +impl From for Color { + fn from(c: Oklch) -> Self { + Self::Oklch(c) + } +} + impl From for Color { fn from(c: Rgba) -> Self { Self::Rgba(c) @@ -300,16 +312,16 @@ impl Color { /// The real arguments (the other arguments are just for the docs, this /// function is a bit involved, so we parse the arguments manually). args: &mut Args, - /// The cyan component. + /// The lightness component. #[external] lightness: RatioComponent, - /// The magenta component. + /// The a ("green/red") component. #[external] a: ABComponent, - /// The yellow component. + /// The b ("blue/yellow") component. #[external] b: ABComponent, - /// The key component. + /// The alpha component. #[external] alpha: RatioComponent, /// Alternatively: The color to convert to Oklab. @@ -335,6 +347,68 @@ impl Color { }) } + /// Create an [Oklch](https://bottosson.github.io/posts/oklab/) color. + /// + /// This color space is well suited for the following use cases: + /// - Color manipulation involving lightness, chroma, and hue + /// - Creating grayscale images with uniform perceived lightness + /// - Creating smooth and uniform color transition and gradients + /// + /// A linear Oklch color is represented internally by an array of four + /// components: + /// - lightness ([`ratio`]($ratio)) + /// - chroma ([`float`]($float) in the range `[-0.4..0.4]`) + /// - hue ([`angle`]($angle)) + /// - alpha ([`ratio`]($ratio)) + /// + /// These components are also available using the + /// [`components`]($color.components) method. + /// + /// ```example + /// #square( + /// fill: oklch(40%, 0.2, 160deg, 50%) + /// ) + /// ``` + #[func] + pub fn oklch( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The lightness component. + #[external] + lightness: RatioComponent, + /// The chroma component. + #[external] + chroma: ABComponent, + /// The hue component. + #[external] + hue: Angle, + /// The alpha component. + #[external] + alpha: RatioComponent, + /// Alternatively: The color to convert to Oklch. + /// + /// If this is given, the individual components should not be given. + #[external] + color: Color, + ) -> SourceResult { + Ok(if let Some(color) = args.find::()? { + color.to_oklch() + } else { + let RatioComponent(l) = args.expect("lightness component")?; + let ABComponent(c) = args.expect("chroma component")?; + let h: Angle = args.expect("hue component")?; + let RatioComponent(alpha) = + args.eat()?.unwrap_or(RatioComponent(Ratio::one())); + Self::Oklch(Oklch::new( + l.get() as f32, + c.get() as f32, + OklabHue::from_degrees(h.to_deg() as f32), + alpha.get() as f32, + )) + }) + } + /// Create an RGB(A) color with linear luma. /// /// This color space is similar to sRGB, but with the distinction that the @@ -656,6 +730,7 @@ impl Color { /// |-------------------------|-----------|------------|-----------|--------| /// | [`luma`]($color.luma) | Lightness | | | | /// | [`oklab`]($color.oklab) | Lightness | `a` | `b` | Alpha | + /// | [`oklch`]($color.oklch) | Lightness | Chroma | Hue | Alpha | /// | [`linear-rgb`]($color.linear-rgb) | Red | Green | Blue | Alpha | /// | [`rgb`]($color.rgb) | Red | Green | Blue | Alpha | /// | [`cmyk`]($color.cmyk) | Cyan | Magenta | Yellow | Key | @@ -697,6 +772,26 @@ impl Color { ] } } + Self::Oklch(c) => { + if alpha { + array![ + Ratio::new(c.l as _), + (c.chroma as f64 * 1000.0).round() / 1000.0, + Angle::deg( + c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _ + ), + Ratio::new(c.alpha as _), + ] + } else { + array![ + Ratio::new(c.l as _), + (c.chroma as f64 * 1000.0).round() / 1000.0, + Angle::deg( + c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _ + ), + ] + } + } Self::LinearRgb(c) => { if alpha { array![ @@ -779,8 +874,9 @@ impl Color { } /// Returns the constructor function for this color's space: - /// - [`oklab`]($color.oklab) /// - [`luma`]($color.luma) + /// - [`oklab`]($color.oklab) + /// - [`oklch`]($color.oklch) /// - [`linear-rgb`]($color.linear-rgb) /// - [`rgb`]($color.rgb) /// - [`cmyk`]($color.cmyk) @@ -796,6 +892,7 @@ impl Color { match self { Self::Luma(_) => ColorSpace::D65Gray, Self::Oklab(_) => ColorSpace::Oklab, + Self::Oklch(_) => ColorSpace::Oklch, Self::LinearRgb(_) => ColorSpace::LinearRgb, Self::Rgba(_) => ColorSpace::Srgb, Self::Cmyk(_) => ColorSpace::Cmyk, @@ -828,6 +925,7 @@ impl Color { match self { Self::Luma(c) => Self::Luma(c.lighten(factor)), Self::Oklab(c) => Self::Oklab(c.lighten(factor)), + Self::Oklch(c) => Self::Oklch(c.lighten(factor)), Self::LinearRgb(c) => Self::LinearRgb(c.lighten(factor)), Self::Rgba(c) => Self::Rgba(c.lighten(factor)), Self::Cmyk(c) => Self::Cmyk(c.lighten(factor)), @@ -847,6 +945,7 @@ impl Color { match self { Self::Luma(c) => Self::Luma(c.darken(factor)), Self::Oklab(c) => Self::Oklab(c.darken(factor)), + Self::Oklch(c) => Self::Oklch(c.darken(factor)), Self::LinearRgb(c) => Self::LinearRgb(c.darken(factor)), Self::Rgba(c) => Self::Rgba(c.darken(factor)), Self::Cmyk(c) => Self::Cmyk(c.darken(factor)), @@ -870,6 +969,7 @@ impl Color { .with_hint("try converting your color to RGB first")); } Self::Oklab(_) => self.to_hsv().saturate(span, factor)?.to_oklab(), + Self::Oklch(_) => self.to_hsv().saturate(span, factor)?.to_oklch(), Self::LinearRgb(_) => self.to_hsv().saturate(span, factor)?.to_linear_rgb(), Self::Rgba(_) => self.to_hsv().saturate(span, factor)?.to_rgba(), Self::Cmyk(_) => self.to_hsv().saturate(span, factor)?.to_cmyk(), @@ -893,6 +993,7 @@ impl Color { .with_hint("try converting your color to RGB first")); } Self::Oklab(_) => self.to_hsv().desaturate(span, factor)?.to_oklab(), + Self::Oklch(_) => self.to_hsv().desaturate(span, factor)?.to_oklch(), Self::LinearRgb(_) => self.to_hsv().desaturate(span, factor)?.to_linear_rgb(), Self::Rgba(_) => self.to_hsv().desaturate(span, factor)?.to_rgba(), Self::Cmyk(_) => self.to_hsv().desaturate(span, factor)?.to_cmyk(), @@ -907,6 +1008,12 @@ impl Color { match self { Self::Luma(c) => Self::Luma(Luma::new(1.0 - c.luma)), Self::Oklab(c) => Self::Oklab(Oklab::new(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()), + c.alpha, + )), Self::LinearRgb(c) => Self::LinearRgb(LinearRgba::new( 1.0 - c.red, 1.0 - c.green, @@ -940,18 +1047,35 @@ impl Color { span: Span, /// The angle to rotate the hue by. angle: Angle, + /// The color space used to rotate. By default, this happens in a perceptual + /// color space ([`oklch`]($color.oklch)). + #[named] + #[default(ColorSpace::Oklch)] + space: ColorSpace, ) -> SourceResult { - Ok(match self { - Self::Luma(_) => { - bail!(error!(span, "cannot rotate grayscale color") - .with_hint("try converting your color to RGB first")); + Ok(match space { + ColorSpace::Oklch => { + let Self::Oklch(oklch) = self.to_oklch() else { + unreachable!(); + }; + let rotated = oklch.shift_hue(angle.to_deg() as f32); + Self::Oklch(rotated).to_space(self.space()) } - Self::Oklab(_) => self.to_hsv().rotate(span, angle)?.to_oklab(), - Self::LinearRgb(_) => self.to_hsv().rotate(span, angle)?.to_linear_rgb(), - Self::Rgba(_) => self.to_hsv().rotate(span, angle)?.to_rgba(), - Self::Cmyk(_) => self.to_hsv().rotate(span, angle)?.to_cmyk(), - Self::Hsl(c) => Self::Hsl(c.shift_hue(angle.to_deg() as f32)), - Self::Hsv(c) => Self::Hsv(c.shift_hue(angle.to_deg() as f32)), + ColorSpace::Hsl => { + let Self::Hsl(hsl) = self.to_hsl() else { + unreachable!(); + }; + let rotated = hsl.shift_hue(angle.to_deg() as f32); + Self::Hsl(rotated).to_space(self.space()) + } + ColorSpace::Hsv => { + let Self::Hsv(hsv) = self.to_hsv() else { + unreachable!(); + }; + let rotated = hsv.shift_hue(angle.to_deg() as f32); + Self::Hsv(rotated).to_space(self.space()) + } + _ => bail!(span, "this colorspace does not support hue rotation"), }) } @@ -1009,6 +1133,7 @@ impl Color { 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])), ColorSpace::Srgb => Color::Rgba(Rgba::new(m[0], m[1], m[2], m[3])), ColorSpace::LinearRgb => { Color::LinearRgb(LinearRgba::new(m[0], m[1], m[2], m[3])) @@ -1050,6 +1175,7 @@ impl Color { match self { Color::Luma(_) | Color::Cmyk(_) => None, Color::Oklab(c) => Some(c.alpha), + Color::Oklch(c) => Some(c.alpha), Color::Rgba(c) => Some(c.alpha), Color::LinearRgb(c) => Some(c.alpha), Color::Hsl(c) => Some(c.alpha), @@ -1062,6 +1188,7 @@ impl Color { match &mut self { Color::Luma(_) | Color::Cmyk(_) => {} Color::Oklab(c) => c.alpha = alpha, + Color::Oklch(c) => c.alpha = alpha, Color::Rgba(c) => c.alpha = alpha, Color::LinearRgb(c) => c.alpha = alpha, Color::Hsl(c) => c.alpha = alpha, @@ -1076,6 +1203,12 @@ impl Color { match self { Color::Luma(c) => [c.luma; 4], Color::Oklab(c) => [c.l, c.a, c.b, c.alpha], + Color::Oklch(c) => [ + c.l, + c.chroma, + c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON), + c.alpha, + ], Color::Rgba(c) => [c.red, c.green, c.blue, c.alpha], Color::LinearRgb(c) => [c.red, c.green, c.blue, c.alpha], Color::Cmyk(c) => [c.c, c.m, c.y, c.k], @@ -1102,6 +1235,7 @@ impl Color { pub fn to_space(self, space: ColorSpace) -> Self { match space { ColorSpace::Oklab => self.to_oklab(), + ColorSpace::Oklch => self.to_oklch(), ColorSpace::Srgb => self.to_rgba(), ColorSpace::LinearRgb => self.to_linear_rgb(), ColorSpace::Hsl => self.to_hsl(), @@ -1115,6 +1249,7 @@ impl Color { Self::Luma(match self { Self::Luma(c) => c, Self::Oklab(c) => Luma::from_color(c), + Self::Oklch(c) => Luma::from_color(c), Self::Rgba(c) => Luma::from_color(c), Self::LinearRgb(c) => Luma::from_color(c), Self::Cmyk(c) => Luma::from_color(c.to_rgba()), @@ -1127,6 +1262,7 @@ impl Color { Self::Oklab(match self { Self::Luma(c) => Oklab::from_color(c), Self::Oklab(c) => c, + Self::Oklch(c) => Oklab::from_color(c), Self::Rgba(c) => Oklab::from_color(c), Self::LinearRgb(c) => Oklab::from_color(c), Self::Cmyk(c) => Oklab::from_color(c.to_rgba()), @@ -1135,10 +1271,24 @@ impl Color { }) } + pub fn to_oklch(self) -> Self { + Self::Oklch(match self { + Self::Luma(c) => Oklch::from_color(c), + Self::Oklab(c) => Oklch::from_color(c), + Self::Oklch(c) => c, + Self::Rgba(c) => Oklch::from_color(c), + Self::LinearRgb(c) => Oklch::from_color(c), + Self::Cmyk(c) => Oklch::from_color(c.to_rgba()), + Self::Hsl(c) => Oklch::from_color(c), + Self::Hsv(c) => Oklch::from_color(c), + }) + } + pub fn to_linear_rgb(self) -> Self { Self::LinearRgb(match self { Self::Luma(c) => LinearRgba::from_color(c), Self::Oklab(c) => LinearRgba::from_color(c), + Self::Oklch(c) => LinearRgba::from_color(c), Self::Rgba(c) => LinearRgba::from_color(c), Self::LinearRgb(c) => c, Self::Cmyk(c) => LinearRgba::from_color(c.to_rgba()), @@ -1151,6 +1301,7 @@ impl Color { Self::Rgba(match self { Self::Luma(c) => Rgba::from_color(c), Self::Oklab(c) => Rgba::from_color(c), + Self::Oklch(c) => Rgba::from_color(c), Self::Rgba(c) => c, Self::LinearRgb(c) => Rgba::from_linear(c), Self::Cmyk(c) => c.to_rgba(), @@ -1163,6 +1314,7 @@ impl Color { Self::Cmyk(match self { Self::Luma(c) => Cmyk::from_luma(c), Self::Oklab(c) => Cmyk::from_rgba(Rgba::from_color(c)), + Self::Oklch(c) => Cmyk::from_rgba(Rgba::from_color(c)), Self::Rgba(c) => Cmyk::from_rgba(c), Self::LinearRgb(c) => Cmyk::from_rgba(Rgba::from_linear(c)), Self::Cmyk(c) => c, @@ -1175,6 +1327,7 @@ impl Color { Self::Hsl(match self { Self::Luma(c) => Hsl::from_color(c), Self::Oklab(c) => Hsl::from_color(c), + Self::Oklch(c) => Hsl::from_color(c), Self::Rgba(c) => Hsl::from_color(c), Self::LinearRgb(c) => Hsl::from_color(Rgba::from_linear(c)), Self::Cmyk(c) => Hsl::from_color(c.to_rgba()), @@ -1187,6 +1340,7 @@ impl Color { Self::Hsv(match self { Self::Luma(c) => Hsv::from_color(c), Self::Oklab(c) => Hsv::from_color(c), + Self::Oklch(c) => Hsv::from_color(c), Self::Rgba(c) => Hsv::from_color(c), Self::LinearRgb(c) => Hsv::from_color(Rgba::from_linear(c)), Self::Cmyk(c) => Hsv::from_color(c.to_rgba()), @@ -1246,6 +1400,30 @@ impl Repr for Color { ) } } + Self::Oklch(c) => { + if c.alpha == 1.0 { + eco_format!( + "oklch({}, {}, {})", + Ratio::new(c.l as _).repr(), + format_float(c.chroma as _, Some(3), ""), + Angle::deg( + c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _ + ) + .repr() + ) + } else { + eco_format!( + "oklch({}, {}, {}, {})", + Ratio::new(c.l as _).repr(), + format_float(c.chroma as _, Some(3), ""), + Angle::deg( + c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _ + ) + .repr(), + Ratio::new(c.alpha as _).repr(), + ) + } + } Self::Hsl(c) => { if c.alpha == 1.0 { eco_format!( @@ -1308,6 +1486,7 @@ impl PartialEq for Color { (a.luma * 255.0).round() as u8 == (b.luma * 255.0).round() as u8 } (Self::Oklab(a), Self::Oklab(b)) => a == b, + (Self::Oklch(a), Self::Oklch(b)) => a == b, (Self::LinearRgb(a), Self::LinearRgb(b)) => a == b, (Self::Cmyk(a), Self::Cmyk(b)) => a == b, (Self::Hsl(a), Self::Hsl(b)) => a == b, @@ -1468,12 +1647,15 @@ cast! { v: Ratio => Self(v.get()), } -/// A color space for mixing. +/// A color space for color manipulation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ColorSpace { - /// A perceptual color space. + /// The perceptual Oklab color space. Oklab, + /// The perceptual Oklch color space. + Oklch, + /// The standard RGB color space. Srgb, @@ -1497,6 +1679,7 @@ cast! { ColorSpace, self => match self { Self::Oklab => Color::oklab_data(), + Self::Oklch => Color::oklch_data(), Self::Srgb => Color::rgb_data(), Self::D65Gray => Color::luma_data(), Self::LinearRgb => Color::linear_rgb_data(), @@ -1505,7 +1688,7 @@ cast! { Self::Cmyk => Color::cmyk_data(), }.into_value(), v: Value => { - let expected = "expected `rgb`, `luma`, `cmyk`, `oklab`, `color.linear-rgb`, `color.hsl`, or `color.hsv`"; + let expected = "expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv`"; let Value::Func(func) = v else { bail!("{expected}, found {}", v.ty()); }; @@ -1514,6 +1697,8 @@ cast! { // whereas the `NativeFuncData` is not. if func == Color::oklab_data() { Self::Oklab + } else if func == Color::oklch_data() { + Self::Oklch } else if func == Color::rgb_data() { Self::Srgb } else if func == Color::luma_data() { diff --git a/crates/typst/src/geom/gradient.rs b/crates/typst/src/geom/gradient.rs index 8a10ed2bf..13ed25e3e 100644 --- a/crates/typst/src/geom/gradient.rs +++ b/crates/typst/src/geom/gradient.rs @@ -101,6 +101,7 @@ use crate::syntax::{Span, Spanned}; /// | Color space | Perceptually uniform? | /// | ------------------------------- |-----------------------| /// | [Oklab]($color.oklab) | *Yes* | +/// | [Oklch]($color.oklch) | *Yes* | /// | [sRGB]($color.rgb) | *No* | /// | [linear-RGB]($color.linear-rgb) | *Yes* | /// | [CMYK]($color.cmyk) | *No* | @@ -113,6 +114,7 @@ use crate::syntax::{Span, Spanned}; /// >>> #set block(spacing: 0pt) /// #let spaces = ( /// ("Oklab", color.oklab), +/// ("Oklch", color.oklch), /// ("linear-RGB", color.linear-rgb), /// ("sRGB", color.rgb), /// ("CMYK", color.cmyk), diff --git a/tests/ref/compiler/color.png b/tests/ref/compiler/color.png index ff9514da1..078a0b621 100644 Binary files a/tests/ref/compiler/color.png and b/tests/ref/compiler/color.png differ diff --git a/tests/ref/compiler/repr-color-gradient.png b/tests/ref/compiler/repr-color-gradient.png index 25aae2e3f..36747d450 100644 Binary files a/tests/ref/compiler/repr-color-gradient.png and b/tests/ref/compiler/repr-color-gradient.png differ diff --git a/tests/typ/compiler/color.typ b/tests/typ/compiler/color.typ index 71fce1b10..86dd15424 100644 --- a/tests/typ/compiler/color.typ +++ b/tests/typ/compiler/color.typ @@ -24,6 +24,7 @@ #box(square(size: 9pt, fill: col)) #box(square(size: 9pt, fill: rgb(col))) #box(square(size: 9pt, fill: oklab(col))) +#box(square(size: 9pt, fill: oklch(col))) #box(square(size: 9pt, fill: luma(col))) #box(square(size: 9pt, fill: cmyk(col))) #box(square(size: 9pt, fill: color.linear-rgb(col))) @@ -34,16 +35,19 @@ // Test hue rotation #let col = rgb(50%, 64%, 16%) +// Oklch #for x in range(0, 11) { box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg))) } +// HSL #for x in range(0, 11) { - box(square(size: 9pt, fill: color.hsv(col).rotate(x * 36deg))) + box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg, space: color.hsl))) } +// HSV #for x in range(0, 11) { - box(square(size: 9pt, fill: color.hsl(col).rotate(x * 36deg))) + box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg, space: color.hsv))) } --- diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ index f7e7ed8fc..0ae17ae78 100644 --- a/tests/typ/compiler/methods.typ +++ b/tests/typ/compiler/methods.typ @@ -108,6 +108,7 @@ #test-repr(luma(40).components(), (15.69%, )) #test-repr(cmyk(4%, 5%, 6%, 7%).components(), (4%, 5%, 6%, 7%)) #test-repr(oklab(10%, 0.2, 0.3).components(), (10%, 0.2, 0.3, 100%)) +#test-repr(oklch(10%, 0.2, 90deg).components(), (10%, 0.2, 90deg, 100%)) #test-repr(color.linear-rgb(10%, 20%, 30%).components(), (10%, 20%, 30%, 100%)) #test-repr(color.hsv(10deg, 20%, 30%).components(), (10deg, 20%, 30%, 100%)) #test-repr(color.hsl(10deg, 20%, 30%).components(), (10deg, 20%, 30%, 100%)) @@ -131,6 +132,9 @@ #test-repr(color.hsv(rgb(1, 2, 3)), color.hsv(-150deg, 66.67%, 1.18%)) #test-repr(oklab(luma(40)).components(), (27.68%, 0.0, 0.0, 100%)) #test-repr(oklab(rgb(1, 2, 3)).components(), (8.23%, -0.004, -0.007, 100%)) +#test-repr(oklch(oklab(40%, 0.2, 0.2)).components(), (40%, 0.283, 45deg, 100%)) +#test-repr(oklch(luma(40)).components(), (27.68%, 0.0, 72.49deg, 100%)) +#test-repr(oklch(rgb(1, 2, 3)).components(), (8.23%, 0.008, 240.75deg, 100%)) --- // Test gradient functions. @@ -143,6 +147,7 @@ #test(gradient.linear(red, green, blue, space: rgb).sample(100%), blue) #test(gradient.linear(red, green, space: rgb).space(), rgb) #test(gradient.linear(red, green, space: oklab).space(), oklab) +#test(gradient.linear(red, green, space: oklch).space(), oklch) #test(gradient.linear(red, green, space: cmyk).space(), cmyk) #test(gradient.linear(red, green, space: luma).space(), luma) #test(gradient.linear(red, green, space: color.linear-rgb).space(), color.linear-rgb) diff --git a/tests/typ/compiler/repr-color-gradient.typ b/tests/typ/compiler/repr-color-gradient.typ index d1c639c06..ef1589742 100644 --- a/tests/typ/compiler/repr-color-gradient.typ +++ b/tests/typ/compiler/repr-color-gradient.typ @@ -7,6 +7,7 @@ #blue \ #color.linear-rgb(blue) \ #oklab(blue) \ +#oklch(blue) \ #cmyk(blue) \ #color.hsl(blue) \ #color.hsv(blue) \ diff --git a/tests/typ/compute/construct.typ b/tests/typ/compute/construct.typ index 58693a49a..f6856c6f2 100644 --- a/tests/typ/compute/construct.typ +++ b/tests/typ/compute/construct.typ @@ -37,6 +37,7 @@ #test(rgb(rgb(10, 20, 30)).space(), rgb) #test(color.linear-rgb(rgb(10, 20, 30)).space(), color.linear-rgb) #test(oklab(rgb(10, 20, 30)).space(), oklab) +#test(oklch(rgb(10, 20, 30)).space(), oklch) #test(color.hsl(rgb(10, 20, 30)).space(), color.hsl) #test(color.hsv(rgb(10, 20, 30)).space(), color.hsv) #test(cmyk(rgb(10, 20, 30)).space(), cmyk) @@ -45,6 +46,7 @@ #test(rgb(color.linear-rgb(10, 20, 30)).space(), rgb) #test(color.linear-rgb(color.linear-rgb(10, 20, 30)).space(), color.linear-rgb) #test(oklab(color.linear-rgb(10, 20, 30)).space(), oklab) +#test(oklch(color.linear-rgb(10, 20, 30)).space(), oklch) #test(color.hsl(color.linear-rgb(10, 20, 30)).space(), color.hsl) #test(color.hsv(color.linear-rgb(10, 20, 30)).space(), color.hsv) #test(cmyk(color.linear-rgb(10, 20, 30)).space(), cmyk) @@ -53,14 +55,25 @@ #test(rgb(oklab(10%, 20%, 30%)).space(), rgb) #test(color.linear-rgb(oklab(10%, 20%, 30%)).space(), color.linear-rgb) #test(oklab(oklab(10%, 20%, 30%)).space(), oklab) +#test(oklch(oklab(10%, 20%, 30%)).space(), oklch) #test(color.hsl(oklab(10%, 20%, 30%)).space(), color.hsl) #test(color.hsv(oklab(10%, 20%, 30%)).space(), color.hsv) #test(cmyk(oklab(10%, 20%, 30%)).space(), cmyk) #test(luma(oklab(10%, 20%, 30%)).space(), luma) +#test(rgb(oklch(60%, 40%, 0deg)).space(), rgb) +#test(color.linear-rgb(oklch(60%, 40%, 0deg)).space(), color.linear-rgb) +#test(oklab(oklch(60%, 40%, 0deg)).space(), oklab) +#test(oklch(oklch(60%, 40%, 0deg)).space(), oklch) +#test(color.hsl(oklch(60%, 40%, 0deg)).space(), color.hsl) +#test(color.hsv(oklch(60%, 40%, 0deg)).space(), color.hsv) +#test(cmyk(oklch(60%, 40%, 0deg)).space(), cmyk) +#test(luma(oklch(60%, 40%, 0deg)).space(), luma) + #test(rgb(color.hsl(10deg, 20%, 30%)).space(), rgb) #test(color.linear-rgb(color.hsl(10deg, 20%, 30%)).space(), color.linear-rgb) #test(oklab(color.hsl(10deg, 20%, 30%)).space(), oklab) +#test(oklch(color.hsl(10deg, 20%, 30%)).space(), oklch) #test(color.hsl(color.hsl(10deg, 20%, 30%)).space(), color.hsl) #test(color.hsv(color.hsl(10deg, 20%, 30%)).space(), color.hsv) #test(cmyk(color.hsl(10deg, 20%, 30%)).space(), cmyk) @@ -69,6 +82,7 @@ #test(rgb(color.hsv(10deg, 20%, 30%)).space(), rgb) #test(color.linear-rgb(color.hsv(10deg, 20%, 30%)).space(), color.linear-rgb) #test(oklab(color.hsv(10deg, 20%, 30%)).space(), oklab) +#test(oklch(color.hsv(10deg, 20%, 30%)).space(), oklch) #test(color.hsl(color.hsv(10deg, 20%, 30%)).space(), color.hsl) #test(color.hsv(color.hsv(10deg, 20%, 30%)).space(), color.hsv) #test(cmyk(color.hsv(10deg, 20%, 30%)).space(), cmyk) @@ -77,6 +91,7 @@ #test(rgb(cmyk(10%, 20%, 30%, 40%)).space(), rgb) #test(color.linear-rgb(cmyk(10%, 20%, 30%, 40%)).space(), color.linear-rgb) #test(oklab(cmyk(10%, 20%, 30%, 40%)).space(), oklab) +#test(oklch(cmyk(10%, 20%, 30%, 40%)).space(), oklch) #test(color.hsl(cmyk(10%, 20%, 30%, 40%)).space(), color.hsl) #test(color.hsv(cmyk(10%, 20%, 30%, 40%)).space(), color.hsv) #test(cmyk(cmyk(10%, 20%, 30%, 40%)).space(), cmyk) @@ -85,6 +100,7 @@ #test(rgb(luma(10%)).space(), rgb) #test(color.linear-rgb(luma(10%)).space(), color.linear-rgb) #test(oklab(luma(10%)).space(), oklab) +#test(oklch(luma(10%)).space(), oklch) #test(color.hsl(luma(10%)).space(), color.hsl) #test(color.hsv(luma(10%)).space(), color.hsv) #test(cmyk(luma(10%)).space(), cmyk) @@ -130,15 +146,15 @@ #color.mix((red, 1, 2)) --- -// Error: 31-38 expected `rgb`, `luma`, `cmyk`, `oklab`, `color.linear-rgb`, `color.hsl`, or `color.hsv`, found string +// Error: 31-38 expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv`, found string #color.mix(red, green, space: "cyber") --- -// Error: 31-36 expected `rgb`, `luma`, `cmyk`, `oklab`, `color.linear-rgb`, `color.hsl`, or `color.hsv` +// Error: 31-36 expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv` #color.mix(red, green, space: image) --- -// Error: 31-41 expected `rgb`, `luma`, `cmyk`, `oklab`, `color.linear-rgb`, `color.hsl`, or `color.hsv` +// Error: 31-41 expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv` #color.mix(red, green, space: calc.round) ---