Implement Oklch support (#2611)
This commit is contained in:
parent
7f0fcda376
commit
d93ed1b3d8
@ -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})",
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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!(
|
||||
|
@ -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<f32>;
|
||||
pub type Oklch = palette::oklch::Oklcha<f32>;
|
||||
pub type LinearRgba = palette::rgb::Rgba<Linear<encoding::Srgb>, f32>;
|
||||
pub type Rgba = palette::rgb::Rgba<encoding::Srgb, f32>;
|
||||
pub type Hsl = palette::hsl::Hsla<encoding::Srgb, f32>;
|
||||
@ -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<Oklab> for Color {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oklch> for Color {
|
||||
fn from(c: Oklch) -> Self {
|
||||
Self::Oklch(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> 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<Color> {
|
||||
Ok(if let Some(color) = args.find::<Color>()? {
|
||||
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<Color> {
|
||||
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() {
|
||||
|
@ -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),
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 49 KiB |
@ -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)))
|
||||
}
|
||||
|
||||
---
|
||||
|
@ -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)
|
||||
|
@ -7,6 +7,7 @@
|
||||
#blue \
|
||||
#color.linear-rgb(blue) \
|
||||
#oklab(blue) \
|
||||
#oklch(blue) \
|
||||
#cmyk(blue) \
|
||||
#color.hsl(blue) \
|
||||
#color.hsv(blue) \
|
||||
|
@ -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)
|
||||
|
||||
---
|
||||
|
Loading…
Reference in New Issue
Block a user