Naming the 256 XTerm colors
You can download this article as a Jupyter Notebook.
Why don’t XTerm colors have names? X11 colors have names, as do web, SVG and
lipstick colors. There must be dozens of people worldwide who are reduced to
highlighting their text with xterm-76
, instead of fungal hallucination pink
and sizzling watermelon. Well, not anymore. I have named all the colors, and
made the list available as an rgb.txt file, as well as a color to
index lookup file.
If you know me, then you also know that the rest of this post is a winding article about colors, where I somehow find my way to explaining a data structure you don’t care about. But before we get to 3D nearest-neighbor search, a short overview of prior art.
Prior Art
There exists an attemp to map vim colors onto XTerm colors, reproduced e.g. in this list. The main problem with the result is that Vim doesn’t have enough colors to cover everything, and also it uses names like “Blue3”, which are boring.
A casual look around the internet reveals that people mostly use X11 colors (aka
rgb.txt
), SVG colors or Web Colors whenever they need to refer to terminal
colors, but they don’t match up very well, for two reasons:
- There aren’t enough of them. X11 only knows about 130 colors, SVG and Web colors similar numbers.
- The shades are off - xterm-256 has a very specific set of swatches that are unlikely to appear in color database with 8 bits per channel.
As far as I could find, nobody has bothered to make an XTerm rgb.txt
yet.
What are the XTerm colors
XTerm is terminal emulator that shipped
with the X Window System, which
you might know by its modern names X11 or X.org. It was one of the first
emulators to ship support for 256 colors, and to this day, most terminal
emulators behave like XTerm, and even declare themselves to be xterm-256color
.
What I will now generously start refering to as the XTerm Colorspace consists of 3 groups of color swatches:
- 16 ANSI colors
- 216 RGB colors
- 24 shades of grey
Let’s start by looking at the simplest part - the ANSI colors.
ANSI Colors
from typing import NamedTuple
class Color(NamedTuple):
"""An RGB color with an optional list of names."""
names: list[str]
channels: tuple[float, float, float]
ANSI_COLORS_RGB = (
(0x00, 0x00, 0x00),
(0x80, 0x00, 0x00),
(0x00, 0x80, 0x00),
(0x80, 0x80, 0x00),
(0x00, 0x00, 0x80),
(0x80, 0x00, 0x80),
(0x00, 0x80, 0x80),
(0xC0, 0xC0, 0xC0),
(0x80, 0x80, 0x80),
(0xFF, 0x00, 0x00),
(0x00, 0xFF, 0x00),
(0xFF, 0xFF, 0x00),
(0x00, 0x00, 0xFF),
(0xFF, 0x00, 0xFF),
(0x00, 0xFF, 0xFF),
(0xFF, 0xFF, 0xFF),
)
"""The standard 16 ANSI colors."""
ANSI_COLOR_NAMES = (
"Black",
"Red",
"Green",
"Yellow",
"Blue",
"Magenta",
"Cyan",
"White",
"Bright Black",
"Bright Red",
"Bright Green",
"Bright Yellow",
"Bright Blue",
"Bright Magenta",
"Bright Cyan",
"Bright White",
)
ANSI_COLORS = [
Color(
names=[f"ansi-{i}", name],
channels=tuple(c / 0xFF for c in ANSI_COLORS_RGB[i]), # type: ignore
)
for i, name in enumerate(ANSI_COLOR_NAMES)
]
These are the ANSI colors. There are eight of them, and each additonally has a “bright variant”. They have names, too, but the names are strange. Let’s view them as an HTML table.
from IPython import display
from typing import Iterable
def rgb_to_hex(rgb: tuple[float, float, float]) -> str:
"""Convert an RGB color to a hex string like ff0000 for red."""
return "".join([f"{int(ch*255):02x}" for ch in rgb])
def contrast_color(rgb: tuple[float, float, float]) -> tuple[float, float, float]:
"""Return a color that will be legible against the background color."""
r, g, b = rgb
if r * 0.9 + g * 1.3 + b * 0.7 > 0.9:
return (0, 0, 0)
else:
return (1, 1, 1)
def colors_html(colors: Iterable[Color]) -> Iterable[str]:
"""Generate an HTML table of colors."""
yield "<table style>"
for color in colors:
fg_color = rgb_to_hex(contrast_color(color.channels))
bg_color = rgb_to_hex(color.channels)
yield f'<tr style="margin:0; background:#{bg_color}; color:#{fg_color}">'
for name in color.names:
yield f'<td style=\"border:none\">{name}</td>'
yield f"<td style=\"border:none\">{bg_color}</td>"
yield "</tr>"
yield "</table>"
colors = (ANSI_COLORS[i] for i in range(16))
display.HTML("\n".join(colors_html(colors)))
ansi-0 | Black | 000000 |
ansi-1 | Red | 800000 |
ansi-2 | Green | 008000 |
ansi-3 | Yellow | 808000 |
ansi-4 | Blue | 000080 |
ansi-5 | Magenta | 800080 |
ansi-6 | Cyan | 008080 |
ansi-7 | White | c0c0c0 |
ansi-8 | Bright Black | 808080 |
ansi-9 | Bright Red | ff0000 |
ansi-10 | Bright Green | 00ff00 |
ansi-11 | Bright Yellow | ffff00 |
ansi-12 | Bright Blue | 0000ff |
ansi-13 | Bright Magenta | ff00ff |
ansi-14 | Bright Cyan | 00ffff |
ansi-15 | Bright White | ffffff |
You might notice that some of these colors are ridiculous. “Bright White” is a color most humans would just call “White”, and of course “Bright Black” is non-sensical. But at least they have names.
XTerm 256 colors and greyscale
The next thing on the list is the 216 RGB colors, followed by some shades of grey. Before we visualize them, a word about how all of that works.
Modern colors, are mostly 24-bit (8 bits per RGB channel, like the familiar
0xff00ff
), or 32-bit (adds an alpha channel). All this is sometimes called
True Color.
XTerm has 8-bit colors, which means that the color of any character on the
screen is given by a single byte with values from 0x00
to 0xff
. The astute
reader will notice that there is no way to divide 8 by 3 and end up happy,
unless 2-bit channels are the kind of thing that makes you happy.
Base-6 all day, baby
Some 256-color systems hand-pick their color shades and use a lookup table, but not XTerm. XTerm is old school UNIX stuff, and so XTerm uses base-6 channels. An RGB color with 6 values per channel gives 216 combinations, which lets us put legacy ANSI at the beginning for backwards compatibility, and still leaves room at the end for fancy shades of grey and stuff.
The base-6 channels are converted to decimal as you’d expect - the only gotcha is
that that 256 can’t be divided by 6, and also dark colors are pointless in
terminals with black background, and so the possible non-zero channel values are
all above 0x5f
.
Here’s some quick conversion code from XTerm index into RGB:
# The xterm-256 color space is sparse at low luminosity.
CHANNEL_STEPFUNC = (0, 0x5F, 0x87, 0xAF, 0xD7, 0xFF)
def xterm_color(color_index: int) -> Color:
"""Convert the xterm color (0-255) to an RGB equivalent."""
if color_index < 16:
return Color(
[f"{color_index}"],
(
ANSI_COLORS_RGB[color_index][0] / 0xFF,
ANSI_COLORS_RGB[color_index][1] / 0xFF,
ANSI_COLORS_RGB[color_index][2] / 0xFF,
),
)
if color_index >= 232:
# Greyscale
value = (0x8 + ((color_index - 232) * 0xA)) / 255
return Color([f"{color_index}"], (value, value, value))
red, r = divmod(color_index - 16, 6**2)
green, blue = divmod(r, 6)
return Color(
[f"{color_index}"],
(
CHANNEL_STEPFUNC[red] / 255,
CHANNEL_STEPFUNC[green] / 255,
CHANNEL_STEPFUNC[blue] / 255,
),
)
colors = [xterm_color(i) for i in range(0, 256)]
display.HTML("\n".join(colors_html(colors[30:45])))
30 | 008787 |
31 | 0087af |
32 | 0087d7 |
33 | 0087ff |
34 | 00af00 |
35 | 00af5f |
36 | 00af87 |
37 | 00afaf |
38 | 00afd7 |
39 | 00afff |
40 | 00d700 |
41 | 00d75f |
42 | 00d787 |
43 | 00d7af |
44 | 00d7d7 |
Here we’re looking at a slice of the XTerm colorspace. It looks kind of like nothing, but let’s try sorting it by hue.
import colorsys
colors.sort(key=lambda x: colorsys.rgb_to_hsv(*x.channels))
display.HTML("\n".join(colors_html(colors[45:60])))
1 | 800000 |
88 | 870000 |
124 | af0000 |
160 | d70000 |
9 | ff0000 |
196 | ff0000 |
209 | ff875f |
216 | ffaf87 |
173 | d7875f |
202 | ff5f00 |
166 | d75f00 |
223 | ffd7af |
180 | d7af87 |
137 | af875f |
215 | ffaf5f |
Much better. Now we’re looking at a few shades of red and orange, which are
looking quite attractive, if I say so myself. One small wart is that 9
and
196
are the same color. This is expected - some of the ANSI colors also appear
in the base-6 part of the colorspace.
While xterm-202
is a satisfying shade of of orange, its name doesn’t quite
roll of the tongue, or tell us anything about what color to expect.
So let’s get to naming them.
Naming XTerm Colors
My plan is to get a large color database, find shades similar to each XTerm color, and then pick one of the closest matches and use the name for the XTerm color.
First, let’s grab a color database. A cursory online search leads us to a Github
project by the user meodai
:
%pip install requests > /dev/null
import requests
import csv
import os
def get_medoai_colors() -> Iterable[Color]:
"""Get the color names from the meodai color names project."""
MEODAI_ALL_COLORS_URL = (
"https://github.com/meodai/color-names/raw/v10.19.0/dist/colornames.csv"
)
CACHE_PATH = ".cache_colornames.csv"
if not os.path.exists(CACHE_PATH):
response = requests.get(MEODAI_ALL_COLORS_URL)
response.raise_for_status()
response.encoding = "utf-8"
with open(CACHE_PATH, "w") as cache_file:
cache_file.write(response.text)
with open(CACHE_PATH, "r") as cache_file:
reader = csv.DictReader(cache_file)
for row in reader:
rgb = hex_to_rgb(row["hex"])
yield Color([row["name"]], rgb)
def hex_to_rgb(hex_color: str) -> tuple[float, float, float]:
"""Convert a hex color (e.g. #ff00ff) to an RGB equivalent."""
hex_color = hex_color.lstrip("#")
r = int(hex_color[0:2], 16) / 255
g = int(hex_color[2:4], 16) / 255
b = int(hex_color[4:6], 16) / 255
return (r, g, b)
medoai_colors = list(get_medoai_colors())
f"Database contains {len(medoai_colors)} colors"
'Database contains 30241 colors'
The RGB Color Space
The database contains a lot of colors. Let’s take a look at them. A good way to visualize RGB colors is by assinging each color channel to one of the 3D axes, like below.
First, we’ll need a library:
%pip install ipympl > /dev/null
%matplotlib widget
Now some simple drawing code. Each color in the database will be shown at the coordinates given by its RGB value.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from typing import cast
from matplotlib.figure import Figure
import random
def plot_color_dots(
ax: Axes3D,
rate: float,
labels: tuple[str, str, str],
colors: Iterable[tuple[tuple[float, float, float], Color]],
):
"""Plot a scatter plot of colors in 3D."""
ax.set_xlabel(labels[0])
ax.set_ylabel(labels[1])
ax.set_zlabel(labels[2])
x: list[float] = []
y: list[float] = []
z: list[float] = []
rgb: list[tuple[float, float, float]] = []
for (r, g, b), color in colors:
if random.random() > rate:
continue
x.append(r)
y.append(g)
z.append(b)
rgb.append(color.channels)
ax.scatter3D(x, y, z, c=rgb) # type: ignore
def color_space_plot() -> tuple[Figure, Axes3D]:
"""Create a 3D scatter plot of the color space."""
fig = plt.figure()
axes = cast(Axes3D, fig.add_subplot(projection="3d"))
axes.set_aspect("equal")
axes.view_init(30, 30, 0)
return (fig, axes)
fig, ax = color_space_plot()
plot_color_dots(ax, 1.0, ("Red", "Green", "Blue"), [(color.channels, color) for color in medoai_colors])
plt.show()
Nearest-neighbor color matching
For each XTerm color, we want to find its nearest neighbors in the database. This would mean checking each XTerm color’s 3D distance against every color in the database - in other words, the complexity is $O(N^2)$. With this database, that would mean $30,000 \times 216 \approx 6,500,000$ distance checks. Not too bad, on a modern computer, but not great if you want to reuse this database for live lookups (and I do).
If we only had greyscale colors, then finding the nearest match would be easy: just sort the array and do binary search. But we have three channels (R, G and B), so we have to do something a little smarter.
In video games, collision detection and raytracing have to solve a similar problem: checking ovelap between every pair of triangles in the scene would be $O(N^2)$, but by dividing space into smaller volumes, it’s possible to quickly find things are that nearby, for some definition of nearby.
The most common data-structure for dividing space into such volumes is the humble binary tree. Each node splits its bounding volume in two. The only trick is that each level does the splitting along a different axis. This type of tree has two names, depending on how we decide where to split the axis:
If the axis is always split down the middle, then we end up dividing the space into smaller and smaller half-cubes, and this is called a k-d tree.
Alternatively, we can split the axis so that each side of the subtree contains the same number of elements - the resulting volumes are smaller and uneven, along with some empty spaces, but each volume is guaranteed to contain something. Such a tree is called a bounding volume hierarchy, or BVH for short.
As it turns out, RGB colors also exist in 3D space, with similar-looking colors being near each other, so we can use this idea and build a BVH.
Bounding Volumes
The BV in BVH stands for “Bounding Volume”. This is basically just a simple 3D shape that’s exactly large enough to completely contain some other, more complicated 3D shape, like a point cloud. The simplest bounding volumes are spheres and non-rotated boxes, called Axis-Aligned Bounding Boxes, or AABB. We could actually split the color space into spheres, but an AABB is traditional, so let’s define one.
from typing import NamedTuple, Any, Iterable
class AABB(NamedTuple):
"""Axis-aligned bounding box."""
lo: tuple[float, float, float]
hi: tuple[float, float, float]
@classmethod
def from_point(cls, point: tuple[float, float, float]) -> "AABB":
"""A convenience constructor same as calling AABB(point, point)."""
return cls(point, point)
def overlaps(self, other: "AABB") -> bool:
"""Does this volume overlap with the other volume?"""
return (
self.hi[0] >= other.lo[0]
and self.lo[0] <= other.hi[0]
and self.hi[1] >= other.lo[1]
and self.lo[1] <= other.hi[1]
and self.hi[2] >= other.lo[2]
and self.lo[2] <= other.hi[2]
)
def encapsulate(self, other: "AABB") -> "AABB":
"""Return a new AABB that bounds both self and the other."""
return AABB(
lo=(
min(self.lo[0], other.lo[0]),
min(self.lo[1], other.lo[1]),
min(self.lo[2], other.lo[2]),
),
hi=(
max(self.hi[0], other.hi[0]),
max(self.hi[1], other.hi[1]),
max(self.hi[2], other.hi[2]),
),
)
def axis_center(self, axis: int) -> float:
"""The linear center of this box for the given axis."""
return (self.lo[axis] + self.hi[axis]) / 2
def bounding_volume(volumes: Iterable[AABB]) -> AABB:
"""Bring all the volumes and in the darkness bound them."""
volumes = iter(volumes)
result = next(volumes)
for volume in volumes:
result = result.encapsulate(volume)
return result
BVH
OK, we have the volume defined, now what about the hierarchy? As discussed, a BVH is a binary tree that divides a population of 3D volumes into half with each step. The algorithm to query a BVH is a little different a regular BST, because a volume might possibly overlap both left and right.
DIMENSIONS = 3
class BVH(NamedTuple):
"""Bounding volume hierarchy."""
bounds: AABB
value: Color|None
left: "BVH|None"
right: "BVH|None"
depth: int
def search(self, bounds: AABB) -> Iterable[tuple[AABB, Color]]:
"""Search for all values within the given bounds."""
if self.bounds.overlaps(bounds):
if self.value is not None:
yield (self.bounds, self.value)
if self.left is not None:
yield from self.left.search(bounds)
if self.right is not None:
yield from self.right.search(bounds)
Building a BVH
The algorithm to construct a BVH is almost identical to quicksort. Just like in quicksort, each level of recursion picks a pivot element, and then swaps elements until everything left of the pivot is smaller and everything right of the pivot greater than the pivot.
Just like with quicksort, we need to pick a partition function - the code below uses to often-taught Hoare Partition with a median-of-three algorithm for pivot selection.
def make_bvh(data: list[tuple[AABB, Color]]) -> BVH:
"""Create a BVH from a list of AABBs and values."""
if not data:
raise ValueError("Must provide at least one volume")
bounds = bounding_volume(aabb for aabb, _ in data)
res = _make_bvh(data, 0, len(data) - 1, bounds, 0)
assert res is not None
return res
def _make_bvh(
data: list[tuple[AABB, Any]], lo: int, hi: int, bounds: AABB, depth: int
) -> BVH | None:
n = hi - lo + 1
if n == 0:
return None
if n == 1:
return BVH(bounds=data[lo][0], value=data[lo][1], left=None, right=None, depth=depth)
if n == 2:
return BVH(
bounds=bounds,
value=None,
left=BVH(
bounds=data[lo][0],
value=data[lo][1],
left=None,
right=None,
depth=depth + 1,
),
right=BVH(
bounds=data[hi][0],
value=data[hi][1],
left=None,
right=None,
depth=depth + 1,
),
depth=depth,
)
# This part is basically quicksort, but with the partition using different
# axes based on depth.
axis = depth % DIMENSIONS
mid = _hoare_partition(data, lo, hi, axis)
left_bounds = bounding_volume(aabb for aabb, _ in data[lo:mid])
right_bounds = bounding_volume(aabb for aabb, _ in data[mid : hi + 1])
return BVH(
bounds=bounds,
value=None,
left=_make_bvh(data, lo, mid - 1, left_bounds, depth + 1),
right=_make_bvh(data, mid, hi, right_bounds, depth + 1),
depth=depth,
)
def _hoare_partition(a: list[tuple[AABB, BVH]], lo: int, hi: int, axis: int) -> int:
pivot = a[_median_of_three(a, lo, hi, axis)][0].axis_center(axis)
i = lo - 1
j = hi + 1
while True:
i += 1
while a[i][0].axis_center(axis) < pivot:
i += 1
j -= 1
while a[j][0].axis_center(axis) > pivot:
j -= 1
if i >= j:
return j
a[i], a[j] = a[j], a[i]
def _median_of_three(a: list[tuple[AABB, BVH]], lo: int, hi: int, axis: int) -> int:
"""Median of three pivot selection adapted from quicksort."""
mid = (lo + hi) // 2
if a[mid][0].axis_center(axis) < a[lo][0].axis_center(axis):
a[lo], a[mid] = a[mid], a[lo]
if a[hi][0].axis_center(axis) < a[lo][0].axis_center(axis):
a[lo], a[hi] = a[hi], a[lo]
if a[mid][0].axis_center(axis) < a[hi][0].axis_center(axis):
a[mid], a[hi] = a[hi], a[mid]
return mid
We have a BVH - let’s test it by looking for some colors similar to #FF8000, and see what we find.
color_db = make_bvh([(AABB.from_point(color.channels), color) for color in medoai_colors])
list(color_db.search(AABB((0.9, 0.5, 0.0), (1.0, 0.55, 0.0))))
[(AABB(lo=(0.9019607843137255, 0.5411764705882353, 0.0), hi=(0.9019607843137255, 0.5411764705882353, 0.0)),
Color(names=['Hotter Butter'], channels=(0.9019607843137255, 0.5411764705882353, 0.0))),
(AABB(lo=(0.9333333333333333, 0.5333333333333333, 0.0), hi=(0.9333333333333333, 0.5333333333333333, 0.0)),
Color(names=['Clear Orange'], channels=(0.9333333333333333, 0.5333333333333333, 0.0))),
(AABB(lo=(0.9411764705882353, 0.5137254901960784, 0.0), hi=(0.9411764705882353, 0.5137254901960784, 0.0)),
Color(names=['Mikan Orange'], channels=(0.9411764705882353, 0.5137254901960784, 0.0))),
(AABB(lo=(0.9490196078431372, 0.5215686274509804, 0.0), hi=(0.9490196078431372, 0.5215686274509804, 0.0)),
Color(names=['Tangerine Skin'], channels=(0.9490196078431372, 0.5215686274509804, 0.0))),
(AABB(lo=(1.0, 0.5450980392156862, 0.0), hi=(1.0, 0.5450980392156862, 0.0)),
Color(names=['American Orange'], channels=(1.0, 0.5450980392156862, 0.0))),
(AABB(lo=(1.0, 0.5490196078431373, 0.0), hi=(1.0, 0.5490196078431373, 0.0)),
Color(names=['Sun Crete'], channels=(1.0, 0.5490196078431373, 0.0))),
(AABB(lo=(1.0, 0.5333333333333333, 0.0), hi=(1.0, 0.5333333333333333, 0.0)),
Color(names=['Mandarin Jelly'], channels=(1.0, 0.5333333333333333, 0.0))),
(AABB(lo=(1.0, 0.5176470588235295, 0.0), hi=(1.0, 0.5176470588235295, 0.0)),
Color(names=['The New Black'], channels=(1.0, 0.5176470588235295, 0.0)))]
OK, that looks about right, we have a few shades of orange. To understand how the BVH works, though, it’d be helpful to visualize the recursive search in 3D, like we did with the colors themselves. First, let’s slightly modify the search function to generate a trace.
def trace_bvh_search(bvh, bounds: AABB) -> Iterable[BVH]:
"""Search for all values within the given bounds, yielding a full trace,
including nodes without values."""
if bvh.bounds.overlaps(bounds):
yield bvh
if bvh.left is not None:
yield from trace_bvh_search(bvh.left, bounds)
if bvh.right is not None:
yield from trace_bvh_search(bvh.right, bounds)
Now we need a way to draw a 3D box. GPUs can only draw triangles, from what’s called a mesh. A mesh is commonly given as two arrays:
- An array of vertices - points in 3D space
- An array of triangles - triples of vertex numbers that form a triangle
Our 3D library does us a solid (that’s a graphics pun) and lets us specify arbitrary polygons, like rectangles, and it splits those into triangles for us. So what we need to provide is a list of vertices, and their connections.
Any 3D “box” is always going to have the same connections between its vertices, so that part is simple:
BOX_SIDES = [
(0, 1, 2, 3),
(0, 4, 5, 1),
(1, 5, 6, 2),
(2, 6, 7, 3),
(3, 7, 4, 0),
(4, 5, 6, 7),
]
"""The indices of the corners of each face of a box."""
'The indices of the corners of each face of a box.'
We can get vertices from the AABB
volume we declared earlier, like so:
import numpy
from typing import Collection
def aabb_vertices(aabb: AABB) -> Collection[tuple[float, float, float]]:
return numpy.array(
[
aabb.lo,
(aabb.hi[0], aabb.lo[1], aabb.lo[2]),
(aabb.hi[0], aabb.hi[1], aabb.lo[2]),
(aabb.lo[0], aabb.hi[1], aabb.lo[2]),
(aabb.lo[0], aabb.lo[1], aabb.hi[2]),
(aabb.hi[0], aabb.lo[1], aabb.hi[2]),
aabb.hi,
(aabb.lo[0], aabb.hi[1], aabb.hi[2]),
]
)
def aabb_mesh(vertices) -> Any:
"""Return the vertices of the box as a list of faces, in the format that
pyplot expects."""
return [[vertices[i] for i in side] for side in BOX_SIDES]
Now we just need some unremarkable drawing code, lifted straight from the documentation.
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
def plot_aabb(axes: Any, aabb: AABB, color: str = "#0000ff", alpha: float = 0.05):
v = aabb_vertices(aabb)
axes.add_collection3d(
Poly3DCollection(
aabb_mesh(v),
facecolors=color,
linewidths=0.1,
edgecolors="k",
alpha=alpha,
zsort="max",
)
)
def plot_bvh_trace(axes: Any, bvh: BVH, bounds: AABB) -> None:
for node in trace_bvh_search(bvh, bounds):
if node.value is None:
plot_aabb(axes, node.bounds, color="#0000ff", alpha=0.0001)
else:
plot_aabb(
axes, node.bounds, color=f"#{rgb_to_hex(node.value.channels)}", alpha=0.0001
)
fig, axes = color_space_plot()
plot_bvh_trace(axes, color_db, AABB((0.9, 0.5, 0.0), (1.0, 0.55, 0.0)))
plt.show()
And there you go - a BVH search for a few shades of orange. Each recursion level is smaller, and most of the tree didn’t need to be checked.
We can use this same approach to find colors similar to other colors.
Finding Similar Shades
MAX_MATCHES = 5
for color in colors:
r, g, b = color.channels
max_dist = 0.15
near_matches = list(
color_db.search(
AABB(
lo=(r - max_dist, g - max_dist, b - max_dist),
hi=(r + max_dist, g + max_dist, b + max_dist),
)
)
)
# Sort the matches by square distance
dist = lambda x: sum((a - b) ** 2 for a, b in zip(x[0].lo, (r, g, b)))
near_matches.sort(key=dist)
for _, color2 in near_matches[:MAX_MATCHES]:
color.names.extend(color2.names)
display.HTML("\n".join(colors_html(colors)))
0 | Black | Vantablack | Registration Black | Black Hole | Armor Wash | 000000 |
16 | Black | Vantablack | Registration Black | Black Hole | Armor Wash | 000000 |
232 | Reversed Grey | Accursed Black | Badab Black Wash | Black Metal | Existential Angst | 080808 |
233 | Dark Tone Ink | Sticky Black Tarmac | Dreamless Sleep | Cursed Black | Glimpse into Space | 121212 |
234 | Eerie Black | Coco’s Black | Gluon Grey | Siyâh Black | Dynamic Black | 1c1c1c |
235 | Nero | Bitter Liquorice | Dire Wolf | Bokara Grey | Darth Vader | 262626 |
236 | Off Black | Tricorn Black | Coated | Tap Shoe | Black Cat | 303030 |
237 | Dead Pixel | Black Liquorice | Boltgun Metal | Montana | Limousine Leather | 3a3a3a |
238 | Goshawk Grey | Medium Black | Greenish Black | Machine Gun Metal | Vulcanized | 444444 |
239 | Black Oak | Charadon Granite | Perle Noir | Thunder | Fiftieth Shade of Grey | 4e4e4e |
240 | Shadow Mountain | Charcoal Dust | Sumi Ink | Carbon Dating | Industrial Grey | 585858 |
59 | Rhine Castle | Shades On | Charcoal Smudge | Iron | Hematite | 5f5f5f |
241 | Kettleman | Grizzle Grey | Digital | Roycroft Pewter | Tornado Wind | 626262 |
242 | Dove Grey | Scapa Flow | Boat Anchor | Shadows | Dark Ash | 6c6c6c |
243 | Lucky Grey | Sonic Silver | Steel Wool | Iron Mountain | Riverstone | 767676 |
8 | Grey | Pound Sterling | Mt. Rushmore | Trolley Grey | Captain Nemo | 808080 |
244 | Grey | Pound Sterling | Mt. Rushmore | Trolley Grey | Captain Nemo | 808080 |
102 | Mithril | Lunar Base | Looking Glass | Argent | Jumbo | 878787 |
245 | Wild Dove | Argent | Falcon Grey | Heavy Rain | Pewter Ring | 8a8a8a |
246 | Grey Shingle | Moonlit Orchid | Grey Summit | Nickel | Shark Fin | 949494 |
247 | Mortar Grey | Smoky Tone | Waiting | Cold Grey | Hugh’s Hue | 9e9e9e |
248 | Uniform Grey | Nosferatu | Elephant in the Room | Ultimate Grey | Moon Landing | a8a8a8 |
145 | Smoke Screen | Aluminum Sky | Bombay | Industrial Age | Tin Foil | afafaf |
249 | Tangled Web | Palladium | Silverstone | Harbour Fog | Praise Giving | b2b2b2 |
250 | Dust to Dust | Glacier Grey | Gravel Fint | Alaskan Grey | Anti Rainbow Grey | bcbcbc |
7 | Silver | Stonewall Grey | Waxwing | Silver Tipped Sage | Neo Tokyo Grey | c0c0c0 |
251 | Silver Polish | Autonomous | Dreamscape Grey | Paternoster | Lunar Rock | c6c6c6 |
252 | Ancestral Water | Cool Elegance | Clouded Vision | American Silver | White Metal | d0d0d0 |
188 | Cape Hope | Silver Medal | Tundra | Windchill | Desired Dawn | d7d7d7 |
253 | Porpoise | Subtle Touch | Urban Snowfall | Orochimaru | Silver Setting | dadada |
254 | Titanium White | Windswept Beach | Tripoli White | Grey Whisper | Cold Morning | e4e4e4 |
255 | Super Silver | White Whale | Essence of Violet | Crystal Bell | White Edgar | eeeeee |
15 | White | White as Heaven | Whitecap Snow | Pale Grey | Polar Bear In A Blizzard | ffffff |
231 | White | White as Heaven | Whitecap Snow | Pale Grey | Polar Bear In A Blizzard | ffffff |
224 | We Peep | Forgotten Pink | Cottagecore Sunset | Go Go Pink | Satin Ribbon | ffd7d7 |
181 | Mary Rose | Radiant Rouge | Victoriana | Pale Persimmon | Ballet Rose | d7afaf |
138 | Woodrose | Orchid Red | Warm Comfort | Audrey’s Blush | Retro Pink | af8787 |
95 | Rabbit Paws | Aged Beech | Tarsier | Forbidden Thrill | Rose Garland | 875f5f |
217 | Fancy Flamingo | Cornflower Lilac | Wildflower Bouquet | Peach Bud | Apricot Haze | ffafaf |
174 | Peaches of Immortality | Copperfield | Rhubarb Pie | Finest Blush | Mauve Glow | d78787 |
131 | Italian Villa | Poppy Prose | Sienna Red | Cinnamon Candle | Spiced Tea | af5f5f |
210 | Red Mull | Coral Trails | Tulip | Prime Pink | Camaron Pink | ff8787 |
167 | Roman | Happy Hearts | Salami Slice | Deep Sea Coral | Tory Red | d75f5f |
203 | Fusion Red | Pineapple Salmon | Pompelmo | Pastel Red | Grapefruit | ff5f5f |
52 | Spikey Red | Red Blood | Vampire Hunter | Soooo Bloody | Khorne Red | 5f0000 |
1 | Maroon | Salami | Dark Red | Sacrifice Altar | Chanticleer | 800000 |
88 | Chanticleer | Glass Bull | Sacrifice Altar | Dark Red | Scab Red | 870000 |
124 | Red Door | Velvet Volcano | Heartbeat | Artful Red | Red Pentacle | af0000 |
160 | Red Republic | Rosso Corsa | Hot Fever | Red Pegasus | Red Epiphyllum | d70000 |
9 | Red | Rainbow’s Outer Rim | Fire Engine | Encarnado | Left on Red | ff0000 |
196 | Red | Rainbow’s Outer Rim | Fire Engine | Encarnado | Left on Red | ff0000 |
209 | After Burn | Pink Fire | Protein High | Mango Orange | Nectarine | ff875f |
216 | Spice Pink | Sunset over the Alps | Super Sepia | Coral Correlation | Coral Dusk | ffaf87 |
173 | Bright Sienna | Copper Tan | Harvest Time | Georgian Leather | Show Business | d7875f |
202 | Vivid Orange | Maximum Orange | Molten Core | Safety Orange | Willpower Orange | ff5f00 |
166 | Exuberance | Ancient Bamboo | Orange Danger | Raging Leaf | Tenné | d75f00 |
223 | Delicious Melon | Acini di Pepe | Venus Deathtrap | Forgotten Sunset | Satin Latour | ffd7af |
180 | Porcini | Santa Fe Tan | Calico | Caramel Cloud | Buttery Leather | d7af87 |
137 | Clay Ochre | Roman Coin | Light Oak Brown | Caramel Kiss | Sacred Ground | af875f |
215 | Vintage Orange | Dreamy Sunset | Burning Flame | Rajah | Mango Salsa | ffaf5f |
208 | Mandarin Jelly | The New Black | American Orange | Sun Crete | Orange Juice | ff8700 |
130 | Orange Brown | Orangish Brown | Umber | Ginger | Butter Fudge | af5f00 |
172 | Fleur de Sel Caramel | Fox Tails | Harvest Eve Gold | Orange Pepper | Fulvous | d78700 |
222 | Workout Routine | Surfboard Yellow | Big Bus Yellow | Oberon | Egg Cream | ffd787 |
179 | Sell Gold | Butterscotch Bliss | Equator | Stranglethorn Ochre | French Pale Gold | d7af5f |
214 | Fresh Squeezed | Clementine Jelly | Frenzy | Yellow Exhilaration | Imperial Yellow | ffaf00 |
94 | Rat Brown | Soil Of Avagddu | Alligator | Ground Earth | Hè Sè Brown | 875f00 |
221 | Common Dandelion | Lemon Twist | Aspen Gold | Naples Yellow | Yellow Stagshorn | ffd75f |
136 | Strong Mustard | Chestnut Gold | Mustard Brown | Bark Sawdust | Cobra Leather | af8700 |
178 | Palomino Gold | Deadly Yellow | Chinese Gold | Mustard | Burnt Yellow | d7af00 |
220 | Gold | School Bus | Soviet Gold | Cyber Yellow | Evil-Lyn | ffd700 |
230 | Poetic Yellow | Lit | Sun City | Pumpkin Seed | Matt White | ffffd7 |
187 | Green Mesh | Morning Moor | Kohlrabi Green | Mǐ Bái Beige | Dull Sage | d7d7af |
144 | Daddy-O | Lively Ivy | Wall Green | Underhive Ash | New Bamboo | afaf87 |
101 | Tilleul de Noémie | Green Savage | Green Scene | Spinach Souffle | Bandicoot | 87875f |
229 | VIC 20 Creme | Parchment | Creamy Sunshine Pastel | Ginger Lemon Tea | Bollywood Gold | ffffaf |
186 | Wax Green | Treasury | Lime Ice | Garlic Toast | Golden Delicious | d7d787 |
143 | Palm | Palm Frond | Green Me | April Green | Hemp Tea | afaf5f |
228 | Cinque Foil | Yippie Yellow | Aged Plastic Casing | Butter | Yellowish Tan | ffff87 |
185 | Banana Chalk | Chinese Green | Energized | Species | Sequesta | d7d75f |
227 | Canary | Candy Corn | Duckling Fluff | Laser Lemon | Unmellow Yellow | ffff5f |
58 | Mud Green | Earthy Khaki Green | Green Brown | Ayahuasca Vine | Serrano Pepper | 5f5f00 |
3 | Heart Gold | Drably Olive | Verde Tropa | Mongolian Plateau | Swamp Green | 808000 |
100 | Drably Olive | Heart Gold | Old Asparagus | Krypton Green | Moscow Papyrus | 878700 |
142 | Honey and Thyme | Mustard Green | Dark Citron | Yew | Sulphine Yellow | afaf00 |
184 | Chartreuse Shot | Golden Gun | March Green | Mogwa-Cheong Yellow | Octarine | d7d700 |
11 | Yellow | Bat-Signal | Yell for Yellow | Lemon Glacier | Bright Yellow | ffff00 |
226 | Yellow | Bat-Signal | Yell for Yellow | Lemon Glacier | Bright Yellow | ffff00 |
190 | Lime Zest | Citron Goby | Bitter Lime | Dancing-Lady Orchid | Neon Yellow | d7ff00 |
148 | King Lime | Slimer Green | Immaculate Iguana | Vivid Lime Green | High Grass | afd700 |
106 | Fresh Lawn | Dark Lime | Brilliant Green | Seasoned Apple Green | Grasping Grass | 87af00 |
191 | Isotonic Water | Pear Spritz | Green of Bhabua | Ultra Moss | Tennis Ball | d7ff5f |
64 | Pesto Alla Genovese | Avocado | Olive Green | Topiary Green | Pistachio Flour | 5f8700 |
154 | Lime Acid | Lime Candy Pearl | Lemon Green | Wolf Lichen | Spring Bud | afff00 |
192 | Green Shimmer | Honeydew Peel | Mystic Green | Green Incandescence | Sunny Lime | d7ff87 |
149 | Lime Lizard | Juicy Lime | Last of Lettuce | Citrus Leaf | Badass Grass | afd75f |
112 | Overgrown | Electric Leaf | Alien Armpit | Sheen Green | Green Cape | 87d700 |
70 | Kermit Green | Leaf Green | Appetizing Asparagus | Yoshi | Emerald Glitter | 5faf00 |
118 | Lasting Lime | Bright Lime | Radioactive | Radium | Mochito | 87ff00 |
193 | Lime Mist | Sour Green Cherry | Greedy Green | Breeze of Green | Distilled Venom | d7ffaf |
150 | Fresh Lettuce | Peas In A Pod | Wasabi | Pastel Lime | Feijoa | afd787 |
107 | Broccoli | Chelsea Cucumber | Jealousy | Celuce | Nasturtium Leaf | 87af5f |
155 | Pale Lime Green | Irradiated Green | Luminescent Lime | Stinging Wasabi | Key Lime | afff5f |
76 | Radioactive Lilypad | Tropical Funk | Corrosive Green | Blinking Terminal | Composite Artefact Green | 5fd700 |
82 | Bright Green | Fertility Green | Hyper Green | Bright Lime Green | Sparkling Green | 5fff00 |
156 | Pistachio Mousse | Pale Light Green | Light Yellowish Green | Green Day | Green Incandescence | afff87 |
113 | Lilliputian Lime | Bright Lettuce | Vivid Spring | Amazon Parrot | Fairy Tale Green | 87d75f |
119 | Poisonous Dart | Lighter Green | Stadium Lawn | Amazon Parrot | Astro Arcade Green | 87ff5f |
194 | Transparent Green | Aquarelle Mint | Frosted Plains | Light Carolina | Mint Zest | d7ffd7 |
151 | Flower Stem | Fizz | Big Spender | Coral Springs | Pastel Mint Green | afd7af |
108 | Chatty Cricket | Shaded Willow | Peapod | Meadow | Mermaid’s Cove | 87af87 |
65 | Hippie Green | Tuscan Herbs | Soylent Green | Clouded Pine | Spring Garden | 5f875f |
157 | Creamy Mint | Light Seafoam Green | Light Mint Green | Light Pastel Green | Light Mint | afffaf |
114 | Greek Garden | De York | Electric Lettuce | Feralas Lime | VIC 20 Green | 87d787 |
71 | Boring Green | Chlorella Green | Endo | Exploration Green | Techno Green | 5faf5f |
120 | Easter Green | Cobalt Green | Radar Blip Green | Light Green | Ulva Lactuca Green | 87ff87 |
77 | Lightish Green | Koopa Green Shell | Scorpion Green | Fresh Green | Loud Green | 5fd75f |
83 | Screamin’ Green | Biopunk | Green Katamari | Puyo Blob Green | Goblin Warboss | 5fff5f |
22 | Cucumber | Duck Hunt | Pakistan Green | Forest Ride | Emerald Green | 005f00 |
2 | Hulk | Lucky Clover | Green Hills | Fine Pine | Moth Green | 008000 |
28 | Fine Pine | Lucky Clover | Hulk | Clover | Maniac Green | 008700 |
34 | Phosphor Green | Aquamentus Green | Bubble Bobble Green | Green Glimmer | Waystone Green | 00af00 |
40 | Greenalicious | Nuclear Throne | Tunic Green | Demeter Green | Vibrant Green | 00d700 |
10 | Green | Venom Dart | Electric Pickle | Spring | EGA Green | 00ff00 |
46 | Green | Venom Dart | Electric Pickle | Spring | EGA Green | 00ff00 |
84 | Thallium Flame | Grotesque Green | Flora | Light Green | Goblin Green | 5fff87 |
121 | Esper’s Fungus Green | Green Epiphany | Foam Green | Mild Menthol | Mint Bliss | 87ffaf |
78 | Spring Bouquet | Van Gogh Green | Grotesque Green | Vegetation | Snow Pea | 5fd787 |
47 | Cathode Green | Booger Buster | Mike Wazowski Green | Spring Green | Guppie Green | 00ff5f |
41 | Alienated | Malachite | Limonana | Benzol Green | Green Priestess | 00d75f |
158 | Mintastic | Pale Turquoise | Seafair Green | Icery | Neo Mint | afffd7 |
115 | Jovial Jade | Tropical Trail | Pharaoh’s Jade | Marooned | Aquamarine Ocean | 87d7af |
72 | Verdigris Green | Green Tourmaline | Jade Cream | Sea Grass | Crazy Eyes | 5faf87 |
85 | Ineffable Green | Hanuman Green | Venice Green | Illicit Green | Sea Green | 5fffaf |
48 | Guppie Green | Spring Green | Turquoise Green | Ahaetulla Prasina | Booger Buster | 00ff87 |
35 | Go Green! | Greedo Green | Rita Repulsa | Alhambra Green | Jungle | 00af5f |
42 | Underwater Fern | Benzol Green | Caribbean Green | Cōng Lǜ Green | Aqua Green | 00d787 |
122 | Tibetan Plateau | Roller Derby | Hiroshima Aquamarine | A State of Mint | Calamine BLue | 87ffd7 |
79 | Medium Aquamarine | Sweet Garden | Tropical Tide | Aquarium Blue | Jamaican Jade | 5fd7af |
49 | Greenish Turquoise | Enthusiasm | Yíng Guāng Sè Green | Minty Paradise | Night Pearl | 00ffaf |
29 | Absinthe Turquoise | Chagall Green | Spectral Green | Spanish Viridian | Bosphorus | 00875f |
86 | Rare Wind | Move Mint | Spindrift | Icy Life | Near Moon | 5fffd7 |
36 | Arcadia | Simply Green | Hobgoblin | Chromophobia Green | Moray Eel | 00af87 |
43 | Channel Marker Green | Lifeless Green | Mint Leaf | Pristine Oceanic | Malted Mint Madness | 00d7af |
50 | Ice Ice Baby | Plunge Pool | Vibrant Mint | Frozen Boubble | Bright Teal | 00ffd7 |
195 | Refreshing Primer | Salt Mountain | Mount Olympus | Cold Canada | Ice | d7ffff |
152 | Rivers Edge | Light Continental Waters | Sugar Pool | Light Imagine | Wave Top | afd7d7 |
109 | Hydrology | Bon Voyage | Cold Front Green | Shallow Water Ground | Jitterbug Lure | 87afaf |
66 | Steel Teal | Arctic | Cretan Green | Tasmanian Sea | Pond Sedge | 5f8787 |
159 | Celeste | Italian Sky Blue | Frostbite | Affen Turquoise | Winter Meadow | afffff |
116 | Island Oasis | Mountain Lake Blue | Vibrant Soft Blue | Rainwater | VIC 20 Sky | 87d7d7 |
73 | Aquarelle | Artesian Well | Fountain Blue | Experience | Timid Sea | 5fafaf |
123 | Glitter Shower | Electric Blue | Shallow Water | Photon Projector | Defense Matrix | 87ffff |
80 | Hammam Blue | Pluviophile | Blue Radiance | Watercourse | Jazzy Jade | 5fd7d7 |
87 | Moonglade Water | CGA Blue | Electric Sheep | Frozen Turquoise | Aggressive Baby Blue | 5fffff |
23 | Sandhill Crane | Mosque | Emerald Stone | Enamelled Jewel | Tidal Pool | 005f5f |
6 | Teal | Belly Flop | Pond Bath | Windows 95 Desktop | Macquarie | 008080 |
30 | Green Moblin | Green Lapis | Navigate | Well Blue | Milky Aquamarine | 008787 |
37 | Fiji | Jade Orchid | Bluebird | Cyan Sky | Garish Blue | 00afaf |
44 | Jade Glass | Aztec Turquoise | Mint Morning | Tilla Kari Mosque | First Timer Green | 00d7d7 |
14 | Aqua | Spanish Sky Blue | Fluorescent Turquoise | Agressive Aqua | Arctic Water | 00ffff |
51 | Aqua | Spanish Sky Blue | Fluorescent Turquoise | Agressive Aqua | Arctic Water | 00ffff |
45 | Neon Blue | Tropical Turquoise | Vivid Sky Blue | Bright Sky Blue | Whimsical Blue | 00d7ff |
38 | Malibu Blue | Blue Fire | Blue Atoll | Maldives | Vanadyl Blue | 00afd7 |
31 | Stomy Shower | Lyrebird | Corfu Waters | Tiny Bubbles | Tusche Blue | 0087af |
81 | Athena Blue | Ionized-air Glow | Skyan | Electric Lemonade | Heisenberg Blue | 5fd7ff |
24 | Blue Flame | Impulse | Ink Blotch | Blue League | Blue Heist | 005f87 |
39 | Krishna Blue | Protoss Pylon | Blue Bolt | Hawaii Morning | Democrat | 00afff |
117 | Tranquil Pool | Kul Sharif Blue | Drift on the Sea | Platonic Blue | Clear Sky | 87d7ff |
74 | Flyway | Disembark | Shimmering Brook | Riviera Blue | Crystal Seas | 5fafd7 |
32 | Blue Cola | Calgar Blue | Electron Blue | Kahu Blue | Lvivian Rain | 0087d7 |
25 | Cobalt Stone | Wing Commander | Directoire Blue | Peptalk | Bottled Sea | 005faf |
33 | Too Blue to be True | Bubble Bobble P2 | Brescian Blue | Azure | Starfleet Blue | 0087ff |
153 | Ice Cold Stare | Droplet | Malmö FF | Endless Horizon | Night Snow | afd7ff |
110 | Blue Bell | Birdie Num Num | Buoyant Blue | Boy Blue | Reform | 87afd7 |
67 | Pacific Coast | Shrinking Violet | Lichen Blue | Perfect Periwinkle | Sand Shark | 5f87af |
75 | Tiān Lán Sky | Âbi Blue | Joust Blue | Hello Summer | Blue Jeans | 5fafff |
26 | Blue Ruin | Frosted Blueberries | Royal Navy Blue | Dead Blue Eyes | Pacific Bridge | 005fd7 |
27 | Bright Blue | Blue Ribbon | Megaman Helmet | Nīlā Blue | Blue et une Nuit | 005fff |
111 | Kitten’s Eye | Carolina Blue | Parakeet Blue | Scenic Water | Fly Away | 87afff |
68 | Blue Jay | Livid | Berlin Blue | Little Boy Blue | Marina | 5f87d7 |
69 | Deep Denim | Punch Out Glove | Skinny Jeans | Flickery C64 | C64 NTSC | 5f87ff |
189 | Transparent Blue | Pale Lavender | Icy Plains | Nostalgia Perfume | Contrail | d7d7ff |
146 | Pixie Violet | Freesia Purple | Lavender Wash | Delicate Lilac | High Style | afafd7 |
103 | Aster Purple | Non Skid Grey | Persian Violet | Papilio Argeotus | Skysail Blue | 8787af |
60 | Purple Balloon | Pharaoh Purple | Majestic Purple | Idol | Champion Blue | 5f5f87 |
147 | Winterspring Lilac | Shy Moment | Greyish Lavender | Purple Illusion | Dried Lilac | afafff |
104 | Tanzine | Bailey Bells | Mystic Iris | Adora | Mood Mode | 8787d7 |
61 | Bellflower | Yuè Guāng Lán Moonlight | Blue Iris | Evening Lagoon | Blue Marguerite | 5f5faf |
105 | Lavender Blue Shadow | Periwinkle | Blue Party Parrot | Orchid | Periwinkle Blue | 8787ff |
62 | Exodus Fruit | Dark Periwinkle | Thick Blue | Ameixa | Majorelle Blue | 5f5fd7 |
63 | Blue Genie | Blue Heath Butterfly | Shady Neon Blue | Blue Hepatica | Flickering Sea | 5f5fff |
17 | Abyssal Blue | Alone in the Dark | D. Darx Blue | Alucard’s Night | Prussian Nights | 00005f |
4 | Navy Blue | Midnight in Tokyo | Scotch Blue | Yves Klein Blue | Deep Blue | 000080 |
18 | Midnight in Tokyo | Yves Klein Blue | Navy Blue | Phthalo Blue | Scotch Blue | 000087 |
19 | Bohemian Blue | Traditional Royal Blue | Antarctic Circle | Keese Blue | Cobalt | 0000af |
20 | Bluealicious | Medium Blue | Lady of the Sea | Pure Blue | Nightfall in Suburbia | 0000d7 |
12 | Blue | Graphical 80’s Sky | Star of David | Primary Blue | Strong Blue | 0000ff |
21 | Blue | Graphical 80’s Sky | Star of David | Primary Blue | Strong Blue | 0000ff |
99 | Purple Anemone | Purple Honeycreeper | Blackthorn Berry | Venetian Nights | Irrigo Purple | 875fff |
141 | Lilac Geode | Purple Illusionist | Liliac | Illicit Purple | Queer Purple | af87ff |
98 | Gloomy Purple | Matt Purple | Iridescent Purple | Amethyst | Legendary Lavender | 875fd7 |
57 | Electric Indigo | Aladdin’s Feather | Bright Indigo | Wèi Lán Azure | Tezcatlipōca Blue | 5f00ff |
56 | Trusted Purple | Gonzo Violet | Space Opera | Violet Blue | Sea Serpent’s Tears | 5f00d7 |
183 | Light Violet | Testosterose | Mauve | Teasel Dipsacus | Pink Illusion | d7afff |
140 | Middy’s Purple | Lavender Blossom | Pale Purple | Lenurple | Fleur-De-Lis | af87d7 |
97 | Lusty Lavender | Chive Blossom | Genestealer Purple | Lavish Spending | Knight Elf | 875faf |
135 | Purple Hedonist | Vega Violet | Lighter Purple | Queer Purple | Light Shōtoku Purple | af5fff |
93 | Purple Climax | Amethyst Ganzstar | Violent Violet | Poison Purple | Star Platinum Purple | 8700ff |
55 | Aubergine Perl | Elegant Midnight | Indiviolet Sunset | Purplue | Indigo Purple | 5f00af |
92 | French Violet | Vibrant Violet | Violet Ink | Violet Poison | Voluptuous Violet | 8700d7 |
177 | Lavender Tea | Crash Pink | Grass Pink Orchid | Pink Fetish | Bright Lilac | d787ff |
134 | Teldrassil Purple | Rich Lilac | Medium Orchid | Rich Lavender | Ripe Lavander | af5fd7 |
129 | Poison Purple Paradise | Digital Violets | Bright Violet | The Grape War of 97’ | Spectacular Purple | af00ff |
54 | Peaceful Purple | SQL Injection Purple | Zeus Purple | Extraviolet | Pigment Indigo | 5f0087 |
171 | Flaming Flamingo | After-Party Pink | Pink Fever | Jacaranda Pink | Heliotrope | d75fff |
91 | Shade of Violet | Violet Poison | Shiffurple | Purple Feather Boa | French Violet | 8700af |
128 | Vibrant Purple | Ferocious Fuchsia | Capricious Purple | Vivid Mulberry | Foxy Fuchsia | af00d7 |
165 | Psychedelic Purple | Phlox | Electric Orchid | Vivid Orchid | Hot Purple | d700ff |
225 | Sugarpills | Pink Diamond | Strawberry Frost | Sugar Chic | Silky Pink | ffd7ff |
182 | Whisper of Plum | Bff | Confectionary | Lilac Haze | Sea Lavender | d7afd7 |
139 | Dusty Lavender | Stage Mauve | Voila! | African Violet | Fashionably Plum | af87af |
96 | Candy Violet | Dusty Purple | Ancient Murasaki Purple | Crushed Grape | Orchid Orchestra | 875f87 |
219 | Jigglypuff | Strawberry Buttercream | Distilled Rose | Sweet Slumber Pink | Pink Apotheosis | ffafff |
176 | Lavender Pink | Blush Essence | Purception | Lavender Perceptions | Hibiscus Pop | d787d7 |
133 | Royalty Loyalty | Orchid Dottyback | Mazzy Star | Iris Orchid | Pearly Purple | af5faf |
213 | Darling Bud | Bubble Gum | Hottest Of Pinks | Technolust | Atomic Pink | ff87ff |
170 | Free Speech Magenta | Blueberry Glaze | Thick Pink | Fuchsia Flash | Death of a Star | d75fd7 |
207 | Violet Pink | Pink Flamingo | Magenta Stream | Ultimate Pink | Surati Pink | ff5fff |
53 | Clear Plum | Purple Dreamer | Divine Purple | Amygdala Purple | God of Nights | 5f005f |
5 | Purple | Philippine Violet | Aunt Violet | Mardi Gras | Ultraberry | 800080 |
90 | Mardi Gras | Dark Magenta | Purple | Philippine Violet | Aunt Violet | 870087 |
127 | Energic Eggplant | Purple Potion | Purple Pirate | Heliotrope Magenta | Kinky Koala | af00af |
164 | Fúchsia Intenso | Fungal Hallucinations | Passionate Pink | Awkward Purple | Screaming Magenta | d700d7 |
13 | Magenta | Sixteen Million Pink | Rainbow’s Inner Rim | Brusque Pink | Fresh Neon Pink | ff00ff |
201 | Magenta | Sixteen Million Pink | Rainbow’s Inner Rim | Brusque Pink | Fresh Neon Pink | ff00ff |
200 | Fuchsia Flame | Hot Magenta | Mademoiselle Pink | Vice City | Bright Magenta | ff00d7 |
163 | Aphroditean Fuchsia | Explosive Purple | Purple Pirate | Fúchsia Intenso | Passionate Pink | d700af |
126 | Vibrant Velvet | Katy Berry | Blissful Berry | Eye Popping Cherry | King’s Plum Pie | af0087 |
206 | Illicit Pink | Rose Pink | Drunken Flamingo | Purple Pizzazz | Candy Pink | ff5fd7 |
89 | Grapest | Cardinal Pink | Xereus Purple | Strong Cerise | Patriarch | 87005f |
199 | Mean Girls Lipstick | Bright Pink | Ms. Pac-Man Kiss | Brutal Pink | Shocking Pink | ff00af |
212 | Pink Delight | Princess Perfume | Pout Pink | Shimmering Love | Angel Face Rose | ff87d7 |
169 | Mega Magenta | Pú Táo Zǐ Purple | Purple Hollyhock | Orion | Pink Charge | d75faf |
162 | The Art of Seduction | Snappy Violet | Mexican Pink | Vampire Love Story | Benevolent Pink | d70087 |
125 | Velvet Cupcake | Shy Guy Red | Aztec Warrior | Violet Red | Banafsaji Purple | af005f |
198 | Fancy Fuchsia | Hot Pink | Neon Rose | Flickr Pink | Strong Pink | ff0087 |
218 | Lavender Candy | Spaghetti Strap Pink | Pink Cattleya | Fresh Gum | Hot Aquarelle Pink | ffafd7 |
175 | Springtime Bloom | High Maintenance | Middle Purple | Vivacious Pink | Moonlit Mauve | d787af |
132 | Dahlia Mauve | Wild Mulberry | Guppy Violet | Meadow Mauve | Cure All | af5f87 |
205 | Yíng Guāng Sè Pink | Girls Night Out | Pink Katydid | Pink as Hell | Hot Pink Fusion | ff5faf |
161 | Tête-à-Tête | Magna Cum Laude | Rubine Red | Anger | Rowan | d7005f |
197 | Flaming Hot Flamingoes | Sizzling Watermelon | New York Sunset | Standby Led | Sorx Red | ff005f |
211 | Informative Pink | Tickle Me Pink | Strawberry Dreams | Pinky | Rock’n’Rose | ff87af |
168 | Surfer Girl | Blush d’Amour | Flirty Rose | Preppy Rose | Groovy | d75f87 |
204 | Stellar Strawberry | Brink Pink | Warm Pink | Ultra Red | Rosy Pink | ff5f87 |
Making an rgb.txt
The rgb.txt
file format, as used by X11, is just one color per line, lower
case name followed by #hex
of the color. Optionally, lines starting with #
are comments. Let’s make one for xterm. We’ll use the best match to name each
color.
colors_by_name: dict[str, Color] = {
name: color for color in colors for name in color.names[1:]
}
display_attachment(
data="\n".join(
f"{name.lower()}\t#{rgb_to_hex(color.channels)}"
for name, color in colors_by_name.items()
),
filename="rgb-xterm.txt",
)
rgb-xterm.txtLet’s also make a second file that uses xterm indices instead of RGB colors -
this is easier to use with commands like tput
.
display_attachment(
data="\n".join(
f"{name.lower()}\t{color.names[0]}"
for name, color in colors_by_name.items()
),
filename="xterm-256.txt",
)
xterm-256.txtSome other uses for a color database
There are some other neat things we can do, now that we have a color database. Interesting things become possible if we index by Hue-Luminance-Saturation (HLS) instead of Red-Green-Blue. Here’s what the color space looks like:
import colorsys
fig, axes = color_space_plot()
plot_color_dots(
axes,
0.1,
("Hue", "Lightness", "Saturation"),
[(colorsys.rgb_to_hls(*color.channels), color) for color in medoai_colors],
)
And here’s how easy it is to rebuild the BVH. HLS is still three dimensions, so all we need to do is substitute a different 3-tuple instead of RGB.
Let’s use this to find some highly saturated shades of blue.
colors_by_hls = make_bvh(
[
(AABB.from_point(colorsys.rgb_to_hls(*color.channels)), color)
for color in medoai_colors
]
)
lo_hls = (0.55, 0.55, 0.95)
hi_hls = (0.6, 0.6, 1.0)
results = list(colors_by_hls.search(AABB(lo_hls, hi_hls)))
display.HTML("\n".join(colors_html([color for _, color in results])))
Pool Water | 2188ff |
Fantasy Console Sky | 29adff |
Brilliant Azure | 3399ff |
Cherenkov Radiation | 22bbff |
Clear Chill | 1e90ff |
We can take a look at where these colors are located in the new BVH, as well:
fig, axes = color_space_plot()
plot_bvh_trace(axes, colors_by_hls, AABB(lo_hls, hi_hls))
plot_color_dots(
axes,
1.0,
("Hue", "Lightness", "Saturation"),
[(bounds.lo, color) for bounds, color in results],
)
Useful Functions
All of this lets us define some neat utility functions for applications working with color.
class ColorDB:
_by_hls: BVH
_by_name: dict[str, Color]
def __init__(self, colors: Iterable[Color]):
self._by_name = {
self._normalize_name(name): color
for color in colors
for name in color.names
}
self._by_hls = make_bvh(
[
(AABB.from_point(colorsys.rgb_to_hls(*color.channels)), color)
for color in colors
]
)
def color_by_name(self, name: str) -> Color | None:
return self._by_name.get(self._normalize_name(name))
def similar_colors(self, color: Color, max_dist: float) -> Iterable[Color]:
h, l, s = colorsys.rgb_to_hls(*color.channels)
lo_hls = (h - max_dist, l - max_dist, s - max_dist)
hi_hls = (h + max_dist, l + max_dist, s + max_dist)
return (color for _, color in self._by_hls.search(AABB(lo_hls, hi_hls)))
def shades_of(self, name: str, max_dist: float) -> Iterable[Color]:
color = self.color_by_name(name)
if color is None:
return []
# Find similar shades of grey by luminance, otherwise similar shades of
# color by hue.
h, l, s = colorsys.rgb_to_hls(*color.channels)
if s < 0.1:
lo_hls = (0, l - max_dist, 0)
hi_hls = (1, l + max_dist, 1)
else:
lo_hls = (h - max_dist, 0.2, 0.1)
hi_hls = (h + max_dist, 0.9, 1.0)
return (color for _, color in self._by_hls.search(AABB(lo_hls, hi_hls)))
@staticmethod
def _normalize_name(name: str) -> str:
return name.lower().strip()
For example, we can now query the XTerm color space for available shades of red:
db = ColorDB(colors)
results = db.shades_of("red", 0.05)
display.HTML("\n".join(colors_html(results)))
131 | Italian Villa | Poppy Prose | Sienna Red | Cinnamon Candle | Spiced Tea | af5f5f |
95 | Rabbit Paws | Aged Beech | Tarsier | Forbidden Thrill | Rose Garland | 875f5f |
138 | Woodrose | Orchid Red | Warm Comfort | Audrey’s Blush | Retro Pink | af8787 |
167 | Roman | Happy Hearts | Salami Slice | Deep Sea Coral | Tory Red | d75f5f |
174 | Peaches of Immortality | Copperfield | Rhubarb Pie | Finest Blush | Mauve Glow | d78787 |
88 | Chanticleer | Glass Bull | Sacrifice Altar | Dark Red | Scab Red | 870000 |
124 | Red Door | Velvet Volcano | Heartbeat | Artful Red | Red Pentacle | af0000 |
160 | Red Republic | Rosso Corsa | Hot Fever | Red Pegasus | Red Epiphyllum | d70000 |
1 | Maroon | Salami | Dark Red | Sacrifice Altar | Chanticleer | 800000 |
209 | After Burn | Pink Fire | Protein High | Mango Orange | Nectarine | ff875f |
9 | Red | Rainbow’s Outer Rim | Fire Engine | Encarnado | Left on Red | ff0000 |
203 | Fusion Red | Pineapple Salmon | Pompelmo | Pastel Red | Grapefruit | ff5f5f |
196 | Red | Rainbow’s Outer Rim | Fire Engine | Encarnado | Left on Red | ff0000 |
181 | Mary Rose | Radiant Rouge | Victoriana | Pale Persimmon | Ballet Rose | d7afaf |
210 | Red Mull | Coral Trails | Tulip | Prime Pink | Camaron Pink | ff8787 |
217 | Fancy Flamingo | Cornflower Lilac | Wildflower Bouquet | Peach Bud | Apricot Haze | ffafaf |
Now isn’t that just neat?
Thanks for reading! You can download this article as a Jupyter Notebook.