Skip to content

Commit 4de3491

Browse files
Add alpha blending to Travertino (beeware#3140)
Add methods to Travertino's color API to allow for alpha blending and unbending of colors. Co-authored-by: Russell Keith-Magee <[email protected]>
1 parent 48b094b commit 4de3491

File tree

5 files changed

+910
-6
lines changed

5 files changed

+910
-6
lines changed

changes/3140.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The Travertino library now includes APIs to perform alpha blending operations and conversion of rgba to hsla.

travertino/src/travertino/colors.py

+248-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
from __future__ import annotations
2+
13
# flake8: NOQA: F405
24
from .constants import *
35

46

57
class Color:
6-
"A base class for all colorspace representations"
7-
8-
pass
8+
"A base class for all colorspace representations."
99

1010
def __eq__(self, other):
1111
try:
@@ -33,9 +33,188 @@ def _validate_partial(cls, content_name, value):
3333
def _validate_alpha(cls, value):
3434
cls._validate_partial("alpha", value)
3535

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+
36215

37216
class rgba(Color):
38-
"A representation of an RGBA color"
217+
"A representation of an RGBA color."
39218

40219
def __init__(self, r, g, b, a):
41220
self._validate_rgb("red", r)
@@ -59,7 +238,54 @@ def _validate_rgb(cls, content_name, value):
59238

60239
@property
61240
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
63289

64290

65291
class rgb(rgba):
@@ -73,7 +299,7 @@ def __repr__(self):
73299

74300

75301
class hsla(Color):
76-
"A representation of an HSLA color"
302+
"A representation of an HSLA color."
77303

78304
def __init__(self, h, s, l, a=1.0):
79305
self._validate_between("hue", h, 0, 360)
@@ -91,6 +317,18 @@ def __hash__(self):
91317
def __repr__(self):
92318
return f"hsla({self.h}, {self.s}, {self.l}, {self.a})"
93319

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+
94332
@property
95333
def rgba(self):
96334
c = (1.0 - abs(2.0 * self.l - 1.0)) * self.s
@@ -118,6 +356,10 @@ def rgba(self):
118356
self.a,
119357
)
120358

359+
@property
360+
def rgb(self):
361+
return self.rgba.rgb
362+
121363

122364
class hsl(hsla):
123365
"A representation of an HSL color"

0 commit comments

Comments
 (0)