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', displayInline=False) 42 43# Draw an irregular polygon 44d.append(draw.Lines(-80, -45, 45 70, -49, 46 95, 49, 47 -90, 40, 48 close=False, 49 fill='#eeee00', 50 stroke='black')) 51 52# Draw a rectangle 53r = draw.Rectangle(-80,0,40,50, fill='#1248ff') 54r.appendTitle("Our first rectangle") # Add a tooltip 55d.append(r) 56 57# Draw a circle 58d.append(draw.Circle(-40, -10, 30, 59 fill='red', stroke_width=2, stroke='black')) 60 61# Draw an arbitrary path (a triangle in this case) 62p = draw.Path(stroke_width=2, stroke='lime', 63 fill='black', fill_opacity=0.2) 64p.M(-10, 20) # Start path at point (-10, 20) 65p.C(30, -10, 30, 50, 70, 20) # Draw a curve to (70, 20) 66d.append(p) 67 68# Draw text 69d.append(draw.Text('Basic text', 8, -10, 35, fill='blue')) # Text with font size 8 70d.append(draw.Text('Path text', 8, path=p, text_anchor='start', valign='middle')) 71d.append(draw.Text(['Multi-line', 'text'], 8, path=p, text_anchor='end')) 72 73# Draw multiple circular arcs 74d.append(draw.ArcLine(60,-20,20,60,270, 75 stroke='red', stroke_width=5, fill='red', fill_opacity=0.2)) 76d.append(draw.Arc(60,-20,20,60,270,cw=False, 77 stroke='green', stroke_width=3, fill='none')) 78d.append(draw.Arc(60,-20,20,270,60,cw=True, 79 stroke='blue', stroke_width=1, fill='black', fill_opacity=0.3)) 80 81# Draw arrows 82arrow = draw.Marker(-0.1, -0.5, 0.9, 0.5, scale=4, orient='auto') 83arrow.append(draw.Lines(-0.1, -0.5, -0.1, 0.5, 0.9, 0, fill='red', close=True)) 84p = draw.Path(stroke='red', stroke_width=2, fill='none', 85 marker_end=arrow) # Add an arrow to the end of a path 86p.M(20, -40).L(20, -27).L(0, -20) # Chain multiple path operations 87d.append(p) 88d.append(draw.Line(30, -20, 0, -10, 89 stroke='red', stroke_width=2, fill='none', 90 marker_end=arrow)) # Add an arrow to the end of a line 91 92d.setPixelScale(2) # Set number of pixels per geometry unit 93#d.setRenderSize(400,200) # Alternative to setPixelScale 94d.saveSvg('example.svg') 95d.savePng('example.png') 96 97# Display in Jupyter notebook 98d.rasterize() # Display as PNG 99d # Display as SVG 100``` 101 102[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example1.png)](https://github.com/cduck/drawSvg/blob/master/examples/example1.svg) 103 104### Gradients 105```python 106import drawSvg as draw 107 108d = draw.Drawing(1.5, 0.8, origin='center') 109 110d.draw(draw.Rectangle(-0.75,-0.5,1.5,1, fill='#ddd')) 111 112# Create gradient 113gradient = draw.RadialGradient(0,-0.35,0.7*10) 114gradient.addStop(0.5/0.7/10, 'green', 1) 115gradient.addStop(1/10, 'red', 0) 116 117# Draw a shape to fill with the gradient 118p = draw.Path(fill=gradient, stroke='black', stroke_width=0.002) 119p.arc(0,-0.35,0.7,30,120) 120p.arc(0,-0.35,0.5,120,30,cw=True, includeL=True) 121p.Z() 122d.append(p) 123 124# Draw another shape to fill with the same gradient 125p = draw.Path(fill=gradient, stroke='red', stroke_width=0.002) 126p.arc(0,-0.35,0.75,130,160) 127p.arc(0,-0.35,0,160,130,cw=True, includeL=True) 128p.Z() 129d.append(p) 130 131# Another gradient 132gradient2 = draw.LinearGradient(0.1,-0.35,0.1+0.6,-0.35+0.2) 133gradient2.addStop(0, 'green', 1) 134gradient2.addStop(1, 'red', 0) 135d.append(draw.Rectangle(0.1,-0.35,0.6,0.2, 136 stroke='black', stroke_width=0.002, 137 fill=gradient2)) 138 139# Display 140d.setRenderSize(w=600) 141d 142``` 143 144[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example2.png)](https://github.com/cduck/drawSvg/blob/master/examples/example2.svg) 145 146### Duplicate geometry and clip paths 147```python 148import drawSvg as draw 149 150d = draw.Drawing(1.4, 1.4, origin='center') 151 152# Define clip path 153clip = draw.ClipPath() 154clip.append(draw.Rectangle(-.25,.25-1,1,1)) 155 156# Draw a cropped circle 157c = draw.Circle(0,0,0.5, stroke_width='0.01', stroke='black', 158 fill_opacity=0.3, clip_path=clip, 159 id='circle') 160d.append(c) 161 162# Make a transparent copy, cropped again 163g = draw.Group(opacity=0.5, clip_path=clip) 164g.append(draw.Use('circle', 0.25,0.1)) 165d.append(g) 166 167# Display 168d.setRenderSize(400) 169d.rasterize() 170``` 171 172[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example3.png)](https://github.com/cduck/drawSvg/blob/master/examples/example3.svg) 173 174### Implementing other SVG tags 175```python 176import drawSvg as draw 177 178# Subclass DrawingBasicElement if it cannot have child nodes 179# Subclass DrawingParentElement otherwise 180# Subclass DrawingDef if it must go between <def></def> tags in an SVG 181class Hyperlink(draw.DrawingParentElement): 182 TAG_NAME = 'a' 183 def __init__(self, href, target=None, **kwargs): 184 # Other init logic... 185 # Keyword arguments to super().__init__() correspond to SVG node 186 # arguments: stroke_width=5 -> stroke-width="5" 187 super().__init__(href=href, target=target, **kwargs) 188 189d = draw.Drawing(1, 1.2, origin='center') 190 191# Create hyperlink 192hlink = Hyperlink('https://www.python.org', target='_blank', 193 transform='skewY(-30)') 194# Add child elements 195hlink.append(draw.Circle(0,0,0.5, fill='green')) 196hlink.append(draw.Text('Hyperlink',0.2, 0,0, center=0.6, fill='white')) 197 198# Draw and display 199d.append(hlink) 200d.setRenderSize(200) 201d 202``` 203 204[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example4.png)](https://github.com/cduck/drawSvg/blob/master/examples/example4.svg) 205 206### Animation with the SVG Animate Tag 207```python 208import drawSvg as draw 209 210d = draw.Drawing(200, 200, origin='center') 211 212# Animate the position and color of circle 213c = draw.Circle(0, 0, 20, fill='red') 214# See for supported attributes: 215# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate 216c.appendAnim(draw.Animate('cy', '6s', '-80;80;-80', 217 repeatCount='indefinite')) 218c.appendAnim(draw.Animate('cx', '6s', '0;80;0;-80;0', 219 repeatCount='indefinite')) 220c.appendAnim(draw.Animate('fill', '6s', 'red;green;blue;yellow', 221 calcMode='discrete', 222 repeatCount='indefinite')) 223d.append(c) 224 225# Animate a black circle around an ellipse 226ellipse = draw.Path() 227ellipse.M(-90, 0) 228ellipse.A(90, 40, 360, True, True, 90, 0) # Ellipse path 229ellipse.A(90, 40, 360, True, True, -90, 0) 230ellipse.Z() 231c2 = draw.Circle(0, 0, 10) 232# See for supported attributes: 233# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateMotion 234c2.appendAnim(draw.AnimateMotion(ellipse, '3s', 235 repeatCount='indefinite')) 236# See for supported attributes: 237# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateTransform 238c2.appendAnim(draw.AnimateTransform('scale', '3s', '1,2;2,1;1,2;2,1;1,2', 239 repeatCount='indefinite')) 240d.append(c2) 241 242d.saveSvg('animated.svg') # Save to file 243d # Display in Jupyter notebook 244``` 245 246[![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) 247 248### Interactive Widget 249```python 250import drawSvg as draw 251from drawSvg.widgets import DrawingWidget 252import hyperbolic.poincare.shapes as hyper # pip3 install hyperbolic 253 254# Create drawing 255d = draw.Drawing(2, 2, origin='center') 256d.setRenderSize(500) 257d.append(draw.Circle(0, 0, 1, fill='orange')) 258group = draw.Group() 259d.append(group) 260 261# Update the drawing based on user input 262click_list = [] 263def redraw(points): 264 group.children.clear() 265 for x1, y1 in points: 266 for x2, y2 in points: 267 if (x1, y1) == (x2, y2): continue 268 p1 = hyper.Point.fromEuclid(x1, y1) 269 p2 = hyper.Point.fromEuclid(x2, y2) 270 if p1.distanceTo(p2) <= 2: 271 line = hyper.Line.fromPoints(*p1, *p2, segment=True) 272 group.draw(line, hwidth=0.2, fill='white') 273 for x, y in points: 274 p = hyper.Point.fromEuclid(x, y) 275 group.draw(hyper.Circle.fromCenterRadius(p, 0.1), 276 fill='green') 277redraw(click_list) 278 279# Create interactive widget and register mouse events 280widget = DrawingWidget(d) 281@widget.mousedown 282def mousedown(widget, x, y, info): 283 if (x**2 + y**2) ** 0.5 + 1e-5 < 1: 284 click_list.append((x, y)) 285 redraw(click_list) 286 widget.refresh() 287@widget.mousemove 288def mousemove(widget, x, y, info): 289 if (x**2 + y**2) ** 0.5 + 1e-5 < 1: 290 redraw(click_list + [(x, y)]) 291 widget.refresh() 292widget 293``` 294 295![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example5.gif) 296 297### Animation with Python 298```python 299import drawSvg as draw 300 301# Draw a frame of the animation 302def draw_frame(t): 303 d = draw.Drawing(2, 6.05, origin=(-1,-1.05)) 304 d.setRenderSize(h=300) 305 d.append(draw.Rectangle(-2, -2, 4, 8, fill='white')) 306 d.append(draw.Rectangle(-1, -1.05, 2, 0.05, fill='brown')) 307 t = (t + 1) % 2 - 1 308 y = 4 - t**2 * 4 309 d.append(draw.Circle(0, y, 1, fill='lime')) 310 return d 311 312with draw.animate_jupyter(draw_frame, delay=0.05) as anim: 313# Or: 314#with draw.animate_video('example6.gif', draw_frame, duration=0.05 315# ) as anim: 316 # Add each frame to the animation 317 for i in range(20): 318 anim.draw_frame(i/10) 319 for i in range(20): 320 anim.draw_frame(i/10) 321 for i in range(20): 322 anim.draw_frame(i/10) 323``` 324 325![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example6.gif) 326 327### Asynchronous Animation in Jupyter 328```python 329# Jupyter cell 1: 330import drawSvg as draw 331from drawSvg.widgets import AsyncAnimation 332widget = AsyncAnimation(fps=10) 333widget 334# [Animation is displayed here (click to pause)] 335 336# Jupyter cell 2: 337global_variable = 'a' 338@widget.set_draw_frame # Animation above is automatically updated 339def draw_frame(secs=0): 340 # Draw something... 341 d = draw.Drawing(100, 40) 342 d.append(draw.Text(global_variable, 20, 0, 10)) 343 d.append(draw.Text('{:0.1f}'.format(secs), 20, 30, 10)) 344 return d 345 346# Jupyter cell 3: 347global_variable = 'b' # Animation above now displays 'b' 348``` 349 350![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example7.gif)