.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HCL Sliders
2 | *HCL Sliders* is a Python plugin made for [Krita](https://krita.org) (free professional and open-source painting program).
3 |
4 | This plugin contains sliders for various hue/colorfulness/lightness models for use in the sRGB color space and its linear counterpart.
5 |
6 | ### Available Models
7 |
8 | **sRGB Color Space**
9 |
10 | *HSV* (Hue, Saturation, Value)
11 |
12 | *HSL* (Hue, Saturation, Lightness)
13 |
14 | *HCY* (Hue, Relative Chroma, Luma/Relative Luminance)
15 |
16 | **Oklab Color Space**
17 |
18 | *OKHCL* (Hue, Relative Chroma, Referenced Lightness)
19 |
20 | *OKHSV* (Hue, Saturation, Value)
21 |
22 | *OKHSL* (Hue, Interpolated Saturation, Referenced Lightness)
23 |
24 | ### Accepted CSS Syntax
25 |
26 | *Hexicimal* notations: Must be 6 digits, i.e. #RRGGBB
27 |
28 | *Oklab* notations: RGB values will be clipped to the sRGB gamut
29 |
30 | *Oklch* notations: Hue must be given in degrees and RGB will be clipped
31 |
32 | ## Slider Interactions
33 | Left Mouse Button/Pen **Press**: Set value for channel
34 |
35 | **Ctrl** + Left Mouse Button/Pen **Press**: Snap value to interval points
36 |
37 | **Shift** + Left Mouse Button/Pen **Drag**: Shift value by 0.1 precision
38 |
39 | **Alt** + Left Mouse Button/Pen **Drag**: Shift value by 0.01 precision
40 |
41 | ## History Interactions
42 | Left Mouse Button/Pen **Click**: Set color to foreground
43 |
44 | **Ctrl** + Left Mouse Button/Pen **Click**: Set color to background
45 |
46 | **Shift** + Left Mouse Button/Pen **Drag**: Scroll color history
47 |
48 | **Alt** + Left Mouse Button/Pen **Click**: Delete selected color from history
49 |
50 | **Alt** + Left Mouse Button/Pen **Drag**: Delete a series of colors from history, starting from the point where mouse/pen is pressed until where the mouse/pen is released
51 |
52 | ## (New) Background Selector Mode
53 | **Click** on the color display panel on the plugin to toggle between selecting foreground and background color.
54 |
55 | ## Install/Update
56 | 1. Download the [ZIP file](https://github.com/lucifer9683/HCLSliders/releases/download/v1.1.4/HCLSlidersV1.1.4.zip)
57 | 2. Open Krita and go to Tools -> Scripts -> Import Python Plugin From File.
58 | 3. Navigate to the download location and select the ZIP file.
59 | 4. Restart Krita.
60 | 5. Go to the Python Plugin Manager again to check if Ten Brush Slots extension is activated.
61 | 6. If not activated, click on the checkbox beside it and restart Krita again.
62 | 7. Go to Settings -> Dockers -> HCL Sliders, click on the checkbox to activate it.
63 |
--------------------------------------------------------------------------------
/hclsliders.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Type=Service
3 | ServiceTypes=Krita/PythonPlugin
4 | X-KDE-Library=hclsliders
5 | X-Python-2-Compatible=false
6 | X-Krita-Manual=Manual.html
7 | Name=HCL Sliders
8 | Comment=HCL sliders for color selection
9 |
--------------------------------------------------------------------------------
/hclsliders/Manual.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | HCL Sliders Manual
7 |
8 |
9 | HCL Sliders
10 | A docker with hue/colofulness/lightness sliders for color selection. Sliders inheriting Oklab color space is limited to the sRGB gamut. This plugin currently can only be used in sRGB and its linear color space.
11 | Usage
12 | Go to Settings → Dockers → HCL Sliders and click the checkbox to activate the docker.
13 | Click on the config button at the top right corner of the docker to configure the plugin. Order of models can be swapped by dragging the listed item and order of channels can be swapped by dragging the tabs. Changes are saved upon closing the window.
14 | Slider Interactions
15 | Left Mouse Button/Pen Press: Set value for channel
16 | Ctrl + Left Mouse Button/Pen Press: Snap value to interval points
17 | Shift + Left Mouse Button/Pen Drag: Shift value by 0.1 precision
18 | Alt + Left Mouse Button/Pen Drag: Shift value by 0.01 precision
19 | History Interactions
20 | Left Mouse Button/Pen Click: Set color to foreground
21 | Ctrl + Left Mouse Button/Pen Click: Set color to background
22 | Shift + Left Mouse Button/Pen Drag: Scroll color history
23 | Alt + Left Mouse Button/Pen Click: Delete selected color from history
24 | Alt + Left Mouse Button/Pen Drag: Delete a series of colors from history, starting from the point where mouse/pen is pressed until where the mouse/pen is released
25 |
26 |
27 |
--------------------------------------------------------------------------------
/hclsliders/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | #
3 | # HCL Sliders is a Krita plugin for color selection.
4 | # Copyright (C) 2024 Lucifer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | from krita import DockWidgetFactory, DockWidgetFactoryBase
20 | from .hclsliders import HCLSliders
21 |
22 | DOCKER_ID = 'pykrita_hclsliders'
23 |
24 | instance = Krita.instance()
25 | dock_widget_factory = DockWidgetFactory(DOCKER_ID,
26 | DockWidgetFactoryBase.DockRight,
27 | HCLSliders)
28 |
29 | instance.addDockWidgetFactory(dock_widget_factory)
30 |
--------------------------------------------------------------------------------
/hclsliders/colorconversion.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later AND MIT
2 | #
3 | # Color conversion script for python.
4 | # Copyright (C) 2024 Lucifer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 | #
19 | # This file incorporates work covered by the following copyright and
20 | # permission notice:
21 | #
22 | # Copyright (c) 2021 Björn Ottosson
23 | #
24 | # Permission is hereby granted, free of charge, to any person obtaining a copy of
25 | # this software and associated documentation files (the "Software"), to deal in
26 | # the Software without restriction, including without limitation the rights to
27 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
28 | # of the Software, and to permit persons to whom the Software is furnished to do
29 | # so, subject to the following conditions:
30 | # The above copyright notice and this permission notice shall be included in all
31 | # copies or substantial portions of the Software.
32 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38 | # SOFTWARE.
39 | #
40 | # Pigment.O is a Krita plugin and it is a Color Picker and Color Mixer.
41 | # Copyright ( C ) 2020 Ricardo Jeremias.
42 | #
43 | # This program is free software: you can redistribute it and/or modify
44 | # it under the terms of the GNU General Public License as published by
45 | # the Free Software Foundation, either version 3 of the License, or
46 | # ( at your option ) any later version.
47 | #
48 | # This program is distributed in the hope that it will be useful,
49 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
50 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
51 | # GNU General Public License for more details.
52 | #
53 | # You should have received a copy of the GNU General Public License
54 | # along with this program. If not, see .
55 |
56 | import math, sys
57 | # luma coefficents for ITU-R BT.709
58 | Y709R = 0.2126
59 | Y709G = 0.7152
60 | Y709B = 0.0722
61 | # constants for sRGB transfer
62 | ALPHA = 0.055
63 | GAMMA = 2.4
64 | PHI = 12.92
65 | # toe functions
66 | K1 = 0.206
67 | K2 = 0.03
68 | K3 = (1.0 + K1) / (1.0 + K2)
69 |
70 |
71 | class Convert:
72 |
73 | @staticmethod
74 | def roundZero(n: float, d: int):
75 | s = -1 if n < 0 else 1
76 | if not isinstance(d, int):
77 | raise TypeError("decimal places must be an integer")
78 | elif d < 0:
79 | raise ValueError("decimal places has to be 0 or more")
80 | elif d == 0:
81 | return math.floor(abs(n)) * s
82 |
83 | f = 10 ** d
84 | return math.floor(abs(n) * f) / f * s
85 |
86 | @staticmethod
87 | def clampF(f: float, u: float=1, l: float=0):
88 | # red may be negative in parts of blue due to color being out of gamut
89 | if f < l:
90 | return l
91 | # round up near 1 and prevent going over 1 from oklab conversion
92 | if (u == 1 and f > 0.999999) or f > u:
93 | return u
94 | return f
95 |
96 | @staticmethod
97 | def componentToSRGB(c: float):
98 | # round(CHI / PHI, 7) = 0.0031308
99 | return (1 + ALPHA) * c ** (1 / GAMMA) - ALPHA if c > 0.0031308 else c * PHI
100 |
101 | @staticmethod
102 | def componentToLinear(c: float):
103 | # CHI = 0.04045
104 | return ((c + ALPHA) / (1 + ALPHA)) ** GAMMA if c > 0.04045 else c / PHI
105 |
106 | @staticmethod
107 | def cartesianToPolar(a: float, b: float):
108 | c = math.hypot(a, b)
109 | hRad = math.atan2(b, a)
110 | if hRad < 0:
111 | hRad += math.pi * 2
112 | h = math.degrees(hRad)
113 | return (c, h)
114 |
115 | @staticmethod
116 | def polarToCartesian(c: float, h: float):
117 | hRad = math.radians(h)
118 | a = c * math.cos(hRad)
119 | b = c * math.sin(hRad)
120 | return (a, b)
121 |
122 | @staticmethod
123 | def linearToOklab(r: float, g: float, b: float):
124 | # convert to approximate cone responses
125 | l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b
126 | m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b
127 | s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b
128 | # apply non-linearity
129 | l_ = l ** (1 / 3)
130 | m_ = m ** (1 / 3)
131 | s_ = s ** (1 / 3)
132 | # transform to Lab coordinates
133 | okL = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_
134 | okA = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_
135 | okB = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
136 | return (okL, okA, okB)
137 |
138 | @staticmethod
139 | def oklabToLinear(okL: float, okA: float, okB: float):
140 | # inverse coordinates
141 | l_ = okL + 0.3963377774 * okA + 0.2158037573 * okB
142 | m_ = okL - 0.1055613458 * okA - 0.0638541728 * okB
143 | s_ = okL - 0.0894841775 * okA - 1.2914855480 * okB
144 | # reverse non-linearity
145 | l = l_ * l_ * l_
146 | m = m_ * m_ * m_
147 | s = s_ * s_ * s_
148 | # convert to linear rgb
149 | r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
150 | g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
151 | b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
152 | return(r, g, b)
153 |
154 | @staticmethod
155 | # toe function for L_r
156 | def toe(x):
157 | return 0.5 * (K3 * x - K1 + ((K3 * x - K1) * (K3 * x - K1) + 4 * K2 * K3 * x) ** (1 / 2))
158 |
159 | @staticmethod
160 | # inverse toe function for L_r
161 | def toeInv(x):
162 | return (x * x + K1 * x) / (K3 * (x + K2))
163 |
164 | @staticmethod
165 | # Finds the maximum saturation possible for a given hue that fits in sRGB
166 | # Saturation here is defined as S = C/L
167 | # a and b must be normalized so a^2 + b^2 == 1
168 | def computeMaxSaturation(a: float, b: float):
169 | # Max saturation will be when one of r, g or b goes below zero.
170 | # Select different coefficients depending on which component goes below zero first
171 | # Blue component
172 | k0 = +1.35733652
173 | k1 = -0.00915799
174 | k2 = -1.15130210
175 | k3 = -0.50559606
176 | k4 = +0.00692167
177 | wl = -0.0041960863
178 | wm = -0.7034186147
179 | ws = +1.7076147010
180 | if -1.88170328 * a - 0.80936493 * b > 1:
181 | # Red component
182 | k0 = +1.19086277
183 | k1 = +1.76576728
184 | k2 = +0.59662641
185 | k3 = +0.75515197
186 | k4 = +0.56771245
187 | wl = +4.0767416621
188 | wm = -3.3077115913
189 | ws = +0.2309699292
190 | elif 1.81444104 * a - 1.19445276 * b > 1:
191 | # Green component
192 | k0 = +0.73956515
193 | k1 = -0.45954404
194 | k2 = +0.08285427
195 | k3 = +0.12541070
196 | k4 = +0.14503204
197 | wl = -1.2684380046
198 | wm = +2.6097574011
199 | ws = -0.3413193965
200 | # Approximate max saturation using a polynomial:
201 | maxS = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b
202 | # Do one step Halley's method to get closer
203 | # this gives an error less than 10e6,
204 | # except for some blue hues where the dS/dh is close to infinite
205 | # this should be sufficient for most applications, otherwise do two/three steps
206 | k_l = +0.3963377774 * a + 0.2158037573 * b
207 | k_m = -0.1055613458 * a - 0.0638541728 * b
208 | k_s = -0.0894841775 * a - 1.2914855480 * b
209 |
210 | l_ = 1.0 + maxS * k_l
211 | m_ = 1.0 + maxS * k_m
212 | s_ = 1.0 + maxS * k_s
213 |
214 | l = l_ * l_ * l_
215 | m = m_ * m_ * m_
216 | s = s_ * s_ * s_
217 |
218 | l_dS = 3.0 * k_l * l_ * l_
219 | m_dS = 3.0 * k_m * m_ * m_
220 | s_dS = 3.0 * k_s * s_ * s_
221 |
222 | l_dS2 = 6.0 * k_l * k_l * l_
223 | m_dS2 = 6.0 * k_m * k_m * m_
224 | s_dS2 = 6.0 * k_s * k_s * s_
225 |
226 | f = wl * l + wm * m + ws * s
227 | f1 = wl * l_dS + wm * m_dS + ws * s_dS
228 | f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2
229 |
230 | maxS = maxS - f * f1 / (f1*f1 - 0.5 * f * f2)
231 | return maxS
232 |
233 | @staticmethod
234 | # finds L_cusp and C_cusp for a given hue
235 | # a and b must be normalized so a^2 + b^2 == 1
236 | def findCuspLC(a: float, b: float):
237 | # First, find the maximum saturation (saturation S = C/L)
238 | maxS = Convert.computeMaxSaturation(a, b)
239 | # Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
240 | maxRgb = Convert.oklabToLinear(1, maxS * a, maxS * b)
241 | cuspL = (1.0 / max(maxRgb[0], maxRgb[1], maxRgb[2])) ** (1 / 3)
242 | cuspC = cuspL * maxS
243 | return (cuspL, cuspC)
244 |
245 | @staticmethod
246 | # Finds intersection of the line defined by
247 | # L = L0 * (1 - t) + t * L1
248 | # C = t * C1
249 | # a and b must be normalized so a^2 + b^2 == 1
250 | def findGamutIntersection(a: float, b: float, l1: float, c1: float, l0: float, cuspLC=None):
251 | # Find the cusp of the gamut triangle
252 | if cuspLC is None:
253 | cuspLC = Convert.findCuspLC(a, b)
254 | # Find the intersection for upper and lower half separately
255 | if ((l1 - l0) * cuspLC[1] - (cuspLC[0] - l1) * c1) <= 0.0:
256 | # Lower half
257 | t = cuspLC[1] * l0 / (c1 * cuspLC[0] + cuspLC[1] * (l0 - l1))
258 | else:
259 | # Upper half
260 | # First intersect with triangle
261 | t = cuspLC[1] * (l0 - 1.0) / (c1 * (cuspLC[0] - 1.0) + cuspLC[1] * (l0 - l1))
262 | # Then one step Halley's method
263 | dL = l1 - l0
264 | dC = c1
265 |
266 | k_l = +0.3963377774 * a + 0.2158037573 * b
267 | k_m = -0.1055613458 * a - 0.0638541728 * b
268 | k_s = -0.0894841775 * a - 1.2914855480 * b
269 |
270 | l_dt = dL + dC * k_l
271 | m_dt = dL + dC * k_m
272 | s_dt = dL + dC * k_s
273 |
274 | # If higher accuracy is required, 2 or 3 iterations of the following block can be used:
275 | l = l0 * (1.0 - t) + t * l1
276 | c = t * c1
277 |
278 | l_ = l + c * k_l
279 | m_ = l + c * k_m
280 | s_ = l + c * k_s
281 |
282 | l = l_ * l_ * l_
283 | m = m_ * m_ * m_
284 | s = s_ * s_ * s_
285 |
286 | ldt = 3 * l_dt * l_ * l_
287 | mdt = 3 * m_dt * m_ * m_
288 | sdt = 3 * s_dt * s_ * s_
289 |
290 | ldt2 = 6 * l_dt * l_dt * l_
291 | mdt2 = 6 * m_dt * m_dt * m_
292 | sdt2 = 6 * s_dt * s_dt * s_
293 |
294 | r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1
295 | r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt
296 | r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2
297 |
298 | u_r = r1 / (r1 * r1 - 0.5 * r * r2)
299 | t_r = -r * u_r
300 |
301 | g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1
302 | g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt
303 | g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2
304 |
305 | u_g = g1 / (g1 * g1 - 0.5 * g * g2)
306 | t_g = -g * u_g
307 |
308 | b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1
309 | b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt
310 | b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2
311 |
312 | u_b = b1 / (b1 * b1 - 0.5 * b * b2)
313 | t_b = -b * u_b
314 |
315 | t_r = t_r if u_r >= 0.0 else sys.float_info.max
316 | t_g = t_g if u_g >= 0.0 else sys.float_info.max
317 | t_b = t_b if u_b >= 0.0 else sys.float_info.max
318 |
319 | t += min(t_r, t_g, t_b)
320 |
321 | return t
322 |
323 | @staticmethod
324 | def cuspToST(cuspLC: tuple):
325 | l: float = cuspLC[0]
326 | c: float = cuspLC[1]
327 | return (c / l, c / (1 - l))
328 |
329 | # Returns a smooth approximation of the location of the cusp
330 | # This polynomial was created by an optimization process
331 | # It has been designed so that S_mid < S_max and T_mid < T_max
332 | @staticmethod
333 | def getMidST(a_: float, b_: float):
334 | s = 0.11516993 + 1.0 / (+7.44778970 + 4.15901240 * b_
335 | + a_ * (-2.19557347 + 1.75198401 * b_
336 | + a_ * (-2.13704948 - 10.02301043 * b_
337 | + a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_
338 | ))))
339 | t = 0.11239642 + 1.0 / (+1.61320320 - 0.68124379 * b_
340 | + a_ * (+0.40370612 + 0.90148123 * b_
341 | + a_ * (-0.27087943 + 0.61223990 * b_
342 | + a_ * (+0.00299215 - 0.45399568 * b_ - 0.14661872 * a_
343 | ))))
344 | return (s, t)
345 |
346 | @staticmethod
347 | def getCs(l: float, a_: float, b_: float):
348 | cuspLC = Convert.findCuspLC(a_, b_)
349 | cMax = Convert.findGamutIntersection(a_, b_, l, 1, l, cuspLC)
350 | maxST = Convert.cuspToST(cuspLC)
351 | # Scale factor to compensate for the curved part of gamut shape:
352 | k = cMax / min(l * maxST[0], (1 - l) * maxST[1])
353 | midST = Convert.getMidST(a_, b_)
354 | # Use a soft minimum function,
355 | # instead of a sharp triangle shape to get a smooth value for chroma.
356 | cMid = 0.9 * k * (1 / (1 / (l * midST[0]) ** 4 + 1 / ((1 - l) * midST[1]) ** 4)) ** (1 / 4)
357 | # for C_0, the shape is independent of hue, so ST are constant.
358 | # Values picked to roughly be the average values of ST.
359 | c0 = (1 / (1 / (l * 0.4) ** 2 + 1 / ((1 - l) * 0.8) ** 2)) ** (1 / 2)
360 | return (c0, cMid, cMax)
361 |
362 | @staticmethod
363 | def rgbToTRC(rgb: tuple, trc: str):
364 | if trc == "sRGB":
365 | r = Convert.clampF(Convert.componentToSRGB(rgb[0]))
366 | g = Convert.clampF(Convert.componentToSRGB(rgb[1]))
367 | b = Convert.clampF(Convert.componentToSRGB(rgb[2]))
368 | return (r, g, b)
369 | else:
370 | r = Convert.componentToLinear(rgb[0])
371 | g = Convert.componentToLinear(rgb[1])
372 | b = Convert.componentToLinear(rgb[2])
373 | return (r, g, b)
374 |
375 | @staticmethod
376 | def rgbFToInt8(r: float, g: float, b: float, trc: str):
377 | if trc == "sRGB":
378 | r = int(r * 255)
379 | g = int(g * 255)
380 | b = int(b * 255)
381 | else:
382 | r = round(Convert.componentToSRGB(r) * 255)
383 | g = round(Convert.componentToSRGB(g) * 255)
384 | b = round(Convert.componentToSRGB(b) * 255)
385 | return (r, g, b)
386 |
387 | @staticmethod
388 | def rgbFToHexS(r: float, g: float, b: float, trc: str):
389 | # hex codes are in 8 bits per color
390 | rgb = Convert.rgbFToInt8(r, g, b, trc)
391 | # hex converts int to str with first 2 char being 0x
392 | r = hex(rgb[0])[2:].zfill(2).upper()
393 | g = hex(rgb[1])[2:].zfill(2).upper()
394 | b = hex(rgb[2])[2:].zfill(2).upper()
395 | return f"#{r}{g}{b}"
396 |
397 | @staticmethod
398 | def hexSToRgbF(syntax: str, trc: str):
399 | if len(syntax) != 7:
400 | print("Invalid syntax")
401 | return
402 | try:
403 | r = int(syntax[1:3], 16) / 255.0
404 | g = int(syntax[3:5], 16) / 255.0
405 | b = int(syntax[5:7], 16) / 255.0
406 | except ValueError:
407 | print("Invalid syntax")
408 | return
409 |
410 | if trc == "sRGB":
411 | return (r, g, b)
412 | r = Convert.componentToLinear(r)
413 | g = Convert.componentToLinear(g)
414 | b = Convert.componentToLinear(b)
415 | return (r, g, b)
416 |
417 | @staticmethod
418 | def rgbFToOklabS(r: float, g: float, b: float, trc: str):
419 | # if rgb not linear, convert to linear for oklab conversion
420 | if trc == "sRGB":
421 | r = Convert.componentToLinear(r)
422 | g = Convert.componentToLinear(g)
423 | b = Convert.componentToLinear(b)
424 | oklab = Convert.linearToOklab(r, g, b)
425 | # l in percentage, a and b is 0 to 0.3+
426 | okL = round(oklab[0] * 100, 2)
427 | okA = Convert.roundZero(oklab[1], 4)
428 | okB = Convert.roundZero(oklab[2], 4)
429 | return f"oklab({okL}% {okA} {okB})"
430 |
431 | @staticmethod
432 | def oklabSToRgbF(syntax: str, trc: str):
433 | strings = syntax[5:].strip("( )").split()
434 | if len(strings) != 3:
435 | print("Invalid syntax")
436 | return
437 | okL = strings[0]
438 | okA = strings[1]
439 | okB = strings[2]
440 | try:
441 | if "%" in okL:
442 | okL = Convert.clampF(float(okL.strip("%")) / 100)
443 | else:
444 | okL = Convert.clampF(float(okL))
445 | if "%" in okA:
446 | okA = Convert.clampF(float(okA.strip("%")) / 250, 0.4, -0.4)
447 | else:
448 | okA = Convert.clampF(float(okA), 0.4, -0.4)
449 | if "%" in okB:
450 | okB = Convert.clampF(float(okB.strip("%")) / 250, 0.4, -0.4)
451 | else:
452 | okB = Convert.clampF(float(okB), 0.4, -0.4)
453 | except ValueError:
454 | print("Invalid syntax")
455 | return
456 | rgb = Convert.oklabToLinear(okL, okA, okB)
457 | # if rgb not linear, perform transfer functions for components
458 | r = Convert.componentToSRGB(rgb[0]) if trc == "sRGB" else rgb[0]
459 | g = Convert.componentToSRGB(rgb[1]) if trc == "sRGB" else rgb[1]
460 | b = Convert.componentToSRGB(rgb[2]) if trc == "sRGB" else rgb[2]
461 | return (Convert.clampF(r), Convert.clampF(g), Convert.clampF(b))
462 |
463 | @staticmethod
464 | def rgbFToOklchS(r: float, g: float, b: float, trc: str):
465 | # if rgb not linear, convert to linear for oklab conversion
466 | if trc == "sRGB":
467 | r = Convert.componentToLinear(r)
468 | g = Convert.componentToLinear(g)
469 | b = Convert.componentToLinear(b)
470 | oklab = Convert.linearToOklab(r, g, b)
471 | l = round(oklab[0] * 100, 2)
472 | ch = Convert.cartesianToPolar(oklab[1], oklab[2])
473 | c = ch[0]
474 | h = 0
475 | # chroma of neutral colors will not be exactly 0 due to floating point errors
476 | if c < 0.000001:
477 | c = 0
478 | else:
479 | # chroma adjustment due to rounding up blue hue
480 | if 264.052 < ch[1] < 264.06:
481 | h = 264.06
482 | c = round(c - 0.0001, 4)
483 | else:
484 | h = round(ch[1], 2)
485 | c = Convert.roundZero(c, 4)
486 | # l in percentage, c is 0 to 0.3+, h in degrees
487 | return f"oklch({l}% {c} {h})"
488 |
489 | @staticmethod
490 | def oklchSToRgbF(syntax: str, trc: str):
491 | strings = syntax[5:].strip("( )").split()
492 | if len(strings) != 3:
493 | print("Invalid syntax")
494 | return
495 | l = strings[0]
496 | c = strings[1]
497 | h = strings[2]
498 | try:
499 | if "%" in l:
500 | l = Convert.clampF(float(l.strip("%")) / 100)
501 | else:
502 | l = Convert.clampF(float(l))
503 | if "%" in c:
504 | c = Convert.clampF(float(c.strip("%")) / 250, 0.4)
505 | else:
506 | c = Convert.clampF(float(c), 0.4)
507 | h = Convert.clampF(float(h.strip("deg")), 360.0)
508 | except ValueError:
509 | print("Invalid syntax")
510 | return
511 | # clip chroma if exceed sRGB gamut
512 | ab = Convert.polarToCartesian(1, h)
513 | if c:
514 | u = Convert.findGamutIntersection(*ab, l, 1, l)
515 | if c > u:
516 | c = u
517 | rgb = Convert.oklabToLinear(l, ab[0] * c, ab[1] * c)
518 | # if rgb not linear, perform transfer functions for components
519 | r = Convert.componentToSRGB(rgb[0]) if trc == "sRGB" else rgb[0]
520 | g = Convert.componentToSRGB(rgb[1]) if trc == "sRGB" else rgb[1]
521 | b = Convert.componentToSRGB(rgb[2]) if trc == "sRGB" else rgb[2]
522 | return (Convert.clampF(r), Convert.clampF(g), Convert.clampF(b))
523 |
524 | @staticmethod
525 | def hSectorToRgbF(hSector: float, v: float, m: float, x: float, trc: str="sRGB"):
526 | # assign max, med and min according to hue sector
527 | if hSector == 1: # between yellow and green
528 | r = x
529 | g = v
530 | b = m
531 | elif hSector == 2: # between green and cyan
532 | r = m
533 | g = v
534 | b = x
535 | elif hSector == 3: # between cyan and blue
536 | r = m
537 | g = x
538 | b = v
539 | elif hSector == 4: # between blue and magenta
540 | r = x
541 | g = m
542 | b = v
543 | elif hSector == 5: # between magenta and red
544 | r = v
545 | g = m
546 | b = x
547 | else: # between red and yellow
548 | r = v
549 | g = x
550 | b = m
551 | # convert to linear if not sRGB
552 | if trc == "sRGB":
553 | return (r, g, b)
554 | r = Convert.componentToLinear(r)
555 | g = Convert.componentToLinear(g)
556 | b = Convert.componentToLinear(b)
557 | return (r, g, b)
558 |
559 | @staticmethod
560 | def rgbFToHsv(r: float, g: float, b: float, trc: str):
561 | # if rgb is linear, convert to sRGB
562 | if trc == "linear":
563 | r = Convert.componentToSRGB(r)
564 | g = Convert.componentToSRGB(g)
565 | b = Convert.componentToSRGB(b)
566 | # value is equal to max(R,G,B) while min(R,G,B) determines saturation
567 | v = max(r,g,b)
568 | m = min(r,g,b)
569 | # chroma is the colorfulness of the color compared to the neutral color of equal value
570 | c = v - m
571 | if c == 0:
572 | # hue cannot be determined if the color is neutral
573 | return (0, 0, round(v * 100, 2))
574 | # hue is defined in 60deg sectors
575 | # hue = primary hue + deviation
576 | # max(R,G,B) determines primary hue while med(R,G,B) determines deviation
577 | # deviation has a range of -0.999... to 0.999...
578 | if v == r:
579 | # red is 0, range of hues that are predominantly red is -0.999... to 0.999...
580 | # dividing (g - b) by chroma takes saturation and value out of the equation
581 | # resulting in hue deviation of the primary color
582 | h = ((g - b) / c) % 6
583 | elif v == g:
584 | # green is 2, range of hues that are predominantly green is 1.000... to 2.999...
585 | h = (b - r) / c + 2
586 | elif v == b:
587 | # blue is 4, range of hues that are predominantly blue is 3.000... to 4.999...
588 | h = (r - g) / c + 4
589 | # saturation is the ratio of chroma of the color to the maximum chroma of equal value
590 | # which is normalized chroma to fit the range of 0-1
591 | s = c / v
592 | return (round(h * 60, 2), round(s * 100, 2), round(v * 100, 2))
593 |
594 | @staticmethod
595 | def hsvToRgbF(h: float, s: float, v: float, trc: str):
596 | # derive hue in 60deg sectors
597 | h /= 60
598 | hSector = int(h)
599 | # scale saturation and value range from 0-100 to 0-1
600 | s /= 100
601 | v /= 100
602 | # max(R,G,B) = value
603 | # chroma = saturation * value
604 | # min(R,G,B) = max(R,G,B) - chroma
605 | m = v * (1 - s)
606 | # calculate deviation from closest secondary color with range of -0.999... to 0.999...
607 | # |deviation| = 1 - derived hue - hue sector if deviation is positive
608 | # |deviation| = derived hue - hue sector if deviation is negative
609 | d = h - hSector if hSector % 2 else 1 - (h - hSector)
610 | # med(R,G,B) = max(R,G,B) - (|deviation| * chroma)
611 | x = v * (1 - d * s)
612 | return Convert.hSectorToRgbF(hSector, v, m, x, trc)
613 |
614 | @staticmethod
615 | def rgbFToHsl(r: float, g: float, b: float, trc: str):
616 | # if rgb is linear, convert to sRGB
617 | if trc == "linear":
618 | r = Convert.componentToSRGB(r)
619 | g = Convert.componentToSRGB(g)
620 | b = Convert.componentToSRGB(b)
621 | v = max(r,g,b)
622 | m = min(r,g,b)
623 | # lightness is defined as the midrange of the RGB components
624 | l = (v + m) / 2
625 | c = v - m
626 | # hue cannot be determined if the color is neutral
627 | if c == 0:
628 | return (0, 0, round(l * 100, 2))
629 | # same formula as hsv to find hue
630 | if v == r:
631 | h = ((g - b) / c) % 6
632 | elif v == g:
633 | h = (b - r) / c + 2
634 | elif v == b:
635 | h = (r - g) / c + 4
636 | # saturation = chroma / chroma range
637 | # max chroma range when lightness at half
638 | s = c / (1 - abs(2 * l - 1))
639 | return (round(h * 60, 2), round(s * 100, 2), round(l * 100, 2))
640 |
641 | @staticmethod
642 | def hslToRgbF(h: float, s: float, l: float, trc: str):
643 | # derive hue in 60deg sectors
644 | h /= 60
645 | hSector = int(h)
646 | # scale saturation and value range from 0-100 to 0-1
647 | s /= 100
648 | l /= 100
649 | # max(R,G,B) = s(l) + l if l<0.5 else s(1 - l) + l
650 | v = l * (1 + s) if l < 0.5 else s * (1 - l) + l
651 | m = 2 * l - v
652 | # calculate deviation from closest secondary color with range of -0.999... to 0.999...
653 | d = h - hSector if hSector % 2 else 1 - (h - hSector)
654 | x = v - d * (v - m)
655 | return Convert.hSectorToRgbF(hSector, v, m, x, trc)
656 |
657 | @staticmethod
658 | def rgbFToHcy(r: float, g: float, b: float, h: float, trc: str, luma: bool):
659 | # if y should always be luma, convert to sRGB
660 | if luma and trc == "linear":
661 | r = Convert.componentToSRGB(r)
662 | g = Convert.componentToSRGB(g)
663 | b = Convert.componentToSRGB(b)
664 | # y can be luma or relative luminance depending on rgb format
665 | y = Y709R * r + Y709G * g + Y709B * b
666 | v = max(r, g, b)
667 | m = min(r, g, b)
668 | c = v - m
669 | yHue = 0
670 | # if color is neutral, use previous hue to calculate luma coefficient of hue
671 | # max(R,G,B) coefficent + med(R,G,B) coefficient * deviation from max(R,G,B) hue
672 | if (c != 0 and v == g) or (c == 0 and 60 <= h <= 180):
673 | h = (b - r) / c + 2 if c != 0 else h / 60
674 | if 1 <= h <= 2: # between yellow and green
675 | d = h - 1
676 | # luma coefficient of hue ranges from 0.9278 to 0.7152
677 | yHue = Y709G + Y709R * (1 - d)
678 | elif 2 < h <= 3: # between green and cyan
679 | d = h - 2
680 | # luma coefficient of hue ranges from 0.7152 to 0.7874
681 | yHue = Y709G + Y709B * d
682 | elif (c != 0 and v == b) or (c == 0 and 180 < h <= 300):
683 | h = (r - g) / c + 4 if c != 0 else h / 60
684 | if 3 < h <= 4: # between cyan and blue
685 | d = h - 3
686 | # luma coefficient of hue ranges from 0.7874 to 0.0722
687 | yHue = Y709B + Y709G * (1 - d)
688 | elif 4 < h <= 5: # between blue and magenta
689 | d = h - 4
690 | # luma coefficient of hue ranges from 0.0722 to 0.2848
691 | yHue = Y709B + Y709R * d
692 | elif (c != 0 and v == r) or (c == 0 and (h > 300 or h < 60)):
693 | h = ((g - b) / c) % 6 if c != 0 else h / 60
694 | if 5 < h <= 6: # between magenta and red
695 | d = h - 5
696 | # luma coefficient of hue ranges from 0.2848 to 0.2126
697 | yHue = Y709R + Y709B * (1 - d)
698 | elif 0 <= h < 1: # between red and yellow
699 | d = h
700 | # luma coefficient of hue ranges from 0.2126 to 0.9278
701 | yHue = Y709R + Y709G * d
702 | # calculate upper limit of chroma for hue and luma pair
703 | u = y / yHue if y <= yHue else (1 - y) / (1 - yHue)
704 | return (round(h * 60, 2), round(c * 100, 3), round(y * 100, 2), round(u * 100, 3))
705 |
706 | @staticmethod
707 | def hcyToRgbF(h: float, c: float, y: float, u: float, trc: str, luma: bool):
708 | # derive hue in 60deg sectors
709 | h /= 60
710 | hSector = int(h)
711 | # pass in y and u as -1 for max chroma conversions
712 | if y != -1:
713 | # scale luma to 1
714 | y /= 100
715 | if c == 0 or y == 0 or y == 1:
716 | # if y is always luma, convert to linear
717 | if luma and trc == "linear":
718 | y = Convert.componentToLinear(y)
719 | # luma coefficients add up to 1
720 | return (y, y, y)
721 | # calculate deviation from closest primary color with range of -0.999... to 0.999...
722 | # |deviation| = 1 - derived hue - hue sector if deviation is negative
723 | # |deviation| = derived hue - hue sector if deviation is positive
724 | d = h - hSector if hSector % 2 == 0 else 1 - (h - hSector)
725 | # calculate luma coefficient of hue
726 | yHue = 0
727 | if hSector == 1: # between yellow and green
728 | yHue = Y709G + Y709R * d
729 | elif hSector == 2: # between green and cyan
730 | yHue = Y709G + Y709B * d
731 | elif hSector == 3: # between cyan and blue
732 | yHue = Y709B + Y709G * d
733 | elif hSector == 4: # between blue and magenta
734 | yHue = Y709B + Y709R * d
735 | elif hSector == 5: # between magenta and red
736 | yHue = Y709R + Y709B * d
737 | else: # between red and yellow
738 | yHue = Y709R + Y709G * d
739 | # when chroma is at maximum, y = luma coefficient of hue
740 | if y == -1:
741 | y = yHue
742 | # it is not always possible for chroma to be constant when adjusting hue or luma
743 | # adjustment have to either clip chroma or have consistent saturation instead
744 | cMax = y / yHue if y <= yHue else (1 - y) / (1 - yHue)
745 | if u == -1:
746 | # scale chroma to 1 before comparing
747 | c /= 100
748 | # clip chroma to new limit
749 | if c > cMax:
750 | c = cMax
751 | else:
752 | # scale chroma to hue or luma adjustment
753 | s = 0
754 | if u:
755 | s = c / u
756 | c = s * cMax
757 | # luma = max(R,G,B) * yHue + min(R,G,B) * (1 - yHue)
758 | # calculate min(R,G,B) based on the equation above
759 | m = y - c * yHue
760 | # med(R,G,B) = min(R,G,B) + (|deviation| * chroma)
761 | x = y - c * (yHue - d)
762 | # max(R,G,B) = min(R,G,B) + chroma
763 | v = y + c * (1 - yHue)
764 | # if y is always luma, hsector to rgbf needs trc param
765 | if luma:
766 | return Convert.hSectorToRgbF(hSector, v, m, x, trc)
767 | return Convert.hSectorToRgbF(hSector, v, m, x)
768 |
769 | @staticmethod
770 | def rgbFToOkhcl(r: float, g: float, b: float, h: float, trc: str):
771 | # if rgb not linear, convert to linear for oklab conversion
772 | if trc == "sRGB":
773 | r = Convert.componentToLinear(r)
774 | g = Convert.componentToLinear(g)
775 | b = Convert.componentToLinear(b)
776 | oklab = Convert.linearToOklab(r, g, b)
777 | l = oklab[0]
778 | ch = Convert.cartesianToPolar(oklab[1], oklab[2])
779 | c = ch[0]
780 | # chroma of neutral colors will not be exactly 0 due to floating point errors
781 | if c < 0.000001:
782 | # use current hue to calulate chroma limit in sRGB gamut for neutral colors
783 | ab = Convert.polarToCartesian(1, h)
784 | cuspLC = Convert.findCuspLC(*ab)
785 | u = Convert.findGamutIntersection(*ab, l, 1, l, cuspLC)
786 | u /= cuspLC[1]
787 | c = 0
788 | else:
789 | # gamut intersection jumps for parts of blue
790 | h = ch[1] if not 264.052 < ch[1] < 264.06 else 264.06
791 | # a and b must be normalized to c = 1 to calculate chroma limit in sRGB gamut
792 | a_ = oklab[1] / c
793 | b_ = oklab[2] / c
794 | cuspLC = Convert.findCuspLC(a_, b_)
795 | u = Convert.findGamutIntersection(a_, b_, l, 1, l, cuspLC)
796 | if c > u:
797 | c = u
798 | u /= cuspLC[1]
799 | c /= cuspLC[1]
800 | l = Convert.toe(l)
801 | return (round(h, 2), round(c * 100, 3), round(l * 100, 2), round(u * 100, 3))
802 |
803 | @staticmethod
804 | def okhclToRgbF(h: float, c: float, l: float, u: float, trc: str):
805 | # convert lref back to okL
806 | l = Convert.toeInv(l / 100)
807 | # clip chroma if exceed sRGB gamut
808 | ab = (0, 0)
809 | if c:
810 | ab = Convert.polarToCartesian(1, h)
811 | cuspLC = Convert.findCuspLC(*ab)
812 | cMax = Convert.findGamutIntersection(*ab, l, 1, l, cuspLC)
813 | if u == -1:
814 | c = c / 100 * cuspLC[1]
815 | if c > cMax:
816 | c = cMax
817 | else:
818 | s = c / u
819 | c = s * cMax
820 | ab = Convert.polarToCartesian(c, h)
821 | rgb = Convert.oklabToLinear(l, *ab)
822 | # perform transfer functions for components if output to sRGB
823 | r = Convert.componentToSRGB(rgb[0]) if trc == "sRGB" else rgb[0]
824 | g = Convert.componentToSRGB(rgb[1]) if trc == "sRGB" else rgb[1]
825 | b = Convert.componentToSRGB(rgb[2]) if trc == "sRGB" else rgb[2]
826 | return (Convert.clampF(r), Convert.clampF(g), Convert.clampF(b))
827 |
828 | @staticmethod
829 | def rgbFToOkhsv(r: float, g: float, b: float, trc: str):
830 | # if rgb not linear, convert to linear for oklab conversion
831 | if trc == "sRGB":
832 | r = Convert.componentToLinear(r)
833 | g = Convert.componentToLinear(g)
834 | b = Convert.componentToLinear(b)
835 | oklab = Convert.linearToOklab(r, g, b)
836 | l = oklab[0]
837 | ch = Convert.cartesianToPolar(oklab[1], oklab[2])
838 | c = ch[0]
839 | # chroma of neutral colors will not be exactly 0 due to floating point errors
840 | if c < 0.000001:
841 | return (0, 0, round(Convert.toe(l) * 100, 2))
842 | else:
843 | # gamut intersection jumps for parts of blue
844 | h = ch[1] if not 264.052 < ch[1] < 264.06 else 264.06
845 | # a and b must be normalized to c = 1 to calculate chroma limit in sRGB gamut
846 | a_ = oklab[1] / c
847 | b_ = oklab[2] / c
848 | cuspLC = Convert.findCuspLC(a_, b_)
849 | st = Convert.cuspToST(cuspLC)
850 | sMax = st[0]
851 | tMax = st[1]
852 | s0 = 0.5
853 | k = 1 - s0 / sMax
854 | # first we find L_v, C_v, L_vt and C_vt
855 | t = tMax / (c + l * tMax)
856 | l_v = t * l
857 | c_v = t * c
858 | l_vt = Convert.toeInv(l_v)
859 | c_vt = c_v * l_vt / l_v
860 | # we can then use these to invert the step that compensates for the toe
861 | # and the curved top part of the triangle:
862 | rgbScale = Convert.oklabToLinear(l_vt, a_ * c_vt, b_ * c_vt)
863 | scaleL = (1 / max(rgbScale[0], rgbScale[1], rgbScale[2])) ** (1 / 3)
864 | l = Convert.toe(l / scaleL)
865 | # // we can now compute v and s:
866 | v = l / l_v
867 | s = (s0 + tMax) * c_v / ((tMax * s0) + tMax * k * c_v)
868 | if s > 1:
869 | s = 1.0
870 | return (round(h, 2), round(s * 100, 2), round(v * 100, 2))
871 |
872 | @staticmethod
873 | def okhsvToRgbF(h: float, s: float, v: float, trc: str):
874 | # scale saturation and value range from 0-100 to 0-1
875 | s /= 100
876 | v /= 100
877 | rgb = None
878 | if v == 0:
879 | return (0, 0, 0)
880 | elif s == 0:
881 | rgb = Convert.oklabToLinear(Convert.toeInv(v), 0, 0)
882 | else:
883 | ab = Convert.polarToCartesian(1, h)
884 | cuspLC = Convert.findCuspLC(*ab)
885 | st = Convert.cuspToST(cuspLC)
886 | sMax = st[0]
887 | tMax = st[1]
888 | s0 = 0.5
889 | k = 1 - s0 / sMax
890 | # first we compute L and V as if the gamut is a perfect triangle:
891 | # L, C when v==1:
892 | l_v = 1 - s * s0 / (s0 + tMax - tMax * k * s)
893 | c_v = s * tMax * s0 / (s0 + tMax - tMax * k * s)
894 | l = v * l_v
895 | c = v * c_v
896 | # then we compensate for both toe and the curved top part of the triangle:
897 | l_vt = Convert.toeInv(l_v)
898 | c_vt = c_v * l_vt / l_v
899 | l_new = Convert.toeInv(l)
900 | c *= l_new / l
901 | l = l_new
902 | rgbScale = Convert.oklabToLinear(l_vt, ab[0] * c_vt, ab[1] * c_vt)
903 | scaleL = (1 / max(rgbScale[0], rgbScale[1], rgbScale[2])) ** (1 / 3)
904 | l *= scaleL
905 | c *= scaleL
906 | rgb = Convert.oklabToLinear(l, ab[0] * c, ab[1] * c)
907 | # perform transfer functions for components if output to sRGB
908 | r = Convert.componentToSRGB(rgb[0]) if trc == "sRGB" else rgb[0]
909 | g = Convert.componentToSRGB(rgb[1]) if trc == "sRGB" else rgb[1]
910 | b = Convert.componentToSRGB(rgb[2]) if trc == "sRGB" else rgb[2]
911 | return (Convert.clampF(r), Convert.clampF(g), Convert.clampF(b))
912 |
913 | @staticmethod
914 | def rgbFToOkhsl(r: float, g: float, b: float, trc: str):
915 | # if rgb not linear, convert to linear for oklab conversion
916 | if trc == "sRGB":
917 | r = Convert.componentToLinear(r)
918 | g = Convert.componentToLinear(g)
919 | b = Convert.componentToLinear(b)
920 | oklab = Convert.linearToOklab(r, g, b)
921 | l = oklab[0]
922 | ch = Convert.cartesianToPolar(oklab[1], oklab[2])
923 | s = 0
924 | c = ch[0]
925 | # chroma of neutral colors will not be exactly 0 due to floating point errors
926 | if c >= 0.000001:
927 | a_ = oklab[1] / c
928 | b_ = oklab[2] / c
929 | cs = Convert.getCs(l, a_, b_)
930 | c0 = cs[0]
931 | cMid = cs[1]
932 | cMax = cs[2]
933 | # Inverse of the interpolation in okhsl_to_srgb:
934 | mid = 0.8
935 | midInv = 1.25
936 | if c < cMid:
937 | k1 = mid * c0
938 | k2 = 1 - k1 / cMid
939 | t = c / (k1 + k2 * c)
940 | s = t * mid
941 | else:
942 | k1 = (1 - mid) * cMid * cMid * midInv * midInv / c0
943 | k2 = 1 - k1 / (cMax - cMid)
944 | t = (c - cMid) / (k1 + k2 * (c - cMid))
945 | s = mid + (1 - mid) * t
946 | # gamut intersection jumps for parts of blue
947 | h = ch[1] if not 264.052 < ch[1] < 264.06 else 264.06
948 | l = Convert.toe(l)
949 | return (round(h, 2), round(s * 100, 2), round(l * 100, 2))
950 |
951 | @staticmethod
952 | def okhslToRgbF(h: float, s: float, l: float, trc: str):
953 | # scale saturation and lightness range from 0-100 to 0-1
954 | s /= 100
955 | l /= 100
956 | if l == 0 or l == 1:
957 | return (l, l, l)
958 | ab = Convert.polarToCartesian(1, h)
959 | l = Convert.toeInv(l)
960 | c = 0
961 | if s:
962 | cs = Convert.getCs(l, *ab)
963 | c0 = cs[0]
964 | cMid = cs[1]
965 | cMax = cs[2]
966 | # Interpolate the three values for C so that:
967 | # At s=0: dC/ds = C_0, C=0
968 | # At s=0.8: C=C_mid
969 | # At s=1.0: C=C_max
970 | mid = 0.8
971 | midInv = 1.25
972 | if s < mid:
973 | t = midInv * s
974 | k1 = mid * c0
975 | k2 = 1 - k1 / cMid
976 | c = t * k1 / (1 - k2 * t)
977 | else:
978 | t = (s - mid) / (1 - mid)
979 | k1 = (1 - mid) * cMid * cMid * midInv * midInv / c0
980 | k2 = 1 - k1 / (cMax - cMid)
981 | c = cMid + t * k1 / (1 - k2 * t)
982 | rgb = Convert.oklabToLinear(l, ab[0] * c, ab[1] * c)
983 | # perform transfer functions for components if output to sRGB
984 | r = Convert.componentToSRGB(rgb[0]) if trc == "sRGB" else rgb[0]
985 | g = Convert.componentToSRGB(rgb[1]) if trc == "sRGB" else rgb[1]
986 | b = Convert.componentToSRGB(rgb[2]) if trc == "sRGB" else rgb[2]
987 | return (Convert.clampF(r), Convert.clampF(g), Convert.clampF(b))
988 |
989 |
--------------------------------------------------------------------------------
/hclsliders/hclsliders.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | #
3 | # HCL Sliders is a Krita plugin for color selection.
4 | # Copyright (C) 2024 Lucifer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 | #
19 | # This file incorporates work covered by the following copyright and
20 | # permission notice:
21 | #
22 | # Pigment.O is a Krita plugin and it is a Color Picker and Color Mixer.
23 | # Copyright ( C ) 2020 Ricardo Jeremias.
24 | #
25 | # This program is free software: you can redistribute it and/or modify
26 | # it under the terms of the GNU General Public License as published by
27 | # the Free Software Foundation, either version 3 of the License, or
28 | # ( at your option ) any later version.
29 | #
30 | # This program is distributed in the hope that it will be useful,
31 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
32 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33 | # GNU General Public License for more details.
34 | #
35 | # You should have received a copy of the GNU General Public License
36 | # along with this program. If not, see .
37 |
38 | from PyQt5.QtCore import Qt, pyqtSignal, QTimer, QSize
39 | from PyQt5.QtGui import QPainter, QBrush, QColor, QLinearGradient, QPixmap, QIcon
40 | from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QDoubleSpinBox, QLabel, QLineEdit,
41 | QPushButton, QListWidget, QListWidgetItem, QDialog, QStackedWidget,
42 | QTabWidget, QCheckBox, QGroupBox, QRadioButton, QSpinBox)
43 | from krita import DockWidget, ManagedColor
44 |
45 | from .colorconversion import Convert
46 |
47 | DOCKER_NAME = 'HCL Sliders'
48 | # adjust plugin sizes and update timing here
49 | TIME = 100 # ms time for plugin to update color from krita, faster updates may make krita slower
50 | DELAY = 300 # ms delay updating color history to prevent flooding when using the color picker
51 | DISPLAY_HEIGHT = 25 # px for color display panel at the top
52 | CHANNEL_HEIGHT = 19 # px for channels, also influences hex/ok syntax box and buttons
53 | MODEL_SPACING = 6 # px for spacing between color models
54 | HISTORY_HEIGHT = 16 # px for color history and area of each color box
55 | VALUES_WIDTH = 63 # px for spinboxes containing channel values
56 | LABEL_WIDTH = 11 # px for spacing of channel indicator/letter
57 | # adjust various sizes of config menu
58 | CONFIG_SIZE = (468, 230) # (width in px, height in px) size for config window
59 | SIDEBAR_WIDTH = 76 # px for sidebar containing channel selection and others button
60 | GROUPBOX_HEIGHT = 64 # px for groupboxes of cursor snapping, chroma mode and color history
61 | SPINBOX_WIDTH = 72 # px for spinboxes of interval, displacement and memory
62 | OTHERS_HEIGHT = 12 # px for spacing before color history in others page
63 | # compatible color profiles in krita
64 | SRGB = ('sRGB-elle-V2-srgbtrc.icc', 'sRGB built-in',
65 | 'Gray-D50-elle-V2-srgbtrc.icc', 'Gray-D50-elle-V4-srgbtrc.icc')
66 | LINEAR = ('sRGB-elle-V2-g10.icc', 'krita-2.5, lcms sRGB built-in with linear gamma TRC',
67 | 'Gray-D50-elle-V2-g10.icc', 'Gray-D50-elle-V4-g10.icc')
68 | NOTATION = ('HEX', 'OKLAB', 'OKLCH')
69 |
70 |
71 | class ColorDisplay(QWidget):
72 |
73 | def __init__(self, parent):
74 | super().__init__(parent)
75 |
76 | self.hcl = parent
77 | self.current = None
78 | self.recent = None
79 | self.foreground = None
80 | self.background = None
81 | self.temp = None
82 | self.bgMode = False
83 | self.switchToolTip()
84 |
85 | def setCurrentColor(self, color=None):
86 | self.current = color
87 | self.update()
88 |
89 | def setForeGroundColor(self, color=None):
90 | self.foreground = color
91 | self.update()
92 |
93 | def setBackGroundColor(self, color=None):
94 | self.background = color
95 | self.update()
96 |
97 | def setTempColor(self, color=None):
98 | self.temp = color
99 | self.update()
100 |
101 | def resetColors(self):
102 | self.current = None
103 | self.recent = None
104 | self.foreground = None
105 | self.background = None
106 | self.temp = None
107 | self.update()
108 |
109 | def isChanged(self):
110 | if self.current is None:
111 | return True
112 | if self.bgMode:
113 | if self.current.components() != self.background.components():
114 | return True
115 | if self.current.colorModel() != self.background.colorModel():
116 | return True
117 | if self.current.colorDepth() != self.background.colorDepth():
118 | return True
119 | if self.current.colorProfile() != self.background.colorProfile():
120 | return True
121 | else:
122 | if self.current.components() != self.foreground.components():
123 | return True
124 | if self.current.colorModel() != self.foreground.colorModel():
125 | return True
126 | if self.current.colorDepth() != self.foreground.colorDepth():
127 | return True
128 | if self.current.colorProfile() != self.foreground.colorProfile():
129 | return True
130 | return False
131 |
132 | def isChanging(self):
133 | if self.recent is None:
134 | return False
135 | if self.recent.components() != self.current.components():
136 | return True
137 | if self.recent.colorModel() != self.current.colorModel():
138 | return True
139 | if self.recent.colorDepth() != self.current.colorDepth():
140 | return True
141 | if self.recent.colorProfile() != self.current.colorProfile():
142 | return True
143 | return False
144 |
145 | def switchToolTip(self):
146 | if self.bgMode:
147 | self.setToolTip("Background Color")
148 | else:
149 | self.setToolTip("Foreground Color")
150 |
151 | def switchMode(self):
152 | self.bgMode = not self.bgMode
153 | self.switchToolTip()
154 | self.update()
155 |
156 | def mousePressEvent(self, event):
157 | self.setFocus()
158 | self.switchMode()
159 |
160 | def paintEvent(self, event):
161 | painter = QPainter(self)
162 | painter.setPen(Qt.PenStyle.NoPen)
163 | width = self.width()
164 | halfwidth = round(width / 2.0)
165 | height = self.height()
166 | # foreground/background color from krita
167 | if self.foreground and not self.bgMode:
168 | painter.setBrush(QBrush(self.foreground.colorForCanvas(self.hcl.canvas())))
169 | elif self.background and self.bgMode:
170 | painter.setBrush(QBrush(self.background.colorForCanvas(self.hcl.canvas())))
171 | else:
172 | painter.setBrush( QBrush(QColor(0, 0, 0)))
173 | painter.drawRect(0, 0, width, height)
174 | # current color from sliders
175 | if self.current:
176 | painter.setBrush(QBrush(self.current.colorForCanvas(self.hcl.canvas())))
177 | if self.bgMode:
178 | painter.drawRect(halfwidth, 0, width - halfwidth, height)
179 | else:
180 | painter.drawRect(0, 0, halfwidth, height)
181 | # indicator for picking past color in other mode
182 | if self.temp:
183 | painter.setBrush(QBrush(self.temp.colorForCanvas(self.hcl.canvas())))
184 | if self.bgMode:
185 | painter.drawRect(0, 0, halfwidth, height)
186 | else:
187 | painter.drawRect(halfwidth, 0, width - halfwidth, height)
188 |
189 |
190 | class ColorHistory(QListWidget):
191 |
192 | def __init__(self, hcl, parent=None):
193 | super().__init__(parent)
194 | # should not pass in hcl as parent if it can be hidden
195 | self.hcl = hcl
196 | self.index = -1
197 | self.modifier = None
198 | self.start = 0
199 | self.position = 0
200 | self.setFlow(QListWidget.Flow.LeftToRight)
201 | self.setFixedHeight(HISTORY_HEIGHT)
202 | self.setViewportMargins(-2, 0, 0, 0)
203 | # grid width + 2 to make gaps between swatches
204 | self.setGridSize(QSize(HISTORY_HEIGHT + 2, HISTORY_HEIGHT))
205 | self.setUniformItemSizes(True)
206 | self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
207 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
208 | self.setHorizontalScrollMode(QListWidget.ScrollMode.ScrollPerPixel)
209 | self.setSelectionMode(QListWidget.SelectionMode.NoSelection)
210 |
211 | def startScrollShift(self, event):
212 | self.start = self.horizontalScrollBar().value()
213 | self.position = event.x()
214 |
215 | def keyPressEvent(self, event):
216 | # disable keyboard interactions
217 | pass
218 |
219 | def mousePressEvent(self, event):
220 | self.hcl.setPressed(True)
221 | item = self.itemAt(event.pos())
222 | index = self.row(item)
223 |
224 | if index != -1:
225 | if (event.buttons() == Qt.MouseButton.LeftButton and
226 | event.modifiers() == Qt.KeyboardModifier.NoModifier):
227 | color = self.hcl.makeManagedColor(*self.hcl.pastColors[index])
228 | if color:
229 | if self.hcl.color.bgMode:
230 | self.hcl.color.setTempColor(color)
231 | else:
232 | self.hcl.color.setCurrentColor(color)
233 | self.index = index
234 | self.modifier = Qt.KeyboardModifier.NoModifier
235 | elif (event.buttons() == Qt.MouseButton.LeftButton and
236 | event.modifiers() == Qt.KeyboardModifier.ControlModifier):
237 | color = self.hcl.makeManagedColor(*self.hcl.pastColors[index])
238 | if color:
239 | if self.hcl.color.bgMode:
240 | self.hcl.color.setCurrentColor(color)
241 | else:
242 | self.hcl.color.setTempColor(color)
243 | self.index = index
244 | self.modifier = Qt.KeyboardModifier.ControlModifier
245 | elif (event.buttons() == Qt.MouseButton.LeftButton and
246 | event.modifiers() == Qt.KeyboardModifier.AltModifier):
247 | self.index = index
248 | self.modifier = Qt.KeyboardModifier.AltModifier
249 | self.startScrollShift(event)
250 |
251 | def mouseMoveEvent(self, event):
252 | if (event.buttons() == Qt.MouseButton.LeftButton and
253 | event.modifiers() == Qt.KeyboardModifier.ShiftModifier):
254 | position = 0
255 | bar = self.horizontalScrollBar()
256 | if bar.maximum():
257 | # speed of grid width squared seems good
258 | speed = (HISTORY_HEIGHT + 2) ** 2
259 | # move bar at constant speed
260 | shift = float(self.position - event.x()) / self.width()
261 | position = round(self.start + shift * speed)
262 | bar.setValue(position)
263 | else:
264 | self.startScrollShift(event)
265 |
266 | def mouseReleaseEvent(self, event):
267 | item = self.itemAt(event.pos())
268 | index = self.row(item)
269 |
270 | if index == self.index and index != -1:
271 | if (event.modifiers() == Qt.KeyboardModifier.NoModifier and
272 | self.modifier == Qt.KeyboardModifier.NoModifier):
273 | self.hcl.setPastColor(index)
274 | elif (event.modifiers() == Qt.KeyboardModifier.ControlModifier and
275 | self.modifier == Qt.KeyboardModifier.ControlModifier):
276 | self.hcl.setPastColor(index, False)
277 |
278 | if (event.modifiers() == Qt.KeyboardModifier.AltModifier and
279 | self.modifier == Qt.KeyboardModifier.AltModifier):
280 | if self.index != -1 and index != -1 :
281 | start = index
282 | stop = self.index
283 | if self.index > index:
284 | start = self.index
285 | stop = index
286 | for i in range(start, stop - 1, -1):
287 | self.takeItem(i)
288 | self.hcl.pastColors.pop(i)
289 |
290 | if self.modifier == Qt.KeyboardModifier.NoModifier and self.index != -1:
291 | if self.hcl.color.bgMode:
292 | self.hcl.color.setTempColor()
293 | else:
294 | # prevent setHistory when krita fg color not changed
295 | self.hcl.color.current = self.hcl.color.foreground
296 | elif self.modifier == Qt.KeyboardModifier.ControlModifier and self.index != -1:
297 | if self.hcl.color.bgMode:
298 | # prevent setHistory when krita bg color not changed
299 | self.hcl.color.current = self.hcl.color.background
300 | else:
301 | self.hcl.color.setTempColor()
302 |
303 | self.modifier = None
304 | self.index = -1
305 | self.hcl.setPressed(False)
306 |
307 |
308 | class ChannelSlider(QWidget):
309 |
310 | valueChanged = pyqtSignal(float)
311 | mousePressed = pyqtSignal(bool)
312 |
313 | def __init__(self, limit: float, parent=None):
314 | super().__init__(parent)
315 |
316 | self.value = 0.0
317 | self.limit = limit
318 | self.interval = 0.1
319 | self.displacement = 0
320 | self.start = 0.0
321 | self.position = 0
322 | self.shift = 0.1
323 | self.colors = []
324 |
325 | def setGradientColors(self, colors: list):
326 | if self.colors:
327 | self.colors = []
328 | for rgb in colors:
329 | # using rgbF as is may result in black as colors are out of gamut
330 | color = QColor(*rgb)
331 | self.colors.append(color)
332 | self.update()
333 |
334 | def setValue(self, value: float):
335 | self.value = value
336 | self.update()
337 |
338 | def setLimit(self, value: float):
339 | self.limit = value
340 | self.update()
341 |
342 | def setInterval(self, interval: float):
343 | limit = 100.0 if self.limit < 360 else 360.0
344 | if interval < 0.1:
345 | interval = 0.1
346 | elif interval > limit:
347 | interval = limit
348 | self.interval = interval
349 |
350 | def setDisplacement(self, displacement: float):
351 | limit = 99.9 if self.limit < 360 else 359.9
352 | if displacement < 0:
353 | displacement = 0
354 | elif displacement > limit:
355 | displacement = limit
356 | self.displacement = displacement
357 |
358 | def emitValueChanged(self, event):
359 | position = event.x()
360 | width = self.width()
361 | if position > width:
362 | position = width
363 | elif position < 0:
364 | position = 0.0
365 | self.value = round((position / width) * self.limit, 3)
366 | self.valueChanged.emit(self.value)
367 | self.mousePressed.emit(True)
368 |
369 | def emitValueSnapped(self, event):
370 | position = event.x()
371 | width = self.width()
372 | if position > width:
373 | position = width
374 | elif position < 0:
375 | position = 0.0
376 | value = round((position / width) * self.limit, 3)
377 |
378 | if value != 0 and value != self.limit:
379 | interval = self.interval if self.interval != 0 else self.limit
380 | if self.limit < 100:
381 | interval = (self.interval / 100) * self.limit
382 | displacement = (value - self.displacement) % interval
383 | if displacement < interval / 2:
384 | value -= displacement
385 | else:
386 | value += interval - displacement
387 | if value > self.limit:
388 | value = self.limit
389 | elif value < 0:
390 | value = 0.0
391 |
392 | self.value = value
393 | self.valueChanged.emit(self.value)
394 | self.mousePressed.emit(True)
395 |
396 | def startValueShift(self, event):
397 | self.start = self.value
398 | self.position = event.x()
399 |
400 | def emitValueShifted(self, event):
401 | position = event.x()
402 | vector = position - self.position
403 | value = self.start + (vector * self.shift)
404 |
405 | if value < 0:
406 | if self.limit == 360:
407 | value += self.limit
408 | else:
409 | value = 0
410 | elif value > self.limit:
411 | if self.limit == 360:
412 | value -= self.limit
413 | else:
414 | value = self.limit
415 |
416 | self.value = value
417 | self.valueChanged.emit(self.value)
418 | self.mousePressed.emit(True)
419 |
420 | def mousePressEvent(self, event):
421 | if (event.buttons() == Qt.MouseButton.LeftButton and
422 | event.modifiers() == Qt.KeyboardModifier.NoModifier):
423 | self.emitValueChanged(event)
424 | elif (event.buttons() == Qt.MouseButton.LeftButton and
425 | event.modifiers() == Qt.KeyboardModifier.ControlModifier):
426 | self.emitValueSnapped(event)
427 | self.startValueShift(event)
428 | self.update()
429 |
430 | def mouseMoveEvent(self, event):
431 | if (event.buttons() == Qt.MouseButton.LeftButton and
432 | event.modifiers() == Qt.KeyboardModifier.NoModifier):
433 | self.emitValueChanged(event)
434 | self.startValueShift(event)
435 | elif (event.buttons() == Qt.MouseButton.LeftButton and
436 | event.modifiers() == Qt.KeyboardModifier.ControlModifier):
437 | self.emitValueSnapped(event)
438 | self.startValueShift(event)
439 | elif (event.buttons() == Qt.MouseButton.LeftButton and
440 | event.modifiers() == Qt.KeyboardModifier.ShiftModifier):
441 | self.shift = 0.1
442 | self.emitValueShifted(event)
443 | elif (event.buttons() == Qt.MouseButton.LeftButton and
444 | event.modifiers() == Qt.KeyboardModifier.AltModifier):
445 | self.shift = 0.01
446 | self.emitValueShifted(event)
447 | self.update()
448 |
449 | def mouseReleaseEvent(self, event):
450 | self.mousePressed.emit(False)
451 |
452 | def paintEvent(self, event):
453 | painter = QPainter(self)
454 | width = self.width()
455 | height = self.height()
456 | # background
457 | painter.setPen(Qt.PenStyle.NoPen)
458 | painter.setBrush( QBrush(QColor(0, 0, 0, 50)))
459 | painter.drawRect(0, 1, width, height - 2)
460 | # gradient
461 | gradient = QLinearGradient(0, 0, width, 0)
462 | if self.colors:
463 | for index, color in enumerate(self.colors):
464 | gradient.setColorAt(index / (len(self.colors) - 1), color)
465 | painter.setBrush(QBrush(gradient))
466 | painter.drawRect(1, 2, width - 2, height - 4)
467 | # cursor
468 | if self.limit:
469 | position = round((self.value / self.limit) * (width - 2))
470 | painter.setBrush( QBrush(QColor(0, 0, 0, 100)))
471 | painter.drawRect(position - 2, 0, 6, height)
472 | painter.setBrush(QBrush(QColor(255, 255, 255, 200)))
473 | painter.drawRect(position, 1, 2, height - 2)
474 |
475 |
476 | class ColorChannel:
477 |
478 | channelList = None
479 |
480 | def __init__(self, name: str, parent):
481 | self.name = name
482 | self.update = parent.updateChannels
483 | self.refresh = parent.updateChannelGradients
484 | wrap = False
485 | interval = 10.0
486 | displacement = 0.0
487 | self.scale = True
488 | self.clip = 0.0
489 | self.colorful = False
490 | self.luma = False
491 | self.limit = 100.0
492 | if self.name[-3:] == "Hue":
493 | wrap = True
494 | interval = 30.0
495 | if self.name[:2] == "ok":
496 | interval = 40.0
497 | displacement = 25.0
498 | self.limit = 360.0
499 | elif self.name[-6:] == "Chroma":
500 | self.limit = 0.0
501 | self.layout = QHBoxLayout()
502 | self.layout.setSpacing(2)
503 |
504 | if self.name[:2] == "ok":
505 | tip = f"{self.name[:5].upper()} {self.name[5:]}"
506 | letter = self.name[5:6]
507 | else:
508 | tip = f"{self.name[:3].upper()} {self.name[3:]}"
509 | if self.name[-4:] == "Luma":
510 | letter = "Y"
511 | else:
512 | letter = self.name[3:4]
513 | self.label = QLabel(letter)
514 | self.label.setFixedHeight(CHANNEL_HEIGHT - 1)
515 | self.label.setFixedWidth(LABEL_WIDTH)
516 | self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
517 | self.label.setToolTip(tip)
518 |
519 | self.slider = ChannelSlider(self.limit)
520 | self.slider.setFixedHeight(CHANNEL_HEIGHT)
521 | self.slider.setMinimumWidth(100)
522 | self.slider.setInterval(interval)
523 | self.slider.setDisplacement(displacement)
524 | self.slider.mousePressed.connect(parent.setPressed)
525 |
526 | self.spinBox = QDoubleSpinBox()
527 | if self.name[-6:] == "Chroma":
528 | self.spinBox.setDecimals(3)
529 | self.spinBox.setMaximum(self.limit)
530 | self.spinBox.setWrapping(wrap)
531 | self.spinBox.setFixedHeight(CHANNEL_HEIGHT)
532 | self.spinBox.setFixedWidth(VALUES_WIDTH)
533 | self.spinBox.editingFinished.connect(parent.finishEditing)
534 |
535 | self.slider.valueChanged.connect(self.updateSpinBox)
536 | self.spinBox.valueChanged.connect(self.updateSlider)
537 | ColorChannel.updateList(name)
538 |
539 | def value(self):
540 | return self.spinBox.value()
541 |
542 | def setValue(self, value: float):
543 | if self.name[-6:] == "Chroma" and self.limit >= 10:
544 | value = round(value, 2)
545 | self.slider.setValue(value)
546 | self.spinBox.setValue(value)
547 |
548 | def setLimit(self, value: float):
549 | decimal = 2 if value >= 10 else 3
550 | self.limit = round(value, decimal)
551 | self.slider.setLimit(self.limit)
552 | self.spinBox.setDecimals(decimal)
553 | self.spinBox.setMaximum(self.limit)
554 | self.spinBox.setSingleStep(self.limit / 100)
555 |
556 | def clipChroma(self, clip: bool):
557 | # do not set chroma channel itself to clip as the clip value will not be updated when adjusting
558 | self.scale = not clip
559 | self.refresh()
560 |
561 | def colorfulHue(self, colorful: bool):
562 | self.colorful = colorful
563 | self.refresh()
564 |
565 | def updateSlider(self, value: float):
566 | self.update(value, self.name, "slider")
567 |
568 | def updateSpinBox(self, value: float):
569 | self.update(value, self.name, "spinBox")
570 |
571 | def updateGradientColors(self, firstConst: float, lastConst: float, trc: str, ChromaLimit: float=-1):
572 | colors = []
573 | if self.name[-3:] == "Hue":
574 | if self.name[:2] == "ok":
575 | # oklab hue needs more points for qcolor to blend more accurately
576 | # range of 0 to 25 - 345 in 15deg increments to 360
577 | points = 26
578 | increment = self.limit / (points - 2)
579 | displacement = increment - 25
580 |
581 | if self.colorful:
582 | for number in range(points):
583 | hue = (number - 1) * increment - displacement
584 | if hue < 0:
585 | hue = 0
586 | elif hue > self.limit:
587 | hue = self.limit
588 | rgb = Convert.okhsvToRgbF(hue, 100.0, 100.0, trc)
589 | colors.append(Convert.rgbFToInt8(*rgb, trc))
590 | elif self.name[:5] == "okhcl":
591 | for number in range(points):
592 | hue = (number - 1) * increment - displacement
593 | if hue < 0:
594 | hue = 0
595 | elif hue > self.limit:
596 | hue = self.limit
597 | rgb = Convert.okhclToRgbF(hue, firstConst, lastConst, ChromaLimit, trc)
598 | colors.append(Convert.rgbFToInt8(*rgb, trc))
599 | elif self.name[:5] == "okhsv":
600 | for number in range(points):
601 | hue = (number - 1) * increment - displacement
602 | if hue < 0:
603 | hue = 0
604 | elif hue > self.limit:
605 | hue = self.limit
606 | rgb = Convert.okhsvToRgbF(hue, firstConst, lastConst, trc)
607 | colors.append(Convert.rgbFToInt8(*rgb, trc))
608 | elif self.name[:5] == "okhsl":
609 | for number in range(points):
610 | hue = (number - 1) * increment - displacement
611 | if hue < 0:
612 | hue = 0
613 | elif hue > self.limit:
614 | hue = self.limit
615 | rgb = Convert.okhslToRgbF(hue, firstConst, lastConst, trc)
616 | colors.append(Convert.rgbFToInt8(*rgb, trc))
617 | else:
618 | # range of 0 to 360deg incrementing by 30deg
619 | points = 13
620 | increment = self.limit / (points - 1)
621 |
622 | if self.colorful:
623 | if self.name[:3] != "hcy":
624 | for number in range(points):
625 | rgb = Convert.hsvToRgbF(number * increment, 100.0, 100.0, trc)
626 | colors.append(Convert.rgbFToInt8(*rgb, trc))
627 | else:
628 | for number in range(points):
629 | rgb = Convert.hcyToRgbF(number * increment, 100.0, -1, -1, trc, self.luma)
630 | colors.append(Convert.rgbFToInt8(*rgb, trc))
631 | elif self.name[:3] == "hsv":
632 | for number in range(points):
633 | rgb = Convert.hsvToRgbF(number * increment, firstConst, lastConst, trc)
634 | colors.append(Convert.rgbFToInt8(*rgb, trc))
635 | elif self.name[:3] == "hsl":
636 | for number in range(points):
637 | rgb = Convert.hslToRgbF(number * increment, firstConst, lastConst, trc)
638 | colors.append(Convert.rgbFToInt8(*rgb, trc))
639 | elif self.name[:3] == "hcy":
640 | for number in range(points):
641 | rgb = Convert.hcyToRgbF(number * increment, firstConst, lastConst,
642 | ChromaLimit, trc, self.luma)
643 | colors.append(Convert.rgbFToInt8(*rgb, trc))
644 | else:
645 | # range of 0 to 100% incrementing by 10%
646 | points = 11
647 | increment = self.limit / (points - 1)
648 |
649 | if self.name[:3] == "hsv":
650 | if self.name[3:] == "Saturation":
651 | for number in range(points):
652 | rgb = Convert.hsvToRgbF(firstConst, number * increment, lastConst, trc)
653 | colors.append(Convert.rgbFToInt8(*rgb, trc))
654 | elif self.name[3:] == "Value":
655 | for number in range(points):
656 | rgb = Convert.hsvToRgbF(firstConst, lastConst, number * increment, trc)
657 | colors.append(Convert.rgbFToInt8(*rgb, trc))
658 | elif self.name[:3] == "hsl":
659 | if self.name[3:] == "Saturation":
660 | for number in range(points):
661 | rgb = Convert.hslToRgbF(firstConst, number * increment, lastConst, trc)
662 | colors.append(Convert.rgbFToInt8(*rgb, trc))
663 | elif self.name[3:] == "Lightness":
664 | for number in range(points):
665 | rgb = Convert.hslToRgbF(firstConst, lastConst, number * increment, trc)
666 | colors.append(Convert.rgbFToInt8(*rgb, trc))
667 | elif self.name[:3] == "hcy":
668 | if self.name[3:] == "Chroma":
669 | for number in range(points):
670 | rgb = Convert.hcyToRgbF(firstConst, number * increment, lastConst,
671 | ChromaLimit, trc, self.luma)
672 | colors.append(Convert.rgbFToInt8(*rgb, trc))
673 | elif self.name[3:] == "Luma":
674 | for number in range(points):
675 | rgb = Convert.hcyToRgbF(firstConst, lastConst, number * increment,
676 | ChromaLimit, trc, self.luma)
677 | colors.append(Convert.rgbFToInt8(*rgb, trc))
678 | elif self.name[:5] == "okhcl":
679 | if self.name[5:] == "Chroma":
680 | for number in range(points):
681 | rgb = Convert.okhclToRgbF(firstConst, number * increment, lastConst,
682 | ChromaLimit, trc)
683 | colors.append(Convert.rgbFToInt8(*rgb, trc))
684 | elif self.name[5:] == "Lightness":
685 | for number in range(points):
686 | rgb = Convert.okhclToRgbF(firstConst, lastConst, number * increment,
687 | ChromaLimit, trc)
688 | colors.append(Convert.rgbFToInt8(*rgb, trc))
689 | elif self.name[:5] == "okhsv":
690 | if self.name[5:] == "Saturation":
691 | for number in range(points):
692 | rgb = Convert.okhsvToRgbF(firstConst, number * increment, lastConst, trc)
693 | colors.append(Convert.rgbFToInt8(*rgb, trc))
694 | elif self.name[5:] == "Value":
695 | for number in range(points):
696 | rgb = Convert.okhsvToRgbF(firstConst, lastConst, number * increment, trc)
697 | colors.append(Convert.rgbFToInt8(*rgb, trc))
698 | elif self.name[:5] == "okhsl":
699 | if self.name[5:] == "Saturation":
700 | for number in range(points):
701 | rgb = Convert.okhslToRgbF(firstConst, number * increment, lastConst, trc)
702 | colors.append(Convert.rgbFToInt8(*rgb, trc))
703 | elif self.name[5:] == "Lightness":
704 | for number in range(points):
705 | rgb = Convert.okhslToRgbF(firstConst, lastConst, number * increment, trc)
706 | colors.append(Convert.rgbFToInt8(*rgb, trc))
707 |
708 | self.slider.setGradientColors(colors)
709 |
710 | def blockSignals(self, block: bool):
711 | self.slider.blockSignals(block)
712 | self.spinBox.blockSignals(block)
713 |
714 | @classmethod
715 | def updateList(cls, name: str):
716 | if cls.channelList is None:
717 | cls.channelList = []
718 | cls.channelList.append(name)
719 |
720 | @classmethod
721 | def getList(cls):
722 | return cls.channelList.copy()
723 |
724 |
725 | class SliderConfig(QDialog):
726 |
727 | def __init__(self, parent):
728 | super().__init__(parent)
729 |
730 | self.hcl = parent
731 | self.setWindowTitle("Configure HCL Sliders")
732 | self.setFixedSize(*CONFIG_SIZE)
733 | self.mainLayout = QHBoxLayout(self)
734 | self.loadPages()
735 |
736 | def loadPages(self):
737 | self.pageList = QListWidget()
738 | self.pageList.setFixedWidth(SIDEBAR_WIDTH)
739 | self.pageList.setDragEnabled(True)
740 | self.pageList.viewport().setAcceptDrops(True)
741 | self.pageList.setDropIndicatorShown(True)
742 | self.pageList.setDragDropMode(QListWidget.DragDropMode.InternalMove)
743 | self.pages = QStackedWidget()
744 |
745 | hidden = ColorChannel.getList()
746 | self.models = {}
747 | for name in self.hcl.displayOrder:
748 | if name[:2] == "ok":
749 | self.models.setdefault(name[:5].upper(), []).append(name)
750 | else:
751 | self.models.setdefault(name[:3].upper(), []).append(name)
752 | hidden.remove(name)
753 | visible = list(self.models.keys())
754 | for name in hidden:
755 | if name[:2] == "ok":
756 | self.models.setdefault(name[:5].upper(), []).append(name)
757 | else:
758 | self.models.setdefault(name[:3].upper(), []).append(name)
759 |
760 | self.checkBoxes = {}
761 | for model, channels in self.models.items():
762 | tabs = QTabWidget()
763 | tabs.setMovable(True)
764 |
765 | for name in channels:
766 | tab = QWidget()
767 | tabLayout = QVBoxLayout()
768 | tabLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
769 | tab.setLayout(tabLayout)
770 | channel: ColorChannel = getattr(self.hcl, name)
771 |
772 | snapGroup = QGroupBox("Cursor Snapping")
773 | snapGroup.setFixedHeight(GROUPBOX_HEIGHT)
774 | snapGroup.setToolTip("Ctrl + Click to snap cursor at intervals")
775 | snapLayout = QHBoxLayout()
776 | interval = QDoubleSpinBox()
777 | interval.setFixedWidth(SPINBOX_WIDTH)
778 | interval.setDecimals(1)
779 | interval.setMinimum(0.1)
780 | snapLayout.addWidget(interval)
781 | intervalLabel = QLabel("Interval")
782 | intervalLabel.setToolTip("Sets the snap interval to amount")
783 | snapLayout.addWidget(intervalLabel)
784 | displacement = QDoubleSpinBox()
785 | displacement.setFixedWidth(SPINBOX_WIDTH)
786 | displacement.setDecimals(1)
787 | snapLayout.addWidget(displacement)
788 | DisplacementLabel = QLabel("Displacement")
789 | DisplacementLabel.setToolTip("Displaces the snap positions by amount")
790 | snapLayout.addWidget(DisplacementLabel)
791 | snapGroup.setLayout(snapLayout)
792 | tabLayout.addWidget(snapGroup)
793 |
794 | param = name[len(model):]
795 | if (model == 'HCY' or model == 'OKHCL') and param != 'Chroma':
796 | radioGroup = QGroupBox("Chroma Mode")
797 | radioGroup.setFixedHeight(GROUPBOX_HEIGHT)
798 | radioGroup.setToolTip("Switches how chroma is adjusted \
799 | to stay within the sRGB gamut")
800 | radioLayout = QHBoxLayout()
801 | clip = QRadioButton("Clip")
802 | clip.setToolTip("Clips chroma if it exceeds the srgb gamut when adjusting")
803 | radioLayout.addWidget(clip)
804 | scale = QRadioButton("Scale")
805 | scale.setToolTip("Scales chroma to maintain constant saturation when adjusting")
806 | radioLayout.addWidget(scale)
807 | if channel.scale:
808 | scale.setChecked(True)
809 | else:
810 | clip.setChecked(True)
811 | clip.toggled.connect(channel.clipChroma)
812 | radioGroup.setLayout(radioLayout)
813 | tabLayout.addWidget(radioGroup)
814 |
815 | if model == 'HCY' and param == 'Luma':
816 | luma = QCheckBox("Always Luma")
817 | luma.setToolTip("Transfer components to sRGB in linear TRCs")
818 | luma.setChecked(channel.luma)
819 | luma.toggled.connect(self.hcl.setLuma)
820 | tabLayout.addWidget(luma)
821 |
822 | if param == 'Hue':
823 | interval.setMaximum(360.0)
824 | interval.setSuffix(u'\N{DEGREE SIGN}')
825 | displacement.setMaximum(359.9)
826 | displacement.setSuffix(u'\N{DEGREE SIGN}')
827 | colorful = QCheckBox("Colorful Gradient")
828 | colorful.setToolTip("Gradient colors will always be at max chroma")
829 | colorful.setChecked(channel.colorful)
830 | colorful.toggled.connect(channel.colorfulHue)
831 | tabLayout.addStretch()
832 | tabLayout.addWidget(colorful)
833 | else:
834 | interval.setMaximum(100.0)
835 | interval.setSuffix('%')
836 | displacement.setSuffix('%')
837 |
838 | interval.setValue(channel.slider.interval)
839 | interval.valueChanged.connect(channel.slider.setInterval)
840 | displacement.setValue(channel.slider.displacement)
841 | displacement.valueChanged.connect(channel.slider.setDisplacement)
842 |
843 | tabs.addTab(tab, param)
844 | checkBox = QCheckBox()
845 | checkBox.setChecked(not((model in visible) and (name in hidden)))
846 | tab.setEnabled(checkBox.isChecked())
847 | self.checkBoxes[name] = checkBox
848 | tabs.tabBar().setTabButton(tabs.tabBar().count() - 1,
849 | tabs.tabBar().ButtonPosition.LeftSide, checkBox)
850 | checkBox.toggled.connect(tab.setEnabled)
851 | checkBox.stateChanged.connect(self.reorderSliders)
852 |
853 | tabs.tabBar().tabMoved.connect(self.reorderSliders)
854 | self.pages.addWidget(tabs)
855 | self.pageList.addItem(model)
856 | item = self.pageList.item(self.pageList.count() - 1)
857 | item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
858 | item.setCheckState(Qt.CheckState.Checked) if model in visible else item.setCheckState(
859 | Qt.CheckState.Unchecked)
860 | tabs.setEnabled(item.checkState() == Qt.CheckState.Checked)
861 | self.pageList.model().rowsMoved.connect(self.reorderSliders)
862 | self.pageList.itemPressed.connect(self.changePage)
863 | self.pageList.currentTextChanged.connect(self.changePage)
864 | self.pageList.itemChanged.connect(self.toggleModel)
865 |
866 | self.others = QPushButton("Others")
867 | self.others.setAutoDefault(False)
868 | self.others.setCheckable(True)
869 | self.others.setFixedWidth(SIDEBAR_WIDTH)
870 | self.others.clicked.connect(self.changeOthers)
871 |
872 | history = QGroupBox("Color History")
873 | history.setFixedHeight(GROUPBOX_HEIGHT)
874 | history.setToolTip("Records foreground color when changed")
875 | history.setCheckable(True)
876 | history.setChecked(self.hcl.history.isEnabled())
877 | history.toggled.connect(self.refreshOthers)
878 | memory = QSpinBox()
879 | memory.setFixedWidth(SPINBOX_WIDTH)
880 | memory.setMaximum(999)
881 | memory.setValue(self.hcl.memory)
882 | memory.valueChanged.connect(self.hcl.setMemory)
883 | memoryLabel = QLabel("Memory")
884 | memoryLabel.setToolTip("Limits color history, set to 0 for unlimited")
885 | clearButton = QPushButton("Clear History")
886 | clearButton.setAutoDefault(False)
887 | clearButton.setToolTip("Removes all colors in history")
888 | clearButton.clicked.connect(self.hcl.clearHistory)
889 | historyLayout = QHBoxLayout()
890 | historyLayout.addWidget(memory)
891 | historyLayout.addWidget(memoryLabel)
892 | historyLayout.addWidget(clearButton)
893 | history.setLayout(historyLayout)
894 | syntax = QCheckBox("Color Syntax")
895 | syntax.setToolTip("Panel for hex/oklab/oklch css syntax")
896 | syntax.setChecked(self.hcl.syntax.isEnabled())
897 | syntax.stateChanged.connect(self.refreshOthers)
898 |
899 | othersTab = QWidget()
900 | pageLayout = QVBoxLayout()
901 | pageLayout.addSpacing(OTHERS_HEIGHT)
902 | pageLayout.addWidget(history)
903 | pageLayout.addStretch()
904 | pageLayout.addWidget(syntax)
905 | pageLayout.addStretch()
906 | othersTab.setLayout(pageLayout)
907 | othersPage = QTabWidget()
908 | othersPage.addTab(othersTab, "Other Settings")
909 | self.pages.addWidget(othersPage)
910 |
911 | listLayout = QVBoxLayout()
912 | listLayout.addWidget(self.pageList)
913 | listLayout.addWidget(self.others)
914 | self.mainLayout.addLayout(listLayout)
915 | self.mainLayout.addWidget(self.pages)
916 |
917 | def changePage(self, item: str|QListWidgetItem):
918 | if isinstance(item, QListWidgetItem):
919 | item = item.text()
920 | self.pages.setCurrentIndex(list(self.models.keys()).index(item))
921 | self.others.setChecked(False)
922 |
923 | def changeOthers(self):
924 | self.others.setChecked(True)
925 | self.pages.setCurrentIndex(self.pages.count() - 1)
926 | self.pageList.clearSelection()
927 |
928 | def refreshOthers(self, state: bool|int):
929 | # toggled vs stateChanged
930 | if isinstance(state, bool):
931 | self.hcl.history.setEnabled(state)
932 | else:
933 | state = state == Qt.CheckState.Checked
934 | self.hcl.syntax.setEnabled(state)
935 | # Refresh hcl layout
936 | self.hcl.clearOthers()
937 | self.hcl.displayOthers()
938 |
939 | def reorderSliders(self):
940 | # Get new display order
941 | self.hcl.displayOrder = []
942 | for row in range(self.pageList.count()):
943 | item = self.pageList.item(row)
944 | if item.checkState() == Qt.CheckState.Checked:
945 | model = item.text()
946 | tabs = self.pages.widget(list(self.models.keys()).index(model))
947 | for index in range(tabs.count()):
948 | # visible tabs have '&' in text used for shortcut
949 | param = tabs.tabText(index).replace('&', '')
950 | name = f"{model.lower()}{param}"
951 | if self.checkBoxes[name].isChecked():
952 | self.hcl.displayOrder.append(name)
953 | # Refresh channel layout
954 | self.hcl.clearChannels()
955 | self.hcl.displayChannels()
956 |
957 | def toggleModel(self, item: QListWidgetItem):
958 | tabs = self.pages.widget(list(self.models.keys()).index(item.text()))
959 | tabs.setEnabled(item.checkState() == Qt.CheckState.Checked)
960 |
961 | self.reorderSliders()
962 |
963 | def closeEvent(self, event):
964 | self.hcl.writeSettings()
965 | event.accept()
966 |
967 |
968 | class HCLSliders(DockWidget):
969 |
970 | def __init__(self):
971 | super().__init__()
972 | self.setWindowTitle(DOCKER_NAME)
973 | mainWidget = QWidget(self)
974 | mainWidget.setContentsMargins(2, 1, 2, 1)
975 | self.setWidget(mainWidget)
976 | self.mainLayout = QVBoxLayout(mainWidget)
977 | self.mainLayout.setSpacing(2)
978 | self.config = None
979 | self.document = None
980 | self.memory = 30
981 | self.trc = "sRGB"
982 | self.notation = NOTATION[0]
983 | self.text = ""
984 | self.pressed = False
985 | self.editing = False
986 | self.pastColors = []
987 | self.loadChannels()
988 | self.history = ColorHistory(self)
989 | self.loadSyntax()
990 | self.readSettings()
991 | self.displayChannels()
992 | self.displayOthers()
993 | self.updateNotations()
994 |
995 | def colorDisplay(self):
996 | # load into channel layout to prevent alignment issue when channels empty
997 | layout = QHBoxLayout()
998 | layout.setSpacing(2)
999 |
1000 | self.color = ColorDisplay(self)
1001 | self.color.setFixedHeight(DISPLAY_HEIGHT)
1002 | layout.addWidget(self.color)
1003 |
1004 | button = QPushButton()
1005 | button.setIcon(Application.icon('configure'))
1006 | button.setFlat(True)
1007 | button.setFixedSize(DISPLAY_HEIGHT, DISPLAY_HEIGHT)
1008 | button.setIconSize(QSize(DISPLAY_HEIGHT - 2, DISPLAY_HEIGHT - 2))
1009 | button.setToolTip("Configure HCL Sliders")
1010 | button.clicked.connect(self.openConfig)
1011 | layout.addWidget(button)
1012 |
1013 | self.timer = QTimer()
1014 | self.timer.timeout.connect(self.getKritaColors)
1015 | self.singleShot = QTimer()
1016 | self.singleShot.setSingleShot(True)
1017 | self.singleShot.timeout.connect(self.setHistory)
1018 | return layout
1019 |
1020 | def loadChannels(self):
1021 | self.channelLayout = QVBoxLayout()
1022 | self.channelLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
1023 | self.channelLayout.setSpacing(2)
1024 | self.channelLayout.addLayout(self.colorDisplay())
1025 | self.channelLayout.addSpacing(1)
1026 |
1027 | self.hsvHue = ColorChannel("hsvHue", self)
1028 | self.hsvSaturation = ColorChannel("hsvSaturation", self)
1029 | self.hsvValue = ColorChannel("hsvValue", self)
1030 |
1031 | self.hslHue = ColorChannel("hslHue", self)
1032 | self.hslSaturation = ColorChannel("hslSaturation", self)
1033 | self.hslLightness = ColorChannel("hslLightness", self)
1034 |
1035 | self.hcyHue = ColorChannel("hcyHue", self)
1036 | self.hcyHue.scale = False
1037 | self.hcyChroma = ColorChannel("hcyChroma", self)
1038 | self.hcyLuma = ColorChannel("hcyLuma", self)
1039 | self.hcyLuma.scale = False
1040 |
1041 | self.okhclHue = ColorChannel("okhclHue", self)
1042 | self.okhclHue.scale = False
1043 | self.okhclChroma = ColorChannel("okhclChroma", self)
1044 | self.okhclLightness = ColorChannel("okhclLightness", self)
1045 | self.okhclLightness.scale = False
1046 |
1047 | self.okhsvHue = ColorChannel("okhsvHue", self)
1048 | self.okhsvSaturation = ColorChannel("okhsvSaturation", self)
1049 | self.okhsvValue = ColorChannel("okhsvValue", self)
1050 |
1051 | self.okhslHue = ColorChannel("okhslHue", self)
1052 | self.okhslSaturation = ColorChannel("okhslSaturation", self)
1053 | self.okhslLightness = ColorChannel("okhslLightness", self)
1054 |
1055 | self.mainLayout.addLayout(self.channelLayout)
1056 |
1057 | def loadSyntax(self):
1058 | self.prevNotation = QPushButton()
1059 | self.prevNotation.setFlat(True)
1060 | self.prevNotation.setFixedSize(CHANNEL_HEIGHT - 1, CHANNEL_HEIGHT - 1)
1061 | self.prevNotation.setIcon(Application.icon('arrow-left'))
1062 | self.prevNotation.setIconSize(QSize(CHANNEL_HEIGHT - 5, CHANNEL_HEIGHT - 5))
1063 | self.prevNotation.clicked.connect(self.switchNotation)
1064 |
1065 | self.nextNotation = QPushButton()
1066 | self.nextNotation.setFlat(True)
1067 | self.nextNotation.setFixedSize(CHANNEL_HEIGHT - 1, CHANNEL_HEIGHT - 1)
1068 | self.nextNotation.setIcon(Application.icon('arrow-right'))
1069 | self.nextNotation.setIconSize(QSize(CHANNEL_HEIGHT - 5, CHANNEL_HEIGHT - 5))
1070 | self.nextNotation.clicked.connect(self.switchNotation)
1071 |
1072 | self.syntax = QLineEdit()
1073 | self.syntax.setFixedHeight(CHANNEL_HEIGHT - 1)
1074 | self.syntax.setAlignment(Qt.AlignmentFlag.AlignCenter)
1075 | self.syntax.editingFinished.connect(self.parseSyntax)
1076 |
1077 | def readSettings(self):
1078 | channels = ColorChannel.getList()
1079 | for name in channels:
1080 | settings: list = Application.readSetting(DOCKER_NAME, name, "").split(",")
1081 | if len(settings) > 1:
1082 | channel: ColorChannel = getattr(self, name)
1083 | try:
1084 | channel.slider.setInterval(float(settings[0]))
1085 | except ValueError:
1086 | print(f"Invalid interval amount for {name}")
1087 |
1088 | try:
1089 | channel.slider.setDisplacement(float(settings[1]))
1090 | except ValueError:
1091 | print(f"Invalid displacement amount for {name}")
1092 |
1093 | if (name[:3] == "hcy" or name[:5] == "okhcl") and name[-6:] != "Chroma":
1094 | channel.scale = settings[2] == "True"
1095 |
1096 | if name[-3:] == "Hue":
1097 | if len(settings) > 3:
1098 | channel.colorful = settings[3] == "True"
1099 | else:
1100 | channel.colorful = settings[2] == "True"
1101 |
1102 | if name[:3] == "hcy":
1103 | channel.luma = settings[-1] == "True"
1104 |
1105 | self.displayOrder = []
1106 | empty = False
1107 | displayed = Application.readSetting(DOCKER_NAME, "displayed", "").split(",")
1108 | for name in displayed:
1109 | if name in channels:
1110 | self.displayOrder.append(name)
1111 | elif name == "None":
1112 | empty = True
1113 | break
1114 | if not self.displayOrder and not empty:
1115 | self.displayOrder = channels
1116 |
1117 | history = Application.readSetting(DOCKER_NAME, "history", "").split(",")
1118 | if len(history) == 2:
1119 | self.history.setEnabled(history[0] != "False")
1120 | try:
1121 | memory = int(history[1])
1122 | if 0 <= memory <= 999:
1123 | self.memory = memory
1124 | except ValueError:
1125 | ("Invalid memory value")
1126 |
1127 | syntax = Application.readSetting(DOCKER_NAME, "syntax", "").split(",")
1128 | if len(syntax) == 2:
1129 | self.syntax.setEnabled(syntax[0] != "False")
1130 | notation = syntax[1]
1131 | if notation in NOTATION:
1132 | self.notation = notation
1133 |
1134 | def writeSettings(self):
1135 | Application.writeSetting(DOCKER_NAME, "displayed", ",".join(self.displayOrder) if self.displayOrder else "None")
1136 |
1137 | for name in ColorChannel.getList():
1138 | settings = []
1139 | channel: ColorChannel = getattr(self, name)
1140 | settings.append(str(channel.slider.interval))
1141 | settings.append(str(channel.slider.displacement))
1142 |
1143 | if (name[:3] == "hcy" or name[:5] == "okhcl") and name[-6:] != "Chroma":
1144 | settings.append(str(channel.scale))
1145 | if name[-3:] == "Hue":
1146 | settings.append(str(channel.colorful))
1147 | if name[:3] == "hcy":
1148 | settings.append(str(channel.luma))
1149 |
1150 | Application.writeSetting(DOCKER_NAME, name, ",".join(settings))
1151 |
1152 | history = [str(self.history.isEnabled()), str(self.memory)]
1153 | Application.writeSetting(DOCKER_NAME, "history", ",".join(history))
1154 |
1155 | syntax = [str(self.syntax.isEnabled()), self.notation]
1156 | Application.writeSetting(DOCKER_NAME, "syntax", ",".join(syntax))
1157 |
1158 | def displayChannels(self):
1159 | prev = ""
1160 | for name in self.displayOrder:
1161 | if MODEL_SPACING:
1162 | model = name[:5] if name[:2] == "ok" else name[:3]
1163 | if prev and prev != model:
1164 | self.channelLayout.addSpacing(MODEL_SPACING)
1165 | prev = model
1166 | channel = getattr(self, name)
1167 | channel.layout.addWidget(channel.label)
1168 | channel.layout.addWidget(channel.slider)
1169 | channel.layout.addWidget(channel.spinBox)
1170 | self.channelLayout.addLayout(channel.layout)
1171 |
1172 | def clearChannels(self):
1173 | # first 2 items in channelLayout is color display and spacing
1174 | for i in reversed(range(self.channelLayout.count() - 2)):
1175 | item = self.channelLayout.itemAt(i + 2)
1176 | layout = item.layout()
1177 | if layout:
1178 | for index in reversed(range(layout.count())):
1179 | widget = layout.itemAt(index).widget()
1180 | layout.removeWidget(widget)
1181 | widget.setParent(None)
1182 | self.channelLayout.removeItem(item)
1183 |
1184 | def displayOthers(self):
1185 | if self.history.isEnabled():
1186 | self.mainLayout.addSpacing(1)
1187 | self.mainLayout.addWidget(self.history)
1188 | if self.syntax.isEnabled():
1189 | self.mainLayout.addSpacing(1)
1190 | syntaxLayout = QHBoxLayout()
1191 | syntaxLayout.addWidget(self.prevNotation)
1192 | syntaxLayout.addWidget(self.syntax)
1193 | syntaxLayout.addWidget(self.nextNotation)
1194 | self.mainLayout.addLayout(syntaxLayout)
1195 |
1196 | def clearOthers(self):
1197 | # first item in mainLayout is channelLayout
1198 | for i in reversed(range(self.mainLayout.count() - 1)):
1199 | item = self.mainLayout.itemAt(i + 1)
1200 | widget = item.widget()
1201 | if widget:
1202 | self.mainLayout.removeWidget(widget)
1203 | widget.setParent(None)
1204 | else:
1205 | layout = item.layout()
1206 | if layout:
1207 | for index in reversed(range(layout.count())):
1208 | widget = layout.itemAt(index).widget()
1209 | layout.removeWidget(widget)
1210 | widget.setParent(None)
1211 | self.mainLayout.removeItem(item)
1212 |
1213 | def openConfig(self):
1214 | if self.config is None:
1215 | self.config = SliderConfig(self)
1216 | self.config.show()
1217 |
1218 | def profileTRC(self, profile: str):
1219 | if profile in SRGB:
1220 | return "sRGB"
1221 | elif profile in LINEAR:
1222 | return "linear"
1223 | print("Incompatible profile")
1224 | return self.trc
1225 |
1226 | def setMemory(self, memory: int):
1227 | self.memory = memory
1228 |
1229 | def setPressed(self, pressed: bool):
1230 | self.pressed = pressed
1231 |
1232 | def finishEditing(self):
1233 | self.editing = False
1234 |
1235 | def getKritaColors(self):
1236 | view = Application.activeWindow().activeView()
1237 | if not view.visible():
1238 | return
1239 |
1240 | if not self.pressed and not self.editing:
1241 | # add to color history after slider sets color
1242 | if self.color.isChanged() and self.color.current:
1243 | self.setHistory()
1244 |
1245 | foreground = view.foregroundColor()
1246 | self.color.setForeGroundColor(foreground)
1247 | background = view.backgroundColor()
1248 | self.color.setBackGroundColor(background)
1249 |
1250 | if self.color.isChanged():
1251 | if self.color.bgMode:
1252 | self.color.setCurrentColor(background)
1253 | else:
1254 | self.color.setCurrentColor(foreground)
1255 |
1256 | current = self.color.current
1257 | rgb = tuple(current.componentsOrdered()[:3])
1258 | if current.colorModel() != "RGBA":
1259 | if current.colorModel() == "A" or current.colorModel() == "GRAYA":
1260 | rgb = (rgb[0], rgb[0], rgb[0])
1261 | else:
1262 | return
1263 |
1264 | trc = self.profileTRC(current.colorProfile())
1265 | self.updateSyntax(rgb, trc)
1266 | if trc != self.trc:
1267 | rgb = Convert.rgbToTRC(rgb, self.trc)
1268 | self.updateChannels(rgb)
1269 | # add to color history after krita changes color
1270 | if not self.singleShot.isActive():
1271 | self.color.recent = current
1272 | self.singleShot.start(DELAY)
1273 |
1274 | def blockChannels(self, block: bool):
1275 | # hsv
1276 | self.hsvHue.blockSignals(block)
1277 | self.hsvSaturation.blockSignals(block)
1278 | self.hsvValue.blockSignals(block)
1279 | # hsl
1280 | self.hslHue.blockSignals(block)
1281 | self.hslSaturation.blockSignals(block)
1282 | self.hslLightness.blockSignals(block)
1283 | # hcy
1284 | self.hcyHue.blockSignals(block)
1285 | self.hcyChroma.blockSignals(block)
1286 | self.hcyLuma.blockSignals(block)
1287 | # okhcl
1288 | self.okhclHue.blockSignals(block)
1289 | self.okhclChroma.blockSignals(block)
1290 | self.okhclLightness.blockSignals(block)
1291 | # okhsv
1292 | self.okhsvHue.blockSignals(block)
1293 | self.okhsvSaturation.blockSignals(block)
1294 | self.okhsvValue.blockSignals(block)
1295 | # okhsl
1296 | self.okhslHue.blockSignals(block)
1297 | self.okhslSaturation.blockSignals(block)
1298 | self.okhslLightness.blockSignals(block)
1299 |
1300 | def updateChannels(self, values: tuple|float, name: str=None, widget: str=None):
1301 | self.timer.stop()
1302 | self.blockChannels(True)
1303 |
1304 | if type(values) is tuple:
1305 | # update color from krita that is not adjusted by this plugin
1306 | self.setChannelValues("hsv", values)
1307 | self.setChannelValues("hsl", values)
1308 | self.setChannelValues("hcy", values)
1309 | self.setChannelValues("okhcl", values)
1310 | self.setChannelValues("okhsv", values)
1311 | self.setChannelValues("okhsl", values)
1312 | else:
1313 | # update slider if spinbox adjusted vice versa
1314 | channel: ColorChannel = getattr(self, name)
1315 | channelWidget = getattr(channel, widget)
1316 | channelWidget.setValue(values)
1317 | if widget == "slider":
1318 | # prevent getKritaColors when still editing spinBox
1319 | self.editing = True
1320 | # adjusting hsv sliders
1321 | if name[:3] == "hsv":
1322 | hue = self.hsvHue.value()
1323 | rgb = Convert.hsvToRgbF(hue, self.hsvSaturation.value(),
1324 | self.hsvValue.value(), self.trc)
1325 | self.setKritaColor(rgb)
1326 | self.setChannelValues("hsl", rgb, hue)
1327 | if self.hcyLuma.luma or self.trc == "sRGB":
1328 | self.setChannelValues("hcy", rgb, hue)
1329 | else:
1330 | self.setChannelValues("hcy", rgb)
1331 | self.setChannelValues("okhcl", rgb)
1332 | self.setChannelValues("okhsv", rgb)
1333 | self.setChannelValues("okhsl", rgb)
1334 | # adjusting hsl sliders
1335 | elif name[:3] == "hsl":
1336 | hue = self.hslHue.value()
1337 | rgb = Convert.hslToRgbF(hue, self.hslSaturation.value(),
1338 | self.hslLightness.value(), self.trc)
1339 | self.setKritaColor(rgb)
1340 | self.setChannelValues("hsv", rgb, hue)
1341 | if self.hcyLuma.luma or self.trc == "sRGB":
1342 | self.setChannelValues("hcy", rgb, hue)
1343 | else:
1344 | self.setChannelValues("hcy", rgb)
1345 | self.setChannelValues("okhcl", rgb)
1346 | self.setChannelValues("okhsv", rgb)
1347 | self.setChannelValues("okhsl", rgb)
1348 | # adjusting hcy sliders
1349 | elif name[:3] == "hcy":
1350 | hue = self.hcyHue.value()
1351 | chroma = self.hcyChroma.value()
1352 | limit = -1
1353 | if channel.scale:
1354 | if self.hcyChroma.limit > 0:
1355 | self.hcyChroma.clip = chroma
1356 | limit = self.hcyChroma.limit
1357 | else:
1358 | if self.hcyChroma.clip == 0:
1359 | self.hcyChroma.clip = chroma
1360 | else:
1361 | chroma = self.hcyChroma.clip
1362 | rgb = Convert.hcyToRgbF(hue, chroma, self.hcyLuma.value(),
1363 | limit, self.trc, channel.luma)
1364 | self.setKritaColor(rgb)
1365 | if name[-6:] != "Chroma":
1366 | hcy = Convert.rgbFToHcy(*rgb, hue, self.trc, channel.luma)
1367 | self.hcyChroma.setLimit(hcy[3])
1368 | self.hcyChroma.setValue(hcy[1])
1369 | # relative luminance doesnt match luma in hue
1370 | if channel.luma or self.trc == "sRGB":
1371 | self.setChannelValues("hsv", rgb, hue)
1372 | self.setChannelValues("hsl", rgb, hue)
1373 | else:
1374 | self.setChannelValues("hsv", rgb)
1375 | self.setChannelValues("hsl", rgb)
1376 | self.setChannelValues("okhcl", rgb)
1377 | self.setChannelValues("okhsv", rgb)
1378 | self.setChannelValues("okhsl", rgb)
1379 | # adjusting okhcl sliders
1380 | elif name[:5] == "okhcl":
1381 | hue = self.okhclHue.value()
1382 | chroma = self.okhclChroma.value()
1383 | limit = -1
1384 | if channel.scale:
1385 | if self.okhclChroma.limit > 0:
1386 | self.okhclChroma.clip = chroma
1387 | limit = self.okhclChroma.limit
1388 | else:
1389 | if self.okhclChroma.clip == 0:
1390 | self.okhclChroma.clip = chroma
1391 | else:
1392 | chroma = self.okhclChroma.clip
1393 | rgb = Convert.okhclToRgbF(hue, chroma, self.okhclLightness.value(), limit, self.trc)
1394 | self.setKritaColor(rgb)
1395 | if name[-6:] != "Chroma":
1396 | okhcl = Convert.rgbFToOkhcl(*rgb, hue, self.trc)
1397 | self.okhclChroma.setLimit(okhcl[3])
1398 | self.okhclChroma.setValue(okhcl[1])
1399 | self.setChannelValues("hsv", rgb)
1400 | self.setChannelValues("hsl", rgb)
1401 | self.setChannelValues("hcy", rgb)
1402 | self.setChannelValues("okhsv", rgb, hue)
1403 | self.setChannelValues("okhsl", rgb, hue)
1404 | # adjusting okhsv sliders
1405 | elif name[:5] == "okhsv":
1406 | hue = self.okhsvHue.value()
1407 | rgb = Convert.okhsvToRgbF(hue, self.okhsvSaturation.value(),
1408 | self.okhsvValue.value(), self.trc)
1409 | self.setKritaColor(rgb)
1410 | self.setChannelValues("hsv", rgb)
1411 | self.setChannelValues("hsl", rgb)
1412 | self.setChannelValues("hcy", rgb)
1413 | self.setChannelValues("okhcl", rgb, hue)
1414 | self.setChannelValues("okhsl", rgb, hue)
1415 | # adjusting okhsl sliders
1416 | elif name[:5] == "okhsl":
1417 | hue = self.okhslHue.value()
1418 | rgb = Convert.okhslToRgbF(hue, self.okhslSaturation.value(),
1419 | self.okhslLightness.value(), self.trc)
1420 | self.setKritaColor(rgb)
1421 | self.setChannelValues("hsv", rgb)
1422 | self.setChannelValues("hsl", rgb)
1423 | self.setChannelValues("hcy", rgb)
1424 | self.setChannelValues("okhcl", rgb, hue)
1425 | self.setChannelValues("okhsv", rgb, hue)
1426 |
1427 | self.updateChannelGradients()
1428 | self.blockChannels(False)
1429 | if TIME:
1430 | self.timer.start(TIME)
1431 |
1432 | def updateChannelGradients(self, channels: str=None):
1433 | if not channels or channels == "hsv":
1434 | self.hsvHue.updateGradientColors(self.hsvSaturation.value(), self.hsvValue.value(),
1435 | self.trc)
1436 | self.hsvSaturation.updateGradientColors(self.hsvHue.value(), self.hsvValue.value(),
1437 | self.trc)
1438 | self.hsvValue.updateGradientColors(self.hsvHue.value(), self.hsvSaturation.value(),
1439 | self.trc)
1440 | if not channels or channels == "hsl":
1441 | self.hslHue.updateGradientColors(self.hslSaturation.value(), self.hslLightness.value(),
1442 | self.trc)
1443 | self.hslSaturation.updateGradientColors(self.hslHue.value(), self.hslLightness.value(),
1444 | self.trc)
1445 | self.hslLightness.updateGradientColors(self.hslHue.value(), self.hslSaturation.value(),
1446 | self.trc)
1447 | if not channels or channels == "hcy":
1448 | hcyClip = self.hcyChroma.value()
1449 | if self.hcyChroma.clip > 0:
1450 | hcyClip = self.hcyChroma.clip
1451 | if self.hcyHue.scale:
1452 | self.hcyHue.updateGradientColors(self.hcyChroma.value(), self.hcyLuma.value(),
1453 | self.trc, self.hcyChroma.limit)
1454 | else:
1455 | self.hcyHue.updateGradientColors(hcyClip, self.hcyLuma.value(), self.trc)
1456 | self.hcyChroma.updateGradientColors(self.hcyHue.value(), self.hcyLuma.value(),
1457 | self.trc, self.hcyChroma.limit)
1458 | if self.hcyLuma.scale:
1459 | self.hcyLuma.updateGradientColors(self.hcyHue.value(), self.hcyChroma.value(),
1460 | self.trc, self.hcyChroma.limit)
1461 | else:
1462 | self.hcyLuma.updateGradientColors(self.hcyHue.value(), hcyClip, self.trc)
1463 | if not channels or channels == "okhcl":
1464 | okhclClip = self.okhclChroma.value()
1465 | if self.okhclChroma.clip > 0:
1466 | okhclClip = self.okhclChroma.clip
1467 | if self.okhclHue.scale:
1468 | self.okhclHue.updateGradientColors(self.okhclChroma.value(), self.okhclLightness.value(),
1469 | self.trc, self.okhclChroma.limit)
1470 | else:
1471 | self.okhclHue.updateGradientColors(okhclClip, self.okhclLightness.value(), self.trc)
1472 | self.okhclChroma.updateGradientColors(self.okhclHue.value(), self.okhclLightness.value(),
1473 | self.trc, self.okhclChroma.limit)
1474 | if self.okhclLightness.scale:
1475 | self.okhclLightness.updateGradientColors(self.okhclHue.value(), self.okhclChroma.value(),
1476 | self.trc, self.okhclChroma.limit)
1477 | else:
1478 | self.okhclLightness.updateGradientColors(self.okhclHue.value(), okhclClip, self.trc)
1479 | if not channels or channels == "okhsv":
1480 | self.okhsvHue.updateGradientColors(self.okhsvSaturation.value(),
1481 | self.okhsvValue.value(), self.trc)
1482 | self.okhsvSaturation.updateGradientColors(self.okhsvHue.value(),
1483 | self.okhsvValue.value(), self.trc)
1484 | self.okhsvValue.updateGradientColors(self.okhsvHue.value(),
1485 | self.okhsvSaturation.value(), self.trc)
1486 | if not channels or channels == "okhsl":
1487 | self.okhslHue.updateGradientColors(self.okhslSaturation.value(),
1488 | self.okhslLightness.value(), self.trc)
1489 | self.okhslSaturation.updateGradientColors(self.okhslHue.value(),
1490 | self.okhslLightness.value(), self.trc)
1491 | self.okhslLightness.updateGradientColors(self.okhslHue.value(),
1492 | self.okhslSaturation.value(), self.trc)
1493 |
1494 | def setChannelValues(self, channels: str, rgb: tuple, hue: float=-1):
1495 | if channels == "hsv":
1496 | hsv = Convert.rgbFToHsv(*rgb, self.trc)
1497 | if hue != -1:
1498 | self.hsvHue.setValue(hue)
1499 | elif hsv[1] > 0:
1500 | self.hsvHue.setValue(hsv[0])
1501 | if hsv[2] > 0:
1502 | self.hsvSaturation.setValue(hsv[1])
1503 | self.hsvValue.setValue(hsv[2])
1504 | elif channels == "hsl":
1505 | hsl = Convert.rgbFToHsl(*rgb, self.trc)
1506 | if hue != -1:
1507 | self.hslHue.setValue(hue)
1508 | elif hsl[1] > 0:
1509 | self.hslHue.setValue(hsl[0])
1510 | if hsl[2] > 0:
1511 | self.hslSaturation.setValue(hsl[1])
1512 | self.hslLightness.setValue(hsl[2])
1513 | elif channels == "hcy":
1514 | self.hcyChroma.clip = 0.0
1515 | hcy = Convert.rgbFToHcy(*rgb, self.hcyHue.value(), self.trc, self.hcyLuma.luma)
1516 | if hue != -1:
1517 | self.hcyHue.setValue(hue)
1518 | elif hcy[1] > 0:
1519 | self.hcyHue.setValue(hcy[0])
1520 | # must always set limit before setting chroma value
1521 | self.hcyChroma.setLimit(hcy[3])
1522 | self.hcyChroma.setValue(hcy[1])
1523 | self.hcyLuma.setValue(hcy[2])
1524 | elif channels == "okhcl":
1525 | self.okhclChroma.clip = 0.0
1526 | okhcl = Convert.rgbFToOkhcl(*rgb, self.okhclHue.value(), self.trc)
1527 | if hue != -1:
1528 | self.okhclHue.setValue(hue)
1529 | else:
1530 | self.okhclHue.setValue(okhcl[0])
1531 | # must always set limit before setting chroma value
1532 | self.okhclChroma.setLimit(okhcl[3])
1533 | self.okhclChroma.setValue(okhcl[1])
1534 | self.okhclLightness.setValue(okhcl[2])
1535 | elif channels == "okhsv":
1536 | okhsv = Convert.rgbFToOkhsv(*rgb, self.trc)
1537 | if hue != -1:
1538 | self.okhsvHue.setValue(hue)
1539 | elif okhsv[1] > 0:
1540 | self.okhsvHue.setValue(okhsv[0])
1541 | if okhsv[2] > 0:
1542 | self.okhsvSaturation.setValue(okhsv[1])
1543 | self.okhsvValue.setValue(okhsv[2])
1544 | elif channels == "okhsl":
1545 | okhsl = Convert.rgbFToOkhsl(*rgb, self.trc)
1546 | if hue != -1:
1547 | self.okhslHue.setValue(hue)
1548 | elif okhsl[1] > 0:
1549 | self.okhslHue.setValue(okhsl[0])
1550 | if okhsl[2] > 0:
1551 | self.okhslSaturation.setValue(okhsl[1])
1552 | self.okhslLightness.setValue(okhsl[2])
1553 |
1554 | def makeManagedColor(self, rgb: tuple, profile: str=None):
1555 | model = "RGBA"
1556 | depth = self.document.colorDepth()
1557 | if not profile:
1558 | if self.trc == "sRGB":
1559 | profile = SRGB[0]
1560 | else:
1561 | profile = LINEAR[0]
1562 | elif profile not in Application.profiles(model, depth):
1563 | models = filter(lambda cm: cm != "RGBA", Application.colorModels())
1564 | for cm in models:
1565 | if profile in Application.profiles(cm, depth):
1566 | model = cm
1567 | break
1568 |
1569 | color = ManagedColor(model, depth, profile)
1570 | components = color.components()
1571 | # support for other models in the future
1572 | if model == "RGBA":
1573 | # unordered sequence is BGRA for uint but RGBA for float
1574 | if depth[0] == "U":
1575 | components[0] = rgb[2]
1576 | components[1] = rgb[1]
1577 | components[2] = rgb[0]
1578 | else:
1579 | components[0] = rgb[0]
1580 | components[1] = rgb[1]
1581 | components[2] = rgb[2]
1582 | components[3] = 1.0
1583 | color.setComponents(components)
1584 | return color
1585 | elif model == "A" or model == "GRAYA":
1586 | components[0] = rgb[0]
1587 | components[1] = 1.0
1588 | color.setComponents(components)
1589 | return color
1590 |
1591 | def setKritaColor(self, rgb: tuple):
1592 | view = Application.activeWindow().activeView()
1593 | if not view.visible():
1594 | return
1595 |
1596 | color = self.makeManagedColor(rgb)
1597 | if color:
1598 | self.color.setCurrentColor(color)
1599 | self.updateSyntax(rgb, self.trc)
1600 | if self.color.bgMode:
1601 | view.setBackGroundColor(color)
1602 | else:
1603 | view.setForeGroundColor(color)
1604 | self.color.recent = color
1605 |
1606 | def setLuma(self, luma: bool):
1607 | self.timer.stop()
1608 | self.blockChannels(True)
1609 |
1610 | self.hcyHue.luma = luma
1611 | self.hcyChroma.luma = luma
1612 | self.hcyLuma.luma = luma
1613 |
1614 | if self.color.current:
1615 | rgb = tuple(self.color.current.componentsOrdered()[:3])
1616 | trc = self.profileTRC(self.color.current.colorProfile())
1617 | if trc != self.trc:
1618 | rgb = Convert.rgbToTRC(rgb, self.trc)
1619 | if luma or self.trc == "sRGB":
1620 | self.setChannelValues("hcy", rgb, self.hsvHue.value())
1621 | else:
1622 | self.setChannelValues("hcy", rgb)
1623 | self.updateChannelGradients("hcy")
1624 |
1625 | self.blockChannels(False)
1626 | if TIME:
1627 | self.timer.start(TIME)
1628 |
1629 | def setHistory(self):
1630 | if self.color.isChanging():
1631 | # allow getKritaColors to start timer for set history
1632 | self.color.current = None
1633 | return
1634 |
1635 | current = self.color.current
1636 | rgb = tuple(current.componentsOrdered()[:3])
1637 | if current.colorModel() == "A" or current.colorModel() == "GRAYA":
1638 | rgb = (rgb[0], rgb[0], rgb[0])
1639 | profile = current.colorProfile()
1640 | color = (rgb, profile)
1641 | if color in self.pastColors:
1642 | index = self.pastColors.index(color)
1643 | if index:
1644 | self.pastColors.pop(index)
1645 | self.pastColors.insert(0, color)
1646 | item = self.history.takeItem(index)
1647 | self.history.insertItem(0, item)
1648 | else:
1649 | self.pastColors.insert(0, color)
1650 | pixmap = QPixmap(HISTORY_HEIGHT, HISTORY_HEIGHT)
1651 | pixmap.fill(QColor(*Convert.rgbFToInt8(*rgb, self.profileTRC(profile))))
1652 | item = QListWidgetItem()
1653 | item.setIcon(QIcon(pixmap))
1654 | self.history.insertItem(0, item)
1655 | if self.memory:
1656 | for i in reversed(range(self.history.count())):
1657 | if i > self.memory - 1:
1658 | self.history.takeItem(i)
1659 | self.pastColors.pop()
1660 | else:
1661 | break
1662 | self.history.horizontalScrollBar().setValue(0)
1663 |
1664 | def setPastColor(self, index: int, fg=True):
1665 | view = Application.activeWindow().activeView()
1666 | if not view.visible():
1667 | return
1668 |
1669 | if (self.color.bgMode and not fg) or (fg and not self.color.bgMode):
1670 | self.history.takeItem(index)
1671 | color = self.pastColors.pop(index)
1672 | rgb = color[0]
1673 | trc = self.profileTRC(color[1])
1674 | self.updateSyntax(rgb, trc)
1675 | if trc != self.trc:
1676 | rgb = Convert.rgbToTRC(rgb, self.trc)
1677 | self.updateChannels(rgb)
1678 |
1679 | current = self.color.current
1680 | if fg:
1681 | view.setForeGroundColor(current)
1682 | # prevent setHistory again during getKritaColors
1683 | self.color.setForeGroundColor(current)
1684 | else:
1685 | view.setBackGroundColor(current)
1686 | # prevent setHistory again during getKritaColors
1687 | self.color.setBackGroundColor(current)
1688 | self.color.recent = current
1689 | self.setHistory()
1690 | else:
1691 | temp = self.color.temp
1692 | if fg:
1693 | view.setForeGroundColor(temp)
1694 | self.color.setForeGroundColor(temp)
1695 | else:
1696 | view.setBackGroundColor(temp)
1697 | self.color.setBackGroundColor(temp)
1698 |
1699 | def clearHistory(self):
1700 | self.history.clear()
1701 | self.pastColors = []
1702 |
1703 | def updateSyntax(self, rgb: tuple, trc: str):
1704 | if self.notation == NOTATION[0]:
1705 | self.text = Convert.rgbFToHexS(*rgb, trc)
1706 | elif self.notation == NOTATION[1]:
1707 | self.text = Convert.rgbFToOklabS(*rgb, trc)
1708 | elif self.notation == NOTATION[2]:
1709 | self.text = Convert.rgbFToOklchS(*rgb, trc)
1710 | self.syntax.setText(self.text)
1711 |
1712 | def switchNotation(self):
1713 | view = Application.activeWindow().activeView()
1714 | if not view.visible():
1715 | return
1716 |
1717 | notation = self.sender().toolTip()
1718 | self.setNotation(notation)
1719 | self.updateNotations()
1720 | color = view.foregroundColor()
1721 | trc = self.profileTRC(color.colorProfile())
1722 | self.updateSyntax(color.componentsOrdered()[:3], trc)
1723 |
1724 | def setNotation(self, notation: str):
1725 | self.notation = notation
1726 | # syntax needs to be on to set notation currently
1727 | Application.writeSetting(DOCKER_NAME, "syntax", ",".join(["True", notation]))
1728 |
1729 | def updateNotations(self):
1730 | i = NOTATION.index(self.notation)
1731 | if i == 0:
1732 | self.prevNotation.setToolTip(NOTATION[len(NOTATION) - 1])
1733 | self.nextNotation.setToolTip(NOTATION[i + 1])
1734 | elif i == len(NOTATION) - 1:
1735 | self.prevNotation.setToolTip(NOTATION[i - 1])
1736 | self.nextNotation.setToolTip(NOTATION[0])
1737 | else:
1738 | self.prevNotation.setToolTip(NOTATION[i - 1])
1739 | self.nextNotation.setToolTip(NOTATION[i + 1])
1740 |
1741 | def parseSyntax(self):
1742 | view = Application.activeWindow().activeView()
1743 | if not view.visible():
1744 | return
1745 | syntax = self.syntax.text().strip()
1746 | if syntax == self.text:
1747 | return
1748 |
1749 | rgb = None
1750 | notation = self.notation
1751 | if syntax[:1] == "#":
1752 | self.setNotation(NOTATION[0])
1753 | rgb = Convert.hexSToRgbF(syntax, self.trc)
1754 | elif syntax[:5].upper() == NOTATION[1]:
1755 | self.setNotation(NOTATION[1])
1756 | rgb = Convert.oklabSToRgbF(syntax, self.trc)
1757 | elif syntax[:5].upper() == NOTATION[2]:
1758 | self.setNotation(NOTATION[2])
1759 | rgb = Convert.oklchSToRgbF(syntax, self.trc)
1760 |
1761 | if notation != self.notation:
1762 | self.updateNotations()
1763 | if rgb:
1764 | self.setKritaColor(rgb)
1765 | self.updateChannels(rgb)
1766 | else:
1767 | color = view.foregroundColor()
1768 | trc = self.profileTRC(color.colorProfile())
1769 | self.updateSyntax(color.componentsOrdered()[:3], trc)
1770 |
1771 | def showEvent(self, event):
1772 | if TIME:
1773 | self.timer.start(TIME)
1774 |
1775 | def closeEvent(self, event):
1776 | self.timer.stop()
1777 |
1778 | def canvasChanged(self, canvas):
1779 | if self.document != Application.activeDocument():
1780 | self.color.resetColors()
1781 | self.syntax.setText("")
1782 | self.document = Application.activeDocument()
1783 | if self.document:
1784 | self.trc = self.profileTRC(self.document.colorProfile())
1785 | self.getKritaColors()
1786 |
1787 |
--------------------------------------------------------------------------------