xulbux 1.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xulbux might be problematic. Click here for more details.
- xulbux/__init__.py +57 -0
- xulbux/_cli_.py +53 -0
- xulbux/_consts_.py +145 -0
- xulbux/xx_code.py +105 -0
- xulbux/xx_color.py +955 -0
- xulbux/xx_console.py +378 -0
- xulbux/xx_data.py +531 -0
- xulbux/xx_env_path.py +113 -0
- xulbux/xx_file.py +65 -0
- xulbux/xx_format_codes.py +305 -0
- xulbux/xx_json.py +106 -0
- xulbux/xx_path.py +107 -0
- xulbux/xx_regex.py +156 -0
- xulbux/xx_string.py +159 -0
- xulbux/xx_system.py +85 -0
- xulbux-1.6.1.dist-info/LICENSE +21 -0
- xulbux-1.6.1.dist-info/METADATA +110 -0
- xulbux-1.6.1.dist-info/RECORD +21 -0
- xulbux-1.6.1.dist-info/WHEEL +5 -0
- xulbux-1.6.1.dist-info/entry_points.txt +3 -0
- xulbux-1.6.1.dist-info/top_level.txt +1 -0
xulbux/xx_color.py
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`rgba`:
|
|
3
|
+
An RGB/RGBA color: is a tuple of 3 integers, representing the red (`0`-`255`), green (`0`-`255`), and blue (`0`-`255`).
|
|
4
|
+
Also includes an optional 4th param, which is a float, that represents the alpha channel (`0.0`-`1.0`).
|
|
5
|
+
`hsla`:
|
|
6
|
+
An HSL/HSB color: is a tuple of 3 integers, representing the hue (`0`-`360`), saturation (`0`-`100`), and lightness (`0`-`100`).
|
|
7
|
+
Also includes an optional 4th param, which is a float, that represents the alpha channel (`0.0`-`1.0`).
|
|
8
|
+
`hexa`:
|
|
9
|
+
A HEX color: is a string in the format `RGB`, `RGBA`, `RRGGBB` or `RRGGBBAA` (where `R` `G` `B` `A` are hexadecimal digits).
|
|
10
|
+
|
|
11
|
+
-------------------------------------------------------------------------------------------------------------------------------------
|
|
12
|
+
The `Color` class, which contains all sorts of different color-related methods:
|
|
13
|
+
- validate colors:
|
|
14
|
+
- is valid rgba
|
|
15
|
+
- is valid hsla
|
|
16
|
+
- is valid hexa
|
|
17
|
+
- is valid in any format
|
|
18
|
+
- check if a color has an alpha channel
|
|
19
|
+
- convert between different color formats:
|
|
20
|
+
- color to rgba
|
|
21
|
+
- color to hsla
|
|
22
|
+
- color to hexa
|
|
23
|
+
- recognize colors inside strings and convert them to color types:
|
|
24
|
+
- string to rgba
|
|
25
|
+
- convert an RGBA color to a HEX integer
|
|
26
|
+
- convert a HEX integer to an RGBA color
|
|
27
|
+
- get a colors luminance from the RGB channels
|
|
28
|
+
- get the optimal text color for on a colored background
|
|
29
|
+
- adjust different color channels:
|
|
30
|
+
- brightness
|
|
31
|
+
- saturation
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from .xx_regex import Regex
|
|
35
|
+
|
|
36
|
+
import re as _re
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class rgba:
|
|
40
|
+
"""An RGB/RGBA color: is a tuple of 3 integers, representing the red (`0`-`255`), green (`0`-`255`), and blue (`0`-`255`).\n
|
|
41
|
+
Also includes an optional 4th param, which is a float, that represents the alpha channel (`0.0`-`1.0`).\n
|
|
42
|
+
-----------------------------------------------------------------------------------------------------------------------------
|
|
43
|
+
Includes methods:
|
|
44
|
+
- `to_hsla()` to convert to HSL color
|
|
45
|
+
- `to_hexa()` to convert to HEX color
|
|
46
|
+
- `has_alpha()` to check if the color has an alpha channel
|
|
47
|
+
- `lighten(amount)` to create a lighter version of the color
|
|
48
|
+
- `darken(amount)` to create a darker version of the color
|
|
49
|
+
- `saturate(amount)` to increase color saturation
|
|
50
|
+
- `desaturate(amount)` to decrease color saturation
|
|
51
|
+
- `rotate(degrees)` to rotate the hue by degrees
|
|
52
|
+
- `invert()` to get the inverse color
|
|
53
|
+
- `grayscale()` to convert to grayscale
|
|
54
|
+
- `blend(other, ratio)` to blend with another color
|
|
55
|
+
- `is_dark()` to check if the color is considered dark
|
|
56
|
+
- `is_light()` to check if the color is considered light
|
|
57
|
+
- `is_grayscale()` to check if the color is grayscale
|
|
58
|
+
- `is_opaque()` to check if the color has no transparency
|
|
59
|
+
- `with_alpha(alpha)` to create a new color with different alpha
|
|
60
|
+
- `complementary()` to get the complementary color"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, r: int, g: int, b: int, a: float = None):
|
|
63
|
+
if any(isinstance(x, rgba) for x in (r, g, b)):
|
|
64
|
+
raise ValueError("Color is already a rgba() color")
|
|
65
|
+
elif not all(isinstance(x, int) and 0 <= x <= 255 for x in (r, g, b)):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"RGBA color must have R G B as integers in [0, 255]: got",
|
|
68
|
+
(r, g, b),
|
|
69
|
+
)
|
|
70
|
+
elif a is not None and not (isinstance(a, (int, float)) and 0 <= a <= 1):
|
|
71
|
+
raise ValueError(f"Alpha channel must be a float/int in [0.0, 1.0]: got '{a}'")
|
|
72
|
+
self.r, self.g, self.b = r, g, b
|
|
73
|
+
self.a = (1.0 if a > 1.0 else float(a)) if a else None
|
|
74
|
+
|
|
75
|
+
def __len__(self):
|
|
76
|
+
return 4 if self.a else 3
|
|
77
|
+
|
|
78
|
+
def __iter__(self):
|
|
79
|
+
return iter((self.r, self.g, self.b) + ((self.a,) if self.a else ()))
|
|
80
|
+
|
|
81
|
+
def __dict__(self):
|
|
82
|
+
return self.dict()
|
|
83
|
+
|
|
84
|
+
def __getitem__(self, index):
|
|
85
|
+
return ((self.r, self.g, self.b) + ((self.a,) if self.a else ()))[index]
|
|
86
|
+
|
|
87
|
+
def __repr__(self):
|
|
88
|
+
return f'rgba({self.r}, {self.g}, {self.b}{f", {self.a}" if self.a else ""})'
|
|
89
|
+
|
|
90
|
+
def __str__(self):
|
|
91
|
+
return f'({self.r}, {self.g}, {self.b}{f", {self.a}" if self.a else ""})'
|
|
92
|
+
|
|
93
|
+
def __eq__(self, other):
|
|
94
|
+
if not isinstance(other, rgba):
|
|
95
|
+
return False
|
|
96
|
+
return (self.r, self.g, self.b, self.a) == (
|
|
97
|
+
other[0],
|
|
98
|
+
other[1],
|
|
99
|
+
other[2],
|
|
100
|
+
other[3],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def dict(self) -> dict:
|
|
104
|
+
"""Returns the color components as a dictionary with keys `'r'`, `'g'`, `'b'` and optionally `'a'`"""
|
|
105
|
+
return dict(r=self.r, g=self.g, b=self.b, a=self.a) if self.a else dict(r=self.r, g=self.g, b=self.b)
|
|
106
|
+
|
|
107
|
+
def values(self) -> tuple:
|
|
108
|
+
"""Returns the color components as separate values `r, g, b, a`"""
|
|
109
|
+
return self.r, self.g, self.b, self.a
|
|
110
|
+
|
|
111
|
+
def to_hsla(self) -> "hsla":
|
|
112
|
+
"""Returns the color as a `hsla()` color"""
|
|
113
|
+
return hsla(*self._rgb_to_hsl(self.r, self.g, self.b), self.a)
|
|
114
|
+
|
|
115
|
+
def to_hexa(self) -> "hexa":
|
|
116
|
+
"""Returns the color as a `hexa()` color"""
|
|
117
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
118
|
+
|
|
119
|
+
def has_alpha(self) -> bool:
|
|
120
|
+
"""Returns `True` if the color has an alpha channel and `False` otherwise"""
|
|
121
|
+
return self.a is not None
|
|
122
|
+
|
|
123
|
+
def lighten(self, amount: float) -> "rgba":
|
|
124
|
+
"""Increases the colors lightness by the specified amount (`0.0`-`1.0`)"""
|
|
125
|
+
self.r, self.g, self.b, self.a = self.to_hsla().lighten(amount).to_rgba().values()
|
|
126
|
+
return rgba(self.r, self.g, self.b, self.a)
|
|
127
|
+
|
|
128
|
+
def darken(self, amount: float) -> "rgba":
|
|
129
|
+
"""Decreases the colors lightness by the specified amount (`0.0`-`1.0`)"""
|
|
130
|
+
self.r, self.g, self.b, self.a = self.to_hsla().darken(amount).to_rgba().values()
|
|
131
|
+
return rgba(self.r, self.g, self.b, self.a)
|
|
132
|
+
|
|
133
|
+
def saturate(self, amount: float) -> "rgba":
|
|
134
|
+
"""Increases the colors saturation by the specified amount (`0.0`-`1.0`)"""
|
|
135
|
+
self.r, self.g, self.b, self.a = self.to_hsla().saturate(amount).to_rgba().values()
|
|
136
|
+
return rgba(self.r, self.g, self.b, self.a)
|
|
137
|
+
|
|
138
|
+
def desaturate(self, amount: float) -> "rgba":
|
|
139
|
+
"""Decreases the colors saturation by the specified amount (`0.0`-`1.0`)"""
|
|
140
|
+
self.r, self.g, self.b, self.a = self.to_hsla().desaturate(amount).to_rgba().values()
|
|
141
|
+
return rgba(self.r, self.g, self.b, self.a)
|
|
142
|
+
|
|
143
|
+
def rotate(self, degrees: int) -> "rgba":
|
|
144
|
+
"""Rotates the colors hue by the specified number of degrees"""
|
|
145
|
+
self.r, self.g, self.b, self.a = self.to_hsla().rotate(degrees).to_rgba().values()
|
|
146
|
+
return rgba(self.r, self.g, self.b, self.a)
|
|
147
|
+
|
|
148
|
+
def invert(self, invert_alpha: bool = False) -> "rgba":
|
|
149
|
+
"""Inverts the color by rotating hue by 180 degrees and inverting lightness"""
|
|
150
|
+
self.r, self.g, self.b = 255 - self.r, 255 - self.g, 255 - self.b
|
|
151
|
+
if invert_alpha:
|
|
152
|
+
self.a = 1 - self.a
|
|
153
|
+
return rgba(self.r, self.g, self.b, self.a)
|
|
154
|
+
|
|
155
|
+
def grayscale(self) -> "rgba":
|
|
156
|
+
"""Converts the color to grayscale using the luminance formula"""
|
|
157
|
+
self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b)
|
|
158
|
+
return rgba(self.r, self.g, self.b, self.a)
|
|
159
|
+
|
|
160
|
+
def blend(self, other: "rgba", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
|
|
161
|
+
"""Blends the current color with another color using the specified ratio (`0.0`-`1.0`):
|
|
162
|
+
- if `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)
|
|
163
|
+
- if `ratio` is `0.5` it means 50% of both colors (1:1 mixture)
|
|
164
|
+
- if `ratio` is `1.0` it means 0% of the current color and 100% of the `other` color (0:2 mixture)"""
|
|
165
|
+
if not (isinstance(ratio, (int, float)) and 0 <= ratio <= 1):
|
|
166
|
+
raise ValueError("'ratio' must be a float/int in [0.0, 1.0]")
|
|
167
|
+
elif not isinstance(other, rgba):
|
|
168
|
+
if Color.is_valid_rgba(other):
|
|
169
|
+
other = rgba(*other)
|
|
170
|
+
else:
|
|
171
|
+
raise TypeError("'other' must be a valid RGBA color")
|
|
172
|
+
ratio *= 2
|
|
173
|
+
self.r = max(0, min(255, int(round((self.r * (2 - ratio)) + (other.r * ratio)))))
|
|
174
|
+
self.g = max(0, min(255, int(round((self.g * (2 - ratio)) + (other.g * ratio)))))
|
|
175
|
+
self.b = max(0, min(255, int(round((self.b * (2 - ratio)) + (other.b * ratio)))))
|
|
176
|
+
none_alpha = self.a is None and (len(other) <= 3 or other[3] is None)
|
|
177
|
+
if not none_alpha:
|
|
178
|
+
self_a = self.a if self.a is not None else 1
|
|
179
|
+
other_a = (other[3] if other[3] is not None else 1) if len(other) > 3 else 1
|
|
180
|
+
if additive_alpha:
|
|
181
|
+
self.a = max(0, min(1, (self_a * (2 - ratio)) + (other_a * ratio)))
|
|
182
|
+
else:
|
|
183
|
+
self.a = max(
|
|
184
|
+
0,
|
|
185
|
+
min(
|
|
186
|
+
1,
|
|
187
|
+
(self_a * (1 - (ratio / 2))) + (other_a * (ratio / 2)),
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
self.a = None
|
|
192
|
+
return rgba(self.r, self.g, self.b, None if none_alpha else self.a)
|
|
193
|
+
|
|
194
|
+
def is_dark(self) -> bool:
|
|
195
|
+
"""Returns `True` if the color is considered dark (`lightness < 50%`)"""
|
|
196
|
+
return (0.299 * self.r + 0.587 * self.g + 0.114 * self.b) < 128
|
|
197
|
+
|
|
198
|
+
def is_light(self) -> bool:
|
|
199
|
+
"""Returns `True` if the color is considered light (`lightness >= 50%`)"""
|
|
200
|
+
return not self.is_dark()
|
|
201
|
+
|
|
202
|
+
def is_grayscale(self) -> bool:
|
|
203
|
+
"""Returns `True` if the color is grayscale"""
|
|
204
|
+
return self.r == self.g == self.b
|
|
205
|
+
|
|
206
|
+
def is_opaque(self) -> bool:
|
|
207
|
+
"""Returns `True` if the color has no transparency"""
|
|
208
|
+
return self.a == 1 or self.a is None
|
|
209
|
+
|
|
210
|
+
def with_alpha(self, alpha: float) -> "rgba":
|
|
211
|
+
"""Returns a new color with the specified alpha value"""
|
|
212
|
+
if not (isinstance(alpha, (int, float)) and 0 <= alpha <= 1):
|
|
213
|
+
raise ValueError("'alpha' must be a float/int in [0.0, 1.0]")
|
|
214
|
+
return rgba(self.r, self.g, self.b, alpha)
|
|
215
|
+
|
|
216
|
+
def complementary(self) -> "rgba":
|
|
217
|
+
"""Returns the complementary color (180 degrees on the color wheel)"""
|
|
218
|
+
return self.to_hsla().complementary().to_rgba()
|
|
219
|
+
|
|
220
|
+
def _rgb_to_hsl(self, r: int, g: int, b: int) -> tuple:
|
|
221
|
+
r, g, b = r / 255.0, g / 255.0, b / 255.0
|
|
222
|
+
max_c, min_c = max(r, g, b), min(r, g, b)
|
|
223
|
+
l = (max_c + min_c) / 2
|
|
224
|
+
if max_c == min_c:
|
|
225
|
+
h = s = 0
|
|
226
|
+
else:
|
|
227
|
+
delta = max_c - min_c
|
|
228
|
+
s = delta / (1 - abs(2 * l - 1))
|
|
229
|
+
if max_c == r:
|
|
230
|
+
h = ((g - b) / delta) % 6
|
|
231
|
+
elif max_c == g:
|
|
232
|
+
h = ((b - r) / delta) + 2
|
|
233
|
+
else:
|
|
234
|
+
h = ((r - g) / delta) + 4
|
|
235
|
+
h /= 6
|
|
236
|
+
return int(round(h * 360)), int(round(s * 100)), int(round(l * 100))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class hsla:
|
|
240
|
+
"""A HSL/HSLA color: is a tuple of 3 integers, representing hue (`0`-`360`), saturation (`0`-`100`), and lightness (`0`-`100`).\n
|
|
241
|
+
Also includes an optional 4th param, which is a float, that represents the alpha channel (`0.0`-`1.0`).\n
|
|
242
|
+
----------------------------------------------------------------------------------------------------------------------------------
|
|
243
|
+
Includes methods:
|
|
244
|
+
- `to_rgba()` to convert to RGB color
|
|
245
|
+
- `to_hexa()` to convert to HEX color
|
|
246
|
+
- `has_alpha()` to check if the color has an alpha channel
|
|
247
|
+
- `lighten(amount)` to create a lighter version of the color
|
|
248
|
+
- `darken(amount)` to create a darker version of the color
|
|
249
|
+
- `saturate(amount)` to increase color saturation
|
|
250
|
+
- `desaturate(amount)` to decrease color saturation
|
|
251
|
+
- `rotate(degrees)` to rotate the hue by degrees
|
|
252
|
+
- `invert()` to get the inverse color
|
|
253
|
+
- `grayscale()` to convert to grayscale
|
|
254
|
+
- `blend(other, ratio)` to blend with another color
|
|
255
|
+
- `is_dark()` to check if the color is considered dark
|
|
256
|
+
- `is_light()` to check if the color is considered light
|
|
257
|
+
- `is_grayscale()` to check if the color is grayscale
|
|
258
|
+
- `is_opaque()` to check if the color has no transparency
|
|
259
|
+
- `with_alpha(alpha)` to create a new color with different alpha
|
|
260
|
+
- `complementary()` to get the complementary color"""
|
|
261
|
+
|
|
262
|
+
def __init__(self, h: int, s: int, l: int, a: float = None):
|
|
263
|
+
if any(isinstance(x, hsla) for x in (h, s, l)):
|
|
264
|
+
raise ValueError("Color is already a hsla() color")
|
|
265
|
+
elif not (isinstance(h, int) and (0 <= h <= 360) and all(isinstance(x, int) and (0 <= x <= 100) for x in (s, l))):
|
|
266
|
+
raise ValueError(
|
|
267
|
+
"HSL color must have H as integer in [0, 360] and S L as integers in [0, 100]: got",
|
|
268
|
+
(h, s, l),
|
|
269
|
+
)
|
|
270
|
+
elif a is not None and (not isinstance(a, (int, float)) or not 0 <= a <= 1):
|
|
271
|
+
raise ValueError(f"Alpha channel must be a float/int in [0.0, 1.0]: got '{a}'")
|
|
272
|
+
self.h, self.s, self.l = h, s, l
|
|
273
|
+
self.a = (1.0 if a > 1.0 else float(a)) if a else None
|
|
274
|
+
|
|
275
|
+
def __len__(self):
|
|
276
|
+
return 4 if self.a else 3
|
|
277
|
+
|
|
278
|
+
def __iter__(self):
|
|
279
|
+
return iter((self.h, self.s, self.l) + ((self.a,) if self.a else ()))
|
|
280
|
+
|
|
281
|
+
def __dict__(self):
|
|
282
|
+
return self.dict()
|
|
283
|
+
|
|
284
|
+
def __getitem__(self, index):
|
|
285
|
+
return ((self.h, self.s, self.l) + ((self.a,) if self.a else ()))[index]
|
|
286
|
+
|
|
287
|
+
def __repr__(self):
|
|
288
|
+
return f'hsla({self.h}, {self.s}, {self.l}{f", {self.a}" if self.a else ""})'
|
|
289
|
+
|
|
290
|
+
def __str__(self):
|
|
291
|
+
return f'({self.h}, {self.s}, {self.l}{f", {self.a}" if self.a else ""})'
|
|
292
|
+
|
|
293
|
+
def __eq__(self, other):
|
|
294
|
+
if not isinstance(other, hsla):
|
|
295
|
+
return False
|
|
296
|
+
return (self.h, self.s, self.l, self.a) == (
|
|
297
|
+
other[0],
|
|
298
|
+
other[1],
|
|
299
|
+
other[2],
|
|
300
|
+
other[3],
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def dict(self) -> dict:
|
|
304
|
+
"""Returns the color components as a dictionary with keys `'h'`, `'s'`, `'l'` and optionally `'a'`"""
|
|
305
|
+
return dict(h=self.h, s=self.s, l=self.l, a=self.a) if self.a else dict(h=self.h, s=self.s, l=self.l)
|
|
306
|
+
|
|
307
|
+
def values(self) -> tuple:
|
|
308
|
+
"""Returns the color components as separate values `h, s, l, a`"""
|
|
309
|
+
return self.h, self.s, self.l, self.a
|
|
310
|
+
|
|
311
|
+
def to_rgba(self) -> "rgba":
|
|
312
|
+
"""Returns the color as a `rgba()` color"""
|
|
313
|
+
return rgba(*self._hsl_to_rgb(self.h, self.s, self.l), self.a)
|
|
314
|
+
|
|
315
|
+
def to_hexa(self) -> "hexa":
|
|
316
|
+
"""Returns the color as a `hexa()` color"""
|
|
317
|
+
r, g, b = self._hsl_to_rgb(self.h, self.s, self.l)
|
|
318
|
+
return hexa(f'#{r:02X}{g:02X}{b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
319
|
+
|
|
320
|
+
def has_alpha(self) -> bool:
|
|
321
|
+
"""Returns `True` if the color has an alpha channel and `False` otherwise"""
|
|
322
|
+
return self.a is not None
|
|
323
|
+
|
|
324
|
+
def lighten(self, amount: float) -> "hsla":
|
|
325
|
+
"""Increases the colors lightness by the specified amount (`0.0`-`1.0`)"""
|
|
326
|
+
if not (isinstance(amount, (int, float)) and 0 <= amount <= 1):
|
|
327
|
+
raise ValueError("'amount' must be a float/int in [0.0, 1.0]")
|
|
328
|
+
self.l = int(min(100, self.l + (100 - self.l) * amount))
|
|
329
|
+
return hsla(self.h, self.s, self.l, self.a)
|
|
330
|
+
|
|
331
|
+
def darken(self, amount: float) -> "hsla":
|
|
332
|
+
"""Decreases the colors lightness by the specified amount (`0.0`-`1.0`)"""
|
|
333
|
+
if not (isinstance(amount, (int, float)) and 0 <= amount <= 1):
|
|
334
|
+
raise ValueError("'amount' must be a float/int in [0.0, 1.0]")
|
|
335
|
+
self.l = int(max(0, self.l * (1 - amount)))
|
|
336
|
+
return hsla(self.h, self.s, self.l, self.a)
|
|
337
|
+
|
|
338
|
+
def saturate(self, amount: float) -> "hsla":
|
|
339
|
+
"""Increases the colors saturation by the specified amount (`0.0`-`1.0`)"""
|
|
340
|
+
if not (isinstance(amount, (int, float)) and 0 <= amount <= 1):
|
|
341
|
+
raise ValueError("'amount' must be a float/int in [0.0, 1.0]")
|
|
342
|
+
self.s = int(min(100, self.s + (100 - self.s) * amount))
|
|
343
|
+
return hsla(self.h, self.s, self.l, self.a)
|
|
344
|
+
|
|
345
|
+
def desaturate(self, amount: float) -> "hsla":
|
|
346
|
+
"""Decreases the colors saturation by the specified amount (`0.0`-`1.0`)"""
|
|
347
|
+
if not (isinstance(amount, (int, float)) and 0 <= amount <= 1):
|
|
348
|
+
raise ValueError("'amount' must be a float/int in [0.0, 1.0]")
|
|
349
|
+
self.s = int(max(0, self.s * (1 - amount)))
|
|
350
|
+
return hsla(self.h, self.s, self.l, self.a)
|
|
351
|
+
|
|
352
|
+
def rotate(self, degrees: int) -> "hsla":
|
|
353
|
+
"""Rotates the colors hue by the specified number of degrees"""
|
|
354
|
+
self.h = (self.h + degrees) % 360
|
|
355
|
+
return hsla(self.h, self.s, self.l, self.a)
|
|
356
|
+
|
|
357
|
+
def invert(self, invert_alpha: bool = False) -> "hsla":
|
|
358
|
+
"""Inverts the color by rotating hue by 180 degrees and inverting lightness"""
|
|
359
|
+
self.h = (self.h + 180) % 360
|
|
360
|
+
self.l = 100 - self.l
|
|
361
|
+
if invert_alpha:
|
|
362
|
+
self.a = 1 - self.a
|
|
363
|
+
return hsla(self.h, self.s, self.l, self.a)
|
|
364
|
+
|
|
365
|
+
def grayscale(self) -> "hsla":
|
|
366
|
+
"""Converts the color to grayscale using the luminance formula"""
|
|
367
|
+
l = Color.luminance(*self._hsl_to_rgb(self.h, self.s, self.l))
|
|
368
|
+
self.h, self.s, self.l, _ = rgba(l, l, l).to_hsla().values()
|
|
369
|
+
return hsla(self.h, self.s, self.l, self.a)
|
|
370
|
+
|
|
371
|
+
def blend(self, other: "hsla", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
|
|
372
|
+
"""Blends the current color with another color using the specified ratio (`0.0`-`1.0`):
|
|
373
|
+
- if `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)
|
|
374
|
+
- if `ratio` is `0.5` it means 50% of both colors (1:1 mixture)
|
|
375
|
+
- if `ratio` is `1.0` it means 0% of the current color and 100% of the `other` color (0:2 mixture)"""
|
|
376
|
+
self.h, self.s, self.l, self.a = self.to_rgba().blend(Color.to_rgba(other), ratio, additive_alpha).to_hsla().values()
|
|
377
|
+
return hsla(self.h, self.s, self.l, self.a)
|
|
378
|
+
|
|
379
|
+
def is_dark(self) -> bool:
|
|
380
|
+
"""Returns `True` if the color is considered dark (`lightness < 50%`)"""
|
|
381
|
+
return self.l < 50
|
|
382
|
+
|
|
383
|
+
def is_light(self) -> bool:
|
|
384
|
+
"""Returns `True` if the color is considered light (`lightness >= 50%`)"""
|
|
385
|
+
return not self.is_dark()
|
|
386
|
+
|
|
387
|
+
def is_grayscale(self) -> bool:
|
|
388
|
+
"""Returns `True` if the color is considered grayscale"""
|
|
389
|
+
return self.s == 0
|
|
390
|
+
|
|
391
|
+
def is_opaque(self) -> bool:
|
|
392
|
+
"""Returns `True` if the color has no transparency"""
|
|
393
|
+
return self.a == 1 or self.a is None
|
|
394
|
+
|
|
395
|
+
def with_alpha(self, alpha: float) -> "hsla":
|
|
396
|
+
"""Returns a new color with the specified alpha value"""
|
|
397
|
+
if not (isinstance(alpha, (int, float)) and 0 <= alpha <= 1):
|
|
398
|
+
raise ValueError("'alpha' must be a float/int in [0.0, 1.0]")
|
|
399
|
+
return hsla(self.h, self.s, self.l, alpha)
|
|
400
|
+
|
|
401
|
+
def complementary(self) -> "hsla":
|
|
402
|
+
"""Returns the complementary color (180 degrees on the color wheel)"""
|
|
403
|
+
return hsla((self.h + 180) % 360, self.s, self.l, self.a)
|
|
404
|
+
|
|
405
|
+
def _hsl_to_rgb(self, h: int, s: int, l: int) -> tuple:
|
|
406
|
+
h, s, l = h / 360, s / 100, l / 100
|
|
407
|
+
if s == 0:
|
|
408
|
+
r = g = b = int(l * 255)
|
|
409
|
+
else:
|
|
410
|
+
|
|
411
|
+
def hue_to_rgb(p, q, t):
|
|
412
|
+
if t < 0:
|
|
413
|
+
t += 1
|
|
414
|
+
if t > 1:
|
|
415
|
+
t -= 1
|
|
416
|
+
if t < 1 / 6:
|
|
417
|
+
return p + (q - p) * 6 * t
|
|
418
|
+
if t < 1 / 2:
|
|
419
|
+
return q
|
|
420
|
+
if t < 2 / 3:
|
|
421
|
+
return p + (q - p) * (2 / 3 - t) * 6
|
|
422
|
+
return p
|
|
423
|
+
|
|
424
|
+
q = l * (1 + s) if l < 0.5 else l + s - l * s
|
|
425
|
+
p = 2 * l - q
|
|
426
|
+
r = int(round(hue_to_rgb(p, q, h + 1 / 3) * 255))
|
|
427
|
+
g = int(round(hue_to_rgb(p, q, h) * 255))
|
|
428
|
+
b = int(round(hue_to_rgb(p, q, h - 1 / 3) * 255))
|
|
429
|
+
return r, g, b
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class hexa:
|
|
433
|
+
"""A HEX color: is a string representing a hexadecimal color code with optional alpha channel.\n
|
|
434
|
+
-------------------------------------------------------------------------------------------------
|
|
435
|
+
Supports formats: RGB, RGBA, RRGGBB, RRGGBBAA (with or without prefix)
|
|
436
|
+
Includes methods:
|
|
437
|
+
- `to_rgba()` to convert to RGB color
|
|
438
|
+
- `to_hsla()` to convert to HSL color
|
|
439
|
+
- `has_alpha()` to check if the color has an alpha channel
|
|
440
|
+
- `lighten(amount)` to create a lighter version of the color
|
|
441
|
+
- `darken(amount)` to create a darker version of the color
|
|
442
|
+
- `saturate(amount)` to increase color saturation
|
|
443
|
+
- `desaturate(amount)` to decrease color saturation
|
|
444
|
+
- `rotate(degrees)` to rotate the hue by degrees
|
|
445
|
+
- `invert()` to get the inverse color
|
|
446
|
+
- `grayscale()` to convert to grayscale
|
|
447
|
+
- `blend(other, ratio)` to blend with another color
|
|
448
|
+
- `is_dark()` to check if the color is considered dark
|
|
449
|
+
- `is_light()` to check if the color is considered light
|
|
450
|
+
- `is_grayscale()` to check if the color is grayscale
|
|
451
|
+
- `is_opaque()` to check if the color has no transparency
|
|
452
|
+
- `with_alpha(alpha)` to create a new color with different alpha
|
|
453
|
+
- `complementary()` to get the complementary color"""
|
|
454
|
+
|
|
455
|
+
def __init__(self, color: str | int):
|
|
456
|
+
if isinstance(color, hexa):
|
|
457
|
+
raise ValueError("Color is already a hexa() color")
|
|
458
|
+
if isinstance(color, str):
|
|
459
|
+
if color.startswith("#"):
|
|
460
|
+
color = color[1:].upper()
|
|
461
|
+
elif color.startswith("0x"):
|
|
462
|
+
color = color[2:].upper()
|
|
463
|
+
if len(color) == 3: # RGB
|
|
464
|
+
self.r, self.g, self.b, self.a = (
|
|
465
|
+
int(color[0] * 2, 16),
|
|
466
|
+
int(color[1] * 2, 16),
|
|
467
|
+
int(color[2] * 2, 16),
|
|
468
|
+
None,
|
|
469
|
+
)
|
|
470
|
+
elif len(color) == 4: # RGBA
|
|
471
|
+
self.r, self.g, self.b, self.a = (
|
|
472
|
+
int(color[0] * 2, 16),
|
|
473
|
+
int(color[1] * 2, 16),
|
|
474
|
+
int(color[2] * 2, 16),
|
|
475
|
+
int(color[3] * 2, 16) / 255.0,
|
|
476
|
+
)
|
|
477
|
+
elif len(color) == 6: # RRGGBB
|
|
478
|
+
self.r, self.g, self.b, self.a = (
|
|
479
|
+
int(color[0:2], 16),
|
|
480
|
+
int(color[2:4], 16),
|
|
481
|
+
int(color[4:6], 16),
|
|
482
|
+
None,
|
|
483
|
+
)
|
|
484
|
+
elif len(color) == 8: # RRGGBBAA
|
|
485
|
+
self.r, self.g, self.b, self.a = (
|
|
486
|
+
int(color[0:2], 16),
|
|
487
|
+
int(color[2:4], 16),
|
|
488
|
+
int(color[4:6], 16),
|
|
489
|
+
int(color[6:8], 16) / 255.0,
|
|
490
|
+
)
|
|
491
|
+
else:
|
|
492
|
+
raise ValueError(f"Invalid HEX format '{color}'")
|
|
493
|
+
elif isinstance(color, int):
|
|
494
|
+
self.r, self.g, self.b, self.a = Color.hex_int_to_rgba(color)
|
|
495
|
+
else:
|
|
496
|
+
raise TypeError(f"HEX color must be of type 'str' or 'int': got '{type(color)}'")
|
|
497
|
+
|
|
498
|
+
def __len__(self):
|
|
499
|
+
return 4 if self.a else 3
|
|
500
|
+
|
|
501
|
+
def __iter__(self):
|
|
502
|
+
return iter((f"{self.r:02X}", f"{self.g:02X}", f"{self.b:02X}") + ((f"{int(self.a * 255):02X}",) if self.a else ()))
|
|
503
|
+
|
|
504
|
+
def __dict__(self):
|
|
505
|
+
return self.dict()
|
|
506
|
+
|
|
507
|
+
def __getitem__(self, index):
|
|
508
|
+
return ((f"{self.r:02X}", f"{self.g:02X}", f"{self.b:02X}") + ((f"{int(self.a * 255):02X}",) if self.a else ()))[index]
|
|
509
|
+
|
|
510
|
+
def __repr__(self):
|
|
511
|
+
return f'hexa(#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""})'
|
|
512
|
+
|
|
513
|
+
def __str__(self):
|
|
514
|
+
return f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}'
|
|
515
|
+
|
|
516
|
+
def __eq__(self, other):
|
|
517
|
+
if not isinstance(other, hexa):
|
|
518
|
+
return False
|
|
519
|
+
return (self.r, self.g, self.b, self.a) == (
|
|
520
|
+
other[0],
|
|
521
|
+
other[1],
|
|
522
|
+
other[2],
|
|
523
|
+
other[3],
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
def dict(self) -> dict:
|
|
527
|
+
"""Returns the color components as a dictionary with hex string values for keys `'r'`, `'g'`, `'b'` and optionally `'a'`"""
|
|
528
|
+
return (
|
|
529
|
+
dict(
|
|
530
|
+
r=f"{self.r:02X}",
|
|
531
|
+
g=f"{self.g:02X}",
|
|
532
|
+
b=f"{self.b:02X}",
|
|
533
|
+
a=f"{int(self.a * 255):02X}",
|
|
534
|
+
)
|
|
535
|
+
if self.a
|
|
536
|
+
else dict(r=f"{self.r:02X}", g=f"{self.g:02X}", b=f"{self.b:02X}")
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
def values(self) -> tuple:
|
|
540
|
+
"""Returns the color components as separate values `r, g, b, a`"""
|
|
541
|
+
return self.r, self.g, self.b, self.a
|
|
542
|
+
|
|
543
|
+
def to_rgba(self, round_alpha: bool = True) -> "rgba":
|
|
544
|
+
"""Returns the color as a `rgba()` color"""
|
|
545
|
+
return rgba(
|
|
546
|
+
self.r,
|
|
547
|
+
self.g,
|
|
548
|
+
self.b,
|
|
549
|
+
(round(self.a, 2) if round_alpha else self.a) if self.a else None,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def to_hsla(self, round_alpha: bool = True) -> "hsla":
|
|
553
|
+
"""Returns the color as a `hsla()` color"""
|
|
554
|
+
return self.to_rgba(round_alpha).to_hsla()
|
|
555
|
+
|
|
556
|
+
def has_alpha(self) -> bool:
|
|
557
|
+
"""Returns `True` if the color has an alpha channel and `False` otherwise"""
|
|
558
|
+
return self.a is not None
|
|
559
|
+
|
|
560
|
+
def lighten(self, amount: float) -> "hexa":
|
|
561
|
+
"""Increases the colors lightness by the specified amount (`0.0`-`1.0`)"""
|
|
562
|
+
self.r, self.g, self.b, self.a = self.to_rgba(False).lighten(amount).values()
|
|
563
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
564
|
+
|
|
565
|
+
def darken(self, amount: float) -> "hexa":
|
|
566
|
+
"""Decreases the colors lightness by the specified amount (`0.0`-`1.0`)"""
|
|
567
|
+
self.r, self.g, self.b, self.a = self.to_rgba(False).darken(amount).values()
|
|
568
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
569
|
+
|
|
570
|
+
def saturate(self, amount: float) -> "hexa":
|
|
571
|
+
"""Increases the colors saturation by the specified amount (`0.0`-`1.0`)"""
|
|
572
|
+
self.r, self.g, self.b, self.a = self.to_rgba(False).saturate(amount).values()
|
|
573
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
574
|
+
|
|
575
|
+
def desaturate(self, amount: float) -> "hexa":
|
|
576
|
+
"""Decreases the colors saturation by the specified amount (`0.0`-`1.0`)"""
|
|
577
|
+
self.r, self.g, self.b, self.a = self.to_rgba(False).desaturate(amount).values()
|
|
578
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
579
|
+
|
|
580
|
+
def rotate(self, degrees: int) -> "hexa":
|
|
581
|
+
"""Rotates the colors hue by the specified number of degrees"""
|
|
582
|
+
self.r, self.g, self.b, self.a = self.to_rgba(False).rotate(degrees).values()
|
|
583
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
584
|
+
|
|
585
|
+
def invert(self, invert_alpha: bool = False) -> "hexa":
|
|
586
|
+
"""Inverts the color by rotating hue by 180 degrees and inverting lightness"""
|
|
587
|
+
self.r, self.g, self.b, self.a = self.to_rgba(False).invert().values()
|
|
588
|
+
if invert_alpha:
|
|
589
|
+
self.a = 1 - self.a
|
|
590
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
591
|
+
|
|
592
|
+
def grayscale(self) -> "hexa":
|
|
593
|
+
"""Converts the color to grayscale using the luminance formula"""
|
|
594
|
+
self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b)
|
|
595
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
596
|
+
|
|
597
|
+
def blend(self, other: "hexa", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
|
|
598
|
+
"""Blends the current color with another color using the specified ratio (`0.0`-`1.0`):
|
|
599
|
+
- if `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)
|
|
600
|
+
- if `ratio` is `0.5` it means 50% of both colors (1:1 mixture)
|
|
601
|
+
- if `ratio` is `1.0` it means 0% of the current color and 100% of the `other` color (0:2 mixture)"""
|
|
602
|
+
self.r, self.g, self.b, self.a = self.to_rgba(False).blend(Color.to_rgba(other), ratio, additive_alpha).values()
|
|
603
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
|
|
604
|
+
|
|
605
|
+
def is_dark(self) -> bool:
|
|
606
|
+
"""Returns `True` if the color is considered dark (`lightness < 50%`)"""
|
|
607
|
+
return self.to_hsla(False).is_dark()
|
|
608
|
+
|
|
609
|
+
def is_light(self) -> bool:
|
|
610
|
+
"""Returns `True` if the color is considered light (`lightness >= 50%`)"""
|
|
611
|
+
return self.to_hsla(False).is_light()
|
|
612
|
+
|
|
613
|
+
def is_grayscale(self) -> bool:
|
|
614
|
+
"""Returns `True` if the color is grayscale (`saturation == 0`)"""
|
|
615
|
+
return self.to_hsla(False).is_grayscale()
|
|
616
|
+
|
|
617
|
+
def is_opaque(self) -> bool:
|
|
618
|
+
"""Returns `True` if the color has no transparency (`alpha == 1.0`)"""
|
|
619
|
+
return self.to_hsla(False).is_opaque()
|
|
620
|
+
|
|
621
|
+
def with_alpha(self, alpha: float) -> "hexa":
|
|
622
|
+
"""Returns a new color with the specified alpha value"""
|
|
623
|
+
if not (isinstance(alpha, (int, float)) and 0 <= alpha <= 1):
|
|
624
|
+
raise ValueError("'alpha' must be in [0.0, 1.0]")
|
|
625
|
+
return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(alpha * 255):02X}" if alpha else ""}')
|
|
626
|
+
|
|
627
|
+
def complementary(self) -> "hexa":
|
|
628
|
+
"""Returns the complementary color (180 degrees on the color wheel)"""
|
|
629
|
+
return self.to_hsla(False).complementary().to_hexa()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class Color:
|
|
633
|
+
|
|
634
|
+
@staticmethod
|
|
635
|
+
def is_valid_rgba(color: str | list | tuple | dict, allow_alpha: bool = True) -> bool:
|
|
636
|
+
try:
|
|
637
|
+
if isinstance(color, rgba):
|
|
638
|
+
return True
|
|
639
|
+
elif isinstance(color, (list, tuple)):
|
|
640
|
+
if allow_alpha and Color.has_alpha(color):
|
|
641
|
+
return (
|
|
642
|
+
0 <= color[0] <= 255
|
|
643
|
+
and 0 <= color[1] <= 255
|
|
644
|
+
and 0 <= color[2] <= 255
|
|
645
|
+
and (0 <= color[3] <= 1 or color[3] is None)
|
|
646
|
+
)
|
|
647
|
+
return 0 <= color[0] <= 255 and 0 <= color[1] <= 255 and 0 <= color[2] <= 255
|
|
648
|
+
elif isinstance(color, dict):
|
|
649
|
+
if allow_alpha and Color.has_alpha(color):
|
|
650
|
+
return (
|
|
651
|
+
0 <= color["r"] <= 255
|
|
652
|
+
and 0 <= color["g"] <= 255
|
|
653
|
+
and 0 <= color["b"] <= 255
|
|
654
|
+
and (0 <= color["a"] <= 1 or color["a"] is None)
|
|
655
|
+
)
|
|
656
|
+
return 0 <= color["r"] <= 255 and 0 <= color["g"] <= 255 and 0 <= color["b"] <= 255
|
|
657
|
+
elif isinstance(color, str):
|
|
658
|
+
return bool(_re.fullmatch(Regex.rgba_str(allow_alpha=allow_alpha), color))
|
|
659
|
+
return False
|
|
660
|
+
except Exception:
|
|
661
|
+
return False
|
|
662
|
+
|
|
663
|
+
@staticmethod
|
|
664
|
+
def is_valid_hsla(color: str | list | tuple | dict, allow_alpha: bool = True) -> bool:
|
|
665
|
+
try:
|
|
666
|
+
if isinstance(color, hsla):
|
|
667
|
+
return True
|
|
668
|
+
elif isinstance(color, (list, tuple)):
|
|
669
|
+
if allow_alpha and Color.has_alpha(color):
|
|
670
|
+
return (
|
|
671
|
+
0 <= color[0] <= 360
|
|
672
|
+
and 0 <= color[1] <= 100
|
|
673
|
+
and 0 <= color[2] <= 100
|
|
674
|
+
and (0 <= color[3] <= 1 or color[3] is None)
|
|
675
|
+
)
|
|
676
|
+
else:
|
|
677
|
+
return 0 <= color[0] <= 360 and 0 <= color[1] <= 100 and 0 <= color[2] <= 100
|
|
678
|
+
elif isinstance(color, dict):
|
|
679
|
+
if allow_alpha and Color.has_alpha(color):
|
|
680
|
+
return (
|
|
681
|
+
0 <= color["h"] <= 360
|
|
682
|
+
and 0 <= color["s"] <= 100
|
|
683
|
+
and 0 <= color["l"] <= 100
|
|
684
|
+
and (0 <= color["a"] <= 1 or color["a"] is None)
|
|
685
|
+
)
|
|
686
|
+
else:
|
|
687
|
+
return 0 <= color["h"] <= 360 and 0 <= color["s"] <= 100 and 0 <= color["l"] <= 100
|
|
688
|
+
elif isinstance(color, str):
|
|
689
|
+
return bool(_re.fullmatch(Regex.hsla_str(allow_alpha=allow_alpha), color))
|
|
690
|
+
except Exception:
|
|
691
|
+
return False
|
|
692
|
+
|
|
693
|
+
@staticmethod
|
|
694
|
+
def is_valid_hexa(color: str | int, allow_alpha: bool = True, get_prefix: bool = False) -> bool | tuple[bool, str]:
|
|
695
|
+
try:
|
|
696
|
+
if isinstance(color, hexa):
|
|
697
|
+
return (True, "#")
|
|
698
|
+
elif isinstance(color, int):
|
|
699
|
+
is_valid = 0 <= color <= (0xFFFFFFFF if allow_alpha else 0xFFFFFF)
|
|
700
|
+
return (is_valid, "0x") if get_prefix else is_valid
|
|
701
|
+
elif isinstance(color, str):
|
|
702
|
+
color, prefix = (
|
|
703
|
+
(color[1:], "#")
|
|
704
|
+
if color.startswith("#")
|
|
705
|
+
else (color[2:], "0x") if color.startswith("0x") else (color, None)
|
|
706
|
+
)
|
|
707
|
+
return (
|
|
708
|
+
(bool(_re.fullmatch(Regex.hexa_str(allow_alpha=allow_alpha), color)), prefix)
|
|
709
|
+
if get_prefix
|
|
710
|
+
else bool(_re.fullmatch(Regex.hexa_str(allow_alpha=allow_alpha), color))
|
|
711
|
+
)
|
|
712
|
+
except Exception:
|
|
713
|
+
return (False, None) if get_prefix else False
|
|
714
|
+
|
|
715
|
+
@staticmethod
|
|
716
|
+
def is_valid(color: str | list | tuple | dict, allow_alpha: bool = True) -> bool:
|
|
717
|
+
return (
|
|
718
|
+
Color.is_valid_rgba(color, allow_alpha)
|
|
719
|
+
or Color.is_valid_hsla(color, allow_alpha)
|
|
720
|
+
or Color.is_valid_hexa(color, allow_alpha)
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
@staticmethod
|
|
724
|
+
def has_alpha(color: rgba | hsla | hexa) -> bool:
|
|
725
|
+
"""Check if the given color has an alpha channel.\n
|
|
726
|
+
---------------------------------------------------------------------------
|
|
727
|
+
Input a RGBA, HSLA or HEXA color as `color`.
|
|
728
|
+
Returns `True` if the color has an alpha channel and `False` otherwise."""
|
|
729
|
+
if isinstance(color, (rgba, hsla, hexa)):
|
|
730
|
+
return color.has_alpha()
|
|
731
|
+
if Color.is_valid_hexa(color):
|
|
732
|
+
if isinstance(color, str):
|
|
733
|
+
if color.startswith("#"):
|
|
734
|
+
color = color[1:]
|
|
735
|
+
return len(color) == 4 or len(color) == 8
|
|
736
|
+
if isinstance(color, int):
|
|
737
|
+
hex_length = len(f"{color:X}")
|
|
738
|
+
return hex_length == 4 or hex_length == 8
|
|
739
|
+
elif isinstance(color, (list, tuple)) and len(color) == 4 and color[3] is not None:
|
|
740
|
+
return True
|
|
741
|
+
elif isinstance(color, dict) and len(color) == 4 and color["a"] is not None:
|
|
742
|
+
return True
|
|
743
|
+
return False
|
|
744
|
+
|
|
745
|
+
@staticmethod
|
|
746
|
+
def to_rgba(color: hsla | hexa) -> rgba:
|
|
747
|
+
"""Will try to convert any color type to a color of type RGBA."""
|
|
748
|
+
if isinstance(color, (hsla, hexa)):
|
|
749
|
+
return color.to_rgba()
|
|
750
|
+
elif Color.is_valid_hsla(color):
|
|
751
|
+
return hsla(*color).to_rgba() if Color.has_alpha(color) else hsla(color[0], color[1], color[2]).to_rgba()
|
|
752
|
+
elif Color.is_valid_hexa(color):
|
|
753
|
+
return hexa(color).to_rgba()
|
|
754
|
+
elif Color.is_valid_rgba(color):
|
|
755
|
+
return (
|
|
756
|
+
color
|
|
757
|
+
if isinstance(color, rgba)
|
|
758
|
+
else (rgba(*color) if Color.has_alpha(color) else rgba(color[0], color[1], color[2]))
|
|
759
|
+
)
|
|
760
|
+
raise ValueError(f"Invalid color format '{color}'")
|
|
761
|
+
|
|
762
|
+
@staticmethod
|
|
763
|
+
def to_hsla(color: rgba | hexa) -> hsla:
|
|
764
|
+
"""Will try to convert any color type to a color of type HSLA."""
|
|
765
|
+
if isinstance(color, (rgba, hexa)):
|
|
766
|
+
return color.to_hsla()
|
|
767
|
+
elif Color.is_valid_rgba(color):
|
|
768
|
+
return rgba(*color).to_hsla() if Color.has_alpha(color) else rgba(color[0], color[1], color[2]).to_hsla()
|
|
769
|
+
elif Color.is_valid_hexa(color):
|
|
770
|
+
return hexa(color).to_hsla()
|
|
771
|
+
elif Color.is_valid_hsla(color):
|
|
772
|
+
return (
|
|
773
|
+
color
|
|
774
|
+
if isinstance(color, hsla)
|
|
775
|
+
else (hsla(*color) if Color.has_alpha(color) else hsla(color[0], color[1], color[2]))
|
|
776
|
+
)
|
|
777
|
+
raise ValueError(f"Invalid color format '{color}'")
|
|
778
|
+
|
|
779
|
+
@staticmethod
|
|
780
|
+
def to_hexa(color: rgba | hsla) -> hexa:
|
|
781
|
+
"""Will try to convert any color type to a color of type HEXA."""
|
|
782
|
+
if isinstance(color, (rgba, hsla)):
|
|
783
|
+
return color.to_hexa()
|
|
784
|
+
elif Color.is_valid_rgba(color):
|
|
785
|
+
return rgba(*color).to_hexa() if Color.has_alpha(color) else rgba(color[0], color[1], color[2]).to_hexa()
|
|
786
|
+
elif Color.is_valid_hsla(color):
|
|
787
|
+
return hsla(*color).to_hexa() if Color.has_alpha(color) else hsla(color[0], color[1], color[2]).to_hexa()
|
|
788
|
+
elif Color.is_valid_hexa(color):
|
|
789
|
+
return color if isinstance(color, hexa) else hexa(f"#{color}")
|
|
790
|
+
raise ValueError(f"Invalid color format '{color}'")
|
|
791
|
+
|
|
792
|
+
@staticmethod
|
|
793
|
+
def str_to_rgba(string: str, only_first: bool = False) -> rgba | list[rgba] | None:
|
|
794
|
+
"""Will try to recognize RGBA colors inside a string and output the found ones as RGBA objects.\n
|
|
795
|
+
--------------------------------------------------------------------------------------------------
|
|
796
|
+
If `only_first` is `True` only the first found color will be returned (not as a list)."""
|
|
797
|
+
if only_first:
|
|
798
|
+
match = _re.search(Regex.rgb_str(allow_alpha=True), string)
|
|
799
|
+
if not match:
|
|
800
|
+
return None
|
|
801
|
+
m = match.groups()
|
|
802
|
+
return rgba(
|
|
803
|
+
int(m[0]),
|
|
804
|
+
int(m[1]),
|
|
805
|
+
int(m[2]),
|
|
806
|
+
((int(m[3]) if "." not in m[3] else float(m[3])) if m[3] else None),
|
|
807
|
+
)
|
|
808
|
+
else:
|
|
809
|
+
matches = _re.findall(Regex.rgb_str(allow_alpha=True), string)
|
|
810
|
+
if not matches:
|
|
811
|
+
return None
|
|
812
|
+
return [
|
|
813
|
+
rgba(
|
|
814
|
+
int(m[0]),
|
|
815
|
+
int(m[1]),
|
|
816
|
+
int(m[2]),
|
|
817
|
+
((int(m[3]) if "." not in m[3] else float(m[3])) if m[3] else None),
|
|
818
|
+
)
|
|
819
|
+
for m in matches
|
|
820
|
+
]
|
|
821
|
+
|
|
822
|
+
@staticmethod
|
|
823
|
+
def rgba_to_hex_int(
|
|
824
|
+
r: int,
|
|
825
|
+
g: int,
|
|
826
|
+
b: int,
|
|
827
|
+
a: float = None,
|
|
828
|
+
preserve_original: bool = False,
|
|
829
|
+
) -> int:
|
|
830
|
+
"""Convert RGBA channels to a HEXA integer (alpha is optional).\n
|
|
831
|
+
--------------------------------------------------------------------------------------------
|
|
832
|
+
To preserve leading zeros, the function will add a `1` at the beginning, if the HEX integer
|
|
833
|
+
would start with a `0`.
|
|
834
|
+
This could affect the color a little bit, but will make sure, that it won't be interpreted
|
|
835
|
+
as a completely different color, when initializing it as a `hexa()` color or changing it
|
|
836
|
+
back to RGBA using `Color.hex_int_to_rgba()`.\n
|
|
837
|
+
⇾ You can disable this behavior by setting `preserve_original` to `True`"""
|
|
838
|
+
r = max(0, min(255, int(r)))
|
|
839
|
+
g = max(0, min(255, int(g)))
|
|
840
|
+
b = max(0, min(255, int(b)))
|
|
841
|
+
if a is not None:
|
|
842
|
+
a = max(0, min(255, int(a * 255)))
|
|
843
|
+
hex_int = (r << 24) | (g << 16) | (b << 8) | a
|
|
844
|
+
if not preserve_original and r == 0:
|
|
845
|
+
hex_int |= 0x01000000
|
|
846
|
+
else:
|
|
847
|
+
hex_int = (r << 16) | (g << 8) | b
|
|
848
|
+
if not preserve_original and (hex_int & 0xF00000) == 0:
|
|
849
|
+
hex_int |= 0x010000
|
|
850
|
+
return hex_int
|
|
851
|
+
|
|
852
|
+
@staticmethod
|
|
853
|
+
def hex_int_to_rgba(hex_int: int, preserve_original: bool = False) -> tuple[int, int, int, float | None]:
|
|
854
|
+
"""Convert a HEX integer to RGBA channels.\n
|
|
855
|
+
-------------------------------------------------------------------------------------------
|
|
856
|
+
If the red channel is `1` after conversion, it will be set to `0`, because when converting
|
|
857
|
+
from RGBA to a HEX integer, the first `0` will be set to `1` to preserve leading zeros.
|
|
858
|
+
This is the correction, so the color doesn't even look slightly different.\n
|
|
859
|
+
⇾ You can disable this behavior by setting `preserve_original` to `True`"""
|
|
860
|
+
if not isinstance(hex_int, int):
|
|
861
|
+
raise ValueError("Input must be an integer")
|
|
862
|
+
hex_str = f"{hex_int:x}"
|
|
863
|
+
if len(hex_str) <= 6:
|
|
864
|
+
hex_str = hex_str.zfill(6)
|
|
865
|
+
return (
|
|
866
|
+
r if (r := int(hex_str[0:2], 16)) != 1 or preserve_original else 0,
|
|
867
|
+
int(hex_str[2:4], 16),
|
|
868
|
+
int(hex_str[4:6], 16),
|
|
869
|
+
None,
|
|
870
|
+
)
|
|
871
|
+
elif len(hex_str) <= 8:
|
|
872
|
+
hex_str = hex_str.zfill(8)
|
|
873
|
+
return (
|
|
874
|
+
r if (r := int(hex_str[0:2], 16)) != 1 or preserve_original else 0,
|
|
875
|
+
int(hex_str[2:4], 16),
|
|
876
|
+
int(hex_str[4:6], 16),
|
|
877
|
+
int(hex_str[6:8], 16) / 255.0,
|
|
878
|
+
)
|
|
879
|
+
else:
|
|
880
|
+
raise ValueError(f"Invalid HEX integer '0x{hex_str}': expected in range [0x000000, 0xFFFFFF]")
|
|
881
|
+
|
|
882
|
+
@staticmethod
|
|
883
|
+
def luminance(r: int, g: int, b: int, output_type: type = None) -> int | float:
|
|
884
|
+
"""Gets the colors luminance using the luminance formula.\n
|
|
885
|
+
------------------------------------------------------------
|
|
886
|
+
The param `output_type` can be set to:
|
|
887
|
+
- `int` =⠀integer in [0, 100]
|
|
888
|
+
- `float` =⠀float in [0.0, 1.0]
|
|
889
|
+
- `None` =⠀integer in [0, 255]"""
|
|
890
|
+
r, g, b = r / 255.0, g / 255.0, b / 255.0
|
|
891
|
+
if r < 0.03928:
|
|
892
|
+
r = r / 12.92
|
|
893
|
+
else:
|
|
894
|
+
r = ((r + 0.055) / 1.055) ** 2.4
|
|
895
|
+
if g < 0.03928:
|
|
896
|
+
g = g / 12.92
|
|
897
|
+
else:
|
|
898
|
+
g = ((g + 0.055) / 1.055) ** 2.4
|
|
899
|
+
if b < 0.03928:
|
|
900
|
+
b = b / 12.92
|
|
901
|
+
else:
|
|
902
|
+
b = ((b + 0.055) / 1.055) ** 2.4
|
|
903
|
+
l = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
904
|
+
return round(l * 100) if isinstance(output_type, int) else round(l * 255) if output_type is None else l
|
|
905
|
+
|
|
906
|
+
@staticmethod
|
|
907
|
+
def text_color_for_on_bg(
|
|
908
|
+
text_bg_color: rgba | hexa = "#FFF",
|
|
909
|
+
) -> rgba | hexa:
|
|
910
|
+
was_hexa, was_int = Color.is_valid_hexa(text_bg_color), isinstance(text_bg_color, int)
|
|
911
|
+
text_bg_color = Color.to_rgba(text_bg_color)
|
|
912
|
+
brightness = 0.2126 * text_bg_color[0] + 0.7152 * text_bg_color[1] + 0.0722 * text_bg_color[2]
|
|
913
|
+
return (
|
|
914
|
+
(hexa("#FFF") if was_hexa else rgba(255, 255, 255))
|
|
915
|
+
if brightness < 128
|
|
916
|
+
else ((0x000 if was_int else hexa("#000")) if was_hexa else rgba(0, 0, 0))
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
@staticmethod
|
|
920
|
+
def adjust_lightness(color: rgba | hexa, lightness_change: float) -> rgba | hexa:
|
|
921
|
+
"""In- or decrease the lightness of the input color.\n
|
|
922
|
+
-----------------------------------------------------------------------------------------------------
|
|
923
|
+
- color (rgba|hexa): HEX or RGBA color
|
|
924
|
+
- lightness_change (float): float between -1.0 (darken by `100%`) and 1.0 (lighten by `100%`)\n
|
|
925
|
+
-----------------------------------------------------------------------------------------------------
|
|
926
|
+
returns (rgba|hexa): the adjusted color in the format of the input color"""
|
|
927
|
+
was_hexa = Color.is_valid_hexa(color)
|
|
928
|
+
color = Color.to_hsla(color)
|
|
929
|
+
h, s, l, a = (
|
|
930
|
+
color[0],
|
|
931
|
+
color[1],
|
|
932
|
+
color[2],
|
|
933
|
+
color[3] if Color.has_alpha(color) else None,
|
|
934
|
+
)
|
|
935
|
+
l = int(max(0, min(100, l + lightness_change * 100)))
|
|
936
|
+
return Color.to_hexa((h, s, l, a)) if was_hexa else Color.to_rgba((h, s, l, a))
|
|
937
|
+
|
|
938
|
+
@staticmethod
|
|
939
|
+
def adjust_saturation(color: rgba | hexa, saturation_change: float) -> rgba | hexa:
|
|
940
|
+
"""In- or decrease the saturation of the input color.\n
|
|
941
|
+
-----------------------------------------------------------------------------------------------------------
|
|
942
|
+
- color (rgba|hexa): HEX or RGBA color
|
|
943
|
+
- saturation_change (float): float between -1.0 (saturate by `100%`) and 1.0 (desaturate by `100%`)\n
|
|
944
|
+
-----------------------------------------------------------------------------------------------------------
|
|
945
|
+
returns (rgba|hexa): the adjusted color in the format of the input color"""
|
|
946
|
+
was_hexa = Color.is_valid_hexa(color)
|
|
947
|
+
color = Color.to_hsla(color)
|
|
948
|
+
h, s, l, a = (
|
|
949
|
+
color[0],
|
|
950
|
+
color[1],
|
|
951
|
+
color[2],
|
|
952
|
+
color[3] if Color.has_alpha(color) else None,
|
|
953
|
+
)
|
|
954
|
+
s = int(max(0, min(100, s + saturation_change * 100)))
|
|
955
|
+
return Color.to_hexa((h, s, l, a)) if was_hexa else Color.to_rgba((h, s, l, a))
|