Skip to content

Commit 3d9650b

Browse files
committed
Improve color keys
1 parent 43345bd commit 3d9650b

5 files changed

Lines changed: 205 additions & 66 deletions

File tree

Cargo.lock

Lines changed: 112 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dirs = "6.0.0"
1313
fast_image_resize = { version = "6.0.0", features = ["image", "rayon"] }
1414
image = "0.25.9"
1515
nix = { version = "0.31.1", features = ["process"] }
16+
palette = "0.7.6"
1617
rexiv2 = { version = "0.10.0" }
1718
serde = { version = "1.0.228", features = ["derive"] }
1819
serde_json = "1.0.149"

src/colors.rs

Lines changed: 65 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
use std::collections::HashMap;
22

3-
use image::Rgba;
3+
use image::{Pixel, Rgba};
4+
use palette::{IntoColor, Lab, Srgb, color_difference::EuclideanDistance};
45

56
use crate::{WFetchResult, full_path};
67

7-
fn normalize_channel(channel: u8) -> f64 {
8-
let channel = f64::from(channel) / 255.0;
9-
if channel <= 0.03928 {
10-
channel / 12.92
11-
} else {
12-
((channel + 0.055) / 1.055).powf(2.4)
13-
}
14-
}
15-
168
pub type Rgba8 = Rgba<u8>;
179
pub const BLACK: Rgba8 = Rgba([0, 0, 0, 255]);
1810
pub const WHITE: Rgba8 = Rgba([255, 255, 255, 255]);
@@ -24,6 +16,8 @@ pub trait Rgba8Ext {
2416
where
2517
Self: Sized;
2618

19+
fn to_lab(&self) -> Lab;
20+
2721
#[must_use]
2822
fn with_alpha(self, alpha: u8) -> Self;
2923

@@ -32,10 +26,6 @@ pub trait Rgba8Ext {
3226
#[must_use]
3327
fn multiply(&self, other: Self) -> Self;
3428

35-
fn relative_luminance(&self) -> f64;
36-
37-
fn contrast_ratio(&self, other: &Self) -> f64;
38-
3929
/// ansi color code for terminal background in a format suitable for fastfetch
4030
fn term_fg(&self) -> String;
4131

@@ -73,36 +63,16 @@ impl Rgba8Ext for Rgba8 {
7363
Self([self[0], self[1], self[2], alpha])
7464
}
7565

66+
fn to_lab(&self) -> Lab {
67+
Srgb::new(self[0], self[1], self[2])
68+
.into_format::<f32>()
69+
.into_color()
70+
}
71+
7672
#[allow(clippy::cast_possible_truncation)]
7773
#[allow(clippy::cast_sign_loss)]
7874
fn multiply(&self, other: Self) -> Self {
79-
Self([
80-
(f64::from(self[0]) * f64::from(other[0]) / 255.0) as u8,
81-
(f64::from(self[1]) * f64::from(other[1]) / 255.0) as u8,
82-
(f64::from(self[2]) * f64::from(other[2]) / 255.0) as u8,
83-
self[3],
84-
])
85-
}
86-
87-
/// relative luminance, as defined by WCAG
88-
/// <https://www.w3.org/TR/WCAG20/#relativeluminancedef>
89-
fn relative_luminance(&self) -> f64 {
90-
let r = normalize_channel(self[0]);
91-
let g = normalize_channel(self[1]);
92-
let b = normalize_channel(self[2]);
93-
94-
0.0722_f64.mul_add(b, 0.2126_f64.mul_add(r, 0.7152 * g))
95-
}
96-
97-
fn contrast_ratio(&self, other: &Self) -> f64 {
98-
let l1 = self.relative_luminance();
99-
let l2 = other.relative_luminance();
100-
101-
if l1 > l2 {
102-
(l1 + 0.05) / (l2 + 0.05)
103-
} else {
104-
(l2 + 0.05) / (l1 + 0.05)
105-
}
75+
self.map2(&other, |a, b| (f64::from(a) * f64::from(b) / 255.0) as u8)
10676
}
10777

10878
fn term_fg(&self) -> String {
@@ -114,33 +84,71 @@ impl Rgba8Ext for Rgba8 {
11484
}
11585
}
11686

117-
fn color_pair_score(color1: Rgba8, color2: Rgba8) -> f64 {
118-
color1.contrast_ratio(&color2)
119-
+ color1.contrast_ratio(&BLACK)
120-
+ color2.contrast_ratio(&BLACK)
121-
+ color1.contrast_ratio(&WHITE)
122-
+ color2.contrast_ratio(&WHITE)
123-
}
87+
/// find the most contrasting n colors in a list
88+
pub fn most_contrasting_colors(colors: &[Rgba<u8>], n: usize) -> Vec<Rgba<u8>> {
89+
let colors: HashMap<_, _> = colors.iter().map(|c| (c, c.to_lab())).collect();
90+
let mut unique_colors: Vec<Lab> = Vec::new();
91+
92+
for lab in colors.values() {
93+
// Only keep the color if it's far enough from all current unique colors
94+
// deltaE 2.3 is barely perceptible, 10.0 is significantly different
95+
if unique_colors.iter().all(|c| lab.distance(*c) > 10.0) {
96+
unique_colors.push(*lab);
97+
}
98+
}
12499

125-
/// find the most contrasting pair of colors in a list
126-
pub fn most_contrasting_pair(colors: &[Rgba8]) -> (Rgba8, Rgba8) {
127100
let mut max_score = 0.0;
128-
let mut most_contrasting_pair = (image::Rgba([0, 0, 0, 255]), image::Rgba([0, 0, 0, 255]));
101+
let mut pair = (Lab::default(), Lab::default());
129102

130-
for color1 in colors {
131-
for color2 in colors {
132-
if color1 == color2 {
103+
for c1 in &unique_colors {
104+
for c2 in &unique_colors {
105+
if c1 == c2 {
133106
continue;
134107
}
135108

136-
let score = color_pair_score(*color1, *color2);
109+
// lab distance
110+
let score = c1.distance(*c2);
137111
if score > max_score {
138112
max_score = score;
139-
most_contrasting_pair = (*color1, *color2);
113+
pair = (*c1, *c2);
140114
}
141115
}
142116
}
143-
most_contrasting_pair
117+
118+
let mut selected = vec![pair.0, pair.1];
119+
120+
for _ in 2..n {
121+
let min = unique_colors
122+
.iter()
123+
.min_by(|a, b| {
124+
let a_dist: f32 = selected
125+
.iter()
126+
.filter(|sel| sel != a && sel != b)
127+
.map(|sel| a.distance(*sel))
128+
.sum();
129+
let b_dist: f32 = selected
130+
.iter()
131+
.filter(|sel| sel != a && sel != b)
132+
.map(|sel| b.distance(*sel))
133+
.sum();
134+
135+
a_dist.total_cmp(&b_dist)
136+
})
137+
.expect("no min");
138+
139+
selected.push(*min);
140+
}
141+
142+
selected
143+
.iter()
144+
.map(|sel| {
145+
**colors
146+
.iter()
147+
.find(|(_, l)| *l == sel)
148+
.expect("could not find lab color equivalent")
149+
.0
150+
})
151+
.collect()
144152
}
145153

146154
#[derive(serde::Deserialize)]

0 commit comments

Comments
 (0)