11use std:: collections:: HashMap ;
22
3- use image:: Rgba ;
3+ use image:: { Pixel , Rgba } ;
4+ use palette:: { IntoColor , Lab , Srgb , color_difference:: EuclideanDistance } ;
45
56use 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-
168pub type Rgba8 = Rgba < u8 > ;
179pub const BLACK : Rgba8 = Rgba ( [ 0 , 0 , 0 , 255 ] ) ;
1810pub 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