Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets
1import math 2 3try: 4 import numpy as np 5 import pwkit.colormaps 6except ImportError as e: 7 raise ImportError( 8 'Optional dependencies not installed. ' 9 'Install with `python3 -m pip install "drawsvg[color]"' 10 ) from e 11 12 13# Most calculations from http://www.chilliant.com/rgb2hsv.html 14 15 16def limit(v, low=0, high=1): 17 return max(min(v, high), low) 18 19class Srgb: 20 LUMA_WEIGHTS = (0.299, 0.587, 0.114) 21 def __init__(self, r, g, b): 22 self.r = float(r) 23 self.g = float(g) 24 self.b = float(b) 25 def __iter__(self): 26 return iter((self.r, self.g, self.b)) 27 def __repr__(self): 28 return 'RGB({}, {}, {})'.format(self.r, self.g, self.b) 29 def __str__(self): 30 return 'rgb({}%,{}%,{}%)'.format(self.r*100, self.g*100, self.b*100) 31 def luma(self, wts=None): 32 if wts is None: wts = self.LUMA_WEIGHTS 33 rw, gw, bw = wts 34 return rw*self.r + gw*self.g + bw*self.b 35 def to_srgb(self): 36 return self 37 @staticmethod 38 def from_hue(h): 39 h = h % 1 40 r = abs(h * 6 - 3) - 1 41 g = 2 - abs(h * 6 - 2) 42 b = 2 - abs(h * 6 - 4) 43 return Srgb(limit(r), limit(g), limit(b)) 44 45class Hsl: 46 def __init__(self, h, s, l): 47 self.h = float(h) % 1 48 self.s = float(s) 49 self.l = float(l) 50 def __iter__(self): 51 return iter((self.h, self.s, self.l)) 52 def __repr__(self): 53 return 'HSL({}, {}, {})'.format(self.h, self.s, self.l) 54 def __str__(self): 55 r, g, b = self.to_srgb() 56 return 'rgb({}%,{}%,{}%)'.format( 57 round(r*100, 2), round(g*100, 2), round(b*100, 2)) 58 def to_srgb(self): 59 hs = Srgb.from_hue(self.h) 60 c = (1 - abs(2 * self.l - 1)) * self.s 61 return Srgb( 62 (hs.r - 0.5) * c + self.l, 63 (hs.g - 0.5) * c + self.l, 64 (hs.b - 0.5) * c + self.l 65 ) 66 67class Hsv: 68 def __init__(self, h, s, v): 69 self.h = float(h) % 1 70 self.s = float(s) 71 self.v = float(v) 72 def __iter__(self): 73 return iter((self.h, self.s, self.v)) 74 def __repr__(self): 75 return 'HSV({}, {}, {})'.format(self.h, self.s, self.v) 76 def __str__(self): 77 r, g, b = self.to_srgb() 78 return 'rgb({}%,{}%,{}%)'.format( 79 round(r*100, 2), round(g*100, 2), round(b*100, 2)) 80 def to_srgb(self): 81 hs = Srgb.from_hue(self.h) 82 c = self.v * self.s 83 hp = self.h * 6 84 x = c * (1 - abs(hp % 2 - 1)) 85 if hp < 1: 86 r1, g1, b1 = c, x, 0 87 elif hp < 2: 88 r1, g1, b1 = x, c, 0 89 elif hp < 3: 90 r1, g1, b1 = 0, c, x 91 elif hp < 4: 92 r1, g1, b1 = 0, x, c 93 elif hp < 5: 94 r1, g1, b1 = x, 0, c 95 else: 96 r1, g1, b1 = c, 0, x 97 m = self.v - c 98 return Srgb(r1+m, g1+m, b1+m) 99 100class Sin: 101 def __init__(self, h, s, l): 102 self.h = float(h) % 1 103 self.s = float(s) 104 self.l = float(l) 105 def __iter__(self): 106 return iter((self.h, self.s, self.l)) 107 def __repr__(self): 108 return 'Sin({}, {}, {})'.format(self.h, self.s, self.l) 109 def __str__(self): 110 r, g, b = self.to_srgb() 111 return 'rgb({}%,{}%,{}%)'.format( 112 round(r*100, 2), round(g*100, 2), round(b*100, 2)) 113 def to_srgb(self): 114 h = self.h 115 scale = self.s / 2 116 shift = self.l #* (1-2*scale) 117 return Srgb( 118 shift + scale * math.cos(math.pi*2 * (h - 0/6)), 119 shift + scale * math.cos(math.pi*2 * (h - 2/6)), 120 shift + scale * math.cos(math.pi*2 * (h - 4/6)), 121 ) 122 123class Hcy: 124 HCY_WEIGHTS = Srgb.LUMA_WEIGHTS 125 def __init__(self, h, c, y): 126 self.h = float(h) % 1 127 self.c = float(c) 128 self.y = float(y) 129 def __iter__(self): 130 return iter((self.h, self.c, self.y)) 131 def __repr__(self): 132 return 'HCY({}, {}, {})'.format(self.h, self.c, self.y) 133 def __str__(self): 134 r, g, b = self.to_srgb() 135 return 'rgb({}%,{}%,{}%)'.format(r*100, g*100, b*100) 136 def to_srgb(self): 137 hs = Srgb.from_hue(self.h) 138 y = hs.luma(wts=self.HCY_WEIGHTS) 139 c = self.c 140 if self.y < y: 141 c *= self.y / y 142 elif y < 1: 143 c *= (1 - self.y) / (1 - y) 144 return Srgb( 145 (hs.r - y) * c + self.y, 146 (hs.g - y) * c + self.y, 147 (hs.b - y) * c + self.y, 148 ) 149 @staticmethod 150 def _rgb_to_hcv(srgb): 151 if srgb.g < srgb.b: 152 p = (srgb.b, srgb.g, -1., 2./3.) 153 else: 154 p = (srgb.g, srgb.b, 0., -1./3.) 155 if srgb.r < p[0]: 156 q = (p[0], p[1], p[3], srgb.r) 157 else: 158 q = (srgb.r, p[1], p[2], p[0]) 159 c = q[0] - min(q[3], q[1]) 160 h = abs((q[3] - q[1]) / (6*c + 1e-10) + q[2]) 161 return (h, c, q[0]) 162 @classmethod 163 def from_srgb(cls, srgb): 164 hcv = list(cls._rgb_to_hcv(srgb)) 165 rw, gw, bw = cls.HCY_WEIGHTS 166 y = rw*srgb.r + gw*srgb.g + bw*srgb.b 167 hs = Srgb.from_hue(hcv[0]) 168 z = rw*hs.r + gw*hs.g + bw*hs.b 169 if y < z: 170 hcv[1] *= z / (y + 1e-10) 171 else: 172 hcv[1] *= (1 - z) / (1 - y + 1e-10) 173 return Hcy(hcv[0], hcv[1], y) 174 175class Cielab: 176 REF_WHITE = (0.95047, 1., 1.08883) 177 def __init__(self, l, a, b): 178 self.l = float(l) 179 self.a = float(a) 180 self.b = float(b) 181 def __iter__(self): 182 return iter((self.l, self.a, self.b)) 183 def __repr__(self): 184 return 'CIELAB({}, {}, {})'.format(self.l, self.a, self.b) 185 def __str__(self): 186 r, g, b = self.to_srgb() 187 return 'rgb({}%,{}%,{}%)'.format( 188 round(r*100, 2), round(g*100, 2), round(b*100, 2)) 189 def to_srgb(self): 190 in_arr = np.array((self.l, self.a, self.b)) 191 xyz = pwkit.colormaps.cielab_to_xyz(in_arr, self.REF_WHITE) 192 lin_srgb = pwkit.colormaps.xyz_to_linsrgb(xyz) 193 r, g, b = pwkit.colormaps.linsrgb_to_srgb(lin_srgb) 194 return Srgb(r, g, b) 195 @classmethod 196 def from_srgb(cls, srgb, ref_white=None): 197 if ref_white is None: ref_white = cls.REF_WHITE 198 in_arr = np.array((*srgb,), dtype=float) 199 lin_srgb = pwkit.colormaps.srgb_to_linsrgb(in_arr) 200 xyz = pwkit.colormaps.linsrgb_to_xyz(lin_srgb) 201 l, a, b = pwkit.colormaps.xyz_to_cielab(xyz, ref_white) 202 return Cielab(l, a, b)