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