diff --git a/src/color.rs b/src/color.rs index 95e5d3b..ccc99c8 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,7 +1,14 @@ use std::sync::LazyLock; use wide::f64x4; -pub trait Color {} +pub trait Color { + /// Computes the Euclidean distance between `self` and `other`. + /// + /// This method finds the difference of each component, squares each + /// difference, sums the squares, then takes the square root of the + /// sum. + fn euclidian_distance(&self, other: &Self) -> f64; +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Rgb { @@ -10,7 +17,36 @@ pub struct Rgb { pub b: u8, } -impl Color for Rgb {} +impl Color for Rgb { + fn euclidian_distance(&self, other: &Self) -> f64 { + let diff_r = (self.r as f64) - (other.r as f64); + let diff_g = (self.g as f64) - (other.g as f64); + let diff_b = (self.b as f64) - (other.b as f64); + (diff_r * diff_r + diff_g * diff_g + diff_b * diff_b).sqrt() + } +} + +impl Rgb { + /// Blends `self` and `other` by `alpha`. + /// + /// This method finds a color between `self` and `other`. An `alpha` + /// of `0.0` returns `self`. An `alpha` of `1.0` returns `other`. A + /// value between `0.0` and `1.0` mixes the two colors in that + /// proportion. + /// + /// The name `lerp` is short for "linear interpolation," a common term + /// in graphics code. + pub fn lerp(&self, other: &Rgb, alpha: f64) -> Rgb { + let r = (1.0 - alpha) * self.r as f64 + alpha * other.r as f64; + let g = (1.0 - alpha) * self.g as f64 + alpha * other.g as f64; + let b = (1.0 - alpha) * self.b as f64 + alpha * other.b as f64; + Rgb { + r: r as u8, + g: g as u8, + b: b as u8, + } + } +} #[derive(Debug, Clone, Copy, PartialEq)] pub struct Lab { @@ -19,7 +55,14 @@ pub struct Lab { pub b: f64, } -impl Color for Lab {} +impl Color for Lab { + fn euclidian_distance(&self, other: &Self) -> f64 { + let diff_l = self.l - other.l; + let diff_a = self.a - other.a; + let diff_b = self.b - other.b; + (diff_l * diff_l + diff_a * diff_a + diff_b * diff_b).sqrt() + } +} /// Constants for the sRGB transfer function. `srgb_to_linear` uses these /// to convert a normalized (`0.0..=1.0`) sRGB channel value to linear-light @@ -264,6 +307,16 @@ impl RgbPlanar { } } +impl From<&[Rgb]> for RgbPlanar { + fn from(value: &[Rgb]) -> Self { + // let collected: Vec<(u8, u8, u8)> = value.iter().map(|rgb| (rgb.r, rgb.g, rgb.b)).collect(); + let r: Vec = value.iter().map(|rgb| rgb.r as f64).collect(); + let g: Vec = value.iter().map(|rgb| rgb.g as f64).collect(); + let b: Vec = value.iter().map(|rgb| rgb.b as f64).collect(); + Self { r, g, b } + } +} + impl PlanarColor for RgbPlanar { fn len(&self) -> usize { self.r.len() diff --git a/src/palette.rs b/src/palette.rs deleted file mode 100644 index 15d77b8..0000000 --- a/src/palette.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::color::Rgb; - -pub const NORD_PALETTE: [Rgb; 16] = [ - Rgb {r: 46, g: 52, b: 64,}, // nord0 - Rgb {r: 59, g: 66, b: 82,}, // nord1 - Rgb {r: 67, g: 76, b: 94,}, // nord2 - Rgb {r: 76, g: 86, b: 106,}, // nord3 - Rgb {r: 216, g: 222, b: 233,}, // nord4 - Rgb {r: 229, g: 233, b: 240,}, // nord5 - Rgb {r: 236, g: 239, b: 244,}, // nord6 - Rgb {r: 143, g: 188, b: 187,}, // nord7 - Rgb {r: 136, g: 192, b: 208,}, // nord8 - Rgb {r: 129, g: 161, b: 193,}, // nord9 - Rgb {r: 94, g: 129, b: 172,}, // nord10 - Rgb {r: 191, g: 97, b: 106,}, // nord11 - Rgb {r: 208, g: 135, b: 112,}, // nord12 - Rgb {r: 235, g: 203, b: 139,}, // nord13 - Rgb {r: 163, g: 190, b: 140,}, // nord14 - Rgb {r: 180, g: 142, b: 173,}, // nord15 -]; diff --git a/src/palette/mod.rs b/src/palette/mod.rs new file mode 100644 index 0000000..baa4aaf --- /dev/null +++ b/src/palette/mod.rs @@ -0,0 +1,89 @@ +use crate::color::{Color, LabPlanar, Rgb, RgbPlanar}; + +mod nord; +pub use nord::NORD_PALETTE; + +pub trait Expansion {} +pub struct CanExpand; +pub struct CannotExpand; +impl Expansion for CanExpand {} +impl Expansion for CannotExpand {} + +pub struct Palette { + colors: Vec, + rgb_planar: RgbPlanar, + lab_planar: LabPlanar, + phantom: std::marker::PhantomData, +} + +impl Palette { + pub fn new(colors: Vec) -> Self { + let rgb_planar: RgbPlanar = (&(*colors)).into(); + let lab_planar: LabPlanar = (&rgb_planar).into(); + Self { + colors, + rgb_planar, + lab_planar, + phantom: std::marker::PhantomData, + } + } + + pub fn colors(&self) -> &[Rgb] { + &self.colors + } + + pub fn len(&self) -> usize { + self.colors.len() + } + + pub fn is_empty(&self) -> bool { + self.colors.is_empty() + } + + pub(crate) fn rgb_planar(&self) -> &RgbPlanar { + &self.rgb_planar + } + + pub(crate) fn lab_planar(&self) -> &LabPlanar { + &self.lab_planar + } +} + +/// The max distance between two colors before `Palette::expand` treats +/// them as too different to interpolate. +/// +/// `expand` measures distance with `Color::euclidian_distance`. It skips +/// a pair of colors when their distance is at or above this value. This +/// value matches the threshold in the `nordify` Python reference +/// implementation. +const EXPANSION_DISTANCE_THRESHOLD: f64 = 150.0; + +impl Palette { + /// Builds a new palette that includes every color from `self`, + /// plus extra colors interpolated between close pairs. + /// + /// This method compares every pair of colors in the palette. If + /// the distance between a pair is less than + /// `EXPANSION_DISTANCE_THRESHOLD`, the method adds + /// `expansion_factor - 1` new colors between them. Each new color + /// is a linear interpolation of the pair, spaced evenly from one + /// color to the other. + /// + /// A low `expansion_factor` value (`0` or `1`) adds no new colors. + #[must_use] + pub fn expand(&self, expansion_factor: usize) -> Palette { + let mut colors = self.colors.clone(); + for (i, color1) in self.colors.iter().enumerate() { + for color2 in self.colors.iter().skip(i + 1) { + let distance = color1.euclidian_distance(color2); + if distance < EXPANSION_DISTANCE_THRESHOLD { + for step in 1..expansion_factor { + let alpha = step as f64 / expansion_factor as f64; + colors.push(color1.lerp(color2, alpha)); + } + } + } + } + Palette::::new(colors) + } +} diff --git a/src/palette/nord.rs b/src/palette/nord.rs new file mode 100644 index 0000000..30af44a --- /dev/null +++ b/src/palette/nord.rs @@ -0,0 +1,84 @@ +use crate::color::Rgb; + +pub const NORD_PALETTE: [Rgb; 16] = [ + Rgb { + r: 46, + g: 52, + b: 64, + }, // nord0 + Rgb { + r: 59, + g: 66, + b: 82, + }, // nord1 + Rgb { + r: 67, + g: 76, + b: 94, + }, // nord2 + Rgb { + r: 76, + g: 86, + b: 106, + }, // nord3 + Rgb { + r: 216, + g: 222, + b: 233, + }, // nord4 + Rgb { + r: 229, + g: 233, + b: 240, + }, // nord5 + Rgb { + r: 236, + g: 239, + b: 244, + }, // nord6 + Rgb { + r: 143, + g: 188, + b: 187, + }, // nord7 + Rgb { + r: 136, + g: 192, + b: 208, + }, // nord8 + Rgb { + r: 129, + g: 161, + b: 193, + }, // nord9 + Rgb { + r: 94, + g: 129, + b: 172, + }, // nord10 + Rgb { + r: 191, + g: 97, + b: 106, + }, // nord11 + Rgb { + r: 208, + g: 135, + b: 112, + }, // nord12 + Rgb { + r: 235, + g: 203, + b: 139, + }, // nord13 + Rgb { + r: 163, + g: 190, + b: 140, + }, // nord14 + Rgb { + r: 180, + g: 142, + b: 173, + }, // nord15 +];