├── README.md ├── index.html └── path-data-polyfill.js /README.md: -------------------------------------------------------------------------------- 1 | # SVG Break Apart 2 | 3 | Demo of how to break apart a long svg path data into multiple smaller paths in JS (like in InkScape). 4 | 5 | See live example: https://delapouite.github.io/svg-break-apart/ 6 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | svg-break-apart 6 | 21 | 22 | 23 | 24 | 25 |
26 |

Original SVG

27 |

Composed of two paths: a black background square and a white drawing.

28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 |
36 |

Broken-apart SVG

37 |

The foreground path has been automatically broken apart in several paths with random colors.

38 |
39 | 40 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /path-data-polyfill.js: -------------------------------------------------------------------------------- 1 | 2 | // @info 3 | // Polyfill for SVG 2 getPathData() and setPathData() methods. Based on: 4 | // - SVGPathSeg polyfill by Philip Rogers (MIT License) 5 | // https://github.com/progers/pathseg 6 | // - SVGPathNormalizer by Tadahisa Motooka (MIT License) 7 | // https://github.com/motooka/SVGPathNormalizer/tree/master/src 8 | // - arcToCubicCurves() by Dmitry Baranovskiy (MIT License) 9 | // https://github.com/DmitryBaranovskiy/raphael/blob/v2.1.1/raphael.core.js#L1837 10 | // @author 11 | // Jarosław Foksa 12 | // @license 13 | // MIT License 14 | if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathData) { 15 | let commandsMap = { 16 | "Z":"Z", "M":"M", "L":"L", "C":"C", "Q":"Q", "A":"A", "H":"H", "V":"V", "S":"S", "T":"T", 17 | "z":"Z", "m":"m", "l":"l", "c":"c", "q":"q", "a":"a", "h":"h", "v":"v", "s":"s", "t":"t" 18 | }; 19 | 20 | class Source { 21 | constructor(string) { 22 | this._string = string; 23 | this._currentIndex = 0; 24 | this._endIndex = this._string.length; 25 | this._prevCommand = null; 26 | 27 | this._skipOptionalSpaces(); 28 | } 29 | 30 | parseSegment() { 31 | let char = this._string[this._currentIndex]; 32 | let command = commandsMap[char] ? commandsMap[char] : null; 33 | 34 | if (command === null) { 35 | if (this._prevCommand === null) { 36 | return null; 37 | } 38 | 39 | if ( 40 | (char === "+" || char === "-" || char === "." || (char >= "0" && char <= "9")) && this._prevCommand !== "Z" 41 | ) { 42 | if (this._prevCommand === "M") { 43 | command = "L"; 44 | } 45 | else if (this._prevCommand === "m") { 46 | command = "l"; 47 | } 48 | else { 49 | command = this._prevCommand; 50 | } 51 | } 52 | else { 53 | command = null; 54 | } 55 | 56 | if (command === null) { 57 | return null; 58 | } 59 | } 60 | else { 61 | this._currentIndex += 1; 62 | } 63 | 64 | this._prevCommand = command; 65 | 66 | let values = null; 67 | let cmd = command.toUpperCase(); 68 | 69 | if (cmd === "H" || cmd === "V") { 70 | values = [this._parseNumber()]; 71 | } 72 | else if (cmd === "M" || cmd === "L" || cmd === "T") { 73 | values = [this._parseNumber(), this._parseNumber()]; 74 | } 75 | else if (cmd === "S" || cmd === "Q") { 76 | values = [this._parseNumber(), this._parseNumber(), this._parseNumber(), this._parseNumber()]; 77 | } 78 | else if (cmd === "C") { 79 | values = [ 80 | this._parseNumber(), 81 | this._parseNumber(), 82 | this._parseNumber(), 83 | this._parseNumber(), 84 | this._parseNumber(), 85 | this._parseNumber() 86 | ]; 87 | } 88 | else if (cmd === "A") { 89 | values = [ 90 | this._parseNumber(), 91 | this._parseNumber(), 92 | this._parseNumber(), 93 | this._parseArcFlag(), 94 | this._parseArcFlag(), 95 | this._parseNumber(), 96 | this._parseNumber() 97 | ]; 98 | } 99 | else if (cmd === "Z") { 100 | this._skipOptionalSpaces(); 101 | values = []; 102 | } 103 | 104 | if (values === null || values.indexOf(null) >= 0) { 105 | return null; 106 | } 107 | else { 108 | return {type: command, values: values}; 109 | } 110 | } 111 | 112 | hasMoreData() { 113 | return this._currentIndex < this._endIndex; 114 | } 115 | 116 | peekSegmentType() { 117 | let char = this._string[this._currentIndex]; 118 | return commandsMap[char] ? commandsMap[char] : null; 119 | } 120 | 121 | initialCommandIsMoveTo() { 122 | if (!this.hasMoreData()) { 123 | return true; 124 | } 125 | 126 | let command = this.peekSegmentType(); 127 | return command === "M" || command === "m"; 128 | } 129 | 130 | _isCurrentSpace() { 131 | let char = this._string[this._currentIndex]; 132 | return char <= " " && (char === " " || char === "\n" || char === "\t" || char === "\r" || char === "\f"); 133 | } 134 | 135 | _skipOptionalSpaces() { 136 | while (this._currentIndex < this._endIndex && this._isCurrentSpace()) { 137 | this._currentIndex += 1; 138 | } 139 | 140 | return this._currentIndex < this._endIndex; 141 | } 142 | 143 | _skipOptionalSpacesOrDelimiter() { 144 | if ( 145 | this._currentIndex < this._endIndex && 146 | !this._isCurrentSpace() && 147 | this._string[this._currentIndex] !== "," 148 | ) { 149 | return false; 150 | } 151 | 152 | if (this._skipOptionalSpaces()) { 153 | if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === ",") { 154 | this._currentIndex += 1; 155 | this._skipOptionalSpaces(); 156 | } 157 | } 158 | return this._currentIndex < this._endIndex; 159 | } 160 | 161 | _parseNumber() { 162 | let exponent = 0; 163 | let integer = 0; 164 | let frac = 1; 165 | let decimal = 0; 166 | let sign = 1; 167 | let expsign = 1; 168 | let startIndex = this._currentIndex; 169 | 170 | this._skipOptionalSpaces(); 171 | 172 | if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === "+") { 173 | this._currentIndex += 1; 174 | } 175 | else if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === "-") { 176 | this._currentIndex += 1; 177 | sign = -1; 178 | } 179 | 180 | if ( 181 | this._currentIndex === this._endIndex || 182 | ( 183 | (this._string[this._currentIndex] < "0" || this._string[this._currentIndex] > "9") && 184 | this._string[this._currentIndex] !== "." 185 | ) 186 | ) { 187 | return null; 188 | } 189 | 190 | let startIntPartIndex = this._currentIndex; 191 | 192 | while ( 193 | this._currentIndex < this._endIndex && 194 | this._string[this._currentIndex] >= "0" && 195 | this._string[this._currentIndex] <= "9" 196 | ) { 197 | this._currentIndex += 1; 198 | } 199 | 200 | if (this._currentIndex !== startIntPartIndex) { 201 | let scanIntPartIndex = this._currentIndex - 1; 202 | let multiplier = 1; 203 | 204 | while (scanIntPartIndex >= startIntPartIndex) { 205 | integer += multiplier * (this._string[scanIntPartIndex] - "0"); 206 | scanIntPartIndex -= 1; 207 | multiplier *= 10; 208 | } 209 | } 210 | 211 | if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === ".") { 212 | this._currentIndex += 1; 213 | 214 | if ( 215 | this._currentIndex >= this._endIndex || 216 | this._string[this._currentIndex] < "0" || 217 | this._string[this._currentIndex] > "9" 218 | ) { 219 | return null; 220 | } 221 | 222 | while ( 223 | this._currentIndex < this._endIndex && 224 | this._string[this._currentIndex] >= "0" && 225 | this._string[this._currentIndex] <= "9" 226 | ) { 227 | frac *= 10; 228 | decimal += (this._string.charAt(this._currentIndex) - "0") / frac; 229 | this._currentIndex += 1; 230 | } 231 | } 232 | 233 | if ( 234 | this._currentIndex !== startIndex && 235 | this._currentIndex + 1 < this._endIndex && 236 | (this._string[this._currentIndex] === "e" || this._string[this._currentIndex] === "E") && 237 | (this._string[this._currentIndex + 1] !== "x" && this._string[this._currentIndex + 1] !== "m") 238 | ) { 239 | this._currentIndex += 1; 240 | 241 | if (this._string[this._currentIndex] === "+") { 242 | this._currentIndex += 1; 243 | } 244 | else if (this._string[this._currentIndex] === "-") { 245 | this._currentIndex += 1; 246 | expsign = -1; 247 | } 248 | 249 | if ( 250 | this._currentIndex >= this._endIndex || 251 | this._string[this._currentIndex] < "0" || 252 | this._string[this._currentIndex] > "9" 253 | ) { 254 | return null; 255 | } 256 | 257 | while ( 258 | this._currentIndex < this._endIndex && 259 | this._string[this._currentIndex] >= "0" && 260 | this._string[this._currentIndex] <= "9" 261 | ) { 262 | exponent *= 10; 263 | exponent += (this._string[this._currentIndex] - "0"); 264 | this._currentIndex += 1; 265 | } 266 | } 267 | 268 | let number = integer + decimal; 269 | number *= sign; 270 | 271 | if (exponent) { 272 | number *= Math.pow(10, expsign * exponent); 273 | } 274 | 275 | if (startIndex === this._currentIndex) { 276 | return null; 277 | } 278 | 279 | this._skipOptionalSpacesOrDelimiter(); 280 | 281 | return number; 282 | } 283 | 284 | _parseArcFlag() { 285 | if (this._currentIndex >= this._endIndex) { 286 | return null; 287 | } 288 | 289 | let flag = null; 290 | let flagChar = this._string[this._currentIndex]; 291 | 292 | this._currentIndex += 1; 293 | 294 | if (flagChar === "0") { 295 | flag = 0; 296 | } 297 | else if (flagChar === "1") { 298 | flag = 1; 299 | } 300 | else { 301 | return null; 302 | } 303 | 304 | this._skipOptionalSpacesOrDelimiter(); 305 | return flag; 306 | } 307 | } 308 | 309 | let parsePathDataString = (string) => { 310 | if (!string || string.length === 0) return []; 311 | 312 | let source = new Source(string); 313 | let pathData = []; 314 | 315 | if (source.initialCommandIsMoveTo()) { 316 | while (source.hasMoreData()) { 317 | let pathSeg = source.parseSegment(); 318 | 319 | if (pathSeg === null) { 320 | break; 321 | } 322 | else { 323 | pathData.push(pathSeg); 324 | } 325 | } 326 | } 327 | 328 | return pathData; 329 | } 330 | 331 | let setAttribute = SVGPathElement.prototype.setAttribute; 332 | let removeAttribute = SVGPathElement.prototype.removeAttribute; 333 | 334 | let $cachedPathData = Symbol(); 335 | let $cachedNormalizedPathData = Symbol(); 336 | 337 | // @info 338 | // Get an array of corresponding cubic bezier curve parameters for given arc curve paramters. 339 | let arcToCubicCurves = (x1, y1, x2, y2, r1, r2, angle, largeArcFlag, sweepFlag, _recursive) => { 340 | let degToRad = (degrees) => { 341 | return (Math.PI * degrees) / 180; 342 | }; 343 | 344 | let rotate = (x, y, angleRad) => { 345 | let X = x * Math.cos(angleRad) - y * Math.sin(angleRad); 346 | let Y = x * Math.sin(angleRad) + y * Math.cos(angleRad); 347 | return {x: X, y: Y}; 348 | }; 349 | 350 | let angleRad = degToRad(angle); 351 | let params = []; 352 | let f1, f2, cx, cy; 353 | 354 | if (_recursive) { 355 | f1 = _recursive[0]; 356 | f2 = _recursive[1]; 357 | cx = _recursive[2]; 358 | cy = _recursive[3]; 359 | } 360 | else { 361 | let p1 = rotate(x1, y1, -angleRad); 362 | x1 = p1.x; 363 | y1 = p1.y; 364 | 365 | let p2 = rotate(x2, y2, -angleRad); 366 | x2 = p2.x; 367 | y2 = p2.y; 368 | 369 | let x = (x1 - x2) / 2; 370 | let y = (y1 - y2) / 2; 371 | let h = (x * x) / (r1 * r1) + (y * y) / (r2 * r2); 372 | 373 | if (h > 1) { 374 | h = Math.sqrt(h); 375 | r1 = h * r1; 376 | r2 = h * r2; 377 | } 378 | 379 | let sign; 380 | 381 | if (largeArcFlag === sweepFlag) { 382 | sign = -1; 383 | } 384 | else { 385 | sign = 1; 386 | } 387 | 388 | let r1Pow = r1 * r1; 389 | let r2Pow = r2 * r2; 390 | 391 | let left = r1Pow * r2Pow - r1Pow * y * y - r2Pow * x * x; 392 | let right = r1Pow * y * y + r2Pow * x * x; 393 | 394 | let k = sign * Math.sqrt(Math.abs(left/right)); 395 | 396 | cx = k * r1 * y / r2 + (x1 + x2) / 2; 397 | cy = k * -r2 * x / r1 + (y1 + y2) / 2; 398 | 399 | f1 = Math.asin(parseFloat(((y1 - cy) / r2).toFixed(9))); 400 | f2 = Math.asin(parseFloat(((y2 - cy) / r2).toFixed(9))); 401 | 402 | if (x1 < cx) { 403 | f1 = Math.PI - f1; 404 | } 405 | if (x2 < cx) { 406 | f2 = Math.PI - f2; 407 | } 408 | 409 | if (f1 < 0) { 410 | f1 = Math.PI * 2 + f1; 411 | } 412 | if (f2 < 0) { 413 | f2 = Math.PI * 2 + f2; 414 | } 415 | 416 | if (sweepFlag && f1 > f2) { 417 | f1 = f1 - Math.PI * 2; 418 | } 419 | if (!sweepFlag && f2 > f1) { 420 | f2 = f2 - Math.PI * 2; 421 | } 422 | } 423 | 424 | let df = f2 - f1; 425 | 426 | if (Math.abs(df) > (Math.PI * 120 / 180)) { 427 | let f2old = f2; 428 | let x2old = x2; 429 | let y2old = y2; 430 | 431 | if (sweepFlag && f2 > f1) { 432 | f2 = f1 + (Math.PI * 120 / 180) * (1); 433 | } 434 | else { 435 | f2 = f1 + (Math.PI * 120 / 180) * (-1); 436 | } 437 | 438 | x2 = cx + r1 * Math.cos(f2); 439 | y2 = cy + r2 * Math.sin(f2); 440 | params = arcToCubicCurves(x2, y2, x2old, y2old, r1, r2, angle, 0, sweepFlag, [f2, f2old, cx, cy]); 441 | } 442 | 443 | df = f2 - f1; 444 | 445 | let c1 = Math.cos(f1); 446 | let s1 = Math.sin(f1); 447 | let c2 = Math.cos(f2); 448 | let s2 = Math.sin(f2); 449 | let t = Math.tan(df / 4); 450 | let hx = 4 / 3 * r1 * t; 451 | let hy = 4 / 3 * r2 * t; 452 | 453 | let m1 = [x1, y1]; 454 | let m2 = [x1 + hx * s1, y1 - hy * c1]; 455 | let m3 = [x2 + hx * s2, y2 - hy * c2]; 456 | let m4 = [x2, y2]; 457 | 458 | m2[0] = 2 * m1[0] - m2[0]; 459 | m2[1] = 2 * m1[1] - m2[1]; 460 | 461 | if (_recursive) { 462 | return [m2, m3, m4].concat(params); 463 | } 464 | else { 465 | params = [m2, m3, m4].concat(params).join().split(","); 466 | 467 | let curves = []; 468 | let curveParams = []; 469 | 470 | params.forEach( function(param, i) { 471 | if (i % 2) { 472 | curveParams.push(rotate(params[i - 1], params[i], angleRad).y); 473 | } 474 | else { 475 | curveParams.push(rotate(params[i], params[i + 1], angleRad).x); 476 | } 477 | 478 | if (curveParams.length === 6) { 479 | curves.push(curveParams); 480 | curveParams = []; 481 | } 482 | }); 483 | 484 | return curves; 485 | } 486 | }; 487 | 488 | let clonePathData = (pathData) => { 489 | return pathData.map((seg) => { 490 | return {type: seg.type, values: [...seg.values]} 491 | }); 492 | }; 493 | 494 | // @info 495 | // Takes any path data, returns path data that consists only from absolute commands. 496 | let absolutizePathData = (pathData) => { 497 | let absolutizedPathData = []; 498 | 499 | let currentX = null; 500 | let currentY = null; 501 | 502 | let subpathX = null; 503 | let subpathY = null; 504 | 505 | for (let seg of pathData) { 506 | let type = seg.type; 507 | 508 | if (type === "M") { 509 | let [x, y] = seg.values; 510 | 511 | absolutizedPathData.push({type: "M", values: [x, y]}); 512 | 513 | subpathX = x; 514 | subpathY = y; 515 | 516 | currentX = x; 517 | currentY = y; 518 | } 519 | 520 | else if (type === "m") { 521 | let [x, y] = seg.values; 522 | 523 | x += currentX; 524 | y += currentY; 525 | 526 | absolutizedPathData.push({type: "M", values: [x, y]}); 527 | 528 | subpathX = x; 529 | subpathY = y; 530 | 531 | currentX = x; 532 | currentY = y; 533 | } 534 | 535 | else if (type === "L") { 536 | let [x, y] = seg.values; 537 | 538 | absolutizedPathData.push({type: "L", values: [x, y]}); 539 | 540 | currentX = x; 541 | currentY = y; 542 | } 543 | 544 | else if (type === "l") { 545 | let [x, y] = seg.values; 546 | 547 | x += currentX; 548 | y += currentY; 549 | 550 | absolutizedPathData.push({type: "L", values: [x, y]}); 551 | 552 | currentX = x; 553 | currentY = y; 554 | } 555 | 556 | else if (type === "C") { 557 | let [x1, y1, x2, y2, x, y] = seg.values; 558 | 559 | absolutizedPathData.push({type: "C", values: [x1, y1, x2, y2, x, y]}); 560 | 561 | currentX = x; 562 | currentY = y; 563 | } 564 | 565 | else if (type === "c") { 566 | let [x1, y1, x2, y2, x, y] = seg.values; 567 | 568 | x1 += currentX; 569 | y1 += currentY; 570 | x2 += currentX; 571 | y2 += currentY; 572 | x += currentX; 573 | y += currentY; 574 | 575 | absolutizedPathData.push({type: "C", values: [x1, y1, x2, y2, x, y]}); 576 | 577 | currentX = x; 578 | currentY = y; 579 | } 580 | 581 | else if (type === "Q") { 582 | let [x1, y1, x, y] = seg.values; 583 | 584 | absolutizedPathData.push({type: "Q", values: [x1, y1, x, y]}); 585 | 586 | currentX = x; 587 | currentY = y; 588 | } 589 | 590 | else if (type === "q") { 591 | let [x1, y1, x, y] = seg.values; 592 | 593 | x1 += currentX; 594 | y1 += currentY; 595 | x += currentX; 596 | y += currentY; 597 | 598 | absolutizedPathData.push({type: "Q", values: [x1, y1, x, y]}); 599 | 600 | currentX = x; 601 | currentY = y; 602 | } 603 | 604 | else if (type === "A") { 605 | let [rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y] = seg.values; 606 | 607 | absolutizedPathData.push({ 608 | type: "A", 609 | values: [rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y] 610 | }); 611 | 612 | currentX = x; 613 | currentY = y; 614 | } 615 | 616 | else if (type === "a") { 617 | let [rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y] = seg.values; 618 | 619 | x += currentX; 620 | y += currentY; 621 | 622 | absolutizedPathData.push({ 623 | type: "A", 624 | values: [rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y] 625 | }); 626 | 627 | currentX = x; 628 | currentY = y; 629 | } 630 | 631 | else if (type === "H") { 632 | let [x] = seg.values; 633 | absolutizedPathData.push({type: "H", values: [x]}); 634 | currentX = x; 635 | } 636 | 637 | else if (type === "h") { 638 | let [x] = seg.values; 639 | x += currentX; 640 | 641 | absolutizedPathData.push({type: "H", values: [x]}); 642 | currentX = x; 643 | } 644 | 645 | else if (type === "V") { 646 | let [y] = seg.values; 647 | 648 | absolutizedPathData.push({type: "V", values: [y]}); 649 | currentY = y; 650 | } 651 | 652 | else if (type === "v") { 653 | let [y] = seg.values; 654 | y += currentY; 655 | 656 | absolutizedPathData.push({type: "V", values: [y]}); 657 | currentY = y; 658 | } 659 | 660 | else if (type === "S") { 661 | let [x2, y2, x, y] = seg.values; 662 | 663 | absolutizedPathData.push({type: "S", values: [x2, y2, x, y]}); 664 | 665 | currentX = x; 666 | currentY = y; 667 | } 668 | 669 | else if (type === "s") { 670 | let [x2, y2, x, y] = seg.values; 671 | 672 | x2 += currentX; 673 | y2 += currentY; 674 | x += currentX; 675 | y += currentY; 676 | 677 | absolutizedPathData.push({type: "S", values: [x2, y2, x, y]}); 678 | 679 | currentX = x; 680 | currentY = y; 681 | } 682 | 683 | else if (type === "T") { 684 | let [x, y] = seg.values; 685 | 686 | absolutizedPathData.push({type: "T", values: [x, y]}); 687 | 688 | currentX = x; 689 | currentY = y; 690 | } 691 | 692 | else if (type === "t") { 693 | let [x, y] = seg.values; 694 | 695 | x += currentX; 696 | y += currentY; 697 | 698 | absolutizedPathData.push({type: "T", values: [x, y]}); 699 | 700 | currentX = x; 701 | currentY = y; 702 | } 703 | 704 | else if (type === "Z" || type === "z") { 705 | absolutizedPathData.push({type: "Z", values: []}); 706 | 707 | currentX = subpathX; 708 | currentY = subpathY; 709 | } 710 | } 711 | 712 | return absolutizedPathData; 713 | }; 714 | 715 | // @info 716 | // Takes path data that consists only from absolute commands, returns path data that consists only from 717 | // "M", "L", "C" and "Z" commands. 718 | let reducePathData = (pathData) => { 719 | let reducedPathData = []; 720 | let lastType = null; 721 | 722 | let lastControlX = null; 723 | let lastControlY = null; 724 | 725 | let currentX = null; 726 | let currentY = null; 727 | 728 | let subpathX = null; 729 | let subpathY = null; 730 | 731 | for (let seg of pathData) { 732 | if (seg.type === "M") { 733 | let [x, y] = seg.values; 734 | 735 | reducedPathData.push({type: "M", values: [x, y]}); 736 | 737 | subpathX = x; 738 | subpathY = y; 739 | 740 | currentX = x; 741 | currentY = y; 742 | } 743 | 744 | else if (seg.type === "C") { 745 | let [x1, y1, x2, y2, x, y] = seg.values; 746 | 747 | reducedPathData.push({type: "C", values: [x1, y1, x2, y2, x, y]}); 748 | 749 | lastControlX = x2; 750 | lastControlY = y2; 751 | 752 | currentX = x; 753 | currentY = y; 754 | } 755 | 756 | else if (seg.type === "L") { 757 | let [x, y] = seg.values; 758 | 759 | reducedPathData.push({type: "L", values: [x, y]}); 760 | 761 | currentX = x; 762 | currentY = y; 763 | } 764 | 765 | else if (seg.type === "H") { 766 | let [x] = seg.values; 767 | 768 | reducedPathData.push({type: "L", values: [x, currentY]}); 769 | 770 | currentX = x; 771 | } 772 | 773 | else if (seg.type === "V") { 774 | let [y] = seg.values; 775 | 776 | reducedPathData.push({type: "L", values: [currentX, y]}); 777 | 778 | currentY = y; 779 | } 780 | 781 | else if (seg.type === "S") { 782 | let [x2, y2, x, y] = seg.values; 783 | let cx1, cy1; 784 | 785 | if (lastType === "C" || lastType === "S") { 786 | cx1 = currentX + (currentX - lastControlX); 787 | cy1 = currentY + (currentY - lastControlY); 788 | } 789 | else { 790 | cx1 = currentX; 791 | cy1 = currentY; 792 | } 793 | 794 | reducedPathData.push({type: "C", values: [cx1, cy1, x2, y2, x, y]}); 795 | 796 | lastControlX = x2; 797 | lastControlY = y2; 798 | 799 | currentX = x; 800 | currentY = y; 801 | } 802 | 803 | else if (seg.type === "T") { 804 | let [x, y] = seg.values; 805 | let x1, y1; 806 | 807 | if (lastType === "Q" || lastType === "T") { 808 | x1 = currentX + (currentX - lastControlX); 809 | y1 = currentY + (currentY - lastControlY); 810 | } 811 | else { 812 | x1 = currentX; 813 | y1 = currentY; 814 | } 815 | 816 | let cx1 = currentX + 2 * (x1 - currentX) / 3; 817 | let cy1 = currentY + 2 * (y1 - currentY) / 3; 818 | let cx2 = x + 2 * (x1 - x) / 3; 819 | let cy2 = y + 2 * (y1 - y) / 3; 820 | 821 | reducedPathData.push({type: "C", values: [cx1, cy1, cx2, cy2, x, y]}); 822 | 823 | lastControlX = x1; 824 | lastControlY = y1; 825 | 826 | currentX = x; 827 | currentY = y; 828 | } 829 | 830 | else if (seg.type === "Q") { 831 | let [x1, y1, x, y] = seg.values; 832 | let cx1 = currentX + 2 * (x1 - currentX) / 3; 833 | let cy1 = currentY + 2 * (y1 - currentY) / 3; 834 | let cx2 = x + 2 * (x1 - x) / 3; 835 | let cy2 = y + 2 * (y1 - y) / 3; 836 | 837 | reducedPathData.push({type: "C", values: [cx1, cy1, cx2, cy2, x, y]}); 838 | 839 | lastControlX = x1; 840 | lastControlY = y1; 841 | 842 | currentX = x; 843 | currentY = y; 844 | } 845 | 846 | else if (seg.type === "A") { 847 | let [r1, r2, angle, largeArcFlag, sweepFlag, x, y] = seg.values; 848 | 849 | if (r1 === 0 || r2 === 0) { 850 | reducedPathData.push({type: "C", values: [currentX, currentY, x, y, x, y]}); 851 | 852 | currentX = x; 853 | currentY = y; 854 | } 855 | else { 856 | if (currentX !== x || currentY !== y) { 857 | let curves = arcToCubicCurves(currentX, currentY, x, y, r1, r2, angle, largeArcFlag, sweepFlag); 858 | 859 | curves.forEach( function(curve) { 860 | reducedPathData.push({type: "C", values: curve}); 861 | 862 | currentX = x; 863 | currentY = y; 864 | }); 865 | } 866 | } 867 | } 868 | 869 | else if (seg.type === "Z") { 870 | reducedPathData.push(seg); 871 | 872 | currentX = subpathX; 873 | currentY = subpathY; 874 | } 875 | 876 | lastType = seg.type; 877 | } 878 | 879 | return reducedPathData; 880 | }; 881 | 882 | SVGPathElement.prototype.setAttribute = function(name, value) { 883 | if (name === "d") { 884 | this[$cachedPathData] = null; 885 | this[$cachedNormalizedPathData] = null; 886 | } 887 | 888 | setAttribute.call(this, name, value); 889 | }; 890 | 891 | SVGPathElement.prototype.removeAttribute = function(name, value) { 892 | if (name === "d") { 893 | this[$cachedPathData] = null; 894 | this[$cachedNormalizedPathData] = null; 895 | } 896 | 897 | removeAttribute.call(this, name); 898 | }; 899 | 900 | SVGPathElement.prototype.getPathData = function(options) { 901 | if (options && options.normalize) { 902 | if (this[$cachedNormalizedPathData]) { 903 | return clonePathData(this[$cachedNormalizedPathData]); 904 | } 905 | else { 906 | let pathData; 907 | 908 | if (this[$cachedPathData]) { 909 | pathData = clonePathData(this[$cachedPathData]); 910 | } 911 | else { 912 | pathData = parsePathDataString(this.getAttribute("d") || ""); 913 | this[$cachedPathData] = clonePathData(pathData); 914 | } 915 | 916 | let normalizedPathData = reducePathData(absolutizePathData(pathData)); 917 | this[$cachedNormalizedPathData] = clonePathData(normalizedPathData); 918 | return normalizedPathData; 919 | } 920 | } 921 | else { 922 | if (this[$cachedPathData]) { 923 | return clonePathData(this[$cachedPathData]); 924 | } 925 | else { 926 | let pathData = parsePathDataString(this.getAttribute("d") || ""); 927 | this[$cachedPathData] = clonePathData(pathData); 928 | return pathData; 929 | } 930 | } 931 | }; 932 | 933 | SVGPathElement.prototype.setPathData = function(pathData) { 934 | if (pathData.length === 0) { 935 | this.removeAttribute("d"); 936 | } 937 | else { 938 | let d = ""; 939 | 940 | for (let i = 0, l = pathData.length; i < l; i += 1) { 941 | let seg = pathData[i]; 942 | 943 | if (i > 0) { 944 | d += " "; 945 | } 946 | 947 | d += seg.type; 948 | 949 | if (seg.values && seg.values.length > 0) { 950 | d += " " + seg.values.join(" "); 951 | } 952 | } 953 | 954 | this.setAttribute("d", d); 955 | } 956 | }; 957 | 958 | SVGRectElement.prototype.getPathData = function(options) { 959 | let x = this.x.baseVal.value; 960 | let y = this.y.baseVal.value; 961 | let width = this.width.baseVal.value; 962 | let height = this.height.baseVal.value; 963 | let rx = this.hasAttribute("rx") ? this.rx.baseVal.value : this.ry.baseVal.value; 964 | let ry = this.hasAttribute("ry") ? this.ry.baseVal.value : this.rx.baseVal.value; 965 | 966 | if (rx > width / 2) { 967 | rx = width / 2; 968 | } 969 | 970 | if (ry > height / 2) { 971 | ry = height / 2; 972 | } 973 | 974 | let pathData = [ 975 | {type: "M", values: [x+rx, y]}, 976 | {type: "H", values: [x+width-rx]}, 977 | {type: "A", values: [rx, ry, 0, 0, 1, x+width, y+ry]}, 978 | {type: "V", values: [y+height-ry]}, 979 | {type: "A", values: [rx, ry, 0, 0, 1, x+width-rx, y+height]}, 980 | {type: "H", values: [x+rx]}, 981 | {type: "A", values: [rx, ry, 0, 0, 1, x, y+height-ry]}, 982 | {type: "V", values: [y+ry]}, 983 | {type: "A", values: [rx, ry, 0, 0, 1, x+rx, y]}, 984 | {type: "Z", values: []} 985 | ]; 986 | 987 | // Get rid of redundant "A" segs when either rx or ry is 0 988 | pathData = pathData.filter(s => s.type === "A" && (s.values[0] === 0 || s.values[1] === 0) ? false : true); 989 | 990 | if (options && options.normalize === true) { 991 | pathData = reducePathData(pathData); 992 | } 993 | 994 | return pathData; 995 | }; 996 | 997 | SVGCircleElement.prototype.getPathData = function(options) { 998 | let cx = this.cx.baseVal.value; 999 | let cy = this.cy.baseVal.value; 1000 | let r = this.r.baseVal.value; 1001 | 1002 | let pathData = [ 1003 | { type: "M", values: [cx + r, cy] }, 1004 | { type: "A", values: [r, r, 0, 0, 1, cx, cy+r] }, 1005 | { type: "A", values: [r, r, 0, 0, 1, cx-r, cy] }, 1006 | { type: "A", values: [r, r, 0, 0, 1, cx, cy-r] }, 1007 | { type: "A", values: [r, r, 0, 0, 1, cx+r, cy] }, 1008 | { type: "Z", values: [] } 1009 | ]; 1010 | 1011 | if (options && options.normalize === true) { 1012 | pathData = reducePathData(pathData); 1013 | } 1014 | 1015 | return pathData; 1016 | }; 1017 | 1018 | SVGEllipseElement.prototype.getPathData = function(options) { 1019 | let cx = this.cx.baseVal.value; 1020 | let cy = this.cy.baseVal.value; 1021 | let rx = this.rx.baseVal.value; 1022 | let ry = this.ry.baseVal.value; 1023 | 1024 | let pathData = [ 1025 | { type: "M", values: [cx + rx, cy] }, 1026 | { type: "A", values: [rx, ry, 0, 0, 1, cx, cy+ry] }, 1027 | { type: "A", values: [rx, ry, 0, 0, 1, cx-rx, cy] }, 1028 | { type: "A", values: [rx, ry, 0, 0, 1, cx, cy-ry] }, 1029 | { type: "A", values: [rx, ry, 0, 0, 1, cx+rx, cy] }, 1030 | { type: "Z", values: [] } 1031 | ]; 1032 | 1033 | if (options && options.normalize === true) { 1034 | pathData = reducePathData(pathData); 1035 | } 1036 | 1037 | return pathData; 1038 | }; 1039 | 1040 | SVGLineElement.prototype.getPathData = function() { 1041 | return [ 1042 | { type: "M", values: [this.x1.baseVal.value, this.y1.baseVal.value] }, 1043 | { type: "L", values: [this.x2.baseVal.value, this.y2.baseVal.value] } 1044 | ]; 1045 | }; 1046 | 1047 | SVGPolylineElement.prototype.getPathData = function() { 1048 | let pathData = []; 1049 | 1050 | for (let i = 0; i < this.points.numberOfItems; i += 1) { 1051 | let point = this.points.getItem(i); 1052 | 1053 | pathData.push({ 1054 | type: (i === 0 ? "M" : "L"), 1055 | values: [point.x, point.y] 1056 | }); 1057 | } 1058 | 1059 | return pathData; 1060 | }; 1061 | 1062 | SVGPolygonElement.prototype.getPathData = function() { 1063 | let pathData = []; 1064 | 1065 | for (let i = 0; i < this.points.numberOfItems; i += 1) { 1066 | let point = this.points.getItem(i); 1067 | 1068 | pathData.push({ 1069 | type: (i === 0 ? "M" : "L"), 1070 | values: [point.x, point.y] 1071 | }); 1072 | } 1073 | 1074 | pathData.push({ 1075 | type: "Z", 1076 | values: [] 1077 | }); 1078 | 1079 | return pathData; 1080 | }; 1081 | } 1082 | --------------------------------------------------------------------------------