Created
October 19, 2011 07:37
-
-
Save YannickBochatay/1297684 to your computer and use it in GitHub Desktop.
javascript workaround for unimplemented normalizedPathSegList
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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