{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "5919980c-2b7c-4e17-8ce8-a64bc71d6ce1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "from pathlib import Path\n",
    "\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from scipy.interpolate import interp1d, CubicSpline, BSpline\n",
    "\n",
    "from coloraide import Color"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "a8c22978-bef8-42ae-a058-501679c5b1fd",
   "metadata": {},
   "outputs": [],
   "source": [
    "def max_chroma_fixed_LH(L, h, space='srgb', eps=1e-6, tol=1e-9):\n",
    "    def ok(C):\n",
    "        # build the exact OKLCH with fixed L,h; check if it lies in target gamut\n",
    "        return Color('oklch', [L, C, h]).convert(space).in_gamut(tolerance=tol)\n",
    "\n",
    "    lo, hi = 0.0, 0.1\n",
    "    while ok(hi): hi *= 2\n",
    "    while hi - lo > eps:\n",
    "        m = (lo + hi) / 2\n",
    "        lo, hi = (m, hi) if ok(m) else (lo, m)\n",
    "\n",
    "    Cmax = lo\n",
    "    c_oklch = Color('oklch', [L, Cmax, h])\n",
    "    c_srgb  = c_oklch.convert('srgb')\n",
    "    return Cmax, c_oklch, c_srgb.to_string(hex=True)\n",
    "    \n",
    "# weighted bezier; larger `w` to bring the curve to the corner point P1\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",
    "    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",
    "    return np.interp(x_query, x_vals, y_vals)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "d7c619e5-71e8-46e0-8c7c-b0de28d999aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "PALETTE_DIR = \"palettes\"\n",
    "\n",
    "# lightness_levels_n1 = list(range(15, 95+1, 1))\n",
    "lightness_levels_n1 = list(range(0, 100+1, 1))\n",
    "lightness_levels_n5 = list(range(15, 95+1, 5))\n",
    "\n",
    "monotone_hue_map = {\n",
    "    \"alpine\": 0,\n",
    "    \"badlands\": 29,\n",
    "    \"chaparral\": 62.5,\n",
    "    \"savanna\": 104,\n",
    "    \"grassland\": 148,\n",
    "    \"tundra\": 262,\n",
    "}\n",
    "accent_hue_map = {\n",
    "    \"red\": 29,\n",
    "    \"orange\": 62.5,\n",
    "    \"yellow\": 104,\n",
    "    \"green\": 148,\n",
    "    \"blue\": 262,\n",
    "}\n",
    "hue_map = {**monotone_hue_map, **accent_hue_map}\n",
    "\n",
    "# existing chroma settings at 5% lightness increments (v1.1.1)\n",
    "mb_chroma_pts = {\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",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a3d28771-c144-474b-8fa8-f6859ef8ce05",
   "metadata": {},
   "source": [
    "### Max chroma bounds and interpolated curves (v1.1.1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "3e5bcb42-c0a6-49b3-8350-67aec5bb391e",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 160396 characters]",
      "text/plain": [
       "<Figure size 600x1200 with 5 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# max chroma bounds\n",
    "hue_cmax_map = {}\n",
    "for hue_key, hue_val in hue_map.items():\n",
    "    hue_cmax_map[hue_key] = []\n",
    "    for ll in lightness_levels_n1:\n",
    "        max_chroma = max_chroma_fixed_LH(ll/100, hue_val)[0]\n",
    "        hue_cmax_map[hue_key].append(max_chroma)\n",
    "\n",
    "# interpolated chroma levels\n",
    "hue_cinterp_lin_map = {}\n",
    "hue_cinterp_cub_map = {}\n",
    "for hue_key, hue_val in accent_hue_map.items():\n",
    "    x1 = np.array(lightness_levels_n1)\n",
    "    x5 = np.array(lightness_levels_n5)\n",
    "    y5 = np.array(mb_chroma_pts[hue_key])\n",
    "\n",
    "    y1 = np.interp(x1, x5, y5)\n",
    "    hue_cinterp_lin_map[hue_key] = y1\n",
    "    \n",
    "    f = interp1d(x5, y5, kind='quadratic', fill_value='extrapolate')\n",
    "    f = CubicSpline(x5, y5, bc_type='natural')\n",
    "    hue_cinterp_cub_map[hue_key] = f(x1)\n",
    "\n",
    "    # use just: start, end, and max\n",
    "    max_idx = np.argmax(y5)\n",
    "    f = CubicSpline(\n",
    "        [x5[0], x5[1], x5[max_idx-1], x5[max_idx+1], x5[-2], x5[-1]],\n",
    "        [y5[0], y5[1], y5[max_idx-1], y5[max_idx+1], y5[-2], y5[-1]],\n",
    "        bc_type='natural'\n",
    "    )\n",
    "    hue_cinterp_cub_map[hue_key] = f(x1)\n",
    "\n",
    "# stacked plots for accents\n",
    "fig, axes = plt.subplots(len(accent_hue_map), 1, sharex=True, sharey=True, figsize=(6, 12))\n",
    "for ax, hue_label in zip(axes, accent_hue_map):\n",
    "    ax.plot(lightness_levels_n1, hue_cmax_map[hue_label])\n",
    "    ax.plot(lightness_levels_n5, mb_chroma_pts[hue_label])\n",
    "    \n",
    "    #ax.plot(lightness_levels_n1, hue_cinterp_lin_map[hue_label])\n",
    "    ax.plot(lightness_levels_n1, hue_cinterp_cub_map[hue_label], c='g', alpha=0.3)\n",
    "\n",
    "    max_idx = np.argmax(mb_chroma_pts[hue_label])\n",
    "    spline_indices = [0, 1, max_idx-1, max_idx+1, -2, -1]\n",
    "    \n",
    "    ax.scatter(\n",
    "        np.array(lightness_levels_n5)[spline_indices],\n",
    "        np.array(mb_chroma_pts[hue_label])[spline_indices],\n",
    "        c='g'\n",
    "    )\n",
    "    \n",
    "    ax.grid(True, alpha=0.3)\n",
    "    ax.text(1.01, 0.5, hue_label, transform=ax.transAxes, va='center')\n",
    "    \n",
    "axes[-1].set_xlabel(\"Lightness\")\n",
    "plt.suptitle(\"Max chroma at different lightness levels w/in sRGB gamut\")\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(top=0.93)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "32059ed7-7ade-42e5-b2d3-05801d6093f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "monotone_chroma = 0.011  # fixed monotone chroma\n",
    "hl_hex_arr = []\n",
    "hl_oklch_map = {}\n",
    "hl_hex_map = {}\n",
    "\n",
    "for i, (hue_name, hue) in enumerate(hue_map.items()):\n",
    "    lo_map, lh_map = {}, {}\n",
    "    hl_hex_arr.append(f\"[{hue_name}]\")\n",
    "    chroma_n1 = hue_cinterp_cub_map.get(hue_name)\n",
    "    for j, lightness in enumerate(lightness_levels_n1):\n",
    "        if i == 0:\n",
    "            chroma = 0\n",
    "        elif i <= 5:\n",
    "            chroma = monotone_chroma\n",
    "        else:\n",
    "            chroma = chroma_n1[j]\n",
    "            \n",
    "        c_oklch = Color('oklch', [lightness/100, chroma, hue])\n",
    "        c_srgb = c_oklch.convert('srgb')\n",
    "        \n",
    "        oklch_str = c_oklch.to_string(percent=True)\n",
    "        hex_str = c_srgb.to_string(hex=True)\n",
    "        \n",
    "        lo_map[f\"l{lightness}\"] = oklch_str\n",
    "        lh_map[f\"l{lightness}\"] = hex_str\n",
    "        hl_hex_arr.append(f'l{lightness} = \"{hex_str}\"')\n",
    "\n",
    "    hl_oklch_map[hue_name] = lo_map\n",
    "    hl_hex_map[hue_name] = lh_map\n",
    "    hl_hex_arr.append(\"\")\n",
    "\n",
    "# write files\n",
    "toml_hex = '\\n'.join(hl_hex_arr)\n",
    "with Path(PALETTE_DIR, 'monobiome-v111.toml').open('w') as f:\n",
    "    f.write(toml_hex)\n",
    "    \n",
    "with Path(PALETTE_DIR, 'monobiome-v111-oklch.json').open('w') as f:\n",
    "    json.dump(hl_oklch_map, f)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a0d65c67-997d-4941-8bca-dce36e839b0e",
   "metadata": {},
   "source": [
    "### Attempt to design unified chroma curve for all accents\n",
    "After full exploration: nice to have this option, but unifying the chroma curve for all accents mutes their unique qualities. I think it's better to flesh out curves that fully leverage their chroma depths; can maybe standardize the start and end chromas but that's all. Perhaps there's some curve quality that can be applied broadly, but even that can be a bit rough."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1832bc77-6e5d-4b70-a268-46405a5deb2d",
   "metadata": {},
   "source": [
    "#### Estimate intersection point"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "3c466cbe-236c-41f1-864d-7c2dbc65ef21",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0.00098114 0.0040535  0.00518646 0.        ]\n",
      "[0.00050583 0.00410004 0.00483475 0.        ]\n",
      "[0.00029984 0.00229111 0.00628815 0.        ]\n",
      "[0.00028915 0.00211182 0.03240891 0.        ]\n",
      "[0.00058517 0.00289383 0.01599808 0.        ]\n",
      "[0.0004776  0.0046257  0.00466537 0.        ]\n",
      "[0.00050583 0.00410004 0.00483475 0.        ]\n",
      "[0.00029984 0.00229111 0.00628815 0.        ]\n",
      "[0.00028915 0.00211182 0.03240891 0.        ]\n",
      "[0.00058517 0.00289383 0.01599808 0.        ]\n",
      "[0.0004776  0.0046257  0.00466537 0.        ]\n",
      "\n",
      "yellow [0.10558548 0.1076973  0.10980911 0.11192093 0.11403275 0.1161438\n",
      " 0.11825562 0.12036743 0.12247925 0.12459106]\n",
      "blue [0.23125534 0.23588028 0.24050522 0.24513092 0.24975586 0.24896698\n",
      " 0.24254303 0.23615799 0.22981262 0.22350693]\n"
     ]
    }
   ],
   "source": [
    "indices = [0, 1, -2, -1]\n",
    "for hue in hue_cmax_map:\n",
    "    print( np.array(np.array(hue_cmax_map[hue])[indices]) )\n",
    "print()\n",
    "\n",
    "# min values after checking 0, 1, -2, -1; then eyeballing l70 and l45\n",
    "indices = [     0,      1,     30,     55,     -2,     -1]\n",
    "values  = [0.0315, 0.0337, 0.0900, 0.1250, 0.0285, 0.0236]\n",
    "\n",
    "# take a look at yellow & blue, the two bottom bounding lines\n",
    "print( 'yellow', np.array(hue_cmax_map[\"yellow\"])[50:60] )\n",
    "print( 'blue', np.array(hue_cmax_map[\"blue\"])[50:60] )\n",
    "# so near guess at that max point is (71, 0.15); see the intersection\n",
    "# point below. This is eyeballed to see where Y & B values cross"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "496b8585-12d7-49d4-9bcf-ccb70c88242d",
   "metadata": {},
   "source": [
    "#### Verify best \"pushed up\" unified chroma curve"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "b3a67cae-1ea8-4587-a403-670e22a8e0b6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 70504 characters]",
      "text/plain": [
       "<Figure size 800x300 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# get the 3 points to shape the bezier\n",
    "indices = [    15,     71,     95]\n",
    "values  = [0.0315, 0.1500, 0.0236]\n",
    "\n",
    "P0 = np.array([indices[0], values[0]])\n",
    "P1 = np.array([indices[1], values[1]])\n",
    "P2 = np.array([indices[2], values[2]])\n",
    "\n",
    "t = np.linspace(0, 1, len(lightness_levels_n1)+1)[:, None]\n",
    "B = quad_bezier_rational(P0, P1, P2, w=5.0, t=t)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(8, 3))\n",
    "for hue_label in accent_hue_map:\n",
    "    ax.plot(lightness_levels_n1, hue_cmax_map[hue_label], c='g', alpha=0.3)\n",
    "\n",
    "# B is 2D, curve is parametrized; first dim are transformed `t` points\n",
    "ax.plot(B[:,:,0], B[:,:,1]) \n",
    "\n",
    "ax.set_xlabel(\"Lightness\")\n",
    "plt.suptitle(\"Best chroma bezier under all accent chroma roofs\")\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(top=0.9)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a798c10a-e67a-409e-a3ed-4420df6728cb",
   "metadata": {},
   "source": [
    "#### Put together unified chroma & generate color strings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "f3365e1d-bb8d-4b54-8253-4b318d174909",
   "metadata": {},
   "outputs": [],
   "source": [
    "# calculate curve at exact lightness; counter the bezier reparameterization\n",
    "shared_max_chroma_n1 = bezier_y_at_x(P0, P1, P2, 5.0, lightness_levels_n1)\n",
    "\n",
    "monotone_chroma = 0.011  # fixed monotone chroma\n",
    "hl_hex_arr = []\n",
    "hl_oklch_map = {}\n",
    "hl_hex_map = {}\n",
    "\n",
    "for i, (hue_name, hue) in enumerate(hue_map.items()):\n",
    "    lo_map, lh_map = {}, {}\n",
    "    hl_hex_arr.append(f\"[{hue_name}]\")\n",
    "    for j, lightness in enumerate(lightness_levels_n1):\n",
    "        if i == 0:\n",
    "            chroma = 0\n",
    "        elif i <= 5:\n",
    "            chroma = monotone_chroma\n",
    "        else:\n",
    "            chroma = shared_max_chroma_n1[j]\n",
    "            \n",
    "        c_oklch = Color('oklch', [lightness/100, chroma, hue])\n",
    "        c_srgb = c_oklch.convert('srgb')\n",
    "        \n",
    "        # oklch_str = c_oklch.to_string(percent=True)\n",
    "        l, c, h = c_oklch.convert('oklch').coords()\n",
    "        oklch_str = f\"oklch({l*100:.1f}% {c:.4f} {h:.1f})\"\n",
    "        \n",
    "        hex_str = c_srgb.to_string(hex=True)\n",
    "        \n",
    "        lo_map[f\"l{lightness}\"] = oklch_str\n",
    "        lh_map[f\"l{lightness}\"] = hex_str\n",
    "        hl_hex_arr.append(f'l{lightness} = \"{hex_str}\"')\n",
    "\n",
    "    hl_oklch_map[hue_name] = lo_map\n",
    "    hl_hex_map[hue_name] = lh_map\n",
    "    hl_hex_arr.append(\"\")\n",
    "\n",
    "# write files -- UCC = \"unified chroma curve\"\n",
    "toml_hex = '\\n'.join(hl_hex_arr)\n",
    "with Path(PALETTE_DIR, 'monobiome-vUCC.toml').open('w') as f:\n",
    "    f.write(toml_hex)\n",
    "    \n",
    "with Path(PALETTE_DIR, 'monobiome-vUCC-oklch.json').open('w') as f:\n",
    "    json.dump(hl_oklch_map, f)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ef58a343-2c93-46aa-accc-1590381e8e66",
   "metadata": {},
   "source": [
    "### Tailored chroma curves"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "id": "a4d02bc3-44ab-4f62-82cc-4e72cac0a449",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 144040 characters]",
      "text/plain": [
       "<Figure size 600x1000 with 5 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# indices = [    15,     95]\n",
    "# values  = [0.0315, 0.0236]\n",
    "indices = [0, 100]\n",
    "values  = [0, 0]\n",
    "\n",
    "# not the same chroma value, but sync *when* the max chroma is hit\n",
    "# so similar curves but allowed to shift up and down\n",
    "unified_cx = 55\n",
    "unified_cy = [0.22, 0.125, 0.1, 0.13, 0.16]\n",
    "\n",
    "hue_cinterp_cub_map_unify = {}\n",
    "hue_cinterp_cub_map_unify_B = {}\n",
    "for i, (hue_key, hue_val) in enumerate(accent_hue_map.items()):\n",
    "    x1 = np.array(lightness_levels_n1)\n",
    "    x5 = np.array(lightness_levels_n5)\n",
    "    y5 = np.array(mb_chroma_pts[hue_key])\n",
    "\n",
    "    max_idx = np.argmax(y5)\n",
    "    # 2-point middle\n",
    "    # f = CubicSpline(\n",
    "    #     [indices[0], x5[max_idx-1], x5[max_idx+1], indices[-1]],\n",
    "    #     [values[0],  y5[max_idx-1], y5[max_idx+1], values[-1]],\n",
    "    #     bc_type='natural'\n",
    "    # )\n",
    "    # 1-point middle\n",
    "    # f = CubicSpline(\n",
    "    #     [indices[0], x5[max_idx], indices[-1]],\n",
    "    #     [values[0],  y5[max_idx], values[-1]],\n",
    "    #     bc_type='natural'\n",
    "    # )\n",
    "    f = CubicSpline(\n",
    "        [indices[0], unified_cx, indices[-1]],\n",
    "        [values[0],  unified_cy[i], values[-1]],\n",
    "        bc_type='natural'\n",
    "    )\n",
    "    # hue_cinterp_cub_map_unify[hue_key] = f(x1)\n",
    "    \n",
    "    P0 = np.array([indices[0], values[0]])\n",
    "    # P1 = np.array([x5[max_idx], y5[max_idx]])\n",
    "    P1 = np.array([unified_cx, unified_cy[i]])\n",
    "    P2 = np.array([indices[-1], values[-1]])\n",
    "    \n",
    "    t = np.linspace(0, 1, 400)[:, None]\n",
    "    B = quad_bezier_rational(P0, P1, P2, w=8.0, t=t)\n",
    "    hue_cinterp_cub_map_unify[hue_key] = B[:,:,1].flatten()\n",
    "    hue_cinterp_cub_map_unify_B[hue_key] = B\n",
    "\n",
    "fig, axes = plt.subplots(len(hue_cmax_map), 1, sharex=True, sharey=True, figsize=(6, 10))\n",
    "for ax, hue_key in zip(axes, accent_hue_map):\n",
    "    ax.grid(True, alpha=0.3)\n",
    "    ax.text(1.01, 0.5, hue_key, transform=ax.transAxes, va='center')\n",
    "    \n",
    "    ax.plot(lightness_levels_n1, hue_cmax_map[hue_key])\n",
    "    ax.plot(lightness_levels_n1, hue_cinterp_cub_map[hue_key], c='g', alpha=0.5)\n",
    "    # ax.plot(lightness_levels_n1, hue_cinterp_cub_map_unify[hue_key], c='b', alpha=0.4)\n",
    "    ax.plot(hue_cinterp_cub_map_unify_B[hue_key][:,:,0], hue_cinterp_cub_map_unify_B[hue_key][:,:,1], c='b', alpha=0.4)\n",
    "    ax.plot(lightness_levels_n5, mb_chroma_pts[hue_key], c='r', alpha=0.3)\n",
    "\n",
    "    max_idx = np.argmax(mb_chroma_pts[hue_key])\n",
    "    spline_indices = [0, max_idx-1, max_idx+1, -1]\n",
    "    \n",
    "    ax.scatter(\n",
    "        np.array(lightness_levels_n5)[spline_indices],\n",
    "        np.array(mb_chroma_pts[hue_key])[spline_indices],\n",
    "        c='g'\n",
    "    )\n",
    "    \n",
    "axes[-1].set_xlabel(\"Lightness\")\n",
    "plt.suptitle(\"Max chroma at different lightness levels w/in sRGB gamut\")\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(top=0.93)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "daefddbc-2cda-45dd-b2cf-8311732f8ba9",
   "metadata": {},
   "outputs": [],
   "source": [
    "indices = [    15,     95]\n",
    "values  = [0.0315, 0.0236]\n",
    "\n",
    "# not the same chroma value, but sync *when* the max chroma is hit\n",
    "# so similar curves but allowed to shift up and down across hue\n",
    "unified_cx = 55\n",
    "unified_cy = [0.21, 0.13, 0.09, 0.13, 0.15]  # these were manually adjusted\n",
    "hue_max_chroma_n1_map = {}\n",
    "for i, (hue_key, hue_val) in enumerate(accent_hue_map.items()):\n",
    "    P0 = np.array([indices[0], values[0]])\n",
    "    # P1 = np.array([x5[max_idx], y5[max_idx]])\n",
    "    P1 = np.array([unified_cx, unified_cy[i]])\n",
    "    P2 = np.array([indices[-1], values[-1]])\n",
    "    \n",
    "    t = np.linspace(0, 1, 400)[:, None]\n",
    "    hue_max_chroma_n1_map[hue_key] = bezier_y_at_x(P0, P1, P2, 5.0, lightness_levels_n1)\n",
    "    \n",
    "monotone_chroma = 0.011  # fixed monotone chroma\n",
    "hl_hex_arr = []\n",
    "hl_oklch_map = {}\n",
    "hl_hex_map = {}\n",
    "\n",
    "for i, (hue_name, hue) in enumerate(hue_map.items()):\n",
    "    lo_map, lh_map = {}, {}\n",
    "    hl_hex_arr.append(f\"[{hue_name}]\")\n",
    "    max_chroma_n1 = hue_max_chroma_n1_map.get(hue_name)\n",
    "    for j, lightness in enumerate(lightness_levels_n1):\n",
    "        if i == 0:\n",
    "            chroma = 0\n",
    "        elif i <= 5:\n",
    "            chroma = monotone_chroma\n",
    "        else:\n",
    "            chroma = max_chroma_n1[j]\n",
    "            \n",
    "        c_oklch = Color('oklch', [lightness/100, chroma, hue])\n",
    "        c_srgb = c_oklch.convert('srgb')\n",
    "        \n",
    "        oklch_str = c_oklch.to_string(percent=True)\n",
    "        hex_str = c_srgb.to_string(hex=True)\n",
    "        \n",
    "        lo_map[f\"l{lightness}\"] = oklch_str\n",
    "        lh_map[f\"l{lightness}\"] = hex_str\n",
    "        hl_hex_arr.append(f'l{lightness} = \"{hex_str}\"')\n",
    "\n",
    "    hl_oklch_map[hue_name] = lo_map\n",
    "    hl_hex_map[hue_name] = lh_map\n",
    "    hl_hex_arr.append(\"\")\n",
    "\n",
    "# write files -- MCL = \"max chroma lightness\"\n",
    "toml_hex = '\\n'.join(hl_hex_arr)\n",
    "with Path(PALETTE_DIR, 'monobiome-vMCL.toml').open('w') as f:\n",
    "    f.write(toml_hex)\n",
    "    \n",
    "with Path(PALETTE_DIR, 'monobiome-vMCL-oklch.json').open('w') as f:\n",
    "    json.dump(hl_oklch_map, f)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7cf22fc-5287-4895-8dab-5b9fe0ffa2c6",
   "metadata": {},
   "source": [
    "#### Verify the written colors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "83456bc7-c8c4-4377-a4ef-1dd5e150bb67",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 39608 characters]",
      "text/plain": [
       "<Figure size 400x400 with 5 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, axes = plt.subplots(len(hue_cmax_map), 1, sharex=True, sharey=True, figsize=(4, 4))\n",
    "for ax, hue_label in zip(axes, accent_hue_map):\n",
    "    chroma_list = [\n",
    "        Color(oklch_str)._coords[1]\n",
    "        for l_str, oklch_str in hl_oklch_map[hue_label].items()\n",
    "    ]\n",
    "    \n",
    "    ax.plot(lightness_levels_n1, hue_cmax_map[hue_label], c='b')\n",
    "    ax.plot(lightness_levels_n1, chroma_list, c='g', alpha=0.7)\n",
    "    \n",
    "axes[-1].set_xlabel(\"Lightness\")\n",
    "plt.suptitle(\"Written chroma levels\")\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(top=0.93)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "8721f417-c6c2-43eb-bccc-5eed9bdc0c73",
   "metadata": {},
   "outputs": [],
   "source": [
    "# preview palette\n",
    "\n",
    "# fig, axes = plt.subplots(len(hue_cmax_map), 1, sharex=True, sharey=True, figsize=(4, 4))\n",
    "# for ax, hue_label in zip(axes, accent_hue_map):\n",
    "#     for l_str, oklch_str in hl_oklch_map[hue_label].items():\n",
    "#         Color(oklch_str)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7b17777-d92f-4541-8627-715b32a21c40",
   "metadata": {},
   "source": [
    "#### 3-point quadratic bezier fit to v1.1.1\n",
    "The more \"systematic\" design choices for picking chroma curves seem to stamp on accent identifies, even the fixed lightness point (but allowing variable chroma).\n",
    "\n",
    "Linear or cubic interpolation of the original 5% lightness jumps is acceptable, but I want two things in the new rev:\n",
    "\n",
    "1. Fixed chroma settings at the beginning (15) and end (95) lightness settings for each accent. This at least gives us some unified bounds for filling in the chroma curve, even if the shapes and control points of the curves vary quite a bit to make the color identities fit as intended.\n",
    "2. Follow a parameterized curve rather than being ad hoc, discrete jumps. The previously chosen chroma changes across 5% lightness jumps were something close to linear interpolation between established control points, but they were not reporoducible. That is, it was based on some loose napkin math rather than an established mathematical relatinship that could be written down. Even if I imitate the original curves as closely as possible, I want a parameterized model for the curve to ulimately produce it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "id": "5136854e-e9b1-4845-83d4-b8925bd6b4bf",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 122044 characters]",
      "text/plain": [
       "<Figure size 600x1000 with 5 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# archiving these weights and offsets: specific to the 15 and 95 endpoints\n",
    "indices = [    15,     95]\n",
    "values  = [0.0315, 0.0236]\n",
    "\n",
    "accent_weights = {\n",
    "    \"red\": 4.0,\n",
    "    \"orange\": 3.0,\n",
    "    \"yellow\": 0.5,\n",
    "    \"green\": 0.5,\n",
    "    \"blue\": 0.8,\n",
    "}\n",
    "accent_offsets = {\n",
    "    \"red\": -5,\n",
    "    \"orange\": -3,\n",
    "    \"yellow\": 0,\n",
    "    \"green\": 0,\n",
    "    \"blue\": -1,\n",
    "}\n",
    "hue_bez_cmax_map = {}\n",
    "for i, (hue_key, hue_val) in enumerate(accent_hue_map.items()):\n",
    "    cmax_idx = np.argmax(hue_cmax_map[hue_key])\n",
    "    cmax_idx += accent_offsets[hue_key]\n",
    "\n",
    "    cmax_lightness = lightness_levels_n1[cmax_idx]\n",
    "    cmax_chroma = hue_cmax_map[hue_key][cmax_idx]\n",
    "\n",
    "    P0 = np.array([indices[0], values[0]])\n",
    "    P1 = np.array([cmax_lightness, cmax_chroma])\n",
    "    P2 = np.array([indices[-1], values[-1]])\n",
    "    \n",
    "    t = np.linspace(0, 1, 400)[:, None]\n",
    "    B = quad_bezier_rational(P0, P1, P2, w=accent_weights[hue_key], t=t)\n",
    "    \n",
    "    hue_bez_cmax_map[hue_key] = B\n",
    "\n",
    "fig, axes = plt.subplots(len(hue_cmax_map), 1, sharex=True, sharey=True, figsize=(6, 10))\n",
    "for ax, hue_key in zip(axes, accent_hue_map):\n",
    "    ax.grid(True, alpha=0.3)\n",
    "    ax.text(1.01, 0.5, hue_key, transform=ax.transAxes, va='center')\n",
    "    \n",
    "    ax.plot(lightness_levels_n1, hue_cmax_map[hue_key])\n",
    "    ax.plot(hue_bez_cmax_map[hue_key][:,:,0], hue_bez_cmax_map[hue_key][:,:,1], c='b', alpha=0.4)\n",
    "    ax.plot(lightness_levels_n5, mb_chroma_pts[hue_key], c='r', alpha=0.3)\n",
    "\n",
    "    max_idx = np.argmax(mb_chroma_pts[hue_key])\n",
    "    spline_indices = [0, max_idx-1, max_idx+1, -1]\n",
    "    \n",
    "    ax.scatter(\n",
    "        np.array(lightness_levels_n5)[spline_indices],\n",
    "        np.array(mb_chroma_pts[hue_key])[spline_indices],\n",
    "        s=8, c='g'\n",
    "    )\n",
    "    \n",
    "axes[-1].set_xlabel(\"Lightness\")\n",
    "plt.suptitle(\"Max chroma at different lightness levels w/in sRGB gamut\")\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(top=0.93)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 138,
   "id": "5eb9adc3-cc6e-4471-ab0c-29a8689e8bd7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 121432 characters]",
      "text/plain": [
       "<Figure size 600x1000 with 5 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "indices = [0, 100]\n",
    "values  = [0, 0]\n",
    "\n",
    "accent_weights = {\n",
    "    \"red\": 3.5,\n",
    "    \"orange\": 4.0,\n",
    "    \"yellow\": 0.5,\n",
    "    \"green\": 0.75,\n",
    "    \"blue\": 1.0,\n",
    "}\n",
    "accent_offsets = {\n",
    "    \"red\": -5,\n",
    "    \"orange\": -2,\n",
    "    \"yellow\": 0,\n",
    "    \"green\": 0,\n",
    "    \"blue\": 0,\n",
    "}\n",
    "hue_bez_cmax_map = {}\n",
    "for i, (hue_key, hue_val) in enumerate(accent_hue_map.items()):\n",
    "    cmax_idx = np.argmax(hue_cmax_map[hue_key])\n",
    "    cmax_idx += accent_offsets[hue_key]\n",
    "\n",
    "    cmax_lightness = lightness_levels_n1[cmax_idx]\n",
    "    cmax_chroma = hue_cmax_map[hue_key][cmax_idx]\n",
    "\n",
    "    P0 = np.array([indices[0], values[0]])\n",
    "    P1 = np.array([cmax_lightness, cmax_chroma])\n",
    "    P2 = np.array([indices[-1], values[-1]])\n",
    "    \n",
    "    t = np.linspace(0, 1, 400)[:, None]\n",
    "    B = quad_bezier_rational(P0, P1, P2, w=accent_weights[hue_key], t=t)\n",
    "    \n",
    "    hue_bez_cmax_map[hue_key] = B\n",
    "\n",
    "fig, axes = plt.subplots(len(accent_hue_map), 1, sharex=True, sharey=True, figsize=(6, 10))\n",
    "for ax, hue_key in zip(axes, accent_hue_map):\n",
    "    ax.grid(True, alpha=0.3)\n",
    "    ax.text(1.01, 0.5, hue_key, transform=ax.transAxes, va='center')\n",
    "    \n",
    "    ax.plot(lightness_levels_n1, hue_cmax_map[hue_key])\n",
    "    ax.plot(hue_bez_cmax_map[hue_key][:,:,0], hue_bez_cmax_map[hue_key][:,:,1], c='b', alpha=0.4)\n",
    "    ax.plot(lightness_levels_n5, mb_chroma_pts[hue_key], c='r', alpha=0.3)\n",
    "\n",
    "    max_idx = np.argmax(mb_chroma_pts[hue_key])\n",
    "    spline_indices = [0, max_idx-1, max_idx+1, -1]\n",
    "    \n",
    "    ax.scatter(\n",
    "        np.array(lightness_levels_n5)[spline_indices],\n",
    "        np.array(mb_chroma_pts[hue_key])[spline_indices],\n",
    "        s=8, c='g'\n",
    "    )\n",
    "    \n",
    "axes[-1].set_xlabel(\"Lightness\")\n",
    "plt.suptitle(\"Max chroma at different lightness levels w/in sRGB gamut\")\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(top=0.93)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 125,
   "id": "ba6adb00-e648-4510-97e4-71882c5564e0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'badlands' breached max chroma at L0\n",
      "'badlands' breached max chroma at L1\n",
      "'badlands' breached max chroma at L2\n",
      "'badlands' breached max chroma at L98\n",
      "'badlands' breached max chroma at L99\n",
      "'badlands' breached max chroma at L100\n",
      "'chaparral' breached max chroma at L0\n",
      "'chaparral' breached max chroma at L1\n",
      "'chaparral' breached max chroma at L2\n",
      "'chaparral' breached max chroma at L3\n",
      "'chaparral' breached max chroma at L4\n",
      "'chaparral' breached max chroma at L99\n",
      "'chaparral' breached max chroma at L100\n",
      "'savanna' breached max chroma at L0\n",
      "'savanna' breached max chroma at L1\n",
      "'savanna' breached max chroma at L2\n",
      "'savanna' breached max chroma at L3\n",
      "'savanna' breached max chroma at L4\n",
      "'savanna' breached max chroma at L5\n",
      "'savanna' breached max chroma at L100\n",
      "'grassland' breached max chroma at L0\n",
      "'grassland' breached max chroma at L1\n",
      "'grassland' breached max chroma at L2\n",
      "'grassland' breached max chroma at L3\n",
      "'grassland' breached max chroma at L100\n",
      "'tundra' breached max chroma at L0\n",
      "'tundra' breached max chroma at L1\n",
      "'tundra' breached max chroma at L2\n",
      "'tundra' breached max chroma at L98\n",
      "'tundra' breached max chroma at L99\n",
      "'tundra' breached max chroma at L100\n",
      "'red' breached max chroma at L85\n",
      "'red' breached max chroma at L86\n",
      "'red' breached max chroma at L87\n",
      "'red' breached max chroma at L88\n",
      "'red' breached max chroma at L89\n",
      "'red' breached max chroma at L90\n",
      "'red' breached max chroma at L91\n",
      "'red' breached max chroma at L92\n",
      "'red' breached max chroma at L93\n",
      "'red' breached max chroma at L94\n",
      "'red' breached max chroma at L95\n",
      "'red' breached max chroma at L96\n",
      "'red' breached max chroma at L97\n",
      "'red' breached max chroma at L98\n",
      "'red' breached max chroma at L99\n",
      "'orange' breached max chroma at L96\n",
      "'orange' breached max chroma at L97\n",
      "'orange' breached max chroma at L98\n",
      "'orange' breached max chroma at L99\n",
      "'blue' breached max chroma at L91\n",
      "'blue' breached max chroma at L92\n",
      "'blue' breached max chroma at L93\n",
      "'blue' breached max chroma at L94\n",
      "'blue' breached max chroma at L95\n",
      "'blue' breached max chroma at L96\n",
      "'blue' breached max chroma at L97\n",
      "'blue' breached max chroma at L98\n",
      "'blue' breached max chroma at L99\n"
     ]
    }
   ],
   "source": [
    "hue_bez_cmax_map = {}\n",
    "for i, (hue_key, hue_val) in enumerate(accent_hue_map.items()):\n",
    "    cmax_idx = np.argmax(hue_cmax_map[hue_key])\n",
    "    cmax_idx += accent_offsets[hue_key]\n",
    "\n",
    "    cmax_lightness = lightness_levels_n1[cmax_idx]\n",
    "    cmax_chroma = hue_cmax_map[hue_key][cmax_idx]\n",
    "\n",
    "    P0 = np.array([indices[0], values[0]])\n",
    "    P1 = np.array([cmax_lightness, cmax_chroma])\n",
    "    P2 = np.array([indices[-1], values[-1]])\n",
    "    \n",
    "    hue_bez_cmax_map[hue_key] = bezier_y_at_x(\n",
    "        P0, P1, P2, accent_weights[hue_key], lightness_levels_n1\n",
    "    )\n",
    "    \n",
    "monotone_chroma = 0.011  # fixed monotone chroma\n",
    "hl_hex_arr = []\n",
    "hl_oklch_map = {}\n",
    "hl_hex_map = {}\n",
    "\n",
    "# filter lightnesses outside bounds\n",
    "# l_points = list(range(10, 98+1))  # full 10-98\n",
    "l_points = list(range(10, 95+1, 5))+[98]  # full 10-98\n",
    "\n",
    "for i, (hue_name, hue) in enumerate(hue_map.items()):\n",
    "    lo_map, lh_map = {}, {}\n",
    "    hl_hex_arr.append(f\"[{hue_name}]\")\n",
    "    \n",
    "    chroma_n1 = hue_bez_cmax_map.get(hue_name)\n",
    "    max_chroma_n1 = hue_cmax_map.get(hue_name)\n",
    "    for j, lightness in enumerate(lightness_levels_n1):\n",
    "        if i == 0:\n",
    "            chroma = 0\n",
    "        elif i <= 5:\n",
    "            if monotone_chroma <= max_chroma_n1[j]:\n",
    "                chroma = monotone_chroma\n",
    "            else:\n",
    "                print(f\"'{hue_name}' breached max chroma at L{lightness}\")\n",
    "                chroma = max_chroma_n1[j]\n",
    "        else:\n",
    "            if chroma_n1[j] <= max_chroma_n1[j]:\n",
    "                chroma = chroma_n1[j]\n",
    "            else:\n",
    "                print(f\"'{hue_name}' breached max chroma at L{lightness}\")\n",
    "                chroma = max_chroma_n1[j]\n",
    "\n",
    "        # filter lightness after change to print\n",
    "        if lightness < l_bounds[0] or lightness > l_bounds[1]:\n",
    "            continue\n",
    "            \n",
    "        c_oklch = Color('oklch', [lightness/100, chroma, hue])\n",
    "        c_srgb = c_oklch.convert('srgb')\n",
    "        \n",
    "        oklch_str = c_oklch.to_string(percent=True)\n",
    "        hex_str = c_srgb.to_string(hex=True)\n",
    "        \n",
    "        lo_map[f\"l{lightness}\"] = oklch_str\n",
    "        lh_map[f\"l{lightness}\"] = hex_str\n",
    "        hl_hex_arr.append(f'l{lightness} = \"{hex_str}\"')\n",
    "\n",
    "    hl_oklch_map[hue_name] = lo_map\n",
    "    hl_hex_map[hue_name] = lh_map\n",
    "    hl_hex_arr.append(\"\")\n",
    "\n",
    "# write files -- QBR = \"quadratic bezier rational\"\n",
    "toml_hex = '\\n'.join(hl_hex_arr)\n",
    "with Path(PALETTE_DIR, 'monobiome-vQBR.toml').open('w') as f:\n",
    "    f.write(toml_hex)\n",
    "    \n",
    "with Path(PALETTE_DIR, 'monobiome-vQBR-oklch.json').open('w') as f:\n",
    "    json.dump(hl_oklch_map, f)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 139,
   "id": "0d68c77d-5517-4339-8a54-25584009d3c0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 66544 characters]",
      "text/plain": [
       "<Figure size 400x800 with 6 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, axes = plt.subplots(len(monotone_hue_map), 1, sharex=True, sharey=True, figsize=(4, 8))\n",
    "c, c_ax = 0, {}\n",
    "for i, hue_label in enumerate(hue_map):\n",
    "    hue_val = hue_map[hue_label]\n",
    "    chroma_list = [\n",
    "        Color(oklch_str)._coords[1]\n",
    "        for l_str, oklch_str in hl_oklch_map[hue_label].items()\n",
    "    ]\n",
    "\n",
    "    if hue_val not in c_ax:\n",
    "        c_ax[hue_val] = axes[i]\n",
    "    ax = c_ax[hue_val]\n",
    "\n",
    "    ax.title.set_text(f\"Hue: {hue_val}\")\n",
    "    xs = np.array(lightness_levels_n1)[l_bounds[0]:l_bounds[1]+1]\n",
    "    cs = np.array(hue_cmax_map[hue_label])[l_bounds[0]:l_bounds[1]+1]\n",
    "    ax.plot(xs, cs, c='b')\n",
    "    ax.plot(xs, chroma_list, alpha=0.7)\n",
    "    \n",
    "axes[-1].set_xlabel(\"Lightness\")\n",
    "ax.set_xticks([l_bounds[0], 50, l_bounds[1]])\n",
    "plt.suptitle(\"Written chroma levels\")\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(top=0.93)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22d8189c-5d07-4101-8920-5ff6536331b0",
   "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
}
