lib.color

  1import drawBot
  2import colorsys
  3import random
  4from typing import Literal
  5from lib import helpers
  6from loguru import logger
  7from icecream import ic
  8
  9
 10def isRGBFloat(color) -> bool:
 11    """
 12    Checks if the given color is an RGB float tuple.
 13
 14    Args:
 15        color: The color to check.
 16
 17    Returns:
 18        True if the color is a tuple of three values, each less than or equal to 1; otherwise, False.
 19    """
 20    "`(1, 0, 0)` => `True`"
 21    return isinstance(color, tuple) and len(color) == 3 and all([c <= 1 for c in color])
 22
 23
 24def isRGBInt(color) -> bool:
 25    """
 26    Checks if the given color is an RGB integer tuple.
 27
 28    Args:
 29        color: The color to check.
 30
 31    Returns:
 32        True if the color is a tuple of three integer values; otherwise, False.
 33    """
 34    "`(255, 0, 0)` => `True`"
 35    return (
 36        isinstance(color, tuple)
 37        and len(color) == 3
 38        and all([isinstance(c, int) for c in color])
 39    )
 40
 41
 42def toRGBInt(color) -> tuple[int, int, int]:
 43    """
 44    Converts a color to an RGB integer tuple.
 45
 46    Args:
 47        color: The color to convert.
 48
 49    Returns:
 50        A tuple of three integers representing the RGB color.
 51    """
 52    "`(1, 0, 0)` => `(255, 0, 0)`"
 53    if isinstance(color, HSL):
 54        color = color.asRGB
 55    else:
 56        color = helpers.expand(color, n=3, format="tuple")
 57
 58    if isRGBFloat(color):
 59        return tuple(round(c * 255) for c in color)
 60    else:
 61        logger.warning("toRGBInt failed for {}", color)
 62
 63
 64def isDark(color, sensitivity=0.5) -> bool:
 65    """
 66    Determines if a color is considered dark based on luminance and sensitivity.
 67
 68    Args:
 69        color: The color to check.
 70        sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
 71
 72    Returns:
 73        True if the color is dark, False otherwise.
 74    """
 75
 76    def _getLuminance(input) -> float:
 77        "`0` = dark, `1` = light"
 78        r, g, b = toRGBInt(input)
 79        return (0.299 * r + 0.587 * g + 0.114 * b) / 255
 80
 81    return _getLuminance(color) < sensitivity
 82
 83
 84class CMYK:
 85    def __init__(self, c=0, m=0, y=0, k=0, a=100):
 86        """
 87        Initializes a CMYK color.
 88
 89        Args:
 90            c: Cyan value.
 91            m: Magenta value.
 92            y: Yellow value.
 93            k: Black value.
 94            a: Alpha value.
 95        """
 96        self.c = c
 97        self.m = m
 98        self.y = y
 99        self.k = k
100        self.alpha = a
101
102    @property
103    def values(self):
104        """Returns a list of float values for drawBot."""
105        return list(
106            map(lambda v: v / 100, [self.c, self.m, self.y, self.k, self.alpha])
107        )
108
109    def setFill(self):
110        """
111        Sets the fill color in drawBot using this CMYK color.
112
113        Returns:
114            self
115        """
116        drawBot.cmykFill(*self.values)
117        return self
118
119    def setStroke(self):
120        """
121        Sets the stroke color in drawBot using this CMYK color.
122
123        Returns:
124            self
125        """
126        drawBot.cmykStroke(*self.values)
127        return self
128
129
130class HSL:
131    def __init__(self, h=0, s=100, l=50, a=1):
132        """
133        Initializes an HSL color.
134
135        Args:
136            h: Hue value.
137            s: Saturation value.
138            l: Lightness value.
139            a: Alpha value.
140        """
141        self.h = h
142        self.s = s
143        self.l = l
144        self.a = a
145
146    def __str__(self) -> str:
147        """Returns a string representation of the HSL color."""
148        return f"HSL({self.h}, {self.s}, {self.l}, {self.a})"
149
150    @property
151    def asRGB(self):
152        """Returns the RGB float tuple representation of this HSL color."""
153        hls = self.h / 360, self.l / 100, self.s / 100  # Different order for colorsys
154        return colorsys.hls_to_rgb(*hls)
155
156    @property
157    def asRGBA(self):
158        """Returns the RGBA float tuple representation of this HSL color."""
159        return (*self.asRGB, self.a)
160
161    def setFill(self):
162        """
163        Sets the fill color in drawBot using this HSL color.
164
165        Returns:
166            self
167        """
168        drawBot.fill(*self.asRGBA)
169        return self
170
171    def setStroke(self):
172        """
173        Sets the stroke color in drawBot using this HSL color.
174
175        Returns:
176            self
177        """
178        drawBot.stroke(*self.asRGBA)
179        return self
180
181    def isDark(self, sensitivity=0.5) -> bool:
182        """
183        Determines if this HSL color is considered dark based on luminance and sensitivity.
184
185        Args:
186            sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
187
188        Returns:
189            True if the color is dark, False otherwise.
190        """
191        return isDark(self.asRGB, sensitivity)
192
193
194def mixComplementary(
195    source: HSL,
196    hueShift=360 / 2,
197    hueDirection: Literal["up", "down", "any"] = "any",
198    lightShift=30,
199    sensitivity=0.5,
200) -> HSL:
201    """
202    Mixes a complementary color based on the source HSL color.
203
204    Args:
205        source: The source HSL color.
206        hueShift: Amount to shift the hue.
207        hueDirection: Direction to shift the hue ("up", "down", or "any").
208        lightShift: Amount to shift the lightness.
209        sensitivity: Threshold for darkness (0 = always create darker, 1 = always create lighter).
210
211    Returns:
212        A new HSL color that is complementary to the source.
213    """
214    # Hue
215    hueDirections = dict(up=[1], down=[-1], any=[-1, 1])
216    hueRotate = lambda val: random.choice(hueDirections.get(hueDirection)) * val
217    h = (source.h + hueRotate(hueShift)) % 360
218    # Lightness
219    sourceIsDark = isDark(source.asRGB, sensitivity)
220    lightDelta = lightShift if sourceIsDark else -lightShift
221    l = source.l + lightDelta
222
223    return HSL(h, 100, l)
def isRGBFloat(color) -> bool:
11def isRGBFloat(color) -> bool:
12    """
13    Checks if the given color is an RGB float tuple.
14
15    Args:
16        color: The color to check.
17
18    Returns:
19        True if the color is a tuple of three values, each less than or equal to 1; otherwise, False.
20    """
21    "`(1, 0, 0)` => `True`"
22    return isinstance(color, tuple) and len(color) == 3 and all([c <= 1 for c in color])

Checks if the given color is an RGB float tuple.

Arguments:
  • color: The color to check.
Returns:

True if the color is a tuple of three values, each less than or equal to 1; otherwise, False.

def isRGBInt(color) -> bool:
25def isRGBInt(color) -> bool:
26    """
27    Checks if the given color is an RGB integer tuple.
28
29    Args:
30        color: The color to check.
31
32    Returns:
33        True if the color is a tuple of three integer values; otherwise, False.
34    """
35    "`(255, 0, 0)` => `True`"
36    return (
37        isinstance(color, tuple)
38        and len(color) == 3
39        and all([isinstance(c, int) for c in color])
40    )

Checks if the given color is an RGB integer tuple.

Arguments:
  • color: The color to check.
Returns:

True if the color is a tuple of three integer values; otherwise, False.

def toRGBInt(color) -> tuple[int, int, int]:
43def toRGBInt(color) -> tuple[int, int, int]:
44    """
45    Converts a color to an RGB integer tuple.
46
47    Args:
48        color: The color to convert.
49
50    Returns:
51        A tuple of three integers representing the RGB color.
52    """
53    "`(1, 0, 0)` => `(255, 0, 0)`"
54    if isinstance(color, HSL):
55        color = color.asRGB
56    else:
57        color = helpers.expand(color, n=3, format="tuple")
58
59    if isRGBFloat(color):
60        return tuple(round(c * 255) for c in color)
61    else:
62        logger.warning("toRGBInt failed for {}", color)

Converts a color to an RGB integer tuple.

Arguments:
  • color: The color to convert.
Returns:

A tuple of three integers representing the RGB color.

def isDark(color, sensitivity=0.5) -> bool:
65def isDark(color, sensitivity=0.5) -> bool:
66    """
67    Determines if a color is considered dark based on luminance and sensitivity.
68
69    Args:
70        color: The color to check.
71        sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
72
73    Returns:
74        True if the color is dark, False otherwise.
75    """
76
77    def _getLuminance(input) -> float:
78        "`0` = dark, `1` = light"
79        r, g, b = toRGBInt(input)
80        return (0.299 * r + 0.587 * g + 0.114 * b) / 255
81
82    return _getLuminance(color) < sensitivity

Determines if a color is considered dark based on luminance and sensitivity.

Arguments:
  • color: The color to check.
  • sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
Returns:

True if the color is dark, False otherwise.

class CMYK:
 85class CMYK:
 86    def __init__(self, c=0, m=0, y=0, k=0, a=100):
 87        """
 88        Initializes a CMYK color.
 89
 90        Args:
 91            c: Cyan value.
 92            m: Magenta value.
 93            y: Yellow value.
 94            k: Black value.
 95            a: Alpha value.
 96        """
 97        self.c = c
 98        self.m = m
 99        self.y = y
100        self.k = k
101        self.alpha = a
102
103    @property
104    def values(self):
105        """Returns a list of float values for drawBot."""
106        return list(
107            map(lambda v: v / 100, [self.c, self.m, self.y, self.k, self.alpha])
108        )
109
110    def setFill(self):
111        """
112        Sets the fill color in drawBot using this CMYK color.
113
114        Returns:
115            self
116        """
117        drawBot.cmykFill(*self.values)
118        return self
119
120    def setStroke(self):
121        """
122        Sets the stroke color in drawBot using this CMYK color.
123
124        Returns:
125            self
126        """
127        drawBot.cmykStroke(*self.values)
128        return self
CMYK(c=0, m=0, y=0, k=0, a=100)
 86    def __init__(self, c=0, m=0, y=0, k=0, a=100):
 87        """
 88        Initializes a CMYK color.
 89
 90        Args:
 91            c: Cyan value.
 92            m: Magenta value.
 93            y: Yellow value.
 94            k: Black value.
 95            a: Alpha value.
 96        """
 97        self.c = c
 98        self.m = m
 99        self.y = y
100        self.k = k
101        self.alpha = a

Initializes a CMYK color.

Arguments:
  • c: Cyan value.
  • m: Magenta value.
  • y: Yellow value.
  • k: Black value.
  • a: Alpha value.
c
m
y
k
alpha
values
103    @property
104    def values(self):
105        """Returns a list of float values for drawBot."""
106        return list(
107            map(lambda v: v / 100, [self.c, self.m, self.y, self.k, self.alpha])
108        )

Returns a list of float values for drawBot.

def setFill(self):
110    def setFill(self):
111        """
112        Sets the fill color in drawBot using this CMYK color.
113
114        Returns:
115            self
116        """
117        drawBot.cmykFill(*self.values)
118        return self

Sets the fill color in drawBot using this CMYK color.

Returns:

self

def setStroke(self):
120    def setStroke(self):
121        """
122        Sets the stroke color in drawBot using this CMYK color.
123
124        Returns:
125            self
126        """
127        drawBot.cmykStroke(*self.values)
128        return self

Sets the stroke color in drawBot using this CMYK color.

Returns:

self

class HSL:
131class HSL:
132    def __init__(self, h=0, s=100, l=50, a=1):
133        """
134        Initializes an HSL color.
135
136        Args:
137            h: Hue value.
138            s: Saturation value.
139            l: Lightness value.
140            a: Alpha value.
141        """
142        self.h = h
143        self.s = s
144        self.l = l
145        self.a = a
146
147    def __str__(self) -> str:
148        """Returns a string representation of the HSL color."""
149        return f"HSL({self.h}, {self.s}, {self.l}, {self.a})"
150
151    @property
152    def asRGB(self):
153        """Returns the RGB float tuple representation of this HSL color."""
154        hls = self.h / 360, self.l / 100, self.s / 100  # Different order for colorsys
155        return colorsys.hls_to_rgb(*hls)
156
157    @property
158    def asRGBA(self):
159        """Returns the RGBA float tuple representation of this HSL color."""
160        return (*self.asRGB, self.a)
161
162    def setFill(self):
163        """
164        Sets the fill color in drawBot using this HSL color.
165
166        Returns:
167            self
168        """
169        drawBot.fill(*self.asRGBA)
170        return self
171
172    def setStroke(self):
173        """
174        Sets the stroke color in drawBot using this HSL color.
175
176        Returns:
177            self
178        """
179        drawBot.stroke(*self.asRGBA)
180        return self
181
182    def isDark(self, sensitivity=0.5) -> bool:
183        """
184        Determines if this HSL color is considered dark based on luminance and sensitivity.
185
186        Args:
187            sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
188
189        Returns:
190            True if the color is dark, False otherwise.
191        """
192        return isDark(self.asRGB, sensitivity)
HSL(h=0, s=100, l=50, a=1)
132    def __init__(self, h=0, s=100, l=50, a=1):
133        """
134        Initializes an HSL color.
135
136        Args:
137            h: Hue value.
138            s: Saturation value.
139            l: Lightness value.
140            a: Alpha value.
141        """
142        self.h = h
143        self.s = s
144        self.l = l
145        self.a = a

Initializes an HSL color.

Arguments:
  • h: Hue value.
  • s: Saturation value.
  • l: Lightness value.
  • a: Alpha value.
h
s
l
a
asRGB
151    @property
152    def asRGB(self):
153        """Returns the RGB float tuple representation of this HSL color."""
154        hls = self.h / 360, self.l / 100, self.s / 100  # Different order for colorsys
155        return colorsys.hls_to_rgb(*hls)

Returns the RGB float tuple representation of this HSL color.

asRGBA
157    @property
158    def asRGBA(self):
159        """Returns the RGBA float tuple representation of this HSL color."""
160        return (*self.asRGB, self.a)

Returns the RGBA float tuple representation of this HSL color.

def setFill(self):
162    def setFill(self):
163        """
164        Sets the fill color in drawBot using this HSL color.
165
166        Returns:
167            self
168        """
169        drawBot.fill(*self.asRGBA)
170        return self

Sets the fill color in drawBot using this HSL color.

Returns:

self

def setStroke(self):
172    def setStroke(self):
173        """
174        Sets the stroke color in drawBot using this HSL color.
175
176        Returns:
177            self
178        """
179        drawBot.stroke(*self.asRGBA)
180        return self

Sets the stroke color in drawBot using this HSL color.

Returns:

self

def isDark(self, sensitivity=0.5) -> bool:
182    def isDark(self, sensitivity=0.5) -> bool:
183        """
184        Determines if this HSL color is considered dark based on luminance and sensitivity.
185
186        Args:
187            sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
188
189        Returns:
190            True if the color is dark, False otherwise.
191        """
192        return isDark(self.asRGB, sensitivity)

Determines if this HSL color is considered dark based on luminance and sensitivity.

Arguments:
  • sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
Returns:

True if the color is dark, False otherwise.

def mixComplementary( source: HSL, hueShift=180.0, hueDirection: Literal['up', 'down', 'any'] = 'any', lightShift=30, sensitivity=0.5) -> HSL:
195def mixComplementary(
196    source: HSL,
197    hueShift=360 / 2,
198    hueDirection: Literal["up", "down", "any"] = "any",
199    lightShift=30,
200    sensitivity=0.5,
201) -> HSL:
202    """
203    Mixes a complementary color based on the source HSL color.
204
205    Args:
206        source: The source HSL color.
207        hueShift: Amount to shift the hue.
208        hueDirection: Direction to shift the hue ("up", "down", or "any").
209        lightShift: Amount to shift the lightness.
210        sensitivity: Threshold for darkness (0 = always create darker, 1 = always create lighter).
211
212    Returns:
213        A new HSL color that is complementary to the source.
214    """
215    # Hue
216    hueDirections = dict(up=[1], down=[-1], any=[-1, 1])
217    hueRotate = lambda val: random.choice(hueDirections.get(hueDirection)) * val
218    h = (source.h + hueRotate(hueShift)) % 360
219    # Lightness
220    sourceIsDark = isDark(source.asRGB, sensitivity)
221    lightDelta = lightShift if sourceIsDark else -lightShift
222    l = source.l + lightDelta
223
224    return HSL(h, 100, l)

Mixes a complementary color based on the source HSL color.

Arguments:
  • source: The source HSL color.
  • hueShift: Amount to shift the hue.
  • hueDirection: Direction to shift the hue ("up", "down", or "any").
  • lightShift: Amount to shift the lightness.
  • sensitivity: Threshold for darkness (0 = always create darker, 1 = always create lighter).
Returns:

A new HSL color that is complementary to the source.