@@ -10,7 +10,6 @@ pub struct ColorQuiz {
1010}
1111
1212impl ColorQuiz {
13- /// Generate a random color quiz
1413 pub fn generate < R : Rng > ( rng : & mut R ) -> Self {
1514 Self {
1615 r : rng. gen_range ( 0 ..=255 ) ,
@@ -19,7 +18,6 @@ impl ColorQuiz {
1918 }
2019 }
2120
22- /// Generate a 16:9 image (640x360) with the color
2321 pub fn generate_image ( & self ) -> Result < Vec < u8 > , Box < dyn std:: error:: Error > > {
2422 let width = 640u32 ;
2523 let height = 360u32 ;
@@ -33,7 +31,6 @@ impl ColorQuiz {
3331
3432 let mut writer = encoder. write_header ( ) ?;
3533
36- // Create image data - each pixel is RGB (3 bytes)
3734 let pixel_count = ( width * height) as usize ;
3835 let mut data = Vec :: with_capacity ( pixel_count * 3 ) ;
3936
@@ -49,24 +46,14 @@ impl ColorQuiz {
4946 Ok ( buffer. into_inner ( ) )
5047 }
5148
52- /// Validate user's answer in hex or oklch format
5349 pub fn validate_answer ( & self , user_answer : & str ) -> bool {
5450 let answer = user_answer. trim ( ) ;
5551
56- // Try hex format first
57- if answer. starts_with ( '#' ) {
58- return self . validate_hex ( answer) ;
52+ if !answer. starts_with ( '#' ) {
53+ return false ;
5954 }
6055
61- // Try oklch format
62- self . validate_oklch ( answer)
63- }
64-
65- /// Validate hex color format: #RRGGBB
66- /// Tolerance: total difference of 20 across all channels
67- fn validate_hex ( & self , hex : & str ) -> bool {
68- // Remove the # and parse
69- let hex = hex. trim_start_matches ( '#' ) ;
56+ let hex = answer. trim_start_matches ( '#' ) ;
7057
7158 if hex. len ( ) != 6 {
7259 return false ;
@@ -81,115 +68,11 @@ impl ColorQuiz {
8168 + ( self . g as i32 - g as i32 ) . abs ( )
8269 + ( self . b as i32 - b as i32 ) . abs ( ) ;
8370
84- return diff <= 20 ;
71+ return diff <= 25 ;
8572 }
8673
8774 false
8875 }
89-
90- /// Validate oklch color format
91- /// Supports: oklch(45.0% 0.306 65.4), 45.0% 0.306 65.4, 45.0%, 0.306, 65.4
92- /// Converts RGB to OKLCH and checks similarity
93- fn validate_oklch ( & self , input : & str ) -> bool {
94- // Parse the input
95- let parsed = self . parse_oklch ( input) ;
96- if parsed. is_none ( ) {
97- return false ;
98- }
99-
100- let ( l_user, c_user, h_user) = parsed. unwrap ( ) ;
101-
102- // Convert our RGB to OKLCH
103- let ( l_actual, c_actual, h_actual) = rgb_to_oklch ( self . r , self . g , self . b ) ;
104-
105- // Check if close enough
106- // L: within 5% (0-100 scale)
107- // C: within 0.05
108- // H: within 10 degrees (handle wrap-around at 360)
109- let l_diff = ( l_actual - l_user) . abs ( ) ;
110- let c_diff = ( c_actual - c_user) . abs ( ) ;
111- let h_diff = {
112- let diff = ( h_actual - h_user) . abs ( ) ;
113- diff. min ( 360.0 - diff) // Handle wrap-around
114- } ;
115-
116- l_diff <= 5.0 && c_diff <= 0.05 && h_diff <= 10.0
117- }
118-
119- /// Parse OKLCH from various formats
120- fn parse_oklch ( & self , input : & str ) -> Option < ( f64 , f64 , f64 ) > {
121- let input = input. trim ( ) ;
122-
123- // Remove oklch( and ) if present
124- let inner = input
125- . strip_prefix ( "oklch(" )
126- . and_then ( |s| s. strip_suffix ( ')' ) )
127- . unwrap_or ( input) ;
128-
129- // Split by whitespace or comma
130- let parts: Vec < & str > = inner
131- . split ( |c : char | c. is_whitespace ( ) || c == ',' )
132- . filter ( |s| !s. is_empty ( ) )
133- . collect ( ) ;
134-
135- if parts. len ( ) != 3 {
136- return None ;
137- }
138-
139- // Parse L (can have % suffix)
140- let l = parts[ 0 ] . trim_end_matches ( '%' ) . parse :: < f64 > ( ) . ok ( ) ?;
141-
142- // Parse C
143- let c = parts[ 1 ] . parse :: < f64 > ( ) . ok ( ) ?;
144-
145- // Parse H
146- let h = parts[ 2 ] . parse :: < f64 > ( ) . ok ( ) ?;
147-
148- Some ( ( l, c, h) )
149- }
150- }
151-
152- /// Convert RGB to OKLCH color space
153- /// This is a simplified approximation
154- fn rgb_to_oklch ( r : u8 , g : u8 , b : u8 ) -> ( f64 , f64 , f64 ) {
155- // First convert to linear RGB
156- let r_linear = srgb_to_linear ( r as f64 / 255.0 ) ;
157- let g_linear = srgb_to_linear ( g as f64 / 255.0 ) ;
158- let b_linear = srgb_to_linear ( b as f64 / 255.0 ) ;
159-
160- // Convert to OKLab using the matrix transformation
161- let l = 0.4122214708 * r_linear + 0.5363325363 * g_linear + 0.0514459929 * b_linear;
162- let m = 0.2119034982 * r_linear + 0.6806995451 * g_linear + 0.1073969566 * b_linear;
163- let s = 0.0883024619 * r_linear + 0.2817188376 * g_linear + 0.6299787005 * b_linear;
164-
165- let l_ = l. cbrt ( ) ;
166- let m_ = m. cbrt ( ) ;
167- let s_ = s. cbrt ( ) ;
168-
169- let l_oklab = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
170- let a_oklab = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
171- let b_oklab = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
172-
173- // Convert OKLab to OKLCH
174- let l_oklch = l_oklab * 100.0 ; // Convert to percentage
175- let c_oklch = ( a_oklab * a_oklab + b_oklab * b_oklab) . sqrt ( ) ;
176- let h_oklch = b_oklab. atan2 ( a_oklab) . to_degrees ( ) ;
177- let h_oklch = if h_oklch < 0.0 {
178- h_oklch + 360.0
179- } else {
180- h_oklch
181- } ;
182-
183- ( l_oklch, c_oklch, h_oklch)
184- }
185-
186- /// Convert sRGB to linear RGB
187- fn srgb_to_linear ( c : f64 ) -> f64 {
188- if c <= 0.04045 {
189- c / 12.92
190- } else {
191- ( ( c + 0.055 ) / 1.055 ) . powf ( 2.4 )
192- }
19376}
19477
19578#[ cfg( test) ]
@@ -204,40 +87,9 @@ mod tests {
20487 b : 200 ,
20588 } ;
20689
207- // Exact match
20890 assert ! ( quiz. validate_answer( "#c8c8c8" ) ) ;
209-
210- // Within tolerance (diff = 10)
21191 assert ! ( quiz. validate_answer( "#bec8ca" ) ) ;
212-
213- // At boundary (diff = 20)
214- assert ! ( quiz. validate_answer( "#b4c8c8" ) ) ;
215-
216- // Outside tolerance (diff = 21)
217- assert ! ( !quiz. validate_answer( "#b3c8c8" ) ) ;
218- }
219-
220- #[ test]
221- fn test_oklch_parsing ( ) {
222- let quiz = ColorQuiz {
223- r : 200 ,
224- g : 200 ,
225- b : 200 ,
226- } ;
227-
228- // Test various formats
229- assert ! ( quiz. parse_oklch( "oklch(45.0% 0.306 65.4)" ) . is_some( ) ) ;
230- assert ! ( quiz. parse_oklch( "45.0% 0.306 65.4" ) . is_some( ) ) ;
231- assert ! ( quiz. parse_oklch( "45.0%, 0.306, 65.4" ) . is_some( ) ) ;
232- assert ! ( quiz. parse_oklch( "45 0.306 65.4" ) . is_some( ) ) ;
233- }
234-
235- #[ test]
236- fn test_rgb_to_oklch ( ) {
237- // Test with a known color (approximate values)
238- let ( l, c, h) = rgb_to_oklch ( 255 , 0 , 0 ) ; // Red
239- assert ! ( l > 60.0 && l < 70.0 ) ; // Lightness around 62-63%
240- assert ! ( c > 0.25 ) ; // High chroma
241- assert ! ( h > 20.0 && h < 40.0 ) ; // Hue around 29 degrees
92+ assert ! ( quiz. validate_answer( "#b3c8c8" ) ) ;
93+ assert ! ( !quiz. validate_answer( "#aec8c8" ) ) ;
24294 }
24395}
0 commit comments