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
RawRGB = tuple[float, float, float]
RawRGBA = tuple[float, float, float, float]
RawHSLA = tuple[float, float, float, float]
def isHex(color: Any) -> bool:
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

def isRGBFloat(color: Any) -> bool:
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

def isRGBAFloat(color: Any) -> bool:
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

def isRGBInt(color: Any) -> bool:
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

def isRGBAInt(color: Any) -> bool:
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.

def isRGBLike(color: Any) -> bool:
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).

def normalizeHex(hexColor: str) -> str:
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.

def normalizeRGB(color: Any) -> tuple[float, float, float]:
 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.

def normalizeRGBA(color: Any) -> tuple[float, float, float, float]:
112def normalizeRGBA(color: Any) -> RawRGBA:
113    try:
114        r, g, b, *_ = color
115        RGB = normalizeRGB((r, g, b))
116        return *RGB, color[3] if len(color) > 3 else 1
117    except (TypeError, ValueError):
118        raise ValueError(f"Unsupported color format: {color}")
def fromHexToRGBA(hexColor: str, alpha=1) -> tuple[float, float, float, float]:
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.

def fromRGBAtoHSLA( rgba: tuple[float, float, float, float]) -> tuple[float, float, float, float]:
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.

def fromHexToHSLA(hexColor: str, alpha=1) -> tuple[float, float, float, float]:
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.

def toHSLA(color: Any) -> tuple[float, float, float, float]:
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.

def pickRandomRGB(lightness=0.5) -> tuple[float, float, float]:
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.

def toRGBInt(color: Any) -> tuple[int, int, int]:
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.

def toRGBAFloat(color: Any) -> tuple[float, float, float, float]:
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.

def isDark(color: Any, sensitivity=0.5) -> bool:
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.

class KColorSwatch(abc.ABC):
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.

asRGBA: tuple[float, float, float, float]
317    @property
318    @abstractmethod
319    def asRGBA(self) -> RawRGBA:
320        """Return color as normalized RGBA floats."""

Return color as normalized RGBA floats.

@abstractmethod
def setFill(self):
322    @abstractmethod
323    def setFill(self):
324        """Set drawBot fill using this swatch."""

Set drawBot fill using this swatch.

@abstractmethod
def setStroke(self):
326    @abstractmethod
327    def setStroke(self):
328        """Set drawBot stroke using this swatch."""

Set drawBot stroke using this swatch.

@abstractmethod
def withAlpha(self, alpha: float):
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.

asHSL: KHSLSwatch
334    @property
335    def asHSL(self) -> "KHSLSwatch":
336        return KHSLSwatch(*fromRGBAtoHSLA(self.asRGBA))
asHex: str
338    @property
339    def asHex(self) -> str:
340        r, g, b = toRGBInt(self.asRGBA)
341        return f"#{r:02X}{g:02X}{b:02X}"
def isDark(self, sensitivity=0.5) -> bool:
343    def isDark(self, sensitivity=0.5) -> bool:
344        return isDark(self.asRGBA, sensitivity)
def nameColor(self) -> str:
346    def nameColor(self) -> str:
347        h, s, l, _ = fromRGBAtoHSLA(self.asRGBA)
348        return _nameColorFromHSL(h, s, l)
@dataclass
class KCMYKSwatch(KColorSwatch):
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)
KCMYKSwatch( c: float = 0, m: float = 0, y: float = 0, k: float = 0, alpha: float = 1)
c: float = 0
m: float = 0
y: float = 0
k: float = 0
alpha: float = 1
@staticmethod
def fromAny(color: Any) -> KCMYKSwatch:
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}")
@staticmethod
def fromRGB(r: float, g: float, b: float, alpha=1) -> KCMYKSwatch:
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)
asCMYK
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.

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

asRGB: tuple[float, float, float]
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.

asRGBA: tuple[float, float, float, float]
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.

def setFill(self):
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

def setStroke(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

def withAlpha(self, alpha: float):
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.

@dataclass
class KRGBSwatch(KColorSwatch):
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)
KRGBSwatch(r: float = 0, g: float = 0, b: float = 0, a: float = 1)
r: float = 0
g: float = 0
b: float = 0
a: float = 1
@staticmethod
def fromAny(color: Any) -> KRGBSwatch:
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.

@staticmethod
def fromHex(hexColor: str, alpha=1) -> KRGBSwatch:
453    @staticmethod
454    def fromHex(hexColor: str, alpha=1) -> "KRGBSwatch":
455        return KRGBSwatch(*fromHexToRGBA(hexColor, alpha))
@staticmethod
def fromHSL(h: float, s=100, l=50, alpha=1) -> KRGBSwatch:
457    @staticmethod
458    def fromHSL(h: float, s=100, l=50, alpha=1) -> "KRGBSwatch":
459        return KRGBSwatch(*KHSLSwatch(h, s, l, alpha).asRGBA)
@staticmethod
def fromInt(r: int, g: int, b: int, a=1) -> KRGBSwatch:
461    @staticmethod
462    def fromInt(r: int, g: int, b: int, a=1) -> "KRGBSwatch":
463        return KRGBSwatch(r / 255, g / 255, b / 255, a)
asRGB: tuple[float, float, float]
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.

asRGBA: tuple[float, float, float, float]
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.

def setFill(self):
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.

def setStroke(self):
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.

def withAlpha(self, alpha: float):
485    def withAlpha(self, alpha: float):
486        return KRGBSwatch(self.r, self.g, self.b, alpha)

Return a copy with a new alpha value.

@dataclass
class KHSLSwatch(KColorSwatch):
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)
KHSLSwatch(h: float = 0, s: float = 100, l: float = 50, alpha: float = 1)
h: float = 0
s: float = 100
l: float = 50
alpha: float = 1
@staticmethod
def fromAny(color: Any) -> KHSLSwatch:
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.

asHSLA: tuple[float, float, float, float]
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.

asRGB: tuple[float, float, float]
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.

asRGBA: tuple[float, float, float, float]
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.

def setFill(self):
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

def setStroke(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

def withAlpha(self, alpha: float):
541    def withAlpha(self, alpha: float):
542        return KHSLSwatch(self.h, self.s, self.l, alpha)

Return a copy with a new alpha value.

def isDark(self, sensitivity=0.5) -> bool:
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.

def nameColor(self) -> str:
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
KColorSwatch
asHSL
asHex
def mixComplementary( source: KHSLSwatch, hueShift=180.0, hueDirection: Literal['up', 'down', 'any'] = 'any', lightShift=30, sensitivity=0.5) -> KHSLSwatch:
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.

RGBA = <class 'KRGBSwatch'>
CMYK = <class 'KCMYKSwatch'>
HSL = <class 'KHSLSwatch'>