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