Skip to content

Instantly share code, notes, and snippets.

@YannickBochatay
Created October 19, 2011 07:37
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save YannickBochatay/1297684 to your computer and use it in GitHub Desktop.
Save YannickBochatay/1297684 to your computer and use it in GitHub Desktop.
javascript workaround for unimplemented normalizedPathSegList
/*
Transform paths with only M,L,C,Z as described by w3c
http://www.w3.org/TR/2003/REC-SVG11-20030114/paths.html#InterfaceSVGAnimatedPathData
Example :
var path = document.createElementNS("http://www.w3.org/2000/svg",tag);
path.setAttribute('d','M30 30S40 23 23 42C113,113 136,113 150,80t40,50T230,240q20 20 54 20s40 23 23 42t20,30a20,30 0,0,1 -50,-50');
path.normalizePath();
console.log(path.getAttribute(d')); //
*/
SVGPathElement.prototype.normalizePath = function(bezierDegree,defaultFlatness) {
var seg,letter,currentPoint,
list = this.pathSegList,
newpath = document.createElementNS("http://www.w3.org/2000/svg",'path'),
method,newseg,
x,y,
i=0,N = list.numberOfItems,
j,M,bezier,
getCurrentPoint = function(i) {
var j=i,
x=false,y=false,
prev;
while (x===false || y===false) {
j--;
if (j<0) {
if (x === false) { x = 0; }
if (y === false) { y = 0; }
}
else {
prev = list.getItem(j);
if (prev.x!==undefined && x === false) { x = prev.x; }
if (prev.y!==undefined && y === false) { y = prev.y; }
}
}
return {x:x,y:y};
};
for (;i<N;i++) {
seg = list.getItem(i);
letter = seg.pathSegTypeAsLetter;
currentPoint = getCurrentPoint(i);
//First transform relative to aboslute segments
if (letter.toUpperCase() !== letter && letter!=='z') {
method = 'createSVGPathSeg';
switch (letter) {
case 'z' : method+='ClosePath'; break;
case 'm' : method+='Moveto'; break;
case 'l' : method+='Lineto'; break;
case 'c' : method+='CurvetoCubic'; break;
case 'q' : method+='CurvetoQuadratic'; break;
case 'a' : method+='Arc'; break;
case 'h' : method+='LinetoHorizontal'; break;
case 'v' : method+='LinetoVertical'; break;
case 's' : method+='CurvetoCubicSmooth'; break;
case 't' : method+='CurvetoQuadraticSmooth'; break;
}
method+='Abs';
args = [];
if (letter === 'h') { args.push(currentPoint.x+seg.x); }
else if (letter === 'v') { args.push(currentPoint.y+seg.y); }
else {
args.push(currentPoint.x+seg.x,currentPoint.y+seg.y);
switch (letter) {
case 'c' : args.push(currentPoint.x+seg.x1,currentPoint.y+seg.y1,currentPoint.x+seg.x2,currentPoint.y+seg.y2); break;
case 'q' : args.push(currentPoint.x+seg.x1,currentPoint.y+seg.y1); break;
case 'a' : args.push(seg.r1,seg.r2,seg.angle,seg.largArcFlag,seg.sweepFlag); break;
case 's' : args.push(currentPoint.x+seg.x2,currentPoint.y+seg.y2); break;
}
}
seg = this[method].apply(this,args);
list.replaceItem(seg,i);
letter = letter.toUpperCase();
}
if (letter === 'H') {
newseg = this.createSVGPathSegLinetoAbs(seg.x,currentPoint.y);
newpath.pathSegList.appendItem(newseg);
}
else if (letter === 'V') {
newseg = this.createSVGPathSegLinetoAbs(currentPoint.x,seg.y);
newpath.pathSegList.appendItem(newseg);
}
else if (letter === 'S') { //transform S to C
if (i === 0 || list.getItem(i-1).pathSegTypeAsLetter !== 'C') {
x = currentPoint.x;
y = currentPoint.y;
}
else {
x = currentPoint.x * 2 - list.getItem(i-1).x2;
y = currentPoint.y * 2 - list.getItem(i-1).y2;
}
newseg = this.createSVGPathSegCurvetoCubicAbs(seg.x,seg.y,x,y,seg.x2,seg.y2);
list.replaceItem(newseg,i);
i--;continue;
}
else if (letter === 'Q') {
newseg = this.createSVGPathSegCurvetoCubicAbs(seg.x,seg.y, 1/3 * currentPoint.x + 2/3 * seg.x1, currentPoint.y/3 + 2/3 *seg.y1,2/3 * seg.x1 + 1/3 * seg.x, 2/3 * seg.y1 + 1/3 * seg.y);
newpath.pathSegList.appendItem(newseg);
}
else if (letter === 'T') { //transform T to Q
if (i === 0 || list.getItem(i-1).pathSegTypeAsLetter !== 'Q') {
x = currentPoint.x;
y = currentPoint.y;
}
else {
x = currentPoint.x * 2 - list.getItem(i-1).x1;
y = currentPoint.y * 2 - list.getItem(i-1).y1;
}
newseg = this.createSVGPathSegCurvetoQuadraticAbs(seg.x,seg.y,x,y,seg.x2,seg.y2);
list.replaceItem( newseg , i );
i--;continue;
}
else if (letter === 'A') {
bezier = seg.toBezier(currentPoint,bezierDegree,defaultFlatness);
for (j=0,M=bezier.numberOfItems;j<M;j++) {
newpath.pathSegList.appendItem(bezier.getItem(j));
};
}
else { newpath.pathSegList.appendItem(seg); }
}
list.clear();
for (j=0,M=newpath.pathSegList.numberOfItems;j<M;j++) {
list.appendItem(newpath.pathSegList.getItem(j));
};
return this;
};
SVGPathSegArcAbs.prototype.toBezier = function(currentPoint,bezierDegree,defaultFlatness) {
///////////////////////////////////////
//from Luc Maisonobe www.spaceroots.org
this.angleRad = this.angle*Math.PI/180;
this.x1 = currentPoint.x;
this.y1 = currentPoint.y;
this.r1 = Math.abs(this.r1);
this.r2 = Math.abs(this.r2);
defaultFlatness = defaultFlatness || 0.5;
bezierDegree = bezierDegree || 1;
// find the number of Bézier curves needed
var found = false,
i,n = 1,
dEta,etaA,etaB,
path = document.createElementNS("http://www.w3.org/2000/svg",'path'),
seg,
coefs = {
degree2 : {
low : [
[
[ 3.92478, -13.5822, -0.233377, 0.0128206 ],
[ -1.08814, 0.859987, 0.000362265, 0.000229036 ],
[ -0.942512, 0.390456, 0.0080909, 0.00723895 ],
[ -0.736228, 0.20998, 0.0129867, 0.0103456 ]
], [
[ -0.395018, 6.82464, 0.0995293, 0.0122198 ],
[ -0.545608, 0.0774863, 0.0267327, 0.0132482 ],
[ 0.0534754, -0.0884167, 0.012595, 0.0343396 ],
[ 0.209052, -0.0599987, -0.00723897, 0.00789976 ]
]
],
high : [
[
[ 0.0863805, -11.5595, -2.68765, 0.181224 ],
[ 0.242856, -1.81073, 1.56876, 1.68544 ],
[ 0.233337, -0.455621, 0.222856, 0.403469 ],
[ 0.0612978, -0.104879, 0.0446799, 0.00867312 ]
], [
[ 0.028973, 6.68407, 0.171472, 0.0211706 ],
[ 0.0307674, -0.0517815, 0.0216803, -0.0749348 ],
[ -0.0471179, 0.1288, -0.0781702, 2.0 ],
[ -0.0309683, 0.0531557, -0.0227191, 0.0434511 ]
]
],
safety : [ 0.001, 4.98, 0.207, 0.0067 ]
},
degree3 : {
low : [
[
[ 3.85268, -21.229, -0.330434, 0.0127842 ],
[ -1.61486, 0.706564, 0.225945, 0.263682 ],
[ -0.910164, 0.388383, 0.00551445, 0.00671814 ],
[ -0.630184, 0.192402, 0.0098871, 0.0102527 ]
],[
[ -0.162211, 9.94329, 0.13723, 0.0124084 ],
[ -0.253135, 0.00187735, 0.0230286, 0.01264 ],
[ -0.0695069, -0.0437594, 0.0120636, 0.0163087 ],
[ -0.0328856, -0.00926032, -0.00173573, 0.00527385 ]
]
],
high : [
[
[ 0.0899116, -19.2349, -4.11711, 0.183362 ],
[ 0.138148, -1.45804, 1.32044, 1.38474 ],
[ 0.230903, -0.450262, 0.219963, 0.414038 ],
[ 0.0590565, -0.101062, 0.0430592, 0.0204699 ]
], [
[ 0.0164649, 9.89394, 0.0919496, 0.00760802 ],
[ 0.0191603, -0.0322058, 0.0134667, -0.0825018 ],
[ 0.0156192, -0.017535, 0.00326508, -0.228157 ],
[ -0.0236752, 0.0405821, -0.0173086, 0.176187 ]
]
],
safety : [ 0.001, 4.98, 0.207, 0.0067 ]
}
},
rationalFunction = function(x,c) {
return (x * (x * c[0] + c[1]) + c[2]) / (x + c[3]);
},
estimateError = function(etaA,etaB) {
var eta = 0.5 * (etaA + etaB);
if (bezierDegree < 2) {
// start point
var aCosEtaA = this.r1 * Math.cos(etaA),
bSinEtaA = this.r2 * Math.sin(etaA),
xA = this.cx + aCosEtaA * Math.cos(this.angleRad) - bSinEtaA * Math.sin(this.angleRad),
yA = this.cy + aCosEtaA * Math.sin(this.angleRad) + Math.sin(this.angleRad) * Math.cos(this.angleRad),
// end point
aCosEtaB = this.r1 * Math.cos(etaB),
bSinEtaB = this.r2 * Math.sin(etaB),
xB = this.cx + aCosEtaB * Math.cos(this.angleRad) - bSinEtaB * Math.sin(this.angleRad),
yB = this.cy + aCosEtaB * Math.sin(this.angleRad) + bSinEtaB * Math.cos(this.angleRad),
// maximal error point
aCosEta = this.r1 * Math.cos(eta),
bSinEta = this.r2 * Math.sin(eta),
x = this.cx + aCosEta * Math.cos(this.angleRad) - bSinEta * Math.sin(this.angleRad),
y = this.cy + aCosEta * Math.sin(this.angleRad) + bSinEta * Math.cos(this.angleRad),
dx = xB - xA,
dy = yB - yA;
return Math.abs(x * dy - y * dx + xB * yA - xA * yB) / Math.sqrt(dx * dx + dy * dy);
}
else {
var x = this.r2 / this.r1,
dEta = etaB - etaA,
cos2 = Math.cos(2 * eta),
cos4 = Math.cos(4 * eta),
cos6 = Math.cos(6 * eta),
// select the right coeficients set according to degree and b/a
coeffs = (x < 0.25) ? coefs['degree'+bezierDegree].low : coefs['degree'+bezierDegree].high,
c0 = rationalFunction(x, coeffs[0][0]) + cos2 * rationalFunction(x, coeffs[0][1]) +
cos4 * rationalFunction(x, coeffs[0][2]) + cos6 * rationalFunction(x, coeffs[0][3]),
c1 = rationalFunction(x, coeffs[1][0]) + cos2 * rationalFunction(x, coeffs[1][1]) + cos4 *
rationalFunction(x, coeffs[1][2]) + cos6 * rationalFunction(x, coeffs[1][3]);
return rationalFunction(x, coefs['degree'+bezierDegree].safety) * this.r1 * Math.exp(c0 + c1 * dEta);
}
};
(function() { //compute center and angles
//from http://www.w3.org/TR/2003/REC-SVG11-20030114/implnote.html#ArcConversionEndpointToCenter
var xp1 = Math.cos(this.angleRad) * (this.x1-this.x) / 2 +
Math.sin(this.angleRad) * (this.y1-this.y) / 2,
yp1 = -Math.sin(this.angleRad) * (this.x1-this.x) / 2 +
Math.cos(this.angleRad) * (this.y1-this.y) / 2,
r1c = Math.pow(this.r1,2),
r2c = Math.pow(this.r2,2),
xp1c = Math.pow(xp1,2),
yp1c = Math.pow(yp1,2),
lambda = xp1c / r1c + yp1c / r2c; //Ensure radii are large enough
if (lambda > 1) {
this.r1*=Math.sqrt(lambda);
this.r2*=Math.sqrt(lambda);
r1c = Math.pow(this.r1,2);
r2c = Math.pow(this.r2,2);
}
var coef = (this.largeArcFlag === this.sweepFlag ? -1 : 1 ) *
Math.sqrt( Math.max(0,( r1c*r2c - r1c*yp1c - r2c*xp1c ) / ( r1c*yp1c + r2c*xp1c)) ),
cpx = coef * ( this.r1 * yp1 ) / this.r2,
cpy = coef * ( - this.r2 * xp1 ) / this.r1,
cx = Math.cos(this.angleRad) * cpx - Math.sin(this.angleRad) * cpy + (this.x1 + this.x) / 2,
cy = Math.sin(this.angleRad) * cpx + Math.cos(this.angleRad) * cpy + (this.y1 + this.y) / 2,
cosTheta = ( (xp1-cpx)/this.r1 ) / Math.sqrt( Math.pow( (xp1-cpx)/this.r1 , 2 ) + Math.pow( (yp1-cpy)/this.r2 , 2 ) ),
theta = ( (yp1-cpy)/this.r2 > 0 ? 1 : -1) * Math.acos(cosTheta),
u = { x : (xp1-cpx) /this.r1 , y : (yp1-cpy) /this.r2 },
v = { x : (-xp1-cpx)/this.r1 , y : (-yp1-cpy)/this.r2 },
cosDeltaTheta = ( u.x * v.x + u.y * v.y ) / ( Math.sqrt(Math.pow(u.x,2) + Math.pow(u.y,2)) * Math.sqrt(Math.pow(v.x,2) + Math.pow(v.y,2)) ),
deltaTheta = (u.x*v.y-u.y*v.x > 0 ? 1 : -1) * Math.acos(Math.max(-1,Math.min(1,cosDeltaTheta))) % (Math.PI*2);
if (this.sweepFlag === false && deltaTheta > 0) { deltaTheta-=Math.PI*2; }
else if (this.sweepFlag === true && deltaTheta < 0) { deltaTheta+=Math.PI*2; }
this.eta1 = theta;
this.eta2 = theta + deltaTheta;
this.cx = cx;
this.cy = cy;
}).call(this);
while ((!found) && (n < 1024)) {
dEta = (this.eta2 - this.eta1) / n;
if (dEta <= 0.5 * Math.PI) {
etaB = this.eta1;
found = true;
for (i=0; found && (i<n); ++i) {
etaA = etaB;
etaB += dEta;
found = (estimateError.call(this,etaA, etaB) <= defaultFlatness);
}
}
n = n << 1;
}
dEta = (this.eta2 - this.eta1) / n;
etaB = this.eta1;
var aCosEtaB = this.r1 * Math.cos(etaB),
bSinEtaB = this.r2 * Math.sin(etaB),
aSinEtaB = this.r1 * Math.sin(etaB),
bCosEtaB = this.r2 * Math.cos(etaB),
xB = this.cx + aCosEtaB * Math.cos(this.angleRad) - bSinEtaB * Math.sin(this.angleRad),
yB = this.cy + aCosEtaB * Math.sin(this.angleRad) + bSinEtaB * Math.cos(this.angleRad),
xADot,yADot,
xBDot = -aSinEtaB * Math.cos(this.angleRad) - bCosEtaB * Math.sin(this.angleRad),
yBDot = -aSinEtaB * Math.sin(this.angleRad) + bCosEtaB * Math.cos(this.angleRad);
var t = Math.tan(0.5 * dEta),
alpha = Math.sin(dEta) * (Math.sqrt(4 + 3 * t * t) - 1) / 3,
xA,yA,xB,yB,xADot,xBDot,k;
for (var i = 0; i < n; ++i) {
etaA = etaB;
xA = xB;
yA = yB;
xADot = xBDot;
yADot = yBDot;
etaB += dEta;
aCosEtaB = this.r1 * Math.cos(etaB);
bSinEtaB = this.r2 * Math.sin(etaB);
aSinEtaB = this.r1 * Math.sin(etaB);
bCosEtaB = this.r2 * Math.cos(etaB);
xB = this.cx + aCosEtaB * Math.cos(this.angleRad) - bSinEtaB * Math.sin(this.angleRad);
yB = this.cy + aCosEtaB * Math.sin(this.angleRad) + bSinEtaB * Math.cos(this.angleRad);
xBDot = -aSinEtaB * Math.cos(this.angleRad) - bCosEtaB * Math.sin(this.angleRad);
yBDot = -aSinEtaB * Math.sin(this.angleRad) + bCosEtaB * Math.cos(this.angleRad);
if (bezierDegree == 1) {
seg = path.createSVGPathSegLinetoAbs(xB,yB);
}
else if (bezierDegree == 2) {
k = (yBDot * (xB - xA) - xBDot * (yB - yA)) / (xADot * yBDot - yADot * xBDot);
seg = path.createSVGPathSegCurvetoQuadraticAbs(xB , yB , xA + k * xADot , yA + k * yADot);
} else {
seg = path.createSVGPathSegCurvetoCubicAbs(xB , yB , xA + alpha * xADot , yA + alpha * yADot, xB - alpha * xBDot, yB - alpha * yBDot);
}
path.pathSegList.appendItem(seg);
}
return path.pathSegList;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment