diff --git a/Cargo.lock b/Cargo.lock index 33210bf2..143d8771 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2527,7 +2527,6 @@ dependencies = [ "lilt", "log", "num-traits", - "palette", "rustc-hash 2.1.1", "smol_str", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index 7e8bbc34..056f8a7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -164,7 +164,6 @@ lyon = "1.0" lyon_path = "1.0" num-traits = "0.2" ouroboros = "0.18" -palette = "0.7" png = "0.17" pulldown-cmark = "0.12" qrcode = { version = "0.13", default-features = false } diff --git a/core/Cargo.toml b/core/Cargo.toml index 1ca7260c..60920e46 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -24,7 +24,6 @@ glam.workspace = true lilt.workspace = true log.workspace = true num-traits.workspace = true -palette.workspace = true rustc-hash.workspace = true smol_str.workspace = true thiserror.workspace = true diff --git a/core/src/color.rs b/core/src/color.rs index a9183776..37a8921d 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,5 +1,3 @@ -use palette::rgb::{Srgb, Srgba}; - /// A color in the `sRGB` color space. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Color { @@ -242,70 +240,9 @@ macro_rules! color { }}; } -/// Converts from palette's `Rgba` type to a [`Color`]. -impl From for Color { - fn from(rgba: Srgba) -> Self { - Color::new(rgba.red, rgba.green, rgba.blue, rgba.alpha) - } -} - -/// Converts from [`Color`] to palette's `Rgba` type. -impl From for Srgba { - fn from(c: Color) -> Self { - Srgba::new(c.r, c.g, c.b, c.a) - } -} - -/// Converts from palette's `Rgb` type to a [`Color`]. -impl From for Color { - fn from(rgb: Srgb) -> Self { - Color::new(rgb.red, rgb.green, rgb.blue, 1.0) - } -} - -/// Converts from [`Color`] to palette's `Rgb` type. -impl From for Srgb { - fn from(c: Color) -> Self { - Srgb::new(c.r, c.g, c.b) - } -} - #[cfg(test)] mod tests { use super::*; - use palette::blend::Blend; - - #[test] - fn srgba_traits() { - let c = Color::from_rgb(0.5, 0.4, 0.3); - // Round-trip conversion to the palette::Srgba type - let s: Srgba = c.into(); - let r: Color = s.into(); - assert_eq!(c, r); - } - - #[test] - fn color_manipulation() { - use approx::assert_relative_eq; - - let c1 = Color::from_rgb(0.5, 0.4, 0.3); - let c2 = Color::from_rgb(0.2, 0.5, 0.3); - - // Convert to linear color for manipulation - let l1 = Srgba::from(c1).into_linear(); - let l2 = Srgba::from(c2).into_linear(); - - // Take the lighter of each of the sRGB components - let lighter = l1.lighten(l2); - - // Convert back to our Color - let result: Color = Srgba::from_linear(lighter).into(); - - assert_relative_eq!(result.r, 0.5); - assert_relative_eq!(result.g, 0.5); - assert_relative_eq!(result.b, 0.3); - assert_relative_eq!(result.a, 1.0); - } #[test] fn parse() { diff --git a/core/src/theme/palette.rs b/core/src/theme/palette.rs index faffee8b..7be5a98e 100644 --- a/core/src/theme/palette.rs +++ b/core/src/theme/palette.rs @@ -1,10 +1,6 @@ //! Define the colors of a theme. use crate::{Color, color}; -use palette::color_difference::Wcag21RelativeContrast; -use palette::rgb::Rgb; -use palette::{FromColor, Hsl, Mix}; - use std::sync::LazyLock; /// A color palette. @@ -607,13 +603,20 @@ impl Danger { } } +struct Hsl { + h: f32, + s: f32, + l: f32, + a: f32, +} + fn darken(color: Color, amount: f32) -> Color { let mut hsl = to_hsl(color); - hsl.lightness = if hsl.lightness - amount < 0.0 { + hsl.l = if hsl.l - amount < 0.0 { 0.0 } else { - hsl.lightness - amount + hsl.l - amount }; from_hsl(hsl) @@ -622,10 +625,10 @@ fn darken(color: Color, amount: f32) -> Color { fn lighten(color: Color, amount: f32) -> Color { let mut hsl = to_hsl(color); - hsl.lightness = if hsl.lightness + amount > 1.0 { + hsl.l = if hsl.l + amount > 1.0 { 1.0 } else { - hsl.lightness + amount + hsl.l + amount }; from_hsl(hsl) @@ -642,17 +645,24 @@ fn deviate(color: Color, amount: f32) -> Color { fn muted(color: Color) -> Color { let mut hsl = to_hsl(color); - hsl.saturation = hsl.saturation.min(0.5); + hsl.s = hsl.s.min(0.5); from_hsl(hsl) } fn mix(a: Color, b: Color, factor: f32) -> Color { - let a_lin = Rgb::from(a).into_linear(); - let b_lin = Rgb::from(b).into_linear(); + let b_amount = factor.clamp(0.0, 1.0); + let a_amount = 1.0 - b_amount; - let mixed = a_lin.mix(b_lin, factor); - Rgb::from_linear(mixed).into() + let a_linear = a.into_linear().map(|c| c * a_amount); + let b_linear = b.into_linear().map(|c| c * b_amount); + + Color::from_linear_rgba( + a_linear[0] + b_linear[0], + a_linear[1] + b_linear[1], + a_linear[2] + b_linear[2], + a_linear[3] + b_linear[3], + ) } fn readable(background: Color, text: Color) -> Color { @@ -680,27 +690,85 @@ fn readable(background: Color, text: Color) -> Color { } fn is_dark(color: Color) -> bool { - to_hsl(color).lightness < 0.6 + to_hsl(color).l < 0.6 } fn is_readable(a: Color, b: Color) -> bool { - let a_srgb = Rgb::from(a); - let b_srgb = Rgb::from(b); - - a_srgb.has_enhanced_contrast_text(b_srgb) + relative_contrast(a, b) >= 7.0 } +// https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio fn relative_contrast(a: Color, b: Color) -> f32 { - let a_srgb = Rgb::from(a); - let b_srgb = Rgb::from(b); - - a_srgb.relative_contrast(b_srgb) + let lum_a = relative_luminance(a); + let lum_b = relative_luminance(b); + (lum_a.max(lum_b) + 0.05) / (lum_a.min(lum_b) + 0.05) } +// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance +fn relative_luminance(color: Color) -> f32 { + let linear = color.into_linear(); + 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2] +} + +// https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB fn to_hsl(color: Color) -> Hsl { - Hsl::from_color(Rgb::from(color)) + let x_max = color.r.max(color.g).max(color.b); + let x_min = color.r.min(color.g).min(color.b); + let c = x_max - x_min; + let l = x_max.midpoint(x_min); + + let h = if c == 0.0 { + 0.0 + } else if x_max == color.r { + 60.0 * ((color.g - color.b) / c).rem_euclid(6.0) + } else if x_max == color.g { + 60.0 * (((color.b - color.r) / c) + 2.0) + } else { + // x_max == color.b + 60.0 * (((color.r - color.g) / c) + 4.0) + }; + + let s = if l == 0.0 || l == 1.0 { + 0.0 + } else { + (x_max - l) / l.min(1.0 - l) + }; + + Hsl { + h, + s, + l, + a: color.a, + } } +// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB fn from_hsl(hsl: Hsl) -> Color { - Rgb::from_color(hsl).into() + let c = (1.0 - (2.0 * hsl.l - 1.0).abs()) * hsl.s; + let h = hsl.h / 60.0; + let x = c * (1.0 - (h.rem_euclid(2.0) - 1.0).abs()); + + let (r1, g1, b1) = if h < 1.0 { + (c, x, 0.0) + } else if h < 2.0 { + (x, c, 0.0) + } else if h < 3.0 { + (0.0, c, x) + } else if h < 4.0 { + (0.0, x, c) + } else if h < 5.0 { + (x, 0.0, c) + } else { + // h < 6.0 + (c, 0.0, x) + }; + + let m = hsl.l - (c / 2.0); + + Color { + r: r1 + m, + g: g1 + m, + b: b1 + m, + a: hsl.a, + } } diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml index 27d67f55..efa744b0 100644 --- a/examples/color_palette/Cargo.toml +++ b/examples/color_palette/Cargo.toml @@ -9,4 +9,4 @@ publish = false iced.workspace = true iced.features = ["canvas"] -palette.workspace = true +palette = "0.7" diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index b11c8a2b..2662d063 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -46,7 +46,7 @@ pub enum Message { impl ColorPalette { fn update(&mut self, message: Message) { let srgb = match message { - Message::RgbColorChanged(rgb) => Rgb::from(rgb), + Message::RgbColorChanged(rgb) => to_rgb(rgb), Message::HslColorChanged(hsl) => Rgb::from_color(hsl), Message::HsvColorChanged(hsv) => Rgb::from_color(hsv), Message::HwbColorChanged(hwb) => Rgb::from_color(hwb), @@ -54,13 +54,13 @@ impl ColorPalette { Message::LchColorChanged(lch) => Rgb::from_color(lch), }; - self.theme = Theme::new(srgb); + self.theme = Theme::new(to_color(srgb)); } fn view(&self) -> Element { let base = self.theme.base; - let srgb = Rgb::from(base); + let srgb = to_rgb(base); let hsl = palette::Hsl::from_color(srgb); let hsv = palette::Hsv::from_color(srgb); let hwb = palette::Hwb::from_color(srgb); @@ -109,7 +109,7 @@ impl Theme { let base = base.into(); // Convert to HSL color for manipulation - let hsl = Hsl::from_color(Rgb::from(base)); + let hsl = Hsl::from_color(to_rgb(base)); let lower = [ hsl.shift_hue(-135.0).lighten(0.075), @@ -128,12 +128,12 @@ impl Theme { Theme { lower: lower .iter() - .map(|&color| Rgb::from_color(color).into()) + .map(|&color| to_color(Rgb::from_color(color))) .collect(), base, higher: higher .iter() - .map(|&color| Rgb::from_color(color).into()) + .map(|&color| to_color(Rgb::from_color(color))) .collect(), canvas_cache: canvas::Cache::default(), } @@ -216,14 +216,14 @@ impl Theme { text.align_y = alignment::Vertical::Bottom; - let hsl = Hsl::from_color(Rgb::from(self.base)); + let hsl = Hsl::from_color(to_rgb(self.base)); for i in 0..self.len() { let pct = (i as f32 + 1.0) / (self.len() as f32 + 1.0); let graded = Hsl { lightness: 1.0 - pct, ..hsl }; - let color: Color = Rgb::from_color(graded).into(); + let color: Color = to_color(Rgb::from_color(graded)); let anchor = Point { x: (i as f32) * box_size.width, @@ -475,3 +475,21 @@ impl ColorSpace for palette::Lch { ) } } + +fn to_rgb(color: Color) -> Rgb { + Rgb { + red: color.r, + green: color.g, + blue: color.b, + ..Rgb::default() + } +} + +fn to_color(rgb: Rgb) -> Color { + Color { + r: rgb.red, + g: rgb.green, + b: rgb.blue, + a: 1.0, + } +}