Skip to content

Instantly share code, notes, and snippets.

@Cenness
Last active November 5, 2023 12:56
Show Gist options
  • Save Cenness/5edf05c5faa97d7c82a7ee372abb038e to your computer and use it in GitHub Desktop.
Save Cenness/5edf05c5faa97d7c82a7ee372abb038e to your computer and use it in GitHub Desktop.
Print out color pairs with their contrast
[
{
"type": "colordef",
"BLACK": [ 33, 34, 37 ],
"RED": [ 254, 63, 22 ],
"GREEN": [ 60, 168, 103 ],
"BROWN": [ 223, 112, 30 ],
"BLUE": [ 0, 106, 170 ],
"MAGENTA": [ 235, 82, 177 ],
"CYAN": [ 7, 163, 167 ],
"DGRAY": [ 138, 139, 172 ],
"LRED": [ 255, 158, 138 ],
"LGREEN": [ 152, 206, 119 ],
"YELLOW": [ 212, 182, 52 ],
"LBLUE": [ 30, 171, 255 ],
"LMAGENTA": [ 247, 156, 188 ],
"LCYAN": [ 58, 206, 192 ],
"GRAY": [ 184, 186, 188 ],
"WHITE": [ 255, 255, 255 ]
}
]
#!/usr/bin/env python3
import os, sys, argparse, json
def translate(value, value_min_range, value_max_range, min_range, max_range):
value_span = value_max_range - value_min_range
span = max_range - min_range
scaled = float(value - value_min_range) / float(value_span)
return min_range + (scaled * span)
def rgb_as_int(rgb1, rgb2):
n_rgb1 = tuple([translate(c, 0, 255, 0, 1) for c in rgb1])
n_rgb2 = tuple([translate(c, 0, 255, 0, 1) for c in rgb2])
return rgb(n_rgb1, n_rgb2)
# https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
def rgb(rgb1, rgb2):
for r, g, b in (rgb1, rgb2):
if not 0.0 <= r <= 1.0:
raise ValueError("r is out of valid range (0.0 - 1.0)")
if not 0.0 <= g <= 1.0:
raise ValueError("g is out of valid range (0.0 - 1.0)")
if not 0.0 <= b <= 1.0:
raise ValueError("b is out of valid range (0.0 - 1.0)")
l1 = _relative_luminance(*rgb1)
l2 = _relative_luminance(*rgb2)
if l1 > l2:
contrast = (l1 + 0.05) / (l2 + 0.05)
else:
contrast = (l2 + 0.05) / (l1 + 0.05)
if contrast >= 7.0:
return "AAA"
elif contrast >= 4.5:
return "AA"
else:
return round(contrast, 3)
# https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
def _relative_luminance(r, g, b):
r = _linearize(r)
g = _linearize(g)
b = _linearize(b)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def _linearize(v):
if v <= 0.03928:
return v / 12.92
else:
return ((v + 0.055) / 1.055) ** 2.4
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"-j",
"--colors",
dest="base_colors",
type=str,
help="path to base colors json",
default="",
)
parser.add_argument(
"-p",
"--print",
dest="print",
type=str,
help="print out color pairs, you'll need terminal with 24-bit color support enabled; enabled by default",
default="yes",
choices=["yes", "no"],
)
args = parser.parse_args(args=None if sys.argv[1:] else ["--help"])
with open(args.base_colors, "rt") as f:
bc_json = json.load(f)[0]
colors = []
column_width = 0
for record in bc_json:
if record != "type":
column_width = max(column_width, len(record) + 2)
pair = {
"name": record,
"rgb": (bc_json[record][0], bc_json[record][1], bc_json[record][2]),
}
colors.append(pair)
for c1 in colors:
for c2 in colors:
if c1["name"] != c2["name"]:
if args.print == "yes":
print(
f'{c1["name"]:>{column_width}s} {c2["name"]:>{column_width}s} {rgb_as_int(c1["rgb"], c2["rgb"]):>7} \x1b[48;2;{c1["rgb"][0]};{c1["rgb"][1]};{c1["rgb"][2]}m\x1b[38;2;{c2["rgb"][0]};{c2["rgb"][1]};{c2["rgb"][2]}m {c2["rgb"][0]:3} {c2["rgb"][1]:3} {c2["rgb"][2]:3} \x1b[0m'
)
else:
print(
f'{c1["name"]:>{column_width}s} {c2["name"]:>{column_width}s} {rgb_as_int(c1["rgb"], c2["rgb"]):>7}'
)
#!/usr/bin/env python3
import os, sys, argparse, json
## https://github.com/Myndex/SAPC-APCA/blob/6ef8c9982973ac4514fa69c717b838d9c0093251/documentation/APCA-W3-LaTeX.md
## direct contrast, without font and score interpolation
def lightness_contrast(rgb1, rgb2):
s_apc = _lightness_difference(*rgb1, *rgb2)
w_offset = 0.027
w_clamp = 0.1
if abs(s_apc) < w_clamp:
return 0
if s_apc > 0:
return abs(round((s_apc - w_offset) * 100.0, 1))
else:
return abs(round((s_apc + w_offset) * 100.0, 1))
def _lightness_difference(r1, g1, b1, r2, g2, b2):
w_scale = 1.14
d_min = 0.0005
n_tx = 0.57
n_bg = 0.56
r_tx = 0.62
r_bg = 0.65
y_bg = _luminance(r1, g1, b1)
y_tx = _luminance(r2, g2, b2)
if abs(y_bg - y_tx) < d_min:
return 0
if y_bg > y_tx:
return (y_bg**n_bg - y_tx**n_tx) * w_scale
else:
return (y_bg**r_bg - y_tx**r_tx) * w_scale
def _luminance(r, g, b):
s_trc = 2.4
b_thrsh = 0.022
b_clip = 1.414
y_r = ((r / 255.0) ** s_trc) * 0.2126729
y_g = ((g / 255.0) ** s_trc) * 0.7151522
y_b = ((b / 255.0) ** s_trc) * 0.0721750
y = y_r + y_g + y_b
if y < b_thrsh:
return y + ((b_thrsh - y) ** b_clip)
else:
return y
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"-j",
"--colors",
dest="base_colors",
type=str,
help="path to json",
default="",
)
parser.add_argument(
"-p",
"--print",
dest="print",
type=str,
help="print out color pairs",
default="yes",
choices=["yes", "no"],
)
args = parser.parse_args(args=None if sys.argv[1:] else ["--help"])
with open(args.base_colors, "rt") as f:
bc_json = json.load(f)[0]
colors = []
column_width = 0
for record in bc_json:
if record != "type":
column_width = max(column_width, len(record) + 2)
pair = {
"name": record,
"rgb": (bc_json[record][0], bc_json[record][1], bc_json[record][2]),
}
colors.append(pair)
for c1 in colors:
for c2 in colors:
if c1["name"] != c2["name"]:
if args.print == "yes":
print(
f'{c1["name"]:>{column_width}s} {c2["name"]:>{column_width}s} {lightness_contrast(c1["rgb"], c2["rgb"]):>6} \x1b[48;2;{c1["rgb"][0]};{c1["rgb"][1]};{c1["rgb"][2]}m\x1b[38;2;{c2["rgb"][0]};{c2["rgb"][1]};{c2["rgb"][2]}m {c2["rgb"][0]:3} {c2["rgb"][1]:3} {c2["rgb"][2]:3} \x1b[0m'
)
else:
print(
f'{c1["name"]:>{column_width}s} {c2["name"]:>{column_width}s} {lightness_contrast(c1["rgb"], c2["rgb"]):>6}'
)
@Cenness
Copy link
Author

Cenness commented Mar 20, 2023

image

@Cenness
Copy link
Author

Cenness commented Nov 5, 2023

image

        small body text only
> 89.7
        body text okay
> 74.7 
        fluent text okay
> 59.7 
        large & sub-fluent text
> 44.7 
        spot & non text only
> 29.7 
        no text usage
> 14.7 
        invisible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment