├── .gitignore ├── index.js ├── .travis.yml ├── LICENSE ├── package.json ├── src └── svg-path-utils.js ├── README.md └── test └── svg-path-utils-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export {default as SVGPathUtils} from './src/svg-path-utils'; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '4.2' 5 | 6 | branch: 7 | only: 8 | - master 9 | 10 | script: 11 | - npm test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Konstantin Skipor 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 5 | and associated documentation files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 13 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 14 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 15 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 16 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg-path-utils", 3 | "version": "0.0.3", 4 | "description": "Some utils for SVG's path data like path data generation, inverse path data calculation, ...", 5 | "keywords": [ 6 | "svg", 7 | "utils", 8 | "path", 9 | "inverse path" 10 | ], 11 | "homepage": "https://github.com/krispo/svg-path-utils", 12 | "license": "MIT", 13 | "main": "build/svg-path-utils.js", 14 | "jsnext:main": "index", 15 | "scripts": { 16 | "uglify": "uglifyjs build/svg-path-utils.js -c -m -o build/svg-path-utils.min.js", 17 | "build": "mkdir -p build && VERSION=`node -e 'console.log(require(\"./package.json\").version)'` && rollup -f umd --banner \"/* svg-path-utils, v${VERSION} */\" -n svg_path_utils -o build/svg-path-utils.js -- index.js && npm run uglify", 18 | "pretest": "npm run build", 19 | "test": "faucet test/*-test.js", 20 | "prepublish": "npm run build" 21 | }, 22 | "author": "Konstantin Skipor", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/krispo/svg-path-utils.git" 26 | }, 27 | "devDependencies": { 28 | "faucet": "0.0.1", 29 | "rollup": "^0.25.4", 30 | "tape": "^4.4.0", 31 | "uglify-js": "^2.6.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/svg-path-utils.js: -------------------------------------------------------------------------------- 1 | export default SVGPathUtils; 2 | 3 | var SVGPathUtils = function(){ 4 | var utils = {}; 5 | 6 | // uppercase (M) - absolute coordinates, lowercase (m) - relative coordinates 7 | // m - moveto 8 | // l - lineto 9 | // h - horizontal lineto 10 | // v - vertical lineto 11 | // c - cubic bezier curveto 12 | // s - smooth cubic bezier curveto 13 | // q - quadratic bezier curveto 14 | // t - smooth quadratic bezier 15 | // a - elliptical arc (+ to add) 16 | // z - close path 17 | utils.M = function M(x,y){ return 'M' + x + ',' + y }; 18 | utils.m = function m(x,y){ return 'm' + x + ',' + y }; 19 | utils.L = function L(x,y){ return 'L' + x + ',' + y }; 20 | utils.l = function l(x,y){ return 'l' + x + ',' + y }; 21 | utils.H = function H(x){ return 'H' + x }; 22 | utils.h = function h(x){ return 'h' + x }; 23 | utils.V = function V(y){ return 'V' + y }; 24 | utils.v = function v(y){ return 'v' + y }; 25 | utils.C = function C(x1,y1,x2,y2,x,y){ return 'C' + x1 + ',' + y1 + ' ' + x2 + ',' + y2 + ' ' + x + ',' + y }; 26 | utils.c = function c(x1,y1,x2,y2,x,y){ return 'c' + x1 + ',' + y1 + ' ' + x2 + ',' + y2 + ' ' + x + ',' + y }; 27 | utils.S = function S(x2,y2,x,y){ return 'S' + x2 + ',' + y2 + ' ' + x + ',' + y }; 28 | utils.s = function s(x2,y2,x,y){ return 's' + x2 + ',' + y2 + ' ' + x + ',' + y }; 29 | utils.Q = function Q(x1,y1,x,y){ return 'Q' + x1 + ',' + y1 + ' ' + x + ',' + y }; 30 | utils.q = function q(x1,y1,x,y){ return 'q' + x1 + ',' + y1 + ' ' + x + ',' + y }; 31 | utils.T = function T(x,y){ return 'T' + x + ',' + y }; 32 | utils.t = function t(x,y){ return 't' + x + ',' + y }; 33 | utils.Z = function Z(){ return 'Z' }; 34 | utils.z = function z(){ return 'z' }; 35 | 36 | utils.angle = function angle(p1, p2){ 37 | return Math.atan2(p2.y - p1.y, p2.x - p1.x)*180/Math.PI; 38 | } 39 | 40 | utils.parse = function(d){ 41 | var operators = d.replace(/[\d,\-\s]+/g, '').split(''); 42 | var points = []; 43 | 44 | var nums = d.replace(/[A-Za-z,]+/g, ' ').trim().replace(/\s\s+/g, ' ').split(' '); 45 | 46 | var f, i = -1; 47 | operators.forEach(function(key){ 48 | if (typeof (f = utils[key]) === 'function') { 49 | if (f.length === 1) { 50 | points.push({ x: +nums[++i] }) 51 | } else { 52 | for (var j = -1, l = f.length/2; ++j < l;) { 53 | points.push({ x: +nums[++i], y: +nums[++i] }) 54 | } 55 | } 56 | } 57 | }); 58 | 59 | return { operators: operators, points: points } 60 | } 61 | 62 | utils.generate = function(_){ 63 | var p = _.points.slice(); 64 | var str = []; 65 | var f; 66 | _.operators.forEach(function(key){ 67 | if (typeof (f = utils[key]) === 'function') { 68 | var args = []; 69 | if (f.length === 1) { 70 | args.push(p.shift().x); 71 | } else { 72 | for (var i = -1, l = f.length/2; ++i < l;) { 73 | var point = p.shift(); 74 | args.push(point.x, point.y); 75 | } 76 | } 77 | str.push(f.apply(null, args)); 78 | } 79 | }); 80 | return str.join(' '); 81 | } 82 | 83 | utils.inversePath = function(d){ 84 | var _ = utils.parse(d); 85 | var ro = _.operators.reverse(); 86 | var rp = _.points.reverse(); 87 | 88 | //define first and last operators (M, Z) 89 | var first, last; 90 | first = ro.pop(); 91 | if ((last = ro[0]).toLowerCase() === 'z') { 92 | ro.push(last); 93 | ro.shift(); 94 | } 95 | ro.unshift(first); 96 | 97 | return utils.generate({ operators: ro, points: rp }); 98 | } 99 | 100 | utils.join = function(){ 101 | if (!arguments.length) return; 102 | return Array.prototype.join.call(arguments, ' '); 103 | } 104 | 105 | return utils; 106 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svg-path-utils 2 | [![Build Status](https://travis-ci.org/krispo/svg-path-utils.svg?branch=master)](https://travis-ci.org/krispo/svg-path-utils) 3 | [![NPM Version](http://img.shields.io/npm/v/svg-path-utils.svg?style=flat)](https://www.npmjs.org/package/svg-path-utils) 4 | 5 | Some utilities for svg [path](https://www.w3.org/TR/SVG/paths.html) element to help operating with [path data](https://www.w3.org/TR/SVG/paths.html#PathData), 6 | like calculating inverse path data... 7 | 8 | ## Install 9 | 10 | npm install svg-path-utils 11 | 12 | ## Usage 13 | 14 | ```js 15 | var spu = require('svg-path-utils'); 16 | var utils = new spu.SVGPathUtils(); 17 | ``` 18 | or in es6 19 | ```js 20 | import {SVGPathUtils} from 'svg-path-utils'; 21 | const utils = new SVGPathUtils(); 22 | ``` 23 | then 24 | ```js 25 | // generate path data 26 | const d = utils.join(utils.M(50,100), utils.L(200,300), utils.Z()); // d = "M50,100 L200,300 Z" 27 | 28 | // calculate inverse path data 29 | const inverse_d = utils.inversePath(d); // inverse_d = "M200,300 L50,100 Z" 30 | ``` 31 | 32 | ## Example 33 | Inverse path data calculation 34 | 35 | | Direct Path | Inverse Path | 36 | |:-------------:|:-------------:| 37 | | | | 38 | | `d="M50,300 L50,250 C50,150 75,150 100,250 C150,450 200,450 200,250 Q200,100 400,100"` | `d="M400,100 Q200,100 200,250 C200,450 150,450 100,250 C75,150 50,150 50,250 L50,300"`| 39 | 40 | Check this [online demo](http://plnkr.co/edit/rIhZfI?p=preview). 41 | 42 | It is also used in [Yarrow](http://krispo.github.io/yarrow/). 43 | 44 | ## API Reference 45 | 46 | #### Path data commands (operators) 47 | 48 | Uppercase (M, L, H, ...) - uses absolute coordinates, lowercase (m, l, h, ...) - uses relative coordinates 49 | 50 | # utils.M(x, y) - [“moveto” commands](http://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands). 51 | 52 | # utils.m(x, y) 53 | 54 | # utils.L(x, y) - [“lineto” commands](http://www.w3.org/TR/SVG/paths.html#PathDataLinetoCommands). 55 | 56 | # utils.l(x, y) 57 | 58 | # utils.H(x) - horizontal [“lineto” commands](http://www.w3.org/TR/SVG/paths.html#PathDataLinetoCommands). 59 | 60 | # utils.h(x) 61 | 62 | # utils.V(y) - vertical [“lineto” commands](http://www.w3.org/TR/SVG/paths.html#PathDataLinetoCommands). 63 | 64 | # utils.v(y) 65 | 66 | # utils.C(x1, y1, x2, y2, x, y) - [cubic Bézier curve commands](http://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands). 67 | 68 | # utils.c(x1, y1, x2, y2, x, y) 69 | 70 | # utils.S(x2, y2, x, y) - smooth [cubic Bézier curve commands](http://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands). 71 | 72 | # utils.s(x2, y2, x, y) 73 | 74 | # utils.Q(x1, y1, x, y) - [quadratic Bézier curve commands](http://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands). 75 | 76 | # utils.q(x1, y1, x, y) 77 | 78 | # utils.T(x, y) - smooth [quadratic Bézier curve commands](http://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands). 79 | 80 | # utils.t(x, y) 81 | 82 | # utils.Z() - [“closepath” commands](http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand). 83 | 84 | # utils.z() 85 | 86 | > Todo: add .A(...) and .a(...) - [elliptical arc curve commands](http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands). 87 | 88 | Example of usage: 89 | 90 | ```js 91 | utils.M(50,100) // return: "M50,100" 92 | utils.L(200,300) // return: "L200,300" 93 | utils.c(10,20,30,40,50,60) // return: "c10,20 30,40 50,60" 94 | utils.Z() // return: "Z" 95 | ``` 96 | 97 | #### Path data utils 98 | 99 | # utils.parse(d) 100 | 101 | Parse path data *d* into sequence of operators ['M', 'L', ...] and sequence of principal points [*(x,y)*]. 102 | 103 | ```js 104 | utils.parse("M50,100 L200,300 H100"); 105 | /* 106 | return: 107 | { 108 | operators: ["M", "L", "H"], 109 | points: [ 110 | {x: 50, y: 100}, 111 | {x: 200, y: 300}, 112 | {x: 100} 113 | ] 114 | } 115 | */ 116 | ``` 117 | 118 | # utils.generate({ operators: [operators], points: [points]}) 119 | 120 | Generate path data *d* from a sequence of operators ['M', 'L', ...] and sequence of principal points [*(x,y)*]. 121 | 122 | ```js 123 | const data = { 124 | operators: ["M", "L", "H"], 125 | points: [ 126 | {x: 50, y: 100}, 127 | {x: 200, y: 300}, 128 | {x: 100} 129 | ] 130 | } 131 | utils.generate(data); // return: "M50,100 L200,300 H100" 132 | ``` 133 | 134 | # utils.inversePath(d) 135 | 136 | Calculate and return inverse path data for path data *d*. 137 | 138 | ```js 139 | utils.inversePath("M50,100 L200,300 Z"); // return: "M200,300 L50,100 Z" 140 | ``` 141 | 142 | # utils.join(args) 143 | 144 | Join input args into a string with space (' ') separator. 145 | 146 | ```js 147 | utils.join("M50,100", "L200,300", "Z"); // return: "M50,100 L200,300 Z" 148 | utils.join(utils.M(50,100), utils.L(200,300), utils.Z()); // return: "M50,100 L200,300 Z" 149 | ``` 150 | 151 | ## Licence 152 | MIT -------------------------------------------------------------------------------- /test/svg-path-utils-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | SVGPathUtils = require('../') 3 | ; 4 | 5 | test('svg-path-utils tests', function(t){ 6 | var utils = new SVGPathUtils.SVGPathUtils(); 7 | 8 | t.test('.M ', function(t){ 9 | t.equal(utils.M(10,20), 'M10,20'); 10 | t.end(); 11 | }); 12 | t.test('.m ', function(t){ 13 | t.equal(utils.m(10,20), 'm10,20'); 14 | t.end(); 15 | }); 16 | 17 | t.test('.L ', function(t){ 18 | t.equal(utils.L(10,20), 'L10,20'); 19 | t.end(); 20 | }); 21 | t.test('.l ', function(t){ 22 | t.equal(utils.l(10,20), 'l10,20'); 23 | t.end(); 24 | }); 25 | 26 | t.test('.H ', function(t){ 27 | t.equal(utils.H(10), 'H10'); 28 | t.end(); 29 | }); 30 | t.test('.h ', function(t){ 31 | t.equal(utils.h(10), 'h10'); 32 | t.end(); 33 | }); 34 | 35 | t.test('.V ', function(t){ 36 | t.equal(utils.V(10), 'V10'); 37 | t.end(); 38 | }); 39 | t.test('.v ', function(t){ 40 | t.equal(utils.v(10), 'v10'); 41 | t.end(); 42 | }); 43 | 44 | t.test('.C ', function(t){ 45 | t.equal(utils.C(10,0,400,400,400,200), 'C10,0 400,400 400,200'); 46 | t.end(); 47 | }); 48 | t.test('.c ', function(t){ 49 | t.equal(utils.c(10,0,400,400,400,200), 'c10,0 400,400 400,200'); 50 | t.end(); 51 | }); 52 | 53 | t.test('.S ', function(t){ 54 | t.equal(utils.S(10,0,400,400), 'S10,0 400,400'); 55 | t.end(); 56 | }); 57 | t.test('.s ', function(t){ 58 | t.equal(utils.s(10,0,400,400), 's10,0 400,400'); 59 | t.end(); 60 | }); 61 | 62 | t.test('.Q ', function(t){ 63 | t.equal(utils.Q(10,0,400,400), 'Q10,0 400,400'); 64 | t.end(); 65 | }); 66 | t.test('.q ', function(t){ 67 | t.equal(utils.q(10,0,400,400), 'q10,0 400,400'); 68 | t.end(); 69 | }); 70 | 71 | t.test('.T ', function(t){ 72 | t.equal(utils.T(10,20), 'T10,20'); 73 | t.end(); 74 | }); 75 | t.test('.t ', function(t){ 76 | t.equal(utils.t(10,20), 't10,20'); 77 | t.end(); 78 | }); 79 | 80 | t.test('.Z ', function(t){ 81 | t.equal(utils.Z(), 'Z'); 82 | t.end(); 83 | }); 84 | t.test('.z ', function(t){ 85 | t.equal(utils.z(), 'z'); 86 | t.end(); 87 | }); 88 | 89 | t.test('.parse ', function(t){ 90 | var d = 'M10,200 C10,0 400,400 400,200 L20,30'; 91 | var _ = utils.parse(d); 92 | t.deepEqual(_.operators, ['M', 'C', 'L']); 93 | t.deepEqual(_.points, [{x: 10, y:200}, {x: 10, y: 0}, {x: 400, y:400}, {x: 400, y: 200}, {x: 20, y: 30}]); 94 | t.end(); 95 | }); 96 | 97 | t.test('.parse with spaces', function(t){ 98 | var d = 'M 10 200 C 10 0 400 400 400 200 L 20 30'; 99 | var _ = utils.parse(d); 100 | t.deepEqual(_.operators, ['M', 'C', 'L']); 101 | t.deepEqual(_.points, [{x: 10, y:200}, {x: 10, y: 0}, {x: 400, y: 400}, {x: 400, y: 200}, {x: 20, y: 30}]); 102 | t.end(); 103 | }); 104 | 105 | t.test('.parse complex string', function(t){ 106 | var d = ' M10, 200 C 10 0 400, 400 400 \n,200L20,30 '; 107 | var _ = utils.parse(d); 108 | t.deepEqual(_.operators, ['M', 'C', 'L']); 109 | t.deepEqual(_.points, [{x: 10, y:200}, {x: 10, y: 0}, {x: 400, y: 400}, {x: 400, y: 200}, {x: 20, y: 30}]); 110 | t.end(); 111 | }); 112 | 113 | t.test('.parse complex string with negatives', function(t){ 114 | var d = ' M-10, -200 C -10 0 -400, -400 -400 ,-200L-20,-30 '; 115 | var _ = utils.parse(d); 116 | t.deepEqual(_.operators, ['M', 'C', 'L']); 117 | t.deepEqual(_.points, [{x: -10, y:-200}, {x: -10, y: 0}, {x: -400, y: -400}, {x: -400, y: -200}, {x: -20, y: -30}]); 118 | t.end(); 119 | }); 120 | 121 | t.test('.parse with H and V', function(t){ 122 | var d = 'M10,200 H20 C10,0 400,400 400,200 V30 L20,30'; 123 | var _ = utils.parse(d); 124 | t.deepEqual(_.operators, ['M', 'H', 'C', 'V', 'L']); 125 | t.deepEqual(_.points, [{x: 10, y:200}, {x: 20}, {x: 10, y: 0}, {x: 400, y:400}, {x: 400, y: 200}, {x: 30}, {x: 20, y: 30}]); 126 | t.end(); 127 | }); 128 | 129 | t.test('.parse with H and V without spaces', function(t){ 130 | var d = 'M10,200H20V30h40v50v60z'; 131 | var _ = utils.parse(d); 132 | t.deepEqual(_.operators, ['M', 'H', 'V', 'h', 'v', 'v', 'z']); 133 | t.deepEqual(_.points, [{x: 10, y:200}, {x: 20}, {x: 30}, {x: 40}, {x: 50}, {x: 60}]); 134 | t.end(); 135 | }); 136 | 137 | t.test('.parse complex string with H and V', function(t){ 138 | var d = ' M10, 200 H 20 V 30 H40 C 10 0 400,400 400 200'; 139 | var _ = utils.parse(d); 140 | t.deepEqual(_.operators, ['M', 'H', 'V', 'H', 'C']); 141 | t.deepEqual(_.points, [{x: 10, y:200}, {x: 20}, {x: 30}, {x: 40}, {x: 10, y: 0}, {x: 400, y: 400}, {x: 400, y: 200}]); 142 | t.end(); 143 | }); 144 | 145 | t.test('.parse complex string with negatives H and V', function(t){ 146 | var d = ' M-10, -200 H -20 V -30 H-40 C -10 0 -400,-400 -400 -200'; 147 | var _ = utils.parse(d); 148 | t.deepEqual(_.operators, ['M', 'H', 'V', 'H', 'C']); 149 | t.deepEqual(_.points, [{x: -10, y: -200}, {x: -20}, {x: -30}, {x: -40}, {x: -10, y: 0}, {x: -400, y: -400}, {x: -400, y: -200}]); 150 | t.end(); 151 | }); 152 | 153 | t.test('.generate ', function(t){ 154 | var _ = { 155 | operators: ['M', 'C', 'L'], 156 | points: [{x: 10, y:200}, {x: 10, y: 0}, {x: 400, y:400}, {x: 400, y: 200}, {x: 20, y:30}] 157 | } 158 | var d = utils.generate(_); 159 | t.equal(d, 'M10,200 C10,0 400,400 400,200 L20,30'); 160 | t.end(); 161 | }); 162 | 163 | t.test('.generate with negatives', function(t){ 164 | var _ = { 165 | operators: ['M', 'C', 'L'], 166 | points: [{x: -10, y: -200}, {x: -10, y: 0}, {x: -400, y: -400}, {x: -400, y: -200}, {x: -20, y: -30}] 167 | } 168 | var d = utils.generate(_); 169 | t.equal(d, 'M-10,-200 C-10,0 -400,-400 -400,-200 L-20,-30'); 170 | t.end(); 171 | }); 172 | 173 | t.test('.generate with H and V', function(t){ 174 | var _ = { 175 | operators: ['M', 'H', 'C', 'V', 'L'], 176 | points: [{x: 10, y: 200}, {x: 20}, {x: 10, y: 0}, {x: 400, y: 400}, {x: 400, y: 200}, {x: 30}, {x: 20, y: 30}] 177 | } 178 | var d = utils.generate(_); 179 | t.equal(d, 'M10,200 H20 C10,0 400,400 400,200 V30 L20,30'); 180 | t.end(); 181 | }); 182 | 183 | t.test('.generate with negatives H and V', function(t){ 184 | var _ = { 185 | operators: ['M', 'H', 'C', 'V', 'L'], 186 | points: [{x: -10, y: -200}, {x: -20}, {x: -10, y: 0}, {x: -400, y: -400}, {x: -400, y: -200}, {x: -30}, {x: -20, y: -30}] 187 | } 188 | var d = utils.generate(_); 189 | t.equal(d, 'M-10,-200 H-20 C-10,0 -400,-400 -400,-200 V-30 L-20,-30'); 190 | t.end(); 191 | }); 192 | 193 | 194 | 195 | 196 | t.test('.inversePath ', function(t){ 197 | var d = 'M10,200 C10,0 400,400 400,200 L20,30'; 198 | var inverse_d = utils.inversePath(d); 199 | t.equal(inverse_d, 'M20,30 L400,200 C400,400 10,0 10,200'); 200 | t.end(); 201 | }); 202 | 203 | t.test('.join ', function(t){ 204 | t.equal(utils.join('foo','bar','buz'), 'foo bar buz'); 205 | t.equal(utils.join(utils.M(10,20), utils.L(200, 100), utils.C(300,100,300,0,0,0), utils.Z()), 'M10,20 L200,100 C300,100 300,0 0,0 Z') 206 | t.end(); 207 | }); 208 | 209 | t.end(); 210 | }); --------------------------------------------------------------------------------