├── README.md ├── SmartSVGPath.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | 2 | # smart-svg-path 3 | 4 | #### SmartSVGPath.js smarter easier SVG path management methods. 5 | #### For total mastery of SVG path data. Make fine tuning complex SVG path animations easier. 6 | 7 | #### Can't find how to: 8 | ##### - Reverse an SVG path. 9 | ##### - Set any path vertex as a path's first vertex. 10 | #### ...I couldn't either! 11 | ##### - Automatically Convert all SVG elements (individually or by collection) to paths, keeping all other attributes. 12 | ##### - Automatically Convert geometric arcTo commands to cubic Bezier curveTo commands. 13 | ##### - Reverse all, some or just specific paths in a collection and not others. 14 | ##### - Reverse all, some or just specific subPaths in a path and not others. 15 | ##### - and more... 16 | 17 | We don't always control the SVG output from Illustrator, Inkscape etc... even when we do, 18 | paths can still come out in an undesired order or direction for animation. Manipulating targeted 19 | problem path data is probably a lot quicker, (now easier) and smarter than having the artist/designer 20 | have to redo work they have already completed just in a different order. 21 | 22 | SmartSVGPath came about because I could not find a library that knew how to reverse an SVG path! 23 | 24 | So reversing SVG path data, and arbitrarily changing which vertex is the starting vertex on a path 25 | are at least two features of this light weight library which you will have trouble finding 26 | elsewhere. Believe me, I looked pretty hard because I had no idea how to do it. 27 | 28 | ### How to use: 29 | This library is designed to be Node.js and Browser compatible, just drop it where-ever you want it 30 | and access it via the SmartSVGPath name space (or your own alias). 31 | 32 | Don't instantiate SmartSVGPath, its a static 'class', just use it... 33 | ```js 34 | var svg = SmartSVGPath; 35 | var reversedPathString = svg.reverse( pathString ); 36 | ``` 37 | The source code is fully self documenting and commented for education and enlightenment. 38 | The source code is also fully YUIdoc-umented, you can generate YUIDocs locally if I have not yet posted the documentation online. 39 | (I have more pressing matters currently, but a GruntFile with the usual tasks and documentation are on the TODO list.) 40 | Or simply read the source: 41 | 42 | #### 'd' String Methods: 43 | Work directly on the SVGElement attribute 'd' data string. Independant so you can build your own tools incorporating them. 44 | Otherwise SmartSVGPath does provide some basic tools... 45 | 46 | #### SVGShape Methods: 47 | Work directly on both individual and HTMLCollections of DOM SVGElements, automatically: 48 | 49 | - Converting their data and attributes to 'd' attribute data string. 50 | 51 | - Rewriting an existing 'd' attribute data string 52 | 53 | ##### Why the strange SmartSVGPath['method'] = function(){} string naming for method declarations? 54 | 55 | Smarter google Closure compatibility. This convention produces optimised output that does not 56 | depend on, or add additional Closure library bloat to your source code, JUST to access method 57 | name 'symbols'. Closure compiler renames properties in Advanced mode, but it never renames 58 | strings. 59 | 60 | Note the method SmartSVGPath['method'] naming convention is only for the public API methods. 61 | So Closure can still aggressively optimise away on the private stuff. 62 | -------------------------------------------------------------------------------- /SmartSVGPath.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Daniel Kristensen (www.smartArtsStudio.com.au) on 1/03/2016. 3 | * 4 | * The MIT License(MIT) http://opensource.org/licenses/mit-license.html 5 | * 6 | * Copyright(c) 2016 Daniel Kristensen (www.smartArtsStudio.com.au) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | (function ( global ) { 'use strict'; 27 | //////////////////////////////////////////////////////////////////////////////////////////////////// 28 | // // 29 | // // 30 | // ██████ ██ ██████ ██ ██ ██████ ██████ ██ ██ // 31 | // ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ // 32 | // ██ ████████ █████ ████ █████ ██ ██ ██ ██ ██ ██ █████ █████ █████ // 33 | // ██████ ██ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ███ ██████ ██ ██ ██ ██ // 34 | // ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ // 35 | // ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ // 36 | // ██████ ██ ██ ██ █████ ██ ████ ██████ ██ ██████ ██ █████ ████ ██ ██ // 37 | // // 38 | // // 39 | //////////////////////////////////////////////////////////////////////////////////////////////////// 40 | 41 | /** 42 | * Name space for smarter SVG path management methods, to gain total mastery of SVG 43 | * path data to make more advanced SVG animations easier and more enjoyable. 44 | * 45 | * We don't always control the SVG output from Illustrator, Inkscape etc... even when we do, 46 | * paths can still come out in an undesired order or direction for animation. Manipulating targeted 47 | * path data is probably a lot quicker, (now easier) and smarter than having the artist/designer 48 | * have to redo work they have already completed just in a different order. 49 | * 50 | * SmartSVGPath came about because I could not find a javascript library that knew how to 51 | * reverse an SVG path!! So reversing SVG path data, and arbitrarily change which vertex is the 52 | * first vertex are at least two features of this library which you will have trouble finding 53 | * elsewhere. Well at least prior to my release of this library, and I looked pretty hard. 54 | * 55 | * The 'd' String Methods: work directly on the SVGElement attribute 'd' data string. 56 | * SVGElement Methods: work directly on both individual and collections of DOM SVGElements 57 | * automatically: 58 | * - converting there data and attributes to 'd' attribute data string. 59 | * - rewriting an existing 'd' attribute data string. 60 | * 61 | * Node.js and Browser compatible, just drop it where-ever you want it and access it via 62 | * SmartSVGPath.methodName (or your own alias). 63 | * 64 | * Don't instantiate SmartSVGPath, its a static 'class', just use it. 65 | * 66 | * Why the strange SmartSVGPath['method'] string naming for method declarations? 67 | * Smarter google Closure compatibility. This produces optimised output that does not 68 | * depend on, or add additional Closure library bloat to your source code, JUST to access method 69 | * name 'symbols'. Closure compiler renames properties in Advanced mode, but it never renames 70 | * strings. 71 | * 72 | * Note the method SmartSVGPath['method'] naming convention is only for the public API methods. 73 | * So Closure can still aggressively optimise away on the private stuff. 74 | * 75 | * @static 76 | * @class SmartSVGPath 77 | * @constructor 78 | */ 79 | var SmartSVGPath = function () {}; 80 | 81 | //////////////////////////////////////////////////////////////////////////////////////////////////// 82 | // // 83 | // ██ ██████ ██ ██ ██ ██ ██ ██ ██ // 84 | // ██ ██ ██ ███ ███ ██ ██ ██ // 85 | // █████ ██ █████ ████ ██ █████ █████ ███████ █████ █████ █████ █████ █████ █████ // 86 | // ██ ██ ██████ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ // 87 | // ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ █████ // 88 | // ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ // 89 | // █████ ██████ ████ ██ ██ ██ ██ █████ ██ ██ █████ ████ ██ ██ █████ █████ █████ // 90 | // ██ // 91 | // █████ // 92 | // // 93 | //////////////////////////////////////////////////////////////////////////////////////////////////// 94 | 95 | /** 96 | * Returns an array of the commands that make up a path attribute string. 97 | * 98 | * @static 99 | * @public 100 | * @method getCommands 101 | * @param {String} d SVG path attribute string. 102 | * @returns {Array} Array of subPaths. 103 | */ 104 | SmartSVGPath["getCommands"] = function getCommands( d ) { 105 | d = SmartSVGPath.normalize( d ); 106 | // Split string before each command. 107 | var subPaths = d.replace( /([a-zA-Z])\s?/g, '|$1' ).split( '|' ); 108 | // Discard the empty string created by origin split('|M'). 109 | if ( subPaths[0] === '' ) { 110 | subPaths.splice( 0, 1 ); 111 | } 112 | return subPaths; 113 | }; 114 | 115 | /** 116 | * Normalise the data from an SVG path attribute string. 117 | * 118 | * Path strings can be delimited in a variety of ways, SmartSVGPath.normalize cleans up and 119 | * standardises the path string for SmartSVGPath operations. 120 | * 121 | * @static 122 | * @public 123 | * @method normalize 124 | * @param {String} d SVG path attribute string. 125 | * @returns {String|Boolean} d Normalised path attribute string, or FALSE if 'd' not String. 126 | */ 127 | SmartSVGPath["normalize"] = function normalize( d ) { 128 | if ( typeof d !== 'string' ) { 129 | return false 130 | } 131 | d = d.replace( /[,\n\r]/g, ' ' ); // Replace commas, new line and carriage return characters with spaces. 132 | d = d.replace( /-/g, ' - ' ); // Space delimit 'negative' signs. (maybe excessive but safe) 133 | d = d.replace( /-\s+/g, '-' ); // Remove space to the right of 'negative's. 134 | d = d.replace( /([a-zA-Z])/g, ' $1 ' ); // Space delimit the command. 135 | d = d.replace( /((\s|\d)\.\d+)(\.\d)/g, '$1 $3' ); // consume Backus-Naur Form production until character or second decimal is encountered. https://www.w3.org/TR/SVG/paths.html#PathDataBNF 136 | d = d.replace( /\s+/g, ' ' ).trim(); // Compact excess whitespace. 137 | return d; 138 | }; 139 | 140 | /** 141 | * Reverse an SVG path attribute. 142 | * ('Absolute' or 'Relative' path commands.) 143 | * 144 | * @static 145 | * @public 146 | * @method reverse 147 | * @param {string} d SVG attribute path string. 148 | * @param {number} [precision=1] How many decimal places to round coordinates to. 149 | * @returns {string} 150 | */ 151 | SmartSVGPath["reverse"] = function reverse( d, precision ) { 152 | var absolutePath = SmartSVGPath.toAbsolute( d, precision ); 153 | return SmartSVGPath.reverseAbsolute( absolutePath ); 154 | }; 155 | 156 | /** 157 | * Reverse an SVG path attribute string containing absolute path 158 | * drawing commands. 159 | * 160 | * Reverses the path arguments and shifts the path commands left by 161 | * one or two vertices depending on the command: 162 | * 163 | * Shifts L two arguments left, 164 | * Shifts Q two arguments left, 165 | * Shifts C four arguments left, 166 | * Shifts A five arguments left, 167 | * Shifts M to the start, 168 | * and appends Z only if required. 169 | * 170 | * @static 171 | * @public 172 | * @method reverseAbsolute 173 | * @param {string} absolutePath SVG attribute string containing 174 | * 'absolute' path drawing commands. 175 | * @param {number} [precision=1] How many decimal places to round coordinates to. 176 | * @returns {string} Reversed absolute path string. 177 | */ 178 | SmartSVGPath["reverseAbsolute"] = function reverseAbsolute( absolutePath, precision ) { 179 | 180 | absolutePath = SmartSVGPath.normalize( absolutePath ); 181 | 182 | var tokens = absolutePath.split( ' ' ); 183 | var tokensLength = tokens.length; 184 | var reversed = []; 185 | // current point & control points. 186 | var x, y, cx1, cy1, cx2, cy2; 187 | // arcTo arguments. 188 | var rx, ry, xAxisRotation, largeArcFlag, sweepFlag; 189 | // Reversed path data. 190 | var path, reversedPath, closePath; 191 | 192 | for (var i = 0; i < tokensLength; i++) { 193 | var token = tokens[i]; 194 | 195 | // Parse tokens for path command. 196 | switch (token) { 197 | // For each command reverse its arguments and 198 | // shift the command itself appropriately. 199 | case 'M': 200 | { 201 | x = tokens[++i]; 202 | y = tokens[++i]; 203 | reversed.push( y, x ); 204 | break; 205 | } 206 | case 'L': 207 | { 208 | x = tokens[++i]; 209 | y = tokens[++i]; 210 | reversed.push( token, y, x ); 211 | break; 212 | } 213 | case 'Q': 214 | { 215 | cx1 = tokens[++i]; 216 | cy1 = tokens[++i]; 217 | x = tokens[++i]; 218 | y = tokens[++i]; 219 | reversed.push( cy1, cx1, token, y, x ); 220 | break; 221 | } 222 | case 'C': 223 | { 224 | cx1 = tokens[++i]; 225 | cy1 = tokens[++i]; 226 | cx2 = tokens[++i]; 227 | cy2 = tokens[++i]; 228 | x = tokens[++i]; 229 | y = tokens[++i]; 230 | reversed.push( cy1, cx1, cy2, cx2, token, y, x ); 231 | break; 232 | } 233 | case 'A': 234 | { 235 | rx = tokens[++i]; 236 | ry = tokens[++i]; 237 | xAxisRotation = tokens[++i]; 238 | largeArcFlag = tokens[++i]; 239 | // cast string to number, reverse as boolean, cast boolean to number. 240 | sweepFlag = +(!(+tokens[++i])); 241 | x = tokens[++i]; 242 | y = tokens[++i]; 243 | reversed.push( sweepFlag, largeArcFlag, xAxisRotation, ry, rx, token, y, x ); 244 | break; 245 | } 246 | case 'Z': 247 | { 248 | reversed.push( ' Z' ); 249 | break; 250 | } 251 | default: 252 | { 253 | // This token is not an absolute path command! 254 | // ie: lower case relative path command from a non absolute path string, 255 | // or corrupted path output. 256 | var before = tokens.slice( Math.max( i - 4, 0 ), 3 ).join( ' ' ); 257 | var after = tokens.slice( i + 1, Math.min( i + 3, tokensLength - 1 ) ).join( ' ' ); 258 | var span = before + ' >[' + token + ']< ' + after; 259 | throw( 260 | '[SmartSVGPath Error] SmartSVGPath.reverseAbsolute expected absolute SVG path command. At vertex ' + i + ' (' + span + ')\n' + 261 | 'either the path command is not an absolute path, or the path has become corrupted. Please check the data string.'); 262 | } 263 | } 264 | } 265 | // Generate reversed path string. 266 | path = reversed.reverse(); 267 | closePath = /[Z]/.test( path[0] ) ? path.shift() : ''; 268 | reversedPath = 'M ' + path.join( ' ' ) + closePath; 269 | 270 | if ( !!precision ) { 271 | reversedPath = SmartSVGPath.roundDecimals( reversedPath, precision ) 272 | } 273 | return reversedPath; 274 | }; 275 | 276 | /** 277 | * Reverse subPaths contained within an string. 278 | * ('Absolute' or 'Relative' path commands.) 279 | * 280 | * By passing an array of subPath Number indices you wish to reverse, to the subPathIndices 281 | * parameter you can target or exclude specific sub-paths. Or pass TRUE: to reverse ALL sub-paths. 282 | * 283 | * If there are no subPaths with in the string, the whole path is reversed. 284 | * 285 | * @static 286 | * @public 287 | * @method reverseSubPath 288 | * @param {String} path string. 289 | * @param {Array.} [subPathIndices=all] Sub-path (1 based) indices you wish to reverse. 290 | * From the range 1 -> total sub-paths. 291 | * @param {Boolean} [absolute=false] FALSE: (default) passes 'd' into SmartSVGPath.toAbsolute(). 292 | * TRUE: skips ensuring 'd' is in absolute path commands. 293 | * Will throw an error if it is not absolute. 294 | * @returns {String} Reversed path/sub-path string. 295 | */ 296 | SmartSVGPath["reverseSubPath"] = function reverseSubPath( path, subPathIndices, absolute ) { 297 | var indices, subPathArray; 298 | // reverseAbsolute() removes the arc commands which toAbsolute() needs the option to maintain. 299 | var absolutePath = !absolute ? SmartSVGPath.toAbsolute( path ) : path; 300 | // Split sub-paths before 'moveTo' command(s). 301 | var paths = absolutePath.replace( /M/g, '|M' ).split( '|' ); 302 | // Discard the empty string created by origin split('|M'). 303 | paths.splice( 0, 1 ); 304 | 305 | // define subPath indices array... 306 | if ( subPathIndices && subPathIndices.length > 0 ) { 307 | indices = subPathIndices.length; 308 | subPathArray = subPathIndices; 309 | } 310 | else { 311 | // set default to an all subPaths array. 312 | indices = paths.length; 313 | subPathArray = SmartSVGPath._newIndicesVector( indices ); 314 | } 315 | if ( paths.length === 1 ) { 316 | // There are no sub-paths contained within this path. 317 | return SmartSVGPath.reverseAbsolute( absolutePath ); 318 | } 319 | for (var i = 0; i < indices; i++) { 320 | // Normalise 'subPathIndices' 1 based index, to the Array's 0 based index. 321 | var subPathIndex = subPathArray[i] - 1; 322 | var subPath = paths[subPathIndex]; 323 | if ( subPath ) { 324 | paths[subPathIndex] = SmartSVGPath.reverseAbsolute( subPath.trim() ); 325 | } 326 | } 327 | return paths.join( ' ' ); 328 | }; 329 | 330 | 331 | /** 332 | * Ensures SVG attribute path string is in 'absolute' commands. 333 | * (Takes either 'Absolute' or 'Relative' path commands.) 334 | * 335 | * @static 336 | * @public 337 | * @method toAbsolute 338 | * @param {String} d SVG attribute path string. 339 | * @param {Number} [precision=1] How many decimal places to limit coordinates to. 340 | * @returns {String} 341 | */ 342 | SmartSVGPath["toAbsolute"] = function toAbsolute( d, precision/*, reversible*/ ) { 343 | if ( typeof d !== 'string' ) { 344 | return d 345 | } 346 | 347 | var absolutePath = ''; 348 | var args = [], argsLength; 349 | var commands = SmartSVGPath.getCommands( d ); 350 | var commandsLength = commands.length; 351 | var j, name, command, token; 352 | // current point 353 | var x = 0, y = 0; 354 | // start point 355 | var xStart = 0, yStart = 0; 356 | // Bézier curve control points. 357 | var cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0; 358 | // arcTo args 359 | var x1 = 0, y1 = 0, rx = 0, ry = 0; 360 | var xAxisRotation, largeArcFlag, sweepFlag; 361 | var x2 = 0, y2 = 0; 362 | var cubicSubPath; 363 | 364 | for (var i = 0; i < commandsLength; i++) { 365 | 366 | // Tokenise sub-path. 367 | command = commands[i]; 368 | name = command.substring( 0, 1 ); 369 | token = name.toLowerCase(); 370 | args = command.replace( name, '' ).trim().split( ' ' ); 371 | argsLength = args.length; 372 | 373 | // Parse tokens & build absolute path string. 374 | // What's with all the '+args[]'? ...The '+' is a bitwise operation, 375 | // The result of which Strings are returned as Numbers (32bits). 376 | switch (token) { 377 | 378 | /* Lines */ 379 | 380 | // moveTo 381 | case 'm': 382 | { 383 | if ( name === 'm' ) { 384 | x += +args[0]; 385 | y += +args[1]; 386 | } 387 | else { 388 | x = +args[0]; 389 | y = +args[1]; 390 | } 391 | absolutePath += ' M' + x + ' ' + y; 392 | // Cache start vertex, for potential closePath command 'Z/z' 393 | xStart = x; 394 | yStart = y; 395 | if ( argsLength > 2 ) { 396 | // Process implied moveTo command(s). 397 | for (j = 0; j < argsLength; j += 2) { 398 | if ( name === 'm' ) { 399 | x += +args[j]; 400 | y += +args[j + 1]; 401 | } 402 | else { 403 | x = +args[j]; 404 | y = +args[j + 1]; 405 | } 406 | absolutePath += ' L ' + x + ' ' + y; 407 | } 408 | } 409 | break; 410 | } 411 | // lineTo 412 | // Draw a line to the given coordinates. 413 | case 'l': 414 | { 415 | for (j = 0; j < argsLength; j += 2) { 416 | if ( name === 'l' ) { 417 | x += +args[j]; 418 | y += +args[j + 1]; 419 | } 420 | else { 421 | x = +args[j]; 422 | y = +args[j + 1]; 423 | } 424 | absolutePath += ' L ' + x + ' ' + y; 425 | } 426 | break; 427 | } 428 | // Draw a horizontal line to the given x-coordinate. 429 | case 'h': 430 | { 431 | for (j = 0; j < argsLength; j++) { 432 | if ( name === 'h' ) { 433 | x += +args[j]; 434 | } 435 | else { 436 | x = +args[j]; 437 | } 438 | absolutePath += ' L ' + x + ' ' + y; 439 | } 440 | break; 441 | } 442 | // Draw a vertical line to the given x-coordinate. 443 | case 'v': 444 | { 445 | for (j = 0; j < argsLength; j++) { 446 | if ( name === 'v' ) { 447 | y += +args[j]; 448 | } 449 | else { 450 | y = +args[j]; 451 | } 452 | absolutePath += ' L ' + x + ' ' + y; 453 | } 454 | break; 455 | } 456 | 457 | /* Bézier Curves */ 458 | 459 | // Quadratic curveTo: from the current point to (x, y) using control point (cx1, cy1). 460 | case 'q': 461 | { 462 | // In loop to account for 'poly-Bézier' arguments. 463 | for (j = 0; j < argsLength; j += 4) { 464 | if ( name === 'q' ) { 465 | cx1 = x + +args[j]; 466 | cy1 = y + +args[j + 1]; 467 | x += +args[j + 2]; 468 | y += +args[j + 3]; 469 | } 470 | else { 471 | cx1 = +args[j]; 472 | cy1 = +args[j + 1]; 473 | x = +args[j + 2]; 474 | y = +args[j + 3]; 475 | } 476 | absolutePath += ' Q ' + cx1 + ' ' + cy1 + ' ' + x + ' ' + y; 477 | } 478 | break; 479 | } 480 | // Quadratic curveTo: from the current point to (x, y) using a control point 481 | // which is the reflection of the previous Q command's control point, 482 | // else the current control point. 483 | case 't': 484 | { 485 | // In loop to account for 'poly-Bézier' arguments. 486 | for (j = 0; j < argsLength; j += 2) { 487 | // Reflect previous control point across current point's new vector. 488 | cx1 = x + (x - cx1); 489 | cy1 = y + (y - cy1); 490 | // New end vertex to interpolate to. 491 | if ( name === 't' ) { 492 | x += +args[j]; 493 | y += +args[j + 1]; 494 | } 495 | else { 496 | x = +args[j]; 497 | y = +args[j + 1]; 498 | } 499 | absolutePath += ' Q ' + cx1 + ' ' + cy1 + ' ' + x + ' ' + y; 500 | } 501 | break; 502 | 503 | } 504 | // Cubic curveTo: from the current point to (x, y) using control point (cx1, cy1) 505 | // as the control point for the beginning of the curve and (cx2, cy2) as the 506 | // control point for the endpoint of the curve. 507 | case 'c': 508 | { 509 | // In loop to account for 'poly-Bézier' arguments. 510 | for (j = 0; j < argsLength; j += 6) { 511 | if ( name === 'c' ) { 512 | cx1 = x + +args[j]; 513 | cy1 = y + +args[j + 1]; 514 | cx2 = x + +args[j + 2]; 515 | cy2 = y + +args[j + 3]; 516 | x += +args[j + 4]; 517 | y += +args[j + 5]; 518 | } 519 | else { 520 | cx1 = +args[j]; 521 | cy1 = +args[j + 1]; 522 | cx2 = +args[j + 2]; 523 | cy2 = +args[j + 3]; 524 | x = +args[j + 4]; 525 | y = +args[j + 5]; 526 | } 527 | absolutePath += ' C ' + cx1 + ' ' + cy1 + ' ' + cx2 + ' ' + cy2 + ' ' + x + ' ' + y; 528 | } 529 | break; 530 | } 531 | // Cubic curveTo: from the current point to (x, y), using (cx2, cy2) as the control 532 | // point for this new endpoint. The first control point will be the reflection 533 | // of the previous C command's ending control point. If there is no previous curve, 534 | // the current point will be used as the first control point. 535 | case 's': 536 | { 537 | // In loop to account for 'poly-Bézier' arguments. 538 | for (j = 0; j < argsLength; j += 4) { 539 | // Reflect previous control point across x/y. 540 | cx1 = x + (x - cx2); 541 | cy1 = y + (y - cy2); 542 | // New end vertex to interpolate to. 543 | if ( name === 's' ) { 544 | cx2 = x + +args[j]; 545 | cy2 = y + +args[j + 1]; 546 | x += +args[j + 2]; 547 | y += +args[j + 3]; 548 | } 549 | else { 550 | cx2 = +args[j]; 551 | cy2 = +args[j + 1]; 552 | x = +args[j + 2]; 553 | y = +args[j + 3]; 554 | } 555 | absolutePath += ' C ' + cx1 + ' ' + cy1 + ' ' + cx2 + ' ' + cy2 + ' ' + x + ' ' + y; 556 | } 557 | break; 558 | } 559 | 560 | /* Geometric Arc curve */ 561 | 562 | // arcTo: from the current (x, y) point using control point (cx1, cy1) 563 | // as the control point for the beginning of the curve and (cx2, cy2) as the 564 | // control point for the endpoint of the curve (x2, y2). 565 | case 'a': 566 | { 567 | x1 = x; 568 | y1 = y; 569 | rx = +args[0]; 570 | ry = +args[1]; 571 | xAxisRotation = +args[2]; 572 | largeArcFlag = +args[3]; 573 | sweepFlag = +args[4]; 574 | 575 | if ( name === 'a' ) { 576 | x2 = x += +args[5]; 577 | y2 = y += +args[6]; 578 | } 579 | else { 580 | x2 = x = +args[5]; 581 | y2 = y = +args[6]; 582 | } 583 | // if ( !!reversible ) { 584 | // // convert arcs to concatenated Bézier curves 585 | // cubicSubPath = SmartSVGPath.arcToCurve( x1, y1, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x2, y2 ); 586 | // // cubic arguments may contain 'poly-Bézier' arguments (chained Bézier curves), 587 | // // which are not 'absolute' path notation, so toAbsolute( command )... 588 | // absolutePath += ' '+ SmartSVGPath.toAbsolute( cubicSubPath, !!reversible ); 589 | // } 590 | // else { 591 | absolutePath += ' A ' + rx + ' ' + ry + ' ' + xAxisRotation + ' ' + largeArcFlag + ' ' 592 | + sweepFlag + ' ' + x + ' ' + y; 593 | // } 594 | break; 595 | } 596 | // closePath 597 | case 'z': 598 | { 599 | // closePath... 600 | absolutePath += ' Z'; 601 | // ...updates 'current vertex' to the start vertex. 602 | // So 'moveTo' start vertex: 603 | x = xStart; 604 | y = yStart; 605 | break; 606 | } 607 | } 608 | } 609 | 610 | if ( !!precision ) { 611 | absolutePath = SmartSVGPath.roundDecimals( absolutePath, precision ) 612 | } 613 | return absolutePath.trim(); 614 | }; 615 | 616 | /** 617 | * Converts the arguments for an 'A/a' arcTo command which generates a geometric arcs, to a 618 | * 'C/c' command that generates an equivalent cubic Bézier curve (path command). 619 | * 620 | * @static 621 | * @public 622 | * @method arcToCurve 623 | * @param x1 current x Absolute coordinates of the current point on 624 | * @param y1 current y the path, obtained from the last two arguments 625 | * of the previous path command. 626 | * @param rx radius x X-axis radius. 627 | * @param ry radius y Y-axis radius. 628 | * @param angle x-axis-rotation From the x-axis of the current coordinate system 629 | * to the x-axis of the ellipse. 630 | * @param largeArcFlag 0 if an arc spanning less than or equal to 180 631 | * degrees is chosen, or 1 if an arc spanning greater 632 | * than 180 degrees is chosen. 633 | * @param sweepFlag 0 if the line joining center to arc sweeps through 634 | * decreasing angles, or 1 if it sweeps through 635 | * increasing angles. 636 | * @param x2 end at X X co-ordinate to interpolate to. 637 | * @param y2 end at Y Y co-ordinate to interpolate to. 638 | * @param polyCubic This cubic-Bézier continues from the arguments 639 | * immediately prior. 640 | * @returns {String} cubic curveTo Cubic (poly)Bézier sub-path. 641 | */ 642 | SmartSVGPath["arcToCurve"] = function arcToCurve( x1, y1, rx, ry, angle, largeArcFlag, sweepFlag, x2, y2, polyCubic ) { 643 | return ' C ' + SmartSVGPath._arcToCubicArgs( x1, y1, rx, ry, angle, largeArcFlag, sweepFlag, x2, y2, polyCubic ).toString(); 644 | }; 645 | 646 | /** 647 | * Converts 'A/a' arcTo command's arguments to an array of cubic Bézier ARGUMENTS. 648 | * 649 | * NOTE: Returns cubic Bézier ARGUMENTS *NOT* a cubic Bézier path. 650 | * SmartSVGPath.arcToCurve converts the _arcToCubicArgs array to a path. 651 | * 652 | * http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes 653 | * 6.3 Parameterization alternatives: 654 | * 655 | * @private 656 | * @method _arcToCubicArgs 657 | * @param {number} x1 658 | * @param {number} y1 659 | * @param {number} rx 660 | * @param {number} ry 661 | * @param {number} angle 662 | * @param {number} largeArcFlag 663 | * @param {number} sweepFlag 664 | * @param {number} x2 665 | * @param {number} y2 666 | * @param {Array} polyCubic 667 | * @returns {Array} 668 | * @private 669 | */ 670 | SmartSVGPath._arcToCubicArgs = function _arcToCubicArgs( x1, y1, rx, ry, angle, largeArcFlag, sweepFlag, x2, y2, polyCubic ) { 671 | var rotate = SmartSVGPath._arcToCubicArgs.rotate; 672 | var sqrt = Math.sqrt; 673 | var abs = Math.abs; 674 | var asin = Math.asin; 675 | var cos = Math.cos; 676 | var sin = Math.sin; 677 | var tan = Math.tan; 678 | var PI = Math.PI; 679 | var degrees120 = PI * 120 / 180; 680 | var radians = PI / 180 * (+angle || 0); 681 | var vector; 682 | var args = []; 683 | 684 | if ( !polyCubic ) { 685 | // Get path vector. 686 | vector = rotate( x1, y1, -radians ); 687 | x1 = vector.x; 688 | y1 = vector.y; 689 | vector = rotate( x2, y2, -radians ); 690 | x2 = vector.x; 691 | y2 = vector.y; 692 | var x = (x1 - x2) / 2; 693 | var y = (y1 - y2) / 2; 694 | // pre calculate 'squared' values. 695 | var rx2 = rx * rx; 696 | var ry2 = ry * ry; 697 | var xx2 = x * x; 698 | var yy2 = y * y; 699 | var yLength = xx2 / rx2 + yy2 / ry2; 700 | if ( yLength > 1 ) { 701 | // Get arc radii. 702 | yLength = sqrt( yLength ); 703 | rx = yLength * rx; 704 | ry = yLength * ry; 705 | } 706 | // Which side of the cubic path should control points be? 707 | var normal = (largeArcFlag === sweepFlag ? -1 : 1) 708 | * sqrt( abs( (rx2 * ry2 - rx2 * yy2 - ry2 * xx2) 709 | / (rx2 * yy2 + ry2 * xx2 ) ) ); 710 | var cx = normal * (rx * y / ry) + ((x1 + x2) / 2); 711 | var cy = normal * (-ry * x / rx) + ((y1 + y2) / 2); 712 | 713 | // Sweeping ClockWise or CounterClockWise? 714 | var flagA = asin( (y1 - cy) / ry ); 715 | var flagB = asin( (y2 - cy) / ry ); 716 | flagA = x1 < cx ? PI - flagA : flagA; 717 | flagB = x2 < cx ? PI - flagB : flagB; 718 | if ( flagA < 0 ) { 719 | flagA = flagA + PI * 2; 720 | } 721 | if ( flagB < 0 ) { 722 | flagB = flagB + PI * 2; 723 | } 724 | if ( sweepFlag && flagA > flagB ) { 725 | flagA = flagA - PI * 2; 726 | } 727 | if ( !sweepFlag && flagB > flagA ) { 728 | flagB = flagB - PI * 2; 729 | } 730 | } 731 | else { 732 | flagA = polyCubic[0]; 733 | flagB = polyCubic[1]; 734 | cx = polyCubic[2]; 735 | cy = polyCubic[3]; 736 | } 737 | var sweepAngle = flagB - flagA; 738 | if ( abs( sweepAngle ) > degrees120 ) { 739 | var x2old = x2; 740 | var y2old = y2; 741 | var flag2old = flagB; 742 | flagB = flagA + degrees120 * (sweepFlag && flagB > flagA ? 1 : -1); 743 | x2 = cx + rx * cos( flagB ); 744 | y2 = cy + ry * sin( flagB ); 745 | args = SmartSVGPath._arcToCubicArgs( x2, y2, rx, ry, angle, 0, sweepFlag, x2old, y2old, [flagB, flag2old, cx, cy] ); 746 | } 747 | 748 | // Build cubic (poly)Bézier curve command arguments. 749 | sweepAngle = flagB - flagA; 750 | var cosA = cos( flagA ); 751 | var sinA = sin( flagA ); 752 | var cosB = cos( flagB ); 753 | var sinB = sin( flagB ); 754 | var arcUnit = tan( sweepAngle / 4 ); 755 | var fourThirds = 4 / 3; 756 | var xLength = rx * fourThirds * arcUnit; 757 | yLength = ry * fourThirds * arcUnit; 758 | var arg1 = [x1, y1]; 759 | var arg2 = [x1 + xLength * sinA, y1 - yLength * cosA]; 760 | var arg3 = [x2 + xLength * sinB, y2 - yLength * cosB]; 761 | var arg4 = [x2, y2]; 762 | arg2[0] = 2 * arg1[0] - arg2[0]; 763 | arg2[1] = 2 * arg1[1] - arg2[1]; 764 | if ( polyCubic ) { 765 | return [arg2, arg3, arg4].concat( args ); 766 | } 767 | else { 768 | args = [arg2, arg3, arg4].concat( args ).join().split( ',' ); 769 | var cubicArgs = []; 770 | var argsLength = args.length; 771 | for (var i = 0; i < argsLength; i++) { 772 | cubicArgs[i] = i % 2 ? rotate( args[i - 1], args[i], radians ).y : rotate( args[i], args[i + 1], radians ).x; 773 | } 774 | return cubicArgs; 775 | } 776 | }; 777 | 778 | /** 779 | * Rotate vector components by radians. 780 | * 781 | * NOTE: Rotate() is a static field for SmartSVGPath._arcToCubicArgs. 782 | * 783 | * @static 784 | * @private 785 | * @method rotate 786 | * @param {number} x 787 | * @param {number} y 788 | * @param {number} radians 789 | * @returns {{x: number, y: number}} 790 | */ 791 | SmartSVGPath._arcToCubicArgs.rotate = function ( x, y, radians ) { 792 | var X = x * Math.cos( radians ) - y * Math.sin( radians ), 793 | Y = x * Math.sin( radians ) + y * Math.cos( radians ); 794 | return {x: X, y: Y}; 795 | }; 796 | 797 | /** 798 | * Creates a path from any SVGElement. 799 | * 800 | * @static 801 | * @public 802 | * @method fromElement 803 | * @param {SVGElement} svgElement 804 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 805 | * @returns {String} 806 | */ 807 | SmartSVGPath["fromElement"] = function fromElement( svgElement, precision ) { 808 | 809 | if ( !svgElement ) { 810 | return 811 | } 812 | switch (svgElement.nodeName) { 813 | case 'circle': 814 | return SmartSVGPath.fromCircle( svgElement, precision ); 815 | case 'ellipse': 816 | return SmartSVGPath.fromEllipse( svgElement, precision ); 817 | case 'line': 818 | return SmartSVGPath.fromLine( svgElement, precision ); 819 | case 'path': 820 | return SmartSVGPath.fromPath( svgElement, precision ); 821 | case 'polygon': 822 | return SmartSVGPath.fromPolygon( svgElement, precision ); 823 | case 'polyline': 824 | return SmartSVGPath.fromPolyline( svgElement, precision ); 825 | case 'rect': 826 | return SmartSVGPath.fromRect( svgElement, precision ); 827 | } 828 | }; 829 | 830 | /** 831 | * Creates a path from a SVGElement. 832 | * 833 | * @static 834 | * @public 835 | * @method fromCircle 836 | * @param {SVGElement} circleElement 837 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 838 | * @returns {String} 839 | */ 840 | SmartSVGPath["fromCircle"] = function fromCircle( circleElement, precision ) { 841 | /* Specific attributes: cx cy r */ 842 | var cx, cy, r, pathString; 843 | 844 | cx = +circleElement.getAttribute( 'cx' ); 845 | cy = +circleElement.getAttribute( 'cy' ); 846 | r = +circleElement.getAttribute( 'r' ); 847 | 848 | pathString = SmartSVGPath._ellipsePathString( cx, cy, r ); 849 | 850 | if ( pathString.indexOf( '.' ) !== -1 ) { 851 | pathString = SmartSVGPath.roundDecimals( pathString, precision || 1 ); 852 | } 853 | return pathString; 854 | }; 855 | 856 | /** 857 | * Creates a path from an SVGElement. 858 | * 859 | * @static 860 | * @public 861 | * @method fromEllipse 862 | * @param {SVGEllipseElement} ellipseElement 863 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 864 | * @returns {String} 865 | */ 866 | SmartSVGPath["fromEllipse"] = function fromEllipse( ellipseElement, precision ) { 867 | /* Specific attributes: cx cy rx ry */ 868 | var cx, cy, rx, ry, transform, pathString; 869 | var xAxisRotation = 0; 870 | var transformArgs = []; 871 | 872 | cx = +ellipseElement.getAttribute( 'cx' ); 873 | cy = +ellipseElement.getAttribute( 'cy' ); 874 | rx = +ellipseElement.getAttribute( 'rx' ); 875 | ry = +ellipseElement.getAttribute( 'ry' ); 876 | transform = ellipseElement.getAttribute( 'transform' ); 877 | 878 | // Check for x axis rotation. 879 | if ( transform && transform.indexOf( 'matrix' ) !== -1 ) { 880 | transformArgs = transform.replace( /^matrix\s*\(([\d.\s-]+)\)/g, '$1' ).split( /\s|,/ ); 881 | } 882 | if ( transformArgs.length > 0 ) { 883 | xAxisRotation = Math.acos( transformArgs[0] ) * 180 / Math.PI; 884 | if ( transformArgs[transformArgs.length - 1] > 0 ) { 885 | xAxisRotation *= -1; 886 | } 887 | } 888 | pathString = SmartSVGPath._ellipsePathString( cx, cy, rx, ry, xAxisRotation ); 889 | 890 | if ( pathString.indexOf( '.' ) !== -1 ) { 891 | pathString = SmartSVGPath.roundDecimals( pathString, precision || 1 ); 892 | } 893 | return pathString; 894 | }; 895 | 896 | /** 897 | * Creates a path string for an SVGElement, which includes a . 898 | * 899 | * @static 900 | * @public 901 | * @method _ellipsePathString 902 | * @param {number} cx 903 | * @param {number} cy 904 | * @param {number} rx 905 | * @param {number} ry 906 | * @param {number} xAxisRotation 907 | * @returns {String} 908 | */ 909 | SmartSVGPath._ellipsePathString = function _ellipsePathString( cx, cy, rx, ry, xAxisRotation ) { 910 | cx = +cx; 911 | cy = +cy; 912 | rx = +rx; 913 | // No 'ry' or 'xAxisRotation' if it is a . 914 | ry = +ry || rx; 915 | xAxisRotation = +xAxisRotation || 0; 916 | var radians = xAxisRotation * Math.PI / 180; 917 | var x1 = cx - rx * Math.cos( radians ); 918 | var y1 = cy - rx * Math.sin( radians ); 919 | var x2 = x1 + 2 * rx * Math.cos( radians ); 920 | var y2 = y1 + 2 * rx * Math.sin( radians ); 921 | var isLargeArc = +(xAxisRotation - ry > 180); 922 | // Absolute path command. 923 | return 'M ' + x1 + ' ' + y1 + 924 | ' A' + rx + ' ' + ry + ' ' + 0 + ' ' + isLargeArc + ' ' + 0 + ' ' + x2 + ' ' + y2 + 925 | ' A' + rx + ' ' + ry + ' ' + 0 + ' ' + isLargeArc + ' ' + 0 + ' ' + x1 + ' ' + y1 + ' ' + 'Z'; 926 | }; 927 | 928 | /** 929 | * Creates a path from a SVGElement. 930 | * 931 | * @static 932 | * @public 933 | * @method fromLine 934 | * @param {SVGElement} lineElement 935 | * @param {number} [precision=1] How many decimal places to round coordinates to. 936 | * @returns {string} 937 | */ 938 | SmartSVGPath["fromLine"] = function fromLine( lineElement, precision ) { 939 | /* Specific attributes: x1 x2 y1 y2 */ 940 | var x1, y1, x2, y2; 941 | 942 | x1 = +lineElement.getAttribute( 'x1' ); 943 | y1 = +lineElement.getAttribute( 'y1' ); 944 | x2 = +lineElement.getAttribute( 'x2' ); 945 | y2 = +lineElement.getAttribute( 'y2' ); 946 | 947 | var pathString = 948 | 'M ' + x1 + ' ' + y1 949 | + ' L ' + x2 + ' ' + y2; 950 | 951 | if ( pathString.indexOf( '.' ) !== -1 ) { 952 | pathString = SmartSVGPath.roundDecimals( pathString, precision || 1 ); 953 | } 954 | return pathString; 955 | }; 956 | 957 | /** 958 | * Creates a path from a SVGElement. 959 | * 960 | * Arbitrary lineTo points defining any CLOSED path using: closePath 'Z/z'. 961 | * 962 | * @static 963 | * @public 964 | * @method fromLine 965 | * @param {SVGPolygonElement} polygonElement 966 | * @param {number} [precision=1] How many decimal places to round coordinates to. 967 | * @returns {string} 968 | */ 969 | SmartSVGPath["fromPolygon"] = function fromPolygon( polygonElement, precision ) { 970 | /* Specific attributes: closedPath 'Z/z', Points */ 971 | var pathString = SmartSVGPath.fromPolyline( polygonElement, precision ); 972 | return pathString + 'Z'; 973 | }; 974 | 975 | /** 976 | * Creates a path from a SVGElement. 977 | * 978 | * Arbitrary drawing commands defining an OPEN or CLOSED path. 979 | * 980 | * @static 981 | * @public 982 | * @method fromLine 983 | * @param {SVGPolygonElement} pathElement 984 | * @param {number} [precision=1] How many decimal places to round coordinates to. 985 | * @returns {string} 986 | */ 987 | SmartSVGPath["fromPath"] = function fromPath( pathElement, precision ) { 988 | /* Specific attributes: 'd' data string */ 989 | var pathString = SmartSVGPath.normalize( pathElement.attributes.d.nodeValue ); 990 | pathString = SmartSVGPath.toAbsolute( pathString ); 991 | 992 | if ( !!precision && pathString.indexOf( '.' ) !== -1 ) { 993 | pathString = SmartSVGPath.roundDecimals( pathString, precision ); 994 | } 995 | return pathString; 996 | }; 997 | 998 | /** 999 | * Creates a path from a SVGElement. 1000 | * 1001 | * Arbitrary lineTo points defining an OPEN path. 1002 | * 1003 | * @static 1004 | * @public 1005 | * @method fromPoly 1006 | * @param {SVGPolylineElement} polylineElement 1007 | * @param {number} [precision=1] How many decimal places to round coordinates to. 1008 | * @returns {string} 1009 | */ 1010 | SmartSVGPath["fromPolyline"] = function fromPolyline( polylineElement, precision ) { 1011 | /* Specific attributes: Points */ 1012 | var points, pathString; 1013 | points = polylineElement.getAttribute( 'points' ); 1014 | // coordinates and coordinate pairs can be separated by commas or whitespace. 1015 | points = SmartSVGPath.normalize( points ); 1016 | points = points.split( ' ' ); 1017 | 1018 | pathString = 'M' + points[0] + ' ' + points[1]; 1019 | points = points.slice( 2 ); 1020 | var pointsLength = points.length; 1021 | for (var i = 0; i < pointsLength; i += 2) { 1022 | pathString += ' L' + points[i] + ' ' + points[i + 1]; 1023 | } 1024 | 1025 | if ( pathString.indexOf( '.' ) !== -1 ) { 1026 | pathString = SmartSVGPath.roundDecimals( pathString, precision || 1 ); 1027 | } 1028 | return pathString; 1029 | }; 1030 | 1031 | /** 1032 | * Creates a path from a SVGElement. 1033 | * 1034 | * @static 1035 | * @public 1036 | * @method fromRect 1037 | * @param {SVGRectElement} rectElement 1038 | * @param {number} [precision=1] How many decimal places to round coordinates to. 1039 | * @returns {string} 1040 | */ 1041 | SmartSVGPath["fromRect"] = function fromRect( rectElement, precision ) { 1042 | /* Specific attributes: x y width height, rx, ry */ 1043 | var x, y, width, height, rx, ry, pathString; 1044 | var x0, x1, x2, x3, x4, y0, y1, y2 , y3 , y4; 1045 | var sideWidth, SideHeight, arcTo, arc1, arc2, arc3, arc4; 1046 | 1047 | x = +rectElement.getAttribute( 'x' ); 1048 | y = +rectElement.getAttribute( 'y' ); 1049 | width = +rectElement.getAttribute( 'width' ); 1050 | height = +rectElement.getAttribute( 'height' ); 1051 | rx = +rectElement.getAttribute( 'rx' ); 1052 | ry = +rectElement.getAttribute( 'ry' ); 1053 | 1054 | if ( rx && rx !== 0 /*&& ry && ry != 0*/ ) { 1055 | // Rounded rectangle. 1056 | sideWidth = width - rx * 2; 1057 | SideHeight = height - ry * 2; 1058 | // Is it even possible to have an rx without an ry? Just in case... 1059 | ry = ry === 0 ? rx : ry; 1060 | arcTo = 'A ' + rx + ' ' + ry + ' ' + 0 + ' ' + 0 + ' ' + 1 + ' '; 1061 | 1062 | // moveTo 1063 | x0 = x += rx; 1064 | y0 = y; 1065 | // lineTo 1066 | x1 = x += sideWidth; 1067 | y1 = y; 1068 | // arcTo 1069 | arc1 = arcTo + (x += rx) + ' ' + (y += ry); 1070 | // lineTo 1071 | x2 = x; 1072 | y2 = y += SideHeight; 1073 | // arcTo 1074 | arc2 = arcTo + (x -= rx) + ' ' + (y += ry); 1075 | // lineTo 1076 | x3 = x -= sideWidth; 1077 | y3 = y; 1078 | // arcTo 1079 | arc3 = arcTo + (x -= rx) + ' ' + (y -= ry); 1080 | // lineTo 1081 | x4 = x; 1082 | y4 = y -= SideHeight; 1083 | // arcTo 1084 | arc4 = arcTo + (x += rx) + ' ' + (y -= ry); 1085 | 1086 | pathString = 1087 | 'M ' + x0 + ' ' + y0 1088 | + ' L ' + x1 + ' ' + y1 + ' ' + arc1 1089 | + ' L ' + x2 + ' ' + y2 + ' ' + arc2 1090 | + ' L ' + x3 + ' ' + y3 + ' ' + arc3 1091 | + ' L ' + x4 + ' ' + y4 + ' ' + arc4 1092 | + ' Z'; 1093 | } 1094 | else { 1095 | // standard rectangle 1096 | x0 = x; 1097 | y0 = y; 1098 | 1099 | x1 = x += width; 1100 | y1 = y; 1101 | 1102 | x2 = x; 1103 | y2 = y += height; 1104 | 1105 | x3 = x -= width; 1106 | y3 = y; 1107 | 1108 | x4 = x; 1109 | y4 = y -= height; 1110 | 1111 | pathString = 1112 | 'M ' + x0 + ' ' + y0 1113 | + ' L ' + x1 + ' ' + y1 1114 | + ' L ' + x2 + ' ' + y2 1115 | + ' L ' + x3 + ' ' + y3 1116 | + ' L ' + x4 + ' ' + y4 1117 | + ' Z'; 1118 | } 1119 | 1120 | if ( pathString.indexOf( '.' ) !== -1 ) { 1121 | pathString = SmartSVGPath.roundDecimals( pathString, precision || 1 ); 1122 | } 1123 | return pathString; 1124 | }; 1125 | 1126 | /** 1127 | * Checks to see it the Attr Node is contained within the exclude Array. 1128 | * Returns TRUE if it is, FALSE otherwise. 1129 | * 1130 | * @static 1131 | * @public 1132 | * @method isExcluded 1133 | * @param {Attr} attribute Attribute to test. 1134 | * @param {Array.} excluded List of attributes to exclude. 1135 | * @returns {Boolean} TRUE if in exclude list. 1136 | * FALSE otherwise. 1137 | */ 1138 | SmartSVGPath["isExcluded"] = function isExcluded( attribute, excluded ) { 1139 | if ( !attribute || !excluded || !Array.isArray( excluded ) ) { 1140 | return false 1141 | } 1142 | return ( excluded.indexOf( attribute.nodeName ) !== -1 ); 1143 | }; 1144 | 1145 | /** 1146 | * Takes any string and ensures all number's decimal places 1147 | * are rounded off to a maximum of the specified precision. 1148 | * 1149 | * NOTE: For CPU and File size optimisation. 1150 | * 1151 | * @static 1152 | * @public 1153 | * @method roundDecimals 1154 | * @param {String} string SVG path attribute string. 1155 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1156 | * @returns {String} SVG path attribute string. 1157 | * (Decimals rounded by precision.) 1158 | */ 1159 | SmartSVGPath["roundDecimals"] = function roundDecimals( string, precision ) { 1160 | var tokens = string.split( ' ' ); 1161 | var tokensLength = tokens.length; 1162 | 1163 | for (var i = 0; i < tokensLength; i++) { 1164 | var token = tokens[i]; 1165 | if ( token !== '' && Number( token ) ) { 1166 | tokens[i] = SmartSVGPath.smartRound( token, precision || 1 ); 1167 | } 1168 | } 1169 | return tokens.join( ' ' ); 1170 | }; 1171 | 1172 | /** 1173 | * Rounds floating point Numbers to a maximum of the specified decimal places. 1174 | * It does not pad insignificant 0's to enforce the specified decimal places 1175 | * like Javascript's Number.toFixed(). 1176 | * 1177 | * toFixed() is uncompromising, appending insignificant trailing 0's to ensure 1178 | * the decimal places satisfy its argument. 1179 | * 1180 | * Also Javascript toFixed() returns values as String data types not Number data 1181 | * types. If you need the safety ans consistency of a Number, NOT a String: 1182 | * 1183 | * When using Number() to cast floating point Strings values to Number values, 1184 | * certain values change a String values decimal places because 'Number' 1185 | * 'evaluates' Strings it does not blindly copy their content. 1186 | * 1187 | * @static 1188 | * @public 1189 | * @method smartRound 1190 | * @param {Number} number Number or String data type. 1191 | * @param {Number} precision How many decimal places to round to. 1192 | * @returns {Number} 1193 | */ 1194 | SmartSVGPath["smartRound"] = function smartRound( number, precision ) { 1195 | if ( precision && precision !== SmartSVGPath.smartRound.PRECISION ) { 1196 | SmartSVGPath.smartRound.ERROR = precision !== false ? +Math.pow( .1, precision ).toFixed( precision ) : 1e-2; 1197 | SmartSVGPath.smartRound.PRECISION = precision; 1198 | } 1199 | // cast Strings to efficient Numbers (efficient == stripped of insignificant trailing decimal 0's) 1200 | number = +number; 1201 | var rounded = +number.toFixed( precision - 1 ); 1202 | if ( +Math.abs( rounded - number ).toFixed( precision ) > SmartSVGPath.smartRound.ERROR ) { 1203 | number = +number.toFixed( precision ) 1204 | } 1205 | else { 1206 | number = rounded; 1207 | } 1208 | return number; 1209 | // TODO: smartRound: investigate if ?Faster alternative: Math.round(number * 100) / 100 (ie for 2 precision) 1210 | // http://jsperf.com/parsefloat-tofixed-vs-math-round 1211 | }; 1212 | SmartSVGPath.smartRound.PRECISION = 1; 1213 | SmartSVGPath.smartRound.ERROR = 1e-2; 1214 | 1215 | /** 1216 | * Set which vertex on the path is the initial vertex. 1217 | * 1218 | * @static 1219 | * @public 1220 | * @method setFirstVertex 1221 | * @param {String} d SVG path attribute string. 1222 | * @param {String|Number} firstVertex 1223 | * @param {Boolean} [lineToWrap=false] If the path is an OPEN path and the firstVertex 1224 | * is mid path, do you want the path to wrap with a: 1225 | * TRUE: lineTo 'L' command (draw the wrap), or 1226 | * FALSE: moveTo 'M' command (don't draw the wrap) 1227 | * @returns {String|Boolean} SVG path attribute string. 1228 | * (Which starts at the nominated vertex) 1229 | * or FALSE if 'd' is not a string. 1230 | */ 1231 | SmartSVGPath["setFirstVertex"] = function setFirstVertex( d, firstVertex, lineToWrap ) { 1232 | if ( typeof d !== 'string' ) { 1233 | return false 1234 | } 1235 | d = SmartSVGPath.toAbsolute( SmartSVGPath.normalize( d ) ); 1236 | var commands = SmartSVGPath.getCommands( d ); 1237 | var vertices = SmartSVGPath.traceVertices( d ); 1238 | // Convert moveTo command to lineTo. 1239 | commands[0] = commands[0].replace( 'M', 'L' ); 1240 | var last = commands.length - 1; 1241 | // wrap firstVertex value if firstVertex > total vertices. 1242 | firstVertex = +firstVertex <= last ? +firstVertex : +firstVertex % last; 1243 | // Handle CLOSED paths. 1244 | var closePath = /[Zz]/.test( commands[last] ) ? commands.splice( last ) : []; 1245 | // Handle OPEN paths potential vertex wrapping. 1246 | var onWrap = !lineToWrap ? 'M ' : 'L '; 1247 | var wrapPath = closePath.length < 1 ? [onWrap + vertices[firstVertex].from.x + ' ' + vertices[firstVertex].from.y] : ['']; 1248 | 1249 | // Rotate paths to bring new first vertex to the start. 1250 | var splice = commands.splice( 0, +firstVertex ); 1251 | var rotatedPaths = commands.concat( wrapPath ).concat( splice ); 1252 | var rotatedVertices = vertices.concat( vertices.splice( 0, firstVertex ) ); // no longer in sync due to commands.concat( wrapPath ) 1253 | // reCreate path data string. 1254 | var pathString = 'M ' + rotatedVertices[0].from.x + ' ' + rotatedVertices[0].from.y + ' '; 1255 | return pathString + rotatedPaths.join().replace( /\s+,/g, ' ' ) + closePath; 1256 | }; 1257 | 1258 | /** 1259 | * Returns an array of vertex data objects, which provide the absolute vertex positions 1260 | * which can be lost from relative commands once rotated. 1261 | * 1262 | * Provide a callback which console.logs the vertices data any way you wish to format it to 1263 | * help identify the vertex you are looking for. Your callback will be passed an array of 1264 | * objects which contain the following properties: 1265 | * 1266 | * [{ 1267 | * count: path vertices count up to and including 'current position' + 1 (the 'to:'), 1268 | * subPathIndex: subPath array index this object references, 1269 | * subPath: the subPath command + arguments string, 1270 | * from: The path commands 'current point' coordinates prior to execution, 1271 | * to: the new 'current point' coordinates after command execution 1272 | * }] 1273 | * 1274 | * @static 1275 | * @public 1276 | * @method traceVertices 1277 | * @param {String} d SVG path attribute string. 1278 | * @param {Function} [logMethod] Callback which console.logs the vertices data. 1279 | * It will be passed an array of objects which contain 1280 | * [{ count: 'current position' path vertices count, 1281 | * subPathIndex: subPath array index this references, 1282 | * subPath: the subPath command + arguments string, 1283 | * from: commands 'current point' coordinates, 1284 | * to: 'current point' coordinates after command 1285 | * }] 1286 | * @returns {Object} Data object. 1287 | */ 1288 | SmartSVGPath["traceVertices"] = function traceVertices( d, logMethod ) { 1289 | if ( typeof d !== 'string' ) { 1290 | return false 1291 | } 1292 | 1293 | var commands = SmartSVGPath.getCommands( d ); 1294 | var commandsLength = commands.length; 1295 | var j, vertices = []; 1296 | // current point 1297 | var x = 0; 1298 | var y = 0; 1299 | // start point 1300 | var xStart = 0; 1301 | var yStart = 0; 1302 | var prevPath, count = 0; 1303 | 1304 | for (var i = 0; i < commandsLength; i++) { 1305 | 1306 | // Tokenise sub-path. 1307 | var command = commands[i]; 1308 | var name = command.substring( 0, 1 ); 1309 | var args = command.replace( name, '' ).trim().split( ' ' ); 1310 | 1311 | switch (name) { 1312 | case 'M': 1313 | { 1314 | prevPath = null; 1315 | vertices[i] = { 1316 | count : count + 2, 1317 | subPathIndex: i, 1318 | subPath : command, 1319 | from : { x: +args[0], y: +args[1]}, 1320 | to : { x: +args[0], y: +args[1] } 1321 | }; 1322 | xStart = x = +args[0]; 1323 | yStart = y = +args[1]; 1324 | prevPath = command; 1325 | break; 1326 | } 1327 | case 'L': 1328 | { 1329 | vertices[i] = { 1330 | count : count + 2, 1331 | subPathIndex: i, 1332 | subPath : command, 1333 | from : { x: x, y: y}, 1334 | to : { x: +args[0], y: +args[1] } 1335 | }; 1336 | x = +args[0]; 1337 | y = +args[1]; 1338 | prevPath = command; 1339 | break; 1340 | } 1341 | case 'A': 1342 | { 1343 | vertices[i] = { 1344 | count : count + 2, 1345 | subPathIndex: i, 1346 | subPath : command, 1347 | from : { x: x, y: y}, 1348 | to : { x: +args[5], y: +args[6] } 1349 | }; 1350 | x = +args[5]; 1351 | y = +args[6]; 1352 | prevPath = command; 1353 | break; 1354 | } 1355 | case 'Q': 1356 | { 1357 | vertices[i] = { 1358 | count : count + 2, 1359 | subPathIndex: i, 1360 | subPath : command, 1361 | from : { x: x, y: y}, 1362 | to : { x: +args[2], y: +args[3] } 1363 | }; 1364 | x = +args[2]; 1365 | y = +args[3]; 1366 | prevPath = command; 1367 | break; 1368 | } 1369 | case 'C': 1370 | { 1371 | vertices[i] = { 1372 | count : count + 2, 1373 | subPathIndex: i, 1374 | subPath : command, 1375 | from : { x: x, y: y}, 1376 | to : { x: +args[4], y: +args[5] } 1377 | }; 1378 | x = +args[4]; 1379 | y = +args[5]; 1380 | prevPath = command; 1381 | break; 1382 | } 1383 | case 'Z': 1384 | { 1385 | vertices[i] = { 1386 | count : count + 2, 1387 | subPathIndex: i, 1388 | subPath : command, 1389 | from : { x: x, y: y}, 1390 | to : { x: xStart, y: yStart } 1391 | }; 1392 | x = xStart; 1393 | y = yStart; 1394 | prevPath = command; 1395 | break; 1396 | } 1397 | default: 1398 | } 1399 | } 1400 | if ( !!logMethod ) { 1401 | logMethod( vertices ); 1402 | } 1403 | return vertices; 1404 | }; 1405 | 1406 | /** 1407 | * @private 1408 | * @method _newIndicesVector 1409 | * @param {Number} length 1410 | * @return {Array.} 1411 | */ 1412 | SmartSVGPath._newIndicesVector = function _newIndicesVector( length ) { 1413 | var array = []; 1414 | var index = 1; 1415 | for (var i = 0; i < length; ++i) { 1416 | array[i] = index++; 1417 | } 1418 | return array; 1419 | }; 1420 | 1421 | //////////////////////////////////////////////////////////////////////////////////////////////////// 1422 | // // 1423 | // // 1424 | // ██████ ██ ██ ██████ ██████ ██ ██ ██ ██ ██ ██ // 1425 | // ██ ██ ██ ██ ██ ██ ███ ███ ██ ██ ██ // 1426 | // ██ ██ ██ ██ ██ █████ █████ █████ █████ ███████ █████ █████ █████ █████ █████ // 1427 | // ██████ ██ ██ ██ ███ ██████ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ // 1428 | // ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██ █████ ██ ██ ██ ██ ██ █████ // 1429 | // ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ // 1430 | // ██████ ██ ██████ ██████ ██ ██ █████ █████ █████ ██ ██ █████ ████ ██ ██ █████ █████ // 1431 | // ██ // 1432 | // ██ // 1433 | // // 1434 | //////////////////////////////////////////////////////////////////////////////////////////////////// 1435 | 1436 | /** 1437 | * Replaces any SVGElement(s) 'shape data' attributes with a 'd' path data attribute. 1438 | * 1439 | * @static 1440 | * @public 1441 | * @method convertElements 1442 | * @param {SVGElement|HTMLCollection} svgElements 1443 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1444 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1445 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1446 | * FALSE otherwise. 1447 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1448 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1449 | * Pass NO subPaths array to reverse whole path. 1450 | * FALSE otherwise. 1451 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references. 1452 | */ 1453 | SmartSVGPath["convertElements"] = function convertElements( svgElements, reversed, subPaths, precision ) { 1454 | if ( !svgElements || svgElements.length === 0 ) { 1455 | return console.log( 'SmartSVGPath ERROR: SmartSVGPath.convertElements received in-compatible svgElements argument.' ) 1456 | } 1457 | var elementName; 1458 | if ( svgElements.nodeName ) { 1459 | elementName = svgElements.nodeName; 1460 | } 1461 | else if ( svgElements[0].nodeName ) { 1462 | elementName = svgElements[0].nodeName; 1463 | } 1464 | 1465 | var Element = elementName.charAt( 0 ).toUpperCase() + elementName.slice( 1 ); 1466 | return SmartSVGPath['convert' + Element]( svgElements, reversed, subPaths, precision ); 1467 | }; 1468 | 1469 | /** 1470 | * Replaces any (s) 'd' attribute with SmartSVGPath parse path data string. 1471 | * 1472 | * NOTE: This essentially does NOTHING to SVGElements. 1473 | * This method really just supports the convertElements() robustness so that 1474 | * you can throw any SVGElement collection(s) at convertElements() without having 1475 | * to filter out. 1476 | * 1477 | * @static 1478 | * @public 1479 | * @method convertPath 1480 | * @param {SVGPathElement|HTMLCollection} pathElements SVG element (or an Array of them). 1481 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1482 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1483 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1484 | * FALSE otherwise. 1485 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1486 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1487 | * Pass NO subPaths array to reverse whole path. 1488 | * FALSE otherwise. 1489 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references. 1490 | */ 1491 | SmartSVGPath["convertPath"] = function convertPath( pathElements, reversed, subPaths, precision ) { 1492 | /* Specific attributes: cx cy r */ 1493 | return SmartSVGPath._convertElementFactory( 'path', pathElements, reversed, subPaths, precision ); 1494 | }; 1495 | 1496 | /** 1497 | * Replaces any 'shape data' attributes with a 'd' path data attribute. 1498 | * 1499 | * @static 1500 | * @public 1501 | * @method convertCircle 1502 | * @param {SVGCircleElement|HTMLCollection} circleElements SVG element (or an Array of them). 1503 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1504 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1505 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1506 | * FALSE otherwise. 1507 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1508 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1509 | * Pass NO subPaths array to reverse whole path. 1510 | * FALSE otherwise. 1511 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references. 1512 | */ 1513 | SmartSVGPath["convertCircle"] = function convertCircle( circleElements, reversed, subPaths, precision ) { 1514 | /* Specific attributes: cx cy r */ 1515 | return SmartSVGPath._convertElementFactory( 'circle', circleElements, reversed, subPaths, precision ); 1516 | }; 1517 | 1518 | /** 1519 | * Replaces any 'shape data' attributes with a 'd' path data attribute. 1520 | * 1521 | * @static 1522 | * @public 1523 | * @method convertEllipse 1524 | * @param {SVGEllipseElement|HTMLCollection} ellipseElements SVG element (or an Array of them). 1525 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1526 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1527 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1528 | * FALSE otherwise. 1529 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1530 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1531 | * Pass NO subPaths array to reverse whole path. 1532 | * FALSE otherwise. 1533 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references. 1534 | */ 1535 | SmartSVGPath["convertEllipse"] = function convertEllipse( ellipseElements, reversed, subPaths, precision ) { 1536 | /* Specific attributes: cx cy rx ry */ 1537 | return SmartSVGPath._convertElementFactory( 'ellipse', ellipseElements, reversed, subPaths, precision ); 1538 | }; 1539 | 1540 | /** 1541 | * Replaces any 'shape data' attributes with a 'd' path data attribute. 1542 | * 1543 | * @static 1544 | * @public 1545 | * @method convertLine 1546 | * @param {SVGLineElement|HTMLCollection} lineElements SVG element (or an Array of them). 1547 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1548 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1549 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1550 | * FALSE otherwise. 1551 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1552 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1553 | * Pass NO subPaths array to reverse whole path. 1554 | * FALSE otherwise. 1555 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references. 1556 | */ 1557 | SmartSVGPath["convertLine"] = function convertLine( lineElements, reversed, subPaths, precision ) { 1558 | /* Specific attributes: x1 x2 y1 y2 */ 1559 | return SmartSVGPath._convertElementFactory( 'line', lineElements, reversed, subPaths, precision ); 1560 | }; 1561 | 1562 | /** 1563 | * Replaces any 'shape data' attributes with a 'd' path data attribute. 1564 | * 1565 | * @static 1566 | * @public 1567 | * @method convertPolygon 1568 | * @param {SVGPolygonElement|HTMLCollection} polygonElements SVG element (or an Array of them). 1569 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1570 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1571 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1572 | * FALSE otherwise. 1573 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1574 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1575 | * Pass NO subPaths array to reverse whole path. 1576 | * FALSE otherwise. 1577 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references. 1578 | */ 1579 | SmartSVGPath["convertPolygon"] = function convertPolygon( polygonElements, reversed, subPaths, precision ) { 1580 | /* Specific attributes: points, closedPath */ 1581 | return SmartSVGPath._convertElementFactory( 'polygon', polygonElements, reversed, subPaths, precision ); 1582 | }; 1583 | 1584 | /** 1585 | * Replaces any 'shape data' attributes with a 'd' path data attribute. 1586 | * 1587 | * @static 1588 | * @public 1589 | * @method convertPolyline 1590 | * @param {SVGPolylineElement|HTMLCollection} polylineElements SVG element (or an Array of them). 1591 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1592 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1593 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1594 | * FALSE otherwise. 1595 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1596 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1597 | * Pass NO subPaths array to reverse whole path. 1598 | * FALSE otherwise. 1599 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references. 1600 | */ 1601 | SmartSVGPath["convertPolyline"] = function convertPolyline( polylineElements, reversed, subPaths, precision ) { 1602 | /* Specific attributes: points */ 1603 | return SmartSVGPath._convertElementFactory( 'polyline', polylineElements, reversed, subPaths, precision ); 1604 | }; 1605 | 1606 | /** 1607 | * Replaces any 'shape data' attributes with a 'd' path data attribute. 1608 | * 1609 | * @static 1610 | * @public 1611 | * @method convertRect 1612 | * @param {SVGRectElement|HTMLCollection} rectElements SVG element (or an Array of them). 1613 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1614 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1615 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1616 | * FALSE otherwise. 1617 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1618 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1619 | * Pass NO subPaths array to reverse whole path. 1620 | * FALSE otherwise. 1621 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references. 1622 | */ 1623 | SmartSVGPath["convertRect"] = function convertRect( rectElements, reversed, subPaths, precision ) { 1624 | /* Specific attributes: x y width height rx ry */ 1625 | return SmartSVGPath._convertElementFactory( 'rect', rectElements, reversed, subPaths, precision ) 1626 | }; 1627 | 1628 | /** 1629 | * @static 1630 | * @private 1631 | * @method _convertElementFactory 1632 | * @param {String} name The name of the SVGShapeElement. 1633 | * @param {SVGElement|HTMLCollection} shapeElements SVG element (or an Array of them). 1634 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1635 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1636 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1637 | * FALSE otherwise. 1638 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1639 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1640 | * Pass NO subPaths array to reverse whole path. 1641 | * FALSE otherwise. 1642 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references, if any. 1643 | */ 1644 | SmartSVGPath._convertElementFactory = function _convertElementFactory( name, shapeElements, reversed, subPaths, precision ) { 1645 | 1646 | switch (name) { 1647 | case 'circle': 1648 | return SmartSVGPath._convertElements( 1649 | shapeElements, 1650 | 'circle', 1651 | [ 'cx', 'cy', 'r' ], 1652 | 'fromCircle', 1653 | reversed, 1654 | subPaths, 1655 | precision 1656 | ); 1657 | 1658 | case 'ellipse': 1659 | return SmartSVGPath._convertElements( 1660 | shapeElements, 1661 | 'ellipse', 1662 | [ 'cx', 'cy', 'rx', 'ry' ], 1663 | 'fromEllipse', 1664 | reversed, 1665 | subPaths, 1666 | precision 1667 | ); 1668 | 1669 | case 'line': 1670 | return SmartSVGPath._convertElements( 1671 | shapeElements, 1672 | 'line', 1673 | ['x1', 'x2', 'y1', 'y2'], 1674 | 'fromLine', 1675 | reversed, 1676 | subPaths, 1677 | precision 1678 | ); 1679 | 1680 | case 'path': 1681 | return SmartSVGPath._convertElements( 1682 | shapeElements, 1683 | 'path', 1684 | ['d'], 1685 | 'fromPath', 1686 | reversed, 1687 | subPaths, 1688 | precision 1689 | ); 1690 | 1691 | case 'polygon': 1692 | return SmartSVGPath._convertElements( 1693 | shapeElements, 1694 | 'polygon', 1695 | ['points'], 1696 | 'fromPolygon', 1697 | reversed, 1698 | subPaths, 1699 | precision 1700 | ); 1701 | 1702 | case 'polyline': 1703 | return SmartSVGPath._convertElements( 1704 | shapeElements, 1705 | 'polyline', 1706 | ['points'], 1707 | 'fromPolyline', 1708 | reversed, 1709 | subPaths, 1710 | precision 1711 | ); 1712 | 1713 | case 'rect': 1714 | return SmartSVGPath._convertElements( 1715 | shapeElements, 1716 | 'rect', 1717 | ['x', 'y', 'width', 'height', 'rx', 'ry'], 1718 | 'fromRect', 1719 | reversed, 1720 | subPaths, 1721 | precision 1722 | ); 1723 | 1724 | default: 1725 | var error = 'SmartSVGPath ERROR: SmartSVGPath._convertElementFactory( ' + name + ' ) received and incompatible SVGElement, or corrupt array of SVGElements. Please check the SVGElement/s data you are trying to convert.'; 1726 | return console.log( error ); 1727 | } 1728 | }; 1729 | 1730 | /** 1731 | * Detects and directs the svgElements by data type. 1732 | * 1733 | * @static 1734 | * @private 1735 | * @method _convertElements 1736 | * @param {SVGElement|HTMLCollection} svgElements SVG element (or an Array of them). 1737 | * @param {String} shape The name of the shape element. 1738 | * @param {Array.} attributes Unique attributes to be filtered out of path clone. 1739 | * @param {Element} pathMethod Method that generates the appropriate 'path' data. 1740 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1741 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1742 | * Specify subPaths with an array of indices [ 2,4,9 ...] 1743 | * FALSE otherwise. 1744 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1745 | * Specify subPaths with an array of indices [ 1,7,11 ...] 1746 | * Pass NO subPaths array to reverse whole path. 1747 | * FALSE otherwise. 1748 | * @returns {SVGPathElement|Array.} Individual or Array of generated SVGPathElement references, if any. 1749 | */ 1750 | SmartSVGPath._convertElements = function _convertElements( svgElements, shape, attributes, pathMethod, reversed, subPaths, precision ) { 1751 | 1752 | if ( svgElements.nodeName === shape ) { 1753 | // SVGElement 1754 | var array = SmartSVGPath._convertCollection( [svgElements], attributes, pathMethod, reversed, subPaths, precision ); 1755 | return array[0]; 1756 | } 1757 | else if ( svgElements[0].nodeName === shape ) { 1758 | // HTMLCollection 1759 | return SmartSVGPath._convertCollection( svgElements, attributes, pathMethod, reversed, subPaths, precision ); 1760 | } 1761 | var method = shape.charAt( 0 ).toUpperCase() + shape.slice( 1 ); 1762 | var error = 'SmartSVGPath ERROR: SmartSVGPath.convert' + method + ' received an incompatible SVGElement.'; 1763 | return console.log( error ); 1764 | }; 1765 | 1766 | /** 1767 | * Generates a node from shapeElement(s) and removes the shapeElement(s) node. 1768 | * 1769 | * @static 1770 | * @private 1771 | * @method _convertCollection 1772 | * @param {Array.|HTMLCollection} svgElementCollection Single SVGElement array, or an HTMLCollection of SVGElements. 1773 | * @param {Array.} attributes List of attributes unique to the shape element. 1774 | * @param {String} fromShapeMethod The name of the fromShape method to extract path. 1775 | * @param {Boolean|Array.} [reversed=false] TRUE To reverse all paths (or just some by...) 1776 | * Specify subPaths with an array of (1-based, no 0 index) 1777 | * indices [ 2,4,9 ...] 1778 | * FALSE otherwise. 1779 | * @param {Boolean|Array.} [subPaths=false] TRUE To reverse all subPaths (or just some by...) 1780 | * Specify subPaths with an array of (1-based, no 0 index) 1781 | * indices [ 2,4,9 ...] 1782 | * Pass NO subPaths array to reverse whole path. 1783 | * FALSE otherwise. 1784 | * @param {Number} [precision=1] How many decimal places to round coordinates to. 1785 | * @returns {Array.} Array of generated SVGPathElement references, if any. 1786 | */ 1787 | SmartSVGPath._convertCollection = function _convertCollection( svgElementCollection, attributes, fromShapeMethod, reversed, subPaths, precision ) { 1788 | var reverseIndices; 1789 | var exclude = attributes; 1790 | var elementsLength = svgElementCollection.length; 1791 | var pathCollection = []; 1792 | if ( !!reversed ) { 1793 | reverseIndices = Array.isArray( reversed ) ? reversed : SmartSVGPath._newIndicesVector( elementsLength ); 1794 | } 1795 | for ( var i = 0; i < elementsLength; i++ ) { 1796 | var node = svgElementCollection[ i ]; 1797 | var pathString = SmartSVGPath[ fromShapeMethod ]( node, precision ); 1798 | // i+1 to account for 1-based indices index. 1799 | if ( !!reversed && reverseIndices.indexOf( i + 1 ) !== -1 ) { 1800 | pathString = SmartSVGPath.reverseSubPath( pathString, subPaths, true ); 1801 | } 1802 | if ( node.nodeName !== 'path' ) { 1803 | var newPathNode = SmartSVGPath.createSVGNode( 'path', node ); 1804 | var nodeAttributes = node.attributes; 1805 | newPathNode = SmartSVGPath.copyFilteredAttributes( newPathNode, nodeAttributes, exclude ); 1806 | newPathNode.setAttribute( 'd', pathString ); 1807 | pathCollection.push( node.parentNode.insertBefore( newPathNode, node ) ); 1808 | } 1809 | else { 1810 | node.setAttribute( 'd', pathString ); 1811 | } 1812 | } 1813 | if ( newPathNode ) { 1814 | // remove converted SVGElements from DOM. 1815 | if ( svgElementCollection.item ) { 1816 | // remove shapeElement collection. 1817 | while ( svgElementCollection.length > 0 ) { 1818 | svgElementCollection.item( 0 ).parentNode.removeChild( svgElementCollection.item( 0 ) ); 1819 | } 1820 | } 1821 | else { 1822 | // remove shapeElement. 1823 | svgElementCollection[ 0 ].parentNode.removeChild( svgElementCollection[ 0 ] ); 1824 | // Free shapeElement memory. 1825 | svgElementCollection[ 0 ] = null; 1826 | } 1827 | } 1828 | return pathCollection; 1829 | }; 1830 | 1831 | /** 1832 | * Create an SVG node by tag name. 1833 | * 1834 | * @static 1835 | * @public 1836 | * @method createSVGNode 1837 | * @param {String} type Node type by name. 1838 | * @param {SVGElement} svgElement Node from the existing SVG, from which to 1839 | * detect active DOM properties. 1840 | * @returns {HTMLElement} 1841 | */ 1842 | SmartSVGPath["createSVGNode"] = function createSVGNode( type, svgElement ) { 1843 | var svg = svgElement; 1844 | // get parent SVGSVGElement reference. 1845 | while (!(svg instanceof SVGSVGElement)) { 1846 | svg = svg.parentNode 1847 | } 1848 | var svgNS = svg.getAttribute( 'xmlns' ); 1849 | 1850 | // get parent document (HTMLHtmlElement) reference. 1851 | var document = svg; 1852 | while (!(document instanceof HTMLDocument)) { 1853 | document = document.parentNode 1854 | } 1855 | return document.createElementNS( svgNS, type ); 1856 | }; 1857 | 1858 | /** 1859 | * Copy all attributes from an HTMLElement.attributes NamedNodeMap, which can exclude 1860 | * specified attributes listed in the 'excluded' array. 1861 | * 1862 | * @static 1863 | * @public 1864 | * @method copyFilteredAttributes 1865 | * @param {HTMLElement} pathNode Shape element to receive copied attributes. 1866 | * @param {NamedNodeMap} attributes NamedNodeMap to copy attributes from. 1867 | * @param {Array.} excluded List of shape element unique properties NOT to copy. 1868 | * @returns {HTMLElement} 1869 | */ 1870 | SmartSVGPath["copyFilteredAttributes"] = function copyFilteredAttributes( pathNode, attributes, excluded ) { 1871 | for (var i = 0; i < attributes.length; i++) { 1872 | if ( !SmartSVGPath.isExcluded( attributes[i], excluded ) ) { 1873 | pathNode.setAttribute( attributes[i].nodeName, attributes[i].nodeValue ); 1874 | } 1875 | } 1876 | return pathNode; 1877 | }; 1878 | 1879 | // Environment Module support. 1880 | 1881 | // AMD 1882 | if ( typeof define === 'function' && define.amd ) { 1883 | define( function () { 1884 | return SmartSVGPath; 1885 | } ); 1886 | } 1887 | else if ( typeof exports !== 'undefined' ) { 1888 | 1889 | if ( typeof module !== 'undefined' && module.exports ) { 1890 | // Node.js 1891 | return exports = module.exports = SmartSVGPath; 1892 | } 1893 | // CommonJS. 1894 | return exports.SmartSVGPath = SmartSVGPath; 1895 | } 1896 | else { 1897 | // Browser 1898 | return global["SmartSVGPath"] = SmartSVGPath; 1899 | } 1900 | 1901 | }( this )); 1902 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart-svg-path", 3 | "version": "1.0.3", 4 | "description": "Smarter, easier SVG path manipulations for fine grain control over SVG path animations.", 5 | "main": "SmartSVGPath.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SmartArtsStudio/smart-svg-path.git" 12 | }, 13 | "keywords": [ 14 | "reverse", 15 | "SVG", 16 | "path", 17 | "animation", 18 | "set", 19 | "first", 20 | "vertex", 21 | "arc", 22 | "to", 23 | "cubic", 24 | "Bezier", 25 | "get", 26 | "sub", 27 | "paths", 28 | "toAbsolute" 29 | ], 30 | "author": "Daniel Kristensen", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/SmartArtsStudio/smart-svg-path/issues" 34 | }, 35 | "homepage": "https://github.com/SmartArtsStudio/smart-svg-path#readme" 36 | } 37 | --------------------------------------------------------------------------------