├── .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 |
17 |
18 |
23 |
24 |
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 |
--------------------------------------------------------------------------------