Source code for ase_notebook.color

# -*- coding: utf-8 -*-
"""Color Library.

Adapted from: https://github.com/vaab/colour/blob/master/colour.py
vaab/colour is licensed under the BSD 2-Clause "Simplified" License

.. :doctest:

This module defines several color formats that can be converted to one or
another.

**Formats**

- HSL: 3-ple of Hue, Saturation, Lightness all between 0.0 and 1.0
- RGB: 3-ple of Red, Green, Blue all between 0.0 and 1.0
- HEX: string object beginning with '#' and with red, green, blue value.
       This format accept color in 3 or 6 value ex: '#fff' or '#ffffff'
- WEB: string object that defaults to HEX representation or human if possible

Notes
-----
Several function exists to convert from one format to another. But all
function are not written. So the best way is to use the object Color.

Please see the documentation of this object for more information.

.. note:: Some constants are defined for convenience in HSL, RGB, HEX

"""
import re

# Soften inequalities and some rounding issue based on float
FLOAT_ERROR = 0.0000005


RGB_TO_COLOR_NAMES = {
    (0, 0, 0): ["Black"],
    (0, 0, 128): ["Navy", "NavyBlue"],
    (0, 0, 139): ["DarkBlue"],
    (0, 0, 205): ["MediumBlue"],
    (0, 0, 255): ["Blue"],
    (0, 100, 0): ["DarkGreen"],
    (0, 128, 0): ["Green"],
    (0, 139, 139): ["DarkCyan"],
    (0, 191, 255): ["DeepSkyBlue"],
    (0, 206, 209): ["DarkTurquoise"],
    (0, 250, 154): ["MediumSpringGreen"],
    (0, 255, 0): ["Lime"],
    (0, 255, 127): ["SpringGreen"],
    (0, 255, 255): ["Cyan", "Aqua"],
    (25, 25, 112): ["MidnightBlue"],
    (30, 144, 255): ["DodgerBlue"],
    (32, 178, 170): ["LightSeaGreen"],
    (34, 139, 34): ["ForestGreen"],
    (46, 139, 87): ["SeaGreen"],
    (47, 79, 79): ["DarkSlateGray", "DarkSlateGrey"],
    (50, 205, 50): ["LimeGreen"],
    (60, 179, 113): ["MediumSeaGreen"],
    (64, 224, 208): ["Turquoise"],
    (65, 105, 225): ["RoyalBlue"],
    (70, 130, 180): ["SteelBlue"],
    (72, 61, 139): ["DarkSlateBlue"],
    (72, 209, 204): ["MediumTurquoise"],
    (75, 0, 130): ["Indigo"],
    (85, 107, 47): ["DarkOliveGreen"],
    (95, 158, 160): ["CadetBlue"],
    (100, 149, 237): ["CornflowerBlue"],
    (102, 205, 170): ["MediumAquamarine"],
    (105, 105, 105): ["DimGray", "DimGrey"],
    (106, 90, 205): ["SlateBlue"],
    (107, 142, 35): ["OliveDrab"],
    (112, 128, 144): ["SlateGray", "SlateGrey"],
    (119, 136, 153): ["LightSlateGray", "LightSlateGrey"],
    (123, 104, 238): ["MediumSlateBlue"],
    (124, 252, 0): ["LawnGreen"],
    (127, 255, 0): ["Chartreuse"],
    (127, 255, 212): ["Aquamarine"],
    (128, 0, 0): ["Maroon"],
    (128, 0, 128): ["Purple"],
    (128, 128, 0): ["Olive"],
    (128, 128, 128): ["Gray", "Grey"],
    (132, 112, 255): ["LightSlateBlue"],
    (135, 206, 235): ["SkyBlue"],
    (135, 206, 250): ["LightSkyBlue"],
    (138, 43, 226): ["BlueViolet"],
    (139, 0, 0): ["DarkRed"],
    (139, 0, 139): ["DarkMagenta"],
    (139, 69, 19): ["SaddleBrown"],
    (143, 188, 143): ["DarkSeaGreen"],
    (144, 238, 144): ["LightGreen"],
    (147, 112, 219): ["MediumPurple"],
    (148, 0, 211): ["DarkViolet"],
    (152, 251, 152): ["PaleGreen"],
    (153, 50, 204): ["DarkOrchid"],
    (154, 205, 50): ["YellowGreen"],
    (160, 82, 45): ["Sienna"],
    (165, 42, 42): ["Brown"],
    (169, 169, 169): ["DarkGray", "DarkGrey"],
    (173, 216, 230): ["LightBlue"],
    (173, 255, 47): ["GreenYellow"],
    (175, 238, 238): ["PaleTurquoise"],
    (176, 196, 222): ["LightSteelBlue"],
    (176, 224, 230): ["PowderBlue"],
    (178, 34, 34): ["Firebrick"],
    (184, 134, 11): ["DarkGoldenrod"],
    (186, 85, 211): ["MediumOrchid"],
    (188, 143, 143): ["RosyBrown"],
    (189, 183, 107): ["DarkKhaki"],
    (192, 192, 192): ["Silver"],
    (199, 21, 133): ["MediumVioletRed"],
    (205, 92, 92): ["IndianRed"],
    (205, 133, 63): ["Peru"],
    (208, 32, 144): ["VioletRed"],
    (210, 105, 30): ["Chocolate"],
    (210, 180, 140): ["Tan"],
    (211, 211, 211): ["LightGray", "LightGrey"],
    (216, 191, 216): ["Thistle"],
    (218, 112, 214): ["Orchid"],
    (218, 165, 32): ["Goldenrod"],
    (219, 112, 147): ["PaleVioletRed"],
    (220, 20, 60): ["Crimson"],
    (220, 220, 220): ["Gainsboro"],
    (221, 160, 221): ["Plum"],
    (222, 184, 135): ["Burlywood"],
    (224, 255, 255): ["LightCyan"],
    (230, 230, 250): ["Lavender"],
    (233, 150, 122): ["DarkSalmon"],
    (238, 130, 238): ["Violet"],
    (238, 221, 130): ["LightGoldenrod"],
    (238, 232, 170): ["PaleGoldenrod"],
    (240, 128, 128): ["LightCoral"],
    (240, 230, 140): ["Khaki"],
    (240, 248, 255): ["AliceBlue"],
    (240, 255, 240): ["Honeydew"],
    (240, 255, 255): ["Azure"],
    (244, 164, 96): ["SandyBrown"],
    (245, 222, 179): ["Wheat"],
    (245, 245, 220): ["Beige"],
    (245, 245, 245): ["WhiteSmoke"],
    (245, 255, 250): ["MintCream"],
    (248, 248, 255): ["GhostWhite"],
    (250, 128, 114): ["Salmon"],
    (250, 235, 215): ["AntiqueWhite"],
    (250, 240, 230): ["Linen"],
    (250, 250, 210): ["LightGoldenrodYellow"],
    (253, 245, 230): ["OldLace"],
    (255, 0, 0): ["Red"],
    (255, 0, 255): ["Magenta", "Fuchsia"],
    (255, 20, 147): ["DeepPink"],
    (255, 69, 0): ["OrangeRed"],
    (255, 99, 71): ["Tomato"],
    (255, 105, 180): ["HotPink"],
    (255, 127, 80): ["Coral"],
    (255, 140, 0): ["DarkOrange"],
    (255, 160, 122): ["LightSalmon"],
    (255, 165, 0): ["Orange"],
    (255, 182, 193): ["LightPink"],
    (255, 192, 203): ["Pink"],
    (255, 215, 0): ["Gold"],
    (255, 218, 185): ["PeachPuff"],
    (255, 222, 173): ["NavajoWhite"],
    (255, 228, 181): ["Moccasin"],
    (255, 228, 196): ["Bisque"],
    (255, 228, 225): ["MistyRose"],
    (255, 235, 205): ["BlanchedAlmond"],
    (255, 239, 213): ["PapayaWhip"],
    (255, 240, 245): ["LavenderBlush"],
    (255, 245, 238): ["Seashell"],
    (255, 248, 220): ["Cornsilk"],
    (255, 250, 205): ["LemonChiffon"],
    (255, 250, 240): ["FloralWhite"],
    (255, 250, 250): ["Snow"],
    (255, 255, 0): ["Yellow"],
    (255, 255, 224): ["LightYellow"],
    (255, 255, 240): ["Ivory"],
    (255, 255, 255): ["White"],
}

# Building inverse relation
COLOR_NAME_TO_RGB = {
    name.lower(): rgb for rgb, names in RGB_TO_COLOR_NAMES.items() for name in names
}


LONG_HEX_COLOR = re.compile(r"^#[0-9a-fA-F]{6}$")
SHORT_HEX_COLOR = re.compile(r"^#[0-9a-fA-F]{3}$")


[docs]class ContainerHSL: """HSL colors container.""" def __getattr__(self, value): """Get attribute.""" label = value.lower() if label in COLOR_NAME_TO_RGB: return rgb2hsl(tuple(v / 255.0 for v in COLOR_NAME_TO_RGB[label])) raise AttributeError( "%s instance has no attribute %r" % (self.__class__, value) )
HSL = ContainerHSL()
[docs]class ContainerRGB: """RGB colors container. Provides a quick color access. >>> from ase_notebook.color import RGB >>> RGB.WHITE (1.0, 1.0, 1.0) >>> RGB.BLUE (0.0, 0.0, 1.0) >>> RGB.DONOTEXISTS # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: ... has no attribute 'DONOTEXISTS' """ def __getattr__(self, value): """Get attribute.""" return hsl2rgb(getattr(HSL, value))
[docs]class ContainerHEX: """RGB colors container. Provides a quick color access. >>> from ase_notebook.color import HEX >>> HEX.WHITE '#fff' >>> HEX.BLUE '#00f' >>> HEX.DONOTEXISTS # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: ... has no attribute 'DONOTEXISTS' """ def __getattr__(self, value): """Get attribute.""" return rgb2hex(getattr(RGB, value))
RGB = ContainerRGB() HEX = ContainerHEX() # # Conversion function #
[docs]def hsl2rgb(hsl): """Convert HSL representation towards RGB. :param h: Hue, position around the chromatic circle (h=1 equiv h=0) :param s: Saturation, color saturation (0=full gray, 1=full color) :param l: Ligthness, Overhaul lightness (0=full black, 1=full white) :returns: 3-uple for RGB values in float between 0 and 1 Hue, Saturation, Range from Lightness is a float between 0 and 1 Note that Hue can be set to any value but as it is a rotation around the chromatic circle, any value above 1 or below 0 can be expressed by a value between 0 and 1 (Note that h=0 is equiv to h=1). This algorithm came from: http://www.easyrgb.com/index.php?X=MATH&H=19#text19 Here are some quick notion of HSL to RGB conversion: >>> from ase_notebook.color import hsl2rgb With a lightness put at 0, RGB is always rgbblack >>> hsl2rgb((0.0, 0.0, 0.0)) (0.0, 0.0, 0.0) >>> hsl2rgb((0.5, 0.0, 0.0)) (0.0, 0.0, 0.0) >>> hsl2rgb((0.5, 0.5, 0.0)) (0.0, 0.0, 0.0) Same for lightness put at 1, RGB is always rgbwhite >>> hsl2rgb((0.0, 0.0, 1.0)) (1.0, 1.0, 1.0) >>> hsl2rgb((0.5, 0.0, 1.0)) (1.0, 1.0, 1.0) >>> hsl2rgb((0.5, 0.5, 1.0)) (1.0, 1.0, 1.0) With saturation put at 0, the RGB should be equal to Lightness: >>> hsl2rgb((0.0, 0.0, 0.25)) (0.25, 0.25, 0.25) >>> hsl2rgb((0.5, 0.0, 0.5)) (0.5, 0.5, 0.5) >>> hsl2rgb((0.5, 0.0, 0.75)) (0.75, 0.75, 0.75) With saturation put at 1, and lightness put to 0.5, we can find normal full red, green, blue colors: >>> hsl2rgb((0 , 1.0, 0.5)) (1.0, 0.0, 0.0) >>> hsl2rgb((1 , 1.0, 0.5)) (1.0, 0.0, 0.0) >>> hsl2rgb((1.0/3 , 1.0, 0.5)) (0.0, 1.0, 0.0) >>> hsl2rgb((2.0/3 , 1.0, 0.5)) (0.0, 0.0, 1.0) Of course: >>> hsl2rgb((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Saturation must be between 0 and 1. And: >>> hsl2rgb((0.0, 0.0, 1.5)) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Lightness must be between 0 and 1. """ h, s, l = [float(v) for v in hsl] # noqa E741 if not (0.0 - FLOAT_ERROR <= s <= 1.0 + FLOAT_ERROR): raise ValueError("Saturation must be between 0 and 1.") if not (0.0 - FLOAT_ERROR <= l <= 1.0 + FLOAT_ERROR): # noqa E741 raise ValueError("Lightness must be between 0 and 1.") if s == 0: return l, l, l if l < 0.5: v2 = l * (1.0 + s) else: v2 = (l + s) - (s * l) v1 = 2.0 * l - v2 r = _hue2rgb(v1, v2, h + (1.0 / 3)) g = _hue2rgb(v1, v2, h) b = _hue2rgb(v1, v2, h - (1.0 / 3)) return r, g, b
[docs]def rgb2hsl(rgb): """Convert RGB representation towards HSL. :param r: Red amount (float between 0 and 1) :param g: Green amount (float between 0 and 1) :param b: Blue amount (float between 0 and 1) :returns: 3-uple for HSL values in float between 0 and 1 This algorithm came from: http://www.easyrgb.com/index.php?X=MATH&H=19#text19 Here are some quick notion of RGB to HSL conversion: >>> from ase_notebook.color import rgb2hsl Note that if red amount is equal to green and blue, then you should have a gray value (from black to white). >>> rgb2hsl((1.0, 1.0, 1.0)) # doctest: +ELLIPSIS (..., 0.0, 1.0) >>> rgb2hsl((0.5, 0.5, 0.5)) # doctest: +ELLIPSIS (..., 0.0, 0.5) >>> rgb2hsl((0.0, 0.0, 0.0)) # doctest: +ELLIPSIS (..., 0.0, 0.0) If only one color is different from the others, it defines the direct Hue: >>> rgb2hsl((0.5, 0.5, 1.0)) # doctest: +ELLIPSIS (0.66..., 1.0, 0.75) >>> rgb2hsl((0.2, 0.1, 0.1)) # doctest: +ELLIPSIS (0.0, 0.33..., 0.15...) Having only one value set, you can check that: >>> rgb2hsl((1.0, 0.0, 0.0)) (0.0, 1.0, 0.5) >>> rgb2hsl((0.0, 1.0, 0.0)) # doctest: +ELLIPSIS (0.33..., 1.0, 0.5) >>> rgb2hsl((0.0, 0.0, 1.0)) # doctest: +ELLIPSIS (0.66..., 1.0, 0.5) Regression check upon very close values in every component of red, green and blue: >>> rgb2hsl((0.9999999999999999, 1.0, 0.9999999999999994)) (0.0, 0.0, 0.999...) Of course: >>> rgb2hsl((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Green must be between 0 and 1. You provided 2.0. And: >>> rgb2hsl((0.0, 0.0, 1.5)) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Blue must be between 0 and 1. You provided 1.5. """ r, g, b = [float(v) for v in rgb] for name, v in {"Red": r, "Green": g, "Blue": b}.items(): if not (0 - FLOAT_ERROR <= v <= 1 + FLOAT_ERROR): raise ValueError("%s must be between 0 and 1. You provided %r." % (name, v)) vmin = min(r, g, b) # Min. value of RGB vmax = max(r, g, b) # Max. value of RGB diff = vmax - vmin # Delta RGB value vsum = vmin + vmax l = vsum / 2 # noqa E741 if diff < FLOAT_ERROR: # This is a gray, no chroma... return (0.0, 0.0, l) # # Chromatic data... # # Saturation if l < 0.5: s = diff / vsum else: s = diff / (2.0 - vsum) dr = (((vmax - r) / 6) + (diff / 2)) / diff dg = (((vmax - g) / 6) + (diff / 2)) / diff db = (((vmax - b) / 6) + (diff / 2)) / diff if r == vmax: h = db - dg elif g == vmax: h = (1.0 / 3) + dr - db elif b == vmax: h = (2.0 / 3) + dg - dr if h < 0: h += 1 if h > 1: h -= 1 return (h, s, l)
[docs]def _hue2rgb(v1, v2, v_h): """Private helper function (Do not call directly). :param vH: rotation around the chromatic circle (between 0..1) """ while v_h < 0: v_h += 1 while v_h > 1: v_h -= 1 if 6 * v_h < 1: return v1 + (v2 - v1) * 6 * v_h if 2 * v_h < 1: return v2 if 3 * v_h < 2: return v1 + (v2 - v1) * ((2.0 / 3) - v_h) * 6 return v1
[docs]def rgb2hex(rgb, force_long=False): """Transform RGB tuple to hex RGB representation. :param rgb: RGB 3-uple of float between 0 and 1 :returns: 3 hex char or 6 hex char string representation Examples -------- >>> from ase_notebook.color import rgb2hex >>> rgb2hex((0.0,1.0,0.0)) '#0f0' Rounding try to be as natural as possible: >>> rgb2hex((0.0,0.999999,1.0)) '#0ff' And if not possible, the 6 hex char representation is used: >>> rgb2hex((0.23,1.0,1.0)) '#3bffff' >>> rgb2hex((0.0,0.999999,1.0), force_long=True) '#00ffff' """ hx = "".join(["%02x" % int(c * 255 + 0.5 - FLOAT_ERROR) for c in rgb]) if not force_long and hx[0::2] == hx[1::2]: hx = "".join(hx[0::2]) return "#%s" % hx
[docs]def hex2rgb(str_rgb): """Transform hex RGB representation to RGB tuple. :param str_rgb: 3 hex char or 6 hex char string representation :returns: RGB 3-uple of float between 0 and 1 >>> from ase_notebook.color import hex2rgb >>> hex2rgb('#00ff00') (0.0, 1.0, 0.0) >>> hex2rgb('#0f0') (0.0, 1.0, 0.0) >>> hex2rgb('#aaa') # doctest: +ELLIPSIS (0.66..., 0.66..., 0.66...) >>> hex2rgb('#aa') # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Invalid value '#aa' provided for rgb color. """ try: rgb = str_rgb[1:] if len(rgb) == 6: r, g, b = rgb[0:2], rgb[2:4], rgb[4:6] elif len(rgb) == 3: r, g, b = rgb[0] * 2, rgb[1] * 2, rgb[2] * 2 else: raise ValueError() except Exception: raise ValueError("Invalid value %r provided for rgb color." % str_rgb) return tuple(float(int(v, 16)) / 255 for v in (r, g, b))
[docs]def hex2web(hex_color): """Convert HEX representation to WEB. :param rgb: 3 hex char or 6 hex char string representation :returns: web string representation (human readable if possible) WEB representation uses X11 rgb.txt to define conversion between RGB and english color names. Examples -------- >>> from ase_notebook.color import hex2web >>> hex2web('#ff0000') 'red' >>> hex2web('#aaaaaa') '#aaa' >>> hex2web('#abc') '#abc' >>> hex2web('#acacac') '#acacac' """ dec_rgb = tuple(int(v * 255) for v in hex2rgb(hex_color)) if dec_rgb in RGB_TO_COLOR_NAMES: # take the first one color_name = RGB_TO_COLOR_NAMES[dec_rgb][0] # Enforce full lowercase for single worded color name. return ( color_name if len(re.sub(r"[^A-Z]", "", color_name)) > 1 else color_name.lower() ) # Hex format is verified by hex2rgb function. And should be 3 or 6 digit if len(hex_color) == 7: if ( hex_color[1] == hex_color[2] and hex_color[3] == hex_color[4] and hex_color[5] == hex_color[6] ): return "#" + hex_color[1] + hex_color[3] + hex_color[5] return hex_color
[docs]def web2hex(web, force_long=False): """Convert WEB representation to HEX. :param rgb: web string representation (human readable if possible) :returns: 3 hex char or 6 hex char string representation WEB representation uses X11 rgb.txt to define conversion between RGB and english color names. Examples -------- >>> from ase_notebook.color import web2hex >>> web2hex('red') '#f00' >>> web2hex('#aaa') '#aaa' >>> web2hex('#foo') # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: '#foo' is not in web format. Need 3 or 6 hex digit. >>> web2hex('#aaa', force_long=True) '#aaaaaa' >>> web2hex('#aaaaaa') '#aaaaaa' >>> web2hex('#aaaa') # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: '#aaaa' is not in web format. Need 3 or 6 hex digit. >>> web2hex('pinky') # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: 'pinky' is not a recognized color. And color names are case insensitive: >>> Color('RED') <Color red> """ if web.startswith("#"): if LONG_HEX_COLOR.match(web) or (not force_long and SHORT_HEX_COLOR.match(web)): return web.lower() elif SHORT_HEX_COLOR.match(web) and force_long: return "#" + "".join([("%s" % (t,)) * 2 for t in web[1:]]) raise AttributeError("%r is not in web format. Need 3 or 6 hex digit." % web) web = web.lower() if web not in COLOR_NAME_TO_RGB: raise ValueError("%r is not a recognized color." % web) # convert dec to hex: return rgb2hex([float(int(v)) / 255 for v in COLOR_NAME_TO_RGB[web]], force_long)
# Missing functions conversion hsl2hex = lambda x: rgb2hex(hsl2rgb(x)) # noqa: E731 hex2hsl = lambda x: rgb2hsl(hex2rgb(x)) # noqa: E731 rgb2web = lambda x: hex2web(rgb2hex(x)) # noqa: E731 web2rgb = lambda x: hex2rgb(web2hex(x)) # noqa: E731 web2hsl = lambda x: rgb2hsl(web2rgb(x)) # noqa: E731 hsl2web = lambda x: rgb2web(hsl2rgb(x)) # noqa: E731 rgb_equivalence = lambda c1, c2: c1.hex_l == c2.hex_l # noqa: E731 hsl_equivalence = lambda c1, c2: c1._hsl == c2._hsl # noqa: E731
[docs]class Color(object): """Abstraction of a color object. Color object keeps information of a color. It can input/output to different format (HSL, RGB, HEX, WEB) and their partial representation. >>> from ase_notebook.color import Color, HSL >>> b = Color() >>> b.hsl = HSL.BLUE Examples -------- >>> b.hue # doctest: +ELLIPSIS 0.66... >>> b.saturation 1.0 >>> b.luminance 0.5 >>> b.red 0.0 >>> b.blue 1.0 >>> b.green 0.0 >>> b.rgb (0.0, 0.0, 1.0) >>> b.hsl # doctest: +ELLIPSIS (0.66..., 1.0, 0.5) >>> b.hex '#00f' **Change values** Let's change Hue toward red tint: >>> b.hue = 0.0 >>> b.hex '#f00' >>> b.hue = 2.0/3 >>> b.hex '#00f' In the other way round: >>> b.hex = '#f00' >>> b.hsl (0.0, 1.0, 0.5) Long hex can be accessed directly: >>> b.hex_l = '#123456' >>> b.hex_l '#123456' >>> b.hex '#123456' >>> b.hex_l = '#ff0000' >>> b.hex_l '#ff0000' >>> b.hex '#f00' **Convenience** >>> c = Color('blue') >>> c <Color blue> >>> c.hue = 0 >>> c <Color red> >>> c.saturation = 0.0 >>> c.hsl # doctest: +ELLIPSIS (..., 0.0, 0.5) >>> c.rgb (0.5, 0.5, 0.5) >>> c.hex '#7f7f7f' >>> c <Color #7f7f7f> >>> c.luminance = 0.0 >>> c <Color black> >>> c.hex '#000' >>> c.green = 1.0 >>> c.blue = 1.0 >>> c.hex '#0ff' >>> c <Color cyan> >>> c = Color('blue', luminance=0.75) >>> c <Color #7f7fff> >>> c = Color('red', red=0.5) >>> c <Color #7f0000> >>> print(c) #7f0000 **Recursive init** To support blind conversion of web strings (or already converted object), the Color object supports instantiation with another Color object. >>> Color(Color(Color('red'))) <Color red> **Equality support** Default equality is RGB hex comparison: >>> Color('red') == Color('blue') False >>> Color('red') == Color('red') True >>> Color('red') != Color('blue') True >>> Color('red') != Color('red') False But this can be changed: >>> saturation_equality = lambda c1, c2: c1.luminance == c2.luminance >>> Color('red', equality=saturation_equality) == Color('blue') True """ _hsl = None # internal representation def __init__(self, color=None, equality=None, **kwargs): """Initialise class.""" if isinstance(color, Color): self.web = color.web else: self.web = color if color else "black" self.equality = equality or rgb_equivalence for k, v in kwargs.items(): setattr(self, k, v) # # Get # @property def hsl(self): """Get value.""" return tuple(self._hsl) @property # noqa: A003 def hex(self): """Get value.""" return rgb2hex(self.rgb) @property def hex_l(self): """Get value.""" return rgb2hex(self.rgb, force_long=True) @property def rgb(self): """Get value.""" return hsl2rgb(self.hsl) @property def hue(self): """Get value.""" return self.hsl[0] @property def saturation(self): """Get value.""" return self.hsl[1] @property def luminance(self): """Get value.""" return self.hsl[2] @property def red(self): """Get value.""" return self.rgb[0] @property def green(self): """Get value.""" return self.rgb[1] @property def blue(self): """Get value.""" return self.rgb[2] @property def web(self): """Get value.""" return hex2web(self.hex) # # Set # @hsl.setter def hsl(self, value): """Set value.""" self._hsl = list(value) @rgb.setter def rgb(self, value): """Set value.""" self.hsl = rgb2hsl(value) @hue.setter def hue(self, value): """Set value.""" self._hsl[0] = value @saturation.setter def saturation(self, value): """Set value.""" assert 0 <= value <= 1, "Saturation must be between 0 and 1." self._hsl[1] = value @luminance.setter def luminance(self, value): """Set value.""" assert 0 <= value <= 1, "Lightness must be between 0 and 1." self._hsl[2] = value @red.setter def red(self, value): """Set value.""" _, g, b = self.rgb self.rgb = (value, g, b) @green.setter def green(self, value): """Set value.""" r, _, b = self.rgb self.rgb = (r, value, b) @blue.setter def blue(self, value): """Set value.""" r, g, _ = self.rgb self.rgb = (r, g, value) @hex.setter # noqa: A003 def hex(self, value): """Set value.""" self.rgb = hex2rgb(value) @hex_l.setter def hex_l(self, value): """Set value.""" self.rgb = hex2rgb(value) @web.setter def web(self, value): """Set value.""" self.hex = web2hex(value) # # Convenience # def __str__(self): """Get string.""" return "%s" % self.web def __repr__(self): """Get representation.""" return "<Color %s>" % self.web def __eq__(self, other): """Test equality.""" if isinstance(other, Color): return self.equality(self, other) return NotImplemented
[docs]def lighten_webcolor(web_color, fraction): """Move a web color towards white by a fraction, or black if the fraction is negative.""" if fraction == 0: return web_color import numpy as np rgb = np.array(web2rgb(web_color)) if fraction > 0: white = np.array([1.0, 1.0, 1.0]) rgb = rgb + (white - rgb) * fraction else: black = np.array([0.0, 0.0, 0.0]) rgb = rgb + (black - rgb) * abs(fraction) return rgb2web(rgb)