839 lines
29 KiB
Plaintext
839 lines
29 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": 1,
|
|
"metadata": {
|
|
"collapsed": true,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"from __future__ import division, print_function"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 2,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": 3,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": 4,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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",
|
|
"# Update: You can now also extract the svg-attributes by setting\n",
|
|
"# return_svg_attributes=True, or with the convenience function svg2paths2\n",
|
|
"from svgpathtools import svg2paths2\n",
|
|
"paths, attributes, svg_attributes = svg2paths2('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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": 5,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Let's make a new SVG that's identical to the first\n",
|
|
"wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='output1.svg')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"![output1.svg](output1.svg)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": 6,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"### 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](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#General_definition)."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 7,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"The ability to convert between Bezier objects to NumPy polynomial objects is very useful. For starters, we can take turn a list of Bézier segments into a NumPy array \n",
|
|
"\n",
|
|
"### Numpy Array operations on Bézier path segments \n",
|
|
"\n",
|
|
"[Example available here](https://github.com/mathandy/svgpathtools/blob/master/examples/compute-many-points-quickly-using-numpy-arrays.py) \n",
|
|
"\n",
|
|
"To further illustrate the power 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 NumPy polynomials) "
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 8,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"### Translations (shifts), reversing orientation, and normal vectors"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 9,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"![vectorframes.svg](vectorframes.svg)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"### Rotations and Translations"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 10,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"![decorated_ellipse.svg](decorated_ellipse.svg)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": 11,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"![output2.svg](output2.svg)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"### Intersections between Bezier curves"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 12,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"![output_intersections.svg](output_intersections.svg)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"### An Advanced Application: Offsetting Paths\n",
|
|
"Here we'll find the [offset curve](https://en.wikipedia.org/wiki/Parallel_curve) for a few paths."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 13,
|
|
"metadata": {
|
|
"collapsed": false,
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"from svgpathtools import parse_path, Line, Path, wsvg\n",
|
|
"def offset_curve(path, offset_distance, steps=1000):\n",
|
|
" \"\"\"Takes in a Path object, `path`, and a distance,\n",
|
|
" `offset_distance`, and outputs an piecewise-linear approximation \n",
|
|
" of the 'parallel' offset curve.\"\"\"\n",
|
|
" nls = []\n",
|
|
" for seg in path:\n",
|
|
" ct = 1\n",
|
|
" for k in range(steps):\n",
|
|
" t = k / steps\n",
|
|
" offset_vector = offset_distance * seg.normal(t)\n",
|
|
" nl = Line(seg.point(t), seg.point(t) + offset_vector)\n",
|
|
" nls.append(nl)\n",
|
|
" connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)]\n",
|
|
" if path.isclosed():\n",
|
|
" connect_the_dots.append(Line(nls[-1].end, nls[0].end))\n",
|
|
" offset_path = Path(*connect_the_dots)\n",
|
|
" return offset_path\n",
|
|
"\n",
|
|
"# Examples:\n",
|
|
"path1 = parse_path(\"m 288,600 c -52,-28 -42,-61 0,-97 \")\n",
|
|
"path2 = parse_path(\"M 151,395 C 407,485 726.17662,160 634,339\").translated(300)\n",
|
|
"path3 = parse_path(\"m 117,695 c 237,-7 -103,-146 457,0\").translated(500+400j)\n",
|
|
"paths = [path1, path2, path3]\n",
|
|
"\n",
|
|
"offset_distances = [10*k for k in range(1,51)]\n",
|
|
"offset_paths = []\n",
|
|
"for path in paths:\n",
|
|
" for distances in offset_distances:\n",
|
|
" offset_paths.append(offset_curve(path, distances))\n",
|
|
"\n",
|
|
"# Note: This will take a few moments\n",
|
|
"wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"source": [
|
|
"![offset_curves.svg](offset_curves.svg)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"deletable": true,
|
|
"editable": true
|
|
},
|
|
"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",
|
|
"\n",
|
|
"Licence\n",
|
|
"-------\n",
|
|
"\n",
|
|
"This module is under a MIT License."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"collapsed": true,
|
|
"deletable": true,
|
|
"editable": 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.13"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 0
|
|
}
|