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