svgpathtoolss/README.ipynb

754 lines
28 KiB
Plaintext
Raw Normal View History

2016-07-06 04:51:11 +00:00
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# svgpathtools\n",
"\n",
"svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.\n",
"\n",
"## Features\n",
"\n",
"svgpathtools contains functions designed to **easily read, write and display SVG files** as well as *a large selection of geometrically\\-oriented tools* to **transform and analyze path elements**.\n",
"\n",
"Additionally, the submodule *bezier.py* contains tools for for working with general **nth order Bezier curves stored as n-tuples**.\n",
"\n",
"Some included tools:\n",
"\n",
"- **read**, **write**, and **display** SVG files containing Path (and other) SVG elements\n",
"- convert Bézier path segments to **numpy.poly1d** (polynomial) objects\n",
"- convert polynomials (in standard form) to their Bézier form\n",
"- compute **tangent vectors** and (right-hand rule) **normal vectors**\n",
"- compute **curvature**\n",
"- break discontinuous paths into their **continuous subpaths**.\n",
"- efficiently compute **intersections** between paths and/or segments\n",
"- find a **bounding box** for a path or segment\n",
"- **reverse** segment/path orientation\n",
"- **crop** and **split** paths and segments\n",
"- **smooth** paths (i.e. smooth away kinks to make paths differentiable)\n",
"- **transition maps** from path domain to segment domain and back (T2t and t2T)\n",
"- compute **area** enclosed by a closed path\n",
"- compute **arc length**\n",
"- compute **inverse arc length**\n",
"- convert RGB color tuples to hexadecimal color strings and back\n",
"\n",
"## Prerequisites\n",
"- **numpy**\n",
"- **svgwrite**\n",
"\n",
"## Setup\n",
"\n",
"If not already installed, you can **install the prerequisites** using pip.\n",
"\n",
"```bash\n",
"$ pip install numpy\n",
"```\n",
"\n",
"```bash\n",
"$ pip install svgwrite\n",
"```\n",
"\n",
"Then **install svgpathtools**:\n",
"```bash\n",
"$ pip install svgpathtools\n",
"``` \n",
" \n",
"### Alternative Setup \n",
"You can download the source from Github and install by using the command (from inside the folder containing setup.py):\n",
"\n",
"```bash\n",
"$ python setup.py install\n",
"```\n",
"\n",
"## Credit where credit's due\n",
"Much of the core of this module was taken from [the svg.path (v2.0) module](https://github.com/regebro/svg.path). Interested svg.path users should see the compatibility notes at bottom of this readme.\n",
"\n",
"## Basic Usage\n",
"\n",
"### Classes\n",
"The svgpathtools module is primarily structured around four path segment classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``. There is also a fifth class, ``Path``, whose objects are sequences of (connected or disconnected<sup id=\"a1\">[1](#f1)</sup>) path segment objects.\n",
"\n",
"* ``Line(start, end)``\n",
"\n",
"* ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See docstring for a detailed explanation of these parameters\n",
"\n",
"* ``QuadraticBezier(start, control, end)``\n",
"\n",
"* ``CubicBezier(start, control1, control2, end)``\n",
"\n",
"* ``Path(*segments)``\n",
"\n",
"See the relevant docstrings in *path.py* or the [official SVG specifications](<http://www.w3.org/TR/SVG/paths.html>) for more information on what each parameter means.\n",
"\n",
"<u id=\"f1\">1</u> Warning: Some of the functionality in this library has not been tested on discontinuous Path objects. A simple workaround is provided, however, by the ``Path.continuous_subpaths()`` method. [↩](#a1)"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"from __future__ import division, print_function"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),\n",
" Line(start=(200+300j), end=(250+350j)))\n",
"Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),\n",
" Line(start=(200+300j), end=(250+350j)))\n",
"True\n",
"M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0\n",
"True\n"
]
}
],
"source": [
"# Coordinates are given as points in the complex plane\n",
"from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc\n",
"seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300)\n",
"seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350)\n",
"path = Path(seg1, seg2) # A path traversing the cubic and then the line\n",
"\n",
"# We could alternatively created this Path object using a d-string\n",
"from svgpathtools import parse_path\n",
"path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350')\n",
"\n",
"# Let's check that these two methods are equivalent\n",
"print(path)\n",
"print(path_alt)\n",
"print(path == path_alt)\n",
"\n",
"# On a related note, the Path.d() method returns a Path object's d-string\n",
"print(path.d())\n",
"print(parse_path(path.d()) == path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``Path`` class is a mutable sequence, so it behaves much like a list.\n",
"So segments can **append**ed, **insert**ed, set by index, **del**eted, **enumerate**d, **slice**d out, etc."
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),\n",
" Line(start=(200+300j), end=(250+350j)),\n",
" CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))\n",
"Path(Line(start=(200+100j), end=(200+300j)),\n",
" Line(start=(200+300j), end=(250+350j)),\n",
" CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))\n",
"path is continuous? True\n",
"path is closed? True\n",
"path contains non-differentiable points? True\n",
"spath contains non-differentiable points? False\n",
"Path(Line(start=(200+101.5j), end=(200+298.5j)),\n",
" CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)),\n",
" Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)),\n",
" CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)),\n",
" CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)),\n",
" CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j)))\n",
"Notice that path contains 3 segments and spath contains 6 segments.\n"
]
}
],
"source": [
"# Let's append another to the end of it\n",
"path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j))\n",
"print(path)\n",
"\n",
"# Let's replace the first segment with a Line object\n",
"path[0] = Line(200+100j, 200+300j)\n",
"print(path)\n",
"\n",
"# You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end)\n",
"print(\"path is continuous? \", path.iscontinuous())\n",
"print(\"path is closed? \", path.isclosed())\n",
"\n",
"# The curve the path follows is not, however, smooth (differentiable)\n",
"from svgpathtools import kinks, smoothed_path\n",
"print(\"path contains non-differentiable points? \", len(kinks(path)) > 0)\n",
"\n",
"# If we want, we can smooth these out (Experimental and only for line/cubic paths)\n",
"# Note: smoothing will always works (except on 180 degree turns), but you may want \n",
"# to play with the maxjointsize and tightness parameters to get pleasing results\n",
"# Note also: smoothing will increase the number of segments in a path\n",
"spath = smoothed_path(path)\n",
"print(\"spath contains non-differentiable points? \", len(kinks(spath)) > 0)\n",
"print(spath)\n",
"\n",
"# Let's take a quick look at the path and its smoothed relative\n",
"# The following commands will open two browser windows to display path and spaths\n",
"from svgpathtools import disvg\n",
"from time import sleep\n",
"disvg(path) \n",
"sleep(1) # needed when not giving the SVGs unique names (or not using timestamp)\n",
"disvg(spath)\n",
"print(\"Notice that path contains {} segments and spath contains {} segments.\"\n",
" \"\".format(len(path), len(spath)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Reading SVGSs\n",
"\n",
"The **svg2paths()** function converts an svgfile to a list of Path objects and a separate list of dictionaries containing the attributes of each said path. \n",
"Note: Line, Polyline, Polygon, and Path SVG elements can all be converted to Path objects using this function."
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)),\n",
" CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j)))\n",
"red\n"
]
}
],
"source": [
"# Read SVG into a list of path objects and list of dictionaries of attributes \n",
"from svgpathtools import svg2paths, wsvg\n",
"paths, attributes = svg2paths('test.svg')\n",
"\n",
"# Let's print out the first path object and the color it was in the SVG\n",
"# We'll see it is composed of two CubicBezier objects and, in the SVG file it \n",
"# came from, it was red\n",
"redpath = paths[0]\n",
"redpath_attribs = attributes[0]\n",
"print(redpath)\n",
"print(redpath_attribs['stroke'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Writing SVGSs (and some geometric functions and methods)\n",
"\n",
"The **wsvg()** function creates an SVG file from a list of path. This function can do many things (see docstring in *paths2svg.py* for more information) and is meant to be quick and easy to use.\n",
"Note: Use the convenience function **disvg()** (or set 'openinbrowser=True') to automatically attempt to open the created svg file in your default SVG viewer."
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Let's make a new SVG that's identical to the first\n",
"wsvg(paths, attributes=attributes, filename='output1.svg')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![output1.svg](output1.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"There will be many more examples of writing and displaying path data below.\n",
"\n",
"### The .point() method and transitioning between path and path segment parameterizations\n",
"SVG Path elements and their segments have official parameterizations.\n",
"These parameterizations can be accessed using the ``Path.point()``, ``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``, and ``Arc.point()`` methods.\n",
"All these parameterizations are defined over the domain 0 <= t <= 1.\n",
"\n",
"**Note:** In this document and in inline documentation and doctrings, I use a capital ``T`` when referring to the parameterization of a Path object and a lower case ``t`` when referring speaking about path segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc objects). \n",
"Given a ``T`` value, the ``Path.T2t()`` method can be used to find the corresponding segment index, ``k``, and segment parameter, ``t``, such that ``path.point(T)=path[k].point(t)``. \n",
"There is also a ``Path.t2T()`` method to solve the inverse problem."
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True\n",
"True\n",
"False\n",
"True\n"
]
}
],
"source": [
"# Example:\n",
"\n",
"# Let's check that the first segment of redpath starts \n",
"# at the same point as redpath\n",
"firstseg = redpath[0] \n",
"print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start)\n",
"\n",
"# Let's check that the last segment of redpath ends on the same point as redpath\n",
"lastseg = redpath[-1] \n",
"print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end)\n",
"\n",
"# This next boolean should return False as redpath is composed multiple segments\n",
"print(redpath.point(0.5) == firstseg.point(0.5))\n",
"\n",
"# If we want to figure out which segment of redpoint the \n",
"# point redpath.point(0.5) lands on, we can use the path.T2t() method\n",
"k, t = redpath.T2t(0.5)\n",
"print(redpath[k].point(t) == redpath.point(0.5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Tangent vectors and Bezier curves as numpy polynomial objects\n",
"Another great way to work with the parameterizations for Line, QuadraticBezier, and CubicBezier objects is to convert them to ``numpy.poly1d`` objects. This is done easily using the ``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()`` methods. \n",
"There's also a ``polynomial2bezier()`` function in the pathtools.py submodule to convert polynomials back to Bezier curves. \n",
"\n",
"**Note:** cubic Bezier curves are parameterized as $$\\mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3$$\n",
"where $P_0$, $P_1$, $P_2$, and $P_3$ are the control points ``start``, ``control1``, ``control2``, and ``end``, respectively, that svgpathtools uses to define a CubicBezier object. The ``CubicBezier.poly()`` method expands this polynomial to its standard form \n",
"$$\\mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3$$\n",
"where\n",
"$$\\begin{bmatrix}c_0\\\\c_1\\\\c_2\\\\c_3\\end{bmatrix} = \n",
"\\begin{bmatrix}\n",
"-1 & 3 & -3 & 1\\\\\n",
"3 & -6 & -3 & 0\\\\\n",
"-3 & 3 & 0 & 0\\\\\n",
"1 & 0 & 0 & 0\\\\\n",
"\\end{bmatrix}\n",
"\\begin{bmatrix}P_0\\\\P_1\\\\P_2\\\\P_3\\end{bmatrix}$$ \n",
"\n",
"QuadraticBezier.poly() and Line.poly() are defined similarly."
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True\n",
"The CubicBezier, b.point(x) = \n",
"\n",
"(300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3\n",
"\n",
"can be rewritten in standard form as \n",
"\n",
" 3 2\n",
"(-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j)\n"
]
}
],
"source": [
"# Example:\n",
"b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j)\n",
"p = b.poly()\n",
"\n",
"# p(t) == b.point(t)\n",
"print(p(0.235) == b.point(0.235))\n",
"\n",
"# What is p(t)? It's just the cubic b written in standard form. \n",
"bpretty = \"{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3\".format(*b.bpoints())\n",
"print(\"The CubicBezier, b.point(x) = \\n\\n\" + \n",
" bpretty + \"\\n\\n\" + \n",
" \"can be rewritten in standard form as \\n\\n\" +\n",
" str(p).replace('x','t'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To illustrate the awesomeness of being able to convert our Bezier curve objects to numpy.poly1d objects and back, lets compute the unit tangent vector of the above CubicBezier object, b, at t=0.5 in four different ways.\n",
"\n",
"### Tangent vectors (and more on polynomials)"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"unit tangent check: True\n"
]
}
],
"source": [
"t = 0.5\n",
"### Method 1: the easy way\n",
"u1 = b.unit_tangent(t)\n",
"\n",
"### Method 2: another easy way \n",
"# Note: This way will fail if it encounters a removable singularity.\n",
"u2 = b.derivative(t)/abs(b.derivative(t))\n",
"\n",
"### Method 2: a third easy way \n",
"# Note: This way will also fail if it encounters a removable singularity.\n",
"dp = p.deriv() \n",
"u3 = dp(t)/abs(dp(t))\n",
"\n",
"### Method 4: the removable-singularity-proof numpy.poly1d way \n",
"# Note: This is roughly how Method 1 works\n",
"from svgpathtools import real, imag, rational_limit\n",
"dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy \n",
"p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2\n",
"# Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and,\n",
"# the limit_{t->t0}[f(t) / abs(f(t))] == \n",
"# sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2])\n",
"from cmath import sqrt\n",
"u4 = sqrt(rational_limit(dp**2, p_mag2, t))\n",
"\n",
"print(\"unit tangent check:\", u1 == u2 == u3 == u4)\n",
"\n",
"# Let's do a visual check\n",
"mag = b.length()/4 # so it's not hard to see the tangent line\n",
"tangent_line = Line(b.point(t), b.point(t) + mag*u1)\n",
"disvg([b, tangent_line], 'bg', nodes=[b.point(t)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Translations (shifts), reversing orientation, and normal vectors"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Speaking of tangents, let's add a normal vector to the picture\n",
"n = b.normal(t)\n",
"normal_line = Line(b.point(t), b.point(t) + mag*n)\n",
"disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)])\n",
"\n",
"# and let's reverse the orientation of b! \n",
"# the tangent and normal lines should be sent to their opposites\n",
"br = b.reversed()\n",
"\n",
"# Let's also shift b_r over a bit to the right so we can view it next to b\n",
"# The simplest way to do this is br = br.translated(3*mag), but let's use \n",
"# the .bpoints() instead, which returns a Bezier's control points\n",
"br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] # \n",
"\n",
"tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t))\n",
"normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t))\n",
"wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r], \n",
" 'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg', \n",
" text=[\"b's tangent\", \"br's tangent\"], text_path=[tangent_line, tangent_line_r])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![vectorframes.svg](vectorframes.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Rotations and Translations"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Let's take a Line and an Arc and make some pictures\n",
"top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1)\n",
"midline = Line(-1.5, 1.5)\n",
"\n",
"# First let's make our ellipse whole\n",
"bottom_half = top_half.rotated(180)\n",
"decorated_ellipse = Path(top_half, bottom_half)\n",
"\n",
"# Now let's add the decorations\n",
"for k in range(12):\n",
" decorated_ellipse.append(midline.rotated(30*k))\n",
" \n",
"# Let's move it over so we can see the original Line and Arc object next\n",
"# to the final product\n",
"decorated_ellipse = decorated_ellipse.translated(4+0j)\n",
"wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![decorated_ellipse.svg](decorated_ellipse.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### arc length and inverse arc length\n",
"\n",
"Here we'll create an SVG that shows off the parametric and geometric midpoints of the paths from ``test.svg``. We'll need to compute use the ``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``, ``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the related inverse arc length methods ``.ilength()`` function to do this."
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# First we'll load the path data from the file test.svg\n",
"paths, attributes = svg2paths('test.svg')\n",
"\n",
"# Let's mark the parametric midpoint of each segment\n",
"# I say \"parametric\" midpoint because Bezier curves aren't \n",
"# parameterized by arclength \n",
"# If they're also the geometric midpoint, let's mark them\n",
"# purple and otherwise we'll mark the geometric midpoint green\n",
"min_depth = 5\n",
"error = 1e-4\n",
"dots = []\n",
"ncols = []\n",
"nradii = []\n",
"for path in paths:\n",
" for seg in path:\n",
" parametric_mid = seg.point(0.5)\n",
" seg_length = seg.length()\n",
" if seg.length(0.5)/seg.length() == 1/2:\n",
" dots += [parametric_mid]\n",
" ncols += ['purple']\n",
" nradii += [5]\n",
" else:\n",
" t_mid = seg.ilength(seg_length/2)\n",
" geo_mid = seg.point(t_mid)\n",
" dots += [parametric_mid, geo_mid]\n",
" ncols += ['red', 'green']\n",
" nradii += [5] * 2\n",
"\n",
"# In 'output2.svg' the paths will retain their original attributes\n",
"wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, \n",
" attributes=attributes, filename='output2.svg')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![output2.svg](output2.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Intersections between Bezier curves"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Let's find all intersections between redpath and the other \n",
"redpath = paths[0]\n",
"redpath_attribs = attributes[0]\n",
"intersections = []\n",
"for path in paths[1:]:\n",
" for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path):\n",
" intersections.append(redpath.point(T1))\n",
" \n",
"disvg(paths, filename='output_intersections.svg', attributes=attributes,\n",
" nodes = intersections, node_radii = [5]*len(intersections))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![output_intersections.svg](output_intersections.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### An Advanced Application: Offsetting Bezier Curves\n",
"Here we'll find the [offset curve](https://en.wikipedia.org/wiki/Parallel_curve) for a few Bezier cubics."
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"def display_offset_curve(bezpath, offset_distance, steps=1000,\n",
" stripe_width=3, stripe_colors='ygb',\n",
" name='offsetcurves.svg', show_normal_line=True):\n",
" \"\"\"Takes in a path of bezier curves, `bezpath`, and a distance,\n",
" `offset_distance`, and displays the 'parallel' offset curve by placing a\n",
" dot at a distance of `offset_distance` away at each step.\"\"\"\n",
" nls = []\n",
" nodes = []\n",
" line_colors = ''\n",
" node_colors = ''\n",
"\n",
" for bez in bezpath:\n",
" ct = 1\n",
" for k in range(steps):\n",
" t = k / steps\n",
" nl = Line(bez.point(t),\n",
" bez.point(t) + offset_distance * bez.normal(t))\n",
" nls.append(nl)\n",
" line_colors += stripe_colors[ct % 3]\n",
" if not (k % stripe_width):\n",
" ct += 1\n",
" nodes.append(bez.point(t))\n",
" nodes.append(nl.end)\n",
" node_colors += 'kr'\n",
" # nls.reverse()\n",
" if show_normal_line:\n",
" wsvg([bezpath] + nls, 'k' + line_colors,\n",
" nodes=nodes, node_colors=node_colors,\n",
" filename=name)\n",
" else:\n",
" wsvg(bezpath, 'k',\n",
" nodes=nodes, node_colors=node_colors,\n",
" filename=name)\n",
"\n",
"bez1 = parse_path(\"m 288,600 c -52,-28 -42,-61 0,-97 \")[0]\n",
"bez2 = parse_path(\"M 151,395 C 407,485 726.17662,160 634,339\")[0]\n",
"bez3 = parse_path(\"m 117,695 c 237,-7 -103,-146 457,0\")[0]\n",
"path = Path(bez1, bez2.translated(300), bez3.translated(500+400j))\n",
"\n",
"display_offset_curve(path, 500)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![offsetcurves.svg](offsetcurves.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Compatibility Notes for users of svg.path (v2.0)\n",
"\n",
"- renamed Arc.arc attribute as Arc.large_arc\n",
"\n",
"- Path.d() : For behavior similar<sup id=\"a2\">[2](#f2)</sup> to svg.path (v2.0), set both useSandT and use_closed_attrib to be True.\n",
"\n",
"<u id=\"f2\">2</u> The behavior would be identical, but the string formatting used in this method has been changed to use default format (instead of the General format, {:G}), for inceased precision. [↩](#a2)\n",
"\n",
"## To Do\n",
"\n",
"- Implement Arc X Arc intersections.\n",
"- Implement Arc.radialrange() method.\n",
"\n",
"Licence\n",
"-------\n",
"\n",
"This module is under a MIT License."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 2",
"language": "python",
"name": "python2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.11"
}
},
"nbformat": 4,
"nbformat_minor": 0
}