Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b580f47679 | |||
| b2e80a9f35 | |||
| 534f2423e9 | |||
| b5b9b12174 | |||
| 4ee9e23c5c | |||
| a5256aaa62 |
1
.gitignore
vendored
@@ -17,7 +17,6 @@ docs/_build/
|
|||||||
# local
|
# local
|
||||||
/Makefile
|
/Makefile
|
||||||
|
|
||||||
data/
|
|
||||||
archive/
|
archive/
|
||||||
|
|
||||||
notebooks/color_spaces_manyview.ipynb
|
notebooks/color_spaces_manyview.ipynb
|
||||||
|
|||||||
11
README.md
@@ -32,11 +32,11 @@ smoothly as a function of lightness within sRGB gamut bounds.
|
|||||||
|
|
||||||
| Chroma curves | Color trajectories |
|
| Chroma curves | Color trajectories |
|
||||||
|----------------------------------------------------------|------------------------------------------|
|
|----------------------------------------------------------|------------------------------------------|
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
| Palette |
|
| Palette |
|
||||||
|----------------------------------------------|
|
|----------------------------------------------|
|
||||||
|  |
|
|  |
|
||||||
|
|
||||||
Chroma curves are designed specifically to establish a distinct role for each
|
Chroma curves are designed specifically to establish a distinct role for each
|
||||||
accent and are non-intersecting over the lightness domain (hence the distinct
|
accent and are non-intersecting over the lightness domain (hence the distinct
|
||||||
@@ -143,6 +143,13 @@ using the `monobiome` CLI:
|
|||||||
(`grassland`). Every part of this process can be customized: the scheme
|
(`grassland`). Every part of this process can be customized: the scheme
|
||||||
parameters, the scheme definitions/file, the app template.
|
parameters, the scheme definitions/file, the app template.
|
||||||
|
|
||||||
|
The separation of duties here facilitates robustness: the palette colors can be
|
||||||
|
tweaked (across versions, say) without changing how those colors get used in
|
||||||
|
app themes. Scheme files don't hardcode color values, and app templates don't
|
||||||
|
refer to palette variables at all. Scheme files bridge palette colors with
|
||||||
|
*intended uses*, and templates need only align those uses under an app's
|
||||||
|
expected config syntax.
|
||||||
|
|
||||||
Running these commands in sequence from the repo root should work
|
Running these commands in sequence from the repo root should work
|
||||||
out-of-the-box after having installed the CLI tool.
|
out-of-the-box after having installed the CLI tool.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version = "1.5.1"
|
version = "1.5.4"
|
||||||
|
|
||||||
[alpine]
|
[alpine]
|
||||||
l10 = "#030303"
|
l10 = "#030303"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version = "1.5.1"
|
version = "1.5.4"
|
||||||
|
|
||||||
[alpine]
|
[alpine]
|
||||||
l10 = "oklch(10.0% 0.0000 0.0)"
|
l10 = "oklch(10.0% 0.0000 0.0)"
|
||||||
|
|||||||
BIN
images/release/1.5.2/chroma-bounds.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
images/release/1.5.2/chroma-curves.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
images/release/1.5.2/palette-bare.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
images/release/1.5.2/palette.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
images/release/1.5.3/chroma-bounds.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
images/release/1.5.3/chroma-curves.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
images/release/1.5.3/palette-bare.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
images/release/1.5.3/palette.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
images/release/1.5.4/chroma-bounds.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
images/release/1.5.4/chroma-curves.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
images/release/1.5.4/palette-bare.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
images/release/1.5.4/palette.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
65
monobiome/data/parameters.toml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
L_min = 10
|
||||||
|
L_max = 98
|
||||||
|
L_step = 5
|
||||||
|
|
||||||
|
[monotone_C_map]
|
||||||
|
alpine = 0
|
||||||
|
badlands = 0.011
|
||||||
|
chaparral = 0.011
|
||||||
|
savanna = 0.011
|
||||||
|
grassland = 0.011
|
||||||
|
reef = 0.011
|
||||||
|
tundra = 0.011
|
||||||
|
heathland = 0.011
|
||||||
|
moorland = 0.011
|
||||||
|
|
||||||
|
[h_weights]
|
||||||
|
red = 3.0
|
||||||
|
orange = 3.8
|
||||||
|
yellow = 3.8
|
||||||
|
green = 3.8
|
||||||
|
cyan = 3.8
|
||||||
|
blue = 3.6
|
||||||
|
violet = 3.0
|
||||||
|
magenta = 3.6
|
||||||
|
|
||||||
|
[h_L_offsets]
|
||||||
|
red = 0
|
||||||
|
orange = -5.5
|
||||||
|
yellow = -13.5
|
||||||
|
green = -12.5
|
||||||
|
cyan = -10.5
|
||||||
|
blue = 9
|
||||||
|
violet = 7
|
||||||
|
magenta = 2
|
||||||
|
|
||||||
|
[h_C_offsets]
|
||||||
|
red = 0
|
||||||
|
orange = -0.015
|
||||||
|
yellow = -0.052
|
||||||
|
green = -0.08
|
||||||
|
cyan = -0.009
|
||||||
|
blue = -0.01
|
||||||
|
violet = -0.047
|
||||||
|
magenta = -0.1
|
||||||
|
|
||||||
|
[monotone_h_map]
|
||||||
|
alpine = 0
|
||||||
|
badlands = 29
|
||||||
|
chaparral = 62.5
|
||||||
|
savanna = 104
|
||||||
|
grassland = 148
|
||||||
|
reef = 205
|
||||||
|
tundra = 262
|
||||||
|
heathland = 306
|
||||||
|
moorland = 350
|
||||||
|
|
||||||
|
[accent_h_map]
|
||||||
|
red = 29
|
||||||
|
orange = 62.5
|
||||||
|
yellow = 104
|
||||||
|
green = 148
|
||||||
|
cyan = 205
|
||||||
|
blue = 262
|
||||||
|
violet = 306
|
||||||
|
magenta = 350
|
||||||
@@ -5,6 +5,10 @@ from importlib.metadata import version
|
|||||||
|
|
||||||
from coloraide import Color
|
from coloraide import Color
|
||||||
|
|
||||||
|
from monobiome.util import (
|
||||||
|
hex_from_rgb8,
|
||||||
|
srgb8_from_color,
|
||||||
|
)
|
||||||
from monobiome.constants import (
|
from monobiome.constants import (
|
||||||
h_map,
|
h_map,
|
||||||
L_points,
|
L_points,
|
||||||
@@ -24,8 +28,8 @@ def compute_hlc_map(notation: str) -> dict[str, Any]:
|
|||||||
oklch = Color('oklch', [_l/100, _c, _h])
|
oklch = Color('oklch', [_l/100, _c, _h])
|
||||||
|
|
||||||
if notation == "hex":
|
if notation == "hex":
|
||||||
srgb = oklch.convert('srgb')
|
rgb8 = srgb8_from_color(oklch)
|
||||||
c_str = srgb.to_string(hex=True)
|
c_str = hex_from_rgb8(rgb8)
|
||||||
elif notation == "oklch":
|
elif notation == "oklch":
|
||||||
ol, oc, oh = oklch.convert('oklch').coords()
|
ol, oc, oh = oklch.convert('oklch').coords()
|
||||||
c_str = f"oklch({ol*100:.1f}% {oc:.4f} {oh:.1f})"
|
c_str = f"oklch({ol*100:.1f}% {oc:.4f} {oh:.1f})"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import matplotlib.pyplot as plt
|
|||||||
from coloraide import Color
|
from coloraide import Color
|
||||||
from matplotlib.collections import LineCollection
|
from matplotlib.collections import LineCollection
|
||||||
|
|
||||||
|
from monobiome.util import srgb8_from_color
|
||||||
from monobiome.palette import compute_hlc_map
|
from monobiome.palette import compute_hlc_map
|
||||||
from monobiome.constants import (
|
from monobiome.constants import (
|
||||||
h_map,
|
h_map,
|
||||||
@@ -72,13 +73,12 @@ def plot_hue_chroma_bounds() -> tuple[plt.Figure, plt.Axes]:
|
|||||||
|
|
||||||
return fig, axes
|
return fig, axes
|
||||||
|
|
||||||
|
|
||||||
def plot_hue_chroma_star() -> tuple[plt.Figure, plt.Axes]:
|
def plot_hue_chroma_star() -> tuple[plt.Figure, plt.Axes]:
|
||||||
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
|
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
|
||||||
|
|
||||||
# uncomment to preview 5 core term colors
|
|
||||||
colors = accent_h_map.keys()
|
colors = accent_h_map.keys()
|
||||||
#colors = set(["red", "orange", "yellow", "green", "blue"])
|
# uncomment to preview just the 5 core term colors
|
||||||
|
# colors = set(["red", "orange", "yellow", "green", "blue"])
|
||||||
|
|
||||||
for h_str, _ in max_Cstar_Horder:
|
for h_str, _ in max_Cstar_Horder:
|
||||||
Lpoints_Cstar = Lpoints_Cstar_Hmap[h_str]
|
Lpoints_Cstar = Lpoints_Cstar_Hmap[h_str]
|
||||||
@@ -88,34 +88,22 @@ def plot_hue_chroma_star() -> tuple[plt.Figure, plt.Axes]:
|
|||||||
|
|
||||||
_h = h_map[h_str]
|
_h = h_map[h_str]
|
||||||
h_colors = [
|
h_colors = [
|
||||||
Color('oklch', [_l/100, _c, _h]).convert("srgb")
|
Color(
|
||||||
|
'oklch', [_l/100, _c, _h]
|
||||||
|
).convert("srgb").fit(method="oklch-chroma")
|
||||||
for _l, _c in zip(L_points, Lpoints_Cstar, strict=True)
|
for _l, _c in zip(L_points, Lpoints_Cstar, strict=True)
|
||||||
]
|
]
|
||||||
|
|
||||||
# # ax.fill_between(
|
|
||||||
# ax.scatter(
|
|
||||||
# L_points,
|
|
||||||
# Lpoints_Cstar,
|
|
||||||
# alpha=0.7,
|
|
||||||
# c=h_colors,
|
|
||||||
# #alpha=0.2,
|
|
||||||
# #color='grey',
|
|
||||||
# label=h_str
|
|
||||||
# )
|
|
||||||
# x, y = L_points, Lpoints_Cstar_Hmap[h_str]
|
|
||||||
# n = int(0.45*len(x))
|
|
||||||
# ax.text(x[n], y[n]-0.01, h_str, rotation=10, va='center', ha='left')
|
|
||||||
|
|
||||||
x = np.asarray(L_points)
|
x = np.asarray(L_points)
|
||||||
y = np.asarray(Lpoints_Cstar)
|
y = np.asarray(Lpoints_Cstar)
|
||||||
pts = np.column_stack([x, y]).reshape(-1, 1, 2)
|
pts = np.column_stack([x, y]).reshape(-1, 1, 2)
|
||||||
segs = np.concatenate([pts[:-1], pts[1:]], axis=1)
|
segs = np.concatenate([pts[:-1], pts[1:]], axis=1)
|
||||||
rgb = np.asarray(h_colors)
|
rgb = np.asarray(h_colors)
|
||||||
seg_colors = (rgb[:-1] + rgb[1:]) / 2
|
seg_colors = (rgb[:-1] + rgb[1:]) / 2
|
||||||
|
|
||||||
lc = LineCollection(segs, colors=seg_colors, linewidth=3,
|
lc = LineCollection(segs, colors=seg_colors, linewidth=3,
|
||||||
capstyle="round", joinstyle="round",
|
capstyle="round", joinstyle="round",
|
||||||
label=h_str)
|
label=h_str)
|
||||||
|
|
||||||
ax.add_collection(lc)
|
ax.add_collection(lc)
|
||||||
ax.autoscale_view()
|
ax.autoscale_view()
|
||||||
|
|
||||||
@@ -126,12 +114,11 @@ def plot_hue_chroma_star() -> tuple[plt.Figure, plt.Axes]:
|
|||||||
|
|
||||||
return fig, ax
|
return fig, ax
|
||||||
|
|
||||||
|
|
||||||
def palette_image(
|
def palette_image(
|
||||||
palette: dict[str, dict[int, str]],
|
palette: dict[str, dict[int, str]],
|
||||||
cell_size: int = 40,
|
cell_size: int = 40,
|
||||||
keys: list[str] | None = None
|
keys: list[str] | None = None
|
||||||
) -> tuple[np.ndarray, list[str], list[list[int]], int, int]:
|
) -> tuple[np.ndarray, list[str], list[list[int]]]:
|
||||||
names = list(palette.keys()) if keys is None else keys
|
names = list(palette.keys()) if keys is None else keys
|
||||||
|
|
||||||
row_count = len(names)
|
row_count = len(names)
|
||||||
@@ -140,7 +127,7 @@ def palette_image(
|
|||||||
|
|
||||||
h = row_count * cell_size
|
h = row_count * cell_size
|
||||||
w = max_cols * cell_size
|
w = max_cols * cell_size
|
||||||
img = np.ones((h, w, 3), float)
|
img = np.ones((h, w, 3), int)
|
||||||
|
|
||||||
lightness_keys_per_row = []
|
lightness_keys_per_row = []
|
||||||
|
|
||||||
@@ -149,14 +136,12 @@ def palette_image(
|
|||||||
lkeys = sorted(shades.keys())
|
lkeys = sorted(shades.keys())
|
||||||
lightness_keys_per_row.append(lkeys)
|
lightness_keys_per_row.append(lkeys)
|
||||||
for c, k in enumerate(lkeys):
|
for c, k in enumerate(lkeys):
|
||||||
col = Color(shades[k]).convert("srgb").fit(method="clip")
|
rgb = srgb8_from_color(shades[k])
|
||||||
rgb = [col["r"], col["g"], col["b"]]
|
|
||||||
r0, r1 = r * cell_size, (r + 1) * cell_size
|
r0, r1 = r * cell_size, (r + 1) * cell_size
|
||||||
c0, c1 = c * cell_size, (c + 1) * cell_size
|
c0, c1 = c * cell_size, (c + 1) * cell_size
|
||||||
img[r0:r1, c0:c1, :] = rgb
|
img[r0:r1, c0:c1, :] = rgb
|
||||||
|
|
||||||
return img, names, lightness_keys_per_row, cell_size, max_cols
|
return img, names, lightness_keys_per_row
|
||||||
|
|
||||||
|
|
||||||
def show_palette(
|
def show_palette(
|
||||||
palette: dict[str, dict[int, str]],
|
palette: dict[str, dict[int, str]],
|
||||||
@@ -165,9 +150,7 @@ def show_palette(
|
|||||||
show_labels: bool = True,
|
show_labels: bool = True,
|
||||||
dpi: int = 100,
|
dpi: int = 100,
|
||||||
) -> tuple[plt.Figure, plt.Axes]:
|
) -> tuple[plt.Figure, plt.Axes]:
|
||||||
img, names, keys, cell_size, max_cols = palette_image(
|
img, names, _ = palette_image(palette, cell_size, keys=keys)
|
||||||
palette, cell_size, keys=keys
|
|
||||||
)
|
|
||||||
|
|
||||||
fig_w = img.shape[1] / 100
|
fig_w = img.shape[1] / 100
|
||||||
fig_h = img.shape[0] / 100
|
fig_h = img.shape[0] / 100
|
||||||
@@ -175,7 +158,7 @@ def show_palette(
|
|||||||
if show_labels:
|
if show_labels:
|
||||||
fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=dpi)
|
fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=dpi)
|
||||||
|
|
||||||
ax.imshow(img, interpolation="none", origin="upper")
|
ax.imshow(img, interpolation="nearest", origin="upper")
|
||||||
ax.set_xticks([])
|
ax.set_xticks([])
|
||||||
|
|
||||||
ytick_pos = [(i + 0.5) * cell_size for i in range(len(names))]
|
ytick_pos = [(i + 0.5) * cell_size for i in range(len(names))]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import math
|
|||||||
from types import GenericAlias
|
from types import GenericAlias
|
||||||
from argparse import ArgumentParser, _SubParsersAction
|
from argparse import ArgumentParser, _SubParsersAction
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
from coloraide import Color
|
from coloraide import Color
|
||||||
|
|
||||||
_SubParsersAction.__class_getitem__ = classmethod(GenericAlias)
|
_SubParsersAction.__class_getitem__ = classmethod(GenericAlias)
|
||||||
@@ -33,3 +34,13 @@ def oklch_distance(xc: Color, yc: Color) -> float:
|
|||||||
dz = l1 - l2
|
dz = l1 - l2
|
||||||
|
|
||||||
return (dx**2 + dy**2 + dz**2)**0.5
|
return (dx**2 + dy**2 + dz**2)**0.5
|
||||||
|
|
||||||
|
def srgb8_from_color(c: str | Color) -> np.ndarray:
|
||||||
|
c = Color(c).convert("srgb").fit(method="oklch-chroma")
|
||||||
|
rgb = np.array([c["r"], c["g"], c["b"]], dtype=float)
|
||||||
|
rgb8 = np.clip(np.round(rgb * 255), 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
return rgb8
|
||||||
|
|
||||||
|
def hex_from_rgb8(rgb8: np.ndarray) -> str:
|
||||||
|
return f"#{int(rgb8[0]):02x}{int(rgb8[1]):02x}{int(rgb8[2]):02x}"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "monobiome"
|
name = "monobiome"
|
||||||
version = "1.5.1"
|
version = "1.5.4"
|
||||||
description = "Monobiome color palette"
|
description = "Monobiome color palette"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ fig.savefig(Path(figure_dir, "chroma-curves.png"))
|
|||||||
# "oklch" causes some slight hex drift when later using an eyedropper
|
# "oklch" causes some slight hex drift when later using an eyedropper
|
||||||
hlc_map = compute_hlc_map("hex") # ("oklch")
|
hlc_map = compute_hlc_map("hex") # ("oklch")
|
||||||
|
|
||||||
fig, ax = plotting.show_palette(hlc_map)
|
fig, ax = plotting.show_palette(hlc_map, cell_size=25)
|
||||||
fig.savefig(Path(figure_dir, "palette.png"))
|
fig.savefig(Path(figure_dir, "palette.png"), pad_inches=0)
|
||||||
|
|
||||||
fig, ax = plotting.show_palette(hlc_map, show_labels=False)
|
fig, ax = plotting.show_palette(hlc_map, show_labels=False)
|
||||||
fig.savefig(Path(figure_dir, "palette-bare.png"), pad_inches=0)
|
fig.savefig(Path(figure_dir, "palette-bare.png"), pad_inches=0)
|
||||||
|
|||||||
@@ -11,3 +11,6 @@ uv run monobiome palette -n oklch -f toml -o colors/oklch-palette.toml
|
|||||||
|
|
||||||
# generate provided app config
|
# generate provided app config
|
||||||
"$script_dir/generate.sh"
|
"$script_dir/generate.sh"
|
||||||
|
|
||||||
|
# generate release plots
|
||||||
|
uv run "$script_dir/plots.py"
|
||||||
|
|||||||