14 Commits

76 changed files with 1091 additions and 38 deletions

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@ archive/
notebooks/color_spaces_manyview.ipynb
notebooks/oklch_srgb_spherical.ipynb
notebooks/v1.4.0/

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

3
monobiome/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from importlib.metadata import version
__version__ = version("monobiome")

19
monobiome/__main__.py Normal file
View File

@@ -0,0 +1,19 @@
from monobiome.cli import create_parser, configure_logging
def main() -> None:
parser = create_parser()
args = parser.parse_args()
# skim off log level to handle higher-level option
if hasattr(args, "log_level") and args.log_level is not None:
configure_logging(args.log_level)
if "func" in args:
args.func(args)
else:
parser.print_help()
if __name__ == "__main__":
main()

32
monobiome/cli/__init__.py Normal file
View File

@@ -0,0 +1,32 @@
import logging
import argparse
from monobiome.cli import scheme, palette
logger: logging.Logger = logging.getLogger(__name__)
def configure_logging(log_level: int) -> None:
"""
Configure logger's logging level.
"""
logger.setLevel(log_level)
def create_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Accent modeling CLI",
)
parser.add_argument(
"--log-level",
type=int,
metavar="int",
choices=[10, 20, 30, 40, 50],
help="Log level: 10=DEBUG, 20=INFO, 30=WARNING, 40=ERROR, 50=CRITICAL",
)
subparsers = parser.add_subparsers(help="subcommand help")
palette.register_parser(subparsers)
scheme.register_parser(subparsers)
return parser

51
monobiome/cli/palette.py Normal file
View File

@@ -0,0 +1,51 @@
import argparse
from pathlib import Path
from monobiome.util import _SubparserType
from monobiome.palette import generate_palette
def register_parser(subparsers: _SubparserType) -> None:
parser = subparsers.add_parser(
"palette",
help="generate primary palette"
)
parser.add_argument(
"-n",
"--notation",
type=str,
default="hex",
choices=["hex", "oklch"],
help="Color notation to export (either hex or oklch)",
)
parser.add_argument(
"-f",
"--format",
type=str,
default="toml",
choices=["json", "toml"],
help="Format of palette file (either JSON or TOML)",
)
parser.add_argument(
"-o",
"--output",
type=str,
help="Output file to write palette content",
)
parser.set_defaults(func=handle_palette)
def handle_palette(args: argparse.Namespace) -> None:
notation = args.notation
file_format = args.format
output = args.output
palette_text = generate_palette(notation, file_format)
if output is None:
print(palette_text)
else:
with Path(output).open("w") as f:
f.write(palette_text)

155
monobiome/cli/scheme.py Normal file
View File

@@ -0,0 +1,155 @@
import argparse
from pathlib import Path
from monobiome.util import _SubparserType
from monobiome.scheme import generate_scheme
from monobiome.constants import monotone_h_map
def register_parser(subparsers: _SubparserType) -> None:
parser = subparsers.add_parser(
"scheme",
help="create scheme variants"
)
parser.add_argument(
"mode",
type=str,
choices=["dark", "light"],
help="Scheme mode (light or dark)"
)
parser.add_argument(
"biome",
type=str,
choices=list(monotone_h_map.keys()),
help="Biome setting for scheme."
)
parser.add_argument(
"-m",
"--metric",
type=str,
default="oklch",
choices=["wcag", "oklch", "lightness"],
help="Metric to use for measuring swatch distances."
)
# e.g., wcag=4.5; oklch=0.40; lightness=40
parser.add_argument(
"-d",
"--distance",
type=float,
default=0.40,
help="Distance threshold for specified metric",
)
parser.add_argument(
"-o",
"--output",
type=str,
help="Output file to write scheme content",
)
# these params remain rooted in lightness; no need to accommodate metric
# given these are monotone adjustments. You *could* consider rooting these
# in metric units, but along monotones, distance=lightness and WCAG isn't a
# particularly good measure of perceptual distinction, so we'd prefer the
# former.
parser.add_argument(
"-l",
"--l-base",
type=int,
default=20,
help="Minimum lightness level (default: 20)",
)
parser.add_argument(
"--l-step",
type=int,
default=5,
help="Lightness step size (default: 5)",
)
# gaps
parser.add_argument(
"--fg-gap",
type=int,
default=50,
help="Foreground lightness gap (default: 50)",
)
parser.add_argument(
"--grey-gap",
type=int,
default=30,
help="Grey lightness gap (default: 30)",
)
parser.add_argument(
"--term-fg-gap",
type=int,
default=65,
help="Terminal foreground lightness gap (default: 60)",
)
parser.set_defaults(func=handle_scheme)
def handle_scheme(args: argparse.Namespace) -> None:
output = args.output
mode = args.mode
biome = args.biome
metric = args.metric
distance = args.distance
l_base = args.l_base
l_step = args.l_step
fg_gap = args.fg_gap
grey_gap = args.grey_gap
term_fg_gap = args.term_fg_gap
full_color_map = {
"red": "red",
"orange": "orange",
"yellow": "yellow",
"green": "green",
"cyan": "cyan",
"blue": "blue",
"violet": "violet",
"magenta": "orange",
}
term_color_map = {
"red": "red",
"yellow": "yellow",
"green": "green",
"cyan": "blue",
"blue": "blue",
"magenta": "orange",
}
vim_color_map = {
"red": "red",
"orange": "orange",
"yellow": "yellow",
"green": "green",
"cyan": "green",
"blue": "blue",
"violet": "blue",
"magenta": "red",
}
# vim_color_map = full_color_map
scheme_text = generate_scheme(
mode,
biome,
metric,
distance,
l_base,
l_step,
fg_gap,
grey_gap,
term_fg_gap,
full_color_map,
term_color_map,
vim_color_map,
)
if output is None:
print(scheme_text)
else:
with Path(output).open("w") as f:
f.write(scheme_text)

123
monobiome/constants.py Normal file
View File

@@ -0,0 +1,123 @@
import tomllib
from importlib.resources import files
import numpy as np
from monobiome.curve import (
l_maxC_h,
bezier_y_at_x,
)
parameters_file = files("monobiome.data") / "parameters.toml"
parameters = tomllib.load(parameters_file.open("rb"))
L_min: int = parameters.get("L_min", 10)
L_max: int = parameters.get("L_max", 98)
L_step: int = parameters.get("L_step", 5)
L_points: list[int] = list(range(L_min, L_max+1))
# L-space just affects accuracy of chroma max
L_space = np.arange(0, 100 + L_step, L_step)
monotone_C_map = parameters.get("monotone_C_map", {})
h_weights = parameters.get("h_weights", {})
h_L_offsets = parameters.get("h_L_offsets", {})
h_C_offsets = parameters.get("h_C_offsets", {})
monotone_h_map = parameters.get("monotone_h_map", {})
accent_h_map = parameters.get("accent_h_map", {})
h_map = {**monotone_h_map, **accent_h_map}
"""
Compute chroma maxima at provided lightness levels across hues.
A map with max chroma values for each hue across lightness space
{
"red": [ Cmax@L=10, Cmax@L=11, Cmax@L=12, ... ],
"orange": [ Cmax@L=10, Cmax@L=11, Cmax@L=12, ... ],
...
}
"""
Lspace_Cmax_Hmap = {
h_str: [l_maxC_h(_L, _h) for _L in L_space]
for h_str, _h in h_map.items()
}
"""
Set QBR curves, *unbounded* chroma curves for all hues
1. Raw bezier chroma values for each hue across the lightness space
Lpoints_Cqbr_Hmap = {
"red": [ Bezier@L=10, Bezier@L=11, Bezier@L=12, ... ],
...
}
2. Three bezier control points for each hue's chroma curve
QBR_ctrl_Hmap = {
"red": np.array([
[ x1, y1 ],
[ x2, y2 ],
[ x3, y3 ]
]),
...
}
"""
Lpoints_Cqbr_Hmap = {}
QBR_ctrl_Hmap = {}
for h_str, _h in monotone_h_map.items():
Lpoints_Cqbr_Hmap[h_str] = np.array(
[monotone_C_map[h_str]]*len(L_points)
)
for h_str, _h in accent_h_map.items():
Lspace_Cmax = Lspace_Cmax_Hmap[h_str]
# get L value of max chroma; will be a bezier control
L_Cmax_idx = np.argmax(Lspace_Cmax)
L_Cmax = L_space[L_Cmax_idx]
# offset control point by any preset x-shift
L_Cmax += h_L_offsets[h_str]
# and get max C at the L offset
Cmax = l_maxC_h(L_Cmax, _h)
# set 3 control points; shift by any global linear offest
C_offset = h_C_offsets.get(h_str, 0)
p_0 = np.array([0, 0])
p_Cmax = np.array([L_Cmax, Cmax + C_offset])
p_100 = np.array([100, 0])
B_L_points = bezier_y_at_x(
p_0, p_Cmax, p_100,
h_weights.get(h_str, 1),
L_points
)
Lpoints_Cqbr_Hmap[h_str] = B_L_points
QBR_ctrl_Hmap[h_str] = np.vstack([p_0, p_Cmax, p_100])
"""
Bezier chroma values, but bounded to attainable gamut colors (bezier fit
can produce invalid chroma values)
h_L_points_Cstar = {
"red": [ bounded-bezier@L=10, bounded-bezier@L=11, ... ],
...
}
"""
Lpoints_Cstar_Hmap = {}
for h_str, L_points_C in Lpoints_Cqbr_Hmap.items():
_h = h_map[h_str]
Lpoints_Cstar_Hmap[h_str] = [
max(0, min(_C, l_maxC_h(_L, _h)))
for _L, _C in zip(L_points, L_points_C, strict=True)
]

77
monobiome/curve.py Normal file
View File

@@ -0,0 +1,77 @@
from functools import cache
import numpy as np
from coloraide import Color
def quad_bezier_rational(
P0: float,
P1: float,
P2: float,
w: float,
t: np.array,
) -> np.array:
"""
Compute the point values of a quadratic rational Bezier curve.
Uses `P0`, `P1`, and `P2` as the three control points of the curve. `w`
controls the weight toward the middle control point ("sharpness" of the
curve"), and `t` is the number of sample points used along the curve.
"""
t = np.asarray(t)[:, None]
num = (1-t)**2*P0 + 2*w*(1-t)*t*P1 + t**2*P2
den = (1-t)**2 + 2*w*(1-t)*t + t**2
return num / den
def bezier_y_at_x(
P0: float,
P1: float,
P2: float,
w: float,
x: float,
n: int = 400,
) -> np.array:
"""
For the provided QBR parameters, provide the curve value at the given
input.
"""
t = np.linspace(0, 1, n)
B = quad_bezier_rational(P0, P1, P2, w, t)
x_vals, y_vals = B[:, 0], B[:, 1]
return np.interp(x, x_vals, y_vals)
@cache
def l_maxC_h(
_l: float,
_h: float,
space: str = 'srgb',
eps: float = 1e-6,
tol: float = 1e-9
) -> float:
"""
Binary search for max attainable OKLCH chroma at fixed lightness and hue.
Parameters:
_l: lightness
_h: hue
Returns:
Max in-gamut chroma at provided lightness and hue
"""
def chroma_in_gamut(_c: float) -> bool:
color = Color('oklch', [_l/100, _c, _h])
return color.convert(space).in_gamut(tolerance=tol)
lo, hi = 0.0, 0.1
while chroma_in_gamut(hi):
hi *= 2
while hi - lo > eps:
m = (lo + hi) / 2
lo, hi = (m, hi) if chroma_in_gamut(m) else (lo, m)
return lo

54
monobiome/palette.py Normal file
View File

@@ -0,0 +1,54 @@
import json
from functools import cache
from importlib.metadata import version
from coloraide import Color
from monobiome.constants import (
h_map,
L_points,
Lpoints_Cstar_Hmap,
)
@cache
def compute_hlc_map(notation: str) -> dict[str, dict[int, str]]:
hlc_map = {}
for h_str, Lpoints_Cstar in Lpoints_Cstar_Hmap.items():
_h = h_map[h_str]
hlc_map[h_str] = {}
for _l, _c in zip(L_points, Lpoints_Cstar, strict=True):
oklch = Color('oklch', [_l/100, _c, _h])
if notation == "hex":
srgb = oklch.convert('srgb')
c_str = srgb.to_string(hex=True)
elif notation == "oklch":
ol, oc, oh = oklch.convert('oklch').coords()
c_str = f"oklch({ol*100:.1f}% {oc:.4f} {oh:.1f})"
hlc_map[h_str][_l] = c_str
return hlc_map
def generate_palette(
notation: str,
file_format: str,
) -> str:
mb_version = version("monobiome")
hlc_map = compute_hlc_map(notation)
if file_format == "json":
hlc_map["version"] = mb_version
return json.dumps(hlc_map, indent=4)
else:
toml_lines = [f"version = {mb_version}", ""]
for _h, _lc_map in hlc_map.items():
toml_lines.append(f"[{_h}]")
for _l, _c in _lc_map.items():
toml_lines.append(f'l{_l} = "{_c}"')
toml_lines.append("")
return "\n".join(toml_lines)

176
monobiome/plotting.py Normal file
View File

@@ -0,0 +1,176 @@
import numpy as np
import matplotlib.pyplot as plt
from monobiome.constants import (
h_map,
L_space,
L_points,
accent_h_map,
monotone_h_map,
Lspace_Cmax_Hmap,
Lpoints_Cstar_Hmap,
)
def plot_hue_chroma_bounds() -> None:
name_h_map = {}
ax_h_map = {}
fig, axes = plt.subplots(
len(monotone_h_map),
1,
sharex=True,
sharey=True,
figsize=(4, 10)
)
for i, h_str in enumerate(Lpoints_Cstar_Hmap):
_h = h_map[h_str]
l_space_Cmax = Lspace_Cmax_Hmap[h_str]
l_points_Cstar = Lpoints_Cstar_Hmap[h_str]
if _h not in ax_h_map:
ax_h_map[_h] = axes[i]
ax = ax_h_map[_h]
if _h not in name_h_map:
name_h_map[_h] = []
name_h_map[_h].append(h_str)
# plot Cmax and Cstar
ax.plot(L_space, l_space_Cmax, c="g", alpha=0.3, label="Cmax")
cstar_label = f"{'accent' if h_str in accent_h_map else 'monotone'} C*"
ax.plot(L_points, l_points_Cstar, alpha=0.7, label=cstar_label)
ax.title.set_text(f"Hue [${_h}$] - {'|'.join(name_h_map[_h])}")
axes[-1].set_xlabel("Lightness (%)")
axes[-1].set_xticks([L_points[0], L_points[-1]])
fig.tight_layout()
fig.subplots_adjust(top=0.9)
handles, labels = axes[-1].get_legend_handles_labels()
unique = dict(zip(labels, handles))
fig.legend(unique.values(), unique.keys(), loc='lower center', bbox_to_anchor=(0.5, -0.06), ncol=3)
plt.suptitle("$C^*$ curves for hue groups")
plt.show()
def plot_hue_chroma_star() -> None:
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
# uncomment to preview 5 core term colors
colors = accent_h_map.keys()
#colors = set(["red", "orange", "yellow", "green", "blue"])
for h_str in Lpoints_Cstar_Hmap:
if h_str not in accent_h_map or h_str not in colors:
continue
ax.fill_between(
L_points,
Lpoints_Cstar_Hmap[h_str],
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')
ax.set_xlabel("Lightness (%)")
ax.set_xticks([L_points[0], 45, 50, 55, 60, 65, 70, L_points[-1]])
plt.suptitle("$C^*$ curves (v1.4.0)")
fig.show()
def palette_image(palette, cell_size=40, keys=None):
if keys is None:
names = list(palette.keys())
else:
names = keys
row_count = len(names)
col_counts = [len(palette[n]) for n in names]
max_cols = max(col_counts)
h = row_count * cell_size
w = max_cols * cell_size
img = np.ones((h, w, 3), float)
lightness_keys_per_row = []
for r, name in enumerate(names):
shades = palette[name]
keys = sorted(shades.keys())
lightness_keys_per_row.append(keys)
for c, k in enumerate(keys):
col = Color(shades[k]).convert("srgb").fit(method="clip")
rgb = [col["r"], col["g"], col["b"]]
r0, r1 = r * cell_size, (r + 1) * cell_size
c0, c1 = c * cell_size, (c + 1) * cell_size
img[r0:r1, c0:c1, :] = rgb
return img, names, lightness_keys_per_row, cell_size, max_cols
def show_palette(palette, cell_size=40, keys=None):
img, names, keys, cell_size, max_cols = palette_image(palette, cell_size, keys=keys)
fig_w = img.shape[1] / 100
fig_h = img.shape[0] / 100
fig, ax = plt.subplots(figsize=(fig_w, fig_h))
ax.imshow(img, interpolation="none", origin="upper")
ax.set_xticks([])
ytick_pos = [(i + 0.5) * cell_size for i in range(len(names))]
ax.set_yticks(ytick_pos)
ax.set_yticklabels(names)
ax.set_ylim(img.shape[0], 0) # ensures rows render correctly without half-cells
plt.show()
if __name__ == "__main__":
from monobiome.constants import OKLCH_hL_dict
keys = [
"alpine",
"badlands",
"chaparral",
"savanna",
"grassland",
"reef",
"tundra",
"heathland",
"moorland",
"orange",
"yellow",
"green",
"cyan",
"blue",
"violet",
"magenta",
"red",
]
term_keys = [
"alpine",
"badlands",
"chaparral",
"savanna",
"grassland",
"tundra",
"red",
"orange",
"yellow",
"green",
"blue",
]
show_palette(OKLCH_hL_dict, cell_size=25, keys=keys)
# show_palette(OKLCH_hL_dict, cell_size=1, keys=term_keys)

254
monobiome/scheme.py Normal file
View File

@@ -0,0 +1,254 @@
from functools import cache
from collections.abc import Callable
from coloraide import Color
from monobiome.util import oklch_distance
from monobiome.palette import compute_hlc_map
from monobiome.constants import (
accent_h_map,
monotone_h_map,
)
@cache
def compute_dma_map(
dT: float,
metric: Callable | None = None
) -> dict[str, dict]:
"""
For threshold `dT`, compute the nearest accent shades that exceed that
threshold for every monotone shade.
Returns: map of minimum constraint satisfying accent colors for monotone
spectra
{
"alpine": {
"oklch( ... )": {
"red": *nearest oklch >= dT from M base*,
...
},
...
},
...
}
"""
if metric is None:
metric = oklch_distance
oklch_hlc_map = compute_hlc_map("oklch")
oklch_color_map = {
c_name: [Color(c_str) for c_str in c_str_dict.values()]
for c_name, c_str_dict in oklch_hlc_map.items()
}
dT_mL_acol_map = {}
for m_name in monotone_h_map:
mL_acol_map = {}
m_colors = oklch_color_map[m_name]
for m_color in m_colors:
acol_min_map = {}
for a_name in accent_h_map:
a_colors = oklch_color_map[a_name]
oklch_dists = filter(
lambda d: (d[1] - dT) >= 0,
[
(ac, metric(m_color, ac))
for ac in a_colors
]
)
oklch_dists = list(oklch_dists)
if oklch_dists:
min_a_color = min(oklch_dists, key=lambda t: t[1])[0]
acol_min_map[a_name] = min_a_color
# make sure the current monotone level has *all* accents; o/w
# ignore
if len(acol_min_map) < len(accent_h_map):
continue
mL = m_color.coords()[0]
mL_acol_map[int(mL*100)] = acol_min_map
dT_mL_acol_map[m_name] = mL_acol_map
return dT_mL_acol_map
def generate_scheme_groups(
mode: str,
biome: str,
metric: str,
distance: float,
l_base: int,
l_step: int,
fg_gap: int,
grey_gap: int,
term_fg_gap: int,
accent_color_map: dict[str, str],
) -> tuple[dict[str, str], ...]:
"""
Parameters:
mode: one of ["dark", "light"]
biome: biome setting
metric: one of ["wcag", "oklch", "lightness"]
"""
metric_map = {
"wcag": lambda mc,ac: ac.contrast(mc, method='wcag21'),
"oklch": oklch_distance,
"lightness": lambda mc,ac: abs(mc.coords()[0]-ac.coords()[0])*100,
}
metric_func = metric_map[metric]
dT_mL_acol_map = compute_dma_map(distance, metric=metric_func)
Lma_map = {
m_name: mL_acol_dict[l_base]
for m_name, mL_acol_dict in dT_mL_acol_map.items()
if l_base in mL_acol_dict
}
# the `mL_acol_dict` only includes lightnesses where all accent colors were
# within threshold. Coverage here will be partial if, at the `mL`, there is
# some monotone base that doesn't have all accents within threshold. This
# can happen at the edge, e.g., alpine@L15 has all accents w/in the
# distance, but the red accent was too far under tundra@L15, so there's no
# entry. This particular case is fairly rare; it's more likely that *all*
# monotones are undefined. Either way, both such cases lead to partial
# scheme coverage.
if len(Lma_map) < len(monotone_h_map):
print(f"Warning: partial scheme coverage for {l_base=}@{distance=}")
if biome not in Lma_map:
print(f"Biome {biome} unable to meet {metric} constraints")
accent_colors = Lma_map.get(biome, {})
meta_pairs = [
("mode", mode),
("biome", biome),
("metric", metric),
("distance", distance),
("l_base", l_base),
("l_step", l_step),
("fg_gap", fg_gap),
("grey_gap", grey_gap),
("term_fg_gap", term_fg_gap),
]
# note how selection_bg steps up by `l_step`, selection_fg steps down by
# `l_step` (from their respective bases)
term_pairs = [
("background", f"f{{{{{biome}.l{l_base}}}}}"),
("selection_bg", f"f{{{{{biome}.l{l_base+l_step}}}}}"),
("selection_fg", f"f{{{{{biome}.l{l_base+term_fg_gap-l_step}}}}}"),
("foreground", f"f{{{{{biome}.l{l_base+term_fg_gap}}}}}"),
("cursor", f"f{{{{{biome}.l{l_base+term_fg_gap-l_step}}}}}"),
("cursor_text", f"f{{{{{biome}.l{l_base+l_step}}}}}"),
]
monotone_pairs = []
monotone_pairs += [
(f"bg{i}", f"f{{{{{biome}.l{l_base+i*l_step}}}}}")
for i in range(4)
]
monotone_pairs += [
(f"fg{3-i}", f"f{{{{{biome}.l{fg_gap+l_base+i*l_step}}}}}")
for i in range(4)
]
accent_pairs = [
("black", f"f{{{{{biome}.l{l_base}}}}}"),
("grey", f"f{{{{{biome}.l{l_base+grey_gap}}}}}"),
("white", f"f{{{{{biome}.l{l_base+term_fg_gap-2*l_step}}}}}"),
]
for color_name, mb_accent in accent_color_map.items():
aL = int(100*accent_colors[mb_accent].coords()[0])
accent_pairs.append(
(
color_name,
f"f{{{{{mb_accent}.l{aL}}}}}"
)
)
return meta_pairs, term_pairs, monotone_pairs, accent_pairs
def generate_scheme(
mode: str,
biome: str,
metric: str,
distance: float,
l_base: int,
l_step: int,
fg_gap: int,
grey_gap: int,
term_fg_gap: int,
full_color_map: dict[str, str],
term_color_map: dict[str, str],
vim_color_map: dict[str, str],
) -> str:
l_sys = l_base
l_app = l_base + l_step
term_bright_offset = 10
# negate gaps if mode is light
if mode == "light":
l_step *= -1
fg_gap *= -1
grey_gap *= -1
term_fg_gap *= -1
term_bright_offset *= -1
meta, _, mt, ac = generate_scheme_groups(
mode, biome, metric, distance,
l_sys, l_step,
fg_gap, grey_gap, term_fg_gap,
full_color_map
)
_, term, _, term_norm_ac = generate_scheme_groups(
mode, biome, metric, distance,
l_app, l_step,
fg_gap, grey_gap, term_fg_gap,
term_color_map
)
_, _, _, term_bright_ac = generate_scheme_groups(
mode, biome, metric, distance,
l_app + term_bright_offset, l_step,
fg_gap, grey_gap, term_fg_gap,
term_color_map
)
_, _, vim_mt, vim_ac = generate_scheme_groups(
mode, biome, metric, distance,
l_app, l_step,
fg_gap, grey_gap, term_fg_gap,
vim_color_map
)
def pair_strings(pair_list: list[tuple[str, str]]) -> list[str]:
return [
f"{lhs:<12} = \"{rhs}\""
for lhs, rhs in pair_list
]
scheme_pairs = []
scheme_pairs += pair_strings(meta)
scheme_pairs += pair_strings(mt)
scheme_pairs += pair_strings(ac)
scheme_pairs += ["", "[term]"]
scheme_pairs += pair_strings(term)
scheme_pairs += ["", "[term.normal]"]
scheme_pairs += pair_strings(term_norm_ac)
scheme_pairs += ["", "[term.bright]"]
scheme_pairs += pair_strings(term_bright_ac)
scheme_pairs += ["", "[vim]"]
scheme_pairs += pair_strings(vim_mt)
scheme_pairs += pair_strings(vim_ac)
return "\n".join(scheme_pairs)

35
monobiome/util.py Normal file
View File

@@ -0,0 +1,35 @@
import math
from types import GenericAlias
from argparse import ArgumentParser, _SubParsersAction
from coloraide import Color
_SubParsersAction.__class_getitem__ = classmethod(GenericAlias)
_SubparserType = _SubParsersAction[ArgumentParser]
def oklch_distance(xc: Color, yc: Color) -> float:
"""
Compute the distance between two colors in OKLCH space.
Note: `xc` and `yc` are presumed to be OKLCH colors already, such that
`.coords()` yields an `(l, c, h)` triple directly rather than first
requiring conversion. When we can make this assumption, we save roughly an
order of magnitude in runtime.
1. `xc.distance(yc, space="oklch")`: 500k evals takes ~2s
2. This method: 500k evals takes ~0.2s
"""
l1, c1, h1 = xc.coords()
l2, c2, h2 = yc.coords()
rad1 = h1 / 180 * math.pi
rad2 = h2 / 180 * math.pi
x1, y1 = c1 * math.cos(rad1), c1 * math.sin(rad1)
x2, y2 = c2 * math.cos(rad2), c2 * math.sin(rad2)
dx = x1 - x2
dy = y1 - y2
dz = l1 - l2
return (dx**2 + dy**2 + dz**2)**0.5

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@@ -618,7 +618,7 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": 22,
"id": "a4b66b95-2c86-452f-bef4-1e3716ab9555",
"metadata": {},
"outputs": [],
@@ -703,49 +703,46 @@
"])\n",
"\n",
"scheme_toml = [\n",
" f\"{lhs:<8} = {rhs:<15}\"\n",
" f\"{lhs:<12} = {rhs:<16}\"\n",
" for lhs, rhs in scheme_pairs\n",
"] "
]
},
{
"cell_type": "code",
"execution_count": 17,
"execution_count": 23,
"id": "908e409c-803f-401d-a5c7-d7fc73aeea9d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['bg0 = f{{alpine.l20}}',\n",
" 'bg1 = f{{alpine.l25}}',\n",
" 'bg2 = f{{alpine.l30}}',\n",
" 'bg3 = f{{alpine.l35}}',\n",
" 'fg3 = f{{alpine.l70}}',\n",
" 'fg2 = f{{alpine.l75}}',\n",
" 'fg1 = f{{alpine.l80}}',\n",
" 'fg0 = f{{alpine.l85}}',\n",
" 'red = f{{red.l62}} ',\n",
" 'organge = f{{orange.l61}}',\n",
" 'yellow = f{{yellow.l60}}',\n",
" 'green = f{{green.l59}} ',\n",
" 'cyan = f{{green.l59}} ',\n",
" 'blue = f{{blue.l60}} ',\n",
" 'violet = f{{blue.l60}} ',\n",
" 'magenta = f{{red.l62}} ',\n",
" 'background = f{{alpine.l20}}',\n",
" 'selection_bg = f{{alpine.l25}}',\n",
" 'selection_fg = f{{alpine.l80}}',\n",
" 'foreground = f{{alpine.l85}}']"
"name": "stdout",
"output_type": "stream",
"text": [
"bg0 = f{{alpine.l20}} \n",
"bg1 = f{{alpine.l25}} \n",
"bg2 = f{{alpine.l30}} \n",
"bg3 = f{{alpine.l35}} \n",
"fg3 = f{{alpine.l70}} \n",
"fg2 = f{{alpine.l75}} \n",
"fg1 = f{{alpine.l80}} \n",
"fg0 = f{{alpine.l85}} \n",
"red = f{{red.l62}} \n",
"organge = f{{orange.l61}} \n",
"yellow = f{{yellow.l60}} \n",
"green = f{{green.l59}} \n",
"cyan = f{{green.l59}} \n",
"blue = f{{blue.l60}} \n",
"violet = f{{blue.l60}} \n",
"magenta = f{{red.l62}} \n",
"background = f{{alpine.l20}} \n",
"selection_bg = f{{alpine.l25}} \n",
"selection_fg = f{{alpine.l80}} \n",
"foreground = f{{alpine.l85}} \n"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"scheme_toml"
"print('\\n'.join(scheme_toml))"
]
},
{

View File

@@ -1,7 +1,11 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "monobiome"
version = "1.2.0"
description = "Add your description here"
version = "1.3.1"
description = "Monobiome color palette"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
@@ -21,3 +25,39 @@ dependencies = [
dev = [
"ipykernel>=7.0.1",
]
[project.scripts]
monobiome = "monobiome.__main__:main"
[project.urls]
Homepage = "https://doc.olog.io/monobiome"
Documentation = "https://doc.olog.io/monobiome"
Repository = "https://git.olog.io/olog/monobiome"
Issues = "https://git.olog.io/olog/monobiome/issues"
[tool.setuptools.packages.find]
include = ["monobiome*"]
[tool.setuptools.package-data]
"monobiome" = ["data/*.toml"]
[tool.ruff]
line-length = 79
[tool.ruff.lint]
select = ["ANN", "E", "F", "UP", "B", "SIM", "I", "C4", "PERF"]
[tool.ruff.lint.isort]
length-sort = true
order-by-type = false
force-sort-within-sections = false
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]
"**/__init__.py" = ["F401"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true

View File

@@ -1,6 +1,6 @@
{
"manifest_version": 2,
"version": "1.2.0",
"version": "f{{theme.version}}",
"name": "monobiome-f{{theme.biome}}",
"theme": {
"colors": {

View File

@@ -1,6 +1,6 @@
{
"manifest_version": 2,
"version": "1.2.0",
"version": "f{{theme.version}}",
"name": "monobiome-f{{theme.biome}}-dark",
"theme": {
"colors": {

View File

@@ -1,6 +1,6 @@
{
"manifest_version": 2,
"version": "1.2.0",
"version": "f{{theme.version}}",
"name": "monobiome-f{{theme.biome}}-light",
"theme": {
"colors": {

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "alpine"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "alpine"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "badlands"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "badlands"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "chaparral"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "chaparral"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "grassland"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "grassland"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "savanna"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "savanna"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "tundra"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "tundra"
contrast = "default"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "alpine"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "alpine"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "badlands"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "badlands"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "chaparral"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "chaparral"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "grassland"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "grassland"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "savanna"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "savanna"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "tundra"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "tundra"
contrast = "hard"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "alpine"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "alpine"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "badlands"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "badlands"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "chaparral"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "chaparral"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "grassland"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "grassland"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "savanna"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "savanna"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "dark"
biome = "tundra"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

View File

@@ -11,6 +11,7 @@
mode = "light"
biome = "tundra"
contrast = "soft"
version = "f{{version}}"
# --------------------------------------------
# - SYS --------------------------------------

4
uv.lock generated
View File

@@ -622,8 +622,8 @@ wheels = [
[[package]]
name = "monobiome"
version = "1.2.0"
source = { virtual = "." }
version = "1.4.0"
source = { editable = "." }
dependencies = [
{ name = "coloraide" },
{ name = "imageio", extra = ["ffmpeg"] },