Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets
1 2import math 3 4# TODO: Support drawing ellipses without manually using Path 5 6 7class DrawingElement: 8 ''' Base class for drawing elements 9 10 Subclasses must implement writeSvgElement ''' 11 def writeSvgElement(self, outputFile): 12 raise NotImplementedError('Abstract base class') 13 def __eq__(self, other): 14 return self is other 15 16class DrawingBasicElement(DrawingElement): 17 ''' Base class for SVG drawing elements that are a single node with no 18 child nodes ''' 19 TAG_NAME = '_' 20 def __init__(self, **args): 21 self.args = args 22 def writeSvgElement(self, outputFile): 23 outputFile.write('<') 24 outputFile.write(self.TAG_NAME) 25 outputFile.write(' ') 26 for k, v in self.args.items(): 27 k = k.replace('_', '-') 28 outputFile.write('{}="{}" '.format(k,v)) 29 outputFile.write('/>') 30 def __eq__(self, other): 31 if isinstance(other, type(self)): 32 return (self.tagName == other.tagName and 33 self.args == other.args) 34 return False 35 36class NoElement(DrawingElement): 37 ''' A drawing element that has no effect ''' 38 def __init__(self): pass 39 def writeSvgElement(self, outputFile): 40 pass 41 def __eq__(self, other): 42 if isinstance(other, type(self)): 43 return True 44 return False 45 46class Rectangle(DrawingBasicElement): 47 ''' A rectangle 48 49 Additional keyword arguments are ouput as additional arguments to the 50 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. ''' 51 TAG_NAME = 'rect' 52 def __init__(self, x, y, width, height, **kwargs): 53 super().__init__(x=x, y=-y-height, width=width, height=height, 54 **kwargs) 55 56class Circle(DrawingBasicElement): 57 ''' A circle 58 59 Additional keyword arguments are ouput as additional properties to the 60 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. ''' 61 TAG_NAME = 'circle' 62 def __init__(self, cx, cy, r, **kwargs): 63 super().__init__(cx=cx, cy=-cy, r=r, **kwargs) 64 65class ArcLine(Circle): 66 ''' An arc 67 68 In most cases, use Arc instead of ArcLine. ArcLine uses the 69 stroke-dasharray SVG property to make the edge of a circle look like 70 an arc. 71 72 Additional keyword arguments are ouput as additional arguments to the 73 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. ''' 74 def __init__(self, cx, cy, r, startDeg, endDeg, **kwargs): 75 if endDeg - startDeg == 360: 76 super().__init__(cx, cy, r, **kwargs) 77 return 78 startDeg, endDeg = (-endDeg) % 360, (-startDeg) % 360 79 arcDeg = (endDeg - startDeg) % 360 80 def arcLen(deg): return math.radians(deg) * r 81 wholeLen = 2 * math.pi * r 82 if endDeg == startDeg: 83 offset = 1 84 dashes = "0 {}".format(wholeLen+2) 85 #elif endDeg >= startDeg: 86 elif True: 87 startLen = arcLen(startDeg) 88 arcLen = arcLen(arcDeg) 89 offLen = wholeLen - arcLen 90 offset = -startLen 91 dashes = "{} {}".format(arcLen, offLen) 92 #else: 93 # firstLen = arcLen(endDeg) 94 # secondLen = arcLen(360-startDeg) 95 # gapLen = wholeLen - firstLen - secondLen 96 # offset = 0 97 # dashes = "{} {} {}".format(firstLen, gapLen, secondLen) 98 super().__init__(cx, cy, r, stroke_dasharray=dashes, 99 stroke_dashoffset=offset, **kwargs) 100 101class Path(DrawingBasicElement): 102 ''' An arbitrary path 103 104 Path Supports building an SVG path by calling instance methods 105 corresponding to path commands. 106 107 Additional keyword arguments are ouput as additional properties to the 108 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. ''' 109 TAG_NAME = 'path' 110 def __init__(self, d='', **kwargs): 111 super().__init__(d=d, **kwargs) 112 def append(self, commandStr, *args): 113 if len(self.args['d']) > 0: 114 commandStr = ' ' + commandStr 115 if len(args) > 0: 116 commandStr = commandStr + ','.join(map(str, args)) 117 self.args['d'] += commandStr 118 def M(self, x, y): self.append('M', x, -y) 119 def m(self, dx, dy): self.append('m', dx, -dy) 120 def L(self, x, y): self.append('L', x, -y) 121 def l(self, dx, dy): self.append('l', dx, -dy) 122 def H(self, x, y): self.append('H', x) 123 def h(self, dx): self.append('h', dx) 124 def V(self, y): self.append('V', -y) 125 def v(self, dy): self.append('v', -dy) 126 def Z(self): self.append('Z') 127 def C(self, cx1, cy1, cx2, cy2, ex, ey): 128 self.append('C', cx1, -cy1, cx2, -cy2, ex, -ey) 129 def c(self, cx1, cy1, cx2, cy2, ex, ey): 130 self.append('c', cx1, -cy1, cx2, -cy2, ex, -ey) 131 def S(self, cx2, cy2, ex, ey): self.append('S', cx2, -cy2, ex, -ey) 132 def s(self, cx2, cy2, ex, ey): self.append('s', cx2, -cy2, ex, -ey) 133 def Q(self, cx, cy, ex, ey): self.append('Q', cx, -cy, ex, -ey) 134 def q(self, cx, cy, ex, ey): self.append('q', cx, -cy, ex, -ey) 135 def T(self, ex, ey): self.append('T', ex, -ey) 136 def t(self, ex, ey): self.append('t', ex, -ey) 137 def A(self, rx, ry, rot, largeArc, sweep, ex, ey): 138 self.append('A', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex, -ey) 139 def a(self, rx, ry, rot, largeArc, sweep, ex, ey): 140 self.append('a', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex, -ey) 141 def arc(self, cx, cy, r, startDeg, endDeg, cw=False, includeM=True, includeL=False): 142 ''' Uses A() to draw a circular arc ''' 143 largeArc = (endDeg - startDeg) % 360 > 180 144 startRad, endRad = startDeg*math.pi/180, endDeg*math.pi/180 145 sx, sy = r*math.cos(startRad), r*math.sin(startRad) 146 ex, ey = r*math.cos(endRad), r*math.sin(endRad) 147 if includeL: 148 self.L(cx+sx, cy+sy) 149 elif includeM: 150 self.M(cx+sx, cy+sy) 151 self.A(r, r, 0, largeArc ^ cw, cw, cx+ex, cy+ey) 152 153class Lines(Path): 154 ''' A sequence of connected lines (or a polygon) 155 156 Additional keyword arguments are ouput as additional properties to the 157 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. ''' 158 def __init__(self, sx, sy, *points, close=False, **kwargs): 159 super().__init__(d='', **kwargs) 160 self.M(sx, sy) 161 assert len(points) % 2 == 0 162 for i in range(len(points) // 2): 163 self.L(points[2*i], points[2*i+1]) 164 if close: 165 self.Z() 166 167class Line(Lines): 168 ''' A line 169 170 Additional keyword arguments are ouput as additional properties to the 171 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. ''' 172 def __init__(self, sx, sy, ex, ey, **kwargs): 173 super().__init__(sx, sy, ex, ey, close=False, **kwargs) 174 175class Arc(Path): 176 ''' An arc 177 178 Additional keyword arguments are ouput as additional properties to the 179 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. ''' 180 def __init__(self, cx, cy, r, startDeg, endDeg, cw=False, **kwargs): 181 super().__init__(d='', **kwargs) 182 self.arc(cx, cy, r, startDeg, endDeg, cw=cw, includeM=True) 183