class SVGPath @apply: (doc, path) -> commands = parse path apply commands, doc 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 params = 0 for c in path if parameters[c]? params = parameters[c] 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 and curArg[curArg.length - 1] isnt 'e') or (c is "." and foundDecimal) continue if curArg.length is 0 if args.length is params # handle reused commands ret[ret.length] = {cmd,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 if curArg.length > 0 if args.length is params # handle reused commands ret[ret.length] = {cmd, args} args = [+curArg] # handle assumed commands cmd = "L" if cmd is "M" cmd = "l" if cmd is "m" else args[args.length] = +curArg ret[ret.length] = {cmd,args} return ret cx = cy = px = py = sx = sy = 0 apply = (commands, doc) -> # current point, control point, and subpath starting point cx = cy = px = py = sx = sy = 0 # run the commands for c, i in commands runners[c.cmd]?(doc, c.args) cx = cy = px = py = 0 runners = M: (doc, a) -> cx = a[0] cy = a[1] px = py = null sx = cx sy = cy doc.moveTo cx, cy m: (doc, a) -> cx += a[0] cy += a[1] px = py = null sx = cx sy = cy doc.moveTo cx, cy C: (doc, a) -> cx = a[4] cy = a[5] px = a[2] py = a[3] doc.bezierCurveTo a... c: (doc, a) -> doc.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: (doc, a) -> if px is null px = cx py = cy doc.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: (doc, a) -> if px is null px = cx py = cy doc.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: (doc, a) -> px = a[0] py = a[1] cx = a[2] cy = a[3] doc.quadraticCurveTo(a[0], a[1], cx, cy) q: (doc, a) -> doc.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: (doc, a) -> if px is null px = cx py = cy else px = cx-(px-cx) py = cy-(py-cy) doc.quadraticCurveTo(px, py, a[0], a[1]) px = cx-(px-cx) py = cy-(py-cy) cx = a[0] cy = a[1] t: (doc, a) -> if px is null px = cx py = cy else px = cx-(px-cx) py = cy-(py-cy) doc.quadraticCurveTo(px, py, cx + a[0], cy + a[1]) cx += a[0] cy += a[1] A: (doc, a) -> solveArc(doc, cx, cy, a) cx = a[5] cy = a[6] a: (doc, a) -> a[5] += cx a[6] += cy solveArc(doc, cx, cy, a) cx = a[5] cy = a[6] L: (doc, a) -> cx = a[0] cy = a[1] px = py = null doc.lineTo(cx, cy) l: (doc, a) -> cx += a[0] cy += a[1] px = py = null doc.lineTo(cx, cy) H: (doc, a) -> cx = a[0] px = py = null doc.lineTo(cx, cy) h: (doc, a) -> cx += a[0] px = py = null doc.lineTo(cx, cy) V: (doc, a) -> cy = a[0] px = py = null doc.lineTo(cx, cy) v: (doc, a) -> cy += a[0] px = py = null doc.lineTo(cx, cy) Z: (doc) -> doc.closePath() cx = sx cy = sy z: (doc) -> doc.closePath() cx = sx cy = sy solveArc = (doc, 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 seg... doc.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 ] module.exports = SVGPath