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