-
-
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; | |
})(); |
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.
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/
Thank you Timo, This is an awesome work. Any updates on converting the text to path part please?!