| 1 |
import colorsys |
| 2 |
from math import pow |
| 3 |
from typing import Tuple |
| 4 |
from datetime import datetime |
| 5 |
|
| 6 |
# Function to calculate relative luminance of a color |
| 7 |
def relative_luminance(rgb: Tuple[int, int, int]) -> float: |
| 8 |
def adjust(c: float) -> float: |
| 9 |
c = c / 255.0 |
| 10 |
return c / 12.92 if c <= 0.03928 else pow((c + 0.055) / 1.055, 2.4) |
| 11 |
|
| 12 |
r, g, b = rgb |
| 13 |
return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b) |
| 14 |
|
| 15 |
# Function to calculate contrast ratio |
| 16 |
def contrast_ratio(foreground: Tuple[int, int, int], background: Tuple[int, int, int]) -> float: |
| 17 |
lum1 = relative_luminance(foreground) |
| 18 |
lum2 = relative_luminance(background) |
| 19 |
if lum1 > lum2: |
| 20 |
return (lum1 + 0.05) / (lum2 + 0.05) |
| 21 |
return (lum2 + 0.05) / (lum1 + 0.05) |
| 22 |
|
| 23 |
# Generate HTML table |
| 24 |
def generate_html(colors: list): |
| 25 |
html = '<style>* {font-family: sans-serif; text-shadow: none;} .textwhite {color: white;} .textblack {color: black;} .bgwhite.result:hover {text-shadow: 1px 1px 1px #000000;} .bgblack.result:hover {text-shadow: 1px 1px 1px #ffffff;} .label {padding: 0.5em; display: block;} .pass100 {color: #000000; background-color: #9cffac;} .pass80 {color: #000000; background-color: #f2ff9c;} .pass60 {color: #000000; background-color: #ffdb9c} .fail {color: #000000; background-color: #ff9c9c;}</style>' |
| 26 |
|
| 27 |
# Helper function to determine text color |
| 28 |
def get_text_color(hex_color): |
| 29 |
rgb = tuple(int(hex_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) |
| 30 |
return "textwhite" if relative_luminance(rgb) < 0.5 else "textblack" |
| 31 |
|
| 32 |
# Helper function to determine text-shadow color |
| 33 |
def get_text_shadow(hex_color): |
| 34 |
rgb = tuple(int(hex_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) |
| 35 |
return "bgwhite" if relative_luminance(rgb) >= 0.5 else "bgblack" |
| 36 |
|
| 37 |
# Store passing combinations for summary table |
| 38 |
summary_data = {bg[1]: {"name": bg[0], "AAA": [], "AA": [], "AALarge": []} for bg in colors} |
| 39 |
|
| 40 |
# Top row |
| 41 |
html += '<h2>Main Contrast Table</h2>' |
| 42 |
html += '<table border="0" style="border-collapse: collapse;">' |
| 43 |
html += '<tr><th style="color: black;">BG →<br>FG ↓</th>' |
| 44 |
for bg in colors: |
| 45 |
text_color = get_text_color(bg[1]) |
| 46 |
html += f'<th class="{text_color}" style="background-color: {bg[1]};">{bg[0]}<br>{bg[1]}</th>' |
| 47 |
html += '</tr>' |
| 48 |
|
| 49 |
# Rows for each foreground color |
| 50 |
for fg in colors: |
| 51 |
text_color = get_text_color(fg[1]) |
| 52 |
html += f'<tr><td class="{text_color}" style="background-color: {fg[1]};">{fg[0]}<br>{fg[1]}</td>' |
| 53 |
for bg in colors: |
| 54 |
rgb_fg = tuple(int(fg[1].lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) |
| 55 |
rgb_bg = tuple(int(bg[1].lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) |
| 56 |
ratio = round(contrast_ratio(rgb_fg, rgb_bg), 3) |
| 57 |
|
| 58 |
# Compliance check and store in summary data |
| 59 |
if ratio >= 7: |
| 60 |
summary_data[bg[1]]["AAA"].append((fg[0], fg[1], ratio)) |
| 61 |
elif ratio >= 4.5: |
| 62 |
summary_data[bg[1]]["AA"].append((fg[0], fg[1], ratio)) |
| 63 |
elif ratio >= 3: |
| 64 |
summary_data[bg[1]]["AALarge"].append((fg[0], fg[1], ratio)) |
| 65 |
|
| 66 |
compliance = ( |
| 67 |
"Pass - 100% (AAA ≥ 7.0)" if ratio >= 7 else |
| 68 |
"Pass - 80% (AA Normal ≥ 4.5)" if ratio >= 4.5 else |
| 69 |
"Pass - 60% (AA Large ≥ 3.0)" if ratio >= 3 else |
| 70 |
"Fail - 0% (< 3.0)" |
| 71 |
) |
| 72 |
labelclass = ( |
| 73 |
"pass100" if ratio >= 7 else |
| 74 |
"pass80" if ratio >= 4.5 else |
| 75 |
"pass60" if ratio >= 3 else |
| 76 |
"fail" |
| 77 |
) |
| 78 |
text_shadow = get_text_shadow(bg[1]) |
| 79 |
html += ( |
| 80 |
f'<td style="background-color: {bg[1]}; color: {fg[1]}; padding: 1em;">' |
| 81 |
f'<strong class="result {text_shadow}">{fg[0]} over {bg[0]}</strong>' |
| 82 |
f'<br><span class="label {labelclass}">{compliance} with ratio {ratio}</span>' |
| 83 |
f'</td>' |
| 84 |
) |
| 85 |
html += '</tr>' |
| 86 |
|
| 87 |
html += '</table>' |
| 88 |
|
| 89 |
# Generate summary table |
| 90 |
html += '<h2>Summary Table: Passing Combinations by Category</h2>' |
| 91 |
html += '<table border="1" style="border-collapse: collapse;">' |
| 92 |
html += '<tr><th style="color: black;">Background</th>' |
| 93 |
html += '<th>AAA ≥ 7.0</th><th>AA Normal ≥ 4.5</th><th>AA Large ≥ 3.0</th></tr>' |
| 94 |
|
| 95 |
for bg_hex, bg_data in summary_data.items(): |
| 96 |
html += f'<tr><td style="background-color: {bg_hex}; padding: 1em;" class="{get_text_color(bg_hex)}">{bg_data["name"]}<br>{bg_hex}</td>' |
| 97 |
|
| 98 |
# AAA Column |
| 99 |
html += f'<td style="background-color: {bg_hex}">' |
| 100 |
for fg_name, fg_hex, ratio in sorted(bg_data["AAA"], key=lambda x: x[2], reverse=True): |
| 101 |
html += f'<div style="background-color: {bg_hex}; color: {fg_hex}; padding: 0.5em;">{fg_name} (Ratio: {ratio})</div>' |
| 102 |
html += '</td>' |
| 103 |
|
| 104 |
# AA Column |
| 105 |
html += f'<td style="background-color: {bg_hex}">' |
| 106 |
for fg_name, fg_hex, ratio in sorted(bg_data["AA"], key=lambda x: x[2], reverse=True): |
| 107 |
html += f'<div style="background-color: {bg_hex}; color: {fg_hex}; padding: 0.5em;">{fg_name} (Ratio: {ratio})</div>' |
| 108 |
html += '</td>' |
| 109 |
|
| 110 |
# AA Large Column |
| 111 |
html += f'<td style="background-color: {bg_hex}">' |
| 112 |
for fg_name, fg_hex, ratio in sorted(bg_data["AALarge"], key=lambda x: x[2], reverse=True): |
| 113 |
html += f'<div style="background-color: {bg_hex}; color: {fg_hex}; padding: 0.5em; font-size: 1.5em;">{fg_name} (Ratio: {ratio})</div>' |
| 114 |
html += '</td>' |
| 115 |
|
| 116 |
html += '</tr>' |
| 117 |
|
| 118 |
html += '</table>' |
| 119 |
return html |
| 120 |
|
| 121 |
# Color palette with names and hex codes |
| 122 |
colors = [ |
| 123 |
("Pure White", "#FFFFFF"), |
| 124 |
("Pure Black", "#000000"), |
| 125 |
("Maroon", "#8C2531"), |
| 126 |
("Black", "#222222"), |
| 127 |
("Dark Gray", "#404040"), |
| 128 |
("Medium Gray", "#7F7F7F"), |
| 129 |
("Light Gray", "#BFBFBF"), |
| 130 |
("Lightest Gray", "#F6F6F6"), |
| 131 |
("Tan", "#ADA28C"), |
| 132 |
("Dark Blue", "#002A3B"), |
| 133 |
("Dark Green", "#225A41"), |
| 134 |
("Blue", "#005E84"), |
| 135 |
("Green", "#7E9A4B"), |
| 136 |
("Orange", "#B2461F"), |
| 137 |
("Yellow", "#D88A00"), |
| 138 |
("Purple", "#3C2D57") |
| 139 |
] |
| 140 |
|
| 141 |
html_output = generate_html(colors) |
| 142 |
|
| 143 |
# Generate timestamp for filename |
| 144 |
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") |
| 145 |
filename = f"color_contrast_grid_{timestamp}.html" |
| 146 |
|
| 147 |
# Write to an HTML file |
| 148 |
with open(filename, "w") as file: |
| 149 |
file.write(html_output) |
| 150 |
|
| 151 |
print(f"HTML file '{filename}' generated.") |