Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets
1[![drawsvg logo](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/logo.svg?sanitize=true)](https://github.com/cduck/drawSvg/blob/master/examples/logo.svg) 2 3A Python 3 library for programmatically generating SVG images and animations that can render and display your drawings in a Jupyter notebook or Jupyter lab. 4 5Most common SVG tags are supported and others can easily be added by writing a small subclass of `DrawableBasicElement` or `DrawableParentElement`. [Nearly all SVG attributes](https://developer.mozilla.org/en-US/docs/Web/SVG) are supported via keyword args (e.g. Python keyword argument `fill_opacity=0.5` becomes SVG attribute `fill-opacity="0.5"`). 6 7An interactive [Jupyter notebook](https://jupyter.org) widget, `drawsvg.widgets.DrawingWidget`, [is included](#interactive-widget) that can update drawings based on mouse events. The widget does not yet work in Jupyter lab. 8 9 10# Install 11 12Drawsvg is available on PyPI: 13```bash 14$ python3 -m pip install "drawsvg~=2.0" 15``` 16 17To enable raster image support (PNG, MP4, and GIF), follow the [full-feature install instructions](#full-feature-install). 18 19 20## Upgrading from version 1.x 21 22Major breaking changes: 23 24- camelCase method and argument names are now snake\_case and the package name is lowercase (except for arguments that correspond to camelCase SVG attributes). 25- The default coordinate system y-axis now matches the SVG coordinate system (y increases down the screen, x increases right) 26- How to fix `ModuleNotFoundError: No module named 'drawSvg'` (with a capital S)? Either pip install `"drawSvg~=1.9"` or update your code for drawsvg 2.x (for example, change `drawSvg` to `drawsvg` and `d.saveSvg` to `d.save_svg`). 27 28 29# Examples 30 31### Basic drawing elements 32```python 33import drawsvg as draw 34 35d = draw.Drawing(200, 100, origin='center') 36 37# Draw an irregular polygon 38d.append(draw.Lines(-80, 45, 39 70, 49, 40 95, -49, 41 -90, -40, 42 close=False, 43 fill='#eeee00', 44 stroke='black')) 45 46# Draw a rectangle 47r = draw.Rectangle(-80, -50, 40, 50, fill='#1248ff') 48r.append_title("Our first rectangle") # Add a tooltip 49d.append(r) 50 51# Draw a circle 52d.append(draw.Circle(-40, 10, 30, 53 fill='red', stroke_width=2, stroke='black')) 54 55# Draw an arbitrary path (a triangle in this case) 56p = draw.Path(stroke_width=2, stroke='lime', fill='black', fill_opacity=0.2) 57p.M(-10, -20) # Start path at point (-10, -20) 58p.C(30, 10, 30, -50, 70, -20) # Draw a curve to (70, -20) 59d.append(p) 60 61# Draw text 62d.append(draw.Text('Basic text', 8, -10, -35, fill='blue')) # 8pt text at (-10, -35) 63d.append(draw.Text('Path text', 8, path=p, text_anchor='start', line_height=1)) 64d.append(draw.Text(['Multi-line', 'text'], 8, path=p, text_anchor='end', center=True)) 65 66# Draw multiple circular arcs 67d.append(draw.ArcLine(60, 20, 20, 60, 270, 68 stroke='red', stroke_width=5, fill='red', fill_opacity=0.2)) 69d.append(draw.Arc(60, 20, 20, 90, -60, cw=True, 70 stroke='green', stroke_width=3, fill='none')) 71d.append(draw.Arc(60, 20, 20, -60, 90, cw=False, 72 stroke='blue', stroke_width=1, fill='black', fill_opacity=0.3)) 73 74# Draw arrows 75arrow = draw.Marker(-0.1, -0.51, 0.9, 0.5, scale=4, orient='auto') 76arrow.append(draw.Lines(-0.1, 0.5, -0.1, -0.5, 0.9, 0, fill='red', close=True)) 77p = draw.Path(stroke='red', stroke_width=2, fill='none', 78 marker_end=arrow) # Add an arrow to the end of a path 79p.M(20, 40).L(20, 27).L(0, 20) # Chain multiple path commands 80d.append(p) 81d.append(draw.Line(30, 20, 0, 10, 82 stroke='red', stroke_width=2, fill='none', 83 marker_end=arrow)) # Add an arrow to the end of a line 84 85d.set_pixel_scale(2) # Set number of pixels per geometry unit 86#d.set_render_size(400, 200) # Alternative to set_pixel_scale 87d.save_svg('example.svg') 88d.save_png('example.png') 89 90# Display in Jupyter notebook 91#d.rasterize() # Display as PNG 92d # Display as SVG 93``` 94 95[![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example1.png)](https://github.com/cduck/drawsvg/blob/master/examples/example1.svg) 96 97### SVG-native animation with playback controls 98```python 99import drawsvg as draw 100 101d = draw.Drawing(400, 200, origin='center', 102 animation_config=draw.types.SyncedAnimationConfig( 103 # Animation configuration 104 duration=8, # Seconds 105 show_playback_progress=True, 106 show_playback_controls=True)) 107d.append(draw.Rectangle(-200, -100, 400, 200, fill='#eee')) # Background 108d.append(draw.Circle(0, 0, 40, fill='green')) # Center circle 109 110# Animation 111circle = draw.Circle(0, 0, 0, fill='gray') # Moving circle 112circle.add_key_frame(0, cx=-100, cy=0, r=0) 113circle.add_key_frame(2, cx=0, cy=-100, r=40) 114circle.add_key_frame(4, cx=100, cy=0, r=0) 115circle.add_key_frame(6, cx=0, cy=100, r=40) 116circle.add_key_frame(8, cx=-100, cy=0, r=0) 117d.append(circle) 118r = draw.Rectangle(0, 0, 0, 0, fill='silver') # Moving square 119r.add_key_frame(0, x=-100, y=0, width=0, height=0) 120r.add_key_frame(2, x=0-20, y=-100-20, width=40, height=40) 121r.add_key_frame(4, x=100, y=0, width=0, height=0) 122r.add_key_frame(6, x=0-20, y=100-20, width=40, height=40) 123r.add_key_frame(8, x=-100, y=0, width=0, height=0) 124d.append(r) 125 126# Changing text 127draw.native_animation.animate_text_sequence( 128 d, 129 [0, 2, 4, 6], 130 ['0', '1', '2', '3'], 131 30, 0, 1, fill='yellow', center=True) 132 133# Save as a standalone animated SVG or HTML 134d.save_svg('playback-controls.svg') 135d.save_html('playback-controls.html') 136 137# Display in Jupyter notebook 138#d.display_image() # Display SVG as an image (will not be interactive) 139#d.display_iframe() # Display as interactive SVG (alternative) 140#d.as_gif('orbit.gif', fps=10) # Render as a GIF image, optionally save to file 141#d.as_mp4('orbig.mp4', fps=60) # Render as an MP4 video, optionally save to file 142d.display_inline() # Display as interactive SVG 143``` 144 145[![Example animated image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/playback-controls.svg?sanitize=true)](https://github.com/cduck/drawsvg/blob/master/examples/playback-controls.svg) 146 147Note: GitHub blocks the playback controls. 148Download the above SVG and open it in a web browser to try. 149 150https://user-images.githubusercontent.com/2476062/221400434-1529d237-e9bf-4363-a143-0ece75cd349a.mp4 151 152### Patterns and gradients 153```python 154import drawsvg as draw 155 156d = draw.Drawing(1.5, 0.8, origin='center') 157 158# Background pattern (not supported by Cairo, d.rasterize() will not show it) 159pattern = draw.Pattern(width=0.13, height=0.23) 160pattern.append(draw.Rectangle(0, 0, .1, .1, fill='yellow')) 161pattern.append(draw.Rectangle(0, .1, .1, .1, fill='orange')) 162d.draw(draw.Rectangle(-0.75, -0.5, 1.5, 1, fill=pattern, fill_opacity=0.4)) 163 164# Create gradient 165gradient = draw.RadialGradient(0, 0.35, 0.7*10) 166gradient.add_stop(0.5/0.7/10, 'green', 1) 167gradient.add_stop(1/10, 'red', 0) 168 169# Draw a shape to fill with the gradient 170p = draw.Path(fill=gradient, stroke='black', stroke_width=0.002) 171p.arc(0, 0.35, 0.7, -30, -120, cw=False) 172p.arc(0, 0.35, 0.5, -120, -30, cw=True, include_l=True) 173p.Z() 174d.append(p) 175 176# Draw another shape to fill with the same gradient 177p = draw.Path(fill=gradient, stroke='red', stroke_width=0.002) 178p.arc(0, 0.35, 0.75, -130, -160, cw=False) 179p.arc(0, 0.35, 0, -160, -130, cw=True, include_l=True) 180p.Z() 181d.append(p) 182 183# Another gradient 184gradient2 = draw.LinearGradient(0.1, 0.35, 0.1+0.6, 0.35+0.2) 185gradient2.add_stop(0, 'green', 1) 186gradient2.add_stop(1, 'red', 0) 187d.append(draw.Rectangle(0.1, 0.15, 0.6, 0.2, 188 stroke='black', stroke_width=0.002, 189 fill=gradient2)) 190 191# Display 192d.set_render_size(w=600) 193d 194``` 195 196[![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example2.svg?sanitize=true)](https://github.com/cduck/drawsvg/blob/master/examples/example2.svg) 197 198### Duplicate geometry and clip paths 199```python 200import drawsvg as draw 201 202d = draw.Drawing(1.4, 1.4, origin='center') 203 204# Define clip path 205clip = draw.ClipPath() 206clip.append(draw.Rectangle(-.25, -.25, 1, 1)) 207 208# Draw a cropped circle 209circle = draw.Circle(0, 0, 0.5, 210 stroke_width='0.01', stroke='black', 211 fill_opacity=0.3, clip_path=clip) 212d.append(circle) 213 214# Make a transparent copy, cropped again 215g = draw.Group(opacity=0.5, clip_path=clip) 216# Here, circle is not directly appended to the drawing. 217# drawsvg recognizes that `Use` references `circle` and automatically adds 218# `circle` to the <defs></defs> section of the SVG. 219g.append(draw.Use(circle, 0.25, -0.1)) 220d.append(g) 221 222# Display 223d.set_render_size(400) 224#d.rasterize() # Display as PNG 225d # Display as SVG 226``` 227 228[![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example3.png)](https://github.com/cduck/drawsvg/blob/master/examples/example3.svg) 229 230### Organizing and duplicating drawing elements 231```python 232import drawsvg as draw 233 234d = draw.Drawing(300, 100) 235d.set_pixel_scale(2) 236 237# Use groups to contain other elements 238# Children elements of groups inherit the coordinate system (transform) 239# and attribute values 240group = draw.Group(fill='orange', transform='rotate(-20)') 241group.append(draw.Rectangle(0, 10, 20, 40)) # This rectangle will be orange 242group.append(draw.Circle(30, 40, 10)) # This circle will also be orange 243group.append(draw.Circle(50, 40, 10, fill='green')) # This circle will not 244d.append(group) 245 246# Use the Use element to make duplicates of elements 247# Each duplicate can be placed at an offset (x, y) location and any additional 248# attributes (like fill color) are inherited if the element didn't specify them. 249d.append(draw.Use(group, 80, 0, stroke='black', stroke_width=1)) 250d.append(draw.Use(group, 80, 20, stroke='blue', stroke_width=2)) 251d.append(draw.Use(group, 80, 40, stroke='red', stroke_width=3)) 252 253d.display_inline() 254``` 255 256[![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example8.png)](https://github.com/cduck/drawsvg/blob/master/examples/example8.svg) 257 258### Implementing other SVG tags 259```python 260import drawsvg as draw 261 262# Subclass DrawingBasicElement if it cannot have child nodes 263# Subclass DrawingParentElement otherwise 264# Subclass DrawingDef if it must go between <def></def> tags in an SVG 265class Hyperlink(draw.DrawingParentElement): 266 TAG_NAME = 'a' 267 def __init__(self, href, target=None, **kwargs): 268 # Other init logic... 269 # Keyword arguments to super().__init__() correspond to SVG node 270 # arguments: stroke_width=5 -> <a stroke-width="5" ...>...</a> 271 super().__init__(href=href, target=target, **kwargs) 272 273d = draw.Drawing(1, 1.2, origin='center') 274 275# Create hyperlink 276hlink = Hyperlink('https://www.python.org', target='_blank', 277 transform='skewY(-30)') 278# Add child elements 279hlink.append(draw.Circle(0, 0, 0.5, fill='green')) 280hlink.append(draw.Text('Hyperlink', 0.2, 0, 0, center=0.6, fill='white')) 281 282# Draw and display 283d.append(hlink) 284d.set_render_size(200) 285d 286``` 287 288[![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example4.png)](https://github.com/cduck/drawsvg/blob/master/examples/example4.svg) 289 290### Animation with the SVG Animate Tag 291```python 292import drawsvg as draw 293 294d = draw.Drawing(200, 200, origin='center') 295 296# Animate the position and color of circle 297c = draw.Circle(0, 0, 20, fill='red') 298# See for supported attributes: 299# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate 300c.append_anim(draw.Animate('cy', '6s', '-80;80;-80', 301 repeatCount='indefinite')) 302c.append_anim(draw.Animate('cx', '6s', '0;80;0;-80;0', 303 repeatCount='indefinite')) 304c.append_anim(draw.Animate('fill', '6s', 'red;green;blue;yellow', 305 calc_mode='discrete', 306 repeatCount='indefinite')) 307d.append(c) 308 309# Animate a black circle around an ellipse 310ellipse = draw.Path() 311ellipse.M(-90, 0) 312ellipse.A(90, 40, 360, True, True, 90, 0) # Ellipse path 313ellipse.A(90, 40, 360, True, True, -90, 0) 314ellipse.Z() 315c2 = draw.Circle(0, 0, 10) 316# See for supported attributes: 317# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate_motion 318c2.append_anim(draw.AnimateMotion(ellipse, '3s', 319 repeatCount='indefinite')) 320# See for supported attributes: 321# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate_transform 322c2.append_anim(draw.AnimateTransform('scale', '3s', '1,2;2,1;1,2;2,1;1,2', 323 repeatCount='indefinite')) 324d.append(c2) 325 326d.save_svg('animated.svg') # Save to file 327d # Display in Jupyter notebook 328``` 329 330[![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) 331 332### Interactive Widget 333```python 334import drawsvg as draw 335from drawsvg.widgets import DrawingWidget 336import hyperbolic.poincare.shapes as hyper # python3 -m pip install hyperbolic 337from hyperbolic import euclid 338 339# Patch the hyperbolic package for drawsvg version 2 340patch = lambda m: lambda self, **kw: m(self, draw, **kw) 341hyper.Circle.to_drawables = patch(hyper.Circle.toDrawables) 342hyper.Line.to_drawables = patch(hyper.Line.toDrawables) 343euclid.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) 344 345# Create drawing 346d = draw.Drawing(2, 2, origin='center', context=draw.Context(invert_y=True)) 347d.set_render_size(500) 348d.append(draw.Circle(0, 0, 1, fill='orange')) 349group = draw.Group() 350d.append(group) 351 352# Update the drawing based on user input 353click_list = [] 354def redraw(points): 355 group.children.clear() 356 for x1, y1 in points: 357 for x2, y2 in points: 358 if (x1, y1) == (x2, y2): continue 359 p1 = hyper.Point.fromEuclid(x1, y1) 360 p2 = hyper.Point.fromEuclid(x2, y2) 361 if p1.distanceTo(p2) <= 2: 362 line = hyper.Line.fromPoints(*p1, *p2, segment=True) 363 group.draw(line, hwidth=0.2, fill='white') 364 for x, y in points: 365 p = hyper.Point.fromEuclid(x, y) 366 group.draw(hyper.Circle.fromCenterRadius(p, 0.1), 367 fill='green') 368redraw(click_list) 369 370# Create interactive widget and register mouse events 371widget = DrawingWidget(d) 372@widget.mousedown 373def mousedown(widget, x, y, info): 374 if (x**2 + y**2) ** 0.5 + 1e-5 < 1: 375 click_list.append((x, y)) 376 redraw(click_list) 377 widget.refresh() 378@widget.mousemove 379def mousemove(widget, x, y, info): 380 if (x**2 + y**2) ** 0.5 + 1e-5 < 1: 381 redraw(click_list + [(x, y)]) 382 widget.refresh() 383widget 384``` 385 386![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example5.gif) 387 388Note: The above example currently only works in `jupyter notebook`, not `jupyter lab`. 389 390### Frame-by-Frame Animation 391```python 392import drawsvg as draw 393 394# Draw a frame of the animation 395def draw_frame(t): 396 d = draw.Drawing(2, 6.05, origin=(-1, -5)) 397 d.set_render_size(h=300) 398 d.append(draw.Rectangle(-2, -6, 4, 8, fill='white')) 399 d.append(draw.Rectangle(-1, 1, 2, 0.05, fill='brown')) 400 t = (t + 1) % 2 - 1 401 y = t**2 * 4 - 4 402 d.append(draw.Circle(0, y, 1, fill='lime')) 403 return d 404 405with draw.frame_animate_jupyter(draw_frame, delay=0.05) as anim: 406# Or: 407#with draw.animate_video('example6.gif', draw_frame, duration=0.05 408# ) as anim: 409 # Add each frame to the animation 410 for i in range(20): 411 anim.draw_frame(i/10) 412 for i in range(20): 413 anim.draw_frame(i/10) 414 for i in range(20): 415 anim.draw_frame(i/10) 416``` 417 418![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example6.gif) 419 420### Asynchronous Frame-based Animation in Jupyter 421```python 422# Jupyter cell 1: 423import drawsvg as draw 424from drawsvg.widgets import AsyncAnimation 425widget = AsyncAnimation(fps=10) 426widget 427# [Animation is displayed here (click to pause)] 428 429# Jupyter cell 2: 430global_variable = 'a' 431@widget.set_draw_frame # Animation above is automatically updated 432def draw_frame(secs=0): 433 # Draw something... 434 d = draw.Drawing(100, 40) 435 d.append(draw.Text(global_variable, 20, 0, 30)) 436 d.append(draw.Text('{:0.1f}'.format(secs), 20, 30, 30)) 437 return d 438 439# Jupyter cell 3: 440global_variable = 'b' # Animation above now displays 'b' 441``` 442 443![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example7.gif) 444 445Note: The above example currently only works in `jupyter notebook`, not `jupyter lab`. 446 447 448--- 449 450# Full-feature install 451Drawsvg may be either be installed with no dependencies (only SVG and SVG-native animation will work): 452```bash 453$ python3 -m pip install "drawsvg~=2.0" 454``` 455 456Or drawsvg may be installed with extra dependencies to support PNG, MP4, and GIF output: 457```bash 458$ python3 -m pip install "drawsvg[all]~=2.0" 459``` 460 461An additional required package, [Cairo](https://www.cairographics.org/download/), cannot be installed with pip and must be installed separately. When Cairo is installed, drawsvg can output PNG and other image formats in addition to SVG. Install it with your preferred package manager. Examples: 462 463**Ubuntu** 464 465```bash 466$ sudo apt install libcairo2 467``` 468 469**macOS** 470 471Using [homebrew](https://brew.sh/) (may require a Python version installed with `brew install python`): 472 473```bash 474$ brew install cairo 475``` 476 477**Any platform** 478 479Using [Anaconda](https://docs.conda.io/en/latest/miniconda.html) (may require Python and cairo installed in the same conda environment): 480 481```bash 482$ conda install -c anaconda cairo 483```