1
+ from __future__ import annotations
2
+
1
3
# flake8: NOQA: F405
2
4
from .constants import *
3
5
4
6
5
7
class Color :
6
- "A base class for all colorspace representations"
7
-
8
- pass
8
+ "A base class for all colorspace representations."
9
9
10
10
def __eq__ (self , other ):
11
11
try :
@@ -33,9 +33,188 @@ def _validate_partial(cls, content_name, value):
33
33
def _validate_alpha (cls , value ):
34
34
cls ._validate_partial ("alpha" , value )
35
35
36
+ def blend_over (
37
+ self ,
38
+ back_color : Color ,
39
+ ) -> rgba :
40
+ """Performs the "over" straight alpha blending operation, compositing
41
+ the front color over the back color.
42
+
43
+ **Straight alpha blending** is not the same as
44
+ **Premultiplied alpha blending**, see:
45
+ https://en.wikipedia.org/wiki/Alpha_compositing#Straight_versus_premultiplied
46
+
47
+ :param back_color: The background color.
48
+
49
+ :returns: The blended color.
50
+ """
51
+ # The blending operation implemented here is the "over" operation and
52
+ # replicates CSS's rgba mechanism. For the formulae used here, see:
53
+ # https://en.wikipedia.org/wiki/Alpha_compositing#Description
54
+
55
+ # Convert the input colors to rgba in order to do the calculation.
56
+ front_color = self .rgba
57
+ back_color = back_color .rgba
58
+
59
+ if front_color .a == 1 :
60
+ # If the front color is fully opaque then the result will be the same as
61
+ # front color.
62
+ return front_color .rgba
63
+
64
+ blended_alpha = min (
65
+ 1 , max (0 , (front_color .a + ((1 - front_color .a ) * back_color .a )))
66
+ )
67
+
68
+ if blended_alpha == 0 :
69
+ # Don't further blend the color, to prevent divide by 0.
70
+ return rgba (0 , 0 , 0 , 0 )
71
+ else :
72
+ blended_color = rgba (
73
+ # Red Component
74
+ min (
75
+ 255 ,
76
+ max (
77
+ 0 ,
78
+ round (
79
+ (
80
+ (front_color .r * front_color .a )
81
+ + (back_color .r * back_color .a * (1 - front_color .a ))
82
+ )
83
+ / blended_alpha
84
+ ),
85
+ ),
86
+ ),
87
+ # Green Component
88
+ min (
89
+ 255 ,
90
+ max (
91
+ 0 ,
92
+ round (
93
+ (
94
+ (front_color .g * front_color .a )
95
+ + (back_color .g * back_color .a * (1 - front_color .a ))
96
+ )
97
+ / blended_alpha
98
+ ),
99
+ ),
100
+ ),
101
+ # Blue Component
102
+ min (
103
+ 255 ,
104
+ max (
105
+ 0 ,
106
+ round (
107
+ (
108
+ (front_color .b * front_color .a )
109
+ + (back_color .b * back_color .a * (1 - front_color .a ))
110
+ )
111
+ / blended_alpha
112
+ ),
113
+ ),
114
+ ),
115
+ # Alpha component
116
+ min (1 , max (0 , blended_alpha )),
117
+ )
118
+ return blended_color
119
+
120
+ def unblend_over (self , back_color : Color , front_color_alpha : float ) -> rgba :
121
+ """Performs the reverse of the "over" straight alpha blending operation,
122
+ returning the front color.
123
+
124
+ Note: Unblending of blended colors might produce front color with slightly
125
+ imprecise component values compared to the original front color. This is
126
+ due to the loss of some amount of precision during the calculation and
127
+ conversion process between the different color formats. For example,
128
+ unblending of a hsla blended color might might produce a slightly imprecise
129
+ original front color, since the alpha blending and unblending is calculated
130
+ after conversion to rgba values.
131
+
132
+ :param back_color: The background color.
133
+ :param front_color_alpha: The original alpha value of the front color,
134
+ within the range of (0, 1].
135
+
136
+ :raises ValueError: If the value of :any:`front_color_alpha` is not within
137
+ the range of (0, 1]. The value cannot be 0, since the blended color produced
138
+ will be equal to the back color, and all information related to the front
139
+ color will be lost.
140
+
141
+ :returns: The original front color.
142
+ """
143
+ # The blending operation implemented here is the reverse of the "over"
144
+ # operation and replicates CSS's rgba mechanism. The formula used here
145
+ # are derived from the "over" straight alpha blending operation formula,
146
+ # see: https://en.wikipedia.org/wiki/Alpha_compositing#Description
147
+
148
+ blended_color = self .rgba
149
+ back_color = back_color .rgba
150
+ if not (0 < front_color_alpha <= 1 ):
151
+ raise ValueError (
152
+ "The value of front_color_alpha must be within the range of (0, 1]."
153
+ )
154
+ else :
155
+ front_color = rgba (
156
+ # Red Component
157
+ min (
158
+ 255 ,
159
+ max (
160
+ 0 ,
161
+ round (
162
+ (
163
+ (blended_color .r * blended_color .a )
164
+ - (
165
+ back_color .r
166
+ * back_color .a
167
+ * (1 - front_color_alpha )
168
+ )
169
+ )
170
+ / front_color_alpha
171
+ ),
172
+ ),
173
+ ),
174
+ # Green Component
175
+ min (
176
+ 255 ,
177
+ max (
178
+ 0 ,
179
+ round (
180
+ (
181
+ (blended_color .g * blended_color .a )
182
+ - (
183
+ back_color .g
184
+ * back_color .a
185
+ * (1 - front_color_alpha )
186
+ )
187
+ )
188
+ / front_color_alpha
189
+ ),
190
+ ),
191
+ ),
192
+ # Blue Component
193
+ min (
194
+ 255 ,
195
+ max (
196
+ 0 ,
197
+ round (
198
+ (
199
+ (blended_color .b * blended_color .a )
200
+ - (
201
+ back_color .b
202
+ * back_color .a
203
+ * (1 - front_color_alpha )
204
+ )
205
+ )
206
+ / front_color_alpha
207
+ ),
208
+ ),
209
+ ),
210
+ # Alpha Component
211
+ front_color_alpha ,
212
+ )
213
+ return front_color .rgba
214
+
36
215
37
216
class rgba (Color ):
38
- "A representation of an RGBA color"
217
+ "A representation of an RGBA color. "
39
218
40
219
def __init__ (self , r , g , b , a ):
41
220
self ._validate_rgb ("red" , r )
@@ -59,7 +238,54 @@ def _validate_rgb(cls, content_name, value):
59
238
60
239
@property
61
240
def rgba (self ):
62
- return self
241
+ # Explicitly return a rgba value, to ensure that classes which inherit the
242
+ # parent rgba class, actually return the rgba() value, instead of the child
243
+ # class. For example, without this fix, doing rgb(20, 20, 20).rgba will
244
+ # return rgb(20, 20, 20), instead of rgba(20, 20, 20, 1).
245
+ return rgba (self .r , self .g , self .b , self .a )
246
+
247
+ @property
248
+ def rgb (self ):
249
+ return rgb (self .r , self .g , self .b )
250
+
251
+ @property
252
+ def hsla (self ) -> hsla :
253
+ # Formula used here is from: https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
254
+ r_prime = self .r / 255
255
+ g_prime = self .g / 255
256
+ b_prime = self .b / 255
257
+
258
+ max_component = max (r_prime , g_prime , b_prime )
259
+ min_component = min (r_prime , g_prime , b_prime )
260
+ value = max_component
261
+ chroma = max_component - min_component
262
+
263
+ lightness = (max_component + min_component ) / 2
264
+
265
+ if chroma == 0 :
266
+ hue = 0
267
+ elif value == r_prime :
268
+ hue = 60 * (((g_prime - b_prime ) / chroma ) % 6 )
269
+ elif value == g_prime :
270
+ hue = 60 * (((b_prime - r_prime ) / chroma ) + 2 )
271
+ else : # value == b_prime:
272
+ hue = 60 * (((r_prime - g_prime ) / chroma ) + 4 )
273
+
274
+ if lightness in {0 , 1 }:
275
+ saturation = 0
276
+ else :
277
+ saturation = chroma / (1 - abs ((2 * value ) - chroma - 1 ))
278
+
279
+ return hsla (
280
+ hue % 360 , # [0,360)
281
+ min (1 , max (0 , saturation )), # [0,1]
282
+ min (1 , max (0 , lightness )), # [0,1]
283
+ min (1 , max (0 , self .a )), # [0,1]
284
+ )
285
+
286
+ @property
287
+ def hsl (self ):
288
+ return self .hsla .hsl
63
289
64
290
65
291
class rgb (rgba ):
@@ -73,7 +299,7 @@ def __repr__(self):
73
299
74
300
75
301
class hsla (Color ):
76
- "A representation of an HSLA color"
302
+ "A representation of an HSLA color. "
77
303
78
304
def __init__ (self , h , s , l , a = 1.0 ):
79
305
self ._validate_between ("hue" , h , 0 , 360 )
@@ -91,6 +317,18 @@ def __hash__(self):
91
317
def __repr__ (self ):
92
318
return f"hsla({ self .h } , { self .s } , { self .l } , { self .a } )"
93
319
320
+ @property
321
+ def hsla (self ):
322
+ # Explicitly return a hsla value, to ensure that classes which inherit the
323
+ # parent hsla class, actually return the hsla() value, instead of the child
324
+ # class. For example, without this fix, doing hsl(210, 1, 0.5).hsla will
325
+ # return hsl(210, 1, 0.5), instead of hsla(210, 1, 0.5, 1).
326
+ return hsla (self .h , self .s , self .l , self .a )
327
+
328
+ @property
329
+ def hsl (self ):
330
+ return hsl (self .h , self .s , self .l )
331
+
94
332
@property
95
333
def rgba (self ):
96
334
c = (1.0 - abs (2.0 * self .l - 1.0 )) * self .s
@@ -118,6 +356,10 @@ def rgba(self):
118
356
self .a ,
119
357
)
120
358
359
+ @property
360
+ def rgb (self ):
361
+ return self .rgba .rgb
362
+
121
363
122
364
class hsl (hsla ):
123
365
"A representation of an HSL color"
0 commit comments