feat(palette): add palette expansion via color interpolation
Add `Color::euclidian_distance` and `Rgb::lerp`, then use them in a new `Palette<T>` type whose `expand` method adds colors between close pairs. Split `palette.rs` into a module, with `NORD_PALETTE` in its own file.
This commit is contained in:
+56
-3
@@ -1,7 +1,14 @@
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use wide::f64x4;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct Rgb {
|
pub struct Rgb {
|
||||||
@@ -10,7 +17,36 @@ pub struct Rgb {
|
|||||||
pub b: u8,
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Lab {
|
pub struct Lab {
|
||||||
@@ -19,7 +55,14 @@ pub struct Lab {
|
|||||||
pub b: f64,
|
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
|
/// 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
|
/// 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<f64> = value.iter().map(|rgb| rgb.r as f64).collect();
|
||||||
|
let g: Vec<f64> = value.iter().map(|rgb| rgb.g as f64).collect();
|
||||||
|
let b: Vec<f64> = value.iter().map(|rgb| rgb.b as f64).collect();
|
||||||
|
Self { r, g, b }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PlanarColor<Rgb> for RgbPlanar {
|
impl PlanarColor<Rgb> for RgbPlanar {
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.r.len()
|
self.r.len()
|
||||||
|
|||||||
@@ -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
|
|
||||||
];
|
|
||||||
@@ -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<T: Expansion> {
|
||||||
|
colors: Vec<Rgb>,
|
||||||
|
rgb_planar: RgbPlanar,
|
||||||
|
lab_planar: LabPlanar,
|
||||||
|
phantom: std::marker::PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Expansion> Palette<T> {
|
||||||
|
pub fn new(colors: Vec<Rgb>) -> 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<CanExpand> {
|
||||||
|
/// 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<CannotExpand> {
|
||||||
|
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::<CannotExpand>::new(colors)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user