diff --git a/colorhash/color.py b/colorhash/color.py new file mode 100644 index 0000000..2248410 --- /dev/null +++ b/colorhash/color.py @@ -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 diff --git a/colorhash/colorizer.py b/colorhash/colorizer.py index 970c34b..7355d57 100644 --- a/colorhash/colorizer.py +++ b/colorhash/colorizer.py @@ -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. diff --git a/colorhash/palettes.py b/colorhash/palettes.py index 7a55403..c14d3b3 100644 --- a/colorhash/palettes.py +++ b/colorhash/palettes.py @@ -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 = { diff --git a/colorhash/svg.py b/colorhash/svg.py index 88c8cab..1e168ae 100644 --- a/colorhash/svg.py +++ b/colorhash/svg.py @@ -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' \n' + svg += f' \n' # Close SVG string svg += ""