lib.color
1import drawBot 2import colorsys 3import random 4from abc import ABC, abstractmethod 5from dataclasses import dataclass 6from typing import Any, Literal 7from lib import helpers 8from loguru import logger 9from icecream import ic 10 11RawRGB = tuple[float, float, float] 12RawRGBA = tuple[float, float, float, float] 13RawHSLA = tuple[float, float, float, float] 14 15 16def isHex(color: Any) -> bool: 17 """Checks if the given color is a hex string. 18 19 Example: `"#ff0000"` => `True` 20 """ 21 return isinstance(color, str) and color.startswith("#") and len(color) in (4, 7) 22 23 24def isRGBFloat(color: Any) -> bool: 25 """ 26 Checks if the given color is an RGB float tuple. 27 28 Returns: 29 True if the color is a tuple of three values, each less than or equal to 1; otherwise, False. 30 31 Example: `(1, 0, 0)` => `True` 32 """ 33 return isinstance(color, tuple) and len(color) == 3 and all([c <= 1 for c in color]) 34 35 36def isRGBAFloat(color: Any) -> bool: 37 """ 38 Checks if the given color is an RGBA float tuple. 39 40 Returns: 41 True if the color is a tuple of four values, each less than or equal to 1; otherwise, False. 42 43 Example: `(1, 0, 0, 1)` => `True` 44 """ 45 return isinstance(color, tuple) and len(color) == 4 and all([c <= 1 for c in color]) 46 47 48def isRGBInt(color: Any) -> bool: 49 """ 50 Checks if the given color is an RGB integer tuple. 51 52 Args: 53 color: The color to check. 54 55 Returns: 56 True if the color is a tuple of three integer values; otherwise, False. 57 58 Example: `(255, 0, 0)` => `True` 59 """ 60 return ( 61 isinstance(color, tuple) 62 and len(color) == 3 63 and all([0 <= c <= 255 for c in color]) 64 ) 65 66 67def isRGBAInt(color: Any) -> bool: 68 """ 69 Checks if the given color is an RGBA integer tuple. 70 """ 71 try: 72 r, g, b, a = color 73 return isRGBInt((r, g, b)) and 0 <= a <= 1 74 except (TypeError, ValueError): 75 return False 76 77 78def isRGBLike(color: Any) -> bool: 79 """Checks if the given color is RGB-like (either RGB or RGBA, in float or int format).""" 80 return ( 81 isinstance(color, tuple) 82 and len(color) in (3, 4) 83 and all(isinstance(c, (int, float)) for c in color) 84 ) 85 86 87def normalizeHex(hexColor: str) -> str: 88 """Normalizes a hex color string to the 6-digit format.""" 89 hexColor = hexColor.lstrip("#").upper() 90 if len(hexColor) not in (3, 6): 91 raise ValueError(f"Invalid hex color format: {hexColor}") 92 if len(hexColor) == 3: 93 hexColor = "".join([c * 2 for c in hexColor]) 94 return f"#{hexColor}" 95 96 97def normalizeRGB(color: Any) -> RawRGB: 98 """Normalizes an RGB color to a float tuple.""" 99 if isRGBFloat(color): 100 return color 101 elif isRGBAFloat(color): 102 return color[:3] 103 elif isRGBInt(color): 104 return tuple(c / 255 for c in color) 105 elif isRGBAInt(color): 106 return tuple(c / 255 for c in color[:3]) 107 else: 108 raise ValueError(f"Unsupported color format: {color}") 109 110 111def normalizeRGBA(color: Any) -> RawRGBA: 112 try: 113 r, g, b, *_ = color 114 RGB = normalizeRGB((r, g, b)) 115 return *RGB, color[3] if len(color) > 3 else 1 116 except (TypeError, ValueError): 117 raise ValueError(f"Unsupported color format: {color}") 118 119 120def fromHexToRGBA(hexColor: str, alpha=1) -> RawRGBA: 121 """Creates an RGBA color from a hex string.""" 122 hexColor = normalizeHex(hexColor) 123 hexColor = hexColor.lstrip("#") 124 r, g, b = tuple(int(hexColor[i : i + 2], 16) for i in (0, 2, 4)) 125 return r / 255, g / 255, b / 255, alpha 126 127 128def fromRGBAtoHSLA(rgba: RawRGBA) -> RawHSLA: 129 """Converts an RGBA color to HSLA format.""" 130 r, g, b, a = rgba 131 h, l, s = colorsys.rgb_to_hls(r, g, b) 132 return h * 360, s * 100, l * 100, a 133 134 135def fromHexToHSLA(hexColor: str, alpha=1) -> RawHSLA: 136 """Creates an HSLA color from a hex string.""" 137 hexColor = normalizeHex(hexColor) 138 r, g, b, a = fromHexToRGBA(hexColor, alpha) 139 return fromRGBAtoHSLA((r, g, b, a)) 140 141 142def toHSLA(color: Any) -> RawHSLA: 143 """Converts a color to HSLA format.""" 144 if isinstance(color, KHSLSwatch): 145 return color.h, color.s, color.l, color.alpha 146 elif isinstance(color, KRGBSwatch): 147 return fromRGBAtoHSLA(color.asRGBA) 148 elif isHex(color): 149 return fromHexToHSLA(color) 150 elif isRGBLike(color): 151 return fromRGBAtoHSLA(normalizeRGBA(color)) 152 else: 153 raise ValueError(f"Unsupported color format: {color}") 154 155 156def pickRandomRGB(lightness=0.5) -> RawRGB: 157 """ 158 Picks a random RGB color with a specified lightness. 159 160 Args: 161 lightness: Desired lightness level (0 = dark, 1 = light). 162 163 Returns: 164 A tuple of three floats representing the RGB color. 165 """ 166 h = random.random() # Hue 167 l = lightness # Lightness 168 s = 1 # Saturation 169 return colorsys.hls_to_rgb(h, l, s) 170 171 172def toRGBInt(color: Any) -> tuple[int, int, int]: 173 """ 174 Converts a color to an RGB integer tuple. 175 176 Args: 177 color: The color to convert. 178 179 Returns: 180 A tuple of three integers representing the RGB color. 181 """ 182 "`(1, 0, 0)` => `(255, 0, 0)`" 183 if isinstance(color, KHSLSwatch): 184 color = color.asRGB 185 else: 186 color = helpers.expand(color, n=3, format="tuple") 187 188 if isRGBFloat(color): 189 return tuple(round(c * 255) for c in color) 190 else: 191 logger.warning("toRGBInt failed for {}", color) 192 raise ValueError(f"Unsupported color format: {color}") 193 194 195def toRGBAFloat(color: Any) -> RawRGBA: 196 """ 197 Converts a color to an RGBA float tuple. 198 199 Args: 200 color: The color to convert. 201 202 Returns: 203 A tuple of four floats representing the RGBA color. 204 """ 205 "`#ff0000` => `(1, 0, 0, 1)`" 206 if isinstance(color, tuple) and len(color) == 4: 207 return color 208 elif isRGBFloat(color): 209 return (*color, 1) 210 elif isRGBInt(color): 211 return tuple(c / 255 for c in color) + (1,) 212 elif isHex(color): 213 return fromHexToRGBA(color) 214 elif isinstance(color, KRGBSwatch): 215 return color.asRGBA 216 elif isinstance(color, KCMYKSwatch): 217 return color.asRGBA 218 elif isinstance(color, KHSLSwatch): 219 return color.asRGBA 220 else: 221 raise ValueError(f"Unsupported color format: {color}") 222 223 224def isDark(color: Any, sensitivity=0.5) -> bool: 225 """ 226 Determines if a color is considered dark based on luminance and sensitivity. 227 228 Args: 229 color: The color to check. 230 sensitivity: Threshold for darkness (0 = always dark, 1 = always light). 231 232 Returns: 233 True if the color is dark, False otherwise. 234 """ 235 236 def _getLuminance(input) -> float: 237 "`0` = dark, `1` = light" 238 r, g, b = toRGBInt(input) 239 return (0.299 * r + 0.587 * g + 0.114 * b) / 255 240 241 return _getLuminance(color) < sensitivity 242 243 244def _nameColorFromHSL(h: float, s: float, l: float) -> str: 245 if l == 100: 246 return "white" 247 elif l == 0: 248 return "black" 249 250 h = h % 360 251 252 if s == 0: 253 hueName = "gray" 254 elif h < 10: 255 hueName = "red" 256 elif h < 20: 257 hueName = "vermilion" 258 elif h < 35: 259 hueName = "orange" 260 elif h < 50: 261 hueName = "amber" 262 elif h < 65: 263 hueName = "yellow" 264 elif h < 85: 265 hueName = "chartreuse" 266 elif h < 105: 267 hueName = "lime" 268 elif h < 140: 269 hueName = "green" 270 elif h < 160: 271 hueName = "emerald" 272 elif h < 180: 273 hueName = "teal" 274 elif h < 200: 275 hueName = "cyan" 276 elif h < 220: 277 hueName = "azure" 278 elif h < 245: 279 hueName = "blue" 280 elif h < 265: 281 hueName = "indigo" 282 elif h < 290: 283 hueName = "violet" 284 elif h < 315: 285 hueName = "purple" 286 elif h < 335: 287 hueName = "magenta" 288 elif h < 350: 289 hueName = "rose" 290 else: 291 hueName = "crimson" 292 293 if l >= 80 and s >= 90: 294 tone = "bright" 295 elif l >= 80 and 0 < s <= 30: 296 tone = "pale" 297 elif l <= 40 and 0 < s <= 20: 298 tone = "muted" 299 elif l <= 20 and s > 0: 300 tone = "deep" 301 elif l <= 20: 302 tone = "darkest" 303 elif l >= 90: 304 tone = "lightest" 305 elif l >= 70: 306 tone = "light" 307 elif l <= 30: 308 tone = "dark" 309 else: 310 tone = "" 311 312 return f"{tone}-{hueName}" if tone else hueName 313 314 315class KColorSwatch(ABC): 316 @property 317 @abstractmethod 318 def asRGBA(self) -> RawRGBA: 319 """Return color as normalized RGBA floats.""" 320 321 @abstractmethod 322 def setFill(self): 323 """Set drawBot fill using this swatch.""" 324 325 @abstractmethod 326 def setStroke(self): 327 """Set drawBot stroke using this swatch.""" 328 329 @abstractmethod 330 def withAlpha(self, alpha: float): 331 """Return a copy with a new alpha value.""" 332 333 @property 334 def asHSL(self) -> "KHSLSwatch": 335 return KHSLSwatch(*fromRGBAtoHSLA(self.asRGBA)) 336 337 @property 338 def asHex(self) -> str: 339 r, g, b = toRGBInt(self.asRGBA) 340 return f"#{r:02X}{g:02X}{b:02X}" 341 342 def isDark(self, sensitivity=0.5) -> bool: 343 return isDark(self.asRGBA, sensitivity) 344 345 def nameColor(self) -> str: 346 h, s, l, _ = fromRGBAtoHSLA(self.asRGBA) 347 return _nameColorFromHSL(h, s, l) 348 349 350@dataclass 351class KCMYKSwatch(KColorSwatch): 352 c: float = 0 353 m: float = 0 354 y: float = 0 355 k: float = 0 356 alpha: float = 1 357 358 @staticmethod 359 def fromAny(color: Any) -> "KCMYKSwatch": 360 if isinstance(color, KCMYKSwatch): 361 return color 362 if isinstance(color, KRGBSwatch): 363 return KCMYKSwatch.fromRGB(*color.asRGB, alpha=color.a) 364 if isinstance(color, KHSLSwatch): 365 return KCMYKSwatch.fromRGB(*color.asRGB, alpha=color.alpha) 366 if isHex(color) or isRGBLike(color): 367 r, g, b, a = toRGBAFloat(color) 368 return KCMYKSwatch.fromRGB(r, g, b, alpha=a) 369 raise ValueError(f"Unsupported color format: {color}") 370 371 @staticmethod 372 def fromRGB(r: float, g: float, b: float, alpha=1) -> "KCMYKSwatch": 373 r, g, b = normalizeRGB((r, g, b)) 374 k = 1 - max(r, g, b) 375 if k >= 1: 376 return KCMYKSwatch(0, 0, 0, 100, alpha) 377 c = (1 - r - k) / (1 - k) 378 m = (1 - g - k) / (1 - k) 379 y = (1 - b - k) / (1 - k) 380 return KCMYKSwatch(c * 100, m * 100, y * 100, k * 100, alpha) 381 382 @property 383 def asCMYK(self): 384 """Returns a list of float values for drawBot.""" 385 return list(map(lambda v: v / 100, [self.c, self.m, self.y, self.k])) 386 387 @property 388 def asCMYKA(self): 389 """Returns a list of float values for drawBot.""" 390 return [*self.asCMYK, self.alpha] 391 392 @property 393 def asRGB(self) -> RawRGB: 394 """Returns an RGB float tuple using a fast CMYK-to-RGB conversion.""" 395 c, m, y, k = self.asCMYK 396 return ( 397 (1 - c) * (1 - k), 398 (1 - m) * (1 - k), 399 (1 - y) * (1 - k), 400 ) 401 402 @property 403 def asRGBA(self) -> RawRGBA: 404 """Returns an RGBA float tuple using a fast CMYK-to-RGB conversion.""" 405 return (*self.asRGB, self.alpha) 406 407 def setFill(self): 408 """ 409 Sets the fill color in drawBot using this CMYK color. 410 411 Returns: 412 self 413 """ 414 drawBot.cmykFill(*self.asCMYKA) 415 return self 416 417 def setStroke(self): 418 """ 419 Sets the stroke color in drawBot using this CMYK color. 420 421 Returns: 422 self 423 """ 424 drawBot.cmykStroke(*self.asCMYKA) 425 return self 426 427 def withAlpha(self, alpha: float): 428 return KCMYKSwatch(self.c, self.m, self.y, self.k, alpha) 429 430 431@dataclass 432class KRGBSwatch(KColorSwatch): 433 r: float = 0 434 g: float = 0 435 b: float = 0 436 a: float = 1 437 438 @staticmethod 439 def fromAny(color: Any) -> "KRGBSwatch": 440 """Creates an RGBA color from any supported color format.""" 441 if isRGBLike(color): 442 return KRGBSwatch(*normalizeRGBA(color)) 443 elif isHex(color): 444 return KRGBSwatch(*fromHexToRGBA(color)) 445 elif isinstance(color, KHSLSwatch): 446 return KRGBSwatch(*color.asRGBA) 447 elif isinstance(color, KCMYKSwatch): 448 return KRGBSwatch(*color.asRGBA) 449 else: 450 raise ValueError(f"Unsupported color format: {color}") 451 452 @staticmethod 453 def fromHex(hexColor: str, alpha=1) -> "KRGBSwatch": 454 return KRGBSwatch(*fromHexToRGBA(hexColor, alpha)) 455 456 @staticmethod 457 def fromHSL(h: float, s=100, l=50, alpha=1) -> "KRGBSwatch": 458 return KRGBSwatch(*KHSLSwatch(h, s, l, alpha).asRGBA) 459 460 @staticmethod 461 def fromInt(r: int, g: int, b: int, a=1) -> "KRGBSwatch": 462 return KRGBSwatch(r / 255, g / 255, b / 255, a) 463 464 @property 465 def asRGB(self) -> RawRGB: 466 """Returns the RGB float tuple representation of this color.""" 467 return self.r, self.g, self.b 468 469 @property 470 def asRGBA(self) -> RawRGBA: 471 """Returns the RGBA float tuple representation of this color.""" 472 return self.r, self.g, self.b, self.a 473 474 def setFill(self): 475 """Sets the fill color in drawBot using this RGBA color.""" 476 drawBot.fill(*self.asRGBA) 477 return self 478 479 def setStroke(self): 480 """Sets the stroke color in drawBot using this RGBA color.""" 481 drawBot.stroke(*self.asRGBA) 482 return self 483 484 def withAlpha(self, alpha: float): 485 return KRGBSwatch(self.r, self.g, self.b, alpha) 486 487 488@dataclass 489class KHSLSwatch(KColorSwatch): 490 h: float = 0 491 s: float = 100 492 l: float = 50 493 alpha: float = 1 494 495 def __str__(self) -> str: 496 """Returns a string representation of the HSL color.""" 497 return f"KHSLSwatch({self.h}, {self.s}, {self.l}, {self.alpha})" 498 499 @staticmethod 500 def fromAny(color: Any) -> "KHSLSwatch": 501 """Creates an HSL color from any supported color format.""" 502 return KHSLSwatch(*toHSLA(color)) 503 504 @property 505 def asHSLA(self) -> RawHSLA: 506 """Returns HSLA float tuple values for drawBot.""" 507 return self.h, self.s, self.l, self.alpha 508 509 @property 510 def asRGB(self) -> RawRGB: 511 """Returns the RGB float tuple representation of this HSL color.""" 512 hls = self.h / 360, self.l / 100, self.s / 100 # Different order for colorsys 513 return colorsys.hls_to_rgb(*hls) 514 515 @property 516 def asRGBA(self) -> RawRGBA: 517 """Returns the RGBA float tuple representation of this HSL color.""" 518 return (*self.asRGB, self.alpha) 519 520 def setFill(self): 521 """ 522 Sets the fill color in drawBot using this HSL color. 523 524 Returns: 525 self 526 """ 527 drawBot.fill(*self.asRGBA) 528 return self 529 530 def setStroke(self): 531 """ 532 Sets the stroke color in drawBot using this HSL color. 533 534 Returns: 535 self 536 """ 537 drawBot.stroke(*self.asRGBA) 538 return self 539 540 def withAlpha(self, alpha: float): 541 return KHSLSwatch(self.h, self.s, self.l, alpha) 542 543 def isDark(self, sensitivity=0.5) -> bool: 544 """ 545 Determines if this HSL color is considered dark based on luminance and sensitivity. 546 547 Args: 548 sensitivity: Threshold for darkness (0 = always dark, 1 = always light). 549 550 Returns: 551 True if the color is dark, False otherwise. 552 """ 553 return isDark(self.asRGB, sensitivity) 554 555 def nameColor(self) -> str: 556 """Return a human-readable label in kebab-case. 557 558 Example outputs: `light-red`, `dark-violet`, `very-light-rose`. 559 """ 560 return _nameColorFromHSL(self.h, self.s, self.l) 561 562 563def mixComplementary( 564 source: KHSLSwatch, 565 hueShift=360 / 2, 566 hueDirection: Literal["up", "down", "any"] = "any", 567 lightShift=30, 568 sensitivity=0.5, 569) -> KHSLSwatch: 570 """ 571 Mixes a complementary color based on the source HSL color. 572 573 Args: 574 source: The source HSL color. 575 hueShift: Amount to shift the hue. 576 hueDirection: Direction to shift the hue ("up", "down", or "any"). 577 lightShift: Amount to shift the lightness. 578 sensitivity: Threshold for darkness (0 = always create darker, 1 = always create lighter). 579 580 Returns: 581 A new HSL color that is complementary to the source. 582 """ 583 # Hue 584 hueDirections = dict(up=[1], down=[-1], any=[-1, 1]) 585 hueRotate = lambda val: random.choice(hueDirections.get(hueDirection)) * val 586 h = (source.h + hueRotate(hueShift)) % 360 587 # Lightness 588 sourceIsDark = isDark(source.asRGB, sensitivity) 589 lightDelta = lightShift if sourceIsDark else -lightShift 590 l = source.l + lightDelta 591 592 return KHSLSwatch(h, 100, l) 593 594 595RGBA = KRGBSwatch 596CMYK = KCMYKSwatch 597HSL = KHSLSwatch
17def isHex(color: Any) -> bool: 18 """Checks if the given color is a hex string. 19 20 Example: `"#ff0000"` => `True` 21 """ 22 return isinstance(color, str) and color.startswith("#") and len(color) in (4, 7)
Checks if the given color is a hex string.
Example: "#ff0000" => True
25def isRGBFloat(color: Any) -> bool: 26 """ 27 Checks if the given color is an RGB float tuple. 28 29 Returns: 30 True if the color is a tuple of three values, each less than or equal to 1; otherwise, False. 31 32 Example: `(1, 0, 0)` => `True` 33 """ 34 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.
Returns:
True if the color is a tuple of three values, each less than or equal to 1; otherwise, False.
Example: (1, 0, 0) => True
37def isRGBAFloat(color: Any) -> bool: 38 """ 39 Checks if the given color is an RGBA float tuple. 40 41 Returns: 42 True if the color is a tuple of four values, each less than or equal to 1; otherwise, False. 43 44 Example: `(1, 0, 0, 1)` => `True` 45 """ 46 return isinstance(color, tuple) and len(color) == 4 and all([c <= 1 for c in color])
Checks if the given color is an RGBA float tuple.
Returns:
True if the color is a tuple of four values, each less than or equal to 1; otherwise, False.
Example: (1, 0, 0, 1) => True
49def isRGBInt(color: Any) -> bool: 50 """ 51 Checks if the given color is an RGB integer tuple. 52 53 Args: 54 color: The color to check. 55 56 Returns: 57 True if the color is a tuple of three integer values; otherwise, False. 58 59 Example: `(255, 0, 0)` => `True` 60 """ 61 return ( 62 isinstance(color, tuple) 63 and len(color) == 3 64 and all([0 <= c <= 255 for c in color]) 65 )
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.
Example: (255, 0, 0) => True
68def isRGBAInt(color: Any) -> bool: 69 """ 70 Checks if the given color is an RGBA integer tuple. 71 """ 72 try: 73 r, g, b, a = color 74 return isRGBInt((r, g, b)) and 0 <= a <= 1 75 except (TypeError, ValueError): 76 return False
Checks if the given color is an RGBA integer tuple.
79def isRGBLike(color: Any) -> bool: 80 """Checks if the given color is RGB-like (either RGB or RGBA, in float or int format).""" 81 return ( 82 isinstance(color, tuple) 83 and len(color) in (3, 4) 84 and all(isinstance(c, (int, float)) for c in color) 85 )
Checks if the given color is RGB-like (either RGB or RGBA, in float or int format).
88def normalizeHex(hexColor: str) -> str: 89 """Normalizes a hex color string to the 6-digit format.""" 90 hexColor = hexColor.lstrip("#").upper() 91 if len(hexColor) not in (3, 6): 92 raise ValueError(f"Invalid hex color format: {hexColor}") 93 if len(hexColor) == 3: 94 hexColor = "".join([c * 2 for c in hexColor]) 95 return f"#{hexColor}"
Normalizes a hex color string to the 6-digit format.
98def normalizeRGB(color: Any) -> RawRGB: 99 """Normalizes an RGB color to a float tuple.""" 100 if isRGBFloat(color): 101 return color 102 elif isRGBAFloat(color): 103 return color[:3] 104 elif isRGBInt(color): 105 return tuple(c / 255 for c in color) 106 elif isRGBAInt(color): 107 return tuple(c / 255 for c in color[:3]) 108 else: 109 raise ValueError(f"Unsupported color format: {color}")
Normalizes an RGB color to a float tuple.
121def fromHexToRGBA(hexColor: str, alpha=1) -> RawRGBA: 122 """Creates an RGBA color from a hex string.""" 123 hexColor = normalizeHex(hexColor) 124 hexColor = hexColor.lstrip("#") 125 r, g, b = tuple(int(hexColor[i : i + 2], 16) for i in (0, 2, 4)) 126 return r / 255, g / 255, b / 255, alpha
Creates an RGBA color from a hex string.
129def fromRGBAtoHSLA(rgba: RawRGBA) -> RawHSLA: 130 """Converts an RGBA color to HSLA format.""" 131 r, g, b, a = rgba 132 h, l, s = colorsys.rgb_to_hls(r, g, b) 133 return h * 360, s * 100, l * 100, a
Converts an RGBA color to HSLA format.
136def fromHexToHSLA(hexColor: str, alpha=1) -> RawHSLA: 137 """Creates an HSLA color from a hex string.""" 138 hexColor = normalizeHex(hexColor) 139 r, g, b, a = fromHexToRGBA(hexColor, alpha) 140 return fromRGBAtoHSLA((r, g, b, a))
Creates an HSLA color from a hex string.
143def toHSLA(color: Any) -> RawHSLA: 144 """Converts a color to HSLA format.""" 145 if isinstance(color, KHSLSwatch): 146 return color.h, color.s, color.l, color.alpha 147 elif isinstance(color, KRGBSwatch): 148 return fromRGBAtoHSLA(color.asRGBA) 149 elif isHex(color): 150 return fromHexToHSLA(color) 151 elif isRGBLike(color): 152 return fromRGBAtoHSLA(normalizeRGBA(color)) 153 else: 154 raise ValueError(f"Unsupported color format: {color}")
Converts a color to HSLA format.
157def pickRandomRGB(lightness=0.5) -> RawRGB: 158 """ 159 Picks a random RGB color with a specified lightness. 160 161 Args: 162 lightness: Desired lightness level (0 = dark, 1 = light). 163 164 Returns: 165 A tuple of three floats representing the RGB color. 166 """ 167 h = random.random() # Hue 168 l = lightness # Lightness 169 s = 1 # Saturation 170 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.
173def toRGBInt(color: Any) -> tuple[int, int, int]: 174 """ 175 Converts a color to an RGB integer tuple. 176 177 Args: 178 color: The color to convert. 179 180 Returns: 181 A tuple of three integers representing the RGB color. 182 """ 183 "`(1, 0, 0)` => `(255, 0, 0)`" 184 if isinstance(color, KHSLSwatch): 185 color = color.asRGB 186 else: 187 color = helpers.expand(color, n=3, format="tuple") 188 189 if isRGBFloat(color): 190 return tuple(round(c * 255) for c in color) 191 else: 192 logger.warning("toRGBInt failed for {}", color) 193 raise ValueError(f"Unsupported color format: {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.
196def toRGBAFloat(color: Any) -> RawRGBA: 197 """ 198 Converts a color to an RGBA float tuple. 199 200 Args: 201 color: The color to convert. 202 203 Returns: 204 A tuple of four floats representing the RGBA color. 205 """ 206 "`#ff0000` => `(1, 0, 0, 1)`" 207 if isinstance(color, tuple) and len(color) == 4: 208 return color 209 elif isRGBFloat(color): 210 return (*color, 1) 211 elif isRGBInt(color): 212 return tuple(c / 255 for c in color) + (1,) 213 elif isHex(color): 214 return fromHexToRGBA(color) 215 elif isinstance(color, KRGBSwatch): 216 return color.asRGBA 217 elif isinstance(color, KCMYKSwatch): 218 return color.asRGBA 219 elif isinstance(color, KHSLSwatch): 220 return color.asRGBA 221 else: 222 raise ValueError(f"Unsupported color format: {color}")
Converts a color to an RGBA float tuple.
Arguments:
- color: The color to convert.
Returns:
A tuple of four floats representing the RGBA color.
225def isDark(color: Any, sensitivity=0.5) -> bool: 226 """ 227 Determines if a color is considered dark based on luminance and sensitivity. 228 229 Args: 230 color: The color to check. 231 sensitivity: Threshold for darkness (0 = always dark, 1 = always light). 232 233 Returns: 234 True if the color is dark, False otherwise. 235 """ 236 237 def _getLuminance(input) -> float: 238 "`0` = dark, `1` = light" 239 r, g, b = toRGBInt(input) 240 return (0.299 * r + 0.587 * g + 0.114 * b) / 255 241 242 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.
316class KColorSwatch(ABC): 317 @property 318 @abstractmethod 319 def asRGBA(self) -> RawRGBA: 320 """Return color as normalized RGBA floats.""" 321 322 @abstractmethod 323 def setFill(self): 324 """Set drawBot fill using this swatch.""" 325 326 @abstractmethod 327 def setStroke(self): 328 """Set drawBot stroke using this swatch.""" 329 330 @abstractmethod 331 def withAlpha(self, alpha: float): 332 """Return a copy with a new alpha value.""" 333 334 @property 335 def asHSL(self) -> "KHSLSwatch": 336 return KHSLSwatch(*fromRGBAtoHSLA(self.asRGBA)) 337 338 @property 339 def asHex(self) -> str: 340 r, g, b = toRGBInt(self.asRGBA) 341 return f"#{r:02X}{g:02X}{b:02X}" 342 343 def isDark(self, sensitivity=0.5) -> bool: 344 return isDark(self.asRGBA, sensitivity) 345 346 def nameColor(self) -> str: 347 h, s, l, _ = fromRGBAtoHSLA(self.asRGBA) 348 return _nameColorFromHSL(h, s, l)
Helper class that provides a standard way to create an ABC using inheritance.
317 @property 318 @abstractmethod 319 def asRGBA(self) -> RawRGBA: 320 """Return color as normalized RGBA floats."""
Return color as normalized RGBA floats.
330 @abstractmethod 331 def withAlpha(self, alpha: float): 332 """Return a copy with a new alpha value."""
Return a copy with a new alpha value.
351@dataclass 352class KCMYKSwatch(KColorSwatch): 353 c: float = 0 354 m: float = 0 355 y: float = 0 356 k: float = 0 357 alpha: float = 1 358 359 @staticmethod 360 def fromAny(color: Any) -> "KCMYKSwatch": 361 if isinstance(color, KCMYKSwatch): 362 return color 363 if isinstance(color, KRGBSwatch): 364 return KCMYKSwatch.fromRGB(*color.asRGB, alpha=color.a) 365 if isinstance(color, KHSLSwatch): 366 return KCMYKSwatch.fromRGB(*color.asRGB, alpha=color.alpha) 367 if isHex(color) or isRGBLike(color): 368 r, g, b, a = toRGBAFloat(color) 369 return KCMYKSwatch.fromRGB(r, g, b, alpha=a) 370 raise ValueError(f"Unsupported color format: {color}") 371 372 @staticmethod 373 def fromRGB(r: float, g: float, b: float, alpha=1) -> "KCMYKSwatch": 374 r, g, b = normalizeRGB((r, g, b)) 375 k = 1 - max(r, g, b) 376 if k >= 1: 377 return KCMYKSwatch(0, 0, 0, 100, alpha) 378 c = (1 - r - k) / (1 - k) 379 m = (1 - g - k) / (1 - k) 380 y = (1 - b - k) / (1 - k) 381 return KCMYKSwatch(c * 100, m * 100, y * 100, k * 100, alpha) 382 383 @property 384 def asCMYK(self): 385 """Returns a list of float values for drawBot.""" 386 return list(map(lambda v: v / 100, [self.c, self.m, self.y, self.k])) 387 388 @property 389 def asCMYKA(self): 390 """Returns a list of float values for drawBot.""" 391 return [*self.asCMYK, self.alpha] 392 393 @property 394 def asRGB(self) -> RawRGB: 395 """Returns an RGB float tuple using a fast CMYK-to-RGB conversion.""" 396 c, m, y, k = self.asCMYK 397 return ( 398 (1 - c) * (1 - k), 399 (1 - m) * (1 - k), 400 (1 - y) * (1 - k), 401 ) 402 403 @property 404 def asRGBA(self) -> RawRGBA: 405 """Returns an RGBA float tuple using a fast CMYK-to-RGB conversion.""" 406 return (*self.asRGB, self.alpha) 407 408 def setFill(self): 409 """ 410 Sets the fill color in drawBot using this CMYK color. 411 412 Returns: 413 self 414 """ 415 drawBot.cmykFill(*self.asCMYKA) 416 return self 417 418 def setStroke(self): 419 """ 420 Sets the stroke color in drawBot using this CMYK color. 421 422 Returns: 423 self 424 """ 425 drawBot.cmykStroke(*self.asCMYKA) 426 return self 427 428 def withAlpha(self, alpha: float): 429 return KCMYKSwatch(self.c, self.m, self.y, self.k, alpha)
359 @staticmethod 360 def fromAny(color: Any) -> "KCMYKSwatch": 361 if isinstance(color, KCMYKSwatch): 362 return color 363 if isinstance(color, KRGBSwatch): 364 return KCMYKSwatch.fromRGB(*color.asRGB, alpha=color.a) 365 if isinstance(color, KHSLSwatch): 366 return KCMYKSwatch.fromRGB(*color.asRGB, alpha=color.alpha) 367 if isHex(color) or isRGBLike(color): 368 r, g, b, a = toRGBAFloat(color) 369 return KCMYKSwatch.fromRGB(r, g, b, alpha=a) 370 raise ValueError(f"Unsupported color format: {color}")
372 @staticmethod 373 def fromRGB(r: float, g: float, b: float, alpha=1) -> "KCMYKSwatch": 374 r, g, b = normalizeRGB((r, g, b)) 375 k = 1 - max(r, g, b) 376 if k >= 1: 377 return KCMYKSwatch(0, 0, 0, 100, alpha) 378 c = (1 - r - k) / (1 - k) 379 m = (1 - g - k) / (1 - k) 380 y = (1 - b - k) / (1 - k) 381 return KCMYKSwatch(c * 100, m * 100, y * 100, k * 100, alpha)
383 @property 384 def asCMYK(self): 385 """Returns a list of float values for drawBot.""" 386 return list(map(lambda v: v / 100, [self.c, self.m, self.y, self.k]))
Returns a list of float values for drawBot.
388 @property 389 def asCMYKA(self): 390 """Returns a list of float values for drawBot.""" 391 return [*self.asCMYK, self.alpha]
Returns a list of float values for drawBot.
393 @property 394 def asRGB(self) -> RawRGB: 395 """Returns an RGB float tuple using a fast CMYK-to-RGB conversion.""" 396 c, m, y, k = self.asCMYK 397 return ( 398 (1 - c) * (1 - k), 399 (1 - m) * (1 - k), 400 (1 - y) * (1 - k), 401 )
Returns an RGB float tuple using a fast CMYK-to-RGB conversion.
403 @property 404 def asRGBA(self) -> RawRGBA: 405 """Returns an RGBA float tuple using a fast CMYK-to-RGB conversion.""" 406 return (*self.asRGB, self.alpha)
Returns an RGBA float tuple using a fast CMYK-to-RGB conversion.
408 def setFill(self): 409 """ 410 Sets the fill color in drawBot using this CMYK color. 411 412 Returns: 413 self 414 """ 415 drawBot.cmykFill(*self.asCMYKA) 416 return self
Sets the fill color in drawBot using this CMYK color.
Returns:
self
418 def setStroke(self): 419 """ 420 Sets the stroke color in drawBot using this CMYK color. 421 422 Returns: 423 self 424 """ 425 drawBot.cmykStroke(*self.asCMYKA) 426 return self
Sets the stroke color in drawBot using this CMYK color.
Returns:
self
428 def withAlpha(self, alpha: float): 429 return KCMYKSwatch(self.c, self.m, self.y, self.k, alpha)
Return a copy with a new alpha value.
Inherited Members
432@dataclass 433class KRGBSwatch(KColorSwatch): 434 r: float = 0 435 g: float = 0 436 b: float = 0 437 a: float = 1 438 439 @staticmethod 440 def fromAny(color: Any) -> "KRGBSwatch": 441 """Creates an RGBA color from any supported color format.""" 442 if isRGBLike(color): 443 return KRGBSwatch(*normalizeRGBA(color)) 444 elif isHex(color): 445 return KRGBSwatch(*fromHexToRGBA(color)) 446 elif isinstance(color, KHSLSwatch): 447 return KRGBSwatch(*color.asRGBA) 448 elif isinstance(color, KCMYKSwatch): 449 return KRGBSwatch(*color.asRGBA) 450 else: 451 raise ValueError(f"Unsupported color format: {color}") 452 453 @staticmethod 454 def fromHex(hexColor: str, alpha=1) -> "KRGBSwatch": 455 return KRGBSwatch(*fromHexToRGBA(hexColor, alpha)) 456 457 @staticmethod 458 def fromHSL(h: float, s=100, l=50, alpha=1) -> "KRGBSwatch": 459 return KRGBSwatch(*KHSLSwatch(h, s, l, alpha).asRGBA) 460 461 @staticmethod 462 def fromInt(r: int, g: int, b: int, a=1) -> "KRGBSwatch": 463 return KRGBSwatch(r / 255, g / 255, b / 255, a) 464 465 @property 466 def asRGB(self) -> RawRGB: 467 """Returns the RGB float tuple representation of this color.""" 468 return self.r, self.g, self.b 469 470 @property 471 def asRGBA(self) -> RawRGBA: 472 """Returns the RGBA float tuple representation of this color.""" 473 return self.r, self.g, self.b, self.a 474 475 def setFill(self): 476 """Sets the fill color in drawBot using this RGBA color.""" 477 drawBot.fill(*self.asRGBA) 478 return self 479 480 def setStroke(self): 481 """Sets the stroke color in drawBot using this RGBA color.""" 482 drawBot.stroke(*self.asRGBA) 483 return self 484 485 def withAlpha(self, alpha: float): 486 return KRGBSwatch(self.r, self.g, self.b, alpha)
439 @staticmethod 440 def fromAny(color: Any) -> "KRGBSwatch": 441 """Creates an RGBA color from any supported color format.""" 442 if isRGBLike(color): 443 return KRGBSwatch(*normalizeRGBA(color)) 444 elif isHex(color): 445 return KRGBSwatch(*fromHexToRGBA(color)) 446 elif isinstance(color, KHSLSwatch): 447 return KRGBSwatch(*color.asRGBA) 448 elif isinstance(color, KCMYKSwatch): 449 return KRGBSwatch(*color.asRGBA) 450 else: 451 raise ValueError(f"Unsupported color format: {color}")
Creates an RGBA color from any supported color format.
465 @property 466 def asRGB(self) -> RawRGB: 467 """Returns the RGB float tuple representation of this color.""" 468 return self.r, self.g, self.b
Returns the RGB float tuple representation of this color.
470 @property 471 def asRGBA(self) -> RawRGBA: 472 """Returns the RGBA float tuple representation of this color.""" 473 return self.r, self.g, self.b, self.a
Returns the RGBA float tuple representation of this color.
475 def setFill(self): 476 """Sets the fill color in drawBot using this RGBA color.""" 477 drawBot.fill(*self.asRGBA) 478 return self
Sets the fill color in drawBot using this RGBA color.
480 def setStroke(self): 481 """Sets the stroke color in drawBot using this RGBA color.""" 482 drawBot.stroke(*self.asRGBA) 483 return self
Sets the stroke color in drawBot using this RGBA color.
Inherited Members
489@dataclass 490class KHSLSwatch(KColorSwatch): 491 h: float = 0 492 s: float = 100 493 l: float = 50 494 alpha: float = 1 495 496 def __str__(self) -> str: 497 """Returns a string representation of the HSL color.""" 498 return f"KHSLSwatch({self.h}, {self.s}, {self.l}, {self.alpha})" 499 500 @staticmethod 501 def fromAny(color: Any) -> "KHSLSwatch": 502 """Creates an HSL color from any supported color format.""" 503 return KHSLSwatch(*toHSLA(color)) 504 505 @property 506 def asHSLA(self) -> RawHSLA: 507 """Returns HSLA float tuple values for drawBot.""" 508 return self.h, self.s, self.l, self.alpha 509 510 @property 511 def asRGB(self) -> RawRGB: 512 """Returns the RGB float tuple representation of this HSL color.""" 513 hls = self.h / 360, self.l / 100, self.s / 100 # Different order for colorsys 514 return colorsys.hls_to_rgb(*hls) 515 516 @property 517 def asRGBA(self) -> RawRGBA: 518 """Returns the RGBA float tuple representation of this HSL color.""" 519 return (*self.asRGB, self.alpha) 520 521 def setFill(self): 522 """ 523 Sets the fill color in drawBot using this HSL color. 524 525 Returns: 526 self 527 """ 528 drawBot.fill(*self.asRGBA) 529 return self 530 531 def setStroke(self): 532 """ 533 Sets the stroke color in drawBot using this HSL color. 534 535 Returns: 536 self 537 """ 538 drawBot.stroke(*self.asRGBA) 539 return self 540 541 def withAlpha(self, alpha: float): 542 return KHSLSwatch(self.h, self.s, self.l, alpha) 543 544 def isDark(self, sensitivity=0.5) -> bool: 545 """ 546 Determines if this HSL color is considered dark based on luminance and sensitivity. 547 548 Args: 549 sensitivity: Threshold for darkness (0 = always dark, 1 = always light). 550 551 Returns: 552 True if the color is dark, False otherwise. 553 """ 554 return isDark(self.asRGB, sensitivity) 555 556 def nameColor(self) -> str: 557 """Return a human-readable label in kebab-case. 558 559 Example outputs: `light-red`, `dark-violet`, `very-light-rose`. 560 """ 561 return _nameColorFromHSL(self.h, self.s, self.l)
500 @staticmethod 501 def fromAny(color: Any) -> "KHSLSwatch": 502 """Creates an HSL color from any supported color format.""" 503 return KHSLSwatch(*toHSLA(color))
Creates an HSL color from any supported color format.
505 @property 506 def asHSLA(self) -> RawHSLA: 507 """Returns HSLA float tuple values for drawBot.""" 508 return self.h, self.s, self.l, self.alpha
Returns HSLA float tuple values for drawBot.
510 @property 511 def asRGB(self) -> RawRGB: 512 """Returns the RGB float tuple representation of this HSL color.""" 513 hls = self.h / 360, self.l / 100, self.s / 100 # Different order for colorsys 514 return colorsys.hls_to_rgb(*hls)
Returns the RGB float tuple representation of this HSL color.
516 @property 517 def asRGBA(self) -> RawRGBA: 518 """Returns the RGBA float tuple representation of this HSL color.""" 519 return (*self.asRGB, self.alpha)
Returns the RGBA float tuple representation of this HSL color.
521 def setFill(self): 522 """ 523 Sets the fill color in drawBot using this HSL color. 524 525 Returns: 526 self 527 """ 528 drawBot.fill(*self.asRGBA) 529 return self
Sets the fill color in drawBot using this HSL color.
Returns:
self
531 def setStroke(self): 532 """ 533 Sets the stroke color in drawBot using this HSL color. 534 535 Returns: 536 self 537 """ 538 drawBot.stroke(*self.asRGBA) 539 return self
Sets the stroke color in drawBot using this HSL color.
Returns:
self
544 def isDark(self, sensitivity=0.5) -> bool: 545 """ 546 Determines if this HSL color is considered dark based on luminance and sensitivity. 547 548 Args: 549 sensitivity: Threshold for darkness (0 = always dark, 1 = always light). 550 551 Returns: 552 True if the color is dark, False otherwise. 553 """ 554 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.
556 def nameColor(self) -> str: 557 """Return a human-readable label in kebab-case. 558 559 Example outputs: `light-red`, `dark-violet`, `very-light-rose`. 560 """ 561 return _nameColorFromHSL(self.h, self.s, self.l)
Return a human-readable label in kebab-case.
Example outputs: light-red, dark-violet, very-light-rose.
Inherited Members
564def mixComplementary( 565 source: KHSLSwatch, 566 hueShift=360 / 2, 567 hueDirection: Literal["up", "down", "any"] = "any", 568 lightShift=30, 569 sensitivity=0.5, 570) -> KHSLSwatch: 571 """ 572 Mixes a complementary color based on the source HSL color. 573 574 Args: 575 source: The source HSL color. 576 hueShift: Amount to shift the hue. 577 hueDirection: Direction to shift the hue ("up", "down", or "any"). 578 lightShift: Amount to shift the lightness. 579 sensitivity: Threshold for darkness (0 = always create darker, 1 = always create lighter). 580 581 Returns: 582 A new HSL color that is complementary to the source. 583 """ 584 # Hue 585 hueDirections = dict(up=[1], down=[-1], any=[-1, 1]) 586 hueRotate = lambda val: random.choice(hueDirections.get(hueDirection)) * val 587 h = (source.h + hueRotate(hueShift)) % 360 588 # Lightness 589 sourceIsDark = isDark(source.asRGB, sensitivity) 590 lightDelta = lightShift if sourceIsDark else -lightShift 591 l = source.l + lightDelta 592 593 return KHSLSwatch(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.