78 lines
3.8 KiB
Python
78 lines
3.8 KiB
Python
"""Source-color analysis only; no application rendering or screenshot modification.
|
|
Run: python .local/palette-contrast-20260912.py
|
|
WCAG sRGB definition: https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
|
|
Float source constants are retained. Hex labels are rounded display conveniences.
|
|
"""
|
|
import json
|
|
|
|
def rgb(h):
|
|
return tuple(int(h[i:i+2], 16)/255 for i in (0, 2, 4))
|
|
|
|
def linear(c):
|
|
return c/12.92 if c <= .04045 else ((c+.055)/1.055)**2.4
|
|
|
|
def encoded(c):
|
|
return 12.92*c if c <= .0031308 else 1.055*c**(1/2.4)-.055
|
|
|
|
def lum(c):
|
|
return sum(w*linear(v) for w, v in zip((.2126, .7152, .0722), c))
|
|
|
|
def contrast(a, b):
|
|
x, y = sorted((lum(a), lum(b)))
|
|
return (y+.05)/(x+.05)
|
|
|
|
def hexcode(c):
|
|
return '#' + ''.join(f'{round(v*255):02X}' for v in c)
|
|
|
|
C = dict(white=(1,1,1), BG=(.945,.956,.969), INK=(.13,.18,.25),
|
|
MUTED=(.43,.48,.55), TEAL=(0,.48,.43), LINE=(.86,.89,.91),
|
|
amber=(.72,.47,.10), entity=(.16,.4,.71), actor=(.58,.35,.7),
|
|
scenario=(.65,.33,.4), state=(.38,.39,.68), other=(.25,.48,.56))
|
|
for name, h in dict(canvas='F6F8FB', activation='F3F8F8', region='F7FAFD',
|
|
machine='EAF1F8', navfill='DAEEEB', navborder='97CBC3',
|
|
rowfill='E0F7F2', edge='909EAD', trace='866BA8',
|
|
keyword='763EA0', identifier='145D8B', string='18714F',
|
|
number='A34E1E', comment='6F7984', error='BA263E',
|
|
punctuation='5B6575', diagnostic_error='AE2E40',
|
|
diagnostic_warning='915D17').items():
|
|
C[name] = rgb(h)
|
|
# iced_core 0.14 palette::Primary::generate and mix use linear-light blending.
|
|
C['editor_selection'] = tuple(encoded(.6*linear(a)+.4*linear(b))
|
|
for a,b in zip(C['TEAL'], C['BG']))
|
|
pairs = [('INK','white'), ('MUTED','white'), ('MUTED','BG'),
|
|
('MUTED','machine'), ('TEAL','white'), ('TEAL','rowfill'),
|
|
('LINE','white'), ('LINE','activation'), ('edge','canvas'),
|
|
('navfill','white'), ('navborder','navfill'), ('navborder','white'),
|
|
('activation','white'), ('region','machine')]
|
|
for name in ('entity','actor','TEAL','amber','scenario','state','other'):
|
|
C[name+'_tint'] = tuple(.94+.06*v for v in C[name])
|
|
pairs.append((name,name+'_tint'))
|
|
for name in ('keyword','identifier','string','number','comment','error','punctuation'):
|
|
pairs += [(name,'BG'), (name,'editor_selection')]
|
|
for name in ('diagnostic_error','diagnostic_warning'):
|
|
pairs.append((name,'BG'))
|
|
# Candidate A retains category accents. Candidate B uses neutral content and blue selection.
|
|
for name,h in dict(A_ink='202C3A', A_secondary='4B5968', A_boundary='667586',
|
|
A_accent='00665C', A_selection='D9EEE8', A_amber='865B00',
|
|
A_actor='754499', A_other='326B80',
|
|
B_ink='202020', B_secondary='494949', B_boundary='595959',
|
|
B_accent='124E96', B_selection='E2ECFA').items(): C[name]=rgb(h)
|
|
for prefix in ('A','B'):
|
|
pairs += [(prefix+'_ink','white'), (prefix+'_secondary','BG'),
|
|
(prefix+'_boundary','white'), (prefix+'_boundary','machine'),
|
|
(prefix+'_accent','white'), (prefix+'_accent',prefix+'_selection'),
|
|
(prefix+'_ink',prefix+'_selection')]
|
|
pairs += [('A_amber','white'), ('A_amber','BG')]
|
|
for name in ('A_actor','A_other','A_amber'):
|
|
C[name+'_tint']=tuple(.94+.06*v for v in C[name])
|
|
pairs.append((name,name+'_tint'))
|
|
for name in ('keyword','identifier','string','number','A_secondary','error','punctuation'):
|
|
pairs.append((name,'A_selection'))
|
|
result=[]
|
|
for a,b in pairs:
|
|
ratio=contrast(C[a],C[b])
|
|
result.append(dict(foreground=a, background=b, fg_hex=hexcode(C[a]),
|
|
bg_hex=hexcode(C[b]), ratio=round(ratio,6),
|
|
normal_text_4_5=ratio>=4.5, meaningful_graphic_3=ratio>=3))
|
|
print(json.dumps(result,indent=2))
|