{ "cells": [ { "cell_type": "markdown", "id": "fbbe6ba8-483c-4d89-a30c-2464452b2b0e", "metadata": {}, "source": [ "To visualize the Monobiome palette construction:\n", "\n", "1. Establish core color bases\n", "2. Fix color hues, establish lightness range, fit bezier chroma curves\n", "3. Pinch bezier tightness and adjust control points for clear hue roles (approx exponential relative integral diffs across chroma domain)\n", "4. Set biome + minimum lightness base, contrast metric, distance minimum; ensure constraint feasbility" ] }, { "cell_type": "code", "execution_count": 12, "id": "1abfc7de-3ced-4407-b38e-3fc276b6a133", "metadata": {}, "outputs": [], "source": [ "import math\n", "import json\n", "from pathlib import Path\n", "import numpy as np\n", "\n", "import plotly.io as pio\n", "import plotly.graph_objects as go\n", "from coloraide import Color\n", "import imageio.v2 as imageio\n", "\n", "from copy import copy\n", "\n", "import gamut_3d_plotly as diagrams\n", "\n", "pio.renderers\n", "pio.renderers.default = \"notebook_connected\"\n", "\n", "\n", "def face_radial_plane(fig, theta_deg, r=2.5, tilt_deg=0):\n", " # normal in xy\n", " nx = -math.sin(math.radians(theta_deg))\n", " ny = math.cos(math.radians(theta_deg))\n", "\n", " # allow optional tilt up/down so it's not perfectly horizontal\n", " tilt = math.radians(tilt_deg) # 0 => horizontal view, positive looks slightly downward\n", " ex = r * nx * math.cos(tilt)\n", " ey = r * ny * math.cos(tilt)\n", " ez = r * math.sin(tilt)\n", "\n", " fig.update_layout(\n", " scene_camera=dict(\n", " eye=dict(x=ex, y=ey, z=ez),\n", " center=dict(x=0, y=0, z=0),\n", " up=dict(x=0, y=0, z=1)\n", " )\n", " )\n", "\n", "def make_rotating_gif(fig, path, n_frames=120, fps=30, z_frac=0.3):\n", " cam = fig.layout.scene.camera\n", " eye = cam.eye\n", " cx, cy, cz = float(eye.x), float(eye.y), float(eye.z)\n", "\n", " r_xy = (cx**2 + cy**2)**0.5\n", " z = r_xy * z_frac if z_frac is not None else cz\n", "\n", " frames = []\n", " for k in range(n_frames):\n", " theta = 2 * np.pi * k / n_frames\n", " new_eye = dict(x=r_xy * np.cos(theta), y=r_xy * np.sin(theta), z=z)\n", " fig.update_layout(scene_camera=dict(eye=new_eye, center=cam.center, projection=cam.projection))\n", " png = fig.to_image(format=\"png\", width=fig.layout.width, height=fig.layout.height, scale=1)\n", " frames.append(imageio.imread(png))\n", "\n", " imageio.mimsave(path, frames, fps=fps)\n" ] }, { "cell_type": "code", "execution_count": 13, "id": "558912e1-07e4-4fbe-806c-b237f532fc8d", "metadata": {}, "outputs": [], "source": [ "import numpy as np, imageio.v2 as imageio\n", "from concurrent.futures import ProcessPoolExecutor\n", "import plotly.graph_objects as go\n", "\n", "def _render_frame(args):\n", " fig_dict, eye, width, height = args\n", " fig = go.Figure(fig_dict)\n", " fig.update_layout(scene_camera=dict(eye=eye))\n", " png = fig.to_image(format=\"png\", width=width, height=height, scale=1)\n", " return png\n", "\n", "def make_rotating_gif_parallel(\n", " fig,\n", " path,\n", " n_frames=120,\n", " fps=30,\n", " z_frac=0.3,\n", " workers=None\n", "):\n", " cam = fig.layout.scene.camera\n", " eye = cam.eye\n", " cx, cy, cz = float(eye.x), float(eye.y), float(eye.z)\n", "\n", " r_xy = (cx**2 + cy**2)**0.5\n", " z = r_xy * z_frac if z_frac is not None else cz\n", "\n", " # Precompute all camera positions\n", " eyes = []\n", " for k in range(n_frames):\n", " theta = 2 * np.pi * k / n_frames\n", " eyes.append(dict(x=r_xy * np.cos(theta), y=r_xy * np.sin(theta), z=z))\n", "\n", " fig_dict = fig.to_dict()\n", " width = int(fig.layout.width) if fig.layout.width else 800\n", " height = int(fig.layout.height) if fig.layout.height else 800\n", "\n", " # Prepare args for each frame\n", " args = [(fig_dict, e, width, height) for e in eyes]\n", "\n", " # Render in parallel\n", " with ProcessPoolExecutor(max_workers=workers) as ex:\n", " pngs = list(ex.map(_render_frame, args))\n", "\n", " frames = [imageio.imread(p) for p in pngs]\n", " imageio.mimsave(path, frames, fps=fps)\n" ] }, { "cell_type": "code", "execution_count": 14, "id": "642b3763-f250-4e24-bd4c-2a080772467d", "metadata": {}, "outputs": [], "source": [ "PALETTE_DIR = \"palettes\"\n", "with Path(PALETTE_DIR, \"monobiome-vQBRsn-130-oklch.json\").open(\"rb\") as f:\n", " mb_vQBRsn_130_dict = json.load(f)\n", " \n", "name = 'oklch'\n", "gamut = 'srgb'\n", "title = f'{name} color space'\n", "\n", "elev=45\n", "azim=29.0 # -60.0\n", "width, height = 800, 600\n", "# width, height = 400, 400\n", "\n", "trajectories = {\n", " c_name: [Color(c_str) for c_str in c_str_dict.values()]\n", " for c_name, c_str_dict in mb_vQBRsn_130_dict.items()\n", "}" ] }, { "cell_type": "code", "execution_count": 15, "id": "deaa2bce-79c7-4e39-b36c-32d201aa16ed", "metadata": {}, "outputs": [ { "data": { "text/html": [ " \n", " \n", " " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
\n", "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig = diagrams.plot_gamut_in_space(\n", " name,\n", " gamuts={\n", " gamut: {\n", " 'opacity': 0.10,\n", " #'edges': 'darkgrey',\n", " }\n", " },\n", " # dark=True,\n", " gmap={'method': 'raytrace'},\n", " title=title,\n", " size=(width, height),\n", " camera={'a': azim, 'e': elev, 'r': 2.5},\n", " projection='orthographic',\n", ")\n", "fig.update_layout(\n", " margin=dict(l=0, r=0, t=0, b=0)\n", ")\n", "face_radial_plane(fig, 29)\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": 16, "id": "6cce6d6b-0048-4b8f-a21c-99d9eeba5752", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "tfig = go.Figure(fig)\n", "\n", "for trajectory in trajectories.values():\n", " diagrams.plot_trajectory(\n", " tfig,\n", " space=\"oklab\",\n", " trajectory_colors=trajectory, #[70:],\n", " gmap={'method': 'raytrace'},\n", " filters=(),\n", " connect_line=True,\n", " marker_size=1.5,\n", " line_width=1.5,\n", " opacity=0.8,\n", " )\n", "tfig.show()\n", "\n", "# make_rotating_gif_parallel(tfig, \"figures/mb_trajectories.gif\", n_frames=120, fps=30)" ] }, { "cell_type": "code", "execution_count": 25, "id": "e4a4e929-3264-4e56-b17d-8d0d68e8f088", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "#diagrams.freeze_scene_axes(tmp_fig)\n", "tfig = go.Figure(fig)\n", "biome = \"alpine\"\n", "colors = set([\"red\", \"orange\", \"yellow\", \"green\", \"blue\"])\n", "\n", "# colors = set([\"red\", \"orange\", \"yellow\", \"green\", \"blue\", \"violet\", \"magenta\"])\n", "# for i, cname in enumerate([\"violet\", \"magenta\"]):\n", "# trajectory = []\n", "# for color in trajectories[\"blue\"]:\n", "# new_color = Color(str(color))\n", "# new_color[1] += 0.035*i\n", "# new_color[2] += 44*(i+1)\n", "# trajectory.append(new_color)\n", "# trajectories[cname] = trajectory\n", "\n", "for c_name, trajectory in trajectories.items():\n", " if c_name not in colors: continue\n", " \n", " diagrams.plot_trajectory(\n", " tfig,\n", " space=\"oklab\",\n", " trajectory_colors=trajectory,\n", " gmap={'method': 'raytrace'},\n", " filters=(),\n", " connect_line=True,\n", " marker_size=0.05,\n", " line_width=2,\n", " opacity=0.5,\n", " )\n", "\n", "for c_name, trajectory in trajectories.items():\n", " if c_name == biome:\n", " trajectory = trajectory[10:11]\n", " elif c_name in colors:\n", " trajectory = trajectory[10+50:61]\n", " else:\n", " continue\n", " \n", " diagrams.plot_trajectory(\n", " tfig,\n", " space=\"oklab\",\n", " trajectory_colors=trajectory,\n", " gmap={'method': 'raytrace'},\n", " filters=(),\n", " # connect_line=True,\n", " marker_size=8.0,\n", " line_width=0,\n", " opacity=1.0 # 0.95,\n", " )\n", " \n", "\n", "radius = 0.5\n", "diagrams.plot_sphere_in_oklch3(\n", " tfig,\n", " center=trajectories[biome][10].to_string(),\n", " radius=radius,\n", " gmap={'method': 'raytrace'},\n", " filters=(),\n", " resolution=1024,\n", " opacity=1.0,\n", ")\n", "\n", "face_radial_plane(tfig, 29)\n", "tfig.show()\n", "\n", "make_rotating_gif_parallel(tfig, f\"figures/mb_contrast_r{int(radius*100)}.gif\", n_frames=120, fps=30)" ] }, { "cell_type": "code", "execution_count": null, "id": "81d66b0f-441c-43f8-9886-16b9c3d76985", "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 }