Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets
1# drawSvg 2 3A Python 3 library for programmatically generating SVG images (vector drawings) and rendering them or displaying them in a Jupyter notebook. 4 5Most common SVG tags are supported and others can easily be added by writing a small subclass of `DrawableBasicElement` or `DrawableParentElement`. 6 7An interactive [Jupyter notebook](https://jupyter.org) widget, `drawSvg.widgets.DrawingWidget`, is included that can update drawings based on mouse events. 8 9# Install 10 11drawSvg is available on PyPI: 12 13``` 14$ pip3 install drawSvg 15``` 16 17## Prerequisites 18 19Cairo needs to be installed separately. When Cairo is installed, drawSvg can output PNG or other image formats in addition to SVG. See platform-specific [instructions for Linux, Windows, and macOS from Cairo](https://www.cairographics.org/download/). Below are some examples for installing Cairo on Linux distributions and macOS. 20 21**Ubuntu** 22 23``` 24$ sudo apt-get install libcairo2 25``` 26 27**macOS** 28 29Using [homebrew](https://brew.sh/): 30 31``` 32$ brew install cairo 33``` 34 35# Examples 36 37### Basic drawing elements 38```python 39import drawSvg as draw 40 41d = draw.Drawing(200, 100, origin='center') 42 43d.append(draw.Lines(-80, -45, 44 70, -49, 45 95, 49, 46 -90, 40, 47 close=False, 48 fill='#eeee00', 49 stroke='black')) 50 51d.append(draw.Rectangle(0,0,40,50, fill='#1248ff')) 52d.append(draw.Circle(-40, -10, 30, 53 fill='red', stroke_width=2, stroke='black')) 54 55p = draw.Path(stroke_width=2, stroke='green', 56 fill='black', fill_opacity=0.5) 57p.M(-30,5) # Start path at point (-30, 5) 58p.l(60,30) # Draw line to (60, 30) 59p.h(-70) # Draw horizontal line to x=-70 60p.Z() # Draw line to start 61d.append(p) 62 63d.append(draw.ArcLine(60,-20,20,60,270, 64 stroke='red', stroke_width=5, fill='red', fill_opacity=0.2)) 65d.append(draw.Arc(60,-20,20,60,270,cw=False, 66 stroke='green', stroke_width=3, fill='none')) 67d.append(draw.Arc(60,-20,20,270,60,cw=True, 68 stroke='blue', stroke_width=1, fill='black', fill_opacity=0.3)) 69 70d.setPixelScale(2) # Set number of pixels per geometry unit 71#d.setRenderSize(400,200) # Alternative to setPixelScale 72d.saveSvg('example.svg') 73d.savePng('example.png') 74 75# Display in Jupyter notebook 76d.rasterize() # Display as PNG 77d # Display as SVG 78``` 79 80[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example1.png)](https://github.com/cduck/drawSvg/blob/master/examples/example1.svg) 81 82### Gradients 83```python 84import drawSvg as draw 85 86d = draw.Drawing(1.5, 0.8, origin='center') 87 88d.draw(draw.Rectangle(-0.75,-0.5,1.5,1, fill='#ddd')) 89 90# Create gradient 91gradient = draw.RadialGradient(0,-0.35,0.7*10) 92gradient.addStop(0.5/0.7/10, 'green', 1) 93gradient.addStop(1/10, 'red', 0) 94 95# Draw a shape to fill with the gradient 96p = draw.Path(fill=gradient, stroke='black', stroke_width=0.002) 97p.arc(0,-0.35,0.7,30,120) 98p.arc(0,-0.35,0.5,120,30,cw=True, includeL=True) 99p.Z() 100d.append(p) 101 102# Draw another shape to fill with the same gradient 103p = draw.Path(fill=gradient, stroke='red', stroke_width=0.002) 104p.arc(0,-0.35,0.75,130,160) 105p.arc(0,-0.35,0,160,130,cw=True, includeL=True) 106p.Z() 107d.append(p) 108 109# Another gradient 110gradient2 = draw.LinearGradient(0.1,-0.35,0.1+0.6,-0.35+0.2) 111gradient2.addStop(0, 'green', 1) 112gradient2.addStop(1, 'red', 0) 113d.append(draw.Rectangle(0.1,-0.35,0.6,0.2, 114 stroke='black', stroke_width=0.002, 115 fill=gradient2)) 116 117# Display 118d.setRenderSize(w=600) 119d 120``` 121 122[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example2.png)](https://github.com/cduck/drawSvg/blob/master/examples/example2.svg) 123 124### Duplicate geometry and clip paths 125```python 126import drawSvg as draw 127 128d = draw.Drawing(1.4, 1.4, origin='center') 129 130# Define clip path 131clip = draw.ClipPath() 132clip.append(draw.Rectangle(-.25,.25-1,1,1)) 133 134# Draw a cropped circle 135c = draw.Circle(0,0,0.5, stroke_width='0.01', stroke='black', 136 fill_opacity=0.3, clip_path=clip, 137 id='circle') 138d.append(c) 139 140# Make a transparent copy, cropped again 141g = draw.Group(opacity=0.5, clip_path=clip) 142g.append(draw.Use('circle', 0.25,0.1)) 143d.append(g) 144 145# Display 146d.setRenderSize(400) 147d.rasterize() 148``` 149 150[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example3.png)](https://github.com/cduck/drawSvg/blob/master/examples/example3.svg) 151 152### Implementing other SVG tags 153```python 154import drawSvg as draw 155 156# Subclass DrawingBasicElement if it cannot have child nodes 157# Subclass DrawingParentElement otherwise 158# Subclass DrawingDef if it must go between <def></def> tags in an SVG 159class Hyperlink(draw.DrawingParentElement): 160 TAG_NAME = 'a' 161 def __init__(self, href, target=None, **kwargs): 162 # Other init logic... 163 # Keyword arguments to super().__init__() correspond to SVG node 164 # arguments: stroke_width=5 -> stroke-width="5" 165 super().__init__(href=href, target=target, **kwargs) 166 167d = draw.Drawing(1, 1.2, origin='center') 168 169# Create hyperlink 170hlink = Hyperlink('https://www.python.org', target='_blank', 171 transform='skewY(-30)') 172# Add child elements 173hlink.append(draw.Circle(0,0,0.5, fill='green')) 174hlink.append(draw.Text('Hyperlink',0.2, 0,0, center=0.6, fill='white')) 175 176# Draw and display 177d.append(hlink) 178d.setRenderSize(200) 179d 180``` 181 182[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example4.png)](https://github.com/cduck/drawSvg/blob/master/examples/example4.svg) 183 184### Interactive Widget 185```python 186import drawSvg as draw 187from drawSvg.widgets import DrawingWidget 188import hyperbolic.poincare.shapes as hyper # pip3 install hyperbolic 189 190# Create drawing 191d = draw.Drawing(2, 2, origin='center') 192d.setRenderSize(500) 193d.append(draw.Circle(0, 0, 1, fill='orange')) 194group = draw.Group() 195d.append(group) 196 197# Update the drawing based on user input 198click_list = [] 199def redraw(points): 200 group.children.clear() 201 for x1, y1 in points: 202 for x2, y2 in points: 203 if (x1, y1) == (x2, y2): continue 204 p1 = hyper.Point.fromEuclid(x1, y1) 205 p2 = hyper.Point.fromEuclid(x2, y2) 206 if p1.distanceTo(p2) <= 2: 207 line = hyper.Line.fromPoints(*p1, *p2, segment=True) 208 group.draw(line, hwidth=0.2, fill='white') 209 for x, y in points: 210 p = hyper.Point.fromEuclid(x, y) 211 group.draw(hyper.Circle.fromCenterRadius(p, 0.1), 212 fill='green') 213redraw(click_list) 214 215# Create interactive widget and register mouse events 216widget = DrawingWidget(d) 217@widget.mousedown 218def mousedown(widget, x, y, info): 219 if (x**2 + y**2) ** 0.5 + 1e-5 < 1: 220 click_list.append((x, y)) 221 redraw(click_list) 222 widget.refresh() 223@widget.mousemove 224def mousemove(widget, x, y, info): 225 if (x**2 + y**2) ** 0.5 + 1e-5 < 1: 226 redraw(click_list + [(x, y)]) 227 widget.refresh() 228widget 229``` 230 231![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example5.gif) 232 233### Animation 234```python 235import drawSvg as draw 236 237# Draw a frame of the animation 238def draw_frame(t): 239 d = draw.Drawing(2, 6.05, origin=(-1,-1.05)) 240 d.setRenderSize(h=300) 241 d.append(draw.Rectangle(-2, -2, 4, 8, fill='white')) 242 d.append(draw.Rectangle(-1, -1.05, 2, 0.05, fill='brown')) 243 t = (t + 1) % 2 - 1 244 y = 4 - t**2 * 4 245 d.append(draw.Circle(0, y, 1, fill='lime')) 246 return d 247 248with draw.animate_jupyter(draw_frame, delay=0.05) as anim: 249# Or: 250#with draw.animate_video('example6.gif', draw_frame, duration=0.05 251# ) as anim: 252 # Add each frame to the animation 253 for i in range(20): 254 anim.draw_frame(i/10) 255 for i in range(20): 256 anim.draw_frame(i/10) 257 for i in range(20): 258 anim.draw_frame(i/10) 259``` 260 261![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example6.gif) 262 263### Asynchronous Animation in Jupyter 264```python 265# Jupyter cell 1: 266widget = AsyncAnimation(fps=10) 267widget 268# [Animation is displayed here (click to pause)] 269 270# Jupyter cell 2: 271global_variable = 'a' 272@widget.set_draw_frame # Animation above is automatically updated 273def draw_frame(secs=0): 274 # Draw something... 275 d = draw.Drawing(100, 40) 276 d.append(draw.Text(global_variable, 20, 0, 10)) 277 d.append(draw.Text('{:0.1f}'.format(secs), 20, 30, 10)) 278 return d 279 280# Jupyter cell 3: 281global_variable = 'b' # Animation above now displays 'b' 282``` 283 284![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example7.gif) 285 286### SVG-Native Animation 287```python 288import drawSvg as draw 289 290d = draw.Drawing(200, 200, origin='center') 291 292# Animate the position and color of circle 293c = draw.Circle(0, 0, 20, fill='red') 294# See for supported attributes: 295# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate 296c.appendAnim(draw.Animate('cy', '6s', '-80;80;-80', 297 repeatCount='indefinite')) 298c.appendAnim(draw.Animate('cx', '6s', '0;80;0;-80;0', 299 repeatCount='indefinite')) 300c.appendAnim(draw.Animate('fill', '6s', 'red;green;blue;yellow', 301 calcMode='discrete', 302 repeatCount='indefinite')) 303d.append(c) 304 305# Animate a black circle around an ellipse 306ellipse = draw.Path() 307ellipse.M(-90, 0) 308ellipse.A(90, 40, 360, True, True, 90, 0) # Ellipse path 309ellipse.A(90, 40, 360, True, True, -90, 0) 310ellipse.Z() 311c2 = draw.Circle(0, 0, 10) 312# See for supported attributes: 313# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateMotion 314c2.appendAnim(draw.AnimateMotion(ellipse, '3s', 315 repeatCount='indefinite')) 316# See for supported attributes: 317# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateTransform 318c2.appendAnim(draw.AnimateTransform('scale', '3s', '1,2;2,1;1,2;2,1;1,2', 319 repeatCount='indefinite')) 320d.append(c2) 321 322d.saveSvg('/Users/cduck/Downloads/animated.svg') # Save to file 323d # Display in Jupyter notebook 324``` 325 326[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/animated-fix-github.svg?sanitize=true)](https://github.com/cduck/drawSvg/blob/master/examples/animated.svg)