Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets
1# Drawsvg Quick Reference 2 3Repository: [https://github.com/cduck/drawsvg](https://github.com/cduck/drawsvg) 4 5 6```python 7import drawsvg as dw 8``` 9 10 11## Canvas and Document Structure 12 13```python 14d = dw.Drawing(width, height, origin=(0, 0), 15 context: drawsvg.types.Context = None, animation_config=None, 16 id_prefix='d', **svg_args) 17``` 18 19It is recommended to use a unique `id_prefix` for each svg if you embed multiple on a web page. 20 21```python 22d = dw.Drawing(400, 300, id_prefix='pic') 23``` 24 25 26## Basic Shapes 27 28### One Line 29 30```python 31dw.Line(sx, sy, ex, ey, **kwargs) 32``` 33 34```python 35line = dw.Line(30, 30, 90, 90, stroke='black') 36d.append(line) 37``` 38 39![svg](img/01_line.svg) 40 41 42### Multiple Lines 43 44This is SVG's `polyline` (but drawsvg renders as path with multiple L). 45 46```python 47dw.Lines(sx, sy, *points, close=False, **kwargs) 48``` 49 50```python 51lines = dw.Lines(10, 90, 10, 10, 80, 90, 80, 10, 52 fill='none', stroke='black') 53d.append(lines) 54``` 55 56![svg](img/01_multilines.svg) 57 58 59```python 60x = [30 + x*10 for x in range(20)] 61y = [80, 20]*10 62xy = [item for sublist in zip(x, y) for item in sublist] 63d.append(dw.Lines(*xy, stroke='black', stroke_width=5, fill='none')) 64``` 65 66![svg](img/01_multilines2.svg) 67 68 69### Polygon 70 71SVG `Polygon` is drawsvg `Lines` with `close=True`. 72 73```python 74polygon = dw.Lines(15, 10, 55, 10, 45, 20, 5, 20, 75 fill='red', stroke='black', close='true') 76star = dw.Lines(48, 16, 16, 96, 96, 48, 0, 48, 88, 96, 77 stroke='black', fill='none', close='true') 78d.append(star) 79``` 80 81![svg](img/01_polygon.svg) 82 83 84### Rectangle 85 86```python 87dw.Rectangle(x, y, width, height, **kwargs) 88``` 89 90```python 91# Black interior, no outline 92d.append(dw.Rectangle(10, 10, 90, 150)) 93# No interior, black outline 94d.append(dw.Rectangle(120, 10, 60, 120, 95 fill='none', stroke='black')) 96# Blue interior, thick semi-transparent red outline 97d.append(dw.Rectangle(210, 10, 75, 90, 98 fill='#0000ff', stroke='red', 99 stroke_width=7, stroke_opacity=0.5)) 100# Semi-transparent yellow interior, dashed green outline 101d.append(dw.Rectangle(300, 10, 105, 60, 102 fill='yellow', fill_opacity=0.5, 103 stroke='green', stroke_width=2, 104 stroke_dasharray='5,2')) 105``` 106 107![svg](img/01_rect.svg) 108 109Rounded corners: 110 111```python 112# Define both rx and ry 113d.append(dw.Rectangle(10, 10, 80, 180, rx='10', ry='10', 114 stroke='black', fill='none')) 115# If only one is given, it applies to both 116d.append(dw.Rectangle(110, 10, 80, 180, ry='20', 117 stroke='black', fill='none')) 118d.append(dw.Rectangle(210, 10, 80, 180, rx='40', 119 stroke='black', fill='none')) 120# Rx and ry unequal 121d.append(dw.Rectangle(310, 10, 80, 180, rx='30', ry='10', 122 stroke='black', fill='none')) 123d.append(dw.Rectangle(410, 10, 80, 180, rx='10', ry='30', 124 stroke='black', fill='none')) 125``` 126 127![svg](img/01_rectround.svg) 128 129 130### Circle 131 132```python 133dw.Circle(cx, cy, r, **kwargs) 134``` 135 136cx and cy point to circle's center, r refer to its radius 137 138```python 139d.append(dw.Circle(50, 50, 40)) 140d.append(dw.Circle(150, 50, 40, 141 stroke='black', fill='none')) 142d.append(dw.Circle(250, 50, 40, 143 stroke='black', fill='none', 144 stroke_width=15)) 145``` 146 147![svg](img/01_circ.svg) 148 149 150### Ellipse 151 152```python 153dw.Ellipse(cx, cy, rx, ry, **kwarg) 154``` 155(cx,cy) points to the center and (rx,ry) tells its radius 156 157```python 158d.append(dw.Ellipse(50, 50, 50, 30)) 159d.append(dw.Ellipse(160, 50, 50, 30, 160 stroke='black', fill='none')) 161d.append(dw.Ellipse(250, 50, 30, 45, 162 stroke='black',fill='none')) 163``` 164 165![svg](img/01_ellip.svg) 166 167 168 169## Color and Painting Properties 170 171For a full list, see [W3C specifications](https://www.w3.org/TR/SVG11/styling.html). 172 173 174### fill and stroke\_color 175 176Some color keyword names are: 177aqua, black, blue, fuchsia, gray, green, lime, maroon, navy, olive, purple, red, silver, teal, white, and yellow. 178 179Also supported is `#rrggbb`, `#rgb` (hexadecimal), or `rgb(R,G,B)` with 0-255 or with 0-100% for each value. 180 181```python 182c = ['red', '#9f9', '#9999ff', 'rgb(255,128,64)', 'rgb(60%,20%,60%)'] 183for i in range(5): 184 y = (i + 1)*10 185 d.append(dw.Line(10, y, 80, y, 186 stroke=c[i], stroke_width=5)) 187``` 188 189![svg](img/02_fsc.svg) 190 191 192### fill\_opacity and stroke\_opacity 193 194Value range from 0 = transparent to 1 = solid. 195 196```python 197for i in range(5): 198 y = (i + 1)*10 199 d.append(dw.Line(0, y, 290, y, 200 stroke='black', stroke_width=5, 201 stroke_opacity=i/5 + 0.1)) 202 d.append(dw.Rectangle(i*60, 70, 50, 50, 203 fill='red', fill_opacity=i/5+0.1)) 204``` 205 206![svg](img/02_foso.svg) 207 208 209### stroke\_dasharray 210 211```python 212# Nine-pixel dash, five-pixel gap 213d.append(dw.Line(10, 10, 100, 10, 214 stroke='black', stroke_width=2, 215 stroke_dasharray='9,5')) 216# Five-pixel dash, three-pixel gap, nine-pixel dash, two-pixel gap 217d.append(dw.Line(10, 20, 100, 20, 218 stroke='black', stroke_width=2, 219 stroke_dasharray='5,3,9,2')) 220# Odd number of entries alternates dashes and gaps 221d.append(dw.Line(10, 30, 100, 30, 222 stroke='black', stroke_width=2, 223 stroke_dasharray='9,3,5')) 224``` 225 226![svg](img/02_dash.svg) 227 228 229### stroke\_width 230 231```python 232for i in range(20): 233 d.append(dw.Line((i+1)*15, 10, (i+1)*15, 90, 234 stroke='black', stroke_width=abs(10-i)+1)) 235``` 236 237![svg](img/02_strokewdth.svg) 238 239 240### stroke\_linecap 241 242`stroke_linecap` can be set to `butt`, `round`, or `square`. 243Note that the latter two extend beyond the end coordinates. 244 245```python 246d.append(dw.Line(10, 15, 50, 15, 247 stroke='black', stroke_width=15, 248 stroke_linecap='butt')) 249d.append(dw.Line(10, 45, 50, 45, 250 stroke='black', stroke_width=15, 251 stroke_linecap='round')) 252d.append(dw.Line(10, 75, 50, 75, 253 stroke='black', stroke_width=15, 254 stroke_linecap='square')) 255# Guide lines 256d.append(dw.Lines(10, 0, 10, 100, stroke='#999')) 257d.append(dw.Lines(50, 0, 50, 100, stroke='#999')) 258``` 259 260![svg](img/02_linecap.svg) 261 262 263### stroke\_linejoin 264 265Define the way lines connect at a corner with `stroke-linejoin`: `miter` (pointed), `round`, or `bevel` (flat). 266 267```python 268d.append(dw.Line(0, 20, 300, 20, stroke='gray')) 269g = dw.Group(stroke_width=20, stroke='black', fill='none') 270g.append(dw.Lines(10, 80, 50, 20, 90, 80, 271 stroke_linejoin='miter')) 272g.append(dw.Lines(110, 80, 150, 20, 190, 80, 273 stroke_linejoin='round')) 274g.append(dw.Lines(210, 80, 250, 20, 290, 80, 275 stroke_linejoin='bevel')) 276d.append(g) 277``` 278 279![svg](img/02_join.svg) 280 281 282### stroke\_miterlimit 283 284When two line segments meet at a sharp angle and miter joins have been specified for `stroke-linejoin`, 285it is possible for the miter to extend far beyond the thickness of the line stroking the path. 286The `stroke-miterlimit` imposes a limit on the ratio of the miter length to the `stroke-width`. 287When the limit is exceeded, the join is converted from a miter to a bevel. 288(From [W3C doc](https://www.w3.org/TR/SVG11/painting.html#StrokeMiterlimitProperty)) 289 290```python 291d.append(dw.Line(0, 30, 300, 30, stroke='gray')) 292g = dw.Group(stroke_width=20, stroke='black', 293 fill='none', stroke_linejoin='miter') 294g.append(dw.Lines(10, 90, 40, 30, 70, 90)) 295g.append(dw.Lines(100, 90, 130, 30, 160, 90, 296 stroke_miterlimit=2.3)) 297g.append(dw.Lines(190, 90, 220, 30, 250, 90, 298 stroke_miterlimit=1)) 299d.append(g) 300``` 301 302![svg](img/02_mlimit.svg) 303 304 305## Path 306 307```python 308path = dw.Path(**kwargs) 309``` 310 311The following Path specifiers are also available as lowercase characters. 312In that case, their movements are relative to current location. 313 314 315### M: moveto 316 317```python 318path.M(x, y) 319``` 320 321Move to `x, y` (and draw nothing). 322 323 324### L: lineto 325 326```python 327path.L(x, y) 328``` 329 330Draw a straight line to `x, y`. 331 332```python 333g = dw.Group(stroke='black', fill='none') 334 335p = dw.Path() 336p.M(10, 10).L(100, 10) 337g.append(p) 338 339p = dw.Path() 340p.M(10, 20).L(100, 20).L(100, 50) 341g.append(p) 342 343p = dw.Path() 344p.M(40, 60).L(10, 60).L(40, 42) 345p.M(60, 60).L(90, 60).L(60, 42) 346g.append(p) 347 348d.append(g) 349``` 350 351![svg](img/03_pL.svg) 352 353 354### H: horizontal line 355 356```python 357path.H(x) 358``` 359 360Draw a horizontal line to the new `x` location. 361 362 363### V: vertical line 364 365```python 366path.V(y) 367``` 368 369Draw a vertical line to the new `y` location. 370 371```python 372p = dw.Path(stroke='black', fill='none') 373d.append(p.M(10, 10).H(100)) 374d.append(p.M(10, 20).H(100).V(50)) 375``` 376![svg](img/03_pHV.svg) 377 378 379### Q: quadratic Bézier curve (one control point) 380 381```python 382path.Q(x_ctl, y_ctl, x_end, y_end) 383``` 384 385Draw a quadratic Bézier curve from current location to `x_end, y_end` by means of `x_ctl, y_ctl`. 386 387```python 388# Curve only (left) 389p = dw.Path(stroke='black', fill='none', stroke_width=3) 390d.append(p.M(30, 75).Q(240, 30, 300, 120)) 391# With control point and construction lines 392d.append(dw.Use(p, 300, 0)) 393g = dw.Group(stroke='gray', fill='gray') 394g.append(dw.Circle(330, 75, 3)) 395g.append(dw.Circle(600, 120, 3)) 396g.append(dw.Circle(540, 30, 3)) 397g.append(dw.Line(330, 75, 540, 30)) 398g.append(dw.Line(540, 30, 600, 120)) 399g.append(dw.Line(330, 75, 600, 120, stroke_dasharray='5,5')) 400g.append(dw.Circle(435, 52.5, 3)) 401g.append(dw.Circle(570, 75, 3)) 402g.append(dw.Line(435, 52.5, 570, 75)) 403g.append(dw.Circle(502.5, 63.75, 4, fill='none')) 404d.append(g) 405``` 406 407![svg](img/03_pQ.svg) 408 409 410### T: smooth quadratic Bézier curve (generated control point) 411 412```python 413path.T(x, y) 414``` 415 416Draws a quadratic Bézier curve from the current point to (x, y). 417The control point is assumed to be the reflection of the control point on the previous command relative to the current point. 418If there is no previous command or if the previous command was not a Q, q, T or t, assume the control point is coincident with the current point. 419(From [W3C Doc](https://www.w3.org/TR/SVG11/paths.html#PathDataQuadraticBezierCommands)) 420 421```python 422# Curve sequence (left) 423p = dw.Path(stroke='black', fill='none', stroke_width=3) 424d.append(p.M(30, 60).Q(80, -10, 100, 60).Q(130, 25, 200, 40)) 425# With smooth continuation (right) 426p = dw.Path(stroke='black', fill='none', stroke_width=3, 427 transform='translate(200,0)') 428d.append(p.M(30, 60).Q(80, -10, 100, 60).T(200, 40)) 429``` 430 431![svg](img/03_pT.svg) 432 433 434### C: cubic Bézier curve (two control points) 435 436```python 437path.C(x_ctl_1, y_ctl_1, x_ctl_2, y_ctl_2, x_end, y_end) 438``` 439 440Draw a cubic Bézier curve by means of two control points (one for start and one for end). 441 442```python 443pnt_1 = (40, 50) 444pnt_2 = (110, 50) 445ctl_1_x = (10, 60, 110, 110, 60, 110) 446ctls_2 = ((140, 10), (90, 10), (40, 10), (40, 10), (90, 90), (40, 90)) 447 448for i in range(6): 449 trans = f'translate({i*100},0)' 450 p = dw.Path(stroke='black', fill='none', 451 stroke_width=3, transform=trans) 452 ctl_1 = (ctl_1_x[i], 10) 453 ctl_2 = ctls_2[i] 454 p.M(*pnt_1) 455 p.C(*ctl_1, *ctl_2, *pnt_2) 456 d.append(p) 457 g = dw.Group(stroke='gray', fill='gray', 458 stroke_width=1, transform=trans) 459 g.append(dw.Circle(*ctl_1, 2)) 460 g.append(dw.Circle(*ctl_2, 2)) 461 g.append(dw.Line(*pnt_1, *ctl_1)) 462 g.append(dw.Line(*pnt_2, *ctl_2)) 463 d.append(g) 464``` 465 466![svg](img/03_pC.svg) 467 468 469### S: smooth cubic Bézier (one control point) 470 471Similar to `T` in quadratic Bézier curve. The first control point is calculated as reflection of the previous second control point. 472 473```python 474path.S(x_ctl_2, y_ctl_2, x_end, y_end) 475``` 476 477```python 478pnt_1 = (30, 100) 479pnt_2 = (100, 100) 480pnt_3 = (200, 80) 481ctl_1 = (50, 30) 482ctl_2 = (70, 50) 483ctl_3 = (150, 40) 484 485p = dw.Path(stroke='black', fill='none', stroke_width=3) 486p.M(*pnt_1) 487p.C(*ctl_1, *ctl_2, *pnt_2) 488p.S(*ctl_3, *pnt_3) 489d.append(p) 490 491for pnt, ctl in zip((pnt_1, pnt_2, pnt_3), (ctl_1, ctl_2, ctl_3)): 492 d.append(dw.Circle(*pnt, 4)) 493 d.append(dw.Circle(*ctl, 2, stroke='gray', fill='gray')) 494 d.append(dw.Line(*pnt, *ctl, stroke='gray')) 495``` 496 497![svg](img/03_pS.svg) 498 499 500### A: elliptical Arc 501 502```python 503path.A(rx, ry, rot, largeArc, sweep, ex, ey) 504 505 rx, ry: radius 506 rot: x-axis rotation 507 largeArc: True or False 508 sweep: True (positive) or False (negative) angle 509 ex, ey: end point 510``` 511 512```python 513p = dw.Path(stroke='red') 514d.append(p.M(125, 75).A(100, 50, rot=0, large_arc=0, sweep=0, ex=225, ey=125)) 515p = dw.Path(stroke='blue') 516d.append(p.M(125, 75).A(100, 50, rot=0, large_arc=0, sweep=1, ex=225, ey=125)) 517p = dw.Path(stroke='rgb(0 80 255)',stroke_dasharray='5 3') 518d.append(p.M(125, 75).A(100, 50, rot=0, large_arc=1, sweep=0, ex=225, ey=125)) 519p = dw.Path(stroke='rgb(255 80 0)',stroke_dasharray='5 3') 520d.append(p.M(125, 75).A(100, 50, rot=0, large_arc=1, sweep=1, ex=225, ey=125)) 521``` 522 523![svg](img/03_pA.svg) 524 525 526### Z: closepath 527 528```python 529path.Z() 530``` 531 532Close the path. 533 534```python 535p = dw.Path(stroke='black', fill='none') 536d.append(p.M(10, 10).h(30).v(50).h(-30).Z()) 537d.append(p.M(50, 10).h(30).v(50).Z()) 538``` 539 540![svg](img/03_pZ.svg) 541 542 543## Text 544```python 545dw.Text(text, fontSize, x=None, y=None, *, center=False, 546 line_height=1, line_offset=0, path=None, 547 start_offset=None, path_args=None, tspan_args=None, 548 cairo_fix=True, **kwargs) 549``` 550 551### Fill and Outline 552 553Default is black as fill color and no outline. 554 555```python 556# Reference lines 557l = dw.Path(stroke='gray') 558l.M(20, 0).V(370) 559for i in range(1, 7): 560 l.M(10, i*60).H(500) 561d.append(l) 562 563d.append(dw.Text('Simplest Text', font_size=50, x=20, y=60)) 564d.append(dw.Text('Outline / Filled', font_size=50, x=20, y=120, stroke='black')) 565d.append(dw.Text('Too big stroke', font_size=50, x=20, y=180, stroke='black', stroke_width=5)) 566d.append(dw.Text('Outlined only', font_size=50, x=20, y=240, stroke='black', stroke_width=0.5, fill='none')) 567d.append(dw.Text('Outlined and colored', font_size=50, x=20, y=300, stroke='black', fill='red')) 568d.append(dw.Text('Colored fill only', font_size=50, x=20, y=360, fill='blue')) 569``` 570 571![svg](img/04_fill.svg) 572 573 574### Weight, Style, Decoration, Spacing 575 576```python 577d.append(dw.Text('bold', font_size=30, x=20, y=35, font_weight='bold')) 578d.append(dw.Text('italic', font_size=30, x=20, y=75, font_style='italic')) 579d.append(dw.Text('under', font_size=30, x=20, y=115, text_decoration='underline')) 580d.append(dw.Text('over', font_size=30, x=20, y=155, text_decoration='overline')) 581d.append(dw.Text('through', font_size=30, x=20, y=195, text_decoration='line-through')) 582d.append(dw.Text('normal word space', font_size=30, x=200, y=35)) 583d.append(dw.Text('more word space', font_size=30, x=200, y=75, word_spacing=10)) 584d.append(dw.Text('less word space', font_size=30, x=200, y=115, word_spacing=-5)) 585d.append(dw.Text('wide letter space', font_size=30, x=200, y=155, letter_spacing=8)) 586d.append(dw.Text('narrow letter space', font_size=30, x=200, y=195, letter_spacing=-2)) 587``` 588 589![svg](img/04_weight.svg) 590 591 592### Text Alignment 593 594Horizontal alignment (`text_anchor`) can be `'start'`, `'middle'` or `'end'`. 595 596Vertical alignment (`dominant_baseline`) can be `'auto'`, `'middle'` or `'hanging'` 597(and more, see [here](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/dominant-baseline)). 598 599```python 600d.append(dw.Line(75, 100, 75, 0, stroke='gray')) 601d.append(dw.Line(140, 30, 250, 30, stroke='gray')) 602d.append(dw.Line(140, 60, 250, 60, stroke='gray')) 603d.append(dw.Line(140, 90, 250, 90, stroke='gray')) 604d.append(dw.Text('Start', 24, 75, 30, text_anchor='start')) 605d.append(dw.Text('Middle', 24, 75, 60, text_anchor='middle')) 606d.append(dw.Text('End', 24, 75, 90, text_anchor='end')) 607d.append(dw.Text('Auto', 24, 150, 30, dominant_baseline='auto')) 608d.append(dw.Text('Middle', 24, 150, 60, dominant_baseline='middle')) 609d.append(dw.Text('Hanging', 24, 150, 90, dominant_baseline='hanging')) 610``` 611 612![svg](img/04_align.svg) 613 614 615### TSpan 616 617Continues a `Text` element. 618 619```python 620txt = dw.Text('Switch among ', 24, 10, 40) 621txt.append(dw.TSpan('italic', font_style='italic')) 622txt.append(dw.TSpan(', normal, and ')) 623txt.append(dw.TSpan('bold', font_weight='bold')) 624txt.append(dw.TSpan(' text.')) 625d.append(txt) 626``` 627 628![svg](img/04_tspan.svg) 629 630 631```python 632txt = dw.Text('F', 24, 10, 30) 633txt.append(dw.TSpan('a', dy=5)) 634txt.append(dw.TSpan('l', dy=31, dx=21)) 635txt.append(dw.TSpan('l', dy=89, dx=54)) 636d.append(txt) 637``` 638 639![svg](img/04_tspan2.svg) 640 641The same could be achieved by a list of dx/dy values: 642 643```python 644d.append(dw.Text('Fall', 24, 10, 30, 645 dx='0,0,21,54', dy='0,5,21,54')) 646``` 647 648![svg](img/04_tspan3.svg) 649 650 651### Rotate 652 653Either one angle (degrees), or a list which is applied to all characters. 654If the list is smaller than the number of characters, the last angle persists. 655 656```python 657d.append(dw.Text('Rotate', 20, 20, 20, letter_spacing=20, rotate='90')) 658d.append(dw.Text('Rotate', 20, 20, 80, letter_spacing=20, rotate='0 90 180 270')) 659``` 660 661![svg](img/04_rot.svg) 662 663`TSpan` can also be used: 664 665```python 666import random 667random.seed(1) 668 669txt = dw.Text('', 20, 20, 50, letter_spacing=20) 670txt.append(dw.TSpan('R', rotate=random.randrange(360))) 671txt.append(dw.TSpan('OT', rotate='50 20')) 672rotate = ' '.join([str(random.randrange(360)) for i in range(3)]) 673txt.append(dw.TSpan('ATE', rotate=rotate)) 674d.append(txt) 675``` 676 677![svg](img/04_rot2.svg) 678 679 680### Setting Text Length 681 682```python 683s = 'Two words' 684d.append(dw.Text(s, 20, 20, 30, textLength=250, lengthAdjust='spacing')) 685d.append(dw.Text(s, 20, 20, 70, textLength=250, lengthAdjust='spacingAndGlyphs')) 686d.append(dw.Text(s+' (normal length)', 20, 20, 110)) 687d.append(dw.Text(s, 20, 20, 150, textLength=80, lengthAdjust='spacing')) 688d.append(dw.Text(s, 20, 20, 190, textLength=80, lengthAdjust='spacingAndGlyphs')) 689 690d.append(dw.Line(20, 10, 20, 195, stroke='gray')) 691d.append(dw.Line(270, 80, 270, 10, stroke='gray')) 692d.append(dw.Line(100, 130, 100, 195, stroke='gray')) 693``` 694 695![svg](img/04_len.svg) 696 697 698### Text on a Path 699 700```python 701curve_path = dw.Path(stroke='gray', fill='none') 702curve_path.M(30, 50).C(50, 20, 70, 20, 120, 50).S(150, 10, 200, 50) 703 704round_corner = dw.Path(stroke='gray', fill='none') 705round_corner.M(250, 30).L(300, 30).A(30, 30, 0, 0, 1, 330, 60).L(330, 110) 706 707sharp_corner = dw.Path(stroke='gray', fill='none') 708sharp_corner.M(30, 110).L(100, 110).L(100, 160) 709 710discontinuous = dw.Path(stroke='gray', fill='none') 711discontinuous.M(150, 110).A(40, 30, 0, 1, 0, 230, 110).M(250, 110).L(270, 140) 712 713center_curve = dw.Path(stroke='gray', fill='none') 714center_curve.M(330, 130).L(330, 160).A(30, 30, 0, 0, 1, 300, 180).L(200, 180) 715 716d.append(curve_path) 717d.append(round_corner) 718d.append(sharp_corner) 719d.append(discontinuous) 720d.append(center_curve) 721 722t_cp = dw.Text('Following a cubic Bézier curve', 14, path=curve_path) 723t_rc = dw.Text("Going 'round the bend", 14, path=round_corner) 724t_sc = dw.Text('Making a quick turn', 14, path=sharp_corner) 725t_dc = dw.Text('Text along a broken path', 14, path=discontinuous) 726t_ct = dw.Text('centered', 14, path=center_curve, offset='50%', text_anchor='middle') 727 728d.append(t_cp) 729d.append(t_rc) 730d.append(t_sc) 731d.append(t_dc) 732d.append(t_ct) 733``` 734 735![svg](img/04_path.svg) 736 737 738### Multi Line Text 739 740This is a particular feature of drawsvg: A list of strings as input for Text() 741is rendered as multi-line text. 742 743```python 744tl = ['this is', 'a', 'multiline text', 'given as a', 'list'] 745d.append(dw.Text(tl, 14, 50, 20, text_anchor='middle')) 746 747ts = 'this is\na\nmultiline text\ngiven as a\nstring' 748d.append(dw.Text(ts, 14, 150, 20, text_anchor='middle')) 749``` 750 751![svg](img/04_multiline_text.svg) 752 753 754### Fonts 755 756Specify fonts via `font_family`. 757 758```python 759d.append(dw.Text('Some text in Times New Roman.', 30, 10, 35, font_family='Times New Roman')) 760d.append(dw.Text('Some text in Arial Black.', 30, 10, 75, font_family='Arial Black')) 761d.append(dw.Text('Some text in Georgia.', 30, 10, 115, font_family='Georgia')) 762``` 763 764![png](img/04_fonts1.png) 765 766Specify a default font. 767 768```python 769d = dw.Drawing(600, 120, font_family='Times New Roman') 770d.append(dw.Text('Some text in global setting (Times New Roman).', 30, 10, 35)) 771d.append(dw.Text('Some text in Arial Black.', 30, 10, 75, font_family='Arial Black')) 772d.append(dw.Text('Some text in Georgia.', 30, 10, 115, font_family='Georgia')) 773``` 774 775![png](img/04_fonts2.png) 776 777 778 779## Gradient, Clip, Mask 780 781### Linear Gradient 782 783```python 784gradient = dw.LinearGradient(x1, y1, x2, y2, gradientUnits='userSpaceOnUse', **kwargs) 785gradient.add_stop(offset, color, opacity=None) 786``` 787 788```python 789grad = dw.LinearGradient(150, 0, 0, 0) 790grad.add_stop(0, 'green') 791grad.add_stop(1, 'yellow') 792d.append(dw.Rectangle(10, 10, 150, 60, 793 stroke='black', fill=grad)) 794``` 795 796![svg](img/05_lingrad.svg) 797 798 799### Radial Gradient 800 801```python 802gradient = dw.RadialGradient(cx, cy, r, **kwargs) 803gradient.add_stop(offset, color, opacity=None) 804``` 805 806```python 807gradient = dw.RadialGradient(200, 100, 100) 808gradient.add_stop(0, 'green', 1) 809gradient.add_stop(1, 'orange', 1) 810bg = dw.Rectangle(x=0, y=0, width='100%', height='100%', fill=gradient) 811d.append(bg) 812``` 813 814![svg](img/05_radgrad.svg) 815 816 817### Clip 818 819```python 820clip_name = dw.ClipPath() 821``` 822 823To add shape as Clip, use `.append()` method. 824To apply Clip, fill `clip_path` argument with `clip_name`. 825 826```python 827# Show both shapes as they are 828d.append(dw.Rectangle(100, 100, 100, 100, 829 stroke='gray', fill='none')) 830d.append(dw.Circle(100, 100, 100, 831 fill='none', stroke='gray', stroke_dasharray='5 5')) 832# Apply rect as clip to circle 833clip = dw.ClipPath() 834clip.append(dw.Rectangle(100, 100, 100, 100)) 835d.append(dw.Circle(100, 100, 100, 836 fill='cyan', clip_path=clip)) 837``` 838 839![svg](img/05_clip.svg) 840 841Another example: 842 843```python 844# Draw a random path in the left half of the canvas 845p = dw.Path(stroke='black', stroke_width=2, fill='none') 846p.M(150, 150) 847import random 848random.seed(1) 849for i in range(40): 850 p.L(random.randint(0, 300), random.randint(0, 200)) 851d.append(p) 852 853# Circle as clipping shape 854circ = dw.Circle(150, 100, 75) 855c = dw.ClipPath() 856c.append(circ) 857 858# Repeat lines in the right half and apply clipping 859d.append(dw.Use(p, 300, 0, clip_path=c)) 860``` 861 862![svg](img/05_clip2.svg) 863 864Complex clip path: 865 866```python 867curve1 = dw.Path(stroke='black', stroke_width=1, stroke_dasharray='3 2', fill='none') 868curve1.M(5, 55).C(25, 5, 45, -25, 75, 55).C(85, 85, 20, 105, 40, 55).Z() 869 870curveClip = dw.ClipPath() 871curveClip.append(dw.Use(curve1, 0, 0)) 872 873text1 = dw.Text('CLIP', 48, 20, 20, font_weight='bold', transform='rotate(60)', 874 stroke='black', stroke_width=1, stroke_dasharray='3 2', fill='none') 875textClip = dw.ClipPath() 876textClip.append(dw.Use(text1, 0, 0)) 877 878shapes = dw.Group() 879shapes.append(dw.Rectangle(0, 50, 90, 60, fill='#999')) 880shapes.append(dw.Circle(25, 25, 25, fill='#666')) 881shapes.append(dw.Lines(30, 0, 80, 0, 80, 100, close='true', fill='#ccc')) 882 883# draw shapes with clip path 884d.append(dw.Use(shapes, 0, 0, clip_path=curveClip)) 885 886# show clip path 887g = dw.Group(transform='translate(100,0)') 888g.append(shapes) 889g.append(dw.Use(curve1, 0, 0)) 890d.append(g) 891 892# draw shapes with text as clip path 893d.append(dw.Use(shapes, 0, 150, clip_path=textClip)) 894 895# show text clip path 896g = dw.Group(transform='translate(100,150)') 897g.append(shapes) 898g.append(dw.Use(text1, 0, 0)) 899d.append(g) 900``` 901 902![svg](img/05_clip3.svg) 903 904 905### Mask 906 907```python 908mask_name = dw.Mask() 909``` 910 911The transparency of the masking object is transfered to the masked object. 912Opaque pixels of the mask produce opaque pixels of the masked object. 913Transparent parts of the mask make the corresponding parts of the masked object invisible. 914 915```python 916gradient = dw.LinearGradient(*[0,0], *[1,0], gradientUnits='objectBoundingBox') 917gradient.add_stop(0, 'white') 918gradient.add_stop(1, 'black') 919 920mask = dw.Mask() 921box = dw.Rectangle(30, 0, 100, 100, fill=gradient) 922mask.append(box) 923 924# Initial shape 925rect = dw.Rectangle(0, 0, 200, 100, 926 fill='cyan', stroke='blue', stroke_width=2) 927d.append(rect) 928 929# After mask 930rect = dw.Rectangle(0, 0, 200, 100, 931 fill='pink', stroke='red', stroke_width=2, 932 mask=mask) 933d.append(rect) 934``` 935 936![svg](img/05_mask.svg) 937 938Mask using opaque colors: 939 940```python 941# Define the masks 942redmask = dw.Mask(maskContentUnits='objectBoundingBox') 943redmask.append(dw.Rectangle(0, 0, 1, 1, fill='#f00')) 944greenmask = dw.Mask(maskContentUnits='objectBoundingBox') 945greenmask.append(dw.Rectangle(0, 0, 1, 1, fill='#0f0')) 946bluemask = dw.Mask(maskContentUnits='objectBoundingBox') 947bluemask.append(dw.Rectangle(0, 0, 1, 1, fill='#00f')) 948whitemask = dw.Mask(maskContentUnits='objectBoundingBox') 949whitemask.append(dw.Rectangle(0, 0, 1, 1, fill='#fff')) 950 951# Display the colors 952d.append(dw.Rectangle(10, 10, 50, 50, fill='#f00')) 953d.append(dw.Rectangle(70, 10, 50, 50, fill='#0f0')) 954d.append(dw.Rectangle(130, 10, 50, 50, fill='#00f')) 955d.append(dw.Rectangle(190, 10, 50, 50, fill='#fff', stroke='black')) 956 957# Mask 958g = dw.Group(mask=redmask) 959g.append(dw.Circle(35,115,25,fill='black')) 960g.append(dw.Text('Red',14,35,80,text_anchor='middle')) 961d.append(g) 962g = dw.Group(mask=greenmask) 963g.append(dw.Circle(95, 115, 25, fill='black')) 964g.append(dw.Text('Green', 14, 95, 80, text_anchor='middle')) 965d.append(g) 966g = dw.Group(mask=bluemask) 967g.append(dw.Circle(155, 115, 25, fill='black')) 968g.append(dw.Text('Blue', 14, 155, 80, text_anchor='middle')) 969d.append(g) 970g = dw.Group(mask=whitemask) 971g.append(dw.Circle(215, 115, 25, fill='black')) 972g.append(dw.Text('White', 14, 215, 80, text_anchor='middle')) 973d.append(g) 974``` 975 976![svg](img/05_mask2.svg) 977 978Mask alpha using opacity only: 979 980```python 981fullmask = dw.Mask(maskContentUnits='objectBoundingBox') 982fullmask.append(dw.Rectangle(0, 0, 1, 1, fill_opacity=1.0, fill='white')) 983three_fourths = dw.Mask(maskContentUnits='objectBoundingBox') 984three_fourths.append(dw.Rectangle(0, 0, 1, 1, fill_opacity=0.75, fill='white')) 985one_half = dw.Mask(maskContentUnits='objectBoundingBox') 986one_half.append(dw.Rectangle(0, 0, 1, 1, fill_opacity=0.5, fill='white')) 987one_fourth = dw.Mask(maskContentUnits='objectBoundingBox') 988one_fourth.append(dw.Rectangle(0, 0, 1, 1, fill_opacity=0.25, fill='white')) 989 990g = dw.Group(mask=fullmask) 991g.append(dw.Circle(35, 35, 25)) 992g.append(dw.Text('100%', 14, 35, 80, text_anchor='middle')) 993d.append(g) 994g = dw.Group(mask=three_fourths) 995g.append(dw.Circle(95, 35, 25)) 996g.append(dw.Text('50%', 14, 95, 80, text_anchor='middle')) 997d.append(g) 998g = dw.Group(mask=one_half) 999g.append(dw.Circle(155, 35, 25)) 1000g.append(dw.Text('50%', 14, 155, 80, text_anchor='middle')) 1001d.append(g) 1002g = dw.Group(mask=one_fourth) 1003g.append(dw.Circle(215, 35, 25)) 1004g.append(dw.Text('25%', 14, 215, 80, text_anchor='middle')) 1005d.append(g) 1006``` 1007 1008![svg](img/05_mask3.svg) 1009 1010 1011## Group, Use, Defs, Image 1012 1013### Group 1014 1015```python 1016dw.Group(**kwargs) 1017``` 1018 1019Any style specified in the g tag will apply to all child elements in the group. 1020 1021```python 1022g_house = dw.Group(id='house', fill='none', stroke='black') 1023g_house.append(dw.Rectangle(6, 50, 60, 60)) 1024g_house.append(dw.Lines(6, 50, 36, 9, 66, 50)) 1025g_house.append(dw.Lines(36, 110, 36, 80, 50, 80, 50, 110)) 1026d.append(g_house) 1027 1028g_man = dw.Group(id='man', fill='none', stroke='blue') 1029g_man.append(dw.Circle(85, 56, 10)) 1030g_man.append(dw.Line(85, 66, 85, 80)) 1031g_man.append(dw.Lines(76, 104, 85, 80, 94, 104)) 1032g_man.append(dw.Lines(76, 70, 85, 76, 94, 70)) 1033d.append(g_man) 1034 1035g_woman = dw.Group(id='woman', fill='none', stroke='red') 1036g_woman.append(dw.Circle(110, 56, 10)) 1037g_woman.append(dw.Lines(110, 66, 110, 80, 100, 90, 120, 90, 110, 80)) 1038g_woman.append(dw.Line(104, 104, 108, 90)) 1039g_woman.append(dw.Line(112, 90, 116, 104)) 1040g_woman.append(dw.Lines(101, 70, 110, 76, 119, 70)) 1041d.append(g_woman) 1042``` 1043 1044![svg](img/06_group.svg) 1045 1046 1047### Use 1048 1049```python 1050dw.Use(other_elem, x, y, **kwargs) 1051``` 1052 1053```python 1054g_house = dw.Group(id='house', fill='none', stroke='black') 1055g_house.append(dw.Rectangle(6, 50, 60, 60)) 1056g_house.append(dw.Lines(6, 50, 36, 9, 66, 50)) 1057g_house.append(dw.Lines(36, 110, 36, 80, 50, 80, 50, 110)) 1058d.append(g_house) 1059 1060# Use id which is set 1061d.append(dw.Use('house', 100, 50)) 1062# Or use variable name 1063d.append(dw.Use(g_house, 150, 20)) 1064``` 1065 1066![svg](img/06_use.svg) 1067 1068 1069### Defs 1070 1071Elements that are not appended to the drawing but are referenced by other elements will automatically be included in `<defs></defs>`. 1072([source](https://github.com/cduck/drawsvg/issues/46)) 1073 1074```python 1075d = dw.Drawing(200, 200, id_prefix='defs') 1076 1077# Do not append `bond` to the drawing 1078bond = dw.Line(0, 0, 10, 10, stroke='black') 1079 1080# `bond` is automatically added into <defs> 1081# A default `id` is generated if one isn't set 1082d.append(dw.Use(bond, 20, 50)) 1083d.append(dw.Use(bond, 50, 50)) 1084d.append(dw.Use(bond, 80, 50)) 1085 1086print(d.as_svg()) 1087``` 1088 1089Output: 1090 1091```svg 1092<?xml version="1.0" encoding="UTF-8"?> 1093<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 1094 width="200" height="200" viewBox="0 0 200 200"> 1095<defs> 1096<path d="M0,0 L10,10" stroke="black" id="defs0" /> 1097</defs> 1098<use xlink:href="#defs0" x="20" y="50" /> 1099<use xlink:href="#defs0" x="50" y="50" /> 1100<use xlink:href="#defs0" x="80" y="50" /> 1101</svg> 1102``` 1103 1104 1105### Image 1106 1107```python 1108dw.Image(x, y, width, height, path=None, data=None, 1109 embed=False, mimeType=None, **kwargs) 1110``` 1111 1112```python 1113d.append(dw.Image(0, 0, 200, 200, 'example1.png', embed=True)) 1114``` 1115 1116![svg](img/06_imag.svg) 1117 1118 1119## Transformations 1120 1121### Translate 1122 1123```python 1124transform = 'translate(x,y)' 1125``` 1126 1127This attribute can be added to many objects. Simple example: 1128 1129```python 1130d.append(dw.Rectangle(0, 0, 40, 40)) 1131d.append(dw.Rectangle(0, 0, 40, 40, fill='red', transform='translate(50,50)')) 1132``` 1133 1134![svg](img/07_trans.svg) 1135 1136 1137### Scale 1138 1139```python 1140transform = 'scale(x_mult[, y_mult])' 1141``` 1142 1143Note that scaling also affects stroke width. 1144 1145```python 1146square = dw.Rectangle(0, 0, 40, 40, fill='none', stroke_width=2) 1147d.append(dw.Use(square, 10, 10, stroke='black')) 1148d.append(dw.Use(square, 10, 10, stroke='red', transform='scale(2)')) 1149``` 1150 1151![svg](img/07_scale.svg) 1152 1153It is possible to specify x and y scale separately: 1154 1155```python 1156square = dw.Rectangle(0, 0, 40, 40, fill='none', stroke_width=2) 1157d.append(dw.Use(square, 10, 10, stroke='black')) 1158d.append(dw.Use(square, 10, 10, stroke='red', transform='scale(3,1.5)')) 1159``` 1160 1161![svg](img/07_scale2.svg) 1162 1163Scaling around a center point: 1164 1165```python 1166# Center of scaling: (100, 100) 1167d.append(dw.Circle(100, 100, 4, fill='black')) 1168# Non-scaled rectangle 1169rect = dw.Rectangle(70, 80, 60, 40, stroke='black', fill='none') 1170d.append(rect) 1171# Scaled rectangles 1172d.append(dw.Use(rect, 0, 0, transform='translate(-100,-100) scale(2)', stroke_width=0.5)) 1173d.append(dw.Use(rect, 0, 0, transform='translate(-150,-150) scale(2.5)', stroke_width=0.4)) 1174d.append(dw.Use(rect, 0, 0, transform='translate(-200,-200) scale(3)', stroke_width=0.33)) 1175``` 1176 1177![svg](img/07_scalcent.svg) 1178 1179 1180### Rotate 1181 1182```python 1183transform = 'rotate(angle,cx=0,cy=0)' 1184``` 1185 1186`angle` counts clockwise in degrees. 1187`cx`/`cy` are the center of rotation. 1188 1189```python 1190# Show frame border 1191d.append(dw.Rectangle(0, 0, 200, 200, stroke='gray', fill='none')) 1192# Rotation is around (0, 0) 1193d.append(dw.Rectangle(70, 30, 40, 40, fill='silver')) 1194d.append(dw.Rectangle(70, 30, 40, 40, fill='gray', transform='rotate(22.5)')) 1195d.append(dw.Rectangle(70, 30, 40, 40, fill='black', transform='rotate(45)')) 1196``` 1197 1198![svg](img/07_rota.svg) 1199 1200```python 1201# Center of rotation 1202d.append(dw.Circle(100, 100, 3, fill='black')) 1203# Non-rotated arrow 1204arrow = dw.Group(id='arrow') 1205arrow.append(dw.Line(110, 100, 160, 100)) 1206arrow.append(dw.Lines(160, 100, 155, 95, 155, 105)) 1207d.append(dw.Use(arrow, 0, 0, stroke='black', fill='black')) 1208# Rotated arrows 1209g = dw.Group(stroke='red', fill='red') 1210g.append(dw.Use(arrow, 0, 0, transform='rotate (60,100,100)')) 1211g.append(dw.Use(arrow, 0, 0, transform='rotate (-90,100,100)')) 1212g.append(dw.Use(arrow, 0, 0, transform='rotate (-150,100,100)')) 1213d.append(g) 1214``` 1215 1216![svg](img/07_rota2.svg) 1217 1218 1219### Skew 1220 1221```python 1222transform = 'skewX(angle)' 1223transform = 'skewY(angle)' 1224``` 1225 1226```python 1227g = dw.Group(stroke='gray', stroke_dasharray='4 4') 1228g.append(dw.Line(0, 0, 200, 0)) 1229g.append(dw.Line(20, 0, 20, 90)) 1230g.append(dw.Line(120, 0, 120, 90)) 1231d.append(g) 1232 1233h = dw.Group(transform='translate(20,0)') 1234h1 = dw.Group(transform='skewX(30)') 1235h1.append(dw.Lines(50, 0, 0, 0, 0, 50, 1236 stroke='black', fill='none', stroke_width=2)) 1237h1.append(dw.Text('skewX', 16, 0, 60)) 1238h.append(h1) 1239d.append(h) 1240 1241i = dw.Group(transform='translate(120,0)') 1242i1 = dw.Group(transform='skewY(30)') 1243i1.append(dw.Lines(50, 0, 0, 0, 0, 50, 1244 stroke='black', fill='none', stroke_width=2)) 1245i1.append(dw.Text('skewY', 16, 0, 60)) 1246i.append(i1) 1247d.append(i) 1248``` 1249 1250![svg](img/07_skew.svg) 1251 1252 1253### Cartesian Coordinates 1254 1255A drawing which can be translated to Cartesian coordinates 1256(where y-coordinates increase upward, not downward) 1257by setting the translate-y value to the drawing's height, and also applying `scale(1,-1)`. 1258 1259Trapezoid with origin to top left (the default): 1260 1261```python 1262d.append(dw.Lines(0, 100, 0, 0, 100, 0, 1263 fill='none', stroke='green', stroke_width=2)) 1264d.append(dw.Lines(40, 40, 100, 40, 70, 70, 40, 70, 1265 fill='silver', stroke='black', stroke_width=2)) 1266d.append(dw.Text('downward y', 12, 5, 95)) 1267``` 1268 1269![svg](img/07_cart1.svg) 1270 1271Translated origin to bottom left and upward-y: 1272 1273```python 1274g = dw.Group(transform='translate(0,100) scale(1,-1)') 1275g.append(dw.Lines(0, 100, 0, 0, 100, 0, 1276 fill='none', stroke='green', stroke_width=2)) 1277g.append(dw.Lines(40, 40, 100, 40, 70, 70, 40, 70, 1278 fill='silver', stroke='black', stroke_width=2)) 1279g.append(dw.Text('upward y', 12, 5, 95)) 1280d.append(g) 1281``` 1282 1283![svg](img/07_cart2.svg) 1284 1285Alternatively, apply `scale(1,-1)` to the whole drawing: 1286 1287```python 1288d = dw.Drawing(100, 100, id_prefix='cart3', transform='scale(1,-1)') 1289d.append(dw.Lines(0, 100, 0, 0, 100, 0, 1290 fill='none', stroke='green', stroke_width=2)) 1291d.append(dw.Lines(40, 40, 100, 40, 70, 70, 40, 70, 1292 fill='silver', stroke='black', stroke_width=2)) 1293d.append(dw.Text('upward y', 12, 5, 95)) 1294``` 1295 1296![svg](img/07_cart3.svg) 1297 1298 1299## Credits 1300 1301Written by joachim heintz 2023. Edited by Casey Duckering. 1302 1303Most examples are based on J. David Eisenberg, SVG Essentials, O'Reilly 2002. 1304 1305Thanks to [Ahmad Aufar Husaini](https://github.com/aufarah) for his fork (draw2Svg) and for providing some documentation [here](https://draw2svg.netlify.app/) (some examples are used in this Quick Reference). 1306 1307Thanks to [Casey Duckering](https://github.com/cduck) for drawsvg and many helpful explanations on its [discussion page](https://github.com/cduck/drawsvg/discussions).