Skip to content

Instantly share code, notes, and snippets.

@timo22345
Last active August 12, 2024 06:19
Show Gist options
  • Save timo22345/9413158 to your computer and use it in GitHub Desktop.
Save timo22345/9413158 to your computer and use it in GitHub Desktop.
Flatten.js, general SVG flattener. Flattens transformations of SVG shapes and paths. All shapes and path commands are supported.
<!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;
})();
@veselin-kutsarov
Copy link

veselin-kutsarov commented Mar 13, 2024

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment