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)
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.
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.
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.
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.
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.
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
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.
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.
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)
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.
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.
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.
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
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
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.
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.