{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "e151bcbd-93f2-41ec-8c2d-3666cec26af9",
   "metadata": {},
   "source": [
    "**This notebook**: simple chroma pulling over v1.2.0 to try and restore a bit of the vibrancy of the older palette (e.g., <v1.1.1).\n",
    "\n",
    "I think with v1.2.0 we tried to make the mid-lightness as symmetric as possible, resisting the urge to lean into the naturally skewed chroma boundary. So I can fill into that as much as I want, it's just that we'll be increasing the difference in the character between light and dark modes under fixed contrast schemes. It's already not *perfectly balanced*, so we've never strictly relied on that quality to begin with."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "cd89e94a-d2f6-4c91-9c27-bdae00378834",
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "from pathlib import Path\n",
    "from functools import cache\n",
    "\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from scipy.interpolate import interp1d, CubicSpline, BSpline\n",
    "\n",
    "from pprint import pprint\n",
    "from coloraide import Color\n",
    "\n",
    "@cache\n",
    "def max_C_Lh(L, h, space='srgb', eps=1e-6, tol=1e-9):\n",
    "    '''\n",
    "    Binary search for max chroma at fixed lightness and hue\n",
    "\n",
    "    Parameters:\n",
    "        L: lightness percentage\n",
    "    '''\n",
    "    def C_in_gamut(C):\n",
    "        return Color('oklch', [L/100, C, h]).convert(space).in_gamut(tolerance=tol)\n",
    "\n",
    "    lo, hi = 0.0, 0.1\n",
    "    while C_in_gamut(hi): hi *= 2\n",
    "    while hi - lo > eps:\n",
    "        m = (lo + hi) / 2\n",
    "        lo, hi = (m, hi) if C_in_gamut(m) else (lo, m)\n",
    "\n",
    "    Cmax = lo\n",
    "    c_oklch = Color('oklch', [L, Cmax, h])\n",
    "    c_srgb  = c_oklch.convert('srgb')\n",
    "    \n",
    "    return Cmax\n",
    "\n",
    "def quad_bezier_rational(P0, P1, P2, w, t):\n",
    "    t = np.asarray(t)[:, None]\n",
    "    num = (1-t)**2*P0 + 2*w*(1-t)*t*P1 + t**2*P2\n",
    "    den = (1-t)**2 + 2*w*(1-t)*t + t**2\n",
    "    \n",
    "    return num/den\n",
    "    \n",
    "def bezier_y_at_x(P0, P1, P2, w, x_query, n=400):\n",
    "    t = np.linspace(0, 1, n)\n",
    "    B = quad_bezier_rational(P0, P1, P2, w, t)\n",
    "    x_vals, y_vals = B[:, 0], B[:, 1]\n",
    "    \n",
    "    return np.interp(x_query, x_vals, y_vals)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c7d2d1d9-dad9-4f8f-8417-c22d38f089f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# SET LIGHTNESS CONTROL POINTS\n",
    "# L_points = [10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 98]\n",
    "L_points = list(range(10, 98+1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "b0617a01-cb05-4950-9bc3-f4fd0de63f56",
   "metadata": {},
   "outputs": [],
   "source": [
    "# FIXED MONOBIOME PARAMETERS\n",
    "L_resolution = 5  # step size along lightness dim\n",
    "L_space = np.arange(0, 100+L_resolution, L_resolution)\n",
    "\n",
    "monotone_C_map = {\n",
    "    \"alpine\": 0,\n",
    "    \"badlands\": 0.011,\n",
    "    \"chaparral\": 0.011,\n",
    "    \"savanna\": 0.011,\n",
    "    \"grassland\": 0.011,\n",
    "    \"tundra\": 0.011,\n",
    "}\n",
    "\n",
    "h_weights = {\n",
    "    \"red\": 3.0,\n",
    "    \"orange\": 3.8,  # 3.6\n",
    "    \"yellow\": 3.8,  # 4.0\n",
    "    \"green\": 3.8,\n",
    "    \"blue\": 3.4,  # 3.8\n",
    "}\n",
    "h_L_offsets = {\n",
    "    \"red\": 0,  # -1,\n",
    "    \"orange\": -5.5,  # -3,\n",
    "    \"yellow\": -13.5,  # -8\n",
    "    \"green\": -11,  # -8\n",
    "    \"blue\": 10,  # 14\n",
    "}\n",
    "h_C_offsets = {\n",
    "    \"red\": 0,  # 0\n",
    "    \"orange\": -0.01,  # -0.02\n",
    "    \"yellow\": -0.052,  # -0.08\n",
    "    \"green\": -0.088,  # -0.105\n",
    "    \"blue\": 0.0,  # 0.01\n",
    "}\n",
    "\n",
    "monotone_h_map = {\n",
    "    \"alpine\": 0,\n",
    "    \"badlands\": 29,\n",
    "    \"chaparral\": 62.5,\n",
    "    \"savanna\": 104,\n",
    "    \"grassland\": 148,\n",
    "    \"tundra\": 262,\n",
    "}\n",
    "accent_h_map = {\n",
    "    \"red\": 29,\n",
    "    \"orange\": 62.5,\n",
    "    \"yellow\": 104,\n",
    "    \"green\": 148,\n",
    "    \"blue\": 262,\n",
    "}\n",
    "h_map = {**monotone_h_map, **accent_h_map}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "46b619a1-9118-4576-9100-c1265ccd15f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# compute C max values over each point in L space\n",
    "h_Lspace_Cmax = {\n",
    "    h_str: [max_C_Lh(_L, _h) for _L in L_space]\n",
    "    for h_str, _h in h_map.items()\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "0da9d0b5-fa60-4dd7-aa55-0b3c81e503fd",
   "metadata": {},
   "outputs": [],
   "source": [
    "# compute *unbounded* chroma curves for all hues\n",
    "h_L_points_C = {}\n",
    "h_ctrl_L_C = {}\n",
    "\n",
    "for h_str, _h in monotone_h_map.items():\n",
    "    h_L_points_C[h_str] = np.array([monotone_C_map[h_str]]*len(L_points))\n",
    "    \n",
    "for h_str, _h in accent_h_map.items():\n",
    "    Lspace_Cmax = h_Lspace_Cmax[h_str]\n",
    "    \n",
    "    # get L value of max chroma; will be a bezier control\n",
    "    L_Cmax_idx = np.argmax(Lspace_Cmax)\n",
    "    L_Cmax = L_space[L_Cmax_idx]\n",
    "\n",
    "    # offset control point by any preset x-shift\n",
    "    L_Cmax += h_L_offsets[h_str]\n",
    "\n",
    "    # and get max C at the L offset\n",
    "    Cmax = max_C_Lh(L_Cmax, _h)\n",
    "\n",
    "    # set 3 control points; shift by any global linear offest\n",
    "    C_offset = h_C_offsets.get(h_str, 0)\n",
    "    \n",
    "    p_0 = np.array([0, 0])\n",
    "    p_Cmax = np.array([L_Cmax, Cmax + C_offset])\n",
    "    p_100 = np.array([100, 0])\n",
    "    \n",
    "    B_L_points = bezier_y_at_x(p_0, p_Cmax, p_100, h_weights.get(h_str, 1), L_points)\n",
    "    h_L_points_C[h_str] = B_L_points\n",
    "    h_ctrl_L_C[h_str] = np.vstack([p_0, p_Cmax, p_100])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "56fd3826-c2b7-480e-ac42-a72e96c045f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# compute full set of final chroma curves; limits every point to in-gamut max\n",
    "h_LC_color_map = {}\n",
    "h_L_points_Cstar = {}\n",
    "\n",
    "for h_str, L_points_C in h_L_points_C.items():\n",
    "    _h = h_map[h_str]\n",
    "\n",
    "    h_L_points_Cstar[h_str] = [\n",
    "        max(0, min(_C, max_C_Lh(_L, _h)))\n",
    "        for _L, _C in zip(L_points, L_points_C)\n",
    "    ]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "3c257cdb-7cb4-4aed-b1a4-bb560f4848b0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# put together objects for output formats\n",
    "toml_lines = []\n",
    "oklch_hL_dict = {}\n",
    "\n",
    "for h_str, L_points_Cstar in h_L_points_Cstar.items():\n",
    "    _h = h_map[h_str]\n",
    "    toml_lines.append(f\"[{h_str}]\")\n",
    "    oklch_hL_dict[h_str] = {}\n",
    "    \n",
    "    for _L, _C in zip(L_points, L_points_Cstar):\n",
    "        oklch = Color('oklch', [_L/100, _C, _h])\n",
    "        srgb = oklch.convert('srgb')\n",
    "        \n",
    "        hex_str = srgb.to_string(hex=True)\n",
    "        \n",
    "        l, c, h = oklch.convert('oklch').coords()\n",
    "        # oklch_str = oklch.to_string(percent=False)\n",
    "        oklch_str = f\"oklch({l*100:.1f}% {c:.4f} {h:.1f})\"\n",
    "        \n",
    "        toml_lines.append(f'l{_L} = \"{hex_str}\"')\n",
    "        oklch_hL_dict[h_str][_L] = oklch_str\n",
    "        \n",
    "    toml_lines.append(\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "281de03a-1ba4-419a-a991-1a9be2a66667",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 61496 characters]",
      "text/plain": [
       "<Figure size 400x800 with 6 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "ax_h_map = {}\n",
    "fig, axes = plt.subplots(\n",
    "    len(monotone_h_map),\n",
    "    1,\n",
    "    sharex=True,\n",
    "    sharey=True,\n",
    "    figsize=(4, 8)\n",
    ")\n",
    "\n",
    "for i, h_str in enumerate(h_L_points_Cstar):\n",
    "    _h = h_map[h_str]\n",
    "    L_points_Cstar = h_L_points_Cstar[h_str]\n",
    "    L_space_Cmax = h_Lspace_Cmax[h_str]\n",
    "    \n",
    "    if _h not in ax_h_map:\n",
    "        ax_h_map[_h] = axes[i]\n",
    "    ax = ax_h_map[_h]\n",
    "\n",
    "    # plot Cmax and Cstar\n",
    "    ax.plot(L_space, L_space_Cmax, c=\"b\", alpha=0.2)\n",
    "    ax.plot(L_points, L_points_Cstar, alpha=0.7)\n",
    "    \n",
    "    ax.title.set_text(f\"Hue [${_h}$]\")\n",
    "    \n",
    "axes[-1].set_xlabel(\"Lightness (%)\")\n",
    "axes[-1].set_xticks([L_points[0], L_points[-1]])\n",
    "\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(top=0.9)\n",
    "\n",
    "plt.suptitle(\"$C^*$ curves for hue groups\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "b52e351a-fa3c-4d60-877c-75a938b52289",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 112188 characters]",
      "text/plain": [
       "<Figure size 500x1000 with 6 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "v111_L_space = list(range(15, 95+1, 5))\n",
    "v111_hC_points = {\n",
    "    \"red\": [0.058, 0.074, 0.092, 0.11, 0.128, 0.147, 0.167, 0.183, 0.193, 0.193, 0.182, 0.164, 0.14, 0.112, 0.081, 0.052, 0.024],\n",
    "    \"orange\": [0.030, 0.038, 0.046, 0.058, 0.07, 0.084, 0.1, 0.114, 0.125, 0.134, 0.138, 0.136, 0.128, 0.112, 0.092, 0.064, 0.032],\n",
    "    \"yellow\": [0.02, 0.024, 0.03, 0.036, 0.044, 0.05, 0.06, 0.068, 0.076, 0.082, 0.088, 0.088, 0.086, 0.082, 0.072, 0.058, 0.04],\n",
    "    \"green\": [0.0401, 0.048, 0.056, 0.064, 0.072, 0.08, 0.09, 0.098, 0.104, 0.108, 0.11, 0.108, 0.102, 0.094, 0.084, 0.072, 0.05],\n",
    "    \"blue\": [0.06, 0.072, 0.084, 0.096, 0.106, 0.116, 0.124, 0.13, 0.132, 0.128, 0.122, 0.11, 0.096, 0.08, 0.064, 0.044, 0.023],\n",
    "}\n",
    "\n",
    "ax_h_map = {}\n",
    "fig, axes = plt.subplots(\n",
    "    len(monotone_h_map),\n",
    "    1,\n",
    "    sharex=True,\n",
    "    sharey=True,\n",
    "    figsize=(5, 10)\n",
    ")\n",
    "\n",
    "for i, h_str in enumerate(h_L_points_Cstar):\n",
    "    _h = h_map[h_str]\n",
    "    L_points_Cstar = h_L_points_Cstar[h_str]\n",
    "    L_space_Cmax = h_Lspace_Cmax[h_str]\n",
    "    \n",
    "    if _h not in ax_h_map:\n",
    "        ax_h_map[_h] = axes[i]\n",
    "    ax = ax_h_map[_h]\n",
    "\n",
    "    # plot Cmax and Cstar\n",
    "    ax.plot(L_space, L_space_Cmax, c=\"b\", alpha=0.2, label='Cmax')\n",
    "    ax.plot(L_points, L_points_Cstar, alpha=0.7, label='C*')\n",
    "\n",
    "    if h_str in v111_hC_points:\n",
    "        ax.scatter(v111_L_space, v111_hC_points[h_str], s=4, label='Cv111')\n",
    "        \n",
    "    if h_str in h_ctrl_L_C:\n",
    "        cpts = h_ctrl_L_C[h_str]\n",
    "        cpt_x, cpt_y = cpts[:, 0], cpts[:, 1]\n",
    "        h_w = h_weights.get(h_str, 1)\n",
    "        \n",
    "        P0, P1, P2 = cpts[0], cpts[1], cpts[2]\n",
    "        d0 = 2 * h_w * (P1 - P0)\n",
    "        d2 = 2 * h_w * (P2 - P1)\n",
    "\n",
    "        handle_scale = 0.25\n",
    "        H0 = P0 + handle_scale * d0\n",
    "        H2 = P2 - handle_scale * d2\n",
    "    \n",
    "        # ax.plot([P0[0], H0[0]], [P0[1], H0[1]], color='tab:blue', lw=1)\n",
    "        # ax.plot([P2[0], H2[0]], [P2[1], H2[1]], color='tab:orange', lw=1)\n",
    "        \n",
    "        ax.plot(cpt_x, cpt_y, '--', color='gray', lw=1, label='Bezier polygon')\n",
    "        ax.scatter(cpt_x, cpt_y, color='red', zorder=5, s=2, label='Control points')\n",
    "        \n",
    "    ax.title.set_text(f\"Hue [${_h}$]\")\n",
    "    \n",
    "axes[-1].set_ylabel(\"Chroma (C)\")\n",
    "axes[-1].set_xlabel(\"Lightness (%)\")\n",
    "axes[-1].set_xticks([L_points[0], 50, 65, L_points[-1]])\n",
    "\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(top=0.9)\n",
    "\n",
    "handles, labels = axes[-1].get_legend_handles_labels()\n",
    "unique = dict(zip(labels, handles))\n",
    "fig.legend(unique.values(), unique.keys(), loc='lower center', bbox_to_anchor=(0.5, -0.06), ncol=3)\n",
    "\n",
    "plt.suptitle(\"$C^*$ curves for hue groups + v111 5% lightness\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "1e2a1a5a-7724-459a-9a1e-69ab0f421c3b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 64696 characters]",
      "text/plain": [
       "<Figure size 800x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from numpy import arctan2, degrees\n",
    "\n",
    "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n",
    "\n",
    "for h_str in h_L_points_Cstar:\n",
    "    if h_str not in accent_h_map:\n",
    "        continue\n",
    "    ax.fill_between(L_points, h_L_points_Cstar[h_str], alpha=0.2, color='grey', label=h_str)\n",
    "\n",
    "    x, y = L_points, h_L_points_Cstar[h_str]\n",
    "    n = int(0.5*len(x))\n",
    "    ax.text(x[n], y[n]-0.01, h_str, rotation=10, va='center', ha='left')\n",
    "    \n",
    "ax.set_xlabel(\"Lightness (%)\")\n",
    "ax.set_xticks([L_points[0], 45, 50, 55, 60, 65, 70, L_points[-1]])\n",
    "plt.suptitle(\"$C^*$ curves (v1.3.0)\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "d137161e-1628-4789-b6ff-b82925b989e5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 55996 characters]",
      "text/plain": [
       "<Figure size 800x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from numpy import arctan2, degrees\n",
    "\n",
    "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n",
    "\n",
    "for h_str in h_L_points_Cstar:\n",
    "    if h_str not in accent_h_map:\n",
    "        continue\n",
    "    ax.fill_between(v111_L_space, v111_hC_points[h_str], alpha=0.2, color='grey', label=h_str)\n",
    "\n",
    "    x, y = v111_L_space, v111_hC_points[h_str]\n",
    "    n = int(0.46*len(x))\n",
    "    ax.text(x[n], y[n]-0.005, h_str, rotation=30, va='center', ha='left')\n",
    "    \n",
    "ax.set_xlabel(\"Lightness (%)\")\n",
    "ax.set_xticks([L_points[0], 45, 50, 55, 60, 65, 70, L_points[-1]])\n",
    "plt.suptitle(\"$C^*$ curves (v1.1.1)\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "d72e7f0b-a1a4-46f1-84fb-752229afafb5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 41080 characters]",
      "text/plain": [
       "<Figure size 800x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = plt.subplots(1, 1, figsize=(8, 6))\n",
    "\n",
    "for h_str in h_L_points_Cstar:\n",
    "    if h_str not in accent_h_map:\n",
    "        continue\n",
    "    ax.fill_between(L_points, h_L_points_Cstar[h_str], alpha=0.2, color=h_str)\n",
    "    \n",
    "    \n",
    "ax.set_xlabel(\"Lightness (%)\")\n",
    "ax.set_xticks([L_points[0], 45, 50, 55,   60, 65, 70, L_points[-1]])\n",
    "ax.set_xlim(45, 70)\n",
    "plt.suptitle(\"$C^*$ curves -- mid-range close-up (v1.3.0)\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "ef0dd0d9-a267-494f-b9c6-90d9ab0e6564",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[TOML] written\n",
      "[JSON] written\n"
     ]
    }
   ],
   "source": [
    "# write files -- QBR = \"quadratic bezier rational\"\n",
    "PALETTE_DIR = \"palettes\"\n",
    "\n",
    "toml_content = '\\n'.join(toml_lines)\n",
    "with Path(PALETTE_DIR, 'monobiome-vQBRsn-130.toml').open('w') as f:\n",
    "    f.write(toml_content)\n",
    "print(\"[TOML] written\")\n",
    "    \n",
    "with Path(PALETTE_DIR, 'monobiome-vQBRsn-130-oklch.json').open('w') as f:\n",
    "    json.dump(oklch_hL_dict, f)\n",
    "print(\"[JSON] written\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82e14f54-32d3-4027-beb6-496ae129fc8a",
   "metadata": {},
   "source": [
    "### Build scheme maps with fixed OKLCH distances"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "2e12883f-54aa-4b16-8e49-42fa8b1aee3c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# unused; computes exact L1 to hit distance `d`. Not\n",
    "# useful currently b/c it can't be used to get colors\n",
    "# along the chroma curve: it solves for lightness, but\n",
    "# at that lightness the chroma will change (and therefore\n",
    "# the distance). *Could* be adapt for more complexity to\n",
    "# optimize along chroma curves, but discrete opt is easier.\n",
    "def solve_L(d, l1, c1, h1, c2, h2):\n",
    "    return l1 + (d**2 - (c1-c2)**2 - (h1-h2)**2)**0.5\n",
    "    \n",
    "# l1, c1, h1 = oklch_color_map[\"alpine\"][0].coords()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "9a66c06d-fe35-425f-a093-48433f25585b",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "def compute_dma_map(dT, metric=None):\n",
    "    \"\"\"\n",
    "    For threshold `dT`, compute the nearest accent shades\n",
    "    that exceed that threshold for every monotone shade.\n",
    "\n",
    "    Returns:\n",
    "\n",
    "    Map like\n",
    "    { \"alpine\": {\n",
    "        \"oklch( ... )\": {\n",
    "            \"red\": *nearest oklch >= dT from M base*\n",
    "    \"\"\"\n",
    "\n",
    "    if metric is None:\n",
    "        metric = lambda mc,ac: mc.distance(ac, space=\"oklch\")\n",
    "    \n",
    "    oklch_color_map = {\n",
    "        c_name: [Color(c_str) for c_str in c_str_dict.values()]\n",
    "        for c_name, c_str_dict in oklch_hL_dict.items()\n",
    "    }\n",
    "    \n",
    "    dT_mL_acol_map = {}\n",
    "    for m_name in monotone_h_map:\n",
    "        mL_acol_map = {}\n",
    "        m_colors = oklch_color_map[m_name]\n",
    "        \n",
    "        for m_color in m_colors:\n",
    "            acol_min_map = {}\n",
    "            \n",
    "            for a_name in accent_h_map:\n",
    "                a_colors = oklch_color_map[a_name]\n",
    "                oklch_dists = filter(\n",
    "                    lambda d: (d[1] - dT) > 0,\n",
    "                    [\n",
    "                        (ac, metric(m_color, ac))\n",
    "                        for ac in a_colors\n",
    "                    ]\n",
    "                )\n",
    "                oklch_dists = list(oklch_dists)\n",
    "                if oklch_dists:\n",
    "                    min_a_color = min(oklch_dists, key=lambda t: t[1])[0]\n",
    "                    acol_min_map[a_name] = min_a_color\n",
    "    \n",
    "            # make sure the current monotone level has *all* accents; o/w ignore\n",
    "            if len(acol_min_map) < len(accent_h_map):\n",
    "                continue\n",
    "\n",
    "            mL = m_color.coords()[0]\n",
    "            mL_acol_map[int(mL*100)] = acol_min_map\n",
    "        dT_mL_acol_map[m_name] = mL_acol_map\n",
    "    return dT_mL_acol_map"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e84f393-cf98-4bb3-a05a-145bd67bc723",
   "metadata": {},
   "source": [
    "#### Construct scheme TOML"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "a4b66b95-2c86-452f-bef4-1e3716ab9555",
   "metadata": {},
   "outputs": [],
   "source": [
    "mode = \"dark\"  # [\"dark\", \"light\"]\n",
    "biome = \"alpine\"  # [ ... ]\n",
    "metric = \"wcag\"  # [\"wcag\", \"oklch\"]\n",
    "metric_map = {\n",
    "    \"wcag\": lambda mc,ac: ac.contrast(mc, method='wcag21'),\n",
    "    \"oklch\": lambda mc,ac: mc.distance(ac, space=\"oklch\"),\n",
    "}\n",
    "metric_func = metric_map[metric]\n",
    "\n",
    "term_color_map = {\n",
    "    \"red\": \"red\",\n",
    "    \"organge\": \"orange\",\n",
    "    \"yellow\": \"yellow\",\n",
    "    \"green\": \"green\",\n",
    "    \"cyan\": \"green\",\n",
    "    \"blue\": \"blue\",\n",
    "    \"violet\": \"blue\",\n",
    "    \"magenta\": \"red\",\n",
    "}\n",
    "\n",
    "L = 20\n",
    "d = 4.5\n",
    "I = 5\n",
    "fg_gap = 50\n",
    "grey_gap = 30\n",
    "\n",
    "dT_mL_acol_map = compute_dma_map(d, metric=metric_func)\n",
    "Lma_map = {\n",
    "    m_name: mL_acol_dict[L]\n",
    "    for m_name, mL_acol_dict in dT_mL_acol_map.items()\n",
    "    if L in mL_acol_dict\n",
    "}\n",
    "\n",
    "# the `mL_acol_dict` only includes lightnesses where all accent\n",
    "# colors were within threshold. Coverage here will be partial if,\n",
    "# at the `mL`, there is some monotone base that doesn't have all\n",
    "# accents within threshold. This can happen at the edge, e.g., alpine@L15\n",
    "# has all accents w/in the distance, but the red accent was too far under\n",
    "# tundra@L15, so there's no entry. This particular case is fairly rare; it's\n",
    "# more likely that *all* monotones are undefined. Either way, both such\n",
    "# cases lead to partial scheme coverage.\n",
    "if len(Lma_map) < len(monotone_h_map):\n",
    "    print(f\"Warning: partial scheme coverage for {mL=}@{dT=}\")\n",
    "if biome not in Lma_map:\n",
    "    print(f\"Biome {biome} unable to meet {metric} constraints\")\n",
    "accent_colors = Lma_map.get(biome, {})\n",
    "\n",
    "scheme_pairs = []\n",
    "for i in range(4):\n",
    "    scheme_pairs.append(\n",
    "        (\n",
    "            f\"bg{i}\",\n",
    "            f\"f{{{{{biome}.l{L+i*I}}}}}\"\n",
    "        )\n",
    "    )\n",
    "for i in range(4):\n",
    "    scheme_pairs.append(\n",
    "        (\n",
    "            f\"fg{3-i}\",\n",
    "            f\"f{{{{{biome}.l{fg_gap+L+i*I}}}}}\"\n",
    "        )\n",
    "    )\n",
    "for term_color, mb_accent in term_color_map.items():\n",
    "    aL = int(100*accent_colors[mb_accent].coords()[0])\n",
    "    scheme_pairs.append(\n",
    "        (\n",
    "            f\"{term_color}\",\n",
    "            f\"f{{{{{mb_accent}.l{aL}}}}}\"\n",
    "        )\n",
    "    )\n",
    "\n",
    "term_fg_gap = 60\n",
    "scheme_pairs.extend([\n",
    "    (\"background\", f\"f{{{{{biome}.l{L}}}}}\"),\n",
    "    (\"selection_bg\", f\"f{{{{{biome}.l{L+I}}}}}\"),\n",
    "    (\"selection_fg\", f\"f{{{{{biome}.l{L+term_fg_gap}}}}}\"),\n",
    "    (\"foreground\", f\"f{{{{{biome}.l{L+term_fg_gap+I}}}}}\"),\n",
    "])\n",
    "\n",
    "scheme_toml = [\n",
    "    f\"{lhs:<8} = {rhs:<15}\"\n",
    "    for lhs, rhs in scheme_pairs\n",
    "] "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "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}}']"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "scheme_toml"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5915c49a-2546-42e7-af7c-10a1fbcdb9cf",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "monobiome",
   "language": "python",
   "name": "monobiome"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
