hadalized 0.4.0__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.
- hadalized/__init__.py +1 -0
- hadalized/__main__.py +6 -0
- hadalized/base.py +137 -0
- hadalized/cache.py +121 -0
- hadalized/cli/__init__.py +1 -0
- hadalized/cli/main.py +221 -0
- hadalized/color.py +595 -0
- hadalized/config.py +442 -0
- hadalized/const.py +6 -0
- hadalized/convert.py.bak +1903 -0
- hadalized/homedirs.py +69 -0
- hadalized/options.py +134 -0
- hadalized/palette.py +133 -0
- hadalized/templates/colors.html +21 -0
- hadalized/templates/config.toml +15 -0
- hadalized/templates/generic.txt +1 -0
- hadalized/templates/item.html +1 -0
- hadalized/templates/lua_module.lua +6 -0
- hadalized/templates/model_dump.json +1 -0
- hadalized/templates/neovim.lua +24 -0
- hadalized/templates/neovim_palette.lua +7 -0
- hadalized/templates/palette.html +53 -0
- hadalized/templates/palette_info.json +1 -0
- hadalized/templates/palette_test.toml +10 -0
- hadalized/templates/starship-all.toml +98 -0
- hadalized/templates/starship.toml +233 -0
- hadalized/templates/wezterm.toml +45 -0
- hadalized/web.py +168 -0
- hadalized/writer.py +243 -0
- hadalized-0.4.0.dist-info/METADATA +79 -0
- hadalized-0.4.0.dist-info/RECORD +33 -0
- hadalized-0.4.0.dist-info/WHEEL +4 -0
- hadalized-0.4.0.dist-info/entry_points.txt +4 -0
hadalized/convert.py.bak
ADDED
|
@@ -0,0 +1,1903 @@
|
|
|
1
|
+
"""
|
|
2
|
+
https://bottosson.github.io/posts/oklab/ for general information explaining
|
|
3
|
+
the oklab space. Exerpts below.
|
|
4
|
+
|
|
5
|
+
A color in Oklab is represented with three coordinates, similar to how CIELAB
|
|
6
|
+
works, but with better perceptual properties. Oklab uses a D65 whitepoint,
|
|
7
|
+
since this is what sRGB and other common color spaces use.
|
|
8
|
+
The three coordinates are:
|
|
9
|
+
|
|
10
|
+
L – perceived lightness
|
|
11
|
+
a – how green/red the color is
|
|
12
|
+
b – how blue/yellow the color is
|
|
13
|
+
|
|
14
|
+
For many operations, Lab-coordinates can be used directly, but they can also
|
|
15
|
+
be transformed into polar form, with the coordinates lightness, chroma and hue.
|
|
16
|
+
|
|
17
|
+
LAB <-> LCh
|
|
18
|
+
|
|
19
|
+
C = sqrt(a^2 + b^2)
|
|
20
|
+
|
|
21
|
+
h∘ = atan2(b, a)
|
|
22
|
+
|
|
23
|
+
From C, and h∘ the coordinates a, b can be computed like this:
|
|
24
|
+
a = C cos(h∘)
|
|
25
|
+
b = C sin(h∘)
|
|
26
|
+
|
|
27
|
+
Given a vector v in XYZ coordinates, with M_1 and M_2 below, the XYZ -> Lab
|
|
28
|
+
transformation is
|
|
29
|
+
M_2((M_1v)^{1/3})
|
|
30
|
+
That is first the XYZ coordinates are converted to "approximate cone responses"
|
|
31
|
+
(lms), then the cubed root of each vector element taken, then the Lab
|
|
32
|
+
transform via M_2.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
import math
|
|
36
|
+
import sys
|
|
37
|
+
from pprint import pprint
|
|
38
|
+
from typing import Callable, ClassVar, NamedTuple, Self
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Typedefs
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
type ScalarFunc = Callable[[float], float]
|
|
45
|
+
type Vector3dPlus = tuple[float, float, float, *tuple[float | None, ...]]
|
|
46
|
+
"""A tuple with at least 3 floats."""
|
|
47
|
+
|
|
48
|
+
type Vector2d = tuple[float, float]
|
|
49
|
+
type Vector3d = tuple[float, float, float]
|
|
50
|
+
type VectorNd = tuple[float, ...]
|
|
51
|
+
|
|
52
|
+
type Matrix3d = tuple[
|
|
53
|
+
Vector3dPlus,
|
|
54
|
+
Vector3dPlus,
|
|
55
|
+
Vector3dPlus,
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
type Matrix3x2 = tuple[
|
|
60
|
+
Vector2d,
|
|
61
|
+
Vector2d,
|
|
62
|
+
Vector2d,
|
|
63
|
+
]
|
|
64
|
+
"""Linar transformations from R^2 -> R^3"""
|
|
65
|
+
|
|
66
|
+
type Matrix3x3 = tuple[
|
|
67
|
+
Vector3d,
|
|
68
|
+
Vector3d,
|
|
69
|
+
Vector3d,
|
|
70
|
+
]
|
|
71
|
+
"""Linar transformations from R^3 -> R^3"""
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# basic helper utils
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def _hex(val: int) -> str:
|
|
77
|
+
return hex(val)[2:].lower()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _iround(val: float) -> int:
|
|
81
|
+
"""Integer rounding."""
|
|
82
|
+
ret = int(val)
|
|
83
|
+
if (val - ret) >= 0.5:
|
|
84
|
+
ret += 1
|
|
85
|
+
return ret
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def to_byte(val: float) -> int:
|
|
89
|
+
"""Map the unit interval to the nearest 8 bit integer."""
|
|
90
|
+
val = 255*max(val, 0)
|
|
91
|
+
ret = int(val)
|
|
92
|
+
if (val - ret) >= 0.5:
|
|
93
|
+
ret += 1
|
|
94
|
+
return min(255, ret)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _ceil(f, num_digits=0):
|
|
99
|
+
f = math.ceil(f * 10**num_digits) / 10.0**num_digits
|
|
100
|
+
return float(format(f, "." + str(num_digits) + "f"))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _floor(f, num_digits=0):
|
|
104
|
+
f = math.floor(f * 10**num_digits) / 10.0**num_digits
|
|
105
|
+
return float(format(f, "." + str(num_digits) + "f"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def dot(v: Vector3dPlus, w: Vector3dPlus)-> float:
|
|
110
|
+
"""Dot product of two vectors in R^3."""
|
|
111
|
+
return v[0]*w[0] + v[1]*w[1] + v[2]*w[2]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def square(val: float) -> float:
|
|
115
|
+
"""Squarethe input."""
|
|
116
|
+
return val**2
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cube(val: float) -> float:
|
|
120
|
+
"""Cube the input."""
|
|
121
|
+
return math.pow(val, 3)
|
|
122
|
+
|
|
123
|
+
def square3d(val: Vector3d) -> Vector3d:
|
|
124
|
+
"""Square each coordinate in a vector in R^3."""
|
|
125
|
+
return (val[0]**2, val[1]**2, val[2]**2)
|
|
126
|
+
|
|
127
|
+
def cube3d(val: Vector3d) -> Vector3d:
|
|
128
|
+
"""Cube each coordinate in a vector in R^3."""
|
|
129
|
+
return (val[0]**3, val[1]**3, val[2]**3)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def hue_to_ab(hue: float) -> tuple[float, float]:
|
|
133
|
+
"""Map cylindrical hue oklch value to an OKLAB (a, b) coordinate
|
|
134
|
+
on the unit circle."""
|
|
135
|
+
return (
|
|
136
|
+
math.cos(math.radians(hue)),
|
|
137
|
+
math.sin(math.radians(hue)),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def dotprod(v: VectorNd, w: VectorNd) -> float:
|
|
142
|
+
"""Take the dot produce of two vectors in R^n."""
|
|
143
|
+
match len(v):
|
|
144
|
+
case 3:
|
|
145
|
+
out = v[0]*w[0] + v[1]*w[1] + v[2]*w[2]
|
|
146
|
+
case 2:
|
|
147
|
+
out = v[0]*w[0] + v[1]*w[1]
|
|
148
|
+
case _:
|
|
149
|
+
out = sum(map(lambda x: x[0]*x[1], zip(v, w)))
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def dot2d(v: Vector2d, w: Vector2d) -> float:
|
|
154
|
+
"""Dot product in R^3"""
|
|
155
|
+
return v[0]*w[0] + v[1]*w[1]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def dot3d(v: Vector3d, w: Vector3d) -> float:
|
|
159
|
+
"""Dot product in R^3"""
|
|
160
|
+
return v[0]*w[0] + v[1]*w[1] + v[2]*w[2]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def linear_trans_2d_to_3d(M: Matrix3x2, v: Vector2d) -> Vector3d:
|
|
164
|
+
"""Linear transformation from R^2 -> R^3."""
|
|
165
|
+
return (
|
|
166
|
+
dot2d(M[0], v),
|
|
167
|
+
dot2d(M[1], v),
|
|
168
|
+
dot2d(M[2], v),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def linear_trans_3d_to_3d(M: Matrix3x3, v: Vector3d) -> Vector3d:
|
|
173
|
+
"""Linear transformation from R^3 -> R^3."""
|
|
174
|
+
return (
|
|
175
|
+
dot3d(M[0], v),
|
|
176
|
+
dot3d(M[1], v),
|
|
177
|
+
dot3d(M[2], v),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def vmap(func: ScalarFunc, vector: VectorNd) -> VectorNd:
|
|
182
|
+
"""Map a scalar function across each element of the vector.
|
|
183
|
+
"""
|
|
184
|
+
return tuple(map(func, vector))
|
|
185
|
+
|
|
186
|
+
def vmul3d(v: Vector3d, w: Vector3d) -> Vector3d:
|
|
187
|
+
return (
|
|
188
|
+
v[0]*w[0],
|
|
189
|
+
v[1]*w[1],
|
|
190
|
+
v[2]*w[2],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def vadd3d(v: Vector3d, w: Vector3d) -> Vector3d:
|
|
195
|
+
return (
|
|
196
|
+
v[0]+w[0],
|
|
197
|
+
v[1]+w[1],
|
|
198
|
+
v[2]+w[2],
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def vmap2d(func: ScalarFunc, vector: Vector2d) -> Vector2d:
|
|
203
|
+
"""Map a scalar function across each element of the vector.
|
|
204
|
+
"""
|
|
205
|
+
return (
|
|
206
|
+
func(vector[0]),
|
|
207
|
+
func(vector[1]),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def vmap3d(func: ScalarFunc, vector: Vector3d) -> Vector3d:
|
|
211
|
+
"""Map a scalar function across each element of the vector.
|
|
212
|
+
"""
|
|
213
|
+
return (
|
|
214
|
+
func(vector[0]),
|
|
215
|
+
func(vector[1]),
|
|
216
|
+
func(vector[2]),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def smul3d(scalar: float, vector: Vector3d) -> Vector3d:
|
|
220
|
+
"""Multiple a scalar value to a vector in R^3."""
|
|
221
|
+
return (scalar*vector[0], scalar*vector[1], scalar*vector[2])
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# functional rewrite
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
# Usually when percentage values have a numeric equivalent in CSS, 100% is
|
|
229
|
+
# equal to the number 1. This is not the case for all of the oklch
|
|
230
|
+
# component values. Here, 100% is equal to 0.4 for the C value.
|
|
231
|
+
|
|
232
|
+
class Vector(NamedTuple):
|
|
233
|
+
"""Base class for color vectors together with an optional alpha channel."""
|
|
234
|
+
x: float
|
|
235
|
+
y: float
|
|
236
|
+
z: float
|
|
237
|
+
alpha: float | None = None
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def cast(cls, val: "Vector") -> Self:
|
|
241
|
+
"""Typecast between subclasses."""
|
|
242
|
+
return cls(val.x, val.y, val.z, val.alpha)
|
|
243
|
+
|
|
244
|
+
def map(self, func: ScalarFunc) -> Self:
|
|
245
|
+
"""Map a scalar function across each element of the vector.
|
|
246
|
+
The alpha channel stays the same.
|
|
247
|
+
"""
|
|
248
|
+
return self.__class__(
|
|
249
|
+
func(self.x),
|
|
250
|
+
func(self.y),
|
|
251
|
+
func(self.z),
|
|
252
|
+
self.alpha,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# def dot(self, vec: Vector3d) -> Self:
|
|
256
|
+
# """Take the dot product of two vectors. The alpha channel remains
|
|
257
|
+
# unchanged.
|
|
258
|
+
# """
|
|
259
|
+
# return self.__class__(
|
|
260
|
+
# self[0]*vec[0],
|
|
261
|
+
# self[1]*vec[1],
|
|
262
|
+
# self[2]*vec[2],
|
|
263
|
+
# self.alpha
|
|
264
|
+
# )
|
|
265
|
+
#
|
|
266
|
+
|
|
267
|
+
def T(self, M: Matrix3d) -> Self:
|
|
268
|
+
"""Apply a linear transformation M(v) where v is the instance.
|
|
269
|
+
Equivalent to vM^T when v is viewed as a row vector.
|
|
270
|
+
|
|
271
|
+
The alpha channel stays the same.
|
|
272
|
+
"""
|
|
273
|
+
return self.__class__(
|
|
274
|
+
dot(M[0], self),
|
|
275
|
+
dot(M[1], self),
|
|
276
|
+
dot(M[2], self),
|
|
277
|
+
self.alpha,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _lrgb_to_srgb(val: float) -> float:
|
|
286
|
+
"""Map linear RGB to standard RGB. The inverse of the SRGB transfer."""
|
|
287
|
+
if val >= 0.0031308:
|
|
288
|
+
ret = (1.055 * math.pow(val, 5 / 12)) - 0.055
|
|
289
|
+
else:
|
|
290
|
+
ret = 12.92 * val
|
|
291
|
+
return ret
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _srgb_to_lrgb(val: float):
|
|
295
|
+
"""Map a Standard RGB channel to linear RGB.
|
|
296
|
+
|
|
297
|
+
Also known as the "transfer" or "gamma" function.
|
|
298
|
+
Cf: https://en.wikipedia.org/wiki/SRGB
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
val: A scalar value representing a color channel in the unit interval.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
A linear intensity.
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
if val >= 0.04045:
|
|
308
|
+
ret = math.pow((val + 0.055) / 1.055, 2.4)
|
|
309
|
+
else:
|
|
310
|
+
ret = val / 12.92
|
|
311
|
+
return ret
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class Oklab(Vector):
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def lightness(self) -> float:
|
|
318
|
+
"""Lightness."""
|
|
319
|
+
return self.x
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def a(self) -> float:
|
|
323
|
+
return self.y
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def b(self) -> float:
|
|
327
|
+
return self.z
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class Oklch(Vector):
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def lightness(self) -> float:
|
|
334
|
+
"""Lightness."""
|
|
335
|
+
return self.x
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def chroma(self) -> float:
|
|
339
|
+
"""Chroma. Similar to saturation. 0.4 represents 100%."""
|
|
340
|
+
return self.y
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def hue(self) -> float:
|
|
344
|
+
"""Hue as degree measurement in [0, 360.]"""
|
|
345
|
+
return self.z
|
|
346
|
+
|
|
347
|
+
def normalized_ab(self) -> tuple[float, float]:
|
|
348
|
+
"""Get normalized a, b values that are provided in the conversion
|
|
349
|
+
to OKLab or in gamut clipping functions.
|
|
350
|
+
"""
|
|
351
|
+
a = math.cos(math.radians(self.hue))
|
|
352
|
+
b = math.sin(math.radians(self.hue))
|
|
353
|
+
return (a, b)
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def parse(cls, val: str) -> Self:
|
|
357
|
+
val, _, alpha = val.strip("oklch(").strip(")").partition("/")
|
|
358
|
+
comps: list[str] = val.strip().split(" ")
|
|
359
|
+
alpha = alpha.strip()
|
|
360
|
+
l_str = comps[0]
|
|
361
|
+
if l_str .endswith("%"):
|
|
362
|
+
lightness = float(l_str[:-1]) / 100
|
|
363
|
+
else:
|
|
364
|
+
lightness = float(l_str)
|
|
365
|
+
return cls(
|
|
366
|
+
lightness,
|
|
367
|
+
float(comps[1]),
|
|
368
|
+
float(comps[2]),
|
|
369
|
+
float(alpha) if alpha else None
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def hex(self) -> str:
|
|
374
|
+
return oklch_to_hex(self)
|
|
375
|
+
|
|
376
|
+
def __str__(self) -> str:
|
|
377
|
+
if self.alpha is not None:
|
|
378
|
+
post = f" / {self.alpha}"
|
|
379
|
+
else:
|
|
380
|
+
post = ""
|
|
381
|
+
return f"oklch({self.lightness} {self.chroma} {self.hue}{post})"
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def css(self) -> str:
|
|
385
|
+
return str(self)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class Rgb(Vector):
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def red(self) -> float:
|
|
393
|
+
return self.x
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def green(self) -> float:
|
|
397
|
+
"""Chroma. Similar to saturation. 0.4 represents 100%."""
|
|
398
|
+
return self.y
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def blue(self) -> float:
|
|
402
|
+
"""Hue as degree measurement in [0, 360.]"""
|
|
403
|
+
return self.z
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def hex(self) -> str:
|
|
407
|
+
return rgb_to_hex(self)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class M:
|
|
413
|
+
"""Container for linear transformations used to convert from OK to RGB."""
|
|
414
|
+
|
|
415
|
+
lab_to_lms: ClassVar[Matrix3d] = (
|
|
416
|
+
(1, +0.3963377774, +0.2158037573),
|
|
417
|
+
(1, -0.1055613458, -0.0638541728),
|
|
418
|
+
(1, -0.0894841775, -1.2914855480),
|
|
419
|
+
)
|
|
420
|
+
"""M_2^-1. First transformation applied to OKLAB coordinates."""
|
|
421
|
+
|
|
422
|
+
lms_to_rgb: ClassVar[Matrix3d] = (
|
|
423
|
+
(4.0767416621, -3.3077115913, 0.2309699292),
|
|
424
|
+
(-1.2684380046, 2.6097574011, -0.3413193965),
|
|
425
|
+
(-0.0041960863, -0.7034186147, 1.7076147010),
|
|
426
|
+
)
|
|
427
|
+
"""M_{xyz_to_srgb}M_1^{-1}
|
|
428
|
+
Second transformation applied to OKLAB coordinates."""
|
|
429
|
+
|
|
430
|
+
rgb_to_lms: ClassVar[Matrix3d] = (
|
|
431
|
+
(+0.4122214708, 0.5363325363, 0.0514459929),
|
|
432
|
+
(+0.2119034982, 0.6806995451, 0.1073969566),
|
|
433
|
+
(+0.0883024619, 0.2817188376, 0.6299787005),
|
|
434
|
+
)
|
|
435
|
+
"""Must be (M_{xyz_to_rgb}M_1^{-1})^{-1} = M_1M_{xyz_to_rgb}^{-1}
|
|
436
|
+
First transformation applied to RGB coordinates."""
|
|
437
|
+
|
|
438
|
+
lms_to_lab: ClassVar[Matrix3d] = (
|
|
439
|
+
(0.2104542553, 0.7936177850, -0.0040720468),
|
|
440
|
+
(1.9779984951, -2.4285922050, 0.4505937099),
|
|
441
|
+
(0.0259040371, 0.7827717662, -0.8086757660),
|
|
442
|
+
)
|
|
443
|
+
"""M_2 in original paper. Second transformation applied to RGB coordinates."""
|
|
444
|
+
|
|
445
|
+
xyz_to_lms: Matrix3d = (
|
|
446
|
+
(0.8189330101, 0.3618667424, -0.1288597137),
|
|
447
|
+
(0.0329845436, 0.9293118715, +0.0361456387),
|
|
448
|
+
(0.0482003018, 0.2643662691, +0.6338517070),
|
|
449
|
+
)
|
|
450
|
+
"""M_1 in original oklab definition."""
|
|
451
|
+
|
|
452
|
+
lms_to_xyz: Matrix3d = (
|
|
453
|
+
(1.22701, -0.5578, 0.281256),
|
|
454
|
+
(-0.0405802, 1.11226, -0.0716767),
|
|
455
|
+
(-0.0763813, -0.421482, 1.58616),
|
|
456
|
+
)
|
|
457
|
+
"""M_1^{-1}, the inverse of M_1. Precision issue?"""
|
|
458
|
+
|
|
459
|
+
xyz_to_rgb: Matrix3d = (
|
|
460
|
+
(3.24061, -1.5376, -0.498486),
|
|
461
|
+
(-0.969136, 1.87604, 0.041436),
|
|
462
|
+
(0.0556693, -0.203778, 1.05749),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
rgb_to_xyz: Matrix3d = (
|
|
466
|
+
(0.412437, 0.357629, 0.180404),
|
|
467
|
+
(0.212634, 0.715156, 0.0722104),
|
|
468
|
+
(0.0192626, 0.118984, 0.950053),
|
|
469
|
+
)
|
|
470
|
+
"""Inverse of M_{xyz_to_rgb}. Precision issue?"""
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def oklch_to_oklab(val: Oklch) -> Oklab:
|
|
477
|
+
(a, b), c = (val.normalized_ab(), val.chroma)
|
|
478
|
+
return Oklab(val.lightness, a * c, b * c, val.alpha)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def oklab_to_linear_rgb(val: Vector) -> Vector:
|
|
482
|
+
return Rgb.cast(
|
|
483
|
+
val.T(M.lab_to_lms)
|
|
484
|
+
.map(cube)
|
|
485
|
+
.T(M.lms_to_rgb)
|
|
486
|
+
# .map(_lrgb_to_srgb)
|
|
487
|
+
# .map(to_byte)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
def oklab_to_rgb(val: Vector) -> Rgb:
|
|
491
|
+
return Rgb.cast(
|
|
492
|
+
val.T(M.lab_to_lms)
|
|
493
|
+
.map(cube)
|
|
494
|
+
.T(M.lms_to_rgb)
|
|
495
|
+
.map(_lrgb_to_srgb)
|
|
496
|
+
.map(to_byte)
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# NOTE: original change
|
|
500
|
+
# _l, a, b = val
|
|
501
|
+
# l3 = math.pow(_l + 0.3963377774 * a + 0.2158037573 * b, 3)
|
|
502
|
+
# m3 = math.pow(_l - 0.1055613458 * a - 0.0638541728 * b, 3)
|
|
503
|
+
# s3 = math.pow(_l - 0.0894841775 * a - 1.2914855480 * b, 3)
|
|
504
|
+
#
|
|
505
|
+
#
|
|
506
|
+
# tr = _srgb_transfer_int
|
|
507
|
+
#
|
|
508
|
+
# return Rgb(
|
|
509
|
+
# red=_srgb_transfer_int(
|
|
510
|
+
# +4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3
|
|
511
|
+
# ),
|
|
512
|
+
# green=_srgb_transfer_int(
|
|
513
|
+
# -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3
|
|
514
|
+
# ),
|
|
515
|
+
# blue=_srgb_transfer_int(
|
|
516
|
+
# -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3
|
|
517
|
+
# ),
|
|
518
|
+
# )
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def oklab_to_oklch(val: Vector) -> Oklch:
|
|
522
|
+
lightness, a, b, alpha = val
|
|
523
|
+
return Oklch(
|
|
524
|
+
lightness,
|
|
525
|
+
math.sqrt(a**2 + b**2),
|
|
526
|
+
math.degrees(math.atan2(b, a)) % 360,
|
|
527
|
+
alpha,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def rgb_to_oklab(val: Vector) -> Oklab:
|
|
533
|
+
"""Map from RGB to OKLAB."""
|
|
534
|
+
return Oklab.cast(
|
|
535
|
+
val.map(lambda x: _srgb_to_lrgb(x / 255))
|
|
536
|
+
.T(M.rgb_to_lms)
|
|
537
|
+
.map(math.cbrt)
|
|
538
|
+
.T(M.lms_to_lab)
|
|
539
|
+
)
|
|
540
|
+
# Original
|
|
541
|
+
# red = _srgb_transfer_inverse(val.red / 255)
|
|
542
|
+
# green = _srgb_transfer_inverse(val.green / 255)
|
|
543
|
+
# blue = _srgb_transfer_inverse(val.blue / 255)
|
|
544
|
+
# exp = 1 / 3
|
|
545
|
+
#
|
|
546
|
+
# l_ = math.pow(
|
|
547
|
+
# 0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue,
|
|
548
|
+
# exp,
|
|
549
|
+
# )
|
|
550
|
+
# m_ = math.pow(
|
|
551
|
+
# 0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue,
|
|
552
|
+
# exp,
|
|
553
|
+
# )
|
|
554
|
+
# s_ = math.pow(
|
|
555
|
+
# 0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue,
|
|
556
|
+
# exp,
|
|
557
|
+
# )
|
|
558
|
+
#
|
|
559
|
+
# return Oklab(
|
|
560
|
+
# 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
|
561
|
+
# 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
|
562
|
+
# 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
|
|
563
|
+
# )
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def rgb_to_hex(val: Vector) -> str:
|
|
567
|
+
r, g, b, alpha = val
|
|
568
|
+
code = f"#{r:02x}{g:02x}{b:02x}"
|
|
569
|
+
if alpha is not None:
|
|
570
|
+
code += f"{to_byte(alpha):02x}"
|
|
571
|
+
return code
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def oklch_to_rgb(val: Oklch) -> Rgb:
|
|
576
|
+
return oklab_to_rgb(oklch_to_oklab(val))
|
|
577
|
+
|
|
578
|
+
def oklch_to_hex(val: Oklch) -> str:
|
|
579
|
+
return rgb_to_hex(oklab_to_rgb(oklch_to_oklab(val)))
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def hex_to_oklch(code: str) -> Oklch:
|
|
583
|
+
"""Map"""
|
|
584
|
+
rgb = parse_hex(code)
|
|
585
|
+
return oklab_to_oklch(rgb_to_oklab(rgb))
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def hex_to_oklch_css(code: str) -> str:
|
|
589
|
+
"""Map"""
|
|
590
|
+
return oklab_to_oklch(rgb_to_oklab(parse_hex(code))).css
|
|
591
|
+
|
|
592
|
+
def parse_hex(code: str) -> Rgb:
|
|
593
|
+
if code.startswith("#"):
|
|
594
|
+
code = code[1:]
|
|
595
|
+
if code.startswith("0x"):
|
|
596
|
+
code = code[2:]
|
|
597
|
+
return Rgb(
|
|
598
|
+
int(code[0:2], 16),
|
|
599
|
+
int(code[2:4], 16),
|
|
600
|
+
int(code[4:6], 16),
|
|
601
|
+
alpha=int(code[-2:], 16) / 255 if len(code) == 8 else None,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def run_test():
|
|
606
|
+
"""Test case consists of an oklch (user defined) and a hex value computed.
|
|
607
|
+
"""
|
|
608
|
+
tests = [
|
|
609
|
+
{
|
|
610
|
+
"desc": "chroma massively out of bounds",
|
|
611
|
+
"oklch": "oklch(0.6 0.50 25)",
|
|
612
|
+
"hex": "#ef0028",
|
|
613
|
+
"oklch_in_srgb": "oklch(60% 0.2433 25)",
|
|
614
|
+
"hex_in_srgb": "#ef0028",
|
|
615
|
+
"hex_to_oklch_color_picker": "oklch(60% 0.2432 25)",
|
|
616
|
+
"hex_in_srgb_to_oklch_color_picker": "oklch(60% 0.2432 25)",
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
"desc": "def in srgb",
|
|
620
|
+
"oklch": "oklch(0.6 0.1 25)",
|
|
621
|
+
"hex": "#b46762",
|
|
622
|
+
"oklch_in_srgb": "oklch(0.6 0.1 25)",
|
|
623
|
+
"hex_in_srgb": "#b46762",
|
|
624
|
+
"hex_to_oklch_color_picker": "oklch(59.9% 0.0998 24.4)",
|
|
625
|
+
"hex_in_srgb_to_oklch_color_picker": "oklch(59.9% 0.0998 24.4)",
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
"desc": "harmonized red 500 in p3",
|
|
629
|
+
"level": "500",
|
|
630
|
+
"hue": "Red",
|
|
631
|
+
"oklch": "oklch(0.73 0.21 20)",
|
|
632
|
+
"hex": "#ff5f6d",
|
|
633
|
+
"oklch_in_srgb": "oklch(0.73 0.1661 20)",
|
|
634
|
+
"hex_in_srgb": "#ff777b",
|
|
635
|
+
"hex_to_oklch_color_picker": "oklch(69.7% 0.194 18.6)",
|
|
636
|
+
"hex_in_srgb_to_oklch_color_picker": "oklch(73.1% 0.1661 20.3)",
|
|
637
|
+
},
|
|
638
|
+
]
|
|
639
|
+
import json
|
|
640
|
+
for num, case in enumerate(tests):
|
|
641
|
+
oklch = Oklch.parse(case["oklch"])
|
|
642
|
+
oklch_in_srgb = Oklch.parse(case["oklch_in_srgb"])
|
|
643
|
+
expected_hex = case["hex"]
|
|
644
|
+
actual_hex = oklch_to_hex(oklch)
|
|
645
|
+
text = f"""case {num}
|
|
646
|
+
{json.dumps(case, indent=4)}
|
|
647
|
+
Defined color = {oklch}
|
|
648
|
+
actual hex == expected hex: {actual_hex == expected_hex}
|
|
649
|
+
{actual_hex=}, {expected_hex=}
|
|
650
|
+
hex to oklch via fun = {hex_to_oklch(expected_hex)}
|
|
651
|
+
-------------------------------------------------------------"""
|
|
652
|
+
print(text)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
if __name__ == "__main__":
|
|
656
|
+
run_test()
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
# ---------------------------------------------------------------------------
|
|
661
|
+
# Original mess
|
|
662
|
+
# ---------------------------------------------------------------------------
|
|
663
|
+
#
|
|
664
|
+
# class Color:
|
|
665
|
+
#
|
|
666
|
+
# # These return a dummy color just so to eliminate an annoying warning.
|
|
667
|
+
# def to_RGB(self) -> "RGB":
|
|
668
|
+
# return RGB(0, 0, 0)
|
|
669
|
+
#
|
|
670
|
+
# def to_HEX(self) -> "HEX":
|
|
671
|
+
# return HEX("#000000")
|
|
672
|
+
#
|
|
673
|
+
# def to_OKLAB(self) -> "OKLAB":
|
|
674
|
+
# return OKLAB(0, 0, 0)
|
|
675
|
+
#
|
|
676
|
+
# def to_OKLCH(self) -> "OKLCH":
|
|
677
|
+
# return OKLCH(0, 0, 0)
|
|
678
|
+
#
|
|
679
|
+
# def is_in_gamut(self) -> bool:
|
|
680
|
+
# return self.to_RGB().is_in_gamut()
|
|
681
|
+
#
|
|
682
|
+
# def __str__(self):
|
|
683
|
+
# return ""
|
|
684
|
+
#
|
|
685
|
+
# # Checks that arg is color
|
|
686
|
+
# @staticmethod
|
|
687
|
+
# def _is_color(arg):
|
|
688
|
+
# if not isinstance(arg, Color):
|
|
689
|
+
# raise ValueError(f"Expected color, received '{type(arg)}'!")
|
|
690
|
+
#
|
|
691
|
+
# # Checks that two colors are close
|
|
692
|
+
# def is_close(self, other):
|
|
693
|
+
# return self.to_HEX().hex_code == other.to_HEX().hex_code
|
|
694
|
+
#
|
|
695
|
+
# # Addition gives the midpoint of the two colors in OKLAB space
|
|
696
|
+
# def __add__(self, other):
|
|
697
|
+
# self._is_color(other)
|
|
698
|
+
#
|
|
699
|
+
# return self.to_OKLAB() + other.to_OKLAB()
|
|
700
|
+
#
|
|
701
|
+
# # Negation gives the complement in OKLAB space
|
|
702
|
+
# def __neg__(self):
|
|
703
|
+
# return self.to_OKLAB().__neg__()
|
|
704
|
+
#
|
|
705
|
+
# # Subtraction gives the midpoint of self and complement of other
|
|
706
|
+
# def __sub__(self, other):
|
|
707
|
+
# self._is_color(other)
|
|
708
|
+
#
|
|
709
|
+
# return self.to_OKLAB() - other.to_OKLAB()
|
|
710
|
+
#
|
|
711
|
+
# # Pipe operator yields the euclidean distance between two colors
|
|
712
|
+
# def __or__(self, other):
|
|
713
|
+
# self._is_color(other)
|
|
714
|
+
#
|
|
715
|
+
# # Convert both colors to oklab:
|
|
716
|
+
# self = self.to_OKLAB()
|
|
717
|
+
# other = other.to_OKLAB()
|
|
718
|
+
#
|
|
719
|
+
# return math.pow(
|
|
720
|
+
# (self.l - other.l) ** 2 + (self.a - other.a) ** 2 + (self.b - other.b) ** 2,
|
|
721
|
+
# 0.5,
|
|
722
|
+
# )
|
|
723
|
+
#
|
|
724
|
+
#
|
|
725
|
+
#
|
|
726
|
+
# ###############################################################################
|
|
727
|
+
# #
|
|
728
|
+
# # Original license for:
|
|
729
|
+
# # - RGB.to_OKLAB()
|
|
730
|
+
# # - RGB._srgb_transfer_function()
|
|
731
|
+
# # - RGB._srgb_transfer_function_inv()
|
|
732
|
+
# # - OKLAB.to_RGB()
|
|
733
|
+
# #
|
|
734
|
+
# # Copyright (c) 2021 Björn Ottosson
|
|
735
|
+
# #
|
|
736
|
+
# # Permission is hereby granted, free of charge, to any person obtaining a
|
|
737
|
+
# # copy of this software and associated documentation files (the "Software"),
|
|
738
|
+
# # to deal in the Software without restriction, including without limitation
|
|
739
|
+
# # the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
740
|
+
# # and/or sell copies of the Software, and to permit persons to whom the
|
|
741
|
+
# # Software is furnished to do so, subject to the following conditions:
|
|
742
|
+
# #
|
|
743
|
+
# # The above copyright notice and this permission notice shall be
|
|
744
|
+
# # included in all copies or substantial portions of the Software.
|
|
745
|
+
# #
|
|
746
|
+
# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
747
|
+
# # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
748
|
+
# # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
749
|
+
# # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
750
|
+
# # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
751
|
+
# # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
752
|
+
# # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
753
|
+
# #
|
|
754
|
+
# ###############################################################################
|
|
755
|
+
#
|
|
756
|
+
#
|
|
757
|
+
# # RGB colors represented as triplets
|
|
758
|
+
# class RGB(Color):
|
|
759
|
+
#
|
|
760
|
+
# def __init__(self, red: int, green: int, blue: int):
|
|
761
|
+
# self.r = red
|
|
762
|
+
# self.g = green
|
|
763
|
+
# self.b = blue
|
|
764
|
+
#
|
|
765
|
+
# def __repr__(self) -> str:
|
|
766
|
+
# return f"rgb({self.r}, {self.g}, {self.b})"
|
|
767
|
+
#
|
|
768
|
+
# def is_close(self, other):
|
|
769
|
+
# return super().is_close(other)
|
|
770
|
+
#
|
|
771
|
+
# # Return type for addition and subtraction is type of first operand
|
|
772
|
+
# def __add__(self, other):
|
|
773
|
+
# return super().__add__(other).to_RGB()
|
|
774
|
+
#
|
|
775
|
+
# def __neg__(self):
|
|
776
|
+
# return super().__neg__().to_RGB()
|
|
777
|
+
#
|
|
778
|
+
# def __sub__(self, other):
|
|
779
|
+
# return super().__sub__(other).to_RGB()
|
|
780
|
+
#
|
|
781
|
+
# def __or__(self, other):
|
|
782
|
+
# return super().__or__(other)
|
|
783
|
+
#
|
|
784
|
+
# # Type Conversions
|
|
785
|
+
# def to_RGB(self) -> Self:
|
|
786
|
+
# return self
|
|
787
|
+
#
|
|
788
|
+
# def to_HEX(self):
|
|
789
|
+
# return HEX(
|
|
790
|
+
# "#{:0>2}{:0>2}{:0>2}".format(_hex(self.r), _hex(self.g), _hex(self.b))
|
|
791
|
+
# )
|
|
792
|
+
#
|
|
793
|
+
# # Functions for converting to linear RGB from standard RGB and vice versa
|
|
794
|
+
# @staticmethod
|
|
795
|
+
# def _srgb_transfer(val: float):
|
|
796
|
+
# """Convert linear RGB to standard RGB."""
|
|
797
|
+
# if val >= 0.0031308:
|
|
798
|
+
# ret = (1.055) * math.pow(val, 1.0 / 2.4) - 0.055
|
|
799
|
+
# else:
|
|
800
|
+
# ret = 12.92 * val
|
|
801
|
+
# return ret
|
|
802
|
+
#
|
|
803
|
+
# @staticmethod
|
|
804
|
+
# def _srgb_transfer_inverse(val: float):
|
|
805
|
+
# if val >= 0.04045:
|
|
806
|
+
# ret = math.pow((val + 0.055) / (1 + 0.055), 2.4)
|
|
807
|
+
# else:
|
|
808
|
+
# ret = val / 12.92
|
|
809
|
+
# return ret
|
|
810
|
+
#
|
|
811
|
+
# def to_OKLAB(self):
|
|
812
|
+
# """Map from RGB to OKLAB."""
|
|
813
|
+
# red = self._srgb_transfer_inverse(self.r / 255)
|
|
814
|
+
# green = self._srgb_transfer_inverse(self.g / 255)
|
|
815
|
+
# blue = self._srgb_transfer_inverse(self.b / 255)
|
|
816
|
+
# exp = 1 / 3
|
|
817
|
+
#
|
|
818
|
+
# l_ = math.pow(
|
|
819
|
+
# 0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue,
|
|
820
|
+
# exp,
|
|
821
|
+
# )
|
|
822
|
+
# m_ = math.pow(
|
|
823
|
+
# 0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue,
|
|
824
|
+
# exp,
|
|
825
|
+
# )
|
|
826
|
+
# s_ = math.pow(
|
|
827
|
+
# 0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue,
|
|
828
|
+
# exp,
|
|
829
|
+
# )
|
|
830
|
+
#
|
|
831
|
+
#
|
|
832
|
+
# return OKLAB(
|
|
833
|
+
# 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
|
834
|
+
# 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
|
835
|
+
# 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
|
|
836
|
+
# )
|
|
837
|
+
#
|
|
838
|
+
# def to_OKLCH(self):
|
|
839
|
+
# return self.to_OKLAB().to_OKLCH()
|
|
840
|
+
#
|
|
841
|
+
# # Check whether the color is in-gamut
|
|
842
|
+
# def is_in_gamut(self):
|
|
843
|
+
# return max(self.r, self.g, self.b) <= 255 and min(self.r, self.g, self.b) >= 0
|
|
844
|
+
#
|
|
845
|
+
#
|
|
846
|
+
# # RGB colors represented as hex code
|
|
847
|
+
# class HEX(Color):
|
|
848
|
+
# def __init__(self, hex_code):
|
|
849
|
+
# if not hex_code[0] == "#":
|
|
850
|
+
# hex_code = "#" + hex_code
|
|
851
|
+
# self.hex_code = hex_code.upper()
|
|
852
|
+
#
|
|
853
|
+
# def __str__(self):
|
|
854
|
+
# return self.hex_code
|
|
855
|
+
#
|
|
856
|
+
# def is_close(self, other):
|
|
857
|
+
# return super().is_close(other)
|
|
858
|
+
#
|
|
859
|
+
# # Return type for addition and subtraction is type of first operand, unless
|
|
860
|
+
# # the result is an invalid color, as this could result in a mangled hex
|
|
861
|
+
# # code which is ambiguous. Therefore, these are left as RGB.
|
|
862
|
+
# # The below function sorts this out:
|
|
863
|
+
# @staticmethod
|
|
864
|
+
# def __get_valid_hex_or_rgb(color):
|
|
865
|
+
# if color.is_in_gamut():
|
|
866
|
+
# return color.to_HEX()
|
|
867
|
+
# else:
|
|
868
|
+
# return color.to_RGB()
|
|
869
|
+
#
|
|
870
|
+
# def __add__(self, other):
|
|
871
|
+
# return self.__get_valid_hex_or_rgb(super().__add__(other))
|
|
872
|
+
#
|
|
873
|
+
# def __neg__(self):
|
|
874
|
+
# return self.__get_valid_hex_or_rgb(super().__neg__())
|
|
875
|
+
#
|
|
876
|
+
# def __sub__(self, other):
|
|
877
|
+
# return self.__get_valid_hex_or_rgb(super().__sub__(other))
|
|
878
|
+
#
|
|
879
|
+
# def __or__(self, other):
|
|
880
|
+
# return super().__or__(other)
|
|
881
|
+
#
|
|
882
|
+
# # Type Conversions
|
|
883
|
+
# def to_RGB(self):
|
|
884
|
+
# return RGB(
|
|
885
|
+
# int(self.hex_code[1:3], 16),
|
|
886
|
+
# int(self.hex_code[3:5], 16),
|
|
887
|
+
# int(self.hex_code[5:7], 16),
|
|
888
|
+
# )
|
|
889
|
+
#
|
|
890
|
+
# def to_HEX(self):
|
|
891
|
+
# return self
|
|
892
|
+
#
|
|
893
|
+
# def to_OKLAB(self):
|
|
894
|
+
# return self.to_RGB().to_OKLAB()
|
|
895
|
+
#
|
|
896
|
+
# def to_OKLCH(self):
|
|
897
|
+
# return self.to_RGB().to_OKLCH()
|
|
898
|
+
#
|
|
899
|
+
# # Check whether the color is in-gamut
|
|
900
|
+
# def is_in_gamut(self):
|
|
901
|
+
# return super().is_in_gamut()
|
|
902
|
+
#
|
|
903
|
+
#
|
|
904
|
+
# # OKLAB colors represented as triplets
|
|
905
|
+
# class OKLAB(Color):
|
|
906
|
+
# def __init__(self, l, a, b):
|
|
907
|
+
# self.l = l
|
|
908
|
+
# self.a = a
|
|
909
|
+
# self.b = b
|
|
910
|
+
#
|
|
911
|
+
# def __str__(self):
|
|
912
|
+
# return f"oklab({self.l}, {self.a}, {self.b})"
|
|
913
|
+
#
|
|
914
|
+
# def is_close(self, other):
|
|
915
|
+
# return super().is_close(other)
|
|
916
|
+
#
|
|
917
|
+
# # Return type for addition and subtraction is type of first operand
|
|
918
|
+
# def __add__(self, other):
|
|
919
|
+
# l = 0.5 * (self.l + other.l)
|
|
920
|
+
# a = 0.5 * (self.a + other.a)
|
|
921
|
+
# b = 0.5 * (self.b + other.b)
|
|
922
|
+
#
|
|
923
|
+
# return OKLAB(l, a, b)
|
|
924
|
+
#
|
|
925
|
+
# def __neg__(self):
|
|
926
|
+
# return OKLAB(1 - self.l, -self.a, -self.b)
|
|
927
|
+
#
|
|
928
|
+
# def __sub__(self, other):
|
|
929
|
+
# return self + other.__neg__()
|
|
930
|
+
#
|
|
931
|
+
# def __or__(self, other):
|
|
932
|
+
# return super().__or__(other)
|
|
933
|
+
#
|
|
934
|
+
# # Type Conversions
|
|
935
|
+
# def to_RGB(self):
|
|
936
|
+
# """OKLAB to RGB transform."""
|
|
937
|
+
# _l, a, b = self.l, self.a, self.b
|
|
938
|
+
# l3 = math.pow(_l + 0.3963377774 * a + 0.2158037573 * b, 3)
|
|
939
|
+
# m3 = math.pow(_l - 0.1055613458 * a - 0.0638541728 * b, 3)
|
|
940
|
+
# s3 = math.pow(_l - 0.0894841775 * a - 1.2914855480 * b, 3)
|
|
941
|
+
#
|
|
942
|
+
#
|
|
943
|
+
# return RGB(
|
|
944
|
+
# _iround(
|
|
945
|
+
# RGB._srgb_transfer(
|
|
946
|
+
# +4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3
|
|
947
|
+
# )
|
|
948
|
+
# * 255
|
|
949
|
+
# ),
|
|
950
|
+
# _iround(
|
|
951
|
+
# RGB._srgb_transfer(
|
|
952
|
+
# -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3
|
|
953
|
+
# )
|
|
954
|
+
# * 255
|
|
955
|
+
# ),
|
|
956
|
+
# _iround(
|
|
957
|
+
# RGB._srgb_transfer(
|
|
958
|
+
# -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3
|
|
959
|
+
# )
|
|
960
|
+
# * 255
|
|
961
|
+
# ),
|
|
962
|
+
# )
|
|
963
|
+
#
|
|
964
|
+
# def to_HEX(self):
|
|
965
|
+
# return self.to_RGB().to_HEX()
|
|
966
|
+
#
|
|
967
|
+
# def to_OKLAB(self):
|
|
968
|
+
# return self
|
|
969
|
+
#
|
|
970
|
+
# def to_OKLCH(self):
|
|
971
|
+
# c = math.sqrt(self.a**2 + self.b**2)
|
|
972
|
+
# h = math.degrees(math.atan2(self.b, self.a))
|
|
973
|
+
# if h < 0:
|
|
974
|
+
# h += 360
|
|
975
|
+
#
|
|
976
|
+
# return OKLCH(self.l, c, h)
|
|
977
|
+
#
|
|
978
|
+
# def is_in_gamut(self):
|
|
979
|
+
# return super().is_in_gamut()
|
|
980
|
+
#
|
|
981
|
+
#
|
|
982
|
+
# class OKLCH(Color):
|
|
983
|
+
#
|
|
984
|
+
# def __init__(self, lightness: float, chroma: float, hue: float):
|
|
985
|
+
# self.l = lightness
|
|
986
|
+
# self.c = chroma
|
|
987
|
+
# self.h = hue
|
|
988
|
+
#
|
|
989
|
+
# def __repr__(self) -> str:
|
|
990
|
+
# return "oklch({self.l} {self.c} {self.h})"
|
|
991
|
+
#
|
|
992
|
+
# def is_close(self, other):
|
|
993
|
+
# return super().is_close(other)
|
|
994
|
+
#
|
|
995
|
+
# # Return type for addition and subtraction is type of first operand
|
|
996
|
+
# def __add__(self, other):
|
|
997
|
+
# return super().__add__(other).to_OKLCH()
|
|
998
|
+
#
|
|
999
|
+
# def __neg__(self):
|
|
1000
|
+
# return super().__neg__().to_OKLCH()
|
|
1001
|
+
#
|
|
1002
|
+
# def __sub__(self, other):
|
|
1003
|
+
# return super().__sub__(other).to_OKLCH()
|
|
1004
|
+
#
|
|
1005
|
+
# def __or__(self, other):
|
|
1006
|
+
# return super().__or__(other)
|
|
1007
|
+
#
|
|
1008
|
+
# # Type Conversions
|
|
1009
|
+
# def to_RGB(self):
|
|
1010
|
+
# return self.to_OKLAB().to_RGB()
|
|
1011
|
+
#
|
|
1012
|
+
# def to_HEX(self):
|
|
1013
|
+
# return self.to_RGB().to_HEX()
|
|
1014
|
+
#
|
|
1015
|
+
# @staticmethod
|
|
1016
|
+
# def _get_normalized_ab(hue: float):
|
|
1017
|
+
# a = math.cos(math.radians(hue))
|
|
1018
|
+
# b = math.sin(math.radians(hue))
|
|
1019
|
+
#
|
|
1020
|
+
# return a, b
|
|
1021
|
+
#
|
|
1022
|
+
# def to_OKLAB(self):
|
|
1023
|
+
# a, b = self._get_normalized_ab(self.h)
|
|
1024
|
+
# return OKLAB(self.l, a * self.c, b * self.c)
|
|
1025
|
+
#
|
|
1026
|
+
# def to_OKLCH(self):
|
|
1027
|
+
# return self
|
|
1028
|
+
#
|
|
1029
|
+
# # Check whether the color is in-gamut
|
|
1030
|
+
# def is_in_gamut(self):
|
|
1031
|
+
# return super().is_in_gamut()
|
|
1032
|
+
|
|
1033
|
+
# # Returns a css string which rounds in such a way as to guarantee an
|
|
1034
|
+
# # in-gamut color (presuming the original color was in-gamut, of course)
|
|
1035
|
+
# def css_string(self):
|
|
1036
|
+
# # Find which way is safe to round l
|
|
1037
|
+
# cusp = find_cusp(hue=self.h)
|
|
1038
|
+
# if self.l > cusp.l:
|
|
1039
|
+
# li = _floor(self.l, 4)
|
|
1040
|
+
# else:
|
|
1041
|
+
# li = _ceil(self.l, 4)
|
|
1042
|
+
#
|
|
1043
|
+
# # Always safe to floor c
|
|
1044
|
+
# c = _floor(self.c, 3)
|
|
1045
|
+
#
|
|
1046
|
+
# # Doesn't matter how h is rounded; it can go directly in format string
|
|
1047
|
+
# return "oklch({:.2%} {:.3f} {:.2f})".format(li, c, self.h)
|
|
1048
|
+
#
|
|
1049
|
+
#
|
|
1050
|
+
#
|
|
1051
|
+
#
|
|
1052
|
+
|
|
1053
|
+
# ----------------------------------------------------------------------------
|
|
1054
|
+
# # Several of the below functions are sourced from Björn Ottosson's blog posts
|
|
1055
|
+
# # which originally defined the OKLAB & OKLCH color spaces. For full
|
|
1056
|
+
# # information on how these functions work, you should read those posts. Other
|
|
1057
|
+
# # than translation, I have made only minimal changes to them.
|
|
1058
|
+
# #
|
|
1059
|
+
# # The original code can be found at:
|
|
1060
|
+
# # https://bottosson.github.io/posts/gamutclipping/
|
|
1061
|
+
# #
|
|
1062
|
+
# ----------------------------------------------------------------------------
|
|
1063
|
+
# ###############################################################################
|
|
1064
|
+
# #
|
|
1065
|
+
# # Copyright (c) 2021 Björn Ottosson
|
|
1066
|
+
# #
|
|
1067
|
+
# # Permission is hereby granted, free of charge, to any person obtaining a
|
|
1068
|
+
# # copy of this software and associated documentation files (the "Software"),
|
|
1069
|
+
# # to deal in the Software without restriction, including without limitation
|
|
1070
|
+
# # the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
1071
|
+
# # and/or sell copies of the Software, and to permit persons to whom the
|
|
1072
|
+
# # Software is furnished to do so, subject to the following conditions:
|
|
1073
|
+
# #
|
|
1074
|
+
# # The above copyright notice and this permission notice shall be
|
|
1075
|
+
# # included in all copies or substantial portions of the Software.
|
|
1076
|
+
# #
|
|
1077
|
+
# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
1078
|
+
# # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
1079
|
+
# # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
1080
|
+
# # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
1081
|
+
# # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
1082
|
+
# # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
1083
|
+
# # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
1084
|
+
# #
|
|
1085
|
+
# ###############################################################################
|
|
1086
|
+
#
|
|
1087
|
+
M_ab_to_lms: Matrix3x2 = (
|
|
1088
|
+
(0.3963377774, +0.2158037573),
|
|
1089
|
+
(-0.1055613458, -0.0638541728),
|
|
1090
|
+
(-0.0894841775, -1.2914855480),
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
def _max_saturation(a: float, b: float):
|
|
1094
|
+
"""Determine the maximum saturation provided the Lab (a, b)-coordinates
|
|
1095
|
+
on the unit circle.
|
|
1096
|
+
Max saturation will be when one of r, g or b goes below zero.
|
|
1097
|
+
"""
|
|
1098
|
+
if a == 0 and b == 0:
|
|
1099
|
+
return 0
|
|
1100
|
+
|
|
1101
|
+
k_lms: Vector3d = linear_trans_2d_to_3d(M_ab_to_lms, (a, b))
|
|
1102
|
+
# original
|
|
1103
|
+
# k_l = +0.3963377774 * a + 0.2158037573 * b
|
|
1104
|
+
# k_m = -0.1055613458 * a - 0.0638541728 * b
|
|
1105
|
+
# k_s = -0.0894841775 * a - 1.2914855480 * b
|
|
1106
|
+
|
|
1107
|
+
# Approximate max saturation using a polynomial whose coefficients
|
|
1108
|
+
# depend on which component goes below zero first.
|
|
1109
|
+
w_lms: Vector3d
|
|
1110
|
+
if (-1.88170328 * a - 0.80936493 * b) > 1:
|
|
1111
|
+
# Red component
|
|
1112
|
+
k0 = +1.19086277
|
|
1113
|
+
k1 = +1.76576728
|
|
1114
|
+
k2 = +0.59662641
|
|
1115
|
+
k3 = +0.75515197
|
|
1116
|
+
k4 = +0.56771245
|
|
1117
|
+
w_lms = (+4.0767416621, -3.3077115913, +0.2309699292)
|
|
1118
|
+
elif (1.81444104 * a - 1.19445276 * b) > 1:
|
|
1119
|
+
# Green component
|
|
1120
|
+
k0 = +0.73956515
|
|
1121
|
+
k1 = -0.45954404
|
|
1122
|
+
k2 = +0.08285427
|
|
1123
|
+
k3 = +0.12541070
|
|
1124
|
+
k4 = +0.14503204
|
|
1125
|
+
w_lms = (-1.2684380046, +2.6097574011, -0.3413193965)
|
|
1126
|
+
else:
|
|
1127
|
+
# Blue component
|
|
1128
|
+
k0 = +1.35733652
|
|
1129
|
+
k1 = -0.00915799
|
|
1130
|
+
k2 = -1.15130210
|
|
1131
|
+
k3 = -0.50559606
|
|
1132
|
+
k4 = +0.00692167
|
|
1133
|
+
w_lms = (-0.0041960863, -0.7034186147, 1.7076147010)
|
|
1134
|
+
S = k0 + (k1 * a) + (k2 * b) + (k3 * a**2) + (k4 * a * b)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
# Apply Halley's method to get closer. Typically error is less than 10e6,
|
|
1139
|
+
# except for some blue hues where dS/dh is close to infinite.
|
|
1140
|
+
# Between one and three iterations steps should suffice.
|
|
1141
|
+
for _ in range(3):
|
|
1142
|
+
lms_: Vector3d = vmap3d(lambda x: S*x + 1, k_lms)
|
|
1143
|
+
# lms_2: Vector3d = square3d(lms_)
|
|
1144
|
+
# lms_3: Vector3d = cube3d(lms_)
|
|
1145
|
+
# l_ = 1 + S * k_l
|
|
1146
|
+
# m_ = 1 + S * k_m
|
|
1147
|
+
# s_ = 1 + S * k_s
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
# lms_**3
|
|
1151
|
+
# original
|
|
1152
|
+
# l3 = l_ * l_ * l_
|
|
1153
|
+
# m3 = m_ * m_ * m_
|
|
1154
|
+
# s3 = s_ * s_ * s_
|
|
1155
|
+
|
|
1156
|
+
# original
|
|
1157
|
+
# l_dS = 3 * k_l * (l_ * l_)
|
|
1158
|
+
# m_dS = 3 * k_m * (m_ * m_)
|
|
1159
|
+
# s_dS = 3 * k_s * (s_ * s_)
|
|
1160
|
+
# 3(k_lms*lms^2)
|
|
1161
|
+
lms_dS: Vector3d = smul3d(3, vmul3d(k_lms, square3d(lms_)))
|
|
1162
|
+
|
|
1163
|
+
# original
|
|
1164
|
+
# l_dS2 = 6 * (k_l * k_l) * l_
|
|
1165
|
+
# m_dS2 = 6 * (k_m * k_m) * m_
|
|
1166
|
+
# s_dS2 = 6 * (k_s * k_s) * s_
|
|
1167
|
+
lms_dS2: Vector3d = smul3d(6, vmul3d(square3d(k_lms), lms_))
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
# f = (wl, ws, wm).lms_3
|
|
1171
|
+
# f1 = (wl, ws, wm).dS
|
|
1172
|
+
# f2 = (wl, ws, wm).dS2
|
|
1173
|
+
f = dot3d(w_lms, cube3d(lms_))
|
|
1174
|
+
f1 = dot3d(w_lms, lms_dS)
|
|
1175
|
+
f2 = dot3d(w_lms, lms_dS2)
|
|
1176
|
+
# original
|
|
1177
|
+
# f = (wl * l3) + (wm * m3) + (ws * s3)
|
|
1178
|
+
# f1 = (wl * l_dS) + wm * m_dS + (ws * s_dS)
|
|
1179
|
+
# f2 = (wl * l_dS2) + (wm * m_dS2) + (ws * s_dS2)
|
|
1180
|
+
|
|
1181
|
+
S = S - (f * f1) / (f1**2 - (0.5 * f * f2))
|
|
1182
|
+
return S
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def find_cusp(hue: float) -> Oklch:
|
|
1186
|
+
a, b = hue_to_ab(hue)
|
|
1187
|
+
S_cusp = _max_saturation(a, b)
|
|
1188
|
+
lrgb = oklab_to_linear_rgb(Oklab(1, S_cusp * a, S_cusp * b))[:3]
|
|
1189
|
+
L_cusp = math.cbrt(1 / max(lrgb))
|
|
1190
|
+
return Oklch(L_cusp, L_cusp * S_cusp, hue)
|
|
1191
|
+
# C_cusp = L_cusp * S_cusp
|
|
1192
|
+
|
|
1193
|
+
# Convert to linear sRGB to find the first point where at least one of r,g,
|
|
1194
|
+
# or b >= 1:
|
|
1195
|
+
# rgb_at_max = max(oklab_to_linear_rgb(Oklab(1, S_cusp * a, S_cusp * b))[:3])
|
|
1196
|
+
# L_cusp = math.cbrt(1 / rgb_at_max[:3]))
|
|
1197
|
+
# # original: it's just oklab_to_linear_srgb
|
|
1198
|
+
# r = _srgb_transfer_inverse(rgb_at_max.red / 255)
|
|
1199
|
+
# g = RGB._srgb_transfer_inverse(rgb_at_max.g / 255)
|
|
1200
|
+
# b = RGB._srgb_transfer_inverse(rgb_at_max.b / 255)
|
|
1201
|
+
# L_cusp = math.pow(1.0 / max(rgb_at_max.r, rgb_at_max.g, rgb_at_max.b), 1 / 3)
|
|
1202
|
+
# C_cusp = L_cusp * S_cusp
|
|
1203
|
+
#
|
|
1204
|
+
# return OKLCH(L_cusp, C_cusp, hue)
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
# class GamutIntersectors:
|
|
1208
|
+
# """A collection of functions that map define lines of intersection in
|
|
1209
|
+
# the (l, c, h=h1) oklch plane P_h1 for a fixed hue h1. Given a point
|
|
1210
|
+
# (l1, c1) in P_h1 the problem is to find a corresponding point (l0, c0) in
|
|
1211
|
+
# P_h1, perhaps depending on (l1, c1), and a value t such that the
|
|
1212
|
+
# point (l_t, c_t) = t(l1, c1) + (1-t)(l0, c0) is on the gamut's roughly
|
|
1213
|
+
# triangular surface.
|
|
1214
|
+
#
|
|
1215
|
+
# a given point (L1, C1) in the oklch
|
|
1216
|
+
# plane a fixed hue h1"""
|
|
1217
|
+
|
|
1218
|
+
# wip reworking of original version that was more a translation from blog post
|
|
1219
|
+
# def _find_gamut_intersection(
|
|
1220
|
+
# L1, C1, color=None, hue=None, L0=None, method="hue_dependent"
|
|
1221
|
+
# ):
|
|
1222
|
+
# # L = L0 * (1 - t) + t * L1
|
|
1223
|
+
# # C = t * C1
|
|
1224
|
+
# # a and b must be normalized so a^2 + b^2 == 1
|
|
1225
|
+
# # I've similarly made this function more flexible for my own ease of use, even
|
|
1226
|
+
# # if it is not intended to be user-facing.
|
|
1227
|
+
# # The minimum information required is L1, C1, and some way of specifying hue.
|
|
1228
|
+
# # L0 is usually not required, since it can be inferred from the method;
|
|
1229
|
+
# # however, an option is given to provide an explicit L0 with method 'manual'.
|
|
1230
|
+
# # Finds intersection of the line defined by
|
|
1231
|
+
# # L = L0 * (1 - t) + t * L1;
|
|
1232
|
+
# # C = t * C1;
|
|
1233
|
+
# col = "oklch(0.86 0.2 25)"
|
|
1234
|
+
# col = "oklch(0.86 0.0744 25)"
|
|
1235
|
+
#
|
|
1236
|
+
#
|
|
1237
|
+
# # Either color or hue may be provided, but exactly one is required.
|
|
1238
|
+
# assert (color is None) ^ (hue is None), (
|
|
1239
|
+
# "Exactly one of color or hue must be provided!"
|
|
1240
|
+
# )
|
|
1241
|
+
#
|
|
1242
|
+
# if hue is not None:
|
|
1243
|
+
# assert isinstance(hue, (float, int)), f"Expected number, received {type(hue)}!"
|
|
1244
|
+
#
|
|
1245
|
+
# else:
|
|
1246
|
+
# assert isinstance(color, Color), (
|
|
1247
|
+
# f"Expected color, received {type(color)}!"
|
|
1248
|
+
# )
|
|
1249
|
+
# if not isinstance(color, OKLCH):
|
|
1250
|
+
# color = color.to_OKLCH()
|
|
1251
|
+
# hue = color.h
|
|
1252
|
+
#
|
|
1253
|
+
# # a and b must be normalized so a^2 + b^2 == 1
|
|
1254
|
+
# a, b = OKLCH._get_normalized_ab(hue)
|
|
1255
|
+
#
|
|
1256
|
+
# # Find the cusp of the gamut triangle
|
|
1257
|
+
# if abs(hue - 264) >= 1:
|
|
1258
|
+
# cusp = find_cusp(hue=hue)
|
|
1259
|
+
# else:
|
|
1260
|
+
# # This handles a strange case with blues where it can converge
|
|
1261
|
+
# # out-of-gamut, resulting in an infinite loop.
|
|
1262
|
+
# cusp = HEX("#023BFB").to_OKLCH()
|
|
1263
|
+
#
|
|
1264
|
+
# # Manual method allows for an explicit L0 value.
|
|
1265
|
+
# if method == "manual":
|
|
1266
|
+
# if L0 is None:
|
|
1267
|
+
# raise ValueError("L0 must be explicitly provided with method 'manual'.")
|
|
1268
|
+
# else:
|
|
1269
|
+
# if L0 is not None:
|
|
1270
|
+
# raise ValueError("L0 cannot be set explicitly unless using method 'manual'!")
|
|
1271
|
+
#
|
|
1272
|
+
# # The other methods specify how L0 should be set.
|
|
1273
|
+
# # Method 'hue_dependent' moves the color towards the point
|
|
1274
|
+
# # (cusp.l, 0, hue) until it intersects the gamut, which is default.
|
|
1275
|
+
# if method == "hue_dependent":
|
|
1276
|
+
# L0 = cusp.l
|
|
1277
|
+
#
|
|
1278
|
+
# # Method 'hue_independent' moves the color towards medium grey.
|
|
1279
|
+
# elif method == "hue_independent":
|
|
1280
|
+
# L0 = 0.5
|
|
1281
|
+
#
|
|
1282
|
+
# # Method 'preserve_lightness' does not alter lightness as long as it's
|
|
1283
|
+
# # a valid value.
|
|
1284
|
+
# elif method == "preserve_lightness":
|
|
1285
|
+
# L0 = min(1, max(0, L1))
|
|
1286
|
+
#
|
|
1287
|
+
# else:
|
|
1288
|
+
# raise ValueError(f"Unknown method: '{method}'!")
|
|
1289
|
+
#
|
|
1290
|
+
# # Find the intersection for upper and lower half separately
|
|
1291
|
+
# if ((L1 - L0) * cusp.c - (cusp.l - L0) * C1) <= 0.0:
|
|
1292
|
+
# # Lower half
|
|
1293
|
+
#
|
|
1294
|
+
# t = cusp.c * L0 / (C1 * cusp.l + cusp.c * (L0 - L1))
|
|
1295
|
+
# output = OKLCH(L0 * (1 - t) + t * L1, t * C1, hue)
|
|
1296
|
+
# else:
|
|
1297
|
+
# # Upper half
|
|
1298
|
+
#
|
|
1299
|
+
# # First intersect with triangle
|
|
1300
|
+
# t = cusp.c * (L0 - 1.0) / (C1 * (cusp.l - 1.0) + cusp.c * (L0 - L1))
|
|
1301
|
+
#
|
|
1302
|
+
# # Then one step Halley's method
|
|
1303
|
+
# dL = L1 - L0
|
|
1304
|
+
# dC = C1
|
|
1305
|
+
#
|
|
1306
|
+
# k_l = +0.3963377774 * a + 0.2158037573 * b
|
|
1307
|
+
# k_m = -0.1055613458 * a - 0.0638541728 * b
|
|
1308
|
+
# k_s = -0.0894841775 * a - 1.2914855480 * b
|
|
1309
|
+
#
|
|
1310
|
+
# l_dt = dL + dC * k_l
|
|
1311
|
+
# m_dt = dL + dC * k_m
|
|
1312
|
+
# s_dt = dL + dC * k_s
|
|
1313
|
+
#
|
|
1314
|
+
# # If higher accuracy is required, 2 or 3 iterations of the following
|
|
1315
|
+
# # block can be used:
|
|
1316
|
+
# output = OKLCH(L0 * (1 - t) + t * L1, t * C1, hue)
|
|
1317
|
+
# while not output.is_in_gamut():
|
|
1318
|
+
# L = L0 * (1.0 - t) + t * L1
|
|
1319
|
+
# C = t * C1
|
|
1320
|
+
#
|
|
1321
|
+
# l_ = L + C * k_l
|
|
1322
|
+
# m_ = L + C * k_m
|
|
1323
|
+
# s_ = L + C * k_s
|
|
1324
|
+
#
|
|
1325
|
+
# l3 = l_ * l_ * l_
|
|
1326
|
+
# m3 = m_ * m_ * m_
|
|
1327
|
+
# s3 = s_ * s_ * s_
|
|
1328
|
+
#
|
|
1329
|
+
# ldt = 3 * l_dt * l_ * l_
|
|
1330
|
+
# mdt = 3 * m_dt * m_ * m_
|
|
1331
|
+
# sdt = 3 * s_dt * s_ * s_
|
|
1332
|
+
#
|
|
1333
|
+
# ldt2 = 6 * l_dt * l_dt * l_
|
|
1334
|
+
# mdt2 = 6 * m_dt * m_dt * m_
|
|
1335
|
+
# sdt2 = 6 * s_dt * s_dt * s_
|
|
1336
|
+
#
|
|
1337
|
+
# r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3 - 1
|
|
1338
|
+
# r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt
|
|
1339
|
+
# r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2
|
|
1340
|
+
#
|
|
1341
|
+
# u_r = r1 / (r1 * r1 - 0.5 * r * r2)
|
|
1342
|
+
# t_r = -r * u_r
|
|
1343
|
+
#
|
|
1344
|
+
# g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3 - 1
|
|
1345
|
+
# g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt
|
|
1346
|
+
# g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2
|
|
1347
|
+
#
|
|
1348
|
+
# u_g = g1 / (g1 * g1 - 0.5 * g * g2)
|
|
1349
|
+
# t_g = -g * u_g
|
|
1350
|
+
#
|
|
1351
|
+
# b = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3 - 1
|
|
1352
|
+
# b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt
|
|
1353
|
+
# b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2
|
|
1354
|
+
#
|
|
1355
|
+
# u_b = b1 / (b1 * b1 - 0.5 * b * b2)
|
|
1356
|
+
# t_b = -b * u_b
|
|
1357
|
+
#
|
|
1358
|
+
# t_r = t_r if u_r >= 0.0 else sys.float_info.max
|
|
1359
|
+
# t_g = t_g if u_g >= 0.0 else sys.float_info.max
|
|
1360
|
+
# t_b = t_b if u_b >= 0.0 else sys.float_info.max
|
|
1361
|
+
#
|
|
1362
|
+
# t += min(t_r, t_g, t_b)
|
|
1363
|
+
# output = OKLCH(L0 * (1 - t) + t * L1, t * C1, hue)
|
|
1364
|
+
#
|
|
1365
|
+
# return output
|
|
1366
|
+
|
|
1367
|
+
# def _original_find_gamut_intersection(
|
|
1368
|
+
# L1, C1, color=None, hue=None, L0=None, method="hue_dependent"
|
|
1369
|
+
# ):
|
|
1370
|
+
# # L = L0 * (1 - t) + t * L1
|
|
1371
|
+
# # C = t * C1
|
|
1372
|
+
# # a and b must be normalized so a^2 + b^2 == 1
|
|
1373
|
+
# # I've similarly made this function more flexible for my own ease of use, even
|
|
1374
|
+
# # if it is not intended to be user-facing.
|
|
1375
|
+
# # The minimum information required is L1, C1, and some way of specifying hue.
|
|
1376
|
+
# # L0 is usually not required, since it can be inferred from the method;
|
|
1377
|
+
# # however, an option is given to provide an explicit L0 with method 'manual'.
|
|
1378
|
+
# # Finds intersection of the line defined by
|
|
1379
|
+
# # L = L0 * (1 - t) + t * L1;
|
|
1380
|
+
# # C = t * C1;
|
|
1381
|
+
#
|
|
1382
|
+
# # Either color or hue may be provided, but exactly one is required.
|
|
1383
|
+
# assert (color is None) ^ (hue is None), (
|
|
1384
|
+
# "Exactly one of color or hue must be provided!"
|
|
1385
|
+
# )
|
|
1386
|
+
#
|
|
1387
|
+
# if hue is not None:
|
|
1388
|
+
# assert isinstance(hue, (float, int)), f"Expected number, received {type(hue)}!"
|
|
1389
|
+
#
|
|
1390
|
+
# else:
|
|
1391
|
+
# assert isinstance(color, Color), (
|
|
1392
|
+
# f"Expected color, received {type(color)}!"
|
|
1393
|
+
# )
|
|
1394
|
+
# if not isinstance(color, OKLCH):
|
|
1395
|
+
# color = color.to_OKLCH()
|
|
1396
|
+
# hue = color.h
|
|
1397
|
+
#
|
|
1398
|
+
# # a and b must be normalized so a^2 + b^2 == 1
|
|
1399
|
+
# a, b = OKLCH._get_normalized_ab(hue)
|
|
1400
|
+
#
|
|
1401
|
+
# # Find the cusp of the gamut triangle
|
|
1402
|
+
# if abs(hue - 264) >= 1:
|
|
1403
|
+
# cusp = find_cusp(hue=hue)
|
|
1404
|
+
# else:
|
|
1405
|
+
# # This handles a strange case with blues where it can converge
|
|
1406
|
+
# # out-of-gamut, resulting in an infinite loop.
|
|
1407
|
+
# cusp = HEX("#023BFB").to_OKLCH()
|
|
1408
|
+
#
|
|
1409
|
+
# # Manual method allows for an explicit L0 value.
|
|
1410
|
+
# if method == "manual":
|
|
1411
|
+
# if L0 is None:
|
|
1412
|
+
# raise ValueError("L0 must be explicitly provided with method 'manual'.")
|
|
1413
|
+
# else:
|
|
1414
|
+
# if L0 is not None:
|
|
1415
|
+
# raise ValueError("L0 cannot be set explicitly unless using method 'manual'!")
|
|
1416
|
+
#
|
|
1417
|
+
# # The other methods specify how L0 should be set.
|
|
1418
|
+
# # Method 'hue_dependent' moves the color towards the point
|
|
1419
|
+
# # (cusp.l, 0, hue) until it intersects the gamut, which is default.
|
|
1420
|
+
# if method == "hue_dependent":
|
|
1421
|
+
# L0 = cusp.l
|
|
1422
|
+
#
|
|
1423
|
+
# # Method 'hue_independent' moves the color towards medium grey.
|
|
1424
|
+
# elif method == "hue_independent":
|
|
1425
|
+
# L0 = 0.5
|
|
1426
|
+
#
|
|
1427
|
+
# # Method 'preserve_lightness' does not alter lightness as long as it's
|
|
1428
|
+
# # a valid value.
|
|
1429
|
+
# elif method == "preserve_lightness":
|
|
1430
|
+
# L0 = min(1, max(0, L1))
|
|
1431
|
+
#
|
|
1432
|
+
# else:
|
|
1433
|
+
# raise ValueError(f"Unknown method: '{method}'!")
|
|
1434
|
+
#
|
|
1435
|
+
# # Find the intersection for upper and lower half separately
|
|
1436
|
+
# if ((L1 - L0) * cusp.c - (cusp.l - L0) * C1) <= 0.0:
|
|
1437
|
+
# # Lower half
|
|
1438
|
+
#
|
|
1439
|
+
# t = cusp.c * L0 / (C1 * cusp.l + cusp.c * (L0 - L1))
|
|
1440
|
+
# output = OKLCH(L0 * (1 - t) + t * L1, t * C1, hue)
|
|
1441
|
+
# else:
|
|
1442
|
+
# # Upper half
|
|
1443
|
+
#
|
|
1444
|
+
# # First intersect with triangle
|
|
1445
|
+
# t = cusp.c * (L0 - 1.0) / (C1 * (cusp.l - 1.0) + cusp.c * (L0 - L1))
|
|
1446
|
+
#
|
|
1447
|
+
# # Then one step Halley's method
|
|
1448
|
+
# dL = L1 - L0
|
|
1449
|
+
# dC = C1
|
|
1450
|
+
#
|
|
1451
|
+
# k_l = +0.3963377774 * a + 0.2158037573 * b
|
|
1452
|
+
# k_m = -0.1055613458 * a - 0.0638541728 * b
|
|
1453
|
+
# k_s = -0.0894841775 * a - 1.2914855480 * b
|
|
1454
|
+
#
|
|
1455
|
+
# l_dt = dL + dC * k_l
|
|
1456
|
+
# m_dt = dL + dC * k_m
|
|
1457
|
+
# s_dt = dL + dC * k_s
|
|
1458
|
+
#
|
|
1459
|
+
# # If higher accuracy is required, 2 or 3 iterations of the following
|
|
1460
|
+
# # block can be used:
|
|
1461
|
+
# output = OKLCH(L0 * (1 - t) + t * L1, t * C1, hue)
|
|
1462
|
+
# while not output.is_in_gamut():
|
|
1463
|
+
# L = L0 * (1.0 - t) + t * L1
|
|
1464
|
+
# C = t * C1
|
|
1465
|
+
#
|
|
1466
|
+
# l_ = L + C * k_l
|
|
1467
|
+
# m_ = L + C * k_m
|
|
1468
|
+
# s_ = L + C * k_s
|
|
1469
|
+
#
|
|
1470
|
+
# l3 = l_ * l_ * l_
|
|
1471
|
+
# m3 = m_ * m_ * m_
|
|
1472
|
+
# s3 = s_ * s_ * s_
|
|
1473
|
+
#
|
|
1474
|
+
# ldt = 3 * l_dt * l_ * l_
|
|
1475
|
+
# mdt = 3 * m_dt * m_ * m_
|
|
1476
|
+
# sdt = 3 * s_dt * s_ * s_
|
|
1477
|
+
#
|
|
1478
|
+
# ldt2 = 6 * l_dt * l_dt * l_
|
|
1479
|
+
# mdt2 = 6 * m_dt * m_dt * m_
|
|
1480
|
+
# sdt2 = 6 * s_dt * s_dt * s_
|
|
1481
|
+
#
|
|
1482
|
+
# r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3 - 1
|
|
1483
|
+
# r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt
|
|
1484
|
+
# r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2
|
|
1485
|
+
#
|
|
1486
|
+
# u_r = r1 / (r1 * r1 - 0.5 * r * r2)
|
|
1487
|
+
# t_r = -r * u_r
|
|
1488
|
+
#
|
|
1489
|
+
# g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3 - 1
|
|
1490
|
+
# g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt
|
|
1491
|
+
# g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2
|
|
1492
|
+
#
|
|
1493
|
+
# u_g = g1 / (g1 * g1 - 0.5 * g * g2)
|
|
1494
|
+
# t_g = -g * u_g
|
|
1495
|
+
#
|
|
1496
|
+
# b = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3 - 1
|
|
1497
|
+
# b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt
|
|
1498
|
+
# b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2
|
|
1499
|
+
#
|
|
1500
|
+
# u_b = b1 / (b1 * b1 - 0.5 * b * b2)
|
|
1501
|
+
# t_b = -b * u_b
|
|
1502
|
+
#
|
|
1503
|
+
# t_r = t_r if u_r >= 0.0 else sys.float_info.max
|
|
1504
|
+
# t_g = t_g if u_g >= 0.0 else sys.float_info.max
|
|
1505
|
+
# t_b = t_b if u_b >= 0.0 else sys.float_info.max
|
|
1506
|
+
#
|
|
1507
|
+
# t += min(t_r, t_g, t_b)
|
|
1508
|
+
# output = OKLCH(L0 * (1 - t) + t * L1, t * C1, hue)
|
|
1509
|
+
#
|
|
1510
|
+
# return output
|
|
1511
|
+
#
|
|
1512
|
+
#
|
|
1513
|
+
# ###############################################################################
|
|
1514
|
+
# #
|
|
1515
|
+
# # The following two functions find the intersections of the line defined by:
|
|
1516
|
+
# # L = color.l * (1 - t1) + t1 * L1
|
|
1517
|
+
# # C = color.c * (1 - t1) + t1 * C1
|
|
1518
|
+
# # where either L1 == color.l or C1 == color.c. These are useful points for
|
|
1519
|
+
# # performing lightening or saturating operations within a given hue.
|
|
1520
|
+
# #
|
|
1521
|
+
# # This differs from the above function in the C term, as for our purposes C0
|
|
1522
|
+
# # may not be equal to 0. This adds an additional unknown, but when
|
|
1523
|
+
# # considering the additional constraints:
|
|
1524
|
+
# # (L, C) = (0, 0) * (1 - t2) + t2 * (L_cusp, C_cusp); (lower half)
|
|
1525
|
+
# # OR
|
|
1526
|
+
# # (L, C) = (L_cusp, C_cusp) * (1 - t2) + t2 * (1, 0); (upper half)
|
|
1527
|
+
# # we end up with five unknowns and five functions nonetheless.
|
|
1528
|
+
# #
|
|
1529
|
+
# ###############################################################################
|
|
1530
|
+
# #
|
|
1531
|
+
# # For the first of our two functions, we take some color with chroma color.c
|
|
1532
|
+
# # which is less than the maximum possible in-gamut chroma for the given
|
|
1533
|
+
# # lightness color.l and hue color.h. This can be used with the trivially
|
|
1534
|
+
# # found minimum chroma of 0 to perform relative operations on a color's
|
|
1535
|
+
# # chroma.
|
|
1536
|
+
# # Therefore, this function solves for chroma C given L1 == color.l:
|
|
1537
|
+
# # => L = color.l
|
|
1538
|
+
# # then in the lower half case:
|
|
1539
|
+
# # (color.l, C) = t2 * (L_cusp, C_cusp)
|
|
1540
|
+
# # => t2 = (color.l, C) / (L_cusp, C_cusp) where (L_cusp, C_cusp) != 0
|
|
1541
|
+
# # => C / C_cusp = color.l / L_cusp
|
|
1542
|
+
# # => C = C_cusp * (color.l / L_cusp)
|
|
1543
|
+
# # and in the upper half case:
|
|
1544
|
+
# # (color.l, C) = (L_cusp, C_cusp) * (1 - t2) + t2 * (1, 0)
|
|
1545
|
+
# # => (color.l, C) - (L_cusp, C_cusp) = t2 * ((1, 0) - (L_cusp, C_cusp))
|
|
1546
|
+
# # => t2 = ((color.l, C) - (L_cusp, C_cusp)) / (1 - L_cusp, -C_cusp)
|
|
1547
|
+
# # where L_cusp != 1 && C_cusp != 0
|
|
1548
|
+
# # => (C - C_cusp) / C_cusp = (L_cusp - color.l) / (1 - L_cusp)
|
|
1549
|
+
# # => C = C_cusp * (L_cusp - color.l) / (1 - L_cusp) + C_cusp
|
|
1550
|
+
# # = C_cusp * (1 + (L_cusp - color.l) / (1 - L_cusp))
|
|
1551
|
+
# # = C_cusp * (1 - color.l) / (1 - L_cusp)
|
|
1552
|
+
# #
|
|
1553
|
+
# ###############################################################################
|
|
1554
|
+
# def _find_chroma_max(color):
|
|
1555
|
+
# # First, get the cusp
|
|
1556
|
+
# cusp = find_cusp(color=color)
|
|
1557
|
+
#
|
|
1558
|
+
# # Next, we consider whether our lightness places us in the upper or lower
|
|
1559
|
+
# # half:
|
|
1560
|
+
# if color.l <= cusp.l:
|
|
1561
|
+
# # Lower half
|
|
1562
|
+
# C = cusp.c * (color.l / cusp.l)
|
|
1563
|
+
# else:
|
|
1564
|
+
# # Upper half
|
|
1565
|
+
# C = cusp.c * (1 - color.l) / (1 - cusp.l)
|
|
1566
|
+
#
|
|
1567
|
+
# # Correct for the concavity of the upper half.
|
|
1568
|
+
# C = _find_gamut_intersection(
|
|
1569
|
+
# color.l, C, color=color, method="preserve_lightness"
|
|
1570
|
+
# ).c
|
|
1571
|
+
#
|
|
1572
|
+
# return C
|
|
1573
|
+
#
|
|
1574
|
+
#
|
|
1575
|
+
# ###############################################################################
|
|
1576
|
+
# #
|
|
1577
|
+
# # For the second of our two functions, we take some color with lightness
|
|
1578
|
+
# # color.l which is between the minimum and maximum possible in-gamut
|
|
1579
|
+
# # lightness for the given chroma color.c and hue color.h. These can be used
|
|
1580
|
+
# # to perform relative operations on a color's lightness.
|
|
1581
|
+
# # Therefore, this function solves for lightness L given C1 == color.c:
|
|
1582
|
+
# # => C = color.c
|
|
1583
|
+
# # then in the lower half case:
|
|
1584
|
+
# # (L, color.c) = t2 * (L_cusp, C_cusp)
|
|
1585
|
+
# # => t2 = (L, color.c) / (L_cusp, C_cusp) where (L_cusp, C_cusp) != 0
|
|
1586
|
+
# # => L / L_cusp = color.c / C_cusp
|
|
1587
|
+
# # => L = L_cusp * (color.c / C_cusp)
|
|
1588
|
+
# # and in the upper half case:
|
|
1589
|
+
# # (L, color.c) = (L_cusp, C_cusp) * (1 - t2) + t2 * (1, 0)
|
|
1590
|
+
# # => (L, color.c) - (L_cusp, C_cusp) = t2 * ((1, 0) - (L_cusp, C_cusp))
|
|
1591
|
+
# # => t2 = ((L, color.c) - (L_cusp, C_cusp)) / (1 - L_cusp, -C_cusp)
|
|
1592
|
+
# # where L_cusp != 1 && C_cusp != 0
|
|
1593
|
+
# # => (L - L_cusp) / (1 - L_cusp) = (C_cusp - color.c) / C_cusp
|
|
1594
|
+
# # = 1 - color.c / C_cusp
|
|
1595
|
+
# # => L = (1 - L_cusp) * (1 - color.c / C_cusp) + L_cusp
|
|
1596
|
+
# # => L = 1 - (1 - L_cusp) * (color.c / C_cusp)
|
|
1597
|
+
# #
|
|
1598
|
+
# # Since the lightness intersects at two points, we are interested in both the
|
|
1599
|
+
# # lower half and the upper half.
|
|
1600
|
+
# #
|
|
1601
|
+
# ###############################################################################
|
|
1602
|
+
# def _find_lightness_bounds(color):
|
|
1603
|
+
# # First, get the cusp
|
|
1604
|
+
# cusp = find_cusp(color=color)
|
|
1605
|
+
#
|
|
1606
|
+
# # Lower half
|
|
1607
|
+
# L1 = cusp.l * (color.c / cusp.c)
|
|
1608
|
+
#
|
|
1609
|
+
# # Upper half
|
|
1610
|
+
# L2 = 1 - (1 - cusp.l) * (color.c / cusp.c)
|
|
1611
|
+
#
|
|
1612
|
+
# # Correct for the concavity of the upper half.
|
|
1613
|
+
# # By manually setting L0 to an arbitrarily large negative number, we can
|
|
1614
|
+
# # easily approximate moving horizontally.
|
|
1615
|
+
# L2 = _find_gamut_intersection(L2, color.c, color=color, method="manual", L0=-1000).l
|
|
1616
|
+
#
|
|
1617
|
+
# return (L1, L2)
|
|
1618
|
+
#
|
|
1619
|
+
#
|
|
1620
|
+
# # A simple lerp
|
|
1621
|
+
# def _lerp(t, a, b):
|
|
1622
|
+
# return a * (1 - t) + b * t
|
|
1623
|
+
#
|
|
1624
|
+
#
|
|
1625
|
+
# ###############################################################################
|
|
1626
|
+
# #
|
|
1627
|
+
# # Public Functions
|
|
1628
|
+
# #
|
|
1629
|
+
# ###############################################################################
|
|
1630
|
+
# #
|
|
1631
|
+
# # All of the below functions implement a similar function header and type
|
|
1632
|
+
# # checking process so that users are able to specify the necessary
|
|
1633
|
+
# # information in whatever format is convenient to them, without compromising
|
|
1634
|
+
# # the stability of the code.
|
|
1635
|
+
# # Generally speaking, a function which requires lightness, hue, or chroma can
|
|
1636
|
+
# # receive either explicit values for each, or a color object with the
|
|
1637
|
+
# # desired properties. A color object need not be OKLCH, and thus is the
|
|
1638
|
+
# # intended way to pass RGB/HEX Many of the functions essentially
|
|
1639
|
+
# # exist to tweak one of the characteristics of an OKLCH color by
|
|
1640
|
+
# # interpolating within certain bounds, and when that is the case the first
|
|
1641
|
+
# # argument is always the parameter for that lerp. Furthermore, a method can
|
|
1642
|
+
# # often be specified between relative, which operates from the provided
|
|
1643
|
+
# # color to the extremum in-gamut color for the given axis, and absolute,
|
|
1644
|
+
# # which only considers the extrema.
|
|
1645
|
+
# #
|
|
1646
|
+
# ###############################################################################
|
|
1647
|
+
#
|
|
1648
|
+
#
|
|
1649
|
+
# # Type-checking used for all of the below:
|
|
1650
|
+
# def __get_OKLCH_if_color(arg):
|
|
1651
|
+
# Color._is_color(arg)
|
|
1652
|
+
# return arg.to_OKLCH()
|
|
1653
|
+
#
|
|
1654
|
+
#
|
|
1655
|
+
# # Lerps the chroma for the given color. Negative t dechromatizes if the method
|
|
1656
|
+
# # is relative
|
|
1657
|
+
# def chromatize(t, color=None, hue=None, lightness=None, method="relative"):
|
|
1658
|
+
# assert (color is None) ^ (hue is None), (
|
|
1659
|
+
# "Exactly one of color or hue must be provided!"
|
|
1660
|
+
# )
|
|
1661
|
+
#
|
|
1662
|
+
# if hue is not None:
|
|
1663
|
+
# if not isinstance(hue, (float, int)):
|
|
1664
|
+
# raise ValueError(f"Expected number, received {type(lightness)}!")
|
|
1665
|
+
#
|
|
1666
|
+
# assert lightness is not None, "Lightness must be specified with explicit hue!"
|
|
1667
|
+
# if not isinstance(lightness, (float, int)):
|
|
1668
|
+
# raise ValueError(f"Expected number, received {type(lightness)}!")
|
|
1669
|
+
#
|
|
1670
|
+
# assert method == "absolute", (
|
|
1671
|
+
# "Explicit hue can only be specified with method 'relative'!"
|
|
1672
|
+
# )
|
|
1673
|
+
# # Generate a color object with the given parameters
|
|
1674
|
+
# color = OKLCH(lightness, 0.0, hue)
|
|
1675
|
+
#
|
|
1676
|
+
# else:
|
|
1677
|
+
# color = __get_OKLCH_if_color(color)
|
|
1678
|
+
#
|
|
1679
|
+
# # Find max chroma
|
|
1680
|
+
# max = _find_chroma_max(color)
|
|
1681
|
+
#
|
|
1682
|
+
# if method == "relative":
|
|
1683
|
+
# if t < -1 or t > 1:
|
|
1684
|
+
# raise ValueError("t should be in the range [-1,1] for method 'relative'!")
|
|
1685
|
+
#
|
|
1686
|
+
# # Interpolate between current c and maximum
|
|
1687
|
+
# if t >= 0:
|
|
1688
|
+
# C = _lerp(t, color.c, max)
|
|
1689
|
+
#
|
|
1690
|
+
# # Negative t means we interpolate towards the minimum (0) instead
|
|
1691
|
+
# else:
|
|
1692
|
+
# C = _lerp(-t, color.c, 0)
|
|
1693
|
+
#
|
|
1694
|
+
# elif method == "absolute":
|
|
1695
|
+
# if t < 0 or t > 1:
|
|
1696
|
+
# raise ValueError("t should be in the range [0,1] for method 'absolute'!")
|
|
1697
|
+
#
|
|
1698
|
+
# # In absolute mode we interpolate between minimum (0) and max
|
|
1699
|
+
# C = _lerp(t, 0, max)
|
|
1700
|
+
#
|
|
1701
|
+
# else:
|
|
1702
|
+
# raise ValueError(f"""Unknown method: '{method}'!
|
|
1703
|
+
# Valid methods are 'relative' and 'absolute'.""")
|
|
1704
|
+
#
|
|
1705
|
+
# ret = OKLCH(color.l, C, color.h)
|
|
1706
|
+
# # Make sure that the color is in-gamut
|
|
1707
|
+
# if ret.is_in_gamut():
|
|
1708
|
+
# return ret
|
|
1709
|
+
# else:
|
|
1710
|
+
# # Clip it into gamut
|
|
1711
|
+
# return gamut_clip_preserve_lightness(ret)
|
|
1712
|
+
#
|
|
1713
|
+
#
|
|
1714
|
+
# # Simply reverses the direction of chromatize
|
|
1715
|
+
# def dechromatize(t, color=None, hue=None, lightness=None, method="relative"):
|
|
1716
|
+
# # Negate t and chromatize
|
|
1717
|
+
# if method == "relative":
|
|
1718
|
+
# return chromatize(-t, color=color, hue=hue, lightness=lightness)
|
|
1719
|
+
#
|
|
1720
|
+
# # Reverse t and chromatize
|
|
1721
|
+
# elif method == "absolute":
|
|
1722
|
+
# return chromatize(
|
|
1723
|
+
# 1 - t, color=color, hue=hue, lightness=lightness, method="absolute"
|
|
1724
|
+
# )
|
|
1725
|
+
#
|
|
1726
|
+
# else:
|
|
1727
|
+
# raise ValueError(f"""Unknown method: '{method}'!
|
|
1728
|
+
# Valid methods are 'relative' and 'absolute'.""")
|
|
1729
|
+
#
|
|
1730
|
+
#
|
|
1731
|
+
# # Chromatize, while technically more correct, is not a very appealing name, so
|
|
1732
|
+
# # detone and tone are provided as aliases.
|
|
1733
|
+
# def detone(t, color=None, hue=None, lightness=None, method="relative"):
|
|
1734
|
+
# return chromatize(t, color=color, hue=hue, lightness=lightness, method=method)
|
|
1735
|
+
#
|
|
1736
|
+
#
|
|
1737
|
+
# def tone(t, color=None, hue=None, lightness=None, method="relative"):
|
|
1738
|
+
# return dechromatize(t, color=color, hue=hue, lightness=lightness, method=method)
|
|
1739
|
+
#
|
|
1740
|
+
#
|
|
1741
|
+
# # Lerps the lightness for the given color. Negative t darkens if the method is
|
|
1742
|
+
# # relative
|
|
1743
|
+
# def lighten(t, color=None, hue=None, chroma=None, method="relative"):
|
|
1744
|
+
# assert (color is None) ^ (hue is None), (
|
|
1745
|
+
# "Exactly one of color or hue must be provided!"
|
|
1746
|
+
# )
|
|
1747
|
+
#
|
|
1748
|
+
# if hue is not None:
|
|
1749
|
+
# if not isinstance(hue, (float, int)):
|
|
1750
|
+
# raise ValueError(f"Expected number, received {type(chroma)}!")
|
|
1751
|
+
#
|
|
1752
|
+
# assert chroma is not None, "Lightness must be specified with explicit hue!"
|
|
1753
|
+
# if not isinstance(chroma, (float, int)):
|
|
1754
|
+
# raise ValueError(f"Expected number, received {type(chroma)}!")
|
|
1755
|
+
#
|
|
1756
|
+
# assert method == "absolute", (
|
|
1757
|
+
# "Explicit hue can only be specified with method 'relative'!"
|
|
1758
|
+
# )
|
|
1759
|
+
# # Generate a color object with the given parameters
|
|
1760
|
+
# color = OKLCH(0.5, chroma, hue)
|
|
1761
|
+
#
|
|
1762
|
+
# else:
|
|
1763
|
+
# color = __get_OKLCH_if_color(color)
|
|
1764
|
+
#
|
|
1765
|
+
# # Find the min and max lightness
|
|
1766
|
+
# bounds = _find_lightness_bounds(color)
|
|
1767
|
+
#
|
|
1768
|
+
# if method == "relative":
|
|
1769
|
+
# if t < -1 or t > 1:
|
|
1770
|
+
# raise ValueError("t should be in the range [-1,1] for method 'relative'!")
|
|
1771
|
+
#
|
|
1772
|
+
# # Interpolate between current l and maximum
|
|
1773
|
+
# if t >= 0:
|
|
1774
|
+
# L = _lerp(t, color.l, bounds[1])
|
|
1775
|
+
#
|
|
1776
|
+
# # Negative t means we interpolate towards the minimum instead
|
|
1777
|
+
# else:
|
|
1778
|
+
# L = _lerp(-t, color.l, bounds[0])
|
|
1779
|
+
#
|
|
1780
|
+
# elif method == "absolute":
|
|
1781
|
+
# if t < 0 or t > 1:
|
|
1782
|
+
# raise ValueError("t should be in the range [0,1] for method 'absolute'!")
|
|
1783
|
+
#
|
|
1784
|
+
# # In absolute mode we interpolate between minimum and max
|
|
1785
|
+
# L = _lerp(t, bounds[0], bounds[1])
|
|
1786
|
+
#
|
|
1787
|
+
# else:
|
|
1788
|
+
# raise ValueError(f"""Unknown method: '{method}'!
|
|
1789
|
+
# Valid methods are 'relative' and 'absolute'.""")
|
|
1790
|
+
#
|
|
1791
|
+
# ret = OKLCH(L, color.c, color.h)
|
|
1792
|
+
# # Make sure that the color is in-gamut
|
|
1793
|
+
# if ret.is_in_gamut():
|
|
1794
|
+
# return ret
|
|
1795
|
+
# else:
|
|
1796
|
+
# # Clip it into gamut
|
|
1797
|
+
# return _find_gamut_intersection(L, color.c, hue=color.h)
|
|
1798
|
+
#
|
|
1799
|
+
#
|
|
1800
|
+
# # Simply reverses the direction of chromatize
|
|
1801
|
+
# def darken(t, color=None, hue=None, chroma=None, method="relative"):
|
|
1802
|
+
# # Negate t and lighten
|
|
1803
|
+
# if method == "relative":
|
|
1804
|
+
# return lighten(-t, color=color, hue=hue, chroma=chroma)
|
|
1805
|
+
#
|
|
1806
|
+
# # Reverse t and lighten
|
|
1807
|
+
# elif method == "absolute":
|
|
1808
|
+
# return lighten(1 - t, color=color, hue=hue, chroma=chroma, method="absolute")
|
|
1809
|
+
#
|
|
1810
|
+
# else:
|
|
1811
|
+
# raise ValueError(f"""Unknown method: '{method}'!
|
|
1812
|
+
# Valid methods are 'relative' and 'absolute'.""")
|
|
1813
|
+
#
|
|
1814
|
+
#
|
|
1815
|
+
# # Linearly interpolate between two colors
|
|
1816
|
+
# # Hue path is determined by method parameter
|
|
1817
|
+
# def interpolate(t, color1, color2, method="shortest"):
|
|
1818
|
+
# color1 = __get_OKLCH_if_color(color1)
|
|
1819
|
+
# color2 = __get_OKLCH_if_color(color2)
|
|
1820
|
+
#
|
|
1821
|
+
# # Get the lerped parameters
|
|
1822
|
+
# li = _lerp(t, color1.l, color2.l)
|
|
1823
|
+
# ch = _lerp(t, color1.c, color2.c)
|
|
1824
|
+
#
|
|
1825
|
+
# # Hue path is determined by method
|
|
1826
|
+
# if method == "shortest":
|
|
1827
|
+
# # Ensures hue takes the shortest path
|
|
1828
|
+
# if color2.h - color1.h > 180:
|
|
1829
|
+
# h = _lerp(t, color1.h + 360, color2.h)
|
|
1830
|
+
# elif color2.h - color1.h < -180:
|
|
1831
|
+
# h = _lerp(t, color1.h, color2.h + 360)
|
|
1832
|
+
# else:
|
|
1833
|
+
# h = _lerp(t, color1.h, color2.h)
|
|
1834
|
+
# elif method == "longest":
|
|
1835
|
+
# # Ensures hue takes the longest path
|
|
1836
|
+
# if 0 < color2.h - color1.h < 180:
|
|
1837
|
+
# h = _lerp(t, color1.h + 360, color2.h)
|
|
1838
|
+
# elif -180 < color2.h - color1.h <= 0:
|
|
1839
|
+
# h = _lerp(t, color1.h, color2.h + 360)
|
|
1840
|
+
# else:
|
|
1841
|
+
# h = _lerp(t, color1.h, color2.h)
|
|
1842
|
+
# elif method == "increasing":
|
|
1843
|
+
# # Ensures hue is strictly increasing
|
|
1844
|
+
# if color2.h < color1.h:
|
|
1845
|
+
# h = _lerp(t, color1.h, color2.h + 360)
|
|
1846
|
+
# else:
|
|
1847
|
+
# h = _lerp(t, color1.h, color2.h)
|
|
1848
|
+
# elif method == "decreasing":
|
|
1849
|
+
# # Ensures hue is strictly decreasing
|
|
1850
|
+
# if color1.h < color2.h:
|
|
1851
|
+
# h = _lerp(t, color1.h + 360, color2.h)
|
|
1852
|
+
# else:
|
|
1853
|
+
# h = _lerp(t, color1.h, color2.h)
|
|
1854
|
+
# elif method == "use_OKLAB":
|
|
1855
|
+
# color1 = color1.to_OKLAB()
|
|
1856
|
+
# color2 = color2.to_OKLAB()
|
|
1857
|
+
#
|
|
1858
|
+
# a = _lerp(t, color1.a, color2.a)
|
|
1859
|
+
# b = _lerp(t, color1.b, color2.b)
|
|
1860
|
+
# result = OKLAB(li, a, b).to_OKLCH()
|
|
1861
|
+
#
|
|
1862
|
+
# ch = result.c
|
|
1863
|
+
# h = result.h
|
|
1864
|
+
# else:
|
|
1865
|
+
# raise ValueError(f"""Unknown method '{method}'! Valid methods are:
|
|
1866
|
+
# 'shortest', 'longest', 'increasing', 'decreasing', and 'use_OKLAB'.""")
|
|
1867
|
+
#
|
|
1868
|
+
# ret = OKLCH(li, ch, h)
|
|
1869
|
+
# # Make sure that the color is in-gamut
|
|
1870
|
+
# if ret.is_in_gamut():
|
|
1871
|
+
# return ret
|
|
1872
|
+
# else:
|
|
1873
|
+
# # Clip it into gamut
|
|
1874
|
+
# return gamut_clip_preserve_lightness(ret)
|
|
1875
|
+
#
|
|
1876
|
+
#
|
|
1877
|
+
# # Gamut clipping:
|
|
1878
|
+
# def gamut_clip_hue_dependent(color):
|
|
1879
|
+
# _color = __get_OKLCH_if_color(color)
|
|
1880
|
+
# if color.is_in_gamut():
|
|
1881
|
+
# return color
|
|
1882
|
+
#
|
|
1883
|
+
# return _find_gamut_intersection(_color.l, _color.c, color=_color)
|
|
1884
|
+
#
|
|
1885
|
+
#
|
|
1886
|
+
# def gamut_clip_hue_independent(color):
|
|
1887
|
+
# _color = __get_OKLCH_if_color(color)
|
|
1888
|
+
# if color.is_in_gamut():
|
|
1889
|
+
# return color
|
|
1890
|
+
#
|
|
1891
|
+
# return _find_gamut_intersection(
|
|
1892
|
+
# _color.l, _color.c, color=_color, method="hue_dependent"
|
|
1893
|
+
# )
|
|
1894
|
+
#
|
|
1895
|
+
#
|
|
1896
|
+
# def gamut_clip_preserve_lightness(color):
|
|
1897
|
+
# _color = __get_OKLCH_if_color(color)
|
|
1898
|
+
# if color.is_in_gamut():
|
|
1899
|
+
# return color
|
|
1900
|
+
#
|
|
1901
|
+
# return _find_gamut_intersection(
|
|
1902
|
+
# _color.l, _color.c, color=_color, method="preserve_lightness"
|
|
1903
|
+
# )
|