Skip to content

Instantly share code, notes, and snippets.

@asakasinsky
Forked from nathforge/gist:604509
Created July 10, 2013 09:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save asakasinsky/5965095 to your computer and use it in GitHub Desktop.
Save asakasinsky/5965095 to your computer and use it in GitHub Desktop.
class RGBColor(object):
def __init__(self, value):
"""
A representation of an RGB color, allowing color manipulation.
Can take values in the form of strings, integers,
three-element iterables, or another RGBColor object.
>>> print RGBColor('#FFF')
#FFFFFF
>>> print RGBColor('#000000')
#000000
>>> print RGBColor(0x123456)
#123456
>>> print RGBColor((0xAA, 0xBB, 0xCC))
#AABBCC
"""
if isinstance(value, basestring):
self.r, self.g, self.b = self._rgb_from_string(value)
elif isinstance(value, int):
self.r, self.g, self.b = self._rgb_from_int(value)
elif isinstance(value, RGBColor):
self.r, self.g, self.b = value.r, value.g, value.b
else:
self.r, self.g, self.b = value
@property
def rgb(self):
return (self.r, self.g, self.b)
def adjust(self, contrast_multiplier=1.0, brightness_offset=0.0,
hue_offset=0.0, saturation_multiplier=1.0):
yuv = self._yuv709_from_rgb(self.rgb)
adjusted_yuv = self._adjust_yuv(yuv,
contrast_multiplier, brightness_offset,
hue_offset, saturation_multiplier
)
rgb = self._rgb_from_yuv709(adjusted_yuv)
return RGBColor(rgb)
def opacity(self, value, background=0xFFFFFF):
"""
Fade a color towards a background color. Accepted values are strings
('0.9', '90%') or floats. Defaults to a white background (#FFFFFF).
>>> print RGBColor('#000000').opacity('90%')
#191919
"""
if isinstance(value, basestring):
if value.endswith('%'):
value = float(value[:-1]) / 100
else:
value = float(value)
if value < 0.0 or value > 1.0:
raise ValueError('value must be in the 0.0-1.0 range')
background = RGBColor(background)
return RGBColor(
background_primary + ((primary - background_primary) * value)
for primary, background_primary in zip(self.rgb, background.rgb)
)
def best_contrast_color(self, colors=(0x000000, 0xFFFFFF)):
"""
Find the color with the greatest contrast to ourselves.
Returns None if no colors are given.
Can be used to find the best text color for a given background.
Black and white:
>>> print RGBColor('#FFFFFF').best_contrast_color(('#000000', '#FFFFFF'))
#000000
>>> print RGBColor('#000000').best_contrast_color(('#000000', '#FFFFFF'))
#FFFFFF
Full-intensity red, green and blue:
>>> print RGBColor('#FF0000').best_contrast_color(('#000000', '#FFFFFF'))
#000000
>>> print RGBColor('#00FF00').best_contrast_color(('#000000', '#FFFFFF'))
#000000
>>> print RGBColor('#0000FF').best_contrast_color(('#000000', '#FFFFFF'))
#FFFFFF
"""
best_contrast_ratio = 0
best_color = None
for color in colors:
color = RGBColor(color)
contrast_ratio = self.contrast_ratio(color)
if contrast_ratio > best_contrast_ratio:
best_contrast_ratio = contrast_ratio
best_color = color
return best_color
def contrast_ratio(self, other):
"""
Calculate contrast ratio between ourselves and another color.
<http://www.w3.org/TR/WCAG20/#contrast-ratiodef>
"""
other = RGBColor(other)
l = self.relative_luminance()
other_l = other.relative_luminance()
light_l = max(l, other_l)
dark_l = min(l, other_l)
return (light_l + 0.05) / (dark_l + 0.05)
def relative_luminance(self):
"""
Calculate our relative luminance.
<http://www.w3.org/TR/WCAG20/#relativeluminancedef>
"""
srgb_r = self.r / 255.0
srgb_g = self.g / 255.0
srgb_b = self.b / 255.0
r = srgb_r / 12.92 if srgb_r <= 0.03928 else ((srgb_r + 0.055) / 1.055) ** 2.4
g = srgb_g / 12.92 if srgb_g <= 0.03928 else ((srgb_g + 0.055) / 1.055) ** 2.4
b = srgb_b / 12.92 if srgb_b <= 0.03928 else ((srgb_b + 0.055) / 1.055) ** 2.4
l = 0.2126 * r + 0.7152 * g + 0.0722 * b
return l
def gradient(self, other, steps):
"""
Calculate the colors needed to draw a gradient from this color to
another, in the given amount of steps.
>>> print ', '.join(map(str, RGBColor('#000000').gradient('#FFFFFF', steps=8)))
#000000, #242424, #484848, #6D6D6D, #919191, #B6B6B6, #DADADA, #FFFFFF
>>> print ', '.join(map(str, RGBColor('#0077FF').gradient('#FF7700', steps=8)))
#0077FF, #2477DA, #4877B6, #6D7791, #91776D, #B67748, #DA7724, #FF7700
"""
other = RGBColor(other)
multipliers = tuple(
(end_primary - start_primary) / max(1.0, float(steps - 1))
for start_primary, end_primary in zip(self.rgb, other.rgb)
)
return tuple(
RGBColor(
int(start_primary + (position * multiplier))
for start_primary, multiplier
in zip(self.rgb, multipliers)
)
for position in xrange(steps)
)
@classmethod
def _adjust_yuv(cls, (y, u, v), contrast_multiplier, brightness_offset,
hue_offset, saturation_multiplier):
if hue_offset != 0.0:
hue_cos, hue_sin = math.cos(hue_offset), math.sin(hue_offset)
u = (u * hue_cos) + (v * hue_sin)
v = (v * hue_cos) - (u * hue_sin)
saturation_multiplier = max(0.0, saturation_multiplier)
y = max(0.0, min(1.0, (y * contrast_multiplier) + brightness_offset))
u = u * contrast_multiplier * saturation_multiplier
v = v * contrast_multiplier * saturation_multiplier
return (y, u, v)
@classmethod
def _yuv709_from_rgb(cls, (r, g, b)):
"""
Convert an (r,g,b) tuple to a (y,u,v) tuple, using the BT.709 matrix.
"""
r, g, b = r / 255.0, g / 255.0, b / 255.0
y = ( 0.2126 * r) + ( 0.7152 * g) + ( 0.0722 * b)
u = (-0.09991 * r) + (-0.33609 * g) + ( 0.436 * b)
v = ( 0.615 * r) + (-0.55861 * g) + (-0.05639 * b)
return (y, u, v)
@classmethod
def _rgb_from_yuv709(cls, (y, u, v)):
"""
Convert a (y,u,v) tuple to an (r,g,b) tuple, using the BT.709 matrix.
"""
r = max(0.0, min(1.0, y + ( 1.28033 * v)))
g = max(0.0, min(1.0, y + (-0.21482 * u) + (-0.38059 * v)))
b = max(0.0, min(1.0, y + ( 2.12798 * u) ))
r, g, b = int(r * 255.0), int(g * 255.0), int(b * 255.0)
return (r, g, b)
@classmethod
def _rgb_from_string(cls, string):
"""
Convert strings in the format "#123456", "#123", "123456" or "123"
into an (r,g,b) tuple.
"""
if string.startswith('#'):
string = string[1:]
if len(string) == 3:
r_hex, g_hex, b_hex = [hex_str + hex_str for hex_str in string]
elif len(string) == 6:
r_hex, g_hex, b_hex = string[:2], string[2:4], string[4:]
else:
raise ValueError('Hex string must be 3 or 6 characters, '
'not including the leading hash')
return (int(r_hex, 16), int(g_hex, 16), int(b_hex, 16),)
@classmethod
def _rgb_from_int(cls, value):
"""
Convert a 24-bit RGB integer value into an (r,g,b) tuple.
"""
return (
(value & 0xFF0000) >> 16,
(value & 0x00FF00) >> 8,
(value & 0x0000FF),
)
def __setattr__(self, name, value):
"""
Require valid values for r, g, b.
"""
if name in ('r', 'g', 'b'):
value = int(value)
if value < 0x00 or value > 0xFF:
raise ValueError('%s must be in the 0x00-0xFF range' % (name,))
super(RGBColor, self).__setattr__(name, value)
def __repr__(self):
return '%s((0x%02X, 0x%02X, 0x%02X))' \
% (self.__class__.__name__, self.r, self.g, self.b)
def __str__(self):
return '#%02X%02X%02X' % (self.r, self.g, self.b)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment