Update colors to be expressed explicitly as RGB or HSL

Generally, palettes are expressed using HSL, but sometimes we want the
output to be in RGB (or some other format). Colors are now expressed
using either RGBColor or HSLColor and can easily be converted between
the two.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2024-05-30 11:02:11 -07:00
parent ee16b14c73
commit 1def2fa7bf
4 changed files with 90 additions and 15 deletions

72
colorhash/color.py Normal file
View File

@@ -0,0 +1,72 @@
import abc
import colorsys
import dataclasses
class Color(metaclass=abc.ABCMeta):
"""
An abstract color class.
This can be used to convert any color format into another color format.
"""
def to_html_color(self) -> str:
"""
Convert this color to an HTML color.
This may produce unexpected results if you are expecting an RGB color. If you are expecting
RGB, then you should use `color.to_rgb().to_html_color()` instead.
"""
@abc.abstractmethod
def to_rgb(self) -> "RGBColor":
"Convert this color into an RGB color."
@abc.abstractmethod
def to_hsl(self) -> "HSLColor":
"Convert this color into an HSL color."
@dataclasses.dataclass
class RGBColor(Color):
"""
An RGB color. Colors are expected to be a floating point value from [0.0-255.0).
"""
r: float
g: float
b: float
def to_html_color(self) -> str:
r, g, b = round(self.r), round(self.g), round(self.b)
return f"#{r:02x}{g:02x}{b:02x}"
def to_rgb(self) -> "RGBColor":
return self
def to_hsl(self) -> "HSLColor":
r = self.r / 255.0
g = self.g / 255.0
b = self.b / 255.0
h, l, s = colorsys.rgb_to_hls(r, g, b)
return HSLColor(h * 360.0, s * 100.0, l * 100.0)
@dataclasses.dataclass
class HSLColor(Color):
h: float
s: float
l: float
def to_html_color(self) -> str:
return f"hsl({self.h:.02f},{self.s:.02f}%,{self.l:.02f}%)"
def to_rgb(self) -> "RGBColor":
h = self.h / 360.0
s = self.s / 100.0
l = self.l / 100.0
r, g, b = colorsys.hls_to_rgb(h, l, s)
return RGBColor(r * 255.0, g * 255.0, b * 255.0)
def to_hsl(self) -> "HSLColor":
return self

View File

@@ -2,11 +2,12 @@
import abc
from typing import Sequence
from .color import Color
from .matricizer import Matrix
from .palettes import Palette
StrMatrix = Sequence[Sequence[str]]
ColorMatrix = Sequence[Sequence[Color]]
class Colorizer(metaclass=abc.ABCMeta):
@@ -18,7 +19,7 @@ class Colorizer(metaclass=abc.ABCMeta):
"""
@abc.abstractmethod
def colorize(self, matrix: Matrix) -> StrMatrix:
def colorize(self, matrix: Matrix) -> ColorMatrix:
"""
Colorize a matrix.
@@ -41,7 +42,7 @@ class PaletteColorizer(Colorizer):
"""
self.palette = palette
def colorize(self, matrix: Matrix) -> StrMatrix:
def colorize(self, matrix: Matrix) -> ColorMatrix:
"""
Colorize the given matrix using this colorizer's palette.

View File

@@ -1,22 +1,24 @@
"Base color palette definitions."
import abc
from typing import Sequence, Self
from typing import Sequence
from .color import Color, HSLColor
class Palette(metaclass=abc.ABCMeta):
"""
A 16-color palette.
All colors must be HTML color strings.
All colors must be a `colorhash.Color`.
"""
@abc.abstractmethod
def choose(self, color: int) -> str:
def choose(self, color: int) -> Color:
"""
Chooses the given color in this palette.
"""
def __getitem__(self, color: int) -> str:
def __getitem__(self, color: int) -> Color:
return self.choose(color)
@@ -25,7 +27,7 @@ class StaticPalette(Palette):
A static color palette with discrete colors.
"""
def __init__(self, colors: Sequence[str]) -> None:
def __init__(self, colors: Sequence[Color]) -> None:
"""
Creates a new static color palette.
@@ -35,13 +37,13 @@ class StaticPalette(Palette):
raise ValueError(f"palette must have exactly 16 colors (got {len(colors)})")
self.colors = colors
def choose(self, color: int) -> str:
def choose(self, color: int) -> Color:
if not isinstance(color, int):
raise KeyError("palette color indices must be an integer")
return self.colors[color]
HSVRange = range | float | int | list[float | int]
HSLRange = range | float | int | list[float | int]
def quantize(r: range, steps: int = 16) -> list[float]:
@@ -57,7 +59,7 @@ def quantize(r: range, steps: int = 16) -> list[float]:
return [r.start + (i * dist / (steps - 1)) for i in range(steps)]
def hsl_colors(hue: HSVRange, sat: HSVRange, light: HSVRange) -> list[str]:
def hsl_colors(hue: HSLRange, sat: HSLRange, light: HSLRange) -> list[HSLColor]:
"""
Utility method to create 16 colors using HSL.
@@ -84,7 +86,7 @@ def hsl_colors(hue: HSVRange, sat: HSVRange, light: HSVRange) -> list[str]:
light = quantize(light)
assert len(light) == 16, "light values must be a list of 16 elements"
return [f"hsl({h:.02f},{s:.02f}%,{l:.02f}%)" for h, s, l in zip(hue, sat, light)]
return [HSLColor(round(h), round(s), round(l)) for h, s, l in zip(hue, sat, light)]
GRADIENT_PALETTES = {

View File

@@ -1,8 +1,8 @@
"SVG-related functions."
from .colorizer import StrMatrix
from .colorizer import ColorMatrix
def gensvg(matrix: StrMatrix, square_size: int) -> str:
def gensvg(matrix: ColorMatrix, square_size: int) -> str:
"""
Generate an SVG based on a given matrix.
@@ -22,7 +22,7 @@ def gensvg(matrix: StrMatrix, square_size: int) -> str:
x = c * square_size
y = r * square_size
color = matrix[r][c]
svg += f' <rect x="{x}" y="{y}" width="{square_size}" height="{square_size}" fill="{color}" />\n'
svg += f' <rect x="{x}" y="{y}" width="{square_size}" height="{square_size}" fill="{color.to_html_color()}" />\n'
# Close SVG string
svg += "</svg>"