-
-
Save timo22345/9413158 to your computer and use it in GitHub Desktop.
| <!doctype html> | |
| <html> | |
| <title>Flatten.js, General SVG Flattener</title> | |
| <head> | |
| <script> | |
| /* | |
| Random path and shape generator, flattener test base: https://jsfiddle.net/fjm9423q/embedded/result/ | |
| Basic usage example: https://jsfiddle.net/nrjvmqur/embedded/result/ | |
| Basic usage: flatten(document.getElementById('svg')); | |
| What it does: Flattens elements (converts elements to paths and flattens transformations). | |
| If the argument element (whose id is above 'svg') has children, or it's descendants has children, | |
| these children elements are flattened also. | |
| If you want to modify path coordinates using non-affine methods (eg. perspective distort), | |
| you can convert all segments to cubic curves using: | |
| flatten(document.getElementById('svg'), true); | |
| There are also arguments 'toAbsolute' (convert coordinates to absolute) and 'dec', | |
| number of digits after decimal separator. | |
| */ | |
| /* | |
| The MIT License (MIT) | |
| Copyright (c) 2014 Timo (https://github.com/timo22345) | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this software and associated documentation files (the "Software"), to deal | |
| in the Software without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in | |
| all copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| THE SOFTWARE. | |
| */ | |
| SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(toElement) { | |
| return toElement.getScreenCTM().inverse().multiply(this.getScreenCTM()); | |
| }; | |
| (function () | |
| { | |
| var p2s = /,?([achlmqrstvxz]),?/gi; | |
| var convertToString = function (arr) | |
| { | |
| return arr.join(',').replace(p2s, '$1'); | |
| }; | |
| // Flattens transformations of element or it's children and sub-children | |
| // elem: DOM element | |
| // toCubics: converts all segments to cubics | |
| // toAbsolute: converts all segments to Absolute | |
| // dec: number of digits after decimal separator | |
| // Returns: no return value | |
| function flatten(elem, toCubics, toAbsolute, rectAsArgs, dec) | |
| { | |
| if (!elem) return; | |
| if (typeof (rectAsArgs) == 'undefined') rectAsArgs = false; | |
| if (typeof (toCubics) == 'undefined') toCubics = false; | |
| if (typeof (toAbsolute) == 'undefined') toAbsolute = false; | |
| if (typeof (dec) == 'undefined') dec = false; | |
| if (elem && elem.children && elem.children.length) | |
| { | |
| for (var i = 0, ilen = elem.children.length; i < ilen; i++) | |
| { | |
| //console.log(elem.children[i]); | |
| flatten(elem.children[i], toCubics, toAbsolute, rectAsArgs, dec); | |
| } | |
| elem.removeAttribute('transform'); | |
| return; | |
| } | |
| if (!(elem instanceof SVGCircleElement || | |
| elem instanceof SVGRectElement || | |
| elem instanceof SVGEllipseElement || | |
| elem instanceof SVGLineElement || | |
| elem instanceof SVGPolygonElement || | |
| elem instanceof SVGPolylineElement || | |
| elem instanceof SVGPathElement)) return; | |
| path_elem = convertToPath(elem, rectAsArgs); | |
| //console.log('path_elem', $(path_elem).wrap('<div />').parent().html() ); | |
| //$(path_elem).unwrap(); | |
| if (!path_elem || path_elem.getAttribute(d) == '') return 'M 0 0'; | |
| // Rounding coordinates to dec decimals | |
| if (dec || dec === 0) | |
| { | |
| if (dec > 15) dec = 15; | |
| else if (dec < 0) dec = 0; | |
| } | |
| else dec = false; | |
| function r(num) | |
| { | |
| if (dec !== false) return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec); | |
| else return num; | |
| } | |
| var arr; | |
| //var pathDOM = path_elem.node; | |
| var pathDOM = path_elem; | |
| var d = pathDOM.getAttribute('d').trim(); | |
| // If you want to retain current path commans, set toCubics to false | |
| if (!toCubics) | |
| { // Set to false to prevent possible re-normalization. | |
| arr = parsePathString(d); // str to array | |
| var arr_orig = arr; | |
| arr = pathToAbsolute(arr); // mahvstcsqz -> uppercase | |
| } | |
| // If you want to modify path data using nonAffine methods, | |
| // set toCubics to true | |
| else | |
| { | |
| arr = path2curve(d); // mahvstcsqz -> MC | |
| var arr_orig = arr; | |
| } | |
| var svgDOM = pathDOM.ownerSVGElement; | |
| // Get the relation matrix that converts path coordinates | |
| // to SVGroot's coordinate space | |
| var matrix = pathDOM.getTransformToElement(svgDOM); | |
| // The following code can bake transformations | |
| // both normalized and non-normalized data | |
| // Coordinates have to be Absolute in the following | |
| var i = 0, | |
| j, m = arr.length, | |
| letter = '', | |
| letter_orig = '', | |
| x = 0, | |
| y = 0, | |
| point, newcoords = [], | |
| newcoords_orig = [], | |
| pt = svgDOM.createSVGPoint(), | |
| subpath_start = {}, prevX = 0, | |
| prevY = 0; | |
| subpath_start.x = null; | |
| subpath_start.y = null; | |
| for (; i < m; i++) | |
| { | |
| letter = arr[i][0].toUpperCase(); | |
| letter_orig = arr_orig[i][0]; | |
| newcoords[i] = []; | |
| newcoords[i][0] = arr[i][0]; | |
| if (letter == 'A') | |
| { | |
| x = arr[i][6]; | |
| y = arr[i][7]; | |
| pt.x = arr[i][6]; | |
| pt.y = arr[i][7]; | |
| newcoords[i] = arc_transform(arr[i][1], arr[i][2], arr[i][3], arr[i][4], arr[i][5], pt, matrix); | |
| // rounding arc parameters | |
| // x,y are rounded normally | |
| // other parameters at least to 5 decimals | |
| // because they affect more than x,y rounding | |
| newcoords[i][1] = newcoords[i][1]; //rx | |
| newcoords[i][2] = newcoords[i][2]; //ry | |
| newcoords[i][3] = newcoords[i][3]; //x-axis-rotation | |
| newcoords[i][6] = newcoords[i][6]; //x | |
| newcoords[i][7] = newcoords[i][7]; //y | |
| } | |
| else if (letter != 'Z') | |
| { | |
| // parse other segs than Z and A | |
| for (j = 1; j < arr[i].length; j = j + 2) | |
| { | |
| if (letter == 'V') y = arr[i][j]; | |
| else if (letter == 'H') x = arr[i][j]; | |
| else | |
| { | |
| x = arr[i][j]; | |
| y = arr[i][j + 1]; | |
| } | |
| pt.x = x; | |
| pt.y = y; | |
| point = pt.matrixTransform(matrix); | |
| if (letter == 'V' || letter == 'H') | |
| { | |
| newcoords[i][0] = 'L'; | |
| newcoords[i][j] = point.x; | |
| newcoords[i][j + 1] = point.y; | |
| } | |
| else | |
| { | |
| newcoords[i][j] = point.x; | |
| newcoords[i][j + 1] = point.y; | |
| } | |
| } | |
| } | |
| if ((letter != 'Z' && subpath_start.x === null) || letter == 'M') | |
| { | |
| subpath_start.x = x; | |
| subpath_start.y = y; | |
| } | |
| if (letter == 'Z') | |
| { | |
| x = subpath_start.x; | |
| y = subpath_start.y; | |
| } | |
| } | |
| // Convert all that was relative back to relative | |
| // This could be combined to above, but to make code more readable | |
| // this is made separately. | |
| var prevXtmp = 0; | |
| var prevYtmp = 0; | |
| subpath_start.x = ''; | |
| for (i = 0; i < newcoords.length; i++) | |
| { | |
| letter_orig = arr_orig[i][0]; | |
| if (letter_orig == 'A' || letter_orig == 'M' || letter_orig == 'L' || letter_orig == 'C' || letter_orig == 'S' || letter_orig == 'Q' || letter_orig == 'T' || letter_orig == 'H' || letter_orig == 'V') | |
| { | |
| var len = newcoords[i].length; | |
| var lentmp = len; | |
| if (letter_orig == 'A') | |
| { | |
| newcoords[i][6] = r(newcoords[i][6]); | |
| newcoords[i][7] = r(newcoords[i][7]); | |
| } | |
| else | |
| { | |
| lentmp--; | |
| while (--lentmp) newcoords[i][lentmp] = r(newcoords[i][lentmp]); | |
| } | |
| prevX = newcoords[i][len - 2]; | |
| prevY = newcoords[i][len - 1]; | |
| } | |
| else | |
| if (letter_orig == 'a') | |
| { | |
| prevXtmp = newcoords[i][6]; | |
| prevYtmp = newcoords[i][7]; | |
| newcoords[i][0] = letter_orig; | |
| newcoords[i][6] = r(newcoords[i][6] - prevX); | |
| newcoords[i][7] = r(newcoords[i][7] - prevY); | |
| prevX = prevXtmp; | |
| prevY = prevYtmp; | |
| } | |
| else | |
| if (letter_orig == 'm' || letter_orig == 'l' || letter_orig == 'c' || letter_orig == 's' || letter_orig == 'q' || letter_orig == 't' || letter_orig == 'h' || letter_orig == 'v') | |
| { | |
| var len = newcoords[i].length; | |
| prevXtmp = newcoords[i][len - 2]; | |
| prevYtmp = newcoords[i][len - 1]; | |
| for (j = 1; j < len; j = j + 2) | |
| { | |
| if (letter_orig == 'h' || letter_orig == 'v') | |
| newcoords[i][0] = 'l'; | |
| else newcoords[i][0] = letter_orig; | |
| newcoords[i][j] = r(newcoords[i][j] - prevX); | |
| newcoords[i][j + 1] = r(newcoords[i][j + 1] - prevY); | |
| } | |
| prevX = prevXtmp; | |
| prevY = prevYtmp; | |
| } | |
| if ((letter_orig.toLowerCase() != 'z' && subpath_start.x == '') || letter_orig.toLowerCase() == 'm') | |
| { | |
| subpath_start.x = prevX; | |
| subpath_start.y = prevY; | |
| } | |
| if (letter_orig.toLowerCase() == 'z') | |
| { | |
| prevX = subpath_start.x; | |
| prevY = subpath_start.y; | |
| } | |
| } | |
| if (toAbsolute) newcoords = pathToAbsolute(newcoords); | |
| path_elem.setAttribute('d', convertToString(newcoords)); | |
| path_elem.removeAttribute('transform'); | |
| } | |
| // Converts all shapes to path retaining attributes. | |
| // oldElem - DOM element to be replaced by path. Can be one of the following: | |
| // ellipse, circle, path, line, polyline, polygon and rect. | |
| // rectAsArgs - Boolean. If true, rect roundings will be as arcs. Otherwise as cubics. | |
| // Return value: path element. | |
| // Source: https://github.com/duopixel/Method-Draw/blob/master/editor/src/svgcanvas.js | |
| // Modifications: Timo (https://github.com/timo22345) | |
| function convertToPath(oldElem, rectAsArgs) | |
| { | |
| if (!oldElem) return; | |
| // Create new path element | |
| var path = document.createElementNS(oldElem.ownerSVGElement.namespaceURI, 'path'); | |
| // All attributes that path element can have | |
| var attrs = ['requiredFeatures', 'requiredExtensions', 'systemLanguage', 'id', 'xml:base', 'xml:lang', 'xml:space', 'onfocusin', 'onfocusout', 'onactivate', 'onclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmousemove', 'onmouseout', 'onload', 'alignment-baseline', 'baseline-shift', 'clip', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cursor', 'direction', 'display', 'dominant-baseline', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'image-rendering', 'kerning', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'overflow', 'pointer-events', 'shape-rendering', 'stop-color', 'stop-opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'unicode-bidi', 'visibility', 'word-spacing', 'writing-mode', 'class', 'style', 'externalResourcesRequired', 'transform', 'd', 'pathLength']; | |
| // Copy attributes of oldElem to path | |
| var attrName, attrValue; | |
| for (var i = 0, ilen = attrs.length; i < ilen; i++) | |
| { | |
| var attrName = attrs[i]; | |
| var attrValue = oldElem.getAttribute(attrName); | |
| if (attrValue) path.setAttribute(attrName, attrValue); | |
| } | |
| var d = ''; | |
| var valid = function (val) | |
| { | |
| return !(typeof (val) !== 'number' || val == Infinity || val < 0); | |
| } | |
| // Possibly the cubed root of 6, but 1.81 works best | |
| var num = 1.81; | |
| var tag = oldElem.tagName; | |
| switch (tag) | |
| { | |
| case 'ellipse': | |
| case 'circle': | |
| var rx = +oldElem.getAttribute('rx'), | |
| ry = +oldElem.getAttribute('ry'), | |
| cx = +oldElem.getAttribute('cx'), | |
| cy = +oldElem.getAttribute('cy'); | |
| if (tag == 'circle') | |
| { | |
| rx = ry = +oldElem.getAttribute('r'); | |
| } | |
| d += convertToString([ | |
| ['M', (cx - rx), (cy)], | |
| ['C', (cx - rx), (cy - ry / num), (cx - rx / num), (cy - ry), (cx), (cy - ry)], | |
| ['C', (cx + rx / num), (cy - ry), (cx + rx), (cy - ry / num), (cx + rx), (cy)], | |
| ['C', (cx + rx), (cy + ry / num), (cx + rx / num), (cy + ry), (cx), (cy + ry)], | |
| ['C', (cx - rx / num), (cy + ry), (cx - rx), (cy + ry / num), (cx - rx), (cy)], | |
| ['Z'] | |
| ]); | |
| break; | |
| case 'path': | |
| d = oldElem.getAttribute('d'); | |
| break; | |
| case 'line': | |
| var x1 = oldElem.getAttribute('x1'), | |
| y1 = oldElem.getAttribute('y1'); | |
| x2 = oldElem.getAttribute('x2'); | |
| y2 = oldElem.getAttribute('y2'); | |
| d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2; | |
| break; | |
| case 'polyline': | |
| d = 'M' + oldElem.getAttribute('points'); | |
| break; | |
| case 'polygon': | |
| d = 'M' + oldElem.getAttribute('points') + 'Z'; | |
| break; | |
| case 'rect': | |
| var rx = +oldElem.getAttribute('rx'), | |
| ry = +oldElem.getAttribute('ry'), | |
| b = oldElem.getBBox(), | |
| x = b.x, | |
| y = b.y, | |
| w = b.width, | |
| h = b.height; | |
| // Validity checks from http://www.w3.org/TR/SVG/shapes.html#RectElement: | |
| // If neither ‘rx’ nor ‘ry’ are properly specified, then set both rx and ry to 0. (This will result in square corners.) | |
| if (!valid(rx) && !valid(ry)) rx = ry = 0; | |
| // Otherwise, if a properly specified value is provided for ‘rx’, but not for ‘ry’, then set both rx and ry to the value of ‘rx’. | |
| else if (valid(rx) && !valid(ry)) ry = rx; | |
| // Otherwise, if a properly specified value is provided for ‘ry’, but not for ‘rx’, then set both rx and ry to the value of ‘ry’. | |
| else if (valid(ry) && !valid(rx)) rx = ry; | |
| else | |
| { | |
| // If rx is greater than half of ‘width’, then set rx to half of ‘width’. | |
| if (rx > w / 2) rx = w / 2; | |
| // If ry is greater than half of ‘height’, then set ry to half of ‘height’. | |
| if (ry > h / 2) ry = h / 2; | |
| } | |
| if (!rx && !ry) | |
| { | |
| d += convertToString([ | |
| ['M', x, y], | |
| ['L', x + w, y], | |
| ['L', x + w, y + h], | |
| ['L', x, y + h], | |
| ['L', x, y], | |
| ['Z'] | |
| ]); | |
| } | |
| else if (rectAsArgs) | |
| { | |
| d += convertToString([ | |
| ['M', x + rx, y], | |
| ['H', x + w - rx], | |
| ['A', rx, ry, 0, 0, 1, x + w, y + ry], | |
| ['V', y + h - ry], | |
| ['A', rx, ry, 0, 0, 1, x + w - rx, y + h], | |
| ['H', x + rx], | |
| ['A', rx, ry, 0, 0, 1, x, y + h - ry], | |
| ['V', y + ry], | |
| ['A', rx, ry, 0, 0, 1, x + rx, y] | |
| ]); | |
| } | |
| else | |
| { | |
| var num = 2.19; | |
| if (!ry) ry = rx | |
| d += convertToString([ | |
| ['M', x, y + ry], | |
| ['C', x, y + ry / num, x + rx / num, y, x + rx, y], | |
| ['L', x + w - rx, y], | |
| ['C', x + w - rx / num, y, x + w, y + ry / num, x + w, y + ry], | |
| ['L', x + w, y + h - ry], | |
| ['C', x + w, y + h - ry / num, x + w - rx / num, y + h, x + w - rx, y + h], | |
| ['L', x + rx, y + h], | |
| ['C', x + rx / num, y + h, x, y + h - ry / num, x, y + h - ry], | |
| ['L', x, y + ry], | |
| ['Z'] | |
| ]); | |
| } | |
| break; | |
| default: | |
| //path.parentNode.removeChild(path); | |
| break; | |
| } | |
| if (d) path.setAttribute('d', d); | |
| // Replace the current element with the converted one | |
| oldElem.parentNode.replaceChild(path, oldElem); | |
| return path; | |
| }; | |
| // This is needed to flatten transformations of elliptical arcs | |
| // Note! This is not needed if Raphael.path2curve is used | |
| function arc_transform(a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint, matrix, svgDOM) | |
| { | |
| function NEARZERO(B) | |
| { | |
| if (Math.abs(B) < 0.0000000000000001) return true; | |
| else return false; | |
| } | |
| var rh, rv, rot; | |
| var m = []; // matrix representation of transformed ellipse | |
| var s, c; // sin and cos helpers (the former offset rotation) | |
| var A, B, C; // ellipse implicit equation: | |
| var ac, A2, C2; // helpers for angle and halfaxis-extraction. | |
| rh = a_rh; | |
| rv = a_rv; | |
| a_offsetrot = a_offsetrot * (Math.PI / 180); // deg->rad | |
| rot = a_offsetrot; | |
| s = parseFloat(Math.sin(rot)); | |
| c = parseFloat(Math.cos(rot)); | |
| // build ellipse representation matrix (unit circle transformation). | |
| // the 2x2 matrix multiplication with the upper 2x2 of a_mat is inlined. | |
| m[0] = matrix.a * +rh * c + matrix.c * rh * s; | |
| m[1] = matrix.b * +rh * c + matrix.d * rh * s; | |
| m[2] = matrix.a * -rv * s + matrix.c * rv * c; | |
| m[3] = matrix.b * -rv * s + matrix.d * rv * c; | |
| // to implict equation (centered) | |
| A = (m[0] * m[0]) + (m[2] * m[2]); | |
| C = (m[1] * m[1]) + (m[3] * m[3]); | |
| B = (m[0] * m[1] + m[2] * m[3]) * 2.0; | |
| // precalculate distance A to C | |
| ac = A - C; | |
| // convert implicit equation to angle and halfaxis: | |
| if (NEARZERO(B)) | |
| { | |
| a_offsetrot = 0; | |
| A2 = A; | |
| C2 = C; | |
| } | |
| else | |
| { | |
| if (NEARZERO(ac)) | |
| { | |
| A2 = A + B * 0.5; | |
| C2 = A - B * 0.5; | |
| a_offsetrot = Math.PI / 4.0; | |
| } | |
| else | |
| { | |
| // Precalculate radical: | |
| var K = 1 + B * B / (ac * ac); | |
| // Clamp (precision issues might need this.. not likely, but better save than sorry) | |
| if (K < 0) K = 0; | |
| else K = Math.sqrt(K); | |
| A2 = 0.5 * (A + C + K * ac); | |
| C2 = 0.5 * (A + C - K * ac); | |
| a_offsetrot = 0.5 * Math.atan2(B, ac); | |
| } | |
| } | |
| // This can get slightly below zero due to rounding issues. | |
| // it's save to clamp to zero in this case (this yields a zero length halfaxis) | |
| if (A2 < 0) A2 = 0; | |
| else A2 = Math.sqrt(A2); | |
| if (C2 < 0) C2 = 0; | |
| else C2 = Math.sqrt(C2); | |
| // now A2 and C2 are half-axis: | |
| if (ac <= 0) | |
| { | |
| a_rv = A2; | |
| a_rh = C2; | |
| } | |
| else | |
| { | |
| a_rv = C2; | |
| a_rh = A2; | |
| } | |
| // If the transformation matrix contain a mirror-component | |
| // winding order of the ellise needs to be changed. | |
| if ((matrix.a * matrix.d) - (matrix.b * matrix.c) < 0) | |
| { | |
| if (!sweep_flag) sweep_flag = 1; | |
| else sweep_flag = 0; | |
| } | |
| // Finally, transform arc endpoint. This takes care about the | |
| // translational part which we ignored at the whole math-showdown above. | |
| endpoint = endpoint.matrixTransform(matrix); | |
| // Radians back to degrees | |
| a_offsetrot = a_offsetrot * 180 / Math.PI; | |
| var r = ['A', a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint.x, endpoint.y]; | |
| return r; | |
| } | |
| // Parts of Raphaël 2.1.0 (MIT licence: http://raphaeljs.com/license.html) | |
| // Contains eg. bugfixed path2curve() function | |
| var R = {}; | |
| var has = 'hasOwnProperty'; | |
| var Str = String; | |
| var array = 'array'; | |
| var isnan = { | |
| 'NaN': 1, | |
| 'Infinity': 1, | |
| '-Infinity': 1 | |
| }; | |
| var lowerCase = Str.prototype.toLowerCase; | |
| var upperCase = Str.prototype.toUpperCase; | |
| var objectToString = Object.prototype.toString; | |
| var concat = 'concat'; | |
| var split = 'split'; | |
| var apply = 'apply'; | |
| var math = Math, | |
| mmax = math.max, | |
| mmin = math.min, | |
| abs = math.abs, | |
| pow = math.pow, | |
| PI = math.PI, | |
| round = math.round, | |
| toFloat = parseFloat, | |
| toInt = parseInt; | |
| var p2s = /,?([achlmqrstvxz]),?/gi; | |
| var pathCommand = /([achlmrqstvz])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig; | |
| var pathValues = /(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/ig; | |
| R.is = function (o, type) | |
| { | |
| type = lowerCase.call(type); | |
| if (type == 'finite') | |
| { | |
| return !isnan[has](+o); | |
| } | |
| if (type == 'array') | |
| { | |
| return o instanceof Array; | |
| } | |
| return type == 'null' && o === null || type == typeof o && o !== null || type == 'object' && o === Object(o) || type == 'array' && Array.isArray && Array.isArray(o) || objectToString.call(o).slice(8, -1).toLowerCase() == type | |
| }; | |
| function clone(obj) | |
| { | |
| if (Object(obj) !== obj) | |
| { | |
| return obj; | |
| } | |
| var res = new obj.constructor; | |
| for (var key in obj) | |
| { | |
| if (obj[has](key)) | |
| { | |
| res[key] = clone(obj[key]); | |
| } | |
| } | |
| return res; | |
| } | |
| R._path2string = function () | |
| { | |
| return this.join(',').replace(p2s, '$1'); | |
| }; | |
| function repush(array, item) | |
| { | |
| for (var i = 0, ii = array.length; i < ii; i++) | |
| if (array[i] === item) | |
| { | |
| return array.push(array.splice(i, 1)[0]); | |
| } | |
| } | |
| var pathClone = function (pathArray) | |
| { | |
| var res = clone(pathArray); | |
| res.toString = R._path2string; | |
| return res; | |
| }; | |
| var paths = function (ps) | |
| { | |
| var p = paths.ps = paths.ps || | |
| {}; | |
| if (p[ps]) p[ps].sleep = 100; | |
| else p[ps] = { | |
| sleep: 100 | |
| }; | |
| setTimeout(function () | |
| { | |
| for (var key in p) | |
| { | |
| if (p[has](key) && key != ps) | |
| { | |
| p[key].sleep--; | |
| !p[key].sleep && delete p[key]; | |
| } | |
| } | |
| }); | |
| return p[ps]; | |
| }; | |
| function catmullRom2bezier(crp, z) | |
| { | |
| var d = []; | |
| for (var i = 0, iLen = crp.length; iLen - 2 * !z > i; i += 2) | |
| { | |
| var p = [ | |
| { | |
| x: +crp[i - 2], | |
| y: +crp[i - 1] | |
| }, | |
| { | |
| x: +crp[i], | |
| y: +crp[i + 1] | |
| }, | |
| { | |
| x: +crp[i + 2], | |
| y: +crp[i + 3] | |
| }, | |
| { | |
| x: +crp[i + 4], | |
| y: +crp[i + 5] | |
| }]; | |
| if (z) | |
| { | |
| if (!i) | |
| { | |
| p[0] = { | |
| x: +crp[iLen - 2], | |
| y: +crp[iLen - 1] | |
| }; | |
| } | |
| else | |
| { | |
| if (iLen - 4 == i) | |
| { | |
| p[3] = { | |
| x: +crp[0], | |
| y: +crp[1] | |
| }; | |
| } | |
| else | |
| { | |
| if (iLen - 2 == i) | |
| { | |
| p[2] = { | |
| x: +crp[0], | |
| y: +crp[1] | |
| }; | |
| p[3] = { | |
| x: +crp[2], | |
| y: +crp[3] | |
| }; | |
| } | |
| } | |
| } | |
| } | |
| else | |
| { | |
| if (iLen - 4 == i) | |
| { | |
| p[3] = p[2]; | |
| } | |
| else | |
| { | |
| if (!i) | |
| { | |
| p[0] = { | |
| x: +crp[i], | |
| y: +crp[i + 1] | |
| }; | |
| } | |
| } | |
| } | |
| d.push(['C', (-p[0].x + 6 * p[1].x + p[2].x) / 6, (-p[0].y + 6 * p[1].y + p[2].y) / 6, (p[1].x + 6 * p[2].x - p[3].x) / 6, (p[1].y + 6 * p[2].y - p[3].y) / 6, p[2].x, p[2].y]) | |
| } | |
| return d | |
| }; | |
| var parsePathString = function (pathString) | |
| { | |
| if (!pathString) return null; | |
| var pth = paths(pathString); | |
| if (pth.arr) return pathClone(pth.arr) | |
| var paramCounts = { | |
| a: 7, | |
| c: 6, | |
| h: 1, | |
| l: 2, | |
| m: 2, | |
| r: 4, | |
| q: 4, | |
| s: 4, | |
| t: 2, | |
| v: 1, | |
| z: 0 | |
| }, data = []; | |
| if (R.is(pathString, array) && R.is(pathString[0], array)) data = pathClone(pathString); | |
| if (!data.length) | |
| { | |
| Str(pathString).replace(pathCommand, function (a, b, c) | |
| { | |
| var params = [], | |
| name = b.toLowerCase(); | |
| c.replace(pathValues, function (a, b) | |
| { | |
| b && params.push(+b); | |
| }); | |
| if (name == 'm' && params.length > 2) | |
| { | |
| data.push([b][concat](params.splice(0, 2))); | |
| name = 'l'; | |
| b = b == 'm' ? 'l' : 'L' | |
| } | |
| if (name == 'r') data.push([b][concat](params)) | |
| else | |
| { | |
| while (params.length >= paramCounts[name]) | |
| { | |
| data.push([b][concat](params.splice(0, paramCounts[name]))); | |
| if (!paramCounts[name]) break; | |
| } | |
| } | |
| }) | |
| } | |
| data.toString = R._path2string; | |
| pth.arr = pathClone(data); | |
| return data; | |
| }; | |
| function repush(array, item) | |
| { | |
| for (var i = 0, ii = array.length; i < ii; i++) | |
| if (array[i] === item) | |
| { | |
| return array.push(array.splice(i, 1)[0]); | |
| } | |
| } | |
| var pathToAbsolute = cacher(function (pathArray) | |
| { | |
| //var pth = paths(pathArray); // Timo: commented to prevent multiple caching | |
| // for some reason only FF proceed correctly | |
| // when not cached using cacher() around | |
| // this function. | |
| //if (pth.abs) return pathClone(pth.abs) | |
| if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) | |
| pathArray = parsePathString(pathArray) | |
| if (!pathArray || !pathArray.length) return [['M', 0, 0]]; | |
| var res = [], | |
| x = 0, | |
| y = 0, | |
| mx = 0, | |
| my = 0, | |
| start = 0; | |
| if (pathArray[0][0] == 'M') | |
| { | |
| x = +pathArray[0][1]; | |
| y = +pathArray[0][2]; | |
| mx = x; | |
| my = y; | |
| start++; | |
| res[0] = ['M', x, y]; | |
| } | |
| var crz = pathArray.length == 3 && pathArray[0][0] == 'M' && pathArray[1][0].toUpperCase() == 'R' && pathArray[2][0].toUpperCase() == 'Z'; | |
| for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) | |
| { | |
| res.push(r = []); | |
| pa = pathArray[i]; | |
| if (pa[0] != upperCase.call(pa[0])) | |
| { | |
| r[0] = upperCase.call(pa[0]); | |
| switch (r[0]) | |
| { | |
| case 'A': | |
| r[1] = pa[1]; | |
| r[2] = pa[2]; | |
| r[3] = pa[3]; | |
| r[4] = pa[4]; | |
| r[5] = pa[5]; | |
| r[6] = +(pa[6] + x); | |
| r[7] = +(pa[7] + y); | |
| break; | |
| case 'V': | |
| r[1] = +pa[1] + y; | |
| break; | |
| case 'H': | |
| r[1] = +pa[1] + x; | |
| break; | |
| case 'R': | |
| var dots = [x, y][concat](pa.slice(1)); | |
| for (var j = 2, jj = dots.length; j < jj; j++) | |
| { | |
| dots[j] = +dots[j] + x; | |
| dots[++j] = +dots[j] + y | |
| } | |
| res.pop(); | |
| res = res[concat](catmullRom2bezier(dots, crz)); | |
| break; | |
| case 'M': | |
| mx = +pa[1] + x; | |
| my = +pa[2] + y; | |
| default: | |
| for (j = 1, jj = pa.length; j < jj; j++) | |
| r[j] = +pa[j] + (j % 2 ? x : y) | |
| } | |
| } | |
| else | |
| { | |
| if (pa[0] == 'R') | |
| { | |
| dots = [x, y][concat](pa.slice(1)); | |
| res.pop(); | |
| res = res[concat](catmullRom2bezier(dots, crz)); | |
| r = ['R'][concat](pa.slice(-2)); | |
| } | |
| else | |
| { | |
| for (var k = 0, kk = pa.length; k < kk; k++) | |
| r[k] = pa[k] | |
| } | |
| } | |
| switch (r[0]) | |
| { | |
| case 'Z': | |
| x = mx; | |
| y = my; | |
| break; | |
| case 'H': | |
| x = r[1]; | |
| break; | |
| case 'V': | |
| y = r[1]; | |
| break; | |
| case 'M': | |
| mx = r[r.length - 2]; | |
| my = r[r.length - 1]; | |
| default: | |
| x = r[r.length - 2]; | |
| y = r[r.length - 1]; | |
| } | |
| } | |
| res.toString = R._path2string; | |
| //pth.abs = pathClone(res); | |
| return res; | |
| }); | |
| function cacher(f, scope, postprocessor) | |
| { | |
| function newf() | |
| { | |
| var arg = Array.prototype.slice.call(arguments, 0), | |
| args = arg.join('\u2400'), | |
| cache = newf.cache = newf.cache || | |
| {}, count = newf.count = newf.count || []; | |
| if (cache.hasOwnProperty(args)) | |
| { | |
| for (var i = 0, ii = count.length; i < ii; i++) | |
| if (count[i] === args) | |
| { | |
| count.push(count.splice(i, 1)[0]); | |
| } | |
| return postprocessor ? postprocessor(cache[args]) : cache[args]; | |
| } | |
| count.length >= 1E3 && delete cache[count.shift()]; | |
| count.push(args); | |
| cache[args] = f.apply(scope, arg); | |
| return postprocessor ? postprocessor(cache[args]) : cache[args]; | |
| } | |
| return newf; | |
| } | |
| var l2c = function (x1, y1, x2, y2) | |
| { | |
| return [x1, y1, x2, y2, x2, y2]; | |
| }, | |
| q2c = function (x1, y1, ax, ay, x2, y2) | |
| { | |
| var _13 = 1 / 3, | |
| _23 = 2 / 3; | |
| return [_13 * x1 + _23 * ax, _13 * y1 + _23 * ay, _13 * x2 + _23 * ax, _13 * y2 + _23 * ay, x2, y2] | |
| }, | |
| a2c = cacher(function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) | |
| { | |
| var _120 = PI * 120 / 180, | |
| rad = PI / 180 * (+angle || 0), | |
| res = [], | |
| xy, | |
| rotate = cacher(function (x, y, rad) | |
| { | |
| var X = x * Math.cos(rad) - y * Math.sin(rad), | |
| Y = x * Math.sin(rad) + y * Math.cos(rad); | |
| return { | |
| x: X, | |
| y: Y | |
| }; | |
| }); | |
| if (!recursive) | |
| { | |
| xy = rotate(x1, y1, -rad); | |
| x1 = xy.x; | |
| y1 = xy.y; | |
| xy = rotate(x2, y2, -rad); | |
| x2 = xy.x; | |
| y2 = xy.y; | |
| var cos = Math.cos(PI / 180 * angle), | |
| sin = Math.sin(PI / 180 * angle), | |
| x = (x1 - x2) / 2, | |
| y = (y1 - y2) / 2; | |
| var h = x * x / (rx * rx) + y * y / (ry * ry); | |
| if (h > 1) | |
| { | |
| h = Math.sqrt(h); | |
| rx = h * rx; | |
| ry = h * ry; | |
| } | |
| var rx2 = rx * rx, | |
| ry2 = ry * ry, | |
| k = (large_arc_flag == sweep_flag ? -1 : 1) * Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))), | |
| cx = k * rx * y / ry + (x1 + x2) / 2, | |
| cy = k * -ry * x / rx + (y1 + y2) / 2, | |
| f1 = Math.asin(((y1 - cy) / ry).toFixed(9)), | |
| f2 = Math.asin(((y2 - cy) / ry).toFixed(9)); | |
| f1 = x1 < cx ? PI - f1 : f1; | |
| f2 = x2 < cx ? PI - f2 : f2; | |
| f1 < 0 && (f1 = PI * 2 + f1); | |
| f2 < 0 && (f2 = PI * 2 + f2); | |
| if (sweep_flag && f1 > f2) | |
| { | |
| f1 = f1 - PI * 2; | |
| } | |
| if (!sweep_flag && f2 > f1) | |
| { | |
| f2 = f2 - PI * 2; | |
| } | |
| } | |
| else | |
| { | |
| f1 = recursive[0]; | |
| f2 = recursive[1]; | |
| cx = recursive[2]; | |
| cy = recursive[3]; | |
| } | |
| var df = f2 - f1; | |
| if (Math.abs(df) > _120) | |
| { | |
| var f2old = f2, | |
| x2old = x2, | |
| y2old = y2; | |
| f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1); | |
| x2 = cx + rx * Math.cos(f2); | |
| y2 = cy + ry * Math.sin(f2); | |
| res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]) | |
| } | |
| df = f2 - f1; | |
| var c1 = Math.cos(f1), | |
| s1 = Math.sin(f1), | |
| c2 = Math.cos(f2), | |
| s2 = Math.sin(f2), | |
| t = Math.tan(df / 4), | |
| hx = 4 / 3 * rx * t, | |
| hy = 4 / 3 * ry * t, | |
| m1 = [x1, y1], | |
| m2 = [x1 + hx * s1, y1 - hy * c1], | |
| m3 = [x2 + hx * s2, y2 - hy * c2], | |
| m4 = [x2, y2]; | |
| m2[0] = 2 * m1[0] - m2[0]; | |
| m2[1] = 2 * m1[1] - m2[1]; | |
| if (recursive) return [m2, m3, m4].concat(res); | |
| else | |
| { | |
| res = [m2, m3, m4].concat(res).join().split(','); | |
| var newres = []; | |
| for (var i = 0, ii = res.length; i < ii; i++) | |
| newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x | |
| return newres | |
| } | |
| }); | |
| var path2curve = cacher(function (path, path2) | |
| { | |
| var pth = !path2 && paths(path); | |
| if (!path2 && pth.curve) return pathClone(pth.curve) | |
| var p = pathToAbsolute(path), | |
| p2 = path2 && pathToAbsolute(path2), | |
| attrs = { | |
| x: 0, | |
| y: 0, | |
| bx: 0, | |
| by: 0, | |
| X: 0, | |
| Y: 0, | |
| qx: null, | |
| qy: null | |
| }, | |
| attrs2 = { | |
| x: 0, | |
| y: 0, | |
| bx: 0, | |
| by: 0, | |
| X: 0, | |
| Y: 0, | |
| qx: null, | |
| qy: null | |
| }, | |
| processPath = function (path, d, pcom) | |
| { | |
| var nx, ny; | |
| if (!path) | |
| { | |
| return ['C', d.x, d.y, d.x, d.y, d.x, d.y]; | |
| }!(path[0] in | |
| { | |
| T: 1, | |
| Q: 1 | |
| }) && (d.qx = d.qy = null); | |
| switch (path[0]) | |
| { | |
| case 'M': | |
| d.X = path[1]; | |
| d.Y = path[2]; | |
| break; | |
| case 'A': | |
| path = ['C'][concat](a2c[apply](0, [d.x, d.y][concat](path.slice(1)))); | |
| break; | |
| case 'S': | |
| if (pcom == 'C' || pcom == 'S') | |
| { | |
| nx = d.x * 2 - d.bx; | |
| ny = d.y * 2 - d.by; | |
| } | |
| else | |
| { | |
| nx = d.x; | |
| ny = d.y; | |
| } | |
| path = ['C', nx, ny][concat](path.slice(1)); | |
| break; | |
| case 'T': | |
| if (pcom == 'Q' || pcom == 'T') | |
| { | |
| d.qx = d.x * 2 - d.qx; | |
| d.qy = d.y * 2 - d.qy; | |
| } | |
| else | |
| { | |
| d.qx = d.x; | |
| d.qy = d.y; | |
| } | |
| path = ['C'][concat](q2c(d.x, d.y, d.qx, d.qy, path[1], path[2])); | |
| break; | |
| case 'Q': | |
| d.qx = path[1]; | |
| d.qy = path[2]; | |
| path = ['C'][concat](q2c(d.x, d.y, path[1], path[2], path[3], path[4])); | |
| break; | |
| case 'L': | |
| path = ['C'][concat](l2c(d.x, d.y, path[1], path[2])); | |
| break; | |
| case 'H': | |
| path = ['C'][concat](l2c(d.x, d.y, path[1], d.y)); | |
| break; | |
| case 'V': | |
| path = ['C'][concat](l2c(d.x, d.y, d.x, path[1])); | |
| break; | |
| case 'Z': | |
| path = ['C'][concat](l2c(d.x, d.y, d.X, d.Y)); | |
| break | |
| } | |
| return path | |
| }, | |
| fixArc = function (pp, i) | |
| { | |
| if (pp[i].length > 7) | |
| { | |
| pp[i].shift(); | |
| var pi = pp[i]; | |
| while (pi.length) | |
| { | |
| pcoms1[i] = 'A'; | |
| p2 && (pcoms2[i] = 'A'); | |
| pp.splice(i++, 0, ['C'][concat](pi.splice(0, 6))); | |
| } | |
| pp.splice(i, 1); | |
| ii = mmax(p.length, p2 && p2.length || 0); | |
| } | |
| }, | |
| fixM = function (path1, path2, a1, a2, i) | |
| { | |
| if (path1 && path2 && path1[i][0] == 'M' && path2[i][0] != 'M') | |
| { | |
| path2.splice(i, 0, ['M', a2.x, a2.y]); | |
| a1.bx = 0; | |
| a1.by = 0; | |
| a1.x = path1[i][1]; | |
| a1.y = path1[i][2]; | |
| ii = mmax(p.length, p2 && p2.length || 0); | |
| } | |
| }, | |
| pcoms1 = [], | |
| pcoms2 = [], | |
| pfirst = '', | |
| pcom = ''; | |
| for (var i = 0, ii = mmax(p.length, p2 && p2.length || 0); i < ii; i++) | |
| { | |
| p[i] && (pfirst = p[i][0]); | |
| if (pfirst != 'C') | |
| { | |
| pcoms1[i] = pfirst; | |
| i && (pcom = pcoms1[i - 1]); | |
| } | |
| p[i] = processPath(p[i], attrs, pcom); | |
| if (pcoms1[i] != 'A' && pfirst == 'C') pcoms1[i] = 'C'; | |
| fixArc(p, i); | |
| if (p2) | |
| { | |
| p2[i] && (pfirst = p2[i][0]); | |
| if (pfirst != 'C') | |
| { | |
| pcoms2[i] = pfirst; | |
| i && (pcom = pcoms2[i - 1]); | |
| } | |
| p2[i] = processPath(p2[i], attrs2, pcom); | |
| if (pcoms2[i] != 'A' && pfirst == 'C') pcoms2[i] = 'C' | |
| fixArc(p2, i); | |
| } | |
| fixM(p, p2, attrs, attrs2, i); | |
| fixM(p2, p, attrs2, attrs, i); | |
| var seg = p[i], | |
| seg2 = p2 && p2[i], | |
| seglen = seg.length, | |
| seg2len = p2 && seg2.length; | |
| attrs.x = seg[seglen - 2]; | |
| attrs.y = seg[seglen - 1]; | |
| attrs.bx = toFloat(seg[seglen - 4]) || attrs.x; | |
| attrs.by = toFloat(seg[seglen - 3]) || attrs.y; | |
| attrs2.bx = p2 && (toFloat(seg2[seg2len - 4]) || attrs2.x); | |
| attrs2.by = p2 && (toFloat(seg2[seg2len - 3]) || attrs2.y); | |
| attrs2.x = p2 && seg2[seg2len - 2]; | |
| attrs2.y = p2 && seg2[seg2len - 1]; | |
| } | |
| if (!p2) pth.curve = pathClone(p); | |
| return p2 ? [p, p2] : p | |
| }, null, pathClone); | |
| // Export function | |
| window.flatten = flatten; | |
| })(); |
How do I make use of this? In the fiddle http://jsfiddle.net/Nv78L/3/embedded/result/ if you inspect the SVG with dev tools, there are still 'transform' attributes. How do you see the output with those flattened? Thanks.
Is this part of a larger open source project, or just standalone? I want to include this in another open source project, but want to reference the canonical source. Also made some modifications/improvements and would make a PR.
@nathancooper: It is possibly due to that Chrome dropped support for pathDOM.getTransformToElement. I made new version which uses shim: http://jsfiddle.net/nrjvmqur/embedded/result/.
Also I updated the "SVG Path and Shape Randomizer, Normalizer, Flattener, Bounding Box calculator": https://jsfiddle.net/fjm9423q/embedded/result/
@brantwedel No, this is not part of the larger project.
The reason for embedding a bit of code from Raphael-library was that Raphael cannot be used in Webworker, so I had to extract needed functions, mainly path2curve. The original Raphael-code had also a bug in that function, so the own version was needed also because of that.
Thanks a lot for this great job. There might be one little bug: svg element transforms are dropped.
Consider this example:
myrect should be at x=160 after flattening. It is at x=60.
Sorry, the example was eaten in my previous comment. So now without tag marks
svg id="wrapper" transform="translate(100, 100)"
rect id="myrect" x="10" y="10" width="100" height="100" fill="hotpink" transform="translate(50 0)" /
/svg
Fantastic work!! Do you have plans for making it into an NPM module?
Your tool is the perfect complement to https://github.com/benjamminf/warpjs
would be awesome with an option to prevent convert shapes to path? is that possible?
uhm why the hell is this
<html>
<title>Flatten.js, General SVG Flattener</title>
<head>
<script>
at the beginning?
There is an issue with arc_transform
that part of code wrongly swaps RX and RY
// now A2 and C2 are half-axis:
if (ac <= 0) {
a_rv = A2;
a_rh = C2;
}
else {
a_rv = C2;
a_rh = A2;
}
Here is the demo:
https://jsfiddle.net/fzsLed38/29/
This seems like an alternate to this script https://svgjs.dev/docs/3.0/manipulating/#flatten.
Here's the parent issue in svg.js: svgdotjs/svg.js#238.
import { registerWindow, SVG } from "@svgdotjs/svg.js";
export async function flattenSVG(input: string): Promise<string> {
const svgdom = await import("svgdom");
const svgWindow = svgdom.createSVGWindow();
const svgDocument = svgWindow.document;
registerWindow(svgWindow, svgDocument);
const canvas = SVG(input);
return canvas.root().flatten(canvas.root()).svg();
}
Your code handles very attractive area of SVG manipulation.
I'm afraid that return value as coded in https://gist.github.com/timo22345/9413158#file-flatten-js-L92 makes no sense.