Created
June 4, 2011 17:09
-
-
Save devongovett/1008076 to your computer and use it in GitHub Desktop.
A CoffeeScript class that parses and renders SVG paths to the <canvas> element.
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
# fill path to ctx | |
Path.fill(ctx, "M250 150 L150 350 L350 350 Z") | |
# stroke | |
Path.stroke(ctx, "M250 150 L150 350 L350 350 Z") | |
# clip | |
Path.render(ctx, "M250 150 L150 350 L350 350 Z") | |
ctx.clip() |
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
class Path | |
@render: (ctx, path) -> | |
commands = parse(path) | |
render(commands, ctx) | |
@fill: (ctx, path) -> | |
@render ctx, path | |
ctx.fill() | |
@stroke: (ctx, path) -> | |
@render ctx, path | |
ctx.stroke() | |
parameters = | |
A: 7 | |
a: 7 | |
C: 6 | |
c: 6 | |
H: 1 | |
h: 1 | |
L: 2 | |
l: 2 | |
M: 2 | |
m: 2 | |
Q: 4 | |
q: 4 | |
S: 4 | |
s: 4 | |
T: 2 | |
t: 2 | |
V: 1 | |
v: 1 | |
Z: 0 | |
z: 0 | |
parse = (path) -> | |
ret = [] | |
args = [] | |
curArg = "" | |
foundDecimal = no | |
for c in path | |
params = parameters[c] | |
if params? | |
if cmd # save existing command | |
args[args.length] = +curArg if curArg.length > 0 | |
ret[ret.length] = {cmd,args} | |
args = [] | |
curArg = "" | |
foundDecimal = no | |
cmd = c | |
else if c in [" ", ","] or (c is "-" and curArg.length > 0) or (c is "." and foundDecimal) | |
continue if curArg.length is 0 | |
if args.length is params # handle reused commands | |
ret[ret.length] = | |
command: cmd | |
args: args | |
args = [+curArg] | |
# handle assumed commands | |
cmd = "L" if cmd is "M" | |
cmd = "l" if cmd is "m" | |
else | |
args[args.length] = +curArg | |
foundDecimal = (c is ".") | |
# fix for negative numbers or repeated decimals with no delimeter between commands | |
curArg = if c in ['-', '.'] then c else '' | |
else | |
curArg += c | |
foundDecimal = true if c is '.' | |
# add the last command | |
args[args.length] = +curArg if curArg.length > 0 | |
ret[ret.length] = {cmd,args} | |
return ret | |
render = (commands, ctx) -> | |
cx = cy = px = py = 0 | |
thisObj = {cx,cy,px,py,ctx} | |
# run the commands | |
for c, i in commands | |
runners[c.cmd]?.call(thisObj, c.args) | |
runners = | |
M: (a) -> | |
@cx = a[0] | |
@cy = a[1] | |
@px = @py = null | |
@ctx.moveTo(@cx, @cy) | |
m: (a) -> | |
@cx += a[0] | |
@cy += a[1] | |
@px = @py = null | |
@ctx.moveTo(@cx, @cy) | |
C: (a) -> | |
@cx = a[4] | |
@cy = a[5] | |
@px = a[2] | |
@py = a[3] | |
@ctx.bezierCurveTo a... | |
c: (a) -> | |
@ctx.bezierCurveTo(a[0] + @cx, a[1] + @cy, a[2] + @cx, a[3] + @cy, a[4] + @cx, a[5] + @cy) | |
@px = @cx + a[2] | |
@py = @cy + a[3] | |
@cx += a[4] | |
@cy += a[5] | |
S: (a) -> | |
if @px is null | |
@px = @cx | |
@py = @cy | |
@ctx.bezierCurveTo(@cx-(@px-@cx), @cy-(@py-@cy), a[0], a[1], a[2], a[3]) | |
@px = a[0] | |
@py = a[1] | |
@cx = a[2] | |
@cy = a[3] | |
s: (a) -> | |
@ctx.bezierCurveTo(@cx-(@px-@cx), @cy-(@py-@cy), @cx + a[0], @cy + a[1], @cx + a[2], @cy + a[3]) | |
@px = @cx + a[0] | |
@py = @cy + a[1] | |
@cx += a[2] | |
@cy += a[3] | |
Q: (a) -> | |
@px = a[0] | |
@py = a[1] | |
@cx = a[2] | |
@cy = a[3] | |
@ctx.quadraticCurveTo(a[0], a[1], @cx, @cy) | |
q: (a) -> | |
@ctx.quadraticCurveTo(a[0] + @cx, a[1] + @cy, a[2] + @cx, a[3] + @cy) | |
@px = @cx + a[0] | |
@py = @cy + a[1] | |
@cx += a[2] | |
@cy += a[3] | |
T: (a) -> | |
if @px is null | |
@px = @cx | |
@py = @cy | |
else | |
@px = @cx-(@px-@cx) | |
@py = @cy-(@py-@cy) | |
@ctx.quadraticCurveTo(@px, @py, a[0], a[1]) | |
@px = @cx-(@px-@cx) | |
@py = @cy-(@py-@cy) | |
@cx = a[0] | |
@cy = a[1] | |
t: (a) -> | |
if @px is null | |
@px = @cx | |
@py = @cy | |
else | |
@px = @cx-(@px-@cx) | |
@py = @cy-(@py-@cy) | |
@ctx.quadraticCurveTo(@px, @py, @cx + a[0], @cy + a[1]) | |
@cx += a[0] | |
@cy += a[1] | |
A: (a) -> | |
solveArc(@cx, @cy, a) | |
@cx = a[5] | |
@cy = a[6] | |
a: (a) -> | |
a[5] += @cx | |
a[6] += @cy | |
solveArc(@cx, @cy, a) | |
@cx = a[5] | |
@cy = a[6] | |
L: (a) -> | |
@cx = a[0] | |
@cy = a[1] | |
@px = @py = null | |
@ctx.lineTo(@cx, @cy) | |
l: (a) -> | |
@cx += a[0] | |
@cy += a[1] | |
@px = @py = null | |
@ctx.lineTo(@cx, @cy) | |
H: (a) -> | |
@cx = a[0] | |
@px = @py = null | |
@ctx.lineTo(@cx, @cy) | |
h: (a) -> | |
@cx += a[0] | |
@px = @py = null | |
@ctx.lineTo(@cx, @cy) | |
V: (a) -> | |
@cy = a[0] | |
@px = @py = null | |
@ctx.lineTo(@cx, @cy) | |
v: (a) -> | |
@cy += a[0] | |
@px = @py = null | |
@ctx.lineTo(@cx, @cy) | |
Z: -> | |
@ctx.closePath() | |
z: -> | |
@ctx.closePath() | |
solveArc = (x, y, coords) -> | |
[rx,ry,rot,large,sweep,ex,ey] = coords | |
segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y) | |
for seg in segs | |
bez = segmentToBezier segs... | |
ctx.bezierCurveTo bez... | |
# from Inkscape svgtopdf, thanks! | |
arcToSegments = (x, y, rx, ry, large, sweep, rotateX, ox, oy) -> | |
th = rotateX * (Math.PI/180) | |
sin_th = Math.sin(th) | |
cos_th = Math.cos(th) | |
rx = Math.abs(rx) | |
ry = Math.abs(ry) | |
px = cos_th * (ox - x) * 0.5 + sin_th * (oy - y) * 0.5 | |
py = cos_th * (oy - y) * 0.5 - sin_th * (ox - x) * 0.5 | |
pl = (px*px) / (rx*rx) + (py*py) / (ry*ry) | |
if pl > 1 | |
pl = Math.sqrt(pl) | |
rx *= pl | |
ry *= pl | |
a00 = cos_th / rx | |
a01 = sin_th / rx | |
a10 = (-sin_th) / ry | |
a11 = (cos_th) / ry | |
x0 = a00 * ox + a01 * oy | |
y0 = a10 * ox + a11 * oy | |
x1 = a00 * x + a01 * y | |
y1 = a10 * x + a11 * y | |
d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0) | |
sfactor_sq = 1 / d - 0.25 | |
sfactor_sq = 0 if sfactor_sq < 0 | |
sfactor = Math.sqrt(sfactor_sq) | |
sfactor = -sfactor if sweep is large | |
xc = 0.5 * (x0 + x1) - sfactor * (y1-y0) | |
yc = 0.5 * (y0 + y1) + sfactor * (x1-x0) | |
th0 = Math.atan2(y0-yc, x0-xc) | |
th1 = Math.atan2(y1-yc, x1-xc) | |
th_arc = th1-th0 | |
if th_arc < 0 && sweep is 1 | |
th_arc += 2*Math.PI | |
else if th_arc > 0 && sweep is 0 | |
th_arc -= 2 * Math.PI | |
segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))) | |
result = [] | |
for i in [0...segments] | |
th2 = th0 + i * th_arc / segments | |
th3 = th0 + (i+1) * th_arc / segments | |
result[i] = [xc, yc, th2, th3, rx, ry, sin_th, cos_th] | |
return result | |
segmentToBezier = (cx, cy, th0, th1, rx, ry, sin_th, cos_th) -> | |
a00 = cos_th * rx | |
a01 = -sin_th * ry | |
a10 = sin_th * rx | |
a11 = cos_th * ry | |
th_half = 0.5 * (th1 - th0) | |
t = (8 / 3) * Math.sin(th_half * 0.5) * Math.sin(th_half * 0.5) / Math.sin(th_half) | |
x1 = cx + Math.cos(th0) - t * Math.sin(th0) | |
y1 = cy + Math.sin(th0) + t * Math.cos(th0) | |
x3 = cx + Math.cos(th1) | |
y3 = cy + Math.sin(th1) | |
x2 = x3 + t * Math.sin(th1) | |
y2 = y3 - t * Math.cos(th1) | |
return [ | |
a00 * x1 + a01 * y1, a10 * x1 + a11 * y1, | |
a00 * x2 + a01 * y2, a10 * x2 + a11 * y2, | |
a00 * x3 + a01 * y3, a10 * x3 + a11 * y3 | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great gist! There is a small error in the 'T' command. Fixed here: https://gist.github.com/3897361