In [1]:
import json
from pathlib import Path

import brushcue

ctx = brushcue.Context()

OUTPUT_PATH = (
    Path(brushcue.__file__).resolve().parents[3]
    / "writing/graphics/chapters/color-formats/assets/color-midpoints.json"
)

RED = (1.0, 0.0, 0.0)
GREEN = (0.0, 1.0, 0.0)
BLUE = (0.0, 0.0, 1.0)
YELLOW = (1.0, 1.0, 0.0)


def srgb(rgb: tuple[float, float, float]) -> brushcue.ProfiledColor:
    return brushcue.ProfiledColor.from_rgba_srgb(
        brushcue.RGBAColor.from_components(*rgb, 1.0)
    )


def to_srgb(color: brushcue.ProfiledColor):
    return color.to_rgb_encoded_with_color_profile(
        brushcue.ColorProfile.srgb()
    ).execute(ctx)


def midpoint(a: brushcue.ProfiledColor, b: brushcue.ProfiledColor) -> brushcue.ProfiledColor:
    """Average two colors in OkLab."""
    a_oklab_a = a.to_ok_lab_a()
    b_oklab_a = b.to_ok_lab_a()
    midpoint_oklab_a = brushcue.OkLabA.from_components(
        (a_oklab_a.l() + b_oklab_a.l()) / 2,
        (a_oklab_a.a() + b_oklab_a.a()) / 2,
        (a_oklab_a.b() + b_oklab_a.b()) / 2,
        1.0,
    )
    return brushcue.ProfiledColor.from_ok_lab_a(midpoint_oklab_a)


def mix(name: str, a_rgb, b_rgb, visualizable: bool = True) -> dict:
    a = srgb(a_rgb)
    b = srgb(b_rgb)
    return {
        "name": name,
        "a": to_srgb(a),
        "b": to_srgb(b),
        # Opponent colors (red/green, blue/yellow) have no mixture we can picture.
        "mix": to_srgb(midpoint(a, b)) if visualizable else None,
    }


mixes = []
[wgpu] using backend Vulkan — adapter 'NVIDIA GeForce RTX 5070' (DiscreteGpu), driver 'NVIDIA'
In [2]:
# reddish blue
mixes.append(mix("reddish-blue", RED, BLUE))
mixes[-1]
Out[2]:
{'name': 'reddish-blue',
 'a': (1.0, 0.0, 0.0, 1.0),
 'b': (0.0, 0.0, 1.0, 1.0),
 'mix': (0.5504043102264404, 0.32561108469963074, 0.6365207433700562, 1.0)}
In [3]:
# reddish yellow
mixes.append(mix("reddish-yellow", RED, YELLOW))
mixes[-1]
Out[3]:
{'name': 'reddish-yellow',
 'a': (1.0, 0.0, 0.0, 1.0),
 'b': (1.0, 1.0, 0.0, 1.0),
 'mix': (1.0, 0.628318190574646, 0.0010351595701649785, 1.0)}
In [4]:
# greenish blue
mixes.append(mix("greenish-blue", GREEN, BLUE))
mixes[-1]
Out[4]:
{'name': 'greenish-blue',
 'a': (0.0, 1.0, 0.0, 1.0),
 'b': (0.0, 0.0, 1.0, 1.0),
 'mix': (0.0, 0.6654877066612244, 0.7480067610740662, 1.0)}
In [5]:
# greenish yellow
mixes.append(mix("greenish-yellow", GREEN, YELLOW))
mixes[-1]
Out[5]:
{'name': 'greenish-yellow',
 'a': (0.0, 1.0, 0.0, 1.0),
 'b': (1.0, 1.0, 0.0, 1.0),
 'mix': (0.6914933919906616, 1.0, 0.0002833229082170874, 1.0)}

Red and green are opponent colors. There is no reddish green we can visualize, so no mixture is computed.

In [6]:
# reddish green
mixes.append(mix("reddish-green", RED, GREEN, visualizable=False))
mixes[-1]
Out[6]:
{'name': 'reddish-green',
 'a': (1.0, 0.0, 0.0, 1.0),
 'b': (0.0, 1.0, 0.0, 1.0),
 'mix': None}
In [7]:
# blueish yellow
mixes.append(mix("blueish-yellow", BLUE, YELLOW, visualizable=False))
mixes[-1]
In [8]:
OUTPUT_PATH.write_text(json.dumps(mixes, indent=4))
Out[8]:
1701