├── license.txt ├── package.json ├── parseSvgPathData.js ├── parsesvgpathdata.gif ├── parsesvgpathdata.html └── readme.md /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-16 Stefan Goessner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-svg-path-data", 3 | "version": "0.2.1", 4 | "description": "SVG Path Data Micro-Parser targeting custom interface objects.", 5 | "keywords": [ 6 | "SVG", 7 | "Pathdata", 8 | "Parser", 9 | "javascript", 10 | "canvas" 11 | ], 12 | "main": "parseSvgPathData.js", 13 | "scripts": { 14 | "minify": "uglifyjs --comments -o ./parseSvgPathData.min.js ./parseSvgPathData.js", 15 | "gzip": "7z -tgzip a ./parseSvgPathData.min.js.gz ./parseSvgPathData.min.js" 16 | }, 17 | "author": "Stefan Goessner ", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/goessner/parseSvgPathData.git" 21 | }, 22 | "license": "MIT", 23 | "devDependencies": { 24 | "concat": "^1.0.0", 25 | "jsdoc-to-markdown": "^1.3.2", 26 | "jslint": "^0.9.5", 27 | "uglifyjs": "^2.4.10" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /parseSvgPathData.js: -------------------------------------------------------------------------------- 1 | /** 2 | * parseSvgPathData (c) 2018 Stefan Goessner 3 | * SVG Path Data Micro Parser 4 | * @license MIT License 5 | * @link https://github.com/goessner/parseSvgPathData 6 | */ 7 | "use strict"; 8 | 9 | function parseSvgPathData(data,ifc,ctx) { 10 | const rex = /([achlmqstvz])([^achlmqstvz]*)/ig; 11 | const seg = { A:{n:7,f:'A'}, C:{n:6,f:'C'}, H:{n:1,f:'H'}, 12 | L:{n:2,f:'L'}, M:{n:2,f:'L'}, Q:{n:4,f:'Q'}, 13 | S:{n:4,f:'S'}, T:{n:2,f:'T'}, V:{n:1,f:'V'}, 14 | Z:{n:0}, 15 | a:{n:7,f:'a'}, c:{n:6,f:'c'}, h:{n:1,f:'h'}, 16 | l:{n:2,f:'l'}, m:{n:2,f:'l'}, q:{n:4,f:'q'}, 17 | s:{n:4,f:'s'}, t:{n:2,f:'t'}, v:{n:1,f:'v'}, 18 | z:{n:0} }; 19 | 20 | const segment = (ifc,type,args) => { 21 | if (type in seg) { 22 | if (args.length === seg[type].n) { 23 | ifc[type](...args); 24 | } 25 | else if (args.length > seg[type].n) { 26 | ifc[type](...args); 27 | args.splice(0,seg[type].n); 28 | segment(ifc,seg[type].f,args); 29 | } 30 | else 31 | console.error(`invalid # of path segment '${type}' arguments: ${args.length} of ${seg[type].n}: '${args}'`) 32 | } 33 | }; 34 | let match; 35 | 36 | if (!ifc) ifc = parseSvgPathData.defaultIfc; 37 | ifc.init(ctx); 38 | // for each explicit named segment ... 39 | while (match = rex.exec(data)) { 40 | segment(ifc, match[1], match[2].replace(/^\s+|\s+$/g,'') // trim whitespace at both ends (str.trim .. !) 41 | .replace(/(\d)\-/g,'$1 -') // insert blank between digit and '-' 42 | .split(/[, \t\n\r]+/g) // as array 43 | .map(Number)) 44 | } 45 | return ifc.ctx; 46 | } 47 | 48 | // simplify segments to minimal absolute set [A,M,L,C] as JSON array 49 | parseSvgPathData.defaultIfc = { 50 | init() { this.x=this.x0=this.y=this.y0=this.ctx.length = 0; }, 51 | A(rx,ry,rot,fA,fS,x,y) { this.ctx.push({type:'A',x:(this.x=x),y:(this.y=y),rx,ry,rot,fA,fS}) }, 52 | M(x,y) { this.ctx.push({type:'M',x:(this.x=this.x0=this.x1=this.x2=x),y:(this.y=this.y0=this.y1=this.y2=y)}) }, 53 | L(x,y) { this.ctx.push({type:'L',x:(this.x=x),y:(this.y=y)}) }, 54 | H(x) { this.ctx.push({type:'L',x:(this.x=x),y:this.y}) }, 55 | V(y) { this.ctx.push({type:'L',x:this.x,y:(this.y=y)}) }, 56 | C(x1,y1,x2,y2,x,y) { 57 | this.ctx.push({type:'C',x:(this.x=x),y:(this.y=y), 58 | x1:(this.x1=x1),y1:(this.y1=y1), 59 | x2:(this.x2=x2),y2:(this.y2=y2)}); 60 | }, 61 | S(x2,y2,x,y) { 62 | this.ctx.push({type:'C',x:(this.x=x),y:(this.y=y), 63 | x1:(this.x1=2*this.x-this.x2),y1:(this.y1=2*this.y-this.y2), 64 | x2:(this.x2=x2),y2:(this.y2=y2)}); 65 | }, 66 | Q(x1,y1,x,y) { 67 | this.ctx.push({type:'C',x:(this.x=x),y:(this.y=y), 68 | x1:(this.x1=x1),y1:(this.y1=y1), 69 | x2:(this.x2=x1),y2:(this.y2=y1)}); 70 | }, 71 | T(x,y) { 72 | this.ctx.push({type:'C',x:(this.x=x),y:(this.y=y), 73 | x1:(this.x1+=2*(this.x-this.x1)),y1:(this.y1+=2*(this.y-this.y1)), 74 | x2:(this.x1),y2:(this.y1)}); 75 | }, 76 | Z() { this.ctx.push({type:'L',x:(this.x=this.x0),y:(this.y=this.y0)}) }, 77 | a(rx,ry,rot,fA,fS,x,y) { this.A(rx,ry,rot,fA,fS,this.x+x,this.y+y) }, 78 | m(x,y) { this.M(this.x+x,this.y+y) }, 79 | l(x,y) { this.L(this.x+x,this.y+y) }, 80 | h(x) { this.H(this.x+x) }, 81 | v(y) { this.V(this.y+y) }, 82 | c(x1,y1,x2,y2,x,y) { this.C(this.x+x1,this.y+y1,this.x+x2,this.y+y2,this.x+x,this.y+y) }, 83 | s(x2,y2,x,y) { this.S(this.x+x2,this.y+y2,this.x+x,this.y+y) }, 84 | q(x1,y1,x,y) { this.Q(this.x+x1,this.y+y1,this.x+x,this.y+y) }, 85 | t(x,y) { this.T(this.x+x,this.y+y) }, 86 | z() { this.Z() }, 87 | x0:0,y0:0, x:0,y:0, x1:0,y1:0, x2:0,y2:0, 88 | ctx:[] 89 | }; 90 | 91 | // direct to CanvasRenderingContext2D interface or to Path2D object 92 | parseSvgPathData.canvasIfc = { 93 | init(ctx) { 94 | this.x=this.x0=this.x1=this.x2=this.y=this.y0=this.y1=this.y2 = 0; 95 | this.ctx = ctx 96 | }, 97 | A(rx,ry,rot,fA,fS,x,y) { 98 | let x12 = x-this.x, y12 = y-this.y, 99 | phi = rot/180*Math.PI, 100 | cp = phi ? Math.cos(phi) : 1, sp = phi ? Math.sin(phi) : 0, 101 | k = ry/rx, 102 | dth_sgn = fS ? 1 : -1, 103 | Nx = dth_sgn*(-x12*cp - y12*sp), Ny = dth_sgn*(-x12*sp + y12*cp), 104 | NN = Math.hypot(Nx, Ny/k), 105 | R = 2*rx > NN ? rx : NN/2, // scale R to a valid value... 106 | dth = 2*dth_sgn*Math.asin(NN/2/R), 107 | th1, ct, st; 108 | 109 | if (fA) 110 | dth = dth > 0 ? 2*Math.PI - dth : -2*Math.PI - dth; 111 | th1 = Math.atan2(k*Nx,Ny) - dth/2, 112 | ct = Math.cos(th1); st = Math.sin(th1); 113 | 114 | this.ctx.ellipse( 115 | this.x - R*(cp*ct - sp*k*st), 116 | this.y - R*(sp*ct + cp*k*st), 117 | R, R*k, phi, th1, th1 + dth, dth_sgn === -1 118 | ) 119 | this.x = x; this.y = y; 120 | }, 121 | M(x,y) { this.ctx.moveTo(this.x=this.x0=x, this.y=this.y0=y) }, 122 | L(x,y) { this.ctx.lineTo(this.x=x, this.y=y) }, 123 | H(x) { this.ctx.lineTo(this.x=x, this.y ) }, 124 | V(y) { this.ctx.lineTo(this.x, this.y=y) }, 125 | C(x1,y1,x2,y2,x,y) { 126 | this.ctx.bezierCurveTo(this.x1=x1,this.y1=y1,this.x2=x2,this.y2=y2,this.x=x,this.y=y) 127 | }, 128 | S(x2,y2,x,y) { 129 | this.ctx.bezierCurveTo(this.x1=2*this.x-this.x2,this.y1=2*this.y-this.y2, 130 | this.x2=x2,this.y2=y2,this.x=x,this.y=y) 131 | }, 132 | Q(x1,y1,x,y) { 133 | this.ctx.quadraticCurveTo(this.x1=x1,this.y1=y1,this.x=x,this.y=y) 134 | }, 135 | T(x,y) { 136 | this.ctx.quadraticCurveTo(this.x1+=2*(this.x-this.x1),this.y1+=2*(this.y-this.y1), 137 | this.x=x,this.y=y) 138 | }, 139 | Z() { this.ctx.closePath() }, 140 | a(rx,ry,rot,fA,fS,x,y) { this.A(rx,ry,rot,fA,fS,this.x+x,this.y+y) }, 141 | m(x,y) { this.M(this.x+x,this.y+y) }, 142 | l(x,y) { this.L(this.x+x,this.y+y) }, 143 | h(x) { this.H(this.x+x) }, 144 | v(y) { this.V(this.y+y) }, 145 | c(x1,y1,x2,y2,x,y) { this.C(this.x+x1,this.y+y1,this.x+x2,this.y+y2,this.x+x,this.y+y) }, 146 | s(x2,y2,x,y) { this.S(this.x+x2,this.y+y2,this.x+x,this.y+y) }, 147 | q(x1,y1,x,y) { this.Q(this.x+x1,this.y+y1,this.x+x,this.y+y) }, 148 | t(x,y) { this.T(this.x+x,this.y+y) }, 149 | z() { this.Z() }, 150 | // current point buffers (start, current, control) 151 | x0:0,y0:0, x:0,y:0, x1:0,y1:0, x2:0,y2:0, 152 | ctx:false 153 | }; 154 | -------------------------------------------------------------------------------- /parsesvgpathdata.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goessner/parseSvgPathData/1aa55731c4b5f1665ccd4c3da725583d7f67a4cd/parsesvgpathdata.gif -------------------------------------------------------------------------------- /parsesvgpathdata.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg path data micro parser 5 | 6 | 7 | 8 |

Left: native            Right: parseSvgPathData

9 | 10 |
11 | 14 | 15 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![License](http://img.shields.io/:license-mit-blue.svg)](https://github.com/goessner/parseSvgPathData/license.txt) 2 | [![npm](https://img.shields.io/npm/v/parse-svg-path-data.svg)](https://www.npmjs.com/package/parse-svg-path-data/) 3 | [![npm](https://img.shields.io/npm/dt/parse-svg-path-data.svg)](https://www.npmjs.com/package/parse-svg-path-data) 4 | [![no dependencies](https://img.shields.io/gemnasium/mathiasbynens/he.svg)](https://github.com/goessner/parseSvgPathData) 5 | 6 | # parseSvgPathData 7 | 8 | parseSvgPathData is a SVG path data micro-parser for parsing the [path data attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) of the [SVG `` element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path) as a single JavaScript function. 9 | 10 | ```js 11 | function parseSvgPathData(data,[ifc],[ctx]) 12 | ``` 13 | 14 | * `data` is the SVG path data string (e.g. `"M 100 100 L 300 100 L 200 300 z"`). 15 | * `ifc` is an interface object [optional] (see below) 16 | * `ctx` is a context object the interface methods will operate on [optional]. 17 | * `return`'s the context object. 18 | 19 | ![image](./parsesvgpathdata.gif) 20 | 21 | [Try it by yourself](https://goessner.github.io/parseSvgPathData/parsesvgpathdata.html) 22 | 23 | The fact that a user defined interface object should be supported, was considered a critical feature here. Similar libraries, which do not support this, are: 24 | 25 | * [parse-svg-path](https://github.com/jkroso/parse-svg-path) A minimal svg path parser 26 | * [svg-path-parser](https://github.com/hughsk/svg-path-parser) A parser for SVG's path syntax 27 | * [svg-pathdata](https://github.com/nfroidure/svg-pathdata) Parse SVG PathDatas 28 | 29 | ## Interface Object 30 | 31 | Interface objects need to have the following interface structure: 32 | 33 | ```js 34 | { 35 | init(ctx), 36 | 37 | A(rx,ry,rot,fA,fS,x,y), 38 | C(x1,y1,x2,y2,x,y), 39 | H(x), 40 | L(x,y), 41 | M(x,y), 42 | Q(x1,y1,x,y), 43 | S(x2,y2,x,y), 44 | T(x,y), 45 | V(y), 46 | Z(), 47 | 48 | a(rx,ry,rot,fA,fS,x,y), 49 | c(x1,y1,x2,y2,x,y), 50 | h(x), 51 | l(x,y), 52 | m(x,y), 53 | q(x1,y1,x,y), 54 | s(x2,y2,x,y), 55 | t(x,y), 56 | v(y), 57 | z() 58 | } 59 | ``` 60 | 61 | `parseSvgPathData` comes with two implemented Interfaces as members of the 62 | `parseSvgPathData` function object itself. 63 | 64 | * `parseSvgPathData.defaultIfc` 65 | * `parseSvgPathData.canvasIfc` 66 | 67 | Have a look into the source how these interface objects are implemented and start to implement your own custom interface object. 68 | 69 | ### `defaultIfc` 70 | 71 | When calling the `parseSvgPathData` function without interface object `ifc` and context `ctx` parameters (e.g. `parseSvgPathData('M0,0 L100,0')`), the default interface object `parseSvgPathData.defaultIfc` will be taken. 72 | 73 | By using this interface object the command segments are simplified and mapped to a minimal set of absolute path commands `[A,L,M,C]` in object notation. 74 | 75 | So parsing the path data `M37,17v15H14V17z M50,0H0v50h50z` using this interface object will result in the object structure: 76 | 77 | ```json 78 | [ {"type":"M","x":37,"y":17}, 79 | {"type":"L","x":37,"y":32}, 80 | {"type":"L","x":14,"y":32}, 81 | {"type":"L","x":14,"y":17}, 82 | {"type":"L","x":37,"y":17}, 83 | {"type":"M","x":50,"y":0}, 84 | {"type":"L","x":0,"y":0}, 85 | {"type":"L","x":0,"y":50}, 86 | {"type":"L","x":50,"y":50}, 87 | {"type":"L","x":50,"y":0} 88 | ] 89 | ``` 90 | 91 | ### `canvasIfc` 92 | 93 | `canvasIfc` is another interface object, which is supplied with `parseSvgPathData`. Here every path command will invoke corresponding `HTML canvas 2D` methods. 94 | 95 | Please notice my paper [A Generalized Approach to Parameterizing Planar Elliptical Arcs](https://www.researchgate.net/publication/324498320_A_Generalized_Approach_to_Parameterizing_Planar_Elliptical_Arcs) in this context. 96 | 97 | The following will render the path data in a given canvas element: 98 | 99 | ```js 100 | const ctx = document.getElementById('canvas').getContext('2d') 101 | // ... 102 | parseSvgPathData('M10,10 L100,10',parseSvgPathData.canvasIfc,ctx); 103 | ctx.stroke() 104 | ctx.fill() 105 | ``` 106 | 107 | You might know, that `CanvasRenderingContext2D` supports the [`Path2D` interface](https://developer.mozilla.org/en-US/docs/Web/API/Path2D). So we can also use it with `parseSvgPathData` 108 | 109 | ```js 110 | const path = new Path2D(); 111 | // ... 112 | parseSvgPathData('M10,10 L100,10',parseSvgPathData.canvasIfc,path); 113 | ``` 114 | or even simpler 115 | 116 | ```js 117 | const path = parseSvgPathData('M10,10 L100,10', 118 | parseSvgPathData.canvasIfc, 119 | new Path2D()); 120 | ``` 121 | Please note, that `parseSvgPathData` might be useful despite the fact, that `Path2D` natively understands and supports SVG path data. 122 | 123 | ```js 124 | const path = new Path2D('M10,10 L100,10'); 125 | ``` 126 | You can see the results of both the native and `parseSvgPathData` output side by side here. 127 | 128 | ## License 129 | 130 | `parseSvgPathData.js` is licensed under the terms of the MIT License. See [License](https://github.com/goessner/parseSvgPathData/license.txt) for details. 131 | 132 | ## FAQ 133 | 134 | -- 135 | 136 | # Change Log 137 | 138 | All notable changes to this project will be documented here. This project adheres to Semantic Versioning. 139 | 140 | ## 0.2.0 - 2018-04-15 141 | 142 | ### First Commit to Github 143 | --------------------------------------------------------------------------------