UNPKG

easyqrcodejs-nodejs

Version:

NodeJS QRCode generator. Can save image or svg to file, get standard base64 image data url text or get SVG serialized text. Cross-browser QRCode generator for pure javascript. Support Dot style, Logo, Background image, Colorful, Title etc. settings. suppo

1,451 lines (1,344 loc) 43.3 kB
/*!! * Canvas 2 Svg for EasyQRCodeJS-NodeJS * A low level canvas to SVG converter. Uses a mock canvas context to build an SVG document. * */ ;(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 &nbsp; == \xa0 lookup["\\xa0"] = "&#160;"; 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: * ctx - existing Context2D to wrap around * 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, veiwBoxWidth: 500, veiwBoxHeight: 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]; options.veiwBoxWidth = arguments[2]; options.veiwBoxHeight = arguments[3]; } 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.veiwBoxWidth = options.veiwBoxWidth || this.width; this.veiwBoxHeight = options.veiwBoxHeight || this.height; this.enableMirroring = options.enableMirroring !== undefined ? options.enableMirroring : defaultOptions.enableMirroring; this.canvas = this; ///point back to this instance! this.__document = options.document || document; this.XMLSerializer = options.XMLSerializer || XMLSerializer; // console.log(document.createElement); // allow passing in an existing context to wrap around // if a context is passed in, we know a canvas already exist if (options.ctx) { this.__ctx = options.ctx; } else { 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); this.__root.setAttribute( "viewBox", "0 0 " + this.veiwBoxWidth + " " + this.veiwBoxHeight ); //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 (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( style.apply, format("url(#{id})", { id: value.__root.getAttribute("id"), }) ); } else if (value instanceof CanvasGradient) { //gradient currentElement.setAttribute( style.apply, 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 this.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") { if (!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-z0-9]+?)\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( "Invalid number of arguments passed to drawImage: " + arguments.length ); } parent = this.__closestGroupOrSvg(); currentElement = this.__currentElement; var translateDirective = "translate(" + dx + ", " + dy + ")"; //canvas image svgImage = this.__createElement("image"); svgImage.setAttribute("width", dw); svgImage.setAttribute("height", dh); svgImage.setAttribute("preserveAspectRatio", "none"); svgImage.setAttribute("opacity", this.globalAlpha); 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.originalSrc ); 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; } })();