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