Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets

Add several color spaces and some conversions

Changed files
+199
+199
color.py
···
···
+
+
import math
+
import numpy as np
+
import pwkit.colormaps # pip3 install pwkit
+
+
+
# Most calculations from http://www.chilliant.com/rgb2hsv.html
+
+
+
def limit(v, low=0, high=1):
+
return max(min(v, high), low)
+
+
class Srgb:
+
LUMA_WEIGHTS = (0.299, 0.587, 0.114)
+
def __init__(self, r, g, b):
+
self.r = float(r)
+
self.g = float(g)
+
self.b = float(b)
+
def __iter__(self):
+
return iter((self.r, self.g, self.b))
+
def __repr__(self):
+
return 'RGB({}, {}, {})'.format(self.r, self.g, self.b)
+
def __str__(self):
+
return 'rgb({}%,{}%,{}%)'.format(self.r*100, self.g*100, self.b*100)
+
def luma(self, wts=None):
+
if wts is None: wts = self.LUMA_WEIGHTS
+
rw, gw, bw = wts
+
return rw*self.r + gw*self.g + bw*self.b
+
def toSrgb(self):
+
return self
+
@staticmethod
+
def fromHue(h):
+
h = h % 1
+
r = abs(h * 6 - 3) - 1
+
g = 2 - abs(h * 6 - 2)
+
b = 2 - abs(h * 6 - 4)
+
return Srgb(limit(r), limit(g), limit(b))
+
+
class Hsl:
+
def __init__(self, h, s, l):
+
self.h = float(h) % 1
+
self.s = float(s)
+
self.l = float(l)
+
def __iter__(self):
+
return iter((self.h, self.s, self.l))
+
def __repr__(self):
+
return 'HSL({}, {}, {})'.format(self.h, self.s, self.l)
+
def __str__(self):
+
r, g, b = self.toSrgb()
+
return 'rgb({}%,{}%,{}%)'.format(round(r*100), round(g*100), round(b*100))
+
def toSrgb(self):
+
hs = Srgb.fromHue(self.h)
+
c = (1 - abs(2 * self.l - 1)) * self.s
+
return Srgb(
+
(hs.r - 0.5) * c + self.l,
+
(hs.g - 0.5) * c + self.l,
+
(hs.b - 0.5) * c + self.l
+
)
+
+
class Hsv:
+
def __init__(self, h, s, v):
+
self.h = float(h) % 1
+
self.s = float(s)
+
self.v = float(v)
+
def __iter__(self):
+
return iter((self.h, self.s, self.v))
+
def __repr__(self):
+
return 'HSV({}, {}, {})'.format(self.h, self.s, self.v)
+
def __str__(self):
+
r, g, b = self.toSrgb()
+
return 'rgb({}%,{}%,{}%)'.format(round(r*100), round(g*100), round(b*100))
+
def toSrgb(self):
+
hs = Srgb.fromHue(self.h)
+
c = self.v * self.s
+
hp = self.h * 6
+
x = c * (1 - abs(hp % 2 - 1))
+
if hp < 1:
+
r1, g1, b1 = c, x, 0
+
elif hp < 2:
+
r1, g1, b1 = x, c, 0
+
elif hp < 3:
+
r1, g1, b1 = 0, c, x
+
elif hp < 4:
+
r1, g1, b1 = 0, x, c
+
elif hp < 5:
+
r1, g1, b1 = x, 0, c
+
else:
+
r1, g1, b1 = c, 0, x
+
m = self.v - c
+
return Srgb(r1+m, g1+m, b1+m)
+
+
class Sin:
+
def __init__(self, h, s, l):
+
self.h = float(h) % 1
+
self.s = float(s)
+
self.l = float(l)
+
def __iter__(self):
+
return iter((self.h, self.s, self.l))
+
def __repr__(self):
+
return 'Sin({}, {}, {})'.format(self.h, self.s, self.l)
+
def __str__(self):
+
r, g, b = self.toSrgb()
+
return 'rgb({}%,{}%,{}%)'.format(round(r*100), round(g*100), round(b*100))
+
def toSrgb(self):
+
h = self.h
+
scale = self.s / 2
+
shift = self.l #* (1-2*scale)
+
return Srgb(
+
shift + scale * math.cos(math.pi*2 * (h - 0/6)),
+
shift + scale * math.cos(math.pi*2 * (h - 2/6)),
+
shift + scale * math.cos(math.pi*2 * (h - 4/6)),
+
)
+
+
class Hcy:
+
HCY_WEIGHTS = Srgb.LUMA_WEIGHTS
+
def __init__(self, h, c, y):
+
self.h = float(h) % 1
+
self.c = float(c)
+
self.y = float(y)
+
def __iter__(self):
+
return iter((self.h, self.c, self.y))
+
def __repr__(self):
+
return 'HCY({}, {}, {})'.format(self.h, self.c, self.y)
+
def __str__(self):
+
r, g, b = self.toSrgb()
+
return 'rgb({}%,{}%,{}%)'.format(r*100, g*100, b*100)
+
def toSrgb(self):
+
hs = Srgb.fromHue(self.h)
+
y = hs.luma(wts=self.HCY_WEIGHTS)
+
c = self.c
+
if self.y < y:
+
c *= self.y / y
+
elif y < 1:
+
c *= (1 - self.y) / (1 - y)
+
return Srgb(
+
(hs.r - y) * c + self.y,
+
(hs.g - y) * c + self.y,
+
(hs.b - y) * c + self.y,
+
)
+
@staticmethod
+
def _rgbToHcv(srgb):
+
if srgb.g < srgb.b:
+
p = (srgb.b, srgb.g, -1., 2./3.)
+
else:
+
p = (srgb.g, srgb.b, 0., -1./3.)
+
if srgb.r < p[0]:
+
q = (p[0], p[1], p[3], srgb.r)
+
else:
+
q = (srgb.r, p[1], p[2], p[0])
+
c = q[0] - min(q[3], q[1])
+
h = abs((q[3] - q[1]) / (6*c + 1e-10) + q[2])
+
return (h, c, q[0])
+
@classmethod
+
def fromSrgb(cls, srgb):
+
hcv = list(cls._rgbToHcv(srgb))
+
rw, gw, bw = cls.HCY_WEIGHTS
+
y = rw*srgb.r + gw*srgb.g + bw*srgb.b
+
hs = Srgb.fromHue(hcv[0])
+
z = rw*hs.r + gw*hs.g + bw*hs.b
+
if y < z:
+
hcv[1] *= z / (y + 1e-10)
+
else:
+
hcv[1] *= (1 - z) / (1 - y + 1e-10)
+
return Hcy(hcv[0], hcv[1], y)
+
+
class Cielab:
+
REF_WHITE = (0.95047, 1., 1.08883)
+
def __init__(self, l, a, b):
+
self.l = float(l)
+
self.a = float(a)
+
self.b = float(b)
+
def __iter__(self):
+
return iter((self.l, self.a, self.b))
+
def __repr__(self):
+
return 'CIELAB({}, {}, {})'.format(self.l, self.a, self.b)
+
def __str__(self):
+
r, g, b = self.toSrgb()
+
return 'rgb({}%,{}%,{}%)'.format(round(r*100), round(g*100), round(b*100))
+
def toSrgb(self):
+
inArr = np.array((*self.l,), dtype=float)
+
xyz = pwkit.colormaps.cielab_to_xyz(inArr, self.REF_WHITE)
+
linSrgb = pwkit.colormaps.xyz_to_linsrgb(xyz)
+
r, g, b = pwkit.colormaps.linsrgb_to_srgb(linSrgb)
+
return Srgb(r, g, b)
+
@classmethod
+
def fromSrgb(cls, srgb, refWhite=None):
+
if refWhite is None: refWhite = cls.REF_WHITE
+
inArr = np.array((*srgb,), dtype=float)
+
linSrgb = pwkit.colormaps.srgb_to_linsrgb(inArr)
+
xyz = pwkit.colormaps.linsrgb_to_xyz(linSrgb)
+
l, a, b = pwkit.colormaps.xyz_to_cielab(xyz, refWhite)
+
return Cielab(l, a, b)
+
def toSrgb(self):
+
inArr = np.array((self.l, self.a, self.b))
+
xyz = pwkit.colormaps.cielab_to_xyz(inArr, self.REF_WHITE)
+
linSrgb = pwkit.colormaps.xyz_to_linsrgb(xyz)
+
r, g, b = pwkit.colormaps.linsrgb_to_srgb(linSrgb)
+
return Srgb(r, g, b)
+