205 lines
5.6 KiB
Python
205 lines
5.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
# This file is part of pygal
|
|
#
|
|
# A python svg graph plotting library
|
|
# Copyright © 2012-2016 Kozea
|
|
#
|
|
# This library is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Lesser General Public License as published by the Free
|
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
# later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
This package is an utility package oriented on color alteration.
|
|
This is used by the :py:mod:`pygal.style` package to generate
|
|
parametric styles.
|
|
|
|
"""
|
|
from __future__ import division
|
|
|
|
|
|
def normalize_float(f):
|
|
"""Round float errors"""
|
|
if abs(f - round(f)) < .0000000000001:
|
|
return round(f)
|
|
return f
|
|
|
|
|
|
def rgb_to_hsl(r, g, b):
|
|
"""Convert a color in r, g, b to a color in h, s, l"""
|
|
r = r or 0
|
|
g = g or 0
|
|
b = b or 0
|
|
r /= 255
|
|
g /= 255
|
|
b /= 255
|
|
max_ = max((r, g, b))
|
|
min_ = min((r, g, b))
|
|
d = max_ - min_
|
|
|
|
if not d:
|
|
h = 0
|
|
elif r is max_:
|
|
h = 60 * (g - b) / d
|
|
elif g is max_:
|
|
h = 60 * (b - r) / d + 120
|
|
else:
|
|
h = 60 * (r - g) / d + 240
|
|
|
|
l = .5 * (max_ + min_)
|
|
if not d:
|
|
s = 0
|
|
elif l < 0.5:
|
|
s = .5 * d / l
|
|
else:
|
|
s = .5 * d / (1 - l)
|
|
return tuple(map(normalize_float, (h % 360, s * 100, l * 100)))
|
|
|
|
|
|
def hsl_to_rgb(h, s, l):
|
|
"""Convert a color in h, s, l to a color in r, g, b"""
|
|
h /= 360
|
|
s /= 100
|
|
l /= 100
|
|
|
|
m2 = l * (s + 1) if l <= .5 else l + s - l * s
|
|
m1 = 2 * l - m2
|
|
|
|
def h_to_rgb(h):
|
|
h = h % 1
|
|
if 6 * h < 1:
|
|
return m1 + 6 * h * (m2 - m1)
|
|
if 2 * h < 1:
|
|
return m2
|
|
if 3 * h < 2:
|
|
return m1 + 6 * (2 / 3 - h) * (m2 - m1)
|
|
return m1
|
|
r, g, b = map(lambda x: round(x * 255),
|
|
map(h_to_rgb, (h + 1 / 3, h, h - 1 / 3)))
|
|
|
|
return r, g, b
|
|
|
|
|
|
def parse_color(color):
|
|
"""Take any css color definition and give back a tuple containing the
|
|
r, g, b, a values along with a type which can be: #rgb, #rgba, #rrggbb,
|
|
#rrggbbaa, rgb, rgba
|
|
"""
|
|
r = g = b = a = type = None
|
|
if color.startswith('#'):
|
|
color = color[1:]
|
|
if len(color) == 3:
|
|
type = '#rgb'
|
|
color = color + 'f'
|
|
if len(color) == 4:
|
|
type = type or '#rgba'
|
|
color = ''.join([c * 2 for c in color])
|
|
if len(color) == 6:
|
|
type = type or '#rrggbb'
|
|
color = color + 'ff'
|
|
assert len(color) == 8
|
|
type = type or '#rrggbbaa'
|
|
r, g, b, a = [
|
|
int(''.join(c), 16) for c in zip(color[::2], color[1::2])]
|
|
a /= 255
|
|
elif color.startswith('rgb('):
|
|
type = 'rgb'
|
|
color = color[4:-1]
|
|
r, g, b, a = [int(c) for c in color.split(',')] + [1]
|
|
elif color.startswith('rgba('):
|
|
type = 'rgba'
|
|
color = color[5:-1]
|
|
r, g, b, a = [int(c) for c in color.split(',')[:-1]] + [
|
|
float(color.split(',')[-1])]
|
|
return r, g, b, a, type
|
|
|
|
|
|
def unparse_color(r, g, b, a, type):
|
|
"""
|
|
Take the r, g, b, a color values and give back
|
|
a type css color string. This is the inverse function of parse_color
|
|
"""
|
|
if type == '#rgb':
|
|
# Don't lose precision on rgb shortcut
|
|
if r % 17 == 0 and g % 17 == 0 and b % 17 == 0:
|
|
return '#%x%x%x' % (int(r / 17), int(g / 17), int(b / 17))
|
|
type = '#rrggbb'
|
|
|
|
if type == '#rgba':
|
|
if r % 17 == 0 and g % 17 == 0 and b % 17 == 0:
|
|
return '#%x%x%x%x' % (int(r / 17), int(g / 17), int(b / 17),
|
|
int(a * 15))
|
|
type = '#rrggbbaa'
|
|
|
|
if type == '#rrggbb':
|
|
return '#%02x%02x%02x' % (r, g, b)
|
|
|
|
if type == '#rrggbbaa':
|
|
return '#%02x%02x%02x%02x' % (r, g, b, int(a * 255))
|
|
|
|
if type == 'rgb':
|
|
return 'rgb(%d, %d, %d)' % (r, g, b)
|
|
|
|
if type == 'rgba':
|
|
return 'rgba(%d, %d, %d, %g)' % (r, g, b, a)
|
|
|
|
|
|
def is_foreground_light(color):
|
|
"""
|
|
Determine if the background color need a light or dark foreground color
|
|
"""
|
|
return rgb_to_hsl(*parse_color(color)[:3])[2] < 17.9
|
|
|
|
|
|
_clamp = lambda x: max(0, min(100, x))
|
|
|
|
|
|
def _adjust(hsl, attribute, percent):
|
|
"""Internal adjust function"""
|
|
hsl = list(hsl)
|
|
if attribute > 0:
|
|
hsl[attribute] = _clamp(hsl[attribute] + percent)
|
|
else:
|
|
hsl[attribute] += percent
|
|
|
|
return hsl
|
|
|
|
|
|
def adjust(color, attribute, percent):
|
|
"""Adjust an attribute of color by a percent"""
|
|
r, g, b, a, type = parse_color(color)
|
|
r, g, b = hsl_to_rgb(*_adjust(rgb_to_hsl(r, g, b), attribute, percent))
|
|
return unparse_color(r, g, b, a, type)
|
|
|
|
|
|
def rotate(color, percent):
|
|
"""Rotate a color by changing its hue value by percent"""
|
|
return adjust(color, 0, percent)
|
|
|
|
|
|
def saturate(color, percent):
|
|
"""Saturate a color by increasing its saturation by percent"""
|
|
return adjust(color, 1, percent)
|
|
|
|
|
|
def desaturate(color, percent):
|
|
"""Desaturate a color by decreasing its saturation by percent"""
|
|
return adjust(color, 1, -percent)
|
|
|
|
|
|
def lighten(color, percent):
|
|
"""Lighten a color by increasing its lightness by percent"""
|
|
return adjust(color, 2, percent)
|
|
|
|
|
|
def darken(color, percent):
|
|
"""Darken a color by decreasing its lightness by percent"""
|
|
return adjust(color, 2, -percent)
|