Created
November 16, 2018 05:05
-
-
Save navono/0be332db5a26ced364fe090e8e53d0ec to your computer and use it in GitHub Desktop.
Canvas path render with pathdata
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
define(function (require) { | |
// come from https://github.com/konvajs/konva/blob/3cfb57681201271b1d71eea8416557d0e5ace8ac/src/shapes/Path.js | |
function getLineLength(x1, y1, x2, y2) { | |
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); | |
}; | |
function getPointOnCubicBezier( | |
pct, | |
P1x, | |
P1y, | |
P2x, | |
P2y, | |
P3x, | |
P3y, | |
P4x, | |
P4y | |
) { | |
function CB1(t) { | |
return t * t * t; | |
} | |
function CB2(t) { | |
return 3 * t * t * (1 - t); | |
} | |
function CB3(t) { | |
return 3 * t * (1 - t) * (1 - t); | |
} | |
function CB4(t) { | |
return (1 - t) * (1 - t) * (1 - t); | |
} | |
var x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct); | |
var y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct); | |
return { | |
x: x, | |
y: y | |
}; | |
}; | |
function getPointOnQuadraticBezier( | |
pct, | |
P1x, | |
P1y, | |
P2x, | |
P2y, | |
P3x, | |
P3y | |
) { | |
function QB1(t) { | |
return t * t; | |
} | |
function QB2(t) { | |
return 2 * t * (1 - t); | |
} | |
function QB3(t) { | |
return (1 - t) * (1 - t); | |
} | |
var x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct); | |
var y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct); | |
return { | |
x: x, | |
y: y | |
}; | |
}; | |
function getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi) { | |
var cosPsi = Math.cos(psi), | |
sinPsi = Math.sin(psi); | |
var pt = { | |
x: rx * Math.cos(theta), | |
y: ry * Math.sin(theta) | |
}; | |
return { | |
x: cx + (pt.x * cosPsi - pt.y * sinPsi), | |
y: cy + (pt.x * sinPsi + pt.y * cosPsi) | |
}; | |
}; | |
function calcLength(x, y, cmd, points) { | |
var len, p1, p2, t; | |
// var path = Konva.Path; | |
switch (cmd) { | |
case 'L': | |
return getLineLength(x, y, points[0], points[1]); | |
case 'C': | |
// Approximates by breaking curve into 100 line segments | |
len = 0.0; | |
p1 = getPointOnCubicBezier( | |
0, | |
x, | |
y, | |
points[0], | |
points[1], | |
points[2], | |
points[3], | |
points[4], | |
points[5] | |
); | |
for (t = 0.01; t <= 1; t += 0.01) { | |
p2 = getPointOnCubicBezier( | |
t, | |
x, | |
y, | |
points[0], | |
points[1], | |
points[2], | |
points[3], | |
points[4], | |
points[5] | |
); | |
len += getLineLength(p1.x, p1.y, p2.x, p2.y); | |
p1 = p2; | |
} | |
return len; | |
case 'Q': | |
// Approximates by breaking curve into 100 line segments | |
len = 0.0; | |
p1 = getPointOnQuadraticBezier( | |
0, | |
x, | |
y, | |
points[0], | |
points[1], | |
points[2], | |
points[3] | |
); | |
for (t = 0.01; t <= 1; t += 0.01) { | |
p2 = getPointOnQuadraticBezier( | |
t, | |
x, | |
y, | |
points[0], | |
points[1], | |
points[2], | |
points[3] | |
); | |
len += getLineLength(p1.x, p1.y, p2.x, p2.y); | |
p1 = p2; | |
} | |
return len; | |
case 'A': | |
// Approximates by breaking curve into line segments | |
len = 0.0; | |
var start = points[4]; | |
// 4 = theta | |
var dTheta = points[5]; | |
// 5 = dTheta | |
var end = points[4] + dTheta; | |
var inc = Math.PI / 180.0; | |
// 1 degree resolution | |
if (Math.abs(start - end) < inc) { | |
inc = Math.abs(start - end); | |
} | |
// Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi | |
p1 = getPointOnEllipticalArc( | |
points[0], | |
points[1], | |
points[2], | |
points[3], | |
start, | |
0 | |
); | |
if (dTheta < 0) { | |
// clockwise | |
for (t = start - inc; t > end; t -= inc) { | |
p2 = getPointOnEllipticalArc( | |
points[0], | |
points[1], | |
points[2], | |
points[3], | |
t, | |
0 | |
); | |
len += getLineLength(p1.x, p1.y, p2.x, p2.y); | |
p1 = p2; | |
} | |
} else { | |
// counter-clockwise | |
for (t = start + inc; t < end; t += inc) { | |
p2 = getPointOnEllipticalArc( | |
points[0], | |
points[1], | |
points[2], | |
points[3], | |
t, | |
0 | |
); | |
len += getLineLength(p1.x, p1.y, p2.x, p2.y); | |
p1 = p2; | |
} | |
} | |
p2 = getPointOnEllipticalArc( | |
points[0], | |
points[1], | |
points[2], | |
points[3], | |
end, | |
0 | |
); | |
len += getLineLength(p1.x, p1.y, p2.x, p2.y); | |
return len; | |
} | |
return 0; | |
}; | |
function convertEndpointToCenterParameterization( | |
x1, | |
y1, | |
x2, | |
y2, | |
fa, | |
fs, | |
rx, | |
ry, | |
psiDeg | |
) { | |
// Derived from: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes | |
var psi = psiDeg * (Math.PI / 180.0); | |
var xp = Math.cos(psi) * (x1 - x2) / 2.0 + Math.sin(psi) * (y1 - y2) / 2.0; | |
var yp = | |
-1 * Math.sin(psi) * (x1 - x2) / 2.0 + Math.cos(psi) * (y1 - y2) / 2.0; | |
var lambda = xp * xp / (rx * rx) + yp * yp / (ry * ry); | |
if (lambda > 1) { | |
rx *= Math.sqrt(lambda); | |
ry *= Math.sqrt(lambda); | |
} | |
var f = Math.sqrt( | |
(rx * rx * (ry * ry) - rx * rx * (yp * yp) - ry * ry * (xp * xp)) / | |
(rx * rx * (yp * yp) + ry * ry * (xp * xp)) | |
); | |
if (fa === fs) { | |
f *= -1; | |
} | |
if (isNaN(f)) { | |
f = 0; | |
} | |
var cxp = f * rx * yp / ry; | |
var cyp = f * -ry * xp / rx; | |
var cx = (x1 + x2) / 2.0 + Math.cos(psi) * cxp - Math.sin(psi) * cyp; | |
var cy = (y1 + y2) / 2.0 + Math.sin(psi) * cxp + Math.cos(psi) * cyp; | |
var vMag = function(v) { | |
return Math.sqrt(v[0] * v[0] + v[1] * v[1]); | |
}; | |
var vRatio = function(u, v) { | |
return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v)); | |
}; | |
var vAngle = function(u, v) { | |
return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v)); | |
}; | |
var theta = vAngle([1, 0], [(xp - cxp) / rx, (yp - cyp) / ry]); | |
var u = [(xp - cxp) / rx, (yp - cyp) / ry]; | |
var v = [(-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry]; | |
var dTheta = vAngle(u, v); | |
if (vRatio(u, v) <= -1) { | |
dTheta = Math.PI; | |
} | |
if (vRatio(u, v) >= 1) { | |
dTheta = 0; | |
} | |
if (fs === 0 && dTheta > 0) { | |
dTheta = dTheta - 2 * Math.PI; | |
} | |
if (fs === 1 && dTheta < 0) { | |
dTheta = dTheta + 2 * Math.PI; | |
} | |
return [cx, cy, rx, ry, theta, dTheta, psi, fs]; | |
}; | |
return { | |
parsePathData: function (data) { | |
// Path Data Segment must begin with a moveTo | |
//m (x y)+ Relative moveTo (subsequent points are treated as lineTo) | |
//M (x y)+ Absolute moveTo (subsequent points are treated as lineTo) | |
//l (x y)+ Relative lineTo | |
//L (x y)+ Absolute LineTo | |
//h (x)+ Relative horizontal lineTo | |
//H (x)+ Absolute horizontal lineTo | |
//v (y)+ Relative vertical lineTo | |
//V (y)+ Absolute vertical lineTo | |
//z (closepath) | |
//Z (closepath) | |
//c (x1 y1 x2 y2 x y)+ Relative Bezier curve | |
//C (x1 y1 x2 y2 x y)+ Absolute Bezier curve | |
//q (x1 y1 x y)+ Relative Quadratic Bezier | |
//Q (x1 y1 x y)+ Absolute Quadratic Bezier | |
//t (x y)+ Shorthand/Smooth Relative Quadratic Bezier | |
//T (x y)+ Shorthand/Smooth Absolute Quadratic Bezier | |
//s (x2 y2 x y)+ Shorthand/Smooth Relative Bezier curve | |
//S (x2 y2 x y)+ Shorthand/Smooth Absolute Bezier curve | |
//a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Relative Elliptical Arc | |
//A (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Absolute Elliptical Arc | |
// return early if data is not defined | |
if (!data) { | |
return []; | |
} | |
// command string | |
var cs = data; | |
// command chars | |
var cc = [ | |
'm', | |
'M', | |
'l', | |
'L', | |
'v', | |
'V', | |
'h', | |
'H', | |
'z', | |
'Z', | |
'c', | |
'C', | |
'q', | |
'Q', | |
't', | |
'T', | |
's', | |
'S', | |
'a', | |
'A' | |
]; | |
// convert white spaces to commas | |
cs = cs.replace(new RegExp(' ', 'g'), ','); | |
// create pipes so that we can split the data | |
for (var n = 0; n < cc.length; n++) { | |
cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]); | |
} | |
// create array | |
var arr = cs.split('|'); | |
var ca = []; | |
var coords = []; | |
// init context point | |
var cpx = 0; | |
var cpy = 0; | |
var re = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/gi; | |
var match; | |
for (n = 1; n < arr.length; n++) { | |
var str = arr[n]; | |
var c = str.charAt(0); | |
str = str.slice(1); | |
coords.length = 0; | |
while ((match = re.exec(str))) { | |
coords.push(match[0]); | |
} | |
// while ((match = re.exec(str))) { | |
// coords.push(match[0]); | |
// } | |
var p = []; | |
for (var j = 0, jlen = coords.length; j < jlen; j++) { | |
var parsed = parseFloat(coords[j]); | |
if (!isNaN(parsed)) { | |
p.push(parsed); | |
} else { | |
p.push(0); | |
} | |
} | |
while (p.length > 0) { | |
if (isNaN(p[0])) { | |
// case for a trailing comma before next command | |
break; | |
} | |
var cmd = null; | |
var points = []; | |
var startX = cpx, | |
startY = cpy; | |
// Move var from within the switch to up here (jshint) | |
var prevCmd, ctlPtx, ctlPty; // Ss, Tt | |
var rx, ry, psi, fa, fs, x1, y1; // Aa | |
// convert l, H, h, V, and v to L | |
switch (c) { | |
// Note: Keep the lineTo's above the moveTo's in this switch | |
case 'l': | |
cpx += p.shift(); | |
cpy += p.shift(); | |
cmd = 'L'; | |
points.push(cpx, cpy); | |
break; | |
case 'L': | |
cpx = p.shift(); | |
cpy = p.shift(); | |
points.push(cpx, cpy); | |
break; | |
// Note: lineTo handlers need to be above this point | |
case 'm': | |
var dx = p.shift(); | |
var dy = p.shift(); | |
cpx += dx; | |
cpy += dy; | |
cmd = 'M'; | |
// After closing the path move the current position | |
// to the the first point of the path (if any). | |
if (ca.length > 2 && ca[ca.length - 1].command === 'z') { | |
for (var idx = ca.length - 2; idx >= 0; idx--) { | |
if (ca[idx].command === 'M') { | |
cpx = ca[idx].points[0] + dx; | |
cpy = ca[idx].points[1] + dy; | |
break; | |
} | |
} | |
} | |
points.push(cpx, cpy); | |
c = 'l'; | |
// subsequent points are treated as relative lineTo | |
break; | |
case 'M': | |
cpx = p.shift(); | |
cpy = p.shift(); | |
cmd = 'M'; | |
points.push(cpx, cpy); | |
c = 'L'; | |
// subsequent points are treated as absolute lineTo | |
break; | |
case 'h': | |
cpx += p.shift(); | |
cmd = 'L'; | |
points.push(cpx, cpy); | |
break; | |
case 'H': | |
cpx = p.shift(); | |
cmd = 'L'; | |
points.push(cpx, cpy); | |
break; | |
case 'v': | |
cpy += p.shift(); | |
cmd = 'L'; | |
points.push(cpx, cpy); | |
break; | |
case 'V': | |
cpy = p.shift(); | |
cmd = 'L'; | |
points.push(cpx, cpy); | |
break; | |
case 'C': | |
points.push(p.shift(), p.shift(), p.shift(), p.shift()); | |
cpx = p.shift(); | |
cpy = p.shift(); | |
points.push(cpx, cpy); | |
break; | |
case 'c': | |
points.push( | |
cpx + p.shift(), | |
cpy + p.shift(), | |
cpx + p.shift(), | |
cpy + p.shift() | |
); | |
cpx += p.shift(); | |
cpy += p.shift(); | |
cmd = 'C'; | |
points.push(cpx, cpy); | |
break; | |
case 'S': | |
ctlPtx = cpx; | |
ctlPty = cpy; | |
prevCmd = ca[ca.length - 1]; | |
if (prevCmd.command === 'C') { | |
ctlPtx = cpx + (cpx - prevCmd.points[2]); | |
ctlPty = cpy + (cpy - prevCmd.points[3]); | |
} | |
points.push(ctlPtx, ctlPty, p.shift(), p.shift()); | |
cpx = p.shift(); | |
cpy = p.shift(); | |
cmd = 'C'; | |
points.push(cpx, cpy); | |
break; | |
case 's': | |
ctlPtx = cpx; | |
ctlPty = cpy; | |
prevCmd = ca[ca.length - 1]; | |
if (prevCmd.command === 'C') { | |
ctlPtx = cpx + (cpx - prevCmd.points[2]); | |
ctlPty = cpy + (cpy - prevCmd.points[3]); | |
} | |
points.push(ctlPtx, ctlPty, cpx + p.shift(), cpy + p.shift()); | |
cpx += p.shift(); | |
cpy += p.shift(); | |
cmd = 'C'; | |
points.push(cpx, cpy); | |
break; | |
case 'Q': | |
points.push(p.shift(), p.shift()); | |
cpx = p.shift(); | |
cpy = p.shift(); | |
points.push(cpx, cpy); | |
break; | |
case 'q': | |
points.push(cpx + p.shift(), cpy + p.shift()); | |
cpx += p.shift(); | |
cpy += p.shift(); | |
cmd = 'Q'; | |
points.push(cpx, cpy); | |
break; | |
case 'T': | |
ctlPtx = cpx; | |
ctlPty = cpy; | |
prevCmd = ca[ca.length - 1]; | |
if (prevCmd.command === 'Q') { | |
ctlPtx = cpx + (cpx - prevCmd.points[0]); | |
ctlPty = cpy + (cpy - prevCmd.points[1]); | |
} | |
cpx = p.shift(); | |
cpy = p.shift(); | |
cmd = 'Q'; | |
points.push(ctlPtx, ctlPty, cpx, cpy); | |
break; | |
case 't': | |
ctlPtx = cpx; | |
ctlPty = cpy; | |
prevCmd = ca[ca.length - 1]; | |
if (prevCmd.command === 'Q') { | |
ctlPtx = cpx + (cpx - prevCmd.points[0]); | |
ctlPty = cpy + (cpy - prevCmd.points[1]); | |
} | |
cpx += p.shift(); | |
cpy += p.shift(); | |
cmd = 'Q'; | |
points.push(ctlPtx, ctlPty, cpx, cpy); | |
break; | |
case 'A': | |
rx = p.shift(); | |
ry = p.shift(); | |
psi = p.shift(); | |
fa = p.shift(); | |
fs = p.shift(); | |
x1 = cpx; | |
y1 = cpy; | |
cpx = p.shift(); | |
cpy = p.shift(); | |
cmd = 'A'; | |
points = convertEndpointToCenterParameterization( | |
x1, | |
y1, | |
cpx, | |
cpy, | |
fa, | |
fs, | |
rx, | |
ry, | |
psi | |
); | |
break; | |
case 'a': | |
rx = p.shift(); | |
ry = p.shift(); | |
psi = p.shift(); | |
fa = p.shift(); | |
fs = p.shift(); | |
x1 = cpx; | |
y1 = cpy; | |
cpx += p.shift(); | |
cpy += p.shift(); | |
cmd = 'A'; | |
points = convertEndpointToCenterParameterization( | |
x1, | |
y1, | |
cpx, | |
cpy, | |
fa, | |
fs, | |
rx, | |
ry, | |
psi | |
); | |
break; | |
} | |
ca.push({ | |
command: cmd || c, | |
points: points, | |
start: { | |
x: startX, | |
y: startY | |
}, | |
pathLength: calcLength(startX, startY, cmd || c, points) | |
}); | |
} | |
if (c === 'z' || c === 'Z') { | |
ca.push({ | |
command: 'z', | |
points: [], | |
start: undefined, | |
pathLength: 0 | |
}); | |
} | |
} | |
return ca; | |
}, | |
draw: function (context , data) { | |
const ca = data; | |
context.beginPath(); | |
for (var n = 0; n < ca.length; n++) { | |
var c = ca[n].command; | |
var p = ca[n].points; | |
switch (c) { | |
case 'L': | |
context.lineTo(p[0], p[1]); | |
break; | |
case 'M': | |
context.moveTo(p[0], p[1]); | |
break; | |
case 'C': | |
context.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]); | |
break; | |
case 'Q': | |
context.quadraticCurveTo(p[0], p[1], p[2], p[3]); | |
break; | |
case 'A': | |
var cx = p[0], | |
cy = p[1], | |
rx = p[2], | |
ry = p[3], | |
theta = p[4], | |
dTheta = p[5], | |
psi = p[6], | |
fs = p[7]; | |
var r = rx > ry ? rx : ry; | |
var scaleX = rx > ry ? 1 : rx / ry; | |
var scaleY = rx > ry ? ry / rx : 1; | |
context.translate(cx, cy); | |
context.rotate(psi); | |
context.scale(scaleX, scaleY); | |
context.arc(0, 0, r, theta, theta + dTheta, 1 - fs); | |
context.scale(1 / scaleX, 1 / scaleY); | |
context.rotate(-psi); | |
context.translate(-cx, -cy); | |
break; | |
case 'z': | |
context.closePath(); | |
break; | |
} | |
} | |
} | |
}; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment