├── .gitignore ├── .nojekyll ├── README.md ├── coord.js ├── index.html ├── index.js ├── library.js ├── outline.js ├── package.json ├── point-curve-shape.js ├── point.js ├── svg-parser.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/js-svg-path/6536b71983d3c565c9a62a4fcc7a9551c48a829c/.nojekyll -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-svg-path 2 | 3 | A parser that turns SVG path strings into a JS object you can mess with. Basically if you want to mess around with `svg` paths, and you want the convenience of "points" rather than "a string" where it comes to the `d` attribute, this library's got you covered. 4 | 5 | ## Installation 6 | 7 | `npm install js-svg-path`, with optional `--save` or `--save-dev` if you need it saved in your package.json file. 8 | 9 | ## Using this thing 10 | 11 | Use in Node.js as: 12 | 13 | ``` 14 | var library = require('js-svg-path'); 15 | ``` 16 | 17 | Or use in the browser as: 18 | ``` 19 | 20 | 21 | ``` 22 | 23 | Easy-peasy. 24 | 25 | ## The API(s) 26 | 27 | There are three objects, and one utility function, exposed in this API. 28 | 29 | ### library.parse(SVGPathString) 30 | 31 | You'll want this 99.99% of the time. This function ingests an SVG path string, and returns an `Outline` object for you to do with as you please. 32 | 33 | ### library.Outline 34 | 35 | The `Outline` object represents a full SVG path outline (which may consist of nested subpats). It is constructed as `new library.Outline()` and has the following API: 36 | 37 | - `getShapes()` - Gets all shapes defined in the path that the outline was built on. 38 | - `getShape(idx)` - This gets a specific subpath in an outline. For obvious reasons, `idx` starts at 0. 39 | - `toSVG()` - Serialize this outline to an SVG path. This will yield a path with *absolute* coordinates, but is for all intents and purposes idempotent: pushing in a path should yield an identically rendered path through `.toSVG()` 40 | 41 | and the following API that most of the time you shouldn't care about but sometimes you will: 42 | 43 | - `startGroup()` - on an empty outline, this essentially "starts recording an outline". 44 | - `startShape()` - this marks the start of a new (sub)path in the outline. 45 | - `addPoint(x,y)` - this adds a vertex to the outline, at absolute coordinate (x/y). 46 | - `setLeftControl(x,y)` - this modifies the current point such that it has a left-side control point. 47 | - `setRightControl(x,y)` - this modifies the current point such that it has a right-side control point. 48 | - `closeShape()` - this signals that we are done chronicalling the current (sub)path. 49 | - `closeGroup()` - this signals that we are done recording this outline entirely. 50 | 51 | ### library.PointCurveShape 52 | 53 | This is an internal structure, but why not expose it to you? Each (sub)path in an outline is a PointCurveShape that is constructed with `new library.PointCurveShap()` and has the following API: 54 | 55 | - `current()` - get the current point. This is relevant while an outline is being built. 56 | - `addPoint(x,y)` - add a vertex to this shape. 57 | - `setLeft(x,y)` - set the left control point for this vertex to (x/y). 58 | - `setRight(x,y)` - set the right control point for this vertex to (x/y). 59 | - `toSVG()` - serializes this "shape" (i.e. path) to SVG form. 60 | 61 | ### library.SVGParser 62 | 63 | This is the main factory object and has very little in the way of its own API: 64 | 65 | - `new library.SVGParser(outline)` - the SVGParser constructor takes an `Outline` object as constructor argument, which will be used to record parsing results. 66 | - `getReceiver()` - returns the outline recorder passed into the constructor. 67 | - `parse(path, [xoffset, yoffset])` - parses an SVG path, with an optional (xoffset/yoffset) offset to translate the entire path uniformly. 68 | 69 | ## An example: 70 | 71 | Let's ingest an SVG's path, and then generate the SVG code that shows you where all the vectices and control points are: 72 | 73 | ```js 74 | const path1 = find("svg path")[0]; 75 | const path2 = find("svg path")[1]; 76 | const d = path1.get("d"); 77 | 78 | var outline = new Receiver(); 79 | const parser = new SVGParser(outline); 80 | parser.parse(d); 81 | 82 | const vertices = [``]; 83 | outline.getShapes().forEach(shape => { 84 | shape.points.forEach(p => { 85 | let m = p.main, l = p.left, r = p.right; 86 | if (l) { 87 | vertices.push(``) 88 | vertices.push(``) 89 | } 90 | if (r) { 91 | vertices.push(``) 92 | vertices.push(``) 93 | } 94 | vertices.push(``) 95 | }); 96 | }); 97 | 98 | const svg2 = find("svg g")[1]; 99 | svg2.innerHTML = vertices.join('\n'); 100 | ``` 101 | 102 | ## Live demo? 103 | 104 | Yeah alright: https://pomax.github.io/js-svg-path, and obviously to see *why* it works, view-source that. 105 | -------------------------------------------------------------------------------- /coord.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Trivial coordinate class 3 | */ 4 | class Coord { 5 | constructor(x,y) { 6 | if (isNaN(x) || isNaN(y)) { 7 | throw new Error(`NaN encountered: ${x}/${y}`); 8 | } 9 | this.x=x; 10 | this.y=y; 11 | } 12 | } 13 | 14 | export default Coord; 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Let's mess up the letter "m" 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import PointCurveShape from "./point-curve-shape.js"; 2 | import Outline from "./outline.js"; 3 | import SVGParser from "./svg-parser.js"; 4 | 5 | const API = { SVGParser, Outline, PointCurveShape, parse: function(pathString) { 6 | const outline = new API.Outline(); 7 | const parser = new API.SVGParser(outline); 8 | parser.parse(pathString); 9 | return outline; 10 | }}; 11 | 12 | export default API; 13 | -------------------------------------------------------------------------------- /library.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.PathConverter = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | /** 8 | * Trivial coordinate class 9 | */ 10 | class Coord { 11 | constructor(x,y) { 12 | if (isNaN(x) || isNaN(y)) { 13 | throw new Error(`NaN encountered: ${x}/${y}`); 14 | } 15 | this.x=x; 16 | this.y=y; 17 | } 18 | } 19 | 20 | /** 21 | * Simple point curve Point class 22 | * - plain points only have "main" set 23 | * - bezier points have "left" and "right" set as control points 24 | * A bezier curve from p1 to p2 will consist of {p1, p1.right, p2.left, p2} 25 | */ 26 | class Point { 27 | constructor(x, y) { this.main = new Coord(x,y); this.left = null; this.right = null; this.isplain = true; } 28 | setLeft(x, y) { this.isplain=false; this.left = new Coord(x,y); } 29 | setRight(x, y) { this.isplain=false; this.right = new Coord(x,y); } 30 | isPlain() { return this.isplain; } 31 | getPoint() { return this.main; } 32 | getLeft() { return this.isplain ? this.main : this.left==null? this.main : this.left; } 33 | getRight() { return this.isplain ? this.main : this.right==null? this.main : this.right; } 34 | } 35 | 36 | /** 37 | * A shape defined in terms of curve points 38 | */ 39 | class PointCurveShape { 40 | constructor() { this.points = []; } 41 | current() { return this.points.slice(-1)[0]; } 42 | addPoint(x, y) { this.points.push(new Point(x,y)); } 43 | setLeft(x, y) { this.current().setLeft(x,y); } 44 | setRight(x, y) { this.current().setRight(x,y); } 45 | 46 | /** 47 | * Convert the point curve to an SVG path string 48 | * (bidirectional conversion? You better believe it). 49 | * This code is very similar to the draw method, 50 | * since it effectively does the same thing. 51 | */ 52 | toSVG() { 53 | // first vertex 54 | let points = this.points; 55 | let first = points[0].getPoint(); 56 | let x = first.x; 57 | let y = first.y; 58 | let svg = "M" + x + (y<0? y : " " + y); 59 | // rest of the shape 60 | for(let p=1; p svg += s.toSVG()); 101 | return svg; 102 | } 103 | 104 | // start a shape group 105 | startGroup() { this.curveshapes = []; } 106 | 107 | // start a new shape in the group 108 | startShape(){ this.current = new PointCurveShape(); } 109 | 110 | // add an on-screen point 111 | addPoint(x, y){ 112 | this.current.addPoint(x,y); 113 | var bounds = this.bounds; 114 | if (!bounds) { bounds = this.bounds = [x,y,x,y]; } 115 | if (xbounds[MAXX]) { bounds[MAXX] = x; } 117 | if (ybounds[MAXY]) { bounds[MAXY] = y; }} 119 | 120 | // set the x/y coordinates for the left/right control points 121 | setLeftControl(x, y){ 122 | this.current.setLeft(x,y); } 123 | 124 | setRightControl(x, y){ 125 | this.current.setRight(x,y); } 126 | 127 | // close the current shape 128 | closeShape() { 129 | this.curveshapes.push(this.current); 130 | this.current=null; } 131 | 132 | // close the group of shapes. 133 | closeGroup(){ 134 | this.curveshapes.push(this.current); 135 | this.current=null; } 136 | } 137 | 138 | class SVGParser { 139 | constructor(receiver) { 140 | this.receiver = receiver; 141 | } 142 | 143 | getReceiver() { return receiver; } 144 | 145 | parse(path, xoffset, yoffset) { 146 | xoffset = xoffset || 0; 147 | yoffset = yoffset || 0; 148 | 149 | // normalize the path 150 | path = path.replace(/\s*([mlvhqczMLVHQCZ])\s*/g,"\n$1 ") 151 | .replace(/,/g," ") 152 | .replace(/-/g," -") 153 | .replace(/ +/g," "); 154 | 155 | // step one: split the path in individual pathing instructions 156 | var strings = path.split("\n"); 157 | let x = xoffset; 158 | let y = yoffset; 159 | 160 | // step two: process each instruction 161 | let receiver = this.receiver; 162 | receiver.startGroup(); 163 | for(let s=1; s1 ? instruction.substring(2).trim().split(" ") : []); 168 | 169 | // move instruction 170 | if(op === "m" || op === "M") { 171 | if(op === "m") { x += parseFloat(terms[0]); y += parseFloat(terms[1]); } 172 | else if(op === "M") { x = parseFloat(terms[0]) + xoffset; y = parseFloat(terms[1]) + yoffset; } 173 | // add a point only if the next operation is not another move operation, or a close operation 174 | if(s1) { receiver.closeShape(); } 179 | receiver.startShape(); 180 | receiver.addPoint(x, y); }}} 181 | 182 | // line instructions 183 | else if(op === "l" || op === "L") { 184 | // this operation take a series of [x2 y2] coordinates 185 | for(let t=0; t { 193 | if(op === "v") { y += parseFloat(y2); } 194 | else if(op === "V" ){ y = parseFloat(y2) + yoffset; } 195 | receiver.addPoint(x, y); 196 | });} 197 | 198 | 199 | // horizontal line shorthand 200 | else if(op === "h" || op === "H") { 201 | terms.forEach( x2 => { 202 | if(op === "h") { x += parseFloat(x2); } 203 | else if(op === "H" ){ x = parseFloat(x2) + yoffset; } 204 | receiver.addPoint(x, y); 205 | });} 206 | 207 | 208 | // quadratic curve instruction 209 | else if(op === "q" || op === "Q") { 210 | // this operation takes a series of [cx cy x2 y2] coordinates 211 | for(let q = 0; q svg += s.toSVG()); 26 | return svg; 27 | } 28 | 29 | // start a shape group 30 | startGroup() { this.curveshapes = []; } 31 | 32 | // start a new shape in the group 33 | startShape(){ this.current = new PointCurveShape(); } 34 | 35 | // add an on-screen point 36 | addPoint(x, y){ 37 | this.current.addPoint(x,y); 38 | var bounds = this.bounds; 39 | if (!bounds) { bounds = this.bounds = [x,y,x,y]; } 40 | if (xbounds[MAXX]) { bounds[MAXX] = x; } 42 | if (ybounds[MAXY]) { bounds[MAXY] = y; }} 44 | 45 | // set the x/y coordinates for the left/right control points 46 | setLeftControl(x, y){ 47 | this.current.setLeft(x,y); } 48 | 49 | setRightControl(x, y){ 50 | this.current.setRight(x,y); } 51 | 52 | // close the current shape 53 | closeShape() { 54 | this.curveshapes.push(this.current); 55 | this.current=null; } 56 | 57 | // close the group of shapes. 58 | closeGroup(){ 59 | this.curveshapes.push(this.current); 60 | this.current=null; } 61 | } 62 | 63 | export default Outline; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-svg-path", 3 | "version": "1.1.0", 4 | "description": "", 5 | "main": "library.js", 6 | "scripts": { 7 | "test": "npm run build && node test", 8 | "build": "rollup index.js --format umd --name PathConverter > library.js" 9 | }, 10 | "engine": { 11 | "node": "^4.x.x" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Pomax/js-svg-path.git" 16 | }, 17 | "keywords": [ 18 | "svg", 19 | "path", 20 | "javascript" 21 | ], 22 | "author": "Pomax", 23 | "license": "Public Domain", 24 | "bugs": { 25 | "url": "https://github.com/Pomax/js-svg-path/issues" 26 | }, 27 | "homepage": "https://github.com/Pomax/js-svg-path#readme", 28 | "dependencies": {}, 29 | "devDependencies": { 30 | "rollup": "^0.41.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /point-curve-shape.js: -------------------------------------------------------------------------------- 1 | import Point from "./point.js"; 2 | 3 | /** 4 | * A shape defined in terms of curve points 5 | */ 6 | class PointCurveShape { 7 | constructor() { this.points = []; } 8 | current() { return this.points.slice(-1)[0]; } 9 | addPoint(x, y) { this.points.push(new Point(x,y)); } 10 | setLeft(x, y) { this.current().setLeft(x,y); } 11 | setRight(x, y) { this.current().setRight(x,y); } 12 | 13 | /** 14 | * Convert the point curve to an SVG path string 15 | * (bidirectional conversion? You better believe it). 16 | * This code is very similar to the draw method, 17 | * since it effectively does the same thing. 18 | */ 19 | toSVG() { 20 | // first vertex 21 | let points = this.points; 22 | let first = points[0].getPoint(); 23 | let x = first.x; 24 | let y = first.y; 25 | let svg = "M" + x + (y<0? y : " " + y); 26 | // rest of the shape 27 | for(let p=1; p1 ? instruction.substring(2).trim().split(" ") : []); 31 | 32 | // move instruction 33 | if(op === "m" || op === "M") { 34 | if(op === "m") { x += parseFloat(terms[0]); y += parseFloat(terms[1]); } 35 | else if(op === "M") { x = parseFloat(terms[0]) + xoffset; y = parseFloat(terms[1]) + yoffset; } 36 | // add a point only if the next operation is not another move operation, or a close operation 37 | if(s1) { receiver.closeShape(); } 42 | receiver.startShape(); 43 | receiver.addPoint(x, y); }}} 44 | 45 | // line instructions 46 | else if(op === "l" || op === "L") { 47 | // this operation take a series of [x2 y2] coordinates 48 | for(let t=0; t { 56 | if(op === "v") { y += parseFloat(y2); } 57 | else if(op === "V" ){ y = parseFloat(y2) + yoffset; } 58 | receiver.addPoint(x, y); 59 | })} 60 | 61 | 62 | // horizontal line shorthand 63 | else if(op === "h" || op === "H") { 64 | terms.forEach( x2 => { 65 | if(op === "h") { x += parseFloat(x2); } 66 | else if(op === "H" ){ x = parseFloat(x2) + yoffset; } 67 | receiver.addPoint(x, y); 68 | })} 69 | 70 | 71 | // quadratic curve instruction 72 | else if(op === "q" || op === "Q") { 73 | // this operation takes a series of [cx cy x2 y2] coordinates 74 | for(let q = 0; q { if (bbox[idx]!==v) process.exit(1); }); 5 | --------------------------------------------------------------------------------