"""
Color model utilities for RGB/HSV/HLS conversion and CSS3 name handling.
This module provides a high-level :class:`Color` class that represents colors in
RGB space and offers convenient conversions to HSV and HLS color models. It
supports construction from CSS3 color names, hex strings, RGB tuples, or other
:class:`Color` instances, and optionally manages alpha (transparency) values.
The module is designed to keep component values within valid ranges:
* RGB, HSV, HLS components are clamped to ``[0.0, 1.0]``.
* Hue values are wrapped (cyclic) to ``[0.0, 1.0]``.
* Alpha values are clamped to ``[0.0, 1.0]``.
Main components:
* :class:`Color` - Color representation with RGB/HSV/HLS conversion support.
.. note::
The RGB/HSV/HLS component proxies returned by :attr:`Color.rgb`,
:attr:`Color.hsv`, and :attr:`Color.hls` allow in-place modifications
of the underlying color instance.
Example::
>>> from hbutils.color import Color
>>> c = Color('red')
>>> str(c)
'#ff0000'
>>> c.rgb.red
1.0
>>> c.hsv.hue
0.0
>>> c.alpha is None
True
>>> c.alpha = 0.5
>>> str(c)
'#ff000080'
"""
import colorsys
import math
import re
from typing import Optional, Union, Tuple, Callable, Iterator, Any
from .base import _name_to_hex, _CSS3_NAME_MAPS
from ..reflection.func import post_process, raising, freduce, dynamic_call, warning_
__all__ = ['Color']
def _round_mapper(min_: float, max_: float) -> Callable[[float], float]:
"""
Create a cyclic range mapper for a numeric interval.
This mapper wraps values into the given interval rather than clamping them.
Values less than ``min_`` or greater than ``max_`` are wrapped by repeatedly
adding or subtracting the interval length.
:param min_: Minimum value of the interval.
:type min_: float
:param max_: Maximum value of the interval.
:type max_: float
:return: A function that maps input values into the interval.
:rtype: Callable[[float], float]
Example::
>>> mapper = _round_mapper(0.0, 1.0)
>>> mapper(1.5)
0.5
>>> mapper(-0.5)
0.5
"""
min_, max_ = min(min_, max_), max(min_, max_)
round_ = max_ - min_
def _func(v: float) -> float:
if v < min_:
v += math.ceil((min_ - v) / round_) * round_
if v > max_:
v -= math.ceil((v - max_) / round_) * round_
return v
return _func
def _range_mapper(min_: Optional[float], max_: Optional[float], warning: Optional[Callable] = None) -> Callable[
[float], float]:
"""
Create a clamping mapper for a numeric interval with optional warnings.
Values outside the provided interval are clamped to the nearest bound.
When a value is out of range, the provided warning callback is invoked
via :func:`warning_`.
:param min_: Minimum value of the range, or ``None`` for no minimum limit.
:type min_: Optional[float]
:param max_: Maximum value of the range, or ``None`` for no maximum limit.
:type max_: Optional[float]
:param warning: Optional warning callback executed when a value is clamped.
:type warning: Optional[Callable]
:return: A function that clamps input values to the specified range.
:rtype: Callable[[float], float]
Example::
>>> mapper = _range_mapper(0.0, 1.0)
>>> mapper(1.5)
1.0
>>> mapper(-0.5)
0.0
"""
if min_ is not None and max_ is not None:
min_, max_ = min(min_, max_), max(min_, max_)
warning = dynamic_call(warning_(warning if warning is not None else lambda: None))
def _func(v: float) -> float:
if max_ is not None and v > max_:
warning(v, min_, max_)
return max_
elif min_ is not None and v < min_:
warning(v, min_, max_)
return min_
else:
return v
return _func
[docs]
class GetSetProxy:
"""
A lightweight getter/setter proxy.
This class provides a unified interface to a getter callable and an optional
setter callable. If no setter is provided, attempting to set will raise
:class:`NotImplementedError`.
:param getter: Function to retrieve the value.
:type getter: Callable
:param setter: Optional function to update the value.
:type setter: Optional[Callable]
"""
[docs]
def __init__(self, getter: Callable, setter: Optional[Callable] = None):
"""
Initialize the GetSetProxy.
:param getter: Function to get the value.
:type getter: Callable
:param setter: Optional function to set the value. If ``None``, the setter
raises :class:`NotImplementedError`.
:type setter: Optional[Callable]
"""
self.__getter = getter
self.__setter = setter or raising(lambda x: NotImplementedError)
[docs]
def set(self, value: Any) -> Any:
"""
Set the value using the setter function.
:param value: Value to set.
:type value: Any
:return: Result of the setter function.
:rtype: Any
"""
return self.__setter(value)
[docs]
def get(self) -> Any:
"""
Get the value using the getter function.
:return: The retrieved value.
:rtype: Any
"""
return self.__getter()
_r_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
'Red value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_g_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
'Green value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_b_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
'Blue value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_a_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
'Alpha value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
[docs]
class RGBColorProxy:
"""
RGB color component proxy for :class:`Color`.
Instances of this class are returned by :attr:`Color.rgb` and provide
readable and writable access to the RGB components.
:param this: Original color object.
:type this: Color
:param r: Get-set proxy for red.
:type r: GetSetProxy
:param g: Get-set proxy for green.
:type g: GetSetProxy
:param b: Get-set proxy for blue.
:type b: GetSetProxy
"""
[docs]
def __init__(self, this: 'Color', r: GetSetProxy, g: GetSetProxy, b: GetSetProxy):
"""
Constructor of :class:`RGBColorProxy`.
:param this: Original color object.
:type this: Color
:param r: Get-set proxy for red.
:type r: GetSetProxy
:param g: Get-set proxy for green.
:type g: GetSetProxy
:param b: Get-set proxy for blue.
:type b: GetSetProxy
"""
self.__this = this
self.__rp = r
self.__gp = g
self.__bp = b
@property
def red(self) -> float:
"""
Red value (within :math:`\\left[0.0, 1.0\\right]`).
.. note::
Setter is available, the change will affect the :class:`Color` object.
:return: Red component value.
:rtype: float
"""
return self.__rp.get()
@red.setter
def red(self, new: float):
"""
Set the red value.
:param new: New red value.
:type new: float
"""
self.__rp.set(new)
@property
def green(self) -> float:
"""
Green value (within :math:`\\left[0.0, 1.0\\right]`).
.. note::
Setter is available, the change will affect the :class:`Color` object.
:return: Green component value.
:rtype: float
"""
return self.__gp.get()
@green.setter
def green(self, new: float):
"""
Set the green value.
:param new: New green value.
:type new: float
"""
self.__gp.set(new)
@property
def blue(self) -> float:
"""
Blue value (within :math:`\\left[0.0, 1.0\\right]`).
.. note::
Setter is available, the change will affect the :class:`Color` object.
:return: Blue component value.
:rtype: float
"""
return self.__bp.get()
@blue.setter
def blue(self, new: float):
"""
Set the blue value.
:param new: New blue value.
:type new: float
"""
self.__bp.set(new)
[docs]
def __iter__(self) -> Iterator[float]:
"""
Iterator for this proxy.
:return: Iterator yielding red, green, and blue values.
:rtype: Iterator[float]
Examples::
>>> from hbutils.color import Color
>>>
>>> c = Color('green')
>>> r, g, b = c.rgb
>>> print(r, g, b)
0.0 0.5019607843137255 0.0
"""
yield self.red
yield self.green
yield self.blue
[docs]
def __repr__(self) -> str:
"""
Representation format.
:return: String representation of the RGB color proxy.
:rtype: str
Examples::
>>> from hbutils.color import Color
>>>
>>> c = Color('green')
>>> c.rgb
<RGBColorProxy red: 0.000, green: 0.502, blue: 0.000>
"""
return '<{cls} red: {red}, green: {green}, blue: {blue}>'.format(
cls=self.__class__.__name__,
red='%.3f' % (self.red,),
green='%.3f' % (self.green,),
blue='%.3f' % (self.blue,),
)
_hsv_h_mapper = _round_mapper(0.0, 1.0)
_hsv_s_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
'Saturation value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_hsv_v_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
'Brightness(value) value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
[docs]
class HSVColorProxy:
"""
HSV color component proxy for :class:`Color`.
Instances of this class are returned by :attr:`Color.hsv` and provide
readable and writable access to HSV components.
:param this: Original color object.
:type this: Color
:param h: Get-set proxy for hue.
:type h: GetSetProxy
:param s: Get-set proxy for saturation.
:type s: GetSetProxy
:param v: Get-set proxy for value.
:type v: GetSetProxy
"""
[docs]
def __init__(self, this: 'Color', h: GetSetProxy, s: GetSetProxy, v: GetSetProxy):
"""
Constructor of :class:`HSVColorProxy`.
:param this: Original color object.
:type this: Color
:param h: Get-set proxy for hue.
:type h: GetSetProxy
:param s: Get-set proxy for saturation.
:type s: GetSetProxy
:param v: Get-set proxy for value.
:type v: GetSetProxy
"""
self.__this = this
self.__hp = h
self.__sp = s
self.__vp = v
@property
def hue(self) -> float:
"""
Hue value (within :math:`\\left[0.0, 1.0\\right]`).
.. note::
Setter is available, the change will affect the :class:`Color` object.
:return: Hue component value.
:rtype: float
"""
return self.__hp.get()
@hue.setter
def hue(self, new: float):
"""
Set the hue value.
:param new: New hue value.
:type new: float
"""
self.__hp.set(new)
@property
def saturation(self) -> float:
"""
Saturation value (within :math:`\\left[0.0, 1.0\\right]`).
.. note::
Setter is available, the change will affect the :class:`Color` object.
:return: Saturation component value.
:rtype: float
"""
return self.__sp.get()
@saturation.setter
def saturation(self, new: float):
"""
Set the saturation value.
:param new: New saturation value.
:type new: float
"""
self.__sp.set(new)
@property
def value(self) -> float:
"""
Value (brightness) value (within :math:`\\left[0.0, 1.0\\right]`).
.. note::
Setter is available, the change will affect the :class:`Color` object.
:return: Value (brightness) component value.
:rtype: float
"""
return self.__vp.get()
@value.setter
def value(self, new: float):
"""
Set the value.
:param new: New value.
:type new: float
"""
self.__vp.set(new)
@property
def brightness(self) -> float:
"""
Alias for :attr:`value`.
:return: Brightness (value) component value.
:rtype: float
"""
return self.value
@brightness.setter
def brightness(self, new: float):
"""
Set the brightness value.
:param new: New brightness value.
:type new: float
"""
self.value = new
[docs]
def __iter__(self) -> Iterator[float]:
"""
Iterator for this proxy.
:return: Iterator yielding hue, saturation, and value.
:rtype: Iterator[float]
Examples::
>>> from hbutils.color import Color
>>>
>>> c = Color('green')
>>> h, s, v = c.hsv
>>> print(h, s, v)
0.3333333333333333 1.0 0.5019607843137255
"""
yield self.hue
yield self.saturation
yield self.value
[docs]
def __repr__(self) -> str:
"""
Representation format.
:return: String representation of the HSV color proxy.
:rtype: str
Examples::
>>> from hbutils.color import Color
>>>
>>> c = Color('green')
>>> c.hsv
<HSVColorProxy hue: 0.333, saturation: 1.000, value: 0.502>
"""
return '<{cls} hue: {hue}, saturation: {saturation}, value: {value}>'.format(
cls=self.__class__.__name__,
hue='%.3f' % (self.hue,),
saturation='%.3f' % (self.saturation,),
value='%.3f' % (self.value,),
)
_hls_h_mapper = _round_mapper(0.0, 1.0)
_hls_l_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
'Lightness value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_hls_s_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
'Saturation value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
[docs]
class HLSColorProxy:
"""
HLS color component proxy for :class:`Color`.
Instances of this class are returned by :attr:`Color.hls` and provide
readable and writable access to HLS components.
:param this: Original color object.
:type this: Color
:param h: Get-set proxy for hue.
:type h: GetSetProxy
:param l: Get-set proxy for lightness.
:type l: GetSetProxy
:param s: Get-set proxy for saturation.
:type s: GetSetProxy
"""
[docs]
def __init__(self, this: 'Color', h: GetSetProxy, l: GetSetProxy, s: GetSetProxy):
"""
Constructor of :class:`HLSColorProxy`.
:param this: Original color object.
:type this: Color
:param h: Get-set proxy for hue.
:type h: GetSetProxy
:param l: Get-set proxy for lightness.
:type l: GetSetProxy
:param s: Get-set proxy for saturation.
:type s: GetSetProxy
"""
self.__this = this
self.__hp = h
self.__lp = l
self.__sp = s
@property
def hue(self) -> float:
"""
Hue value (within :math:`\\left[0.0, 1.0\\right]`).
.. note::
Setter is available, the change will affect the :class:`Color` object.
:return: Hue component value.
:rtype: float
"""
return self.__hp.get()
@hue.setter
def hue(self, new: float):
"""
Set the hue value.
:param new: New hue value.
:type new: float
"""
self.__hp.set(new)
@property
def lightness(self) -> float:
"""
Lightness value (within :math:`\\left[0.0, 1.0\\right]`).
.. note::
Setter is available, the change will affect the :class:`Color` object.
:return: Lightness component value.
:rtype: float
"""
return self.__lp.get()
@lightness.setter
def lightness(self, new: float):
"""
Set the lightness value.
:param new: New lightness value.
:type new: float
"""
self.__lp.set(new)
@property
def saturation(self) -> float:
"""
Saturation value (within :math:`\\left[0.0, 1.0\\right]`).
.. note::
Setter is available, the change will affect the :class:`Color` object.
:return: Saturation component value.
:rtype: float
"""
return self.__sp.get()
@saturation.setter
def saturation(self, new: float):
"""
Set the saturation value.
:param new: New saturation value.
:type new: float
"""
self.__sp.set(new)
[docs]
def __iter__(self) -> Iterator[float]:
"""
Iterator for this proxy.
:return: Iterator yielding hue, lightness, and saturation.
:rtype: Iterator[float]
Examples::
>>> from hbutils.color import Color
>>>
>>> c = Color('green')
>>> h, l, s = c.hls
>>> print(h, l, s)
0.3333333333333333 0.25098039215686274 1.0
"""
yield self.hue
yield self.lightness
yield self.saturation
[docs]
def __repr__(self) -> str:
"""
Representation format.
:return: String representation of the HLS color proxy.
:rtype: str
Examples::
>>> from hbutils.color import Color
>>>
>>> c = Color('green')
>>> c.hls
<HLSColorProxy hue: 0.333, lightness: 0.251, saturation: 1.000>
"""
return '<{cls} hue: {hue}, lightness: {lightness}, saturation: {saturation}>'.format(
cls=self.__class__.__name__,
hue='%.3f' % (self.hue,),
lightness='%.3f' % (self.lightness,),
saturation='%.3f' % (self.saturation,),
)
_ratio_to_255 = lambda x: int(round(x * 255))
_ratio_to_hex = post_process(lambda x: '%02x' % (x,))(_ratio_to_255)
_hex_to_255 = lambda x: int(x, base=16) if x is not None else None
_hex_to_ratio = post_process(lambda x: x / 255.0 if x is not None else None)(_hex_to_255)
_RGB_COLOR_PATTERN = re.compile(r'^#?([a-fA-F\d]{2})([a-fA-F\d]{2})([a-fA-F\d]{2})([a-fA-F\d]{2}|)$')
@freduce(init=None)
def _ratio_or(a: Optional[float], b: float) -> float:
"""
Return ``b`` if ``a`` is ``None``, otherwise return ``a``.
This utility is used to keep the current value when no new component is
provided.
:param a: First value.
:type a: Optional[float]
:param b: Second value.
:type b: float
:return: ``a`` if not ``None``, otherwise ``b``.
:rtype: float
"""
return b if a is None else a
[docs]
class Color:
"""
Color utility object supporting RGB/HSV/HLS and CSS3 names.
A :class:`Color` instance can be created from CSS3 color names, hexadecimal
strings, RGB tuples, or another :class:`Color` object. Component values
are normalized to floats in ``[0.0, 1.0]``, and optional alpha values are
supported.
Examples::
>>> from hbutils.color import Color
>>>
>>> c = Color('red') # from name
>>> c
<Color red>
>>> str(c) # hex format
'#ff0000'
>>> (c.rgb.red, c.rgb.green, c.rgb.blue) # rgb format
(1.0, 0.0, 0.0)
>>> (c.hls.hue, c.hls.lightness, c.hls.saturation) # hls format
(0.0, 0.5, 1.0)
>>> (c.hsv.hue, c.hsv.saturation, c.hsv.value) # hsv format
(0.0, 1.0, 1.0)
>>> c1 = Color('#56a3f0') # from hex
>>> c1
<Color #56a3f0>
>>> str(c1) # hex format
'#56a3f0'
>>> (c1.rgb.red, c1.rgb.green, c1.rgb.blue) # rgb format
(0.33725490196078434, 0.6392156862745098, 0.9411764705882353)
>>> (c1.hls.hue, c1.hls.lightness, c1.hls.saturation) # hls format
(0.5833333333333334, 0.6392156862745098, 0.8369565217391304)
>>> (c1.hsv.hue, c1.hsv.saturation, c1.hsv.value) # hsv format
(0.5833333333333334, 0.6416666666666666, 0.9411764705882353)
>>> c2 = Color('#56a3f077') # from hex with alpha
>>> c2
<Color #56a3f0, alpha: 0.467>
>>> c2.alpha # alpha value
0.4666666666666667
>>> str(c2) # hex format
'#56a3f077'
"""
[docs]
def __init__(self, c: Union[str, Tuple[float, float, float], 'Color'], alpha: Optional[float] = None):
"""
Constructor of :class:`Color`.
:param c: Color value; may be a hex string, an RGB tuple, or a :class:`Color` object.
:type c: Union[str, Tuple[float, float, float], Color]
:param alpha: Alpha value; ``None`` means no alpha is stored.
:type alpha: Optional[float]
:raises TypeError: If ``c`` is not a supported color type.
:raises ValueError: If ``c`` is a string but not a valid hex or CSS3 color name.
"""
if isinstance(c, tuple):
self.__r, self.__g, self.__b = _r_mapper(c[0]), _g_mapper(c[1]), _b_mapper(c[2])
self.__alpha = _a_mapper(alpha) if alpha is not None else None
elif isinstance(c, Color):
self.__init__(str(c), alpha)
elif isinstance(c, str):
if _RGB_COLOR_PATTERN.fullmatch(c):
_rgb_hex = c
else:
try:
_rgb_hex = _name_to_hex(c)
except ValueError:
raise ValueError("Invalid string color, matching of pattern {pattern} or english name "
"expected but {actual} found.".format(pattern=repr(_RGB_COLOR_PATTERN.pattern),
actual=repr(c), ))
_finding = _RGB_COLOR_PATTERN.findall(_rgb_hex)
_first = _finding[0]
rs, gs, bs, as_ = _first
as_ = None if not as_ else as_
r, g, b, a = map(_hex_to_ratio, (rs, gs, bs, as_))
if alpha is not None:
a = alpha
self.__init__((r, g, b), a)
else:
raise TypeError('Unknown color value - {c}.'.format(c=repr(c)))
@property
def alpha(self) -> Optional[float]:
"""
Alpha value (transparent ratio) within :math:`\\left[0.0, 1.0\\right]`.
.. note::
Setter is available.
:return: Alpha value, or ``None`` if no alpha is set.
:rtype: Optional[float]
"""
return self.__alpha
@alpha.setter
def alpha(self, new: Optional[float]):
"""
Set the alpha value.
:param new: New alpha value.
:type new: Optional[float]
"""
if new is not None:
new = _a_mapper(new)
self.__alpha = new
def __get_rgb(self) -> Tuple[float, float, float]:
"""
Get RGB values.
:return: Tuple of (red, green, blue) values.
:rtype: Tuple[float, float, float]
"""
return self.__r, self.__g, self.__b
def __set_rgb(self, r: Optional[float] = None, g: Optional[float] = None, b: Optional[float] = None) -> None:
"""
Set RGB values.
:param r: Red value, or ``None`` to keep current value.
:type r: Optional[float]
:param g: Green value, or ``None`` to keep current value.
:type g: Optional[float]
:param b: Blue value, or ``None`` to keep current value.
:type b: Optional[float]
"""
self.__r, self.__g, self.__b = map(lambda args: _ratio_or(*args), zip((r, g, b), self.__get_rgb()))
@property
def rgb(self) -> RGBColorProxy:
"""
RGB color proxy.
See :class:`RGBColorProxy` for component access and mutation.
:return: RGB color proxy.
:rtype: RGBColorProxy
"""
return RGBColorProxy(
self,
GetSetProxy(
lambda: self.__r,
lambda x: self.__set_rgb(r=_r_mapper(x)),
),
GetSetProxy(
lambda: self.__g,
lambda x: self.__set_rgb(g=_g_mapper(x)),
),
GetSetProxy(
lambda: self.__b,
lambda x: self.__set_rgb(b=_b_mapper(x)),
),
)
def __get_hsv(self) -> Tuple[float, float, float]:
"""
Get HSV values.
:return: Tuple of (hue, saturation, value) values.
:rtype: Tuple[float, float, float]
"""
return colorsys.rgb_to_hsv(self.__r, self.__g, self.__b)
def __set_hsv(self, h: Optional[float] = None, s: Optional[float] = None, v: Optional[float] = None) -> None:
"""
Set HSV values.
:param h: Hue value, or ``None`` to keep current value.
:type h: Optional[float]
:param s: Saturation value, or ``None`` to keep current value.
:type s: Optional[float]
:param v: Value (brightness) value, or ``None`` to keep current value.
:type v: Optional[float]
"""
h, s, v = map(lambda args: _ratio_or(*args), zip((h, s, v), self.__get_hsv()))
self.__r, self.__g, self.__b = colorsys.hsv_to_rgb(h, s, v)
@property
def hsv(self) -> HSVColorProxy:
"""
HSV color proxy.
See :class:`HSVColorProxy` for component access and mutation.
:return: HSV color proxy.
:rtype: HSVColorProxy
"""
return HSVColorProxy(
self,
GetSetProxy(
lambda: self.__get_hsv()[0],
lambda x: self.__set_hsv(h=_hsv_h_mapper(x)),
),
GetSetProxy(
lambda: self.__get_hsv()[1],
lambda x: self.__set_hsv(s=_hsv_s_mapper(x)),
),
GetSetProxy(
lambda: self.__get_hsv()[2],
lambda x: self.__set_hsv(v=_hsv_v_mapper(x)),
),
)
def __get_hls(self) -> Tuple[float, float, float]:
"""
Get HLS values.
:return: Tuple of (hue, lightness, saturation) values.
:rtype: Tuple[float, float, float]
"""
return colorsys.rgb_to_hls(self.__r, self.__g, self.__b)
def __set_hls(self, h: Optional[float] = None, l_: Optional[float] = None, s: Optional[float] = None) -> None:
"""
Set HLS values.
:param h: Hue value, or ``None`` to keep current value.
:type h: Optional[float]
:param l_: Lightness value, or ``None`` to keep current value.
:type l_: Optional[float]
:param s: Saturation value, or ``None`` to keep current value.
:type s: Optional[float]
"""
h, l, s = map(lambda args: _ratio_or(*args), zip((h, l_, s), self.__get_hls()))
self.__r, self.__g, self.__b = colorsys.hls_to_rgb(h, l, s)
@property
def hls(self) -> HLSColorProxy:
"""
HLS color proxy.
See :class:`HLSColorProxy` for component access and mutation.
:return: HLS color proxy.
:rtype: HLSColorProxy
"""
return HLSColorProxy(
self,
GetSetProxy(
lambda: self.__get_hls()[0],
lambda x: self.__set_hls(h=_hls_h_mapper(x)),
),
GetSetProxy(
lambda: self.__get_hls()[1],
lambda x: self.__set_hls(l_=_hls_l_mapper(x)),
),
GetSetProxy(
lambda: self.__get_hls()[2],
lambda x: self.__set_hls(s=_hls_s_mapper(x)),
),
)
def __get_hex(self, include_alpha: bool) -> str:
"""
Get hexadecimal representation of the color.
:param include_alpha: Whether to include the alpha channel.
:type include_alpha: bool
:return: Hexadecimal color string.
:rtype: str
"""
rs, gs, bs = _ratio_to_hex(self.__r), _ratio_to_hex(self.__g), _ratio_to_hex(self.__b)
as_ = _ratio_to_hex(self.__alpha) if self.__alpha is not None and include_alpha else ''
return '#' + rs + gs + bs + as_
def __get_name(self) -> str:
"""
Get the CSS3 color name if available, otherwise return hex string.
:return: Color name or hex string.
:rtype: str
"""
_hex = self.__get_hex(False).lower()
return _CSS3_NAME_MAPS.get(_hex, _hex)
[docs]
def __repr__(self) -> str:
"""
Get the string representation of the Color object.
:return: String representation.
:rtype: str
"""
if self.__alpha is not None:
return '<{cls} {hex}, alpha: {alpha}>'.format(
cls=self.__class__.__name__,
hex=self.__get_name(),
alpha='%.3f' % (self.__alpha,),
)
else:
return '<{cls} {hex}>'.format(
cls=self.__class__.__name__,
hex=self.__get_name(),
)
[docs]
def __str__(self) -> str:
"""
Hex format of this :class:`Color` object.
:return: Hexadecimal color string (includes alpha when present).
:rtype: str
"""
return self.__get_hex(True)
[docs]
def __getstate__(self) -> Tuple[float, float, float, Optional[float]]:
"""
Dump color as a pickle-compatible tuple.
:return: Tuple containing (r, g, b, alpha).
:rtype: Tuple[float, float, float, Optional[float]]
"""
return self.__r, self.__g, self.__b, self.__alpha
[docs]
def __setstate__(self, v: Tuple[float, float, float, Optional[float]]) -> None:
"""
Load color from a pickle-compatible tuple.
:param v: Tuple containing (r, g, b, alpha).
:type v: Tuple[float, float, float, Optional[float]]
"""
self.__r, self.__g, self.__b, self.__alpha = v
[docs]
def __hash__(self) -> int:
"""
Get hash value of current object.
:return: Hash value of current color.
:rtype: int
"""
return hash(self.__getstate__())
[docs]
def __eq__(self, other: object) -> bool:
"""
Check equality between colors.
:param other: Another object to compare with.
:type other: object
:return: ``True`` if equal, ``False`` otherwise.
:rtype: bool
"""
if other is self:
return True
elif type(other) == type(self):
return other.__getstate__() == self.__getstate__()
else:
return False
[docs]
@classmethod
def from_rgb(cls, r: float, g: float, b: float, alpha: Optional[float] = None) -> 'Color':
"""
Load color from RGB system.
:param r: Red value in :math:`\\left[0, 1\\right]`.
:type r: float
:param g: Green value in :math:`\\left[0, 1\\right]`.
:type g: float
:param b: Blue value in :math:`\\left[0, 1\\right]`.
:type b: float
:param alpha: Alpha value in :math:`\\left[0, 1\\right]`; ``None`` means no alpha.
:type alpha: Optional[float]
:return: Color object.
:rtype: Color
"""
return cls((r, g, b), alpha)
[docs]
@classmethod
def from_hex(cls, hex_: str) -> 'Color':
r"""
Load color from a hexadecimal RGB string.
:param hex\_: Hexadecimal string, may start with ``#``.
:type hex\_: str
:return: Color object.
:rtype: Color
"""
return cls(hex_)
[docs]
@classmethod
def from_hsv(cls, h: float, s: float, v: float, alpha: Optional[float] = None) -> 'Color':
"""
Load color from HSV system.
:param h: Hue value in :math:`\\left[0, 1\\right)`.
:type h: float
:param s: Saturation value in :math:`\\left[0, 1\\right]`.
:type s: float
:param v: Brightness (value) in :math:`\\left[0, 1\\right]`.
:type v: float
:param alpha: Alpha value in :math:`\\left[0, 1\\right]`; ``None`` means no alpha.
:type alpha: Optional[float]
:return: Color object.
:rtype: Color
"""
return cls(colorsys.hsv_to_rgb(h, s, v), alpha)
[docs]
@classmethod
def from_hls(cls, h: float, l: float, s: float, alpha: Optional[float] = None) -> 'Color':
"""
Load color from HLS system.
:param h: Hue value in :math:`\\left[0, 1\\right)`.
:type h: float
:param l: Lightness value in :math:`\\left[0, 1\\right]`.
:type l: float
:param s: Saturation value in :math:`\\left[0, 1\\right]`.
:type s: float
:param alpha: Alpha value in :math:`\\left[0, 1\\right]`; ``None`` means no alpha.
:type alpha: Optional[float]
:return: Color object.
:rtype: Color
"""
return cls(colorsys.hls_to_rgb(h, l, s), alpha)