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

Picks a random RGB color with a specified lightness.

Arguments:
  • lightness: Desired lightness level (0 = dark, 1 = light).
Returns:

A tuple of three floats representing the RGB color.

def isRGBFloat(color) -> bool:
27def isRGBFloat(color) -> bool:
28    """
29    Checks if the given color is an RGB float tuple.
30
31    Args:
32        color: The color to check.
33
34    Returns:
35        True if the color is a tuple of three values, each less than or equal to 1; otherwise, False.
36    """
37    "`(1, 0, 0)` => `True`"
38    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:
41def isRGBInt(color) -> bool:
42    """
43    Checks if the given color is an RGB integer tuple.
44
45    Args:
46        color: The color to check.
47
48    Returns:
49        True if the color is a tuple of three integer values; otherwise, False.
50    """
51    "`(255, 0, 0)` => `True`"
52    return (
53        isinstance(color, tuple)
54        and len(color) == 3
55        and all([isinstance(c, int) for c in color])
56    )

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]:
59def toRGBInt(color) -> tuple[int, int, int]:
60    """
61    Converts a color to an RGB integer tuple.
62
63    Args:
64        color: The color to convert.
65
66    Returns:
67        A tuple of three integers representing the RGB color.
68    """
69    "`(1, 0, 0)` => `(255, 0, 0)`"
70    if isinstance(color, HSL):
71        color = color.asRGB
72    else:
73        color = helpers.expand(color, n=3, format="tuple")
74
75    if isRGBFloat(color):
76        return tuple(round(c * 255) for c in color)
77    else:
78        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:
81def isDark(color, sensitivity=0.5) -> bool:
82    """
83    Determines if a color is considered dark based on luminance and sensitivity.
84
85    Args:
86        color: The color to check.
87        sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
88
89    Returns:
90        True if the color is dark, False otherwise.
91    """
92
93    def _getLuminance(input) -> float:
94        "`0` = dark, `1` = light"
95        r, g, b = toRGBInt(input)
96        return (0.299 * r + 0.587 * g + 0.114 * b) / 255
97
98    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:
101class CMYK:
102    def __init__(self, c=0, m=0, y=0, k=0, a=100):
103        """
104        Initializes a CMYK color.
105
106        Args:
107            c: Cyan value.
108            m: Magenta value.
109            y: Yellow value.
110            k: Black value.
111            a: Alpha value.
112        """
113        self.c = c
114        self.m = m
115        self.y = y
116        self.k = k
117        self.alpha = a
118
119    @property
120    def values(self):
121        """Returns a list of float values for drawBot."""
122        return list(
123            map(lambda v: v / 100, [self.c, self.m, self.y, self.k, self.alpha])
124        )
125
126    def setFill(self):
127        """
128        Sets the fill color in drawBot using this CMYK color.
129
130        Returns:
131            self
132        """
133        drawBot.cmykFill(*self.values)
134        return self
135
136    def setStroke(self):
137        """
138        Sets the stroke color in drawBot using this CMYK color.
139
140        Returns:
141            self
142        """
143        drawBot.cmykStroke(*self.values)
144        return self
CMYK(c=0, m=0, y=0, k=0, a=100)
102    def __init__(self, c=0, m=0, y=0, k=0, a=100):
103        """
104        Initializes a CMYK color.
105
106        Args:
107            c: Cyan value.
108            m: Magenta value.
109            y: Yellow value.
110            k: Black value.
111            a: Alpha value.
112        """
113        self.c = c
114        self.m = m
115        self.y = y
116        self.k = k
117        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
119    @property
120    def values(self):
121        """Returns a list of float values for drawBot."""
122        return list(
123            map(lambda v: v / 100, [self.c, self.m, self.y, self.k, self.alpha])
124        )

Returns a list of float values for drawBot.

def setFill(self):
126    def setFill(self):
127        """
128        Sets the fill color in drawBot using this CMYK color.
129
130        Returns:
131            self
132        """
133        drawBot.cmykFill(*self.values)
134        return self

Sets the fill color in drawBot using this CMYK color.

Returns:

self

def setStroke(self):
136    def setStroke(self):
137        """
138        Sets the stroke color in drawBot using this CMYK color.
139
140        Returns:
141            self
142        """
143        drawBot.cmykStroke(*self.values)
144        return self

Sets the stroke color in drawBot using this CMYK color.

Returns:

self

class HSL:
147class HSL:
148    def __init__(self, h=0, s=100, l=50, a=1):
149        """
150        Initializes an HSL color.
151
152        Args:
153            h: Hue value.
154            s: Saturation value.
155            l: Lightness value.
156            a: Alpha value.
157        """
158        self.h = h
159        self.s = s
160        self.l = l
161        self.a = a
162
163    def __str__(self) -> str:
164        """Returns a string representation of the HSL color."""
165        return f"HSL({self.h}, {self.s}, {self.l}, {self.a})"
166
167    @property
168    def asRGB(self):
169        """Returns the RGB float tuple representation of this HSL color."""
170        hls = self.h / 360, self.l / 100, self.s / 100  # Different order for colorsys
171        return colorsys.hls_to_rgb(*hls)
172
173    @property
174    def asRGBA(self):
175        """Returns the RGBA float tuple representation of this HSL color."""
176        return (*self.asRGB, self.a)
177
178    def setFill(self):
179        """
180        Sets the fill color in drawBot using this HSL color.
181
182        Returns:
183            self
184        """
185        drawBot.fill(*self.asRGBA)
186        return self
187
188    def setStroke(self):
189        """
190        Sets the stroke color in drawBot using this HSL color.
191
192        Returns:
193            self
194        """
195        drawBot.stroke(*self.asRGBA)
196        return self
197
198    def isDark(self, sensitivity=0.5) -> bool:
199        """
200        Determines if this HSL color is considered dark based on luminance and sensitivity.
201
202        Args:
203            sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
204
205        Returns:
206            True if the color is dark, False otherwise.
207        """
208        return isDark(self.asRGB, sensitivity)
HSL(h=0, s=100, l=50, a=1)
148    def __init__(self, h=0, s=100, l=50, a=1):
149        """
150        Initializes an HSL color.
151
152        Args:
153            h: Hue value.
154            s: Saturation value.
155            l: Lightness value.
156            a: Alpha value.
157        """
158        self.h = h
159        self.s = s
160        self.l = l
161        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
167    @property
168    def asRGB(self):
169        """Returns the RGB float tuple representation of this HSL color."""
170        hls = self.h / 360, self.l / 100, self.s / 100  # Different order for colorsys
171        return colorsys.hls_to_rgb(*hls)

Returns the RGB float tuple representation of this HSL color.

asRGBA
173    @property
174    def asRGBA(self):
175        """Returns the RGBA float tuple representation of this HSL color."""
176        return (*self.asRGB, self.a)

Returns the RGBA float tuple representation of this HSL color.

def setFill(self):
178    def setFill(self):
179        """
180        Sets the fill color in drawBot using this HSL color.
181
182        Returns:
183            self
184        """
185        drawBot.fill(*self.asRGBA)
186        return self

Sets the fill color in drawBot using this HSL color.

Returns:

self

def setStroke(self):
188    def setStroke(self):
189        """
190        Sets the stroke color in drawBot using this HSL color.
191
192        Returns:
193            self
194        """
195        drawBot.stroke(*self.asRGBA)
196        return self

Sets the stroke color in drawBot using this HSL color.

Returns:

self

def isDark(self, sensitivity=0.5) -> bool:
198    def isDark(self, sensitivity=0.5) -> bool:
199        """
200        Determines if this HSL color is considered dark based on luminance and sensitivity.
201
202        Args:
203            sensitivity: Threshold for darkness (0 = always dark, 1 = always light).
204
205        Returns:
206            True if the color is dark, False otherwise.
207        """
208        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:
211def mixComplementary(
212    source: HSL,
213    hueShift=360 / 2,
214    hueDirection: Literal["up", "down", "any"] = "any",
215    lightShift=30,
216    sensitivity=0.5,
217) -> HSL:
218    """
219    Mixes a complementary color based on the source HSL color.
220
221    Args:
222        source: The source HSL color.
223        hueShift: Amount to shift the hue.
224        hueDirection: Direction to shift the hue ("up", "down", or "any").
225        lightShift: Amount to shift the lightness.
226        sensitivity: Threshold for darkness (0 = always create darker, 1 = always create lighter).
227
228    Returns:
229        A new HSL color that is complementary to the source.
230    """
231    # Hue
232    hueDirections = dict(up=[1], down=[-1], any=[-1, 1])
233    hueRotate = lambda val: random.choice(hueDirections.get(hueDirection)) * val
234    h = (source.h + hueRotate(hueShift)) % 360
235    # Lightness
236    sourceIsDark = isDark(source.asRGB, sensitivity)
237    lightDelta = lightShift if sourceIsDark else -lightShift
238    l = source.l + lightDelta
239
240    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.