User:Surjection/swatch3

From Wiktionary, the free dictionary
Jump to navigation Jump to search

Now outdated: see Wiktionary:Palette/numbered

An alternative swatch: User:Surjection/swatch2 (only WCAG checks)

The base colors for each set are:

red #e63949
scarlet #e0562d
orange #d9760f
amber #cf9211
yellow #baa72f
lime #97b53c
green #60b334
teal #32b378
cyan #28a0b5
blue #3271f0
indigo #584ee6
purple #7f38e8
magenta #9d2cdb
rose #a6448c
grey #666666
brown #856142

The white foreground color is specified as #eaecf0 and the black foreground color as #202122.

We aim for the following contrast ratios, whichever is stricter (in light mode against the black foreground color and in night mode against the white foreground color):

Number WCAG APCA
0 15.00 100 (94.5 for dark mode)
1 13.25 95 (93 for dark mode)
2 11.50 90
3 9.25 85
4 7.50 75
5 6.00 67.5
6 4.50 60
7 3.00 52.5
8 2.10 45
9 1.40 30

Swatches

[edit]

Light mode

[edit]
red-0
red-1
red-2
red-3
red-4
red-5
red-6
red-7
red-8
red-9
scarlet-0
scarlet-1
scarlet-2
scarlet-3
scarlet-4
scarlet-5
scarlet-6
scarlet-7
scarlet-8
scarlet-9
orange-0
orange-1
orange-2
orange-3
orange-4
orange-5
orange-6
orange-7
orange-8
orange-9
amber-0
amber-1
amber-2
amber-3
amber-4
amber-5
amber-6
amber-7
amber-8
amber-9
yellow-0
yellow-1
yellow-2
yellow-3
yellow-4
yellow-5
yellow-6
yellow-7
yellow-8
yellow-9
lime-0
lime-1
lime-2
lime-3
lime-4
lime-5
lime-6
lime-7
lime-8
lime-9
green-0
green-1
green-2
green-3
green-4
green-5
green-6
green-7
green-8
green-9
teal-0
teal-1
teal-2
teal-3
teal-4
teal-5
teal-6
teal-7
teal-8
teal-9
cyan-0
cyan-1
cyan-2
cyan-3
cyan-4
cyan-5
cyan-6
cyan-7
cyan-8
cyan-9
blue-0
blue-1
blue-2
blue-3
blue-4
blue-5
blue-6
blue-7
blue-8
blue-9
indigo-0
indigo-1
indigo-2
indigo-3
indigo-4
indigo-5
indigo-6
indigo-7
indigo-8
indigo-9
purple-0
purple-1
purple-2
purple-3
purple-4
purple-5
purple-6
purple-7
purple-8
purple-9
magenta-0
magenta-1
magenta-2
magenta-3
magenta-4
magenta-5
magenta-6
magenta-7
magenta-8
magenta-9
rose-0
rose-1
rose-2
rose-3
rose-4
rose-5
rose-6
rose-7
rose-8
rose-9
grey-0
grey-1
grey-2
grey-3
grey-4
grey-5
grey-6
grey-7
grey-8
grey-9
brown-0
brown-1
brown-2
brown-3
brown-4
brown-5
brown-6
brown-7
brown-8
brown-9
red-0
red-1
red-2
red-3
red-4
red-5
red-6
red-7
red-8
red-9
scarlet-0
scarlet-1
scarlet-2
scarlet-3
scarlet-4
scarlet-5
scarlet-6
scarlet-7
scarlet-8
scarlet-9
orange-0
orange-1
orange-2
orange-3
orange-4
orange-5
orange-6
orange-7
orange-8
orange-9
amber-0
amber-1
amber-2
amber-3
amber-4
amber-5
amber-6
amber-7
amber-8
amber-9
yellow-0
yellow-1
yellow-2
yellow-3
yellow-4
yellow-5
yellow-6
yellow-7
yellow-8
yellow-9
lime-0
lime-1
lime-2
lime-3
lime-4
lime-5
lime-6
lime-7
lime-8
lime-9
green-0
green-1
green-2
green-3
green-4
green-5
green-6
green-7
green-8
green-9
teal-0
teal-1
teal-2
teal-3
teal-4
teal-5
teal-6
teal-7
teal-8
teal-9
cyan-0
cyan-1
cyan-2
cyan-3
cyan-4
cyan-5
cyan-6
cyan-7
cyan-8
cyan-9
blue-0
blue-1
blue-2
blue-3
blue-4
blue-5
blue-6
blue-7
blue-8
blue-9
indigo-0
indigo-1
indigo-2
indigo-3
indigo-4
indigo-5
indigo-6
indigo-7
indigo-8
indigo-9
purple-0
purple-1
purple-2
purple-3
purple-4
purple-5
purple-6
purple-7
purple-8
purple-9
magenta-0
magenta-1
magenta-2
magenta-3
magenta-4
magenta-5
magenta-6
magenta-7
magenta-8
magenta-9
rose-0
rose-1
rose-2
rose-3
rose-4
rose-5
rose-6
rose-7
rose-8
rose-9
grey-0
grey-1
grey-2
grey-3
grey-4
grey-5
grey-6
grey-7
grey-8
grey-9
brown-0
brown-1
brown-2
brown-3
brown-4
brown-5
brown-6
brown-7
brown-8
brown-9

Night mode

[edit]
red-0
red-1
red-2
red-3
red-4
red-5
red-6
red-7
red-8
red-9
scarlet-0
scarlet-1
scarlet-2
scarlet-3
scarlet-4
scarlet-5
scarlet-6
scarlet-7
scarlet-8
scarlet-9
orange-0
orange-1
orange-2
orange-3
orange-4
orange-5
orange-6
orange-7
orange-8
orange-9
amber-0
amber-1
amber-2
amber-3
amber-4
amber-5
amber-6
amber-7
amber-8
amber-9
yellow-0
yellow-1
yellow-2
yellow-3
yellow-4
yellow-5
yellow-6
yellow-7
yellow-8
yellow-9
lime-0
lime-1
lime-2
lime-3
lime-4
lime-5
lime-6
lime-7
lime-8
lime-9
green-0
green-1
green-2
green-3
green-4
green-5
green-6
green-7
green-8
green-9
teal-0
teal-1
teal-2
teal-3
teal-4
teal-5
teal-6
teal-7
teal-8
teal-9
cyan-0
cyan-1
cyan-2
cyan-3
cyan-4
cyan-5
cyan-6
cyan-7
cyan-8
cyan-9
blue-0
blue-1
blue-2
blue-3
blue-4
blue-5
blue-6
blue-7
blue-8
blue-9
indigo-0
indigo-1
indigo-2
indigo-3
indigo-4
indigo-5
indigo-6
indigo-7
indigo-8
indigo-9
purple-0
purple-1
purple-2
purple-3
purple-4
purple-5
purple-6
purple-7
purple-8
purple-9
magenta-0
magenta-1
magenta-2
magenta-3
magenta-4
magenta-5
magenta-6
magenta-7
magenta-8
magenta-9
rose-0
rose-1
rose-2
rose-3
rose-4
rose-5
rose-6
rose-7
rose-8
rose-9
grey-0
grey-1
grey-2
grey-3
grey-4
grey-5
grey-6
grey-7
grey-8
grey-9
brown-0
brown-1
brown-2
brown-3
brown-4
brown-5
brown-6
brown-7
brown-8
brown-9
red-0
red-1
red-2
red-3
red-4
red-5
red-6
red-7
red-8
red-9
scarlet-0
scarlet-1
scarlet-2
scarlet-3
scarlet-4
scarlet-5
scarlet-6
scarlet-7
scarlet-8
scarlet-9
orange-0
orange-1
orange-2
orange-3
orange-4
orange-5
orange-6
orange-7
orange-8
orange-9
amber-0
amber-1
amber-2
amber-3
amber-4
amber-5
amber-6
amber-7
amber-8
amber-9
yellow-0
yellow-1
yellow-2
yellow-3
yellow-4
yellow-5
yellow-6
yellow-7
yellow-8
yellow-9
lime-0
lime-1
lime-2
lime-3
lime-4
lime-5
lime-6
lime-7
lime-8
lime-9
green-0
green-1
green-2
green-3
green-4
green-5
green-6
green-7
green-8
green-9
teal-0
teal-1
teal-2
teal-3
teal-4
teal-5
teal-6
teal-7
teal-8
teal-9
cyan-0
cyan-1
cyan-2
cyan-3
cyan-4
cyan-5
cyan-6
cyan-7
cyan-8
cyan-9
blue-0
blue-1
blue-2
blue-3
blue-4
blue-5
blue-6
blue-7
blue-8
blue-9
indigo-0
indigo-1
indigo-2
indigo-3
indigo-4
indigo-5
indigo-6
indigo-7
indigo-8
indigo-9
purple-0
purple-1
purple-2
purple-3
purple-4
purple-5
purple-6
purple-7
purple-8
purple-9
magenta-0
magenta-1
magenta-2
magenta-3
magenta-4
magenta-5
magenta-6
magenta-7
magenta-8
magenta-9
rose-0
rose-1
rose-2
rose-3
rose-4
rose-5
rose-6
rose-7
rose-8
rose-9
grey-0
grey-1
grey-2
grey-3
grey-4
grey-5
grey-6
grey-7
grey-8
grey-9
brown-0
brown-1
brown-2
brown-3
brown-4
brown-5
brown-6
brown-7
brown-8
brown-9

Script

[edit]
import colorsys
import re


IDENTIFICATION = "[[User:Surjection/swatch3]]"
HEX_COLOR_RGB = re.compile(r"^#[0-9A-Fa-f]{6}$")


def _hex_to_rgb_comp(x):
    return int(x, 16) / 255.0


def hex_to_rgb(hex_color):
    if not HEX_COLOR_RGB.match(hex_color):
        raise ValueError("hex color format not supported")
    r, g, b = hex_color[1:3], hex_color[3:5], hex_color[5:7]
    return (_hex_to_rgb_comp(r), _hex_to_rgb_comp(g), _hex_to_rgb_comp(b))


def rgb_to_hex(c):
    r, g, b = c
    r = int(round(r * 255.0))
    g = int(round(g * 255.0))
    b = int(round(b * 255.0))
    return "#{:02x}{:02x}{:02x}".format(r, g, b)


def wcag_to_srgb(x):
    if x <= 0.03928:
        return x / 12.2
    else:
        return ((x + 0.055) / 1.055) ** 2.4


def quantize(color):
    r, g, b = color
    r = int(round(r * 255)) / 255
    g = int(round(g * 255)) / 255
    b = int(round(b * 255)) / 255
    return r, g, b


def wcag_luminance(color):
    rs, gs, bs = color
    r = wcag_to_srgb(rs)
    g = wcag_to_srgb(gs)
    b = wcag_to_srgb(bs)
    return 0.2126 * r + 0.7152 * g + 0.0722 * b


def wcag_contrast(fgcolor, bgcolor):
    fgluma = wcag_luminance(fgcolor)
    bgluma = wcag_luminance(bgcolor)
    ratio = (fgluma + 0.05) / (bgluma + 0.05)
    if ratio < 1:
        ratio = 1.0 / ratio
    return ratio


def _simple_srgb_luma(color):
    # <https://github.com/Myndex/SAPC-APCA/blob/master/documentation/APCA-W3-LaTeX.md>
    # <https://en.wiktionary.org/w/index.php?title=Module:palette&oldid=82494940#L-57--L-94>
    
    r, g, b = color
    s_rco, s_gco, s_bco = 0.2126729, 0.7151522, 0.0721750
    main_trc = 2.4
    return clamp(0, s_rco * (r ** main_trc) + s_gco * (g ** main_trc) + s_bco * (b ** main_trc), 1)


def _apca_f_sc(y_c):
    # <https://github.com/Myndex/SAPC-APCA/blob/master/documentation/APCA-W3-LaTeX.md>
    # <https://en.wiktionary.org/w/index.php?title=Module:palette&oldid=82494940#L-57--L-94>
    if y_c <= 0:
        return 0
    elif y_c <= 0.022:
        return y_c + (0.022 - y_c) ** 1.414
    else:
        return y_c


def apca_contrast(fgcolor, bgcolor):
    # <https://github.com/Myndex/SAPC-APCA/blob/master/documentation/APCA-W3-LaTeX.md>
    # <https://en.wiktionary.org/w/index.php?title=Module:palette&oldid=82494940#L-57--L-94>

    ys_fg = _simple_srgb_luma(fgcolor)
    ys_bg = _simple_srgb_luma(bgcolor)

    y_fg = _apca_f_sc(ys_fg)
    y_bg = _apca_f_sc(ys_bg)

    if y_bg > y_fg:
        s_apc = (y_bg ** 0.56 - y_fg ** 0.57) * 1.14
    else:
        s_apc = (y_bg ** 0.65 - y_fg ** 0.62) * 1.14

    if abs(s_apc) < 0.1:
        l_c = 0
    elif s_apc > 0:
        l_c = (s_apc - 0.027) * 100
    else:
        l_c = (s_apc + 0.027) * 100

    return l_c


def clamp(a, x, b):
    return max(a, min(x, b))


def _lighten_comp(component, power):
    if not 0 <= power <= 1:
        raise ValueError("power must be within [0, 1]")
    return power + component * (1 - power)


def lighten(color, power):
    if not 0 <= power <= 1:
        raise ValueError("power must be within [0, 1]")
    r, g, b = color
    return (_lighten_comp(r, power), _lighten_comp(g, power), _lighten_comp(b, power))


def deepen(color, power):
    if not 0 <= power <= 1:
        raise ValueError("power must be within [0, 1]")
    r, g, b = color
    h, s, v = colorsys.rgb_to_hsv(r, g, b)
    s_new = min(s + power, s if s < 0.5 else 1)
    v_new = v * (1 - power)
    return colorsys.hsv_to_rgb(h, s_new, v_new)


def blend(color, offset):
    if offset > 0:
        return lighten(color, offset)
    elif offset < 0:
        return deepen(color, -offset)
    return color


class ContrastedColorMaker:
    def __init__(self, base_color, reference, night_mode):
        self._night_mode = night_mode
        # generate all possible colors
        div = 512
        options = {}
        self._base_color = base_color
        self._colors = {}
        self._wcag = []
        self._apca = []
        for i in range(-div, div + 1):
            offset = clamp(-1, i / div, 1)
            color = quantize(blend(base_color, offset))
            if color not in options:
                options[color] = offset
                self._colors[offset] = color
                self._wcag.append((wcag_contrast(reference, color), offset))
                self._apca.append((abs(apca_contrast(reference, color)), offset))
        self._wcag.sort()
        self._apca.sort()

    def for_contrast(self, contrast_wcag, contrast_apca):
        wcag_meets = set(offset for contrast, offset in self._wcag if contrast >= contrast_wcag)
        if contrast_apca is None:
            meets = wcag_meets
        else:
            apca_meets = set(offset for contrast, offset in self._apca if contrast >= contrast_apca)
            meets = wcag_meets & apca_meets

        if meets:
            return self._colors[(max if self._night_mode else min)(meets)]

        assert False
        return self._colors[max(self._wcag)[-1]]


BASE = {
    "red":         hex_to_rgb("#e63949"),
    "scarlet":     hex_to_rgb("#e0562d"),
    "orange":      hex_to_rgb("#d9760f"),
    "amber":       hex_to_rgb("#cf9211"),
    "yellow":      hex_to_rgb("#baa72f"),
    "lime":        hex_to_rgb("#97b53c"),
    "green":       hex_to_rgb("#60b334"),
    "teal":        hex_to_rgb("#32b378"),
    "cyan":        hex_to_rgb("#28a0b5"),
    "blue":        hex_to_rgb("#3271f0"),
    "indigo":      hex_to_rgb("#584ee6"),
    "purple":      hex_to_rgb("#7f38e8"),
    "magenta":     hex_to_rgb("#9d2cdb"),
    "rose":        hex_to_rgb("#a6448c"),
    "grey":        hex_to_rgb("#666666"),
    "brown":       hex_to_rgb("#856142"),
}
WHITE_BG = "#ffffff"
BLACK_FG = "#202122"
WHITE_FG = "#eaecf0"
BLACK_BG = "#101418"


white = hex_to_rgb(WHITE_FG)
black = hex_to_rgb(BLACK_FG)
CONTRASTS = {
    0: (15.00, 100, 94.5),
    1: (13.25, 95, 93),
    2: (11.50, 90, 90),
    3: ( 9.25, 85, 85),
    4: ( 7.50, 75, 75),
    5: ( 6.00, 67.5, 67.5),
    6: ( 4.50, 60, 60),
    7: ( 3.00, 52.5, 52.5),
    8: ( 2.10, 45, 45),
    9: ( 1.40, 30, 30),
}


colors_light = {}
colors_night = {}


for base_color_name, base_color in BASE.items():
    tints_light = colors_light[base_color_name] = {}
    tints_night = colors_night[base_color_name] = {}

    contrasted_color_maker_light = ContrastedColorMaker(base_color, black, False)
    contrasted_color_maker_night = ContrastedColorMaker(base_color, white, True)

    for tint_number, (contrast_wcag, contrast_apca, contrast_apca_night) in CONTRASTS.items():
        tints_light[tint_number] = contrasted_color_maker_light.for_contrast(contrast_wcag, contrast_apca)
        tints_night[tint_number] = contrasted_color_maker_night.for_contrast(contrast_wcag, contrast_apca_night)


def make_swatch_bg(colors, fg_color):
    lines = []
    for base_color_name, tints in colors.items():
        if lines:
            lines.append('|-')
        else:
            lines.append('{|')
        for tint_number, color in tints.items():
            name = f"{base_color_name}-{tint_number}"
            lines.append(f'| style="background:{rgb_to_hex(color)}" | <div style="color:{fg_color};padding:15px 5px">{name}</div>')
    lines.append("|}")
    return "\n".join(lines)


def make_swatch_fg(colors, bg_color):
    lines = []
    for base_color_name, tints in colors.items():
        if lines:
            lines.append('|-')
        else:
            lines.append("{| " + f'style="background:{bg_color}"')
        for tint_number, color in tints.items():
            name = f"{base_color_name}-{tint_number}"
            lines.append(f'| <div style="color:{rgb_to_hex(color)};padding:5px">{name}</div>')
    lines.append("|}")
    return "\n".join(lines)


def make_palette(colors_light, colors_night):
    def wrap_frame(prefix, indent, suffix):
        def _wrap(lines):
            return prefix + "\n" + "\n".join(indent + line for line in lines) + "\n" + suffix
        return _wrap

    def gather_colors(colors):
        rules = ["/* Autogenerated with " + IDENTIFICATION + " */"]
        for base_color_name, tints in colors.items():
            for tint_number, color in tints.items():
                name = f"{base_color_name}-{tint_number}"
                rules.append(f"--wikt-palette-{name}: {rgb_to_hex(color)};")
        return rules

    light_block = wrap_frame(":root, .skin-invert, .notheme {", "\t", "}")
    night_block_1 = wrap_frame("@media screen {\n\thtml.skin-theme-clientpref-night {", "\t\t", "\t}\n}")
    night_block_2 = wrap_frame("@media screen and (prefers-color-scheme: dark) {\n\thtml.skin-theme-clientpref-os {".strip(), "\t\t", "\t}\n}")

    rules_light = gather_colors(colors_light)
    rules_night = gather_colors(colors_night)

    return light_block(rules_light) + "\n\n/* Styles need to be duplicated exactly between these two selectors. */\n" + night_block_1(rules_night) + "\n" + night_block_2(rules_night)


def print_swatches():
    print("==Swatches==")
    print("")

    print("===Light mode===")
    print("<!-- Autogenerated with " + IDENTIFICATION + " */ -->")
    print(make_swatch_bg(colors_light, BLACK_FG))
    print("")
    print(make_swatch_fg(colors_light, WHITE_BG))
    print("")

    print("===Night mode===")
    print("<!-- Autogenerated with " + IDENTIFICATION + " */ -->")
    print(make_swatch_bg(colors_night, WHITE_FG))
    print("")
    print(make_swatch_fg(colors_night, BLACK_BG))


def print_palette():
    print(make_palette(colors_light, colors_night))


if __name__ == "__main__":
    print_swatches()
    #print_palette()