6 Commits

98 changed files with 1163 additions and 45 deletions

1
.gitignore vendored
View File

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

View File

@@ -8,7 +8,7 @@ are designed to achieve identical contrast with the accents, and thus any one
of the options can be selected to change the feeling of the palette without of the options can be selected to change the feeling of the palette without
sacrificing readability. sacrificing readability.
![Theme preview](images/repo_preview_four_split.png) ![Theme preview](images/repo_preview_primary.png)
_(Preview of default light and dark theme variants)_ _(Preview of default light and dark theme variants)_
See screenshots for the full set of theme variants in [THEMES](THEMES.md) (also See screenshots for the full set of theme variants in [THEMES](THEMES.md) (also

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 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)

189
monobiome/plotting.py Normal file
View File

@@ -0,0 +1,189 @@
import numpy as np
import matplotlib.pyplot as plt
from coloraide import Color
from monobiome.palette import compute_hlc_map
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, strict=True))
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: dict[str, dict[int, str]],
cell_size: int = 40,
keys: list[str] | None = None
) -> None:
names = list(palette.keys()) if keys is None else 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: dict[str, dict[int, str]],
cell_size: int = 40,
keys: list[str] | None = None
) -> 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 w/o half-cells
plt.show()
if __name__ == "__main__":
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",
]
hlc_map = compute_hlc_map("oklch")
show_palette(hlc_map, cell_size=25, keys=keys)
# show_palette(hlc_map, 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", "cell_type": "code",
"execution_count": 16, "execution_count": 22,
"id": "a4b66b95-2c86-452f-bef4-1e3716ab9555", "id": "a4b66b95-2c86-452f-bef4-1e3716ab9555",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -703,49 +703,46 @@
"])\n", "])\n",
"\n", "\n",
"scheme_toml = [\n", "scheme_toml = [\n",
" f\"{lhs:<8} = {rhs:<15}\"\n", " f\"{lhs:<12} = {rhs:<16}\"\n",
" for lhs, rhs in scheme_pairs\n", " for lhs, rhs in scheme_pairs\n",
"] " "] "
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 17, "execution_count": 23,
"id": "908e409c-803f-401d-a5c7-d7fc73aeea9d", "id": "908e409c-803f-401d-a5c7-d7fc73aeea9d",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "name": "stdout",
"text/plain": [ "output_type": "stream",
"['bg0 = f{{alpine.l20}}',\n", "text": [
" 'bg1 = f{{alpine.l25}}',\n", "bg0 = f{{alpine.l20}} \n",
" 'bg2 = f{{alpine.l30}}',\n", "bg1 = f{{alpine.l25}} \n",
" 'bg3 = f{{alpine.l35}}',\n", "bg2 = f{{alpine.l30}} \n",
" 'fg3 = f{{alpine.l70}}',\n", "bg3 = f{{alpine.l35}} \n",
" 'fg2 = f{{alpine.l75}}',\n", "fg3 = f{{alpine.l70}} \n",
" 'fg1 = f{{alpine.l80}}',\n", "fg2 = f{{alpine.l75}} \n",
" 'fg0 = f{{alpine.l85}}',\n", "fg1 = f{{alpine.l80}} \n",
" 'red = f{{red.l62}} ',\n", "fg0 = f{{alpine.l85}} \n",
" 'organge = f{{orange.l61}}',\n", "red = f{{red.l62}} \n",
" 'yellow = f{{yellow.l60}}',\n", "organge = f{{orange.l61}} \n",
" 'green = f{{green.l59}} ',\n", "yellow = f{{yellow.l60}} \n",
" 'cyan = f{{green.l59}} ',\n", "green = f{{green.l59}} \n",
" 'blue = f{{blue.l60}} ',\n", "cyan = f{{green.l59}} \n",
" 'violet = f{{blue.l60}} ',\n", "blue = f{{blue.l60}} \n",
" 'magenta = f{{red.l62}} ',\n", "violet = f{{blue.l60}} \n",
" 'background = f{{alpine.l20}}',\n", "magenta = f{{red.l62}} \n",
" 'selection_bg = f{{alpine.l25}}',\n", "background = f{{alpine.l20}} \n",
" 'selection_fg = f{{alpine.l80}}',\n", "selection_bg = f{{alpine.l25}} \n",
" 'foreground = f{{alpine.l85}}']" "selection_fg = f{{alpine.l80}} \n",
] "foreground = f{{alpine.l85}} \n"
}, ]
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
} }
], ],
"source": [ "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] [project]
name = "monobiome" name = "monobiome"
version = "1.2.0" version = "1.4.0"
description = "Add your description here" description = "Monobiome color palette"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
@@ -14,6 +18,7 @@ dependencies = [
"numpy>=2.3.4", "numpy>=2.3.4",
"pillow>=12.0.0", "pillow>=12.0.0",
"plotly>=6.3.1", "plotly>=6.3.1",
"pyqt5>=5.15.11",
"scipy>=1.16.2", "scipy>=1.16.2",
] ]
@@ -21,3 +26,39 @@ dependencies = [
dev = [ dev = [
"ipykernel>=7.0.1", "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

@@ -7,20 +7,22 @@ prefix=${1:-}
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
render_script="$script_dir/render.sh" render_script="$script_dir/render.sh"
#biomes=(alpine) biomes=(alpine badlands chaparral savanna grassland tundra reef heathland moorland)
#modes=(light dark) modes=(light)
biomes=(alpine)
modes=(dark)
for biome in "${biomes[@]}"; do for biome in "${biomes[@]}"; do
for mode in "${modes[@]}"; do for mode in "${modes[@]}"; do
echo "Applying $biome-$mode theme" echo "Applying $biome-$mode theme"
monobiome scheme "$mode" "$biome" \
-d 0.42 \
-l 85 \
-o ~/.config/symconf/groups/theme/monobiome-none.toml
symconf config \ symconf config \
-a kitty,nvim \ -a kitty,nvim \
-m "$mode" \ -m "$mode" \
-s "default-$biome-monobiome" \ -s monobiome \
-T font=Berkeley -T font=Berkeley
sleep 2 sleep 1
echo "Taking screenshot..." echo "Taking screenshot..."
"$render_script" 800 600 "images/render/$prefix-$biome-$mode.png" nvim \ "$render_script" 800 600 "images/render/$prefix-$biome-$mode.png" nvim \

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

53
uv.lock generated
View File

@@ -622,8 +622,8 @@ wheels = [
[[package]] [[package]]
name = "monobiome" name = "monobiome"
version = "1.2.0" version = "1.3.1"
source = { virtual = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "coloraide" }, { name = "coloraide" },
{ name = "imageio", extra = ["ffmpeg"] }, { name = "imageio", extra = ["ffmpeg"] },
@@ -634,6 +634,7 @@ dependencies = [
{ name = "numpy" }, { name = "numpy" },
{ name = "pillow" }, { name = "pillow" },
{ name = "plotly" }, { name = "plotly" },
{ name = "pyqt5" },
{ name = "scipy" }, { name = "scipy" },
] ]
@@ -653,6 +654,7 @@ requires-dist = [
{ name = "numpy", specifier = ">=2.3.4" }, { name = "numpy", specifier = ">=2.3.4" },
{ name = "pillow", specifier = ">=12.0.0" }, { name = "pillow", specifier = ">=12.0.0" },
{ name = "plotly", specifier = ">=6.3.1" }, { name = "plotly", specifier = ">=6.3.1" },
{ name = "pyqt5", specifier = ">=5.15.11" },
{ name = "scipy", specifier = ">=1.16.2" }, { name = "scipy", specifier = ">=1.16.2" },
] ]
@@ -1021,6 +1023,53 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
] ]
[[package]]
name = "pyqt5"
version = "5.15.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyqt5-qt5" },
{ name = "pyqt5-sip" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" },
{ url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" },
{ url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" },
{ url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" },
]
[[package]]
name = "pyqt5-qt5"
version = "5.15.18"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/90/bf01ac2132400997a3474051dd680a583381ebf98b2f5d64d4e54138dc42/pyqt5_qt5-5.15.18-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:8bb997eb903afa9da3221a0c9e6eaa00413bbeb4394d5706118ad05375684767", size = 39715743, upload-time = "2025-11-09T12:56:42.936Z" },
{ url = "https://files.pythonhosted.org/packages/24/8e/76366484d9f9dbe28e3bdfc688183433a7b82e314216e9b14c89e5fab690/pyqt5_qt5-5.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c656af9c1e6aaa7f59bf3d8995f2fa09adbf6762b470ed284c31dca80d686a26", size = 36798484, upload-time = "2025-11-09T12:56:59.998Z" },
{ url = "https://files.pythonhosted.org/packages/9a/46/ffe177f99f897a59dc237a20059020427bd2d3853d713992b8081933ddfe/pyqt5_qt5-5.15.18-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bf2457e6371969736b4f660a0c153258fa03dbc6a181348218e6f05421682af7", size = 60864590, upload-time = "2025-11-09T12:57:26.724Z" },
]
[[package]]
name = "pyqt5-sip"
version = "12.17.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/4a/195cf4d2a7e1ff480b4cabcd51aa5c0068c03a19a97282317536e4a82e1e/pyqt5_sip-12.17.2.tar.gz", hash = "sha256:7f66565c2a13d34d8ad6aad08e953d355ea3fe466d991d51aa5a0966a5289f05", size = 104246, upload-time = "2025-12-06T13:19:06.821Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/3e/f5c7bc43668147ddc00a1a579f22639dffdbfb9470ce3a5bc1cf27e0d541/pyqt5_sip-12.17.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0bd1a8e59124a90a05f078bceeb9d4d93c3986c349030487c202fffde6612969", size = 124612, upload-time = "2025-12-06T13:18:49.614Z" },
{ url = "https://files.pythonhosted.org/packages/b9/41/63f81a53704425092558f1ec17adbed11787f4322e60a849e0539516b3aa/pyqt5_sip-12.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:55ff374feb4bad783241649c7b946e05d7e83d60b0755526ed8fb25bf54e3408", size = 282364, upload-time = "2025-12-06T13:18:51.179Z" },
{ url = "https://files.pythonhosted.org/packages/b5/cd/2b749a174e61394085d61cafb7dc3c11ddf40307edfb2d71cb9b71b7f320/pyqt5_sip-12.17.2-cp312-cp312-win32.whl", hash = "sha256:45dc6e2121d175fdab1431c448fd3e88c97caf873a33cb65efa2e9ad0056337b", size = 49521, upload-time = "2025-12-06T13:18:53.155Z" },
{ url = "https://files.pythonhosted.org/packages/73/ac/7f6d6a6a4505b251f1174092f09d5611c2ed66602c40d3411d93a1d2a95f/pyqt5_sip-12.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:e3bb16e43afd68dd013228075876cf8f8b1a7d86ba67767dd2c6a97be677c18d", size = 58003, upload-time = "2025-12-06T13:18:52.119Z" },
{ url = "https://files.pythonhosted.org/packages/38/b1/78432c271b2a5477f5fe1ad9eb69cdc482430230b8d552cf5cee393d7862/pyqt5_sip-12.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cfeee3c27f28c091d6a46f8befe9afcfafc76846846bedf1112d403a7299e864", size = 124589, upload-time = "2025-12-06T13:18:54.942Z" },
{ url = "https://files.pythonhosted.org/packages/e8/d9/6451973300f7dffe70476cad7fc4a59ffe08417ee4add6afb3288c91bd85/pyqt5_sip-12.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b5df33e198d5d7cccc8e081f80eb97b8d70100f887362074a029a6c19cb92c8b", size = 282040, upload-time = "2025-12-06T13:18:57.019Z" },
{ url = "https://files.pythonhosted.org/packages/c6/1e/241d9ddef5cb1bb3e3b5839b6f8c05ae727e196be82b4646ea4ef9475ef7/pyqt5_sip-12.17.2-cp313-cp313-win32.whl", hash = "sha256:2c0a278b8fc289d34d4e62bbb9ef6da96b45cc9ab3f6886397b1490d2b4a5604", size = 49497, upload-time = "2025-12-06T13:19:00.012Z" },
{ url = "https://files.pythonhosted.org/packages/fd/33/a393163b6299a7e0743fad86fbcb06cf219878fbdd629ee6cb46d2a4d9f7/pyqt5_sip-12.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:7e0d663b583a4d3ac63c9fbade2228de6ee628b44a025f5fd964b97dbbcbebc9", size = 58075, upload-time = "2025-12-06T13:18:58.069Z" },
{ url = "https://files.pythonhosted.org/packages/58/29/b4943def737d3f8876bfd4f9af1909892ae1998099695b3e81870c39aaa7/pyqt5_sip-12.17.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6f03c25a18294f2d66befc4f2adf3f35fceba877b937dc8a94783fa7da8b7345", size = 124591, upload-time = "2025-12-06T13:19:02.105Z" },
{ url = "https://files.pythonhosted.org/packages/18/62/e7ac79bb080d4e5a7d7fea50ca7d9231a7ded07e01f24d4e123f089e1630/pyqt5_sip-12.17.2-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a6d716801d512643b7b1f50dfbdcd16408fe9a6df907d8627b4ad82190604bec", size = 282412, upload-time = "2025-12-06T13:19:03.787Z" },
{ url = "https://files.pythonhosted.org/packages/92/11/b63ee88ffc2e04af90ced17dbe0d774f5f4e51122c13f8118e565707954e/pyqt5_sip-12.17.2-cp314-cp314-win32.whl", hash = "sha256:c617c29524fdcf826e619d77ffd0d6142622f8422adc2608ecc89edd3e605339", size = 50713, upload-time = "2025-12-06T13:19:05.851Z" },
{ url = "https://files.pythonhosted.org/packages/d0/70/efe47083dea494613fc41da55f25c07b4e73bb90c98dee8fe87afbfbc303/pyqt5_sip-12.17.2-cp314-cp314-win_amd64.whl", hash = "sha256:b008755d2222a064ec90c525fce5df3fe9d410371e47c43a21c049e07683b7fb", size = 59620, upload-time = "2025-12-06T13:19:04.829Z" },
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.4.2" version = "8.4.2"