/*!! * Canvas 2 Svg v1.0.19 * A low level canvas to SVG converter. Uses a mock canvas context to build an SVG document. * * Licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * * Author: * Kerry Liu * * Copyright (c) 2014 Gliffy Inc. */ ;(function () { "use strict"; var STYLES, ctx, CanvasGradient, CanvasPattern, namedEntities; //helper function to format a string function format(str, args) { var keys = Object.keys(args), i; for (i=0; i<keys.length; i++) { str = str.replace(new RegExp("\\{" + keys[i] + "\\}", "gi"), args[keys[i]]); } return str; } //helper function that generates a random string function randomString(holder) { var chars, randomstring, i; if (!holder) { throw new Error("cannot create a random attribute name for an undefined object"); } chars = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; randomstring = ""; do { randomstring = ""; for (i = 0; i < 12; i++) { randomstring += chars[Math.floor(Math.random() * chars.length)]; } } while (holder[randomstring]); return randomstring; } //helper function to map named to numbered entities function createNamedToNumberedLookup(items, radix) { var i, entity, lookup = {}, base10, base16; items = items.split(','); radix = radix || 10; // Map from named to numbered entities. for (i = 0; i < items.length; i += 2) { entity = '&' + items[i + 1] + ';'; base10 = parseInt(items[i], radix); lookup[entity] = '&#'+base10+';'; } //FF and IE need to create a regex from hex values ie == \xa0 lookup["\\xa0"] = ' '; return lookup; } //helper function to map canvas-textAlign to svg-textAnchor function getTextAnchor(textAlign) { //TODO: support rtl languages var mapping = {"left":"start", "right":"end", "center":"middle", "start":"start", "end":"end"}; return mapping[textAlign] || mapping.start; } //helper function to map canvas-textBaseline to svg-dominantBaseline function getDominantBaseline(textBaseline) { //INFO: not supported in all browsers var mapping = {"alphabetic": "alphabetic", "hanging": "hanging", "top":"text-before-edge", "bottom":"text-after-edge", "middle":"central"}; return mapping[textBaseline] || mapping.alphabetic; } // Unpack entities lookup where the numbers are in radix 32 to reduce the size // entity mapping courtesy of tinymce namedEntities = createNamedToNumberedLookup( '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); //Some basic mappings for attributes and default values. STYLES = { "strokeStyle":{ svgAttr : "stroke", //corresponding svg attribute canvas : "#000000", //canvas default svg : "none", //svg default apply : "stroke" //apply on stroke() or fill() }, "fillStyle":{ svgAttr : "fill", canvas : "#000000", svg : null, //svg default is black, but we need to special case this to handle canvas stroke without fill apply : "fill" }, "lineCap":{ svgAttr : "stroke-linecap", canvas : "butt", svg : "butt", apply : "stroke" }, "lineJoin":{ svgAttr : "stroke-linejoin", canvas : "miter", svg : "miter", apply : "stroke" }, "miterLimit":{ svgAttr : "stroke-miterlimit", canvas : 10, svg : 4, apply : "stroke" }, "lineWidth":{ svgAttr : "stroke-width", canvas : 1, svg : 1, apply : "stroke" }, "globalAlpha": { svgAttr : "opacity", canvas : 1, svg : 1, apply : "fill stroke" }, "font":{ //font converts to multiple svg attributes, there is custom logic for this canvas : "10px sans-serif" }, "shadowColor":{ canvas : "#000000" }, "shadowOffsetX":{ canvas : 0 }, "shadowOffsetY":{ canvas : 0 }, "shadowBlur":{ canvas : 0 }, "textAlign":{ canvas : "start" }, "textBaseline":{ canvas : "alphabetic" }, "lineDash" : { svgAttr : "stroke-dasharray", canvas : [], svg : null, apply : "stroke" } }; /** * * @param gradientNode - reference to the gradient * @constructor */ CanvasGradient = function (gradientNode, ctx) { this.__root = gradientNode; this.__ctx = ctx; }; /** * Adds a color stop to the gradient root */ CanvasGradient.prototype.addColorStop = function (offset, color) { var stop = this.__ctx.__createElement("stop"), regex, matches; stop.setAttribute("offset", offset); if (color.indexOf("rgba") !== -1) { //separate alpha value, since webkit can't handle it regex = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi; matches = regex.exec(color); stop.setAttribute("stop-color", format("rgb({r},{g},{b})", {r:matches[1], g:matches[2], b:matches[3]})); stop.setAttribute("stop-opacity", matches[4]); } else { stop.setAttribute("stop-color", color); } this.__root.appendChild(stop); }; CanvasPattern = function (pattern, ctx) { this.__root = pattern; this.__ctx = ctx; }; /** * The mock canvas context * @param o - options include: * width - width of your canvas (defaults to 500) * height - height of your canvas (defaults to 500) * enableMirroring - enables canvas mirroring (get image data) (defaults to false) * document - the document object (defaults to the current document) */ ctx = function (o) { var defaultOptions = { width:500, height:500, enableMirroring : false}, options; //keep support for this way of calling C2S: new C2S(width,height) if (arguments.length > 1) { options = defaultOptions; options.width = arguments[0]; options.height = arguments[1]; } else if ( !o ) { options = defaultOptions; } else { options = o; } if (!(this instanceof ctx)) { //did someone call this without new? return new ctx(options); } //setup options this.width = options.width || defaultOptions.width; this.height = options.height || defaultOptions.height; this.enableMirroring = options.enableMirroring !== undefined ? options.enableMirroring : defaultOptions.enableMirroring; this.canvas = this; ///point back to this instance! this.__document = options.document || document; this.__canvas = this.__document.createElement("canvas"); this.__ctx = this.__canvas.getContext("2d"); this.__setDefaultStyles(); this.__stack = [this.__getStyleState()]; this.__groupStack = []; //the root svg element this.__root = this.__document.createElementNS("http://www.w3.org/2000/svg", "svg"); this.__root.setAttribute("version", 1.1); this.__root.setAttribute("xmlns", "http://www.w3.org/2000/svg"); this.__root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); this.__root.setAttribute("width", this.width); this.__root.setAttribute("height", this.height); //make sure we don't generate the same ids in defs this.__ids = {}; //defs tag this.__defs = this.__document.createElementNS("http://www.w3.org/2000/svg", "defs"); this.__root.appendChild(this.__defs); //also add a group child. the svg element can't use the transform attribute this.__currentElement = this.__document.createElementNS("http://www.w3.org/2000/svg", "g"); this.__root.appendChild(this.__currentElement); }; /** * Creates the specified svg element * @private */ ctx.prototype.__createElement = function (elementName, properties, resetFill) { if (typeof properties === "undefined") { properties = {}; } var element = this.__document.createElementNS("http://www.w3.org/2000/svg", elementName), keys = Object.keys(properties), i, key; if (resetFill) { //if fill or stroke is not specified, the svg element should not display. By default SVG's fill is black. element.setAttribute("fill", "none"); element.setAttribute("stroke", "none"); } for (i=0; i<keys.length; i++) { key = keys[i]; element.setAttribute(key, properties[key]); } return element; }; /** * Applies default canvas styles to the context * @private */ ctx.prototype.__setDefaultStyles = function () { //default 2d canvas context properties see:http://www.w3.org/TR/2dcontext/ var keys = Object.keys(STYLES), i, key; for (i=0; i<keys.length; i++) { key = keys[i]; this[key] = STYLES[key].canvas; } }; /** * Applies styles on restore * @param styleState * @private */ ctx.prototype.__applyStyleState = function (styleState) { var keys = Object.keys(styleState), i, key; for (i=0; i<keys.length; i++) { key = keys[i]; this[key] = styleState[key]; } }; /** * Gets the current style state * @return {Object} * @private */ ctx.prototype.__getStyleState = function () { var i, styleState = {}, keys = Object.keys(STYLES), key; for (i=0; i<keys.length; i++) { key = keys[i]; styleState[key] = this[key]; } return styleState; }; /** * Apples the current styles to the current SVG element. On "ctx.fill" or "ctx.stroke" * @param type * @private */ ctx.prototype.__applyStyleToCurrentElement = function (type) { var currentElement = this.__currentElement; var currentStyleGroup = this.__currentElementsToStyle; if (currentStyleGroup) { currentElement.setAttribute(type, ""); currentElement = currentStyleGroup.element; currentStyleGroup.children.forEach(function (node) { node.setAttribute(type, ""); }) } var keys = Object.keys(STYLES), i, style, value, id, regex, matches; for (i = 0; i < keys.length; i++) { style = STYLES[keys[i]]; value = this[keys[i]]; if (style.apply) { //is this a gradient or pattern? if (style.apply.indexOf("fill") !== -1 && value instanceof CanvasPattern) { //pattern if (value.__ctx) { //copy over defs while(value.__ctx.__defs.childNodes.length) { id = value.__ctx.__defs.childNodes[0].getAttribute("id"); this.__ids[id] = id; this.__defs.appendChild(value.__ctx.__defs.childNodes[0]); } } currentElement.setAttribute("fill", format("url(#{id})", {id:value.__root.getAttribute("id")})); } else if (style.apply.indexOf("fill")!==-1 && value instanceof CanvasGradient) { //gradient currentElement.setAttribute("fill", format("url(#{id})", {id:value.__root.getAttribute("id")})); } else if (style.apply.indexOf(type)!==-1 && style.svg !== value) { if ((style.svgAttr === "stroke" || style.svgAttr === "fill") && value.indexOf("rgba") !== -1) { //separate alpha value, since illustrator can't handle it regex = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi; matches = regex.exec(value); currentElement.setAttribute(style.svgAttr, format("rgb({r},{g},{b})", {r:matches[1], g:matches[2], b:matches[3]})); //should take globalAlpha here var opacity = matches[4]; var globalAlpha = this.globalAlpha; if (globalAlpha != null) { opacity *= globalAlpha; } currentElement.setAttribute(style.svgAttr+"-opacity", opacity); } else { var attr = style.svgAttr; if (keys[i] === 'globalAlpha') { attr = type+'-'+style.svgAttr; if (currentElement.getAttribute(attr)) { //fill-opacity or stroke-opacity has already been set by stroke or fill. continue; } } //otherwise only update attribute if right type, and not svg default currentElement.setAttribute(attr, value); } } } } }; /** * Will return the closest group or svg node. May return the current element. * @private */ ctx.prototype.__closestGroupOrSvg = function (node) { node = node || this.__currentElement; if (node.nodeName === "g" || node.nodeName === "svg") { return node; } else { return this.__closestGroupOrSvg(node.parentNode); } }; /** * Returns the serialized value of the svg so far * @param fixNamedEntities - Standalone SVG doesn't support named entities, which document.createTextNode encodes. * If true, we attempt to find all named entities and encode it as a numeric entity. * @return serialized svg */ ctx.prototype.getSerializedSvg = function (fixNamedEntities) { var serialized = new XMLSerializer().serializeToString(this.__root), keys, i, key, value, regexp, xmlns; //IE search for a duplicate xmnls because they didn't implement setAttributeNS correctly xmlns = /xmlns="http:\/\/www\.w3\.org\/2000\/svg".+xmlns="http:\/\/www\.w3\.org\/2000\/svg/gi; if (xmlns.test(serialized)) { serialized = serialized.replace('xmlns="http://www.w3.org/2000/svg','xmlns:xlink="http://www.w3.org/1999/xlink'); } if (fixNamedEntities) { keys = Object.keys(namedEntities); //loop over each named entity and replace with the proper equivalent. for (i=0; i<keys.length; i++) { key = keys[i]; value = namedEntities[key]; regexp = new RegExp(key, "gi"); if (regexp.test(serialized)) { serialized = serialized.replace(regexp, value); } } } return serialized; }; /** * Returns the root svg * @return */ ctx.prototype.getSvg = function () { return this.__root; }; /** * Will generate a group tag. */ ctx.prototype.save = function () { var group = this.__createElement("g"); var parent = this.__closestGroupOrSvg(); this.__groupStack.push(parent); parent.appendChild(group); this.__currentElement = group; this.__stack.push(this.__getStyleState()); }; /** * Sets current element to parent, or just root if already root */ ctx.prototype.restore = function () { this.__currentElement = this.__groupStack.pop(); this.__currentElementsToStyle = null; //Clearing canvas will make the poped group invalid, currentElement is set to the root group node. if (!this.__currentElement) { this.__currentElement = this.__root.childNodes[1]; } var state = this.__stack.pop(); this.__applyStyleState(state); }; /** * Helper method to add transform * @private */ ctx.prototype.__addTransform = function (t) { //if the current element has siblings, add another group var parent = this.__closestGroupOrSvg(); if (parent.childNodes.length > 0) { if (this.__currentElement.nodeName === "path") { this.__currentElementsToStyle || (this.__currentElementsToStyle = { element: parent, children: [] }); this.__currentElementsToStyle.children.push(this.__currentElement) this.__applyCurrentDefaultPath(); } var group = this.__createElement("g"); parent.appendChild(group); this.__currentElement = group; } var transform = this.__currentElement.getAttribute("transform"); if (transform) { transform += " "; } else { transform = ""; } transform += t; this.__currentElement.setAttribute("transform", transform); }; /** * scales the current element */ ctx.prototype.scale = function (x, y) { if (y === undefined) { y = x; } this.__addTransform(format("scale({x},{y})", {x:x, y:y})); }; /** * rotates the current element */ ctx.prototype.rotate = function (angle) { var degrees = (angle * 180 / Math.PI); this.__addTransform(format("rotate({angle},{cx},{cy})", {angle:degrees, cx:0, cy:0})); }; /** * translates the current element */ ctx.prototype.translate = function (x, y) { this.__addTransform(format("translate({x},{y})", {x:x,y:y})); }; /** * applies a transform to the current element */ ctx.prototype.transform = function (a, b, c, d, e, f) { this.__addTransform(format("matrix({a},{b},{c},{d},{e},{f})", {a:a, b:b, c:c, d:d, e:e, f:f})); }; /** * Create a new Path Element */ ctx.prototype.beginPath = function () { var path, parent; // Note that there is only one current default path, it is not part of the drawing state. // See also: https://html.spec.whatwg.org/multipage/scripting.html#current-default-path this.__currentDefaultPath = ""; this.__currentPosition = {}; path = this.__createElement("path", {}, true); parent = this.__closestGroupOrSvg(); parent.appendChild(path); this.__currentElement = path; }; /** * Helper function to apply currentDefaultPath to current path element * @private */ ctx.prototype.__applyCurrentDefaultPath = function () { var currentElement = this.__currentElement; if (currentElement.nodeName === "path") { currentElement.setAttribute("d", this.__currentDefaultPath); } else { console.error("Attempted to apply path command to node", currentElement.nodeName); } }; /** * Helper function to add path command * @private */ ctx.prototype.__addPathCommand = function (command) { this.__currentDefaultPath += " "; this.__currentDefaultPath += command; }; /** * Adds the move command to the current path element, * if the currentPathElement is not empty create a new path element */ ctx.prototype.moveTo = function (x,y) { if (this.__currentElement.nodeName !== "path") { this.beginPath(); } // creates a new subpath with the given point this.__currentPosition = {x: x, y: y}; this.__addPathCommand(format("M {x} {y}", {x:x, y:y})); }; /** * Closes the current path */ ctx.prototype.closePath = function () { if (this.__currentDefaultPath) { this.__addPathCommand("Z"); } }; /** * Adds a line to command */ ctx.prototype.lineTo = function (x, y) { this.__currentPosition = {x: x, y: y}; if (this.__currentDefaultPath.indexOf('M') > -1) { this.__addPathCommand(format("L {x} {y}", {x:x, y:y})); } else { this.__addPathCommand(format("M {x} {y}", {x:x, y:y})); } }; /** * Add a bezier command */ ctx.prototype.bezierCurveTo = function (cp1x, cp1y, cp2x, cp2y, x, y) { this.__currentPosition = {x: x, y: y}; this.__addPathCommand(format("C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}", {cp1x:cp1x, cp1y:cp1y, cp2x:cp2x, cp2y:cp2y, x:x, y:y})); }; /** * Adds a quadratic curve to command */ ctx.prototype.quadraticCurveTo = function (cpx, cpy, x, y) { this.__currentPosition = {x: x, y: y}; this.__addPathCommand(format("Q {cpx} {cpy} {x} {y}", {cpx:cpx, cpy:cpy, x:x, y:y})); }; /** * Return a new normalized vector of given vector */ var normalize = function (vector) { var len = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]); return [vector[0] / len, vector[1] / len]; }; /** * Adds the arcTo to the current path * * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto */ ctx.prototype.arcTo = function (x1, y1, x2, y2, radius) { // Let the point (x0, y0) be the last point in the subpath. var x0 = this.__currentPosition && this.__currentPosition.x; var y0 = this.__currentPosition && this.__currentPosition.y; // First ensure there is a subpath for (x1, y1). if (typeof x0 == "undefined" || typeof y0 == "undefined") { return; } // Negative values for radius must cause the implementation to throw an IndexSizeError exception. if (radius < 0) { throw new Error("IndexSizeError: The radius provided (" + radius + ") is negative."); } // If the point (x0, y0) is equal to the point (x1, y1), // or if the point (x1, y1) is equal to the point (x2, y2), // or if the radius radius is zero, // then the method must add the point (x1, y1) to the subpath, // and connect that point to the previous point (x0, y0) by a straight line. if (((x0 === x1) && (y0 === y1)) || ((x1 === x2) && (y1 === y2)) || (radius === 0)) { this.lineTo(x1, y1); return; } // Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line, // then the method must add the point (x1, y1) to the subpath, // and connect that point to the previous point (x0, y0) by a straight line. var unit_vec_p1_p0 = normalize([x0 - x1, y0 - y1]); var unit_vec_p1_p2 = normalize([x2 - x1, y2 - y1]); if (unit_vec_p1_p0[0] * unit_vec_p1_p2[1] === unit_vec_p1_p0[1] * unit_vec_p1_p2[0]) { this.lineTo(x1, y1); return; } // Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius, // and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1), // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1), and crosses the point (x2, y2). // The points at which this circle touches these two lines are called the start and end tangent points respectively. // note that both vectors are unit vectors, so the length is 1 var cos = (unit_vec_p1_p0[0] * unit_vec_p1_p2[0] + unit_vec_p1_p0[1] * unit_vec_p1_p2[1]); var theta = Math.acos(Math.abs(cos)); // Calculate origin var unit_vec_p1_origin = normalize([ unit_vec_p1_p0[0] + unit_vec_p1_p2[0], unit_vec_p1_p0[1] + unit_vec_p1_p2[1] ]); var len_p1_origin = radius / Math.sin(theta / 2); var x = x1 + len_p1_origin * unit_vec_p1_origin[0]; var y = y1 + len_p1_origin * unit_vec_p1_origin[1]; // Calculate start angle and end angle // rotate 90deg clockwise (note that y axis points to its down) var unit_vec_origin_start_tangent = [ -unit_vec_p1_p0[1], unit_vec_p1_p0[0] ]; // rotate 90deg counter clockwise (note that y axis points to its down) var unit_vec_origin_end_tangent = [ unit_vec_p1_p2[1], -unit_vec_p1_p2[0] ]; var getAngle = function (vector) { // get angle (clockwise) between vector and (1, 0) var x = vector[0]; var y = vector[1]; if (y >= 0) { // note that y axis points to its down return Math.acos(x); } else { return -Math.acos(x); } }; var startAngle = getAngle(unit_vec_origin_start_tangent); var endAngle = getAngle(unit_vec_origin_end_tangent); // Connect the point (x0, y0) to the start tangent point by a straight line this.lineTo(x + unit_vec_origin_start_tangent[0] * radius, y + unit_vec_origin_start_tangent[1] * radius); // Connect the start tangent point to the end tangent point by arc // and adding the end tangent point to the subpath. this.arc(x, y, radius, startAngle, endAngle); }; /** * Sets the stroke property on the current element */ ctx.prototype.stroke = function () { if (this.__currentElement.nodeName === "path") { this.__currentElement.setAttribute("paint-order", "fill stroke markers"); } this.__applyCurrentDefaultPath(); this.__applyStyleToCurrentElement("stroke"); }; /** * Sets fill properties on the current element */ ctx.prototype.fill = function () { if (this.__currentElement.nodeName === "path") { this.__currentElement.setAttribute("paint-order", "stroke fill markers"); } this.__applyCurrentDefaultPath(); this.__applyStyleToCurrentElement("fill"); }; /** * Adds a rectangle to the path. */ ctx.prototype.rect = function (x, y, width, height) { if (this.__currentElement.nodeName !== "path") { this.beginPath(); } this.moveTo(x, y); this.lineTo(x+width, y); this.lineTo(x+width, y+height); this.lineTo(x, y+height); this.lineTo(x, y); this.closePath(); }; /** * adds a rectangle element */ ctx.prototype.fillRect = function (x, y, width, height) { var rect, parent; rect = this.__createElement("rect", { x : x, y : y, width : width, height : height }, true); parent = this.__closestGroupOrSvg(); parent.appendChild(rect); this.__currentElement = rect; this.__applyStyleToCurrentElement("fill"); }; /** * Draws a rectangle with no fill * @param x * @param y * @param width * @param height */ ctx.prototype.strokeRect = function (x, y, width, height) { var rect, parent; rect = this.__createElement("rect", { x : x, y : y, width : width, height : height }, true); parent = this.__closestGroupOrSvg(); parent.appendChild(rect); this.__currentElement = rect; this.__applyStyleToCurrentElement("stroke"); }; /** * Clear entire canvas: * 1. save current transforms * 2. remove all the childNodes of the root g element */ ctx.prototype.__clearCanvas = function () { var current = this.__closestGroupOrSvg(), transform = current.getAttribute("transform"); var rootGroup = this.__root.childNodes[1]; var childNodes = rootGroup.childNodes; for (var i = childNodes.length - 1; i >= 0; i--) { if (childNodes[i]) { rootGroup.removeChild(childNodes[i]); } } this.__currentElement = rootGroup; //reset __groupStack as all the child group nodes are all removed. this.__groupStack = []; if (transform) { this.__addTransform(transform); } }; /** * "Clears" a canvas by just drawing a white rectangle in the current group. */ ctx.prototype.clearRect = function (x, y, width, height) { //clear entire canvas if (x === 0 && y === 0 && width === this.width && height === this.height) { this.__clearCanvas(); return; } var rect, parent = this.__closestGroupOrSvg(); rect = this.__createElement("rect", { x : x, y : y, width : width, height : height, fill : "#FFFFFF" }, true); parent.appendChild(rect); }; /** * Adds a linear gradient to a defs tag. * Returns a canvas gradient object that has a reference to it's parent def */ ctx.prototype.createLinearGradient = function (x1, y1, x2, y2) { var grad = this.__createElement("linearGradient", { id : randomString(this.__ids), x1 : x1+"px", x2 : x2+"px", y1 : y1+"px", y2 : y2+"px", "gradientUnits" : "userSpaceOnUse" }, false); this.__defs.appendChild(grad); return new CanvasGradient(grad, this); }; /** * Adds a radial gradient to a defs tag. * Returns a canvas gradient object that has a reference to it's parent def */ ctx.prototype.createRadialGradient = function (x0, y0, r0, x1, y1, r1) { var grad = this.__createElement("radialGradient", { id : randomString(this.__ids), cx : x1+"px", cy : y1+"px", r : r1+"px", fx : x0+"px", fy : y0+"px", "gradientUnits" : "userSpaceOnUse" }, false); this.__defs.appendChild(grad); return new CanvasGradient(grad, this); }; /** * Parses the font string and returns svg mapping * @private */ ctx.prototype.__parseFont = function () { var regex = /^\s*(?=(?:(?:[-a-z]+\s*) {0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*) {0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*) {0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*) {0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-,\'\"\sa-z]+?)\s*$/i; var fontPart = regex.exec( this.font ); var data = { style : fontPart[1] || 'normal', size : fontPart[4] || '10px', family : fontPart[6] || 'sans-serif', weight: fontPart[3] || 'normal', decoration : fontPart[2] || 'normal', href : null }; //canvas doesn't support underline natively, but we can pass this attribute if (this.__fontUnderline === "underline") { data.decoration = "underline"; } //canvas also doesn't support linking, but we can pass this as well if (this.__fontHref) { data.href = this.__fontHref; } return data; }; /** * Helper to link text fragments * @param font * @param element * @return {*} * @private */ ctx.prototype.__wrapTextLink = function (font, element) { if (font.href) { var a = this.__createElement("a"); a.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", font.href); a.appendChild(element); return a; } return element; }; /** * Fills or strokes text * @param text * @param x * @param y * @param action - stroke or fill * @private */ ctx.prototype.__applyText = function (text, x, y, action) { var font = this.__parseFont(), parent = this.__closestGroupOrSvg(), textElement = this.__createElement("text", { "font-family" : font.family, "font-size" : font.size, "font-style" : font.style, "font-weight" : font.weight, "text-decoration" : font.decoration, "x" : x, "y" : y, "text-anchor": getTextAnchor(this.textAlign), "dominant-baseline": getDominantBaseline(this.textBaseline) }, true); textElement.appendChild(this.__document.createTextNode(text)); this.__currentElement = textElement; this.__applyStyleToCurrentElement(action); parent.appendChild(this.__wrapTextLink(font,textElement)); }; /** * Creates a text element * @param text * @param x * @param y */ ctx.prototype.fillText = function (text, x, y) { this.__applyText(text, x, y, "fill"); }; /** * Strokes text * @param text * @param x * @param y */ ctx.prototype.strokeText = function (text, x, y) { this.__applyText(text, x, y, "stroke"); }; /** * No need to implement this for svg. * @param text * @return {TextMetrics} */ ctx.prototype.measureText = function (text) { this.__ctx.font = this.font; return this.__ctx.measureText(text); }; /** * Arc command! */ ctx.prototype.arc = function (x, y, radius, startAngle, endAngle, counterClockwise) { // in canvas no circle is drawn if no angle is provided. if (startAngle === endAngle) { return; } startAngle = startAngle % (2*Math.PI); endAngle = endAngle % (2*Math.PI); if (startAngle === endAngle) { //circle time! subtract some of the angle so svg is happy (svg elliptical arc can't draw a full circle) endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI); } var endX = x+radius*Math.cos(endAngle), endY = y+radius*Math.sin(endAngle), startX = x+radius*Math.cos(startAngle), startY = y+radius*Math.sin(startAngle), sweepFlag = counterClockwise ? 0 : 1, largeArcFlag = 0, diff = endAngle - startAngle; // https://github.com/gliffy/canvas2svg/issues/4 if (diff < 0) { diff += 2*Math.PI; } if (counterClockwise) { largeArcFlag = diff > Math.PI ? 0 : 1; } else { largeArcFlag = diff > Math.PI ? 1 : 0; } this.lineTo(startX, startY); this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", {rx:radius, ry:radius, xAxisRotation:0, largeArcFlag:largeArcFlag, sweepFlag:sweepFlag, endX:endX, endY:endY})); this.__currentPosition = {x: endX, y: endY}; }; /** * Generates a ClipPath from the clip command. */ ctx.prototype.clip = function () { var group = this.__closestGroupOrSvg(), clipPath = this.__createElement("clipPath"), id = randomString(this.__ids), newGroup = this.__createElement("g"); this.__applyCurrentDefaultPath(); group.removeChild(this.__currentElement); clipPath.setAttribute("id", id); clipPath.appendChild(this.__currentElement); this.__defs.appendChild(clipPath); //set the clip path to this group group.setAttribute("clip-path", format("url(#{id})", {id:id})); //clip paths can be scaled and transformed, we need to add another wrapper group to avoid later transformations // to this path group.appendChild(newGroup); this.__currentElement = newGroup; }; /** * Draws a canvas, image or mock context to this canvas. * Note that all svg dom manipulation uses node.childNodes rather than node.children for IE support. * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage */ ctx.prototype.drawImage = function () { //convert arguments to a real array var args = Array.prototype.slice.call(arguments), image=args[0], dx, dy, dw, dh, sx=0, sy=0, sw, sh, parent, svg, defs, group, currentElement, svgImage, canvas, context, id; if (args.length === 3) { dx = args[1]; dy = args[2]; sw = image.width; sh = image.height; dw = sw; dh = sh; } else if (args.length === 5) { dx = args[1]; dy = args[2]; dw = args[3]; dh = args[4]; sw = image.width; sh = image.height; } else if (args.length === 9) { sx = args[1]; sy = args[2]; sw = args[3]; sh = args[4]; dx = args[5]; dy = args[6]; dw = args[7]; dh = args[8]; } else { throw new Error("Inavlid number of arguments passed to drawImage: " + arguments.length); } parent = this.__closestGroupOrSvg(); currentElement = this.__currentElement; var translateDirective = "translate(" + dx + ", " + dy + ")"; if (image instanceof ctx) { //canvas2svg mock canvas context. In the future we may want to clone nodes instead. //also I'm currently ignoring dw, dh, sw, sh, sx, sy for a mock context. svg = image.getSvg().cloneNode(true); if (svg.childNodes && svg.childNodes.length > 1) { defs = svg.childNodes[0]; while(defs.childNodes.length) { id = defs.childNodes[0].getAttribute("id"); this.__ids[id] = id; this.__defs.appendChild(defs.childNodes[0]); } group = svg.childNodes[1]; if (group) { //save original transform var originTransform = group.getAttribute("transform"); var transformDirective; if (originTransform) { transformDirective = originTransform+" "+translateDirective; } else { transformDirective = translateDirective; } group.setAttribute("transform", transformDirective); parent.appendChild(group); } } } else if (image.nodeName === "CANVAS" || image.nodeName === "IMG") { //canvas or image svgImage = this.__createElement("image"); svgImage.setAttribute("width", dw); svgImage.setAttribute("height", dh); svgImage.setAttribute("preserveAspectRatio", "none"); if (sx || sy || sw !== image.width || sh !== image.height) { //crop the image using a temporary canvas canvas = this.__document.createElement("canvas"); canvas.width = dw; canvas.height = dh; context = canvas.getContext("2d"); context.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh); image = canvas; } svgImage.setAttribute("transform", translateDirective); svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src")); parent.appendChild(svgImage); } }; /** * Generates a pattern tag */ ctx.prototype.createPattern = function (image, repetition) { var pattern = this.__document.createElementNS("http://www.w3.org/2000/svg", "pattern"), id = randomString(this.__ids), img; pattern.setAttribute("id", id); pattern.setAttribute("width", image.width); pattern.setAttribute("height", image.height); if (image.nodeName === "CANVAS" || image.nodeName === "IMG") { img = this.__document.createElementNS("http://www.w3.org/2000/svg", "image"); img.setAttribute("width", image.width); img.setAttribute("height", image.height); img.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src")); pattern.appendChild(img); this.__defs.appendChild(pattern); } else if (image instanceof ctx) { pattern.appendChild(image.__root.childNodes[1]); this.__defs.appendChild(pattern); } return new CanvasPattern(pattern, this); }; ctx.prototype.setLineDash = function (dashArray) { if (dashArray && dashArray.length > 0) { this.lineDash = dashArray.join(","); } else { this.lineDash = null; } }; /** * Not yet implemented */ ctx.prototype.drawFocusRing = function () {}; ctx.prototype.createImageData = function () {}; ctx.prototype.getImageData = function () {}; ctx.prototype.putImageData = function () {}; ctx.prototype.globalCompositeOperation = function () {}; ctx.prototype.setTransform = function () {}; //add options for alternative namespace if (typeof window === "object") { window.C2S = ctx; } // CommonJS/Browserify if (typeof module === "object" && typeof module.exports === "object") { module.exports = ctx; } }());