64c0998a68
The SVG colors are now an enum of Color objects, instead of hex strings. The "transparent" color was also added, which isn't in the standards but works in QML.
463 lines
16 KiB
Python
463 lines
16 KiB
Python
# Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
|
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
"""Provide the `Color` class and functions to easily construct a `Color`."""
|
|
|
|
import builtins
|
|
import colorsys
|
|
from copy import copy
|
|
from dataclasses import InitVar, dataclass, field
|
|
from enum import Enum
|
|
from typing import Optional, Tuple, Union
|
|
|
|
from hsluv import hex_to_hsluv, hsluv_to_hex, hsluv_to_rgb, rgb_to_hsluv
|
|
|
|
ColorTuple = Tuple[float, float, float, float]
|
|
|
|
|
|
@dataclass(repr=False)
|
|
class Color:
|
|
"""A color manipulable in HSLuv, HSL, RGB, hexadecimal and by SVG name.
|
|
|
|
The `Color` object constructor accepts hexadecimal string
|
|
("#RGB", "#RRGGBB" or "#RRGGBBAA") or another `Color` to copy.
|
|
|
|
Attributes representing the color in HSLuv, HSL, RGB, hexadecimal and
|
|
SVG name formats can be accessed and modified on these `Color` objects.
|
|
|
|
The `hsluv()`/`hsluva()`, `hsl()`/`hsla()` and `rgb()`/`rgba()`
|
|
functions in this module are provided to create an object by specifying
|
|
a color in other formats.
|
|
|
|
Copies of objects with modified attributes can be created with the
|
|
with the `Color.but()`, `Color.plus()` and `Copy.times()` methods.
|
|
|
|
If the `hue` is outside of the normal 0-359 range, the number is
|
|
interpreted as `hue % 360`, e.g. `360` is `0`, `460` is `100`,
|
|
or `-20` is `340`.
|
|
"""
|
|
|
|
# The saturation and luv are properties due to the need for a setter
|
|
# capping the value between 0-100, as hsluv handles numbers outside
|
|
# this range incorrectly.
|
|
|
|
color_or_hex: InitVar[str] = "#00000000"
|
|
hue: float = field(init=False, default=0)
|
|
_saturation: float = field(init=False, default=0)
|
|
_luv: float = field(init=False, default=0)
|
|
alpha: float = field(init=False, default=1)
|
|
|
|
def __post_init__(self, color_or_hex: Union["Color", str]) -> None:
|
|
if isinstance(color_or_hex, Color):
|
|
hsluva = color_or_hex.hsluva
|
|
self.hue, self.saturation, self.luv, self.alpha = hsluva
|
|
else:
|
|
self.hex = color_or_hex
|
|
|
|
# HSLuv
|
|
|
|
@property
|
|
def hsluva(self) -> ColorTuple:
|
|
return (self.hue, self.saturation, self.luv, self.alpha)
|
|
|
|
@hsluva.setter
|
|
def hsluva(self, value: ColorTuple) -> None:
|
|
self.hue, self.saturation, self.luv, self.alpha = value
|
|
|
|
@property
|
|
def saturation(self) -> float:
|
|
return self._saturation
|
|
|
|
@saturation.setter
|
|
def saturation(self, value: float) -> None:
|
|
self._saturation = max(0, min(100, value))
|
|
|
|
@property
|
|
def luv(self) -> float:
|
|
return self._luv
|
|
|
|
@luv.setter
|
|
def luv(self, value: float) -> None:
|
|
self._luv = max(0, min(100, value))
|
|
|
|
# HSL
|
|
|
|
@property
|
|
def hsla(self) -> ColorTuple:
|
|
r, g, b = (self.red / 255, self.green / 255, self.blue / 255)
|
|
h, l, s = colorsys.rgb_to_hls(r, g, b)
|
|
return (h * 360, s * 100, l * 100, self.alpha)
|
|
|
|
@hsla.setter
|
|
def hsla(self, value: ColorTuple) -> None:
|
|
h, s, l = (value[0] / 360, value[1] / 100, value[2] / 100) # noqa
|
|
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
|
self.rgba = (r * 255, g * 255, b * 255, value[3])
|
|
|
|
@property
|
|
def light(self) -> float:
|
|
return self.hsla[2]
|
|
|
|
@light.setter
|
|
def light(self, value: float) -> None:
|
|
self.hsla = (self.hue, self.saturation, value, self.alpha)
|
|
|
|
# RGB
|
|
|
|
@property
|
|
def rgba(self) -> ColorTuple:
|
|
r, g, b = hsluv_to_rgb(self.hsluva)
|
|
return r * 255, g * 255, b * 255, self.alpha
|
|
|
|
@rgba.setter
|
|
def rgba(self, value: ColorTuple) -> None:
|
|
r, g, b = (value[0] / 255, value[1] / 255, value[2] / 255)
|
|
self.hsluva = rgb_to_hsluv((r, g, b)) + (self.alpha,)
|
|
|
|
@property
|
|
def red(self) -> float:
|
|
return self.rgba[0]
|
|
|
|
@red.setter
|
|
def red(self, value: float) -> None:
|
|
self.rgba = (value, self.green, self.blue, self.alpha)
|
|
|
|
@property
|
|
def green(self) -> float:
|
|
return self.rgba[1]
|
|
|
|
@green.setter
|
|
def green(self, value: float) -> None:
|
|
self.rgba = (self.red, value, self.blue, self.alpha)
|
|
|
|
@property
|
|
def blue(self) -> float:
|
|
return self.rgba[2]
|
|
|
|
@blue.setter
|
|
def blue(self, value: float) -> None:
|
|
self.rgba = (self.red, self.green, value, self.alpha)
|
|
|
|
# Hexadecimal
|
|
|
|
@property
|
|
def hex(self) -> str:
|
|
rgb = hsluv_to_hex(self.hsluva)
|
|
alpha = builtins.hex(int(self.alpha * 255))[2:]
|
|
alpha = f"0{alpha}" if len(alpha) == 1 else alpha
|
|
return f"{alpha if self.alpha < 1 else ''}{rgb}".lower()
|
|
|
|
@hex.setter
|
|
def hex(self, value: str) -> None:
|
|
if len(value) == 4:
|
|
template = "#{r}{r}{g}{g}{b}{b}"
|
|
value = template.format(r=value[1], g=value[2], b=value[3])
|
|
|
|
alpha = int(value[-2:] if len(value) == 9 else "ff", 16) / 255
|
|
|
|
self.hsluva = hex_to_hsluv(value) + (alpha,)
|
|
|
|
# name color
|
|
|
|
@property
|
|
def name(self) -> Optional[str]:
|
|
try:
|
|
return SVGColor(self.hex).name
|
|
except ValueError:
|
|
return None
|
|
|
|
@name.setter
|
|
def name(self, value: str) -> None:
|
|
self.hex = SVGColor[value.lower()].value.hex
|
|
|
|
# Other methods
|
|
|
|
def __repr__(self) -> str:
|
|
r, g, b = int(self.red), int(self.green), int(self.blue)
|
|
h, s, luv = int(self.hue), int(self.saturation), int(self.luv)
|
|
l = int(self.light) # noqa
|
|
a = self.alpha
|
|
block = f"\x1b[38;2;{r};{g};{b}m█████\x1b[0m"
|
|
sep = "\x1b[1;33m/\x1b[0m"
|
|
end = f" {sep} {self.name}" if self.name else ""
|
|
# Need a terminal with true color support to render the block!
|
|
return (
|
|
f"{block} hsluva({h}, {s}, {luv}, {a}) {sep} "
|
|
f"hsla({h}, {s}, {l}, {a}) {sep} rgba({r}, {g}, {b}, {a}) {sep} "
|
|
f"{self.hex}{end}"
|
|
)
|
|
|
|
def but(
|
|
self,
|
|
hue: Optional[float] = None,
|
|
saturation: Optional[float] = None,
|
|
luv: Optional[float] = None,
|
|
alpha: Optional[float] = None,
|
|
*,
|
|
hsluva: Optional[ColorTuple] = None,
|
|
hsla: Optional[ColorTuple] = None,
|
|
rgba: Optional[ColorTuple] = None,
|
|
hex: Optional[str] = None,
|
|
name: Optional[str] = None,
|
|
light: Optional[float] = None,
|
|
red: Optional[float] = None,
|
|
green: Optional[float] = None,
|
|
blue: Optional[float] = None,
|
|
) -> "Color":
|
|
"""Return a copy of this `Color` with overriden attributes.
|
|
|
|
Example:
|
|
>>> first = Color(100, 50, 50)
|
|
>>> second = c.but(hue=20, saturation=100)
|
|
>>> second.hsluva
|
|
(20, 50, 100, 1)
|
|
"""
|
|
|
|
new = copy(self)
|
|
|
|
for arg, value in locals().items():
|
|
if arg not in ("new", "self") and value is not None:
|
|
setattr(new, arg, value)
|
|
|
|
return new
|
|
|
|
def plus(
|
|
self,
|
|
hue: Optional[float] = None,
|
|
saturation: Optional[float] = None,
|
|
luv: Optional[float] = None,
|
|
alpha: Optional[float] = None,
|
|
*,
|
|
light: Optional[float] = None,
|
|
red: Optional[float] = None,
|
|
green: Optional[float] = None,
|
|
blue: Optional[float] = None,
|
|
) -> "Color":
|
|
"""Return a copy of this `Color` with values added to attributes.
|
|
|
|
Example:
|
|
>>> first = Color(100, 50, 50)
|
|
>>> second = c.plus(hue=10, saturation=-20)
|
|
>>> second.hsluva
|
|
(110, 30, 50, 1)
|
|
"""
|
|
|
|
new = copy(self)
|
|
|
|
for arg, value in locals().items():
|
|
if arg not in ("new", "self") and value is not None:
|
|
setattr(new, arg, getattr(self, arg) + value)
|
|
|
|
return new
|
|
|
|
def times(
|
|
self,
|
|
hue: Optional[float] = None,
|
|
saturation: Optional[float] = None,
|
|
luv: Optional[float] = None,
|
|
alpha: Optional[float] = None,
|
|
*,
|
|
light: Optional[float] = None,
|
|
red: Optional[float] = None,
|
|
green: Optional[float] = None,
|
|
blue: Optional[float] = None,
|
|
) -> "Color":
|
|
"""Return a copy of this `Color` with multiplied attributes.
|
|
|
|
Example:
|
|
>>> first = Color(100, 50, 50, 0.8)
|
|
>>> second = c.times(luv=2, alpha=0.5)
|
|
>>> second.hsluva
|
|
(100, 50, 100, 0.4)
|
|
"""
|
|
|
|
new = copy(self)
|
|
|
|
for arg, value in locals().items():
|
|
if arg not in ("new", "self") and value is not None:
|
|
setattr(new, arg, getattr(self, arg) * value)
|
|
|
|
return new
|
|
|
|
|
|
class SVGColor(Enum):
|
|
"""Standard SVG/HTML/CSS colors, with the addition of `transparent`."""
|
|
|
|
aliceblue = Color("#f0f8ff")
|
|
antiquewhite = Color("#faebd7")
|
|
aqua = Color("#00ffff")
|
|
aquamarine = Color("#7fffd4")
|
|
azure = Color("#f0ffff")
|
|
beige = Color("#f5f5dc")
|
|
bisque = Color("#ffe4c4")
|
|
black = Color("#000000")
|
|
blanchedalmond = Color("#ffebcd")
|
|
blue = Color("#0000ff")
|
|
blueviolet = Color("#8a2be2")
|
|
brown = Color("#a52a2a")
|
|
burlywood = Color("#deb887")
|
|
cadetblue = Color("#5f9ea0")
|
|
chartreuse = Color("#7fff00")
|
|
chocolate = Color("#d2691e")
|
|
coral = Color("#ff7f50")
|
|
cornflowerblue = Color("#6495ed")
|
|
cornsilk = Color("#fff8dc")
|
|
crimson = Color("#dc143c")
|
|
cyan = Color("#00ffff")
|
|
darkblue = Color("#00008b")
|
|
darkcyan = Color("#008b8b")
|
|
darkgoldenrod = Color("#b8860b")
|
|
darkgray = Color("#a9a9a9")
|
|
darkgreen = Color("#006400")
|
|
darkgrey = Color("#a9a9a9")
|
|
darkkhaki = Color("#bdb76b")
|
|
darkmagenta = Color("#8b008b")
|
|
darkolivegreen = Color("#556b2f")
|
|
darkorange = Color("#ff8c00")
|
|
darkorchid = Color("#9932cc")
|
|
darkred = Color("#8b0000")
|
|
darksalmon = Color("#e9967a")
|
|
darkseagreen = Color("#8fbc8f")
|
|
darkslateblue = Color("#483d8b")
|
|
darkslategray = Color("#2f4f4f")
|
|
darkslategrey = Color("#2f4f4f")
|
|
darkturquoise = Color("#00ced1")
|
|
darkviolet = Color("#9400d3")
|
|
deeppink = Color("#ff1493")
|
|
deepskyblue = Color("#00bfff")
|
|
dimgray = Color("#696969")
|
|
dimgrey = Color("#696969")
|
|
dodgerblue = Color("#1e90ff")
|
|
firebrick = Color("#b22222")
|
|
floralwhite = Color("#fffaf0")
|
|
forestgreen = Color("#228b22")
|
|
fuchsia = Color("#ff00ff")
|
|
gainsboro = Color("#dcdcdc")
|
|
ghostwhite = Color("#f8f8ff")
|
|
gold = Color("#ffd700")
|
|
goldenrod = Color("#daa520")
|
|
gray = Color("#808080")
|
|
green = Color("#008000")
|
|
greenyellow = Color("#adff2f")
|
|
grey = Color("#808080")
|
|
honeydew = Color("#f0fff0")
|
|
hotpink = Color("#ff69b4")
|
|
indianred = Color("#cd5c5c")
|
|
indigo = Color("#4b0082")
|
|
ivory = Color("#fffff0")
|
|
khaki = Color("#f0e68c")
|
|
lavender = Color("#e6e6fa")
|
|
lavenderblush = Color("#fff0f5")
|
|
lawngreen = Color("#7cfc00")
|
|
lemonchiffon = Color("#fffacd")
|
|
lightblue = Color("#add8e6")
|
|
lightcoral = Color("#f08080")
|
|
lightcyan = Color("#e0ffff")
|
|
lightgoldenrodyellow = Color("#fafad2")
|
|
lightgray = Color("#d3d3d3")
|
|
lightgreen = Color("#90ee90")
|
|
lightgrey = Color("#d3d3d3")
|
|
lightpink = Color("#ffb6c1")
|
|
lightsalmon = Color("#ffa07a")
|
|
lightseagreen = Color("#20b2aa")
|
|
lightskyblue = Color("#87cefa")
|
|
lightslategray = Color("#778899")
|
|
lightslategrey = Color("#778899")
|
|
lightsteelblue = Color("#b0c4de")
|
|
lightyellow = Color("#ffffe0")
|
|
lime = Color("#00ff00")
|
|
limegreen = Color("#32cd32")
|
|
linen = Color("#faf0e6")
|
|
magenta = Color("#ff00ff")
|
|
maroon = Color("#800000")
|
|
mediumaquamarine = Color("#66cdaa")
|
|
mediumblue = Color("#0000cd")
|
|
mediumorchid = Color("#ba55d3")
|
|
mediumpurple = Color("#9370db")
|
|
mediumseagreen = Color("#3cb371")
|
|
mediumslateblue = Color("#7b68ee")
|
|
mediumspringgreen = Color("#00fa9a")
|
|
mediumturquoise = Color("#48d1cc")
|
|
mediumvioletred = Color("#c71585")
|
|
midnightblue = Color("#191970")
|
|
mintcream = Color("#f5fffa")
|
|
mistyrose = Color("#ffe4e1")
|
|
moccasin = Color("#ffe4b5")
|
|
navajowhite = Color("#ffdead")
|
|
navy = Color("#000080")
|
|
oldlace = Color("#fdf5e6")
|
|
olive = Color("#808000")
|
|
olivedrab = Color("#6b8e23")
|
|
orange = Color("#ffa500")
|
|
orangered = Color("#ff4500")
|
|
orchid = Color("#da70d6")
|
|
palegoldenrod = Color("#eee8aa")
|
|
palegreen = Color("#98fb98")
|
|
paleturquoise = Color("#afeeee")
|
|
palevioletred = Color("#db7093")
|
|
papayawhip = Color("#ffefd5")
|
|
peachpuff = Color("#ffdab9")
|
|
peru = Color("#cd853f")
|
|
pink = Color("#ffc0cb")
|
|
plum = Color("#dda0dd")
|
|
powderblue = Color("#b0e0e6")
|
|
purple = Color("#800080")
|
|
rebeccapurple = Color("#663399")
|
|
red = Color("#ff0000")
|
|
rosybrown = Color("#bc8f8f")
|
|
royalblue = Color("#4169e1")
|
|
saddlebrown = Color("#8b4513")
|
|
salmon = Color("#fa8072")
|
|
sandybrown = Color("#f4a460")
|
|
seagreen = Color("#2e8b57")
|
|
seashell = Color("#fff5ee")
|
|
sienna = Color("#a0522d")
|
|
silver = Color("#c0c0c0")
|
|
skyblue = Color("#87ceeb")
|
|
slateblue = Color("#6a5acd")
|
|
slategray = Color("#708090")
|
|
slategrey = Color("#708090")
|
|
snow = Color("#fffafa")
|
|
springgreen = Color("#00ff7f")
|
|
steelblue = Color("#4682b4")
|
|
tan = Color("#d2b48c")
|
|
teal = Color("#008080")
|
|
thistle = Color("#d8bfd8")
|
|
tomato = Color("#ff6347")
|
|
transparent = Color("#00000000") # not standard but exists in QML
|
|
turquoise = Color("#40e0d0")
|
|
violet = Color("#ee82ee")
|
|
wheat = Color("#f5deb3")
|
|
white = Color("#ffffff")
|
|
whitesmoke = Color("#f5f5f5")
|
|
yellow = Color("#ffff00")
|
|
yellowgreen = Color("#9acd32")
|
|
|
|
|
|
def hsluva(
|
|
hue: float = 0, saturation: float = 0, luv: float = 0, alpha: float = 1,
|
|
) -> Color:
|
|
"""Return a `Color` from `(0-359, 0-100, 0-100, 0-1)` HSLuv arguments."""
|
|
return Color().but(hue, saturation, luv, alpha)
|
|
|
|
|
|
def hsla(
|
|
hue: float = 0, saturation: float = 0, light: float = 0, alpha: float = 1,
|
|
) -> Color:
|
|
"""Return a `Color` from `(0-359, 0-100, 0-100, 0-1)` HSL arguments."""
|
|
return Color().but(hue, saturation, light=light, alpha=alpha)
|
|
|
|
|
|
def rgba(
|
|
red: float = 0, green: float = 0, blue: float = 0, alpha: float = 1,
|
|
) -> Color:
|
|
"""Return a `Color` from `(0-255, 0-255, 0-255, 0-1)` RGB arguments."""
|
|
return Color().but(red=red, green=green, blue=blue, alpha=alpha)
|
|
|
|
|
|
# Aliases
|
|
color = Color
|
|
hsluv = hsluva
|
|
hsl = hsla
|
|
rgb = rgba
|