@@ -209,4 +209,86 @@ def get_text_color_from_background(background_color):
209209 if vibrancy_value < 0.4 : # Muted bright colors
210210 return "dark" # Use dark text for readability
211211 else :
212- return "light" # Use light text for vibrant bright colors
212+ return "light" # Use light text for vibrant bright colors
213+
214+ def check_color_quality (rgb ):
215+ """
216+ Evaluates if a color is of 'good quality' for UI/text usage.
217+
218+ This function simulates a color visibility tester by checking the color's
219+ contrast ratio against standard black and white backgrounds. A color is
220+ considered 'good quality' if it meets the WCAG AA standard (contrast ratio >= 4.5)
221+ against at least one of the backgrounds.
222+
223+ Parameters:
224+ rgb (tuple): A tuple containing the RGB values (r, g, b).
225+
226+ Returns:
227+ dict: A dictionary containing:
228+ - 'contrast_with_black' (float): Contrast ratio with black.
229+ - 'contrast_with_white' (float): Contrast ratio with white.
230+ - 'is_good_quality' (bool): True if legible on black or white.
231+ """
232+ black = (0 , 0 , 0 )
233+ white = (255 , 255 , 255 )
234+
235+ contrast_black = color_contrast (rgb , black )
236+ contrast_white = color_contrast (rgb , white )
237+
238+ # WCAG AA requires 4.5:1 for normal text
239+ is_good = contrast_black >= 4.5 or contrast_white >= 4.5
240+
241+ return {
242+ "contrast_with_black" : round (contrast_black , 2 ),
243+ "contrast_with_white" : round (contrast_white , 2 ),
244+ "is_good_quality" : is_good
245+ }
246+
247+ def simulate_color_blindness (rgb , blindness_type = "protanopia" , intensity = 1.0 ):
248+ """
249+ Simulates color blindness on a given RGB color.
250+
251+ Parameters:
252+ rgb (tuple): A tuple containing the RGB values (r, g, b).
253+ blindness_type (str): The type of color blindness to simulate.
254+ Options: 'protanopia', 'deuteranopia', 'tritanopia', 'achromatopsia'.
255+ Default is 'protanopia'.
256+ intensity (float): The intensity of the simulation (0.0 to 1.0).
257+ Default is 1.0 (full simulation).
258+
259+ Returns:
260+ tuple: A tuple representing the simulated RGB values.
261+ """
262+ if not (0 <= intensity <= 1 ):
263+ raise ValueError ("Intensity must be between 0 and 1." )
264+
265+ blindness_type = blindness_type .lower ()
266+
267+ # Matrices for color blindness simulation (approximate for sRGB/Linear)
268+ matrices = {
269+ "protanopia" : [[0.567 , 0.433 , 0.0 ], [0.558 , 0.442 , 0.0 ], [0.0 , 0.242 , 0.758 ]],
270+ "deuteranopia" : [[0.625 , 0.375 , 0.0 ], [0.7 , 0.3 , 0.0 ], [0.0 , 0.3 , 0.7 ]],
271+ "tritanopia" : [[0.95 , 0.05 , 0.0 ], [0.0 , 0.433 , 0.567 ], [0.0 , 0.475 , 0.525 ]],
272+ "achromatopsia" : [[0.299 , 0.587 , 0.114 ], [0.299 , 0.587 , 0.114 ], [0.299 , 0.587 , 0.114 ]]
273+ }
274+
275+ if blindness_type not in matrices :
276+ raise ValueError (f"Unknown blindness type: { blindness_type } " )
277+
278+ matrix = matrices [blindness_type ]
279+ r_lin , g_lin , b_lin = rgb_to_linear (rgb )
280+
281+ r_sim = r_lin * matrix [0 ][0 ] + g_lin * matrix [0 ][1 ] + b_lin * matrix [0 ][2 ]
282+ g_sim = r_lin * matrix [1 ][0 ] + g_lin * matrix [1 ][1 ] + b_lin * matrix [1 ][2 ]
283+ b_sim = r_lin * matrix [2 ][0 ] + g_lin * matrix [2 ][1 ] + b_lin * matrix [2 ][2 ]
284+
285+ # Convert back to sRGB (using simplified gamma 2.2 to match rgb_to_linear)
286+ def lin_to_srgb (c ): return c ** (1 / 2.2 )
287+ r_final , g_final , b_final = lin_to_srgb (max (0 , r_sim )) * 255 , lin_to_srgb (max (0 , g_sim )) * 255 , lin_to_srgb (max (0 , b_sim )) * 255
288+
289+ return (
290+ max (0 , min (255 , int (rgb [0 ] * (1 - intensity ) + r_final * intensity ))),
291+ max (0 , min (255 , int (rgb [1 ] * (1 - intensity ) + g_final * intensity ))),
292+ max (0 , min (255 , int (rgb [2 ] * (1 - intensity ) + b_final * intensity )))
293+ )
294+
0 commit comments