(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Superstruct = {})); })(this, (function (exports) { 'use strict'; var svgcanvas = {}; Object.defineProperty(svgcanvas, '__esModule', { value: true }); function toString(obj) { if (!obj) { return obj } if (typeof obj === 'string') { return obj } return obj + ''; } class ImageUtils { /** * Convert svg dataurl to canvas element * * @private */ async svg2canvas(svgDataURL, width, height) { const svgImage = await new Promise((resolve) => { var svgImage = new Image(); svgImage.onload = function() { resolve(svgImage); }; svgImage.src = svgDataURL; }); var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(svgImage, 0, 0); return canvas; } toDataURL(svgNode, width, height, type, encoderOptions, options) { var xml = new XMLSerializer().serializeToString(svgNode); // documentMode is an IE-only property // http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx // http://stackoverflow.com/questions/10964966/detect-ie-version-prior-to-v9-in-javascript var isIE = document.documentMode; if (isIE) { // This is patch from canvas2svg // IE search for a duplicate xmnls because they didn't implement setAttributeNS correctly var xmlns = /xmlns="http:\/\/www\.w3\.org\/2000\/svg".+xmlns="http:\/\/www\.w3\.org\/2000\/svg/gi; if(xmlns.test(xml)) { xml = xml.replace('xmlns="http://www.w3.org/2000/svg','xmlns:xlink="http://www.w3.org/1999/xlink'); } } if (!options) { options = {}; } var SVGDataURL = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(xml); if (type === "image/svg+xml" || !type) { if (options.async) { return Promise.resolve(SVGDataURL) } return SVGDataURL; } if (type === "image/jpeg" || type === "image/png") { if (!options.async) { throw new Error('svgcanvas: options.async must be set to true if type is image/jpeg | image/png') } return (async () => { const canvas = await this.svg2canvas(SVGDataURL, width, height); const dataUrl = canvas.toDataURL(type, encoderOptions); canvas.remove(); return dataUrl; })() } throw new Error('svgcanvas: Unknown type for toDataURL, please use image/jpeg | image/png | image/svg+xml.'); } getImageData(svgNode, width, height, sx, sy, sw, sh, options) { if (!options) { options = {}; } if (!options.async) { throw new Error('svgcanvas: options.async must be set to true for getImageData') } const svgDataURL = this.toDataURL(svgNode, width, height, 'image/svg+xml'); return (async () => { const canvas = await this.svg2canvas(svgDataURL, width, height); const ctx = canvas.getContext('2d'); const imageData = ctx.getImageData(sx, sy, sw, sh); canvas.remove(); return imageData; })() } } const utils = new ImageUtils(); /*!! * SVGCanvas v2.0.3 * Draw on SVG using Canvas's 2D Context API. * * Licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * * Author: * Kerry Liu * Zeno Zeng * * Copyright (c) 2014 Gliffy Inc. * Copyright (c) 2021 Zeno Zeng */ var Context = (function () { var STYLES, Context, CanvasGradient, CanvasPattern, namedEntities; //helper function to format a string function format(str, args) { var keys = Object.keys(args), i; for (i=0; i 1) { options = defaultOptions; options.width = arguments[0]; options.height = arguments[1]; } else if ( !o ) { options = defaultOptions; } else { options = o; } if (!(this instanceof Context)) { //did someone call this without new? return new Context(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; // 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.__styleStack = [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); // init transformation matrix this.resetTransform(); this.__options = options; this.__id = Math.random().toString(16).substring(2, 8); this.__debug(`new`, o); }; /** * Log * * @private */ Context.prototype.__debug = function(...data) { if (!this.__options.debug) { return } console.debug(`svgcanvas#${this.__id}:`, ...data); }; /** * Creates the specified svg element * @private */ Context.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 0) { this.setTransform(this.__transformMatrixStack.pop()); } }; /** * Create a new Path Element */ Context.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 */ Context.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 */ Context.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 */ Context.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: this.__matrixTransform(x, y).x, y: this.__matrixTransform(x, y).y })); }; /** * Closes the current path */ Context.prototype.closePath = function () { if (this.__currentDefaultPath) { this.__addPathCommand("Z"); } }; /** * Adds a line to command */ Context.prototype.lineTo = function (x, y) { this.__currentPosition = {x: x, y: y}; if (this.__currentDefaultPath.indexOf('M') > -1) { this.__addPathCommand(format("L {x} {y}", { x: this.__matrixTransform(x, y).x, y: this.__matrixTransform(x, y).y })); } else { this.__addPathCommand(format("M {x} {y}", { x: this.__matrixTransform(x, y).x, y: this.__matrixTransform(x, y).y })); } }; /** * Add a bezier command */ Context.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: this.__matrixTransform(cp1x, cp1y).x, cp1y: this.__matrixTransform(cp1x, cp1y).y, cp2x: this.__matrixTransform(cp2x, cp2y).x, cp2y: this.__matrixTransform(cp2x, cp2y).y, x: this.__matrixTransform(x, y).x, y: this.__matrixTransform(x, y).y })); }; /** * Adds a quadratic curve to command */ Context.prototype.quadraticCurveTo = function (cpx, cpy, x, y) { this.__currentPosition = {x: x, y: y}; this.__addPathCommand(format("Q {cpx} {cpy} {x} {y}", { cpx: this.__matrixTransform(cpx, cpy).x, cpy: this.__matrixTransform(cpx, cpy).y, x: this.__matrixTransform(x, y).x, y: this.__matrixTransform(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 */ Context.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 */ Context.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 */ Context.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. */ Context.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 */ Context.prototype.fillRect = function (x, y, width, height) { let {a, b, c, d, e, f} = this.getTransform(); if (JSON.stringify([a, b, c, d, e, f]) === JSON.stringify([1, 0, 0, 1, 0, 0])) { //clear entire canvas if (x === 0 && y === 0 && width === this.width && height === this.height) { this.__clearCanvas(); } } 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.__applyTransformation(rect); this.__applyStyleToCurrentElement("fill"); }; /** * Draws a rectangle with no fill * @param x * @param y * @param width * @param height */ Context.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.__applyTransformation(rect); this.__applyStyleToCurrentElement("stroke"); }; /** * Clear entire canvas: * 1. save current transforms * 2. remove all the childNodes of the root g element */ Context.prototype.__clearCanvas = function () { var rootGroup = this.__root.childNodes[1]; this.__root.removeChild(rootGroup); this.__currentElement = this.__document.createElementNS("http://www.w3.org/2000/svg", "g"); this.__root.appendChild(this.__currentElement); //reset __groupStack as all the child group nodes are all removed. this.__groupStack = []; }; /** * "Clears" a canvas by just drawing a white rectangle in the current group. */ Context.prototype.clearRect = function (x, y, width, height) { let {a, b, c, d, e, f} = this.getTransform(); if (JSON.stringify([a, b, c, d, e, f]) === JSON.stringify([1, 0, 0, 1, 0, 0])) { //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); this.__applyTransformation(rect); 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 */ Context.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 */ Context.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); }; /** * Fills or strokes text * @param text * @param x * @param y * @param action - stroke or fill * @private */ Context.prototype.__applyText = function (text, x, y, action) { var el = document.createElement("span"); el.setAttribute("style", 'font:' + this.font); var style = el.style, // CSSStyleDeclaration object parent = this.__closestGroupOrSvg(), textElement = this.__createElement("text", { "font-family": style.fontFamily, "font-size": style.fontSize, "font-style": style.fontStyle, "font-weight": style.fontWeight, // canvas doesn't support underline natively, but we do :) "text-decoration": this.__fontUnderline, "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.__applyTransformation(textElement); this.__applyStyleToCurrentElement(action); if (this.__fontHref) { var a = this.__createElement("a"); // canvas doesn't natively support linking, but we do :) a.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", this.__fontHref); a.appendChild(textElement); textElement = a; } parent.appendChild(textElement); }; /** * Creates a text element * @param text * @param x * @param y */ Context.prototype.fillText = function (text, x, y) { this.__applyText(text, x, y, "fill"); }; /** * Strokes text * @param text * @param x * @param y */ Context.prototype.strokeText = function (text, x, y) { this.__applyText(text, x, y, "stroke"); }; /** * No need to implement this for svg. * @param text * @return {TextMetrics} */ Context.prototype.measureText = function (text) { this.__ctx.font = this.font; return this.__ctx.measureText(text); }; /** * Arc command! */ Context.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; } var scaleX = Math.hypot(this.__transformMatrix.a, this.__transformMatrix.b); var scaleY = Math.hypot(this.__transformMatrix.c, this.__transformMatrix.d); this.lineTo(startX, startY); this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", { rx:radius * scaleX, ry:radius * scaleY, xAxisRotation:0, largeArcFlag:largeArcFlag, sweepFlag:sweepFlag, endX: this.__matrixTransform(endX, endY).x, endY: this.__matrixTransform(endX, endY).y })); this.__currentPosition = {x: endX, y: endY}; }; /** * Ellipse command! */ Context.prototype.ellipse = function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterClockwise) { if (startAngle === endAngle) { return; } var transformedCenter = this.__matrixTransform(x, y); x = transformedCenter.x; y = transformedCenter.y; var scale = this.__getTransformScale(); radiusX = radiusX * scale.x; radiusY = radiusY * scale.y; rotation = rotation + this.__getTransformRotation(); startAngle = startAngle % (2*Math.PI); endAngle = endAngle % (2*Math.PI); if(startAngle === endAngle) { endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI); } var endX = x + Math.cos(-rotation) * radiusX * Math.cos(endAngle) + Math.sin(-rotation) * radiusY * Math.sin(endAngle), endY = y - Math.sin(-rotation) * radiusX * Math.cos(endAngle) + Math.cos(-rotation) * radiusY * Math.sin(endAngle), startX = x + Math.cos(-rotation) * radiusX * Math.cos(startAngle) + Math.sin(-rotation) * radiusY * Math.sin(startAngle), startY = y - Math.sin(-rotation) * radiusX * Math.cos(startAngle) + Math.cos(-rotation) * radiusY * Math.sin(startAngle), sweepFlag = counterClockwise ? 0 : 1, largeArcFlag = 0, diff = endAngle - startAngle; if(diff < 0) { diff += 2*Math.PI; } if(counterClockwise) { largeArcFlag = diff > Math.PI ? 0 : 1; } else { largeArcFlag = diff > Math.PI ? 1 : 0; } // Transform is already applied, so temporarily remove since lineTo // will apply it again. var currentTransform = this.__transformMatrix; this.resetTransform(); this.lineTo(startX, startY); this.__transformMatrix = currentTransform; this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", { rx:radiusX, ry:radiusY, xAxisRotation:rotation*(180/Math.PI), largeArcFlag:largeArcFlag, sweepFlag:sweepFlag, endX:endX, endY:endY })); this.__currentPosition = {x: endX, y: endY}; }; /** * Generates a ClipPath from the clip command. */ Context.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 */ Context.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, 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(); const matrix = this.getTransform().translate(dx, dy); if (image instanceof Context) { //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) { this.__applyTransformation(group, matrix); 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; } this.__applyTransformation(svgImage, matrix); 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 */ Context.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); // We want the pattern sizing to be absolute, and not relative // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Patterns // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/patternUnits pattern.setAttribute("patternUnits", "userSpaceOnUse"); 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 Context) { pattern.appendChild(image.__root.childNodes[1]); this.__defs.appendChild(pattern); } return new CanvasPattern(pattern, this); }; Context.prototype.setLineDash = function (dashArray) { if (dashArray && dashArray.length > 0) { this.lineDash = dashArray.join(","); } else { this.lineDash = null; } }; /** * SetTransform changes the current transformation matrix to * the matrix given by the arguments as described below. * * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setTransform */ Context.prototype.setTransform = function (a, b, c, d, e, f) { if (a instanceof DOMMatrix) { this.__transformMatrix = new DOMMatrix([a.a, a.b, a.c, a.d, a.e, a.f]); } else { this.__transformMatrix = new DOMMatrix([a, b, c, d, e, f]); } }; /** * GetTransform Returns a copy of the current transformation matrix, * as a newly created DOMMAtrix Object * * @returns A DOMMatrix Object */ Context.prototype.getTransform = function () { let {a, b, c, d, e, f} = this.__transformMatrix; return new DOMMatrix([a, b, c, d, e, f]); }; /** * ResetTransform resets the current transformation matrix to the identity matrix * * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/resetTransform */ Context.prototype.resetTransform = function () { this.setTransform(1, 0, 0, 1, 0, 0); }; /** * Add the scaling transformation described by the arguments to the current transformation matrix. * * @param x The x argument represents the scale factor in the horizontal direction * @param y The y argument represents the scale factor in the vertical direction. * @see https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-scale */ Context.prototype.scale = function (x, y) { if (y === undefined) { y = x; } // If either of the arguments are infinite or NaN, then return. if (isNaN(x) || isNaN(y) || !isFinite(x) || !isFinite(y)) { return } let matrix = this.getTransform().scale(x, y); this.setTransform(matrix); }; /** * Rotate adds a rotation to the transformation matrix. * * @param angle The rotation angle, clockwise in radians. You can use degree * Math.PI / 180 to calculate a radian from a degree. * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rotate * @see https://www.w3.org/TR/css-transforms-1 */ Context.prototype.rotate = function (angle) { let matrix = this.getTransform().multiply(new DOMMatrix([ Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0 ])); this.setTransform(matrix); }; /** * Translate adds a translation transformation to the current matrix. * * @param x Distance to move in the horizontal direction. Positive values are to the right, and negative to the left. * @param y Distance to move in the vertical direction. Positive values are down, and negative are up. * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/translate */ Context.prototype.translate = function (x, y) { const matrix = this.getTransform().translate(x, y); this.setTransform(matrix); }; /** * Transform multiplies the current transformation with the matrix described by the arguments of this method. * This lets you scale, rotate, translate (move), and skew the context. * * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform */ Context.prototype.transform = function (a, b, c, d, e, f) { const matrix = this.getTransform().multiply(new DOMMatrix([a, b, c, d, e, f])); this.setTransform(matrix); }; Context.prototype.__matrixTransform = function(x, y) { return new DOMPoint(x, y).matrixTransform(this.__transformMatrix) }; /** * * @returns The scale component of the transform matrix as {x,y}. */ Context.prototype.__getTransformScale = function() { return { x: Math.hypot(this.__transformMatrix.a, this.__transformMatrix.b), y: Math.hypot(this.__transformMatrix.c, this.__transformMatrix.d) }; }; /** * * @returns The rotation component of the transform matrix in radians. */ Context.prototype.__getTransformRotation = function() { return Math.atan2(this.__transformMatrix.b, this.__transformMatrix.a); }; /** * * @param {*} sx The x-axis coordinate of the top-left corner of the rectangle from which the ImageData will be extracted. * @param {*} sy The y-axis coordinate of the top-left corner of the rectangle from which the ImageData will be extracted. * @param {*} sw The width of the rectangle from which the ImageData will be extracted. Positive values are to the right, and negative to the left. * @param {*} sh The height of the rectangle from which the ImageData will be extracted. Positive values are down, and negative are up. * @param {Boolean} options.async Will return a Promise if true, must be set to true * @returns An ImageData object containing the image data for the rectangle of the canvas specified. The coordinates of the rectangle's top-left corner are (sx, sy), while the coordinates of the bottom corner are (sx + sw, sy + sh). */ Context.prototype.getImageData = function(sx, sy, sw, sh, options) { return utils.getImageData(this.getSvg(), this.width, this.height, sx, sy, sw, sh, options); }; /** * Not yet implemented */ Context.prototype.drawFocusRing = function () {}; Context.prototype.createImageData = function () {}; Context.prototype.putImageData = function () {}; Context.prototype.globalCompositeOperation = function () {}; return Context; }()); function SVGCanvasElement(options) { this.ctx = new Context(options); this.svg = this.ctx.__root; // sync attributes to svg var svg = this.svg; var _this = this; var wrapper = document.createElement('div'); wrapper.style.display = 'inline-block'; wrapper.appendChild(svg); this.wrapper = wrapper; Object.defineProperty(this, 'className', { get: function() { return wrapper.getAttribute('class') || ''; }, set: function(val) { return wrapper.setAttribute('class', val); } }); Object.defineProperty(this, 'tagName', { get: function() { return "CANVAS"; }, set: function() {} // no-op }); ["width", "height"].forEach(function(prop) { Object.defineProperty(_this, prop, { get: function() { return svg.getAttribute(prop) | 0; }, set: function(val) { if (isNaN(val) || (typeof val === "undefined")) { return; } _this.ctx[prop] = val; svg.setAttribute(prop, val); return wrapper[prop] = val; } }); }); ["style", "id"].forEach(function(prop) { Object.defineProperty(_this, prop, { get: function() { return wrapper[prop]; }, set: function(val) { if (typeof val !== "undefined") { return wrapper[prop] = val; } } }); }); ["getBoundingClientRect"].forEach(function(fn) { _this[fn] = function() { return svg[fn](); }; }); } SVGCanvasElement.prototype.getContext = function(type) { if (type !== '2d') { throw new Error('Unsupported type of context for SVGCanvas'); } return this.ctx; }; // you should always use URL.revokeObjectURL after your work done SVGCanvasElement.prototype.toObjectURL = function() { var data = new XMLSerializer().serializeToString(this.svg); var svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}); return URL.createObjectURL(svg); }; /** * toDataURL returns a data URI containing a representation of the image in the format specified by the type parameter. * * @param {String} type A DOMString indicating the image format. The default type is image/svg+xml; this image format will be also used if the specified type is not supported. * @param {Number} encoderOptions A Number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range. * @param {Boolean} options.async Will return a Promise if true, must be set to true if type is not image/svg+xml */ SVGCanvasElement.prototype.toDataURL = function(type, encoderOptions, options) { return utils.toDataURL(this.svg, this.width, this.height, type, encoderOptions, options) }; SVGCanvasElement.prototype.addEventListener = function() { return this.svg.addEventListener.apply(this.svg, arguments); }; // will return wrapper element:
SVGCanvasElement.prototype.getElement = function() { return this.wrapper; }; SVGCanvasElement.prototype.getAttribute = function(prop) { return this.wrapper.getAttribute(prop); }; SVGCanvasElement.prototype.setAttribute = function(prop, val) { this.wrapper.setAttribute(prop, val); }; var Context_1 = svgcanvas.Context = Context; svgcanvas.Element = SVGCanvasElement; /** The SVG namespace string. */ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; /** The prefix for Base64-encoded SVG data URIs. */ const SVG_DATA_URI_PREFIX = 'data:image/svg+xml;base64,'; /** The fixed DPI that CSS uses when displaying real-world units. */ const CSS_DPI = 96; /** Convert an SVG string to a data URI. */ function svgStringToDataUri(svg) { const base64 = btoa(svg); const uri = `${SVG_DATA_URI_PREFIX}${base64}`; return uri; } /** Convert an SVG data URI to an SVG string. */ function svgDataUriToString(uri) { const base64 = uri.replace(SVG_DATA_URI_PREFIX, ''); const string = atob(base64); return string; } /** Convert an SVG string to an SVG element. */ function svgStringToElement(string) { const div = document.createElement('div'); div.innerHTML = string; const el = div.firstChild; return el; } /** Convert an SVG element to an SVG string. */ function svgElementToString(el) { const div = document.createElement('div'); div.appendChild(el); const string = div.innerHTML; return string; } /** Apply an `orientation` to a `width` and `height`. */ function applyOrientation(width, height, orientation) { if (orientation === 'square' && width != height) { width = height = Math.min(width, height); } else if (orientation === 'landscape' && width < height) { [width, height] = [height, width]; } else if (orientation === 'portrait' && height < width) { [width, height] = [height, width]; } return [width, height]; } /** Resolve the orientation of a `width` and `height`. */ function resolveOrientation(width, height) { return width === height ? 'square' : width < height ? 'portrait' : 'landscape'; } /** A pseudo-random number generator using the SFC32 algorithm. */ function Sfc32(a, b, c, d) { return () => { a |= 0; b |= 0; c |= 0; d |= 0; let t = (((a + b) | 0) + d) | 0; d = (d + 1) | 0; a = b ^ (b >>> 9); b = (c + (c << 3)) | 0; c = (c << 21) | (c >>> 11); c = (c + t) | 0; return t >>> 0; }; } /** The number of inches in a meter. */ const M_PER_INCH = 0.0254; /** The number of meters in an inch. */ const INCH_PER_M = 1 / M_PER_INCH; /** Conversions for units within their respective system. */ const CONVERSIONS = { m: ['metric', 1], cm: ['metric', 1 / 100], mm: ['metric', 1 / 1000], in: ['imperial', 1], ft: ['imperial', 12], yd: ['imperial', 36], pc: ['imperial', 1 / 6], pt: ['imperial', 1 / 72], }; /** Convert a `value` from one unit to another. */ function convertUnits(value, from, to, options = {}) { if (from === to) return value; const { dpi = CSS_DPI, precision } = options; // Swap pixels for inches using the dynamic `dpi`. let factor = 1; if (from === 'px') (factor /= dpi), (from = 'in'); if (to === 'px') (factor *= dpi), (to = 'in'); // Swap systems if `from` and `to` aren't using the same one. const [inSystem, inFactor] = CONVERSIONS[from]; const [outSystem, outFactor] = CONVERSIONS[to]; factor *= inFactor; factor /= outFactor; if (inSystem !== outSystem) { factor *= inSystem === 'metric' ? INCH_PER_M : M_PER_INCH; } // Calculate the result and optionally truncate to a fixed number of digits. let result = value * factor; if (precision != null && precision !== 0) { const p = 1 / precision; result = Math.trunc(result * p) / p; } return result; } // A reference to whether the keyboard event listeners have been attached. let KEYBOARD_EVENTS = new WeakMap(); // A reference to whether the pointer event listeners have been attached. let POINTER_EVENTS = new WeakMap(); function bool(name, options) { const sketch = assert(); const { traits } = sketch; const probability = typeof options === 'number' ? options : 0.5; const initial = typeof options === 'boolean' ? options : undefined; const r = random(); let value; if (name in traits && typeof traits[name] === 'boolean') { value = traits[name]; } else if (initial != null) { value = initial; } else { value = r < probability; } trait(sketch, name, value, { type: 'boolean', probability, initial }); return value; } function convert(value, from, to, options = {}) { if (typeof to === 'object') (options = to), (to = undefined); const sketch = current(); to = to ?? sketch?.settings.units ?? 'px'; const { dpi = sketch?.settings.dpi, precision } = options; if (from === to) return value; return convertUnits(value, from, to, { dpi, precision }); } /** * Define a `draw` function with the drawing logic to render each frame of an * animated sketch. */ function draw(fn) { const sketch = assert(); sketch.draw = fn; } /** * Attach an event listener to the canvas. */ function event(event, callback) { const sketch = assert(); const { el } = sketch; const fn = (e) => { exec(sketch, () => callback(e)); }; el.addEventListener(event, fn); const off = () => el.removeEventListener(event, fn); on(sketch, 'stop', off); return off; } function float(name, min, max, step) { const sketch = assert(); const { traits } = sketch; let initial; if (max == null) { initial = min; min = Number.MIN_VALUE; max = Number.MAX_VALUE; } let value = random(min, max, step); if (name in traits && typeof traits[name] === 'number') { value = traits[name]; } else if (initial != null) { value = initial; } trait(sketch, name, value, { type: 'float', min, max, step, initial }); return value; } /** * Run a function `fn` with a fork of the current pseudo-random number generator * seed, so that it only consumes one random value. */ function fork$1(fn) { const sketch = assert(); return fork(sketch, fn); } function int(name, min, max, step) { step = step == null ? 1 : Math.trunc(step); const sketch = assert(); const { traits } = sketch; let initial; if (max == null) { initial = min; min = Number.MIN_SAFE_INTEGER; max = Number.MAX_SAFE_INTEGER; } let value = random(min, max, step); if (name in traits && typeof traits[name] === 'number') { value = traits[name]; } else if (initial != null) { value = initial; } trait(sketch, name, value, { type: 'int', min, max, step, initial }); return value; } /** * Get a reference to the current keyboard data. * * The returned object is mutable and will continue to stay up to date as keys * are pressed down and lifted up. */ function keyboard$1() { const sketch = assert(); const keyboard$1 = keyboard(sketch); if (!KEYBOARD_EVENTS.has(sketch)) { event('keydown', (e) => { keyboard$1.code = e.code; keyboard$1.key = e.key; keyboard$1.codes[e.code] = true; keyboard$1.keys[e.key] = true; }); event('keyup', (e) => { keyboard$1.code = null; keyboard$1.key = null; delete keyboard$1.codes[e.code]; delete keyboard$1.keys[e.key]; }); KEYBOARD_EVENTS.set(sketch, true); } return keyboard$1; } /** * Get a new canvas layer to draw on, placed above all other layers. * * If the `name` argument is omitted it will be auto-generated. */ function layer$1(name) { const sketch = assert(); const { settings, output, el } = sketch; const { width, height, margin, units } = settings; const canvas = document.createElement('canvas'); const vector = output.type === 'svg' || output.type === 'pdf'; const ctx = vector ? new Context_1(`${width}${units}`, `${height}${units}`) : canvas.getContext('2d'); if (!ctx) { throw new Error(`Unable to get 2D rendering context from canvas!`); } const [top, , , left] = margin; const [totalWidth, totalHeight] = dimensions(sketch); const [pixelWidth, pixelHeight] = dimensions(sketch, 'pixel'); const [deviceWidth, deviceHeight] = dimensions(sketch, 'device'); const layer$1 = layer(sketch, name); canvas.width = deviceWidth; canvas.height = deviceHeight; canvas.style.position = 'absolute'; canvas.style.display = layer$1.hidden ? 'none' : 'block'; canvas.style.width = `${pixelWidth}px`; canvas.style.height = `${pixelHeight}px`; el.appendChild(canvas); ctx.scale(deviceWidth / totalWidth, deviceHeight / totalHeight); ctx.translate(top, left); layer$1.export = () => { return vector ? svgStringToDataUri(ctx.getSerializedSvg()) : canvas.toDataURL('image/png'); }; return ctx; } function pick(name, initial, choices) { if (choices == null) (choices = initial), (initial = undefined); const sketch = assert(); const { traits } = sketch; const { names, weights, mapping } = normalizeChoices(choices); const r = random(); let value; if (name in traits) { if (traits[name] in mapping) { value = traits[name]; } else { throw new Error(`Cannot re-pick traits "${name}"!`); } } else if (initial !== undefined) { value = String(initial); // allow booleans, numbers, etc. to be real } else { const sum = weights.reduce((m, w) => m + w, 0); const threshold = r * sum; let current = 0; const i = weights.findIndex((weight) => threshold < (current += weight)); value = names[i]; } trait(sketch, name, value, { type: 'pick', names, weights }); return mapping[value]; } /** * Get a reference to the current pointer (eg. mouse, pen, finger) data. * * The returned object is mutable and will continue to stay up to date as the * viewer clicks, taps, or hovers. * * Note that the pointer only refers to the "primary" pointer, and to handle * multi-touch scenarios you'll need to attach your own event handlers with the * `Void.event()` method instead. */ function pointer$1() { const sketch = assert(); const pointer$1 = pointer(sketch); if (!POINTER_EVENTS.has(sketch)) { event('pointermove', (e) => { if (!e.isPrimary) return; const { el } = sketch; const canvas = el.querySelector('canvas'); if (!canvas) return; pointer$1.type = e.pointerType; pointer$1.position ??= []; pointer$1.x = pointer$1.position[0] = convert(e.offsetX, 'px'); pointer$1.y = pointer$1.position[1] = convert(e.offsetY, 'px'); }); event('pointerleave', (e) => { if (!e.isPrimary) return; pointer$1.x = null; pointer$1.y = null; pointer$1.position = null; }); event('pointerdown', (e) => { if (!e.isPrimary) return; pointer$1.button = e.button; pointer$1.buttons[e.button] = true; }); event('pointerup', (e) => { if (!e.isPrimary) return; pointer$1.button = null; delete pointer$1.buttons[e.button]; }); POINTER_EVENTS.set(sketch, true); } return pointer$1; } function random(min, max, step) { if (min == null) (min = 0), (max = 1); if (max == null) (max = min), (min = 0); const sketch = assert(); const prng$1 = prng(sketch); const r = prng$1() / 2 ** 32; let value = r * (max - min + (step ?? 0)); if (step != null) { const s = 1 / step; // avoid common floating point errors by dividing first value = Math.floor(value * s) / s; } value += min; return value; } function settings$1(config) { if (typeof config === 'string' || Array.isArray(config)) { config = { dimensions: config }; } const sketch = assert(); const settings$1 = settings(sketch, config ?? {}); return settings$1; } // Normalize the shorthand for defining choices into schema objects. function normalizeChoices(shorthand) { const names = []; const weights = []; const mapping = {}; const entries = Array.isArray(shorthand) ? shorthand.entries() : Object.entries(shorthand); for (const [i, v] of entries) { const [weight, value] = Array.isArray(v) ? v : [1, v]; const name = typeof i === 'number' ? String(value) : i; names.push(name); weights.push(weight); mapping[name] = value; } return { names, weights, mapping }; } var _void = /*#__PURE__*/Object.freeze({ __proto__: null, bool: bool, convert: convert, draw: draw, event: event, float: float, fork: fork$1, int: int, keyboard: keyboard$1, layer: layer$1, pick: pick, pointer: pointer$1, random: random, settings: settings$1 }); /** Resolve the dimensions of a `config`. */ function dimensions$2(config) { let d = config.dimensions; return d == null ? [Infinity, Infinity, 'px'] : is(d) ? dimensions$1(d) : d; } /** Resolve the margin from a `config`. */ function margin(config) { let { margin } = config; if (margin == null) { let [, , du] = dimensions$2(config); margin = [0, 0, 0, 0, du]; } else if (margin.length === 2) { let [a, mu] = margin; margin = [a, a, a, a, mu]; } else if (margin.length === 3) { let [v, h, mu] = margin; margin = [v, h, v, h, mu]; } else if (margin.length === 4) { let [t, h, b, mu] = margin; margin = [t, h, b, h, mu]; } return margin; } /** Resolve the orientation from a `config`. */ function orientation(config) { let { orientation } = config; if (orientation == null) { let [w, h] = dimensions$2(config); orientation = w === Infinity || h === Infinity ? undefined : resolveOrientation(w, h); } return orientation; } /** Resolve the precision of a `config`. */ function precision(config) { let u = units(config); let p = u === 'px' ? 1 : 0; return config.precision ?? [p, u]; } /** Resolve the units of a `config`. */ function units(config) { return config.units ?? dimensions$2(config)[2]; } var methods$2 = /*#__PURE__*/Object.freeze({ __proto__: null, dimensions: dimensions$2, margin: margin, orientation: orientation, precision: precision, units: units }); /** Get the dimensions for a size. */ function dimensions$1(size) { if (is(size)) return SIZES[size].slice(); throw new Error(`Unrecognized size keyword: "${size}"`); } /** Check if a `value` is a size keyword. */ function is(value) { return typeof value === 'string' && value in SIZES; } /** Try to match a `width`, `height`, and `units` to a size keyword. */ function match(width, height, units) { for (let [size, [w, h, u]] of Object.entries(SIZES)) { if (u !== units) continue; if ((w !== width || h !== height) && (w !== height || h !== width)) continue; return size; } return null; } var methods$1 = /*#__PURE__*/Object.freeze({ __proto__: null, dimensions: dimensions$1, is: is, match: match }); /** Export the size-related methods namespace. */ /** A dictionary of recognized paper sizes. */ const SIZES = { // ISO A // https://en.wikipedia.org/wiki/ISO_216 A0: [841, 1189, 'mm'], A1: [594, 841, 'mm'], A2: [420, 594, 'mm'], A3: [297, 420, 'mm'], A4: [210, 297, 'mm'], A5: [148, 210, 'mm'], A6: [105, 148, 'mm'], A7: [74, 105, 'mm'], A8: [52, 74, 'mm'], A9: [37, 52, 'mm'], A10: [26, 37, 'mm'], // ISO B // https://en.wikipedia.org/wiki/ISO_216 B0: [1000, 1414, 'mm'], B1: [707, 1000, 'mm'], B2: [500, 707, 'mm'], B3: [353, 500, 'mm'], B4: [250, 353, 'mm'], B5: [176, 250, 'mm'], B6: [125, 176, 'mm'], B7: [88, 125, 'mm'], B8: [62, 88, 'mm'], B9: [44, 62, 'mm'], B10: [31, 44, 'mm'], // ISO C // https://en.wikipedia.org/wiki/ISO_216 C0: [917, 1297, 'mm'], C1: [648, 917, 'mm'], C2: [458, 648, 'mm'], C3: [324, 458, 'mm'], C4: [229, 324, 'mm'], C5: [162, 229, 'mm'], C6: [114, 162, 'mm'], C7: [81, 114, 'mm'], C8: [57, 81, 'mm'], C9: [40, 57, 'mm'], C10: [28, 40, 'mm'], // US // https://en.wikipedia.org/wiki/Paper_size // https://papersizes.io // https://www.princexml.com/doc/page-size-keywords/ Letter: [8.5, 11, 'in'], Legal: [8.5, 14, 'in'], Tabloid: [11, 17, 'in'], // JIS B // https://en.wikipedia.org/wiki/Paper_size 'JIS B0': [1030, 1456, 'mm'], 'JIS B1': [728, 1030, 'mm'], 'JIS B2': [515, 728, 'mm'], 'JIS B3': [364, 515, 'mm'], 'JIS B4': [257, 364, 'mm'], 'JIS B5': [182, 257, 'mm'], 'JIS B6': [128, 182, 'mm'], 'JIS B7': [91, 128, 'mm'], 'JIS B8': [64, 91, 'mm'], 'JIS B9': [45, 64, 'mm'], 'JIS B10': [32, 45, 'mm'], // ANSI // https://en.wikipedia.org/wiki/Paper_size 'ANSI A': [8.5, 11, 'in'], 'ANSI B': [11, 17, 'in'], 'ANSI C': [17, 22, 'in'], 'ANSI D': [22, 34, 'in'], 'ANSI E': [34, 44, 'in'], // ARCH // https://en.wikipedia.org/wiki/Paper_size 'Arch A': [9, 12, 'in'], 'Arch B': [12, 18, 'in'], 'Arch C': [18, 24, 'in'], 'Arch D': [24, 36, 'in'], 'Arch E': [36, 48, 'in'], // Photographic // https://en.wikipedia.org/wiki/Photo_print_sizes '4R': [4, 6, 'in'], '5R': [5, 7, 'in'], '6R': [6, 8, 'in'], '8R': [8, 10, 'in'], '10R': [10, 12, 'in'], '11R': [11, 14, 'in'], '12R': [12, 15, 'in'], '14R': [14, 17, 'in'], '16R': [16, 20, 'in'], '20R': [20, 24, 'in'], // 16:9 (UHD) // https://en.wikipedia.org/wiki/Display_resolution // https://support.google.com/youtube/answer/6375112 '240p': [426, 240, 'px'], '360p': [640, 360, 'px'], '480p': [854, 480, 'px'], '720p': [1280, 720, 'px'], '1080p': [1920, 1080, 'px'], '1440p': [2560, 1440, 'px'], '2160p': [3840, 2160, 'px'], '4320p': [7680, 4320, 'px'], }; /** A dictionary of output mime types. */ const OUTPUT_MIME_TYPES = { png: 'image/png', jpg: 'image/jpeg', webp: 'image/webp', svg: 'image/svg+xml', pdf: 'application/pdf', }; /** Get the current sketch and assert one exists. */ function assert() { let sketch = current(); if (!sketch) { throw new Error('Could not find your sketch! You must call `Void.*` methods inside a sketch function!'); } return sketch; } /** Attach the sketch to the DOM. */ function attach(sketch) { let { container, el } = sketch; let [width, height] = dimensions(sketch, 'pixel'); container.style.position = 'relative'; el.style.position = 'absolute'; el.style.width = `${width}px`; el.style.height = `${height}px`; el.style.top = '50%'; el.style.left = '50%'; el.style.margin = `${-height / 2}px 0 0 ${-width / 2}px`; el.style.background = 'white'; el.style.outline = '1px solid #e4e4e4'; container.appendChild(el); } /** Get the current sketch from the global `VOID` context. */ function current() { return globalThis.VOID?.sketch; } /** Detach the sketch from the DOM. */ function detach(sketch) { let { container, el } = sketch; stop(sketch); if (el.parentNode === container) container.removeChild(el); } /** Get the full-sized dimensions of a `sketch`, including margins, in the sketch's own units. */ function dimensions(sketch, mode = 'sketch') { let { settings } = sketch; let { width, height, margin, units } = settings; let [top, right, bottom, left] = margin; let precision = 1; let to = mode === 'sketch' ? settings.units : 'px'; let dpi = mode === 'pixel' ? CSS_DPI : mode === 'device' ? CSS_DPI * window.devicePixelRatio : settings.dpi; let w = width + left + right; let h = height + top + bottom; let x = convertUnits(w, units, to, { dpi, precision }); let y = convertUnits(h, units, to, { dpi, precision }); return [x, y]; } /** Emit an `event` to all the sketch's handlers. */ function emit(sketch, event, ...args) { for (let callback of sketch.handlers?.[event] ?? []) { callback(...args); } } /** Execute a `fn` with the sketch loaded on the global `VOID` context. */ function exec(sketch, fn) { let VOID = (globalThis.VOID ??= {}); let prev = VOID.sketch; VOID.sketch = sketch; try { fn(); } catch (e) { if (sketch.handlers?.error.length) { emit(sketch, 'error', e); } else { throw e; } } finally { VOID.sketch = prev; } } /** Run a `fn` with a fork of the PRNG, consuming only one random value. */ function fork(sketch, fn) { let p = prng(sketch); sketch.prng = Sfc32(p(), p(), p(), p()); let ret = fn(); sketch.prng = p; return ret; } /** Get the sketch's current frame information. */ function frame(sketch) { return (sketch.frame ??= { count: -1, time: window.performance.now(), rate: sketch.settings.fps, }); } /** Get the sketch's current keyboard information. */ function keyboard(sketch) { return (sketch.keyboard ??= { key: null, keys: {}, code: null, codes: {}, }); } /** Create a new layer with `name`. */ function layer(sketch, name) { if (!name) { let { length } = Object.keys(sketch.layers); while ((name = `Layer ${++length}`) in sketch.layers) { } } // Delete and reassign existing layers to preserve the sketch's layer order. let layer = sketch.layers[name] ?? { hidden: false }; if (name in sketch.layers) delete sketch.layers[name]; sketch.layers[name] = layer; return layer; } /** Create a sketch from a `construct` function, with optional `el` and `overrides`. */ function of(attrs) { let { construct, container, el, hash, output = { type: 'png' }, config = {}, layers = {}, traits = {}, } = attrs; if (el == null) { if (typeof document !== 'undefined') { el = document.createElement('div'); } else { throw new Error('Must have `document` global present to create a sketch!'); } } return { config, construct, container, el, hash, layers, output, settings: { dpi: CSS_DPI, fps: 60, frames: Infinity, height: container.offsetHeight, margin: [0, 0, 0, 0], precision: 0, units: 'px', width: container.offsetWidth, }, traits, }; } /** Attach a `callback` to when an `event` is emitted. */ function on(sketch, event, callback) { sketch.handlers ??= { construct: [], draw: [], error: [], play: [], pause: [], stop: [], }; sketch.handlers[event].push(callback); } /** Play the sketch's draw loop. */ function play(sketch) { if (sketch.raf) return; if (sketch.status === 'stopped') return; let { status, settings } = sketch; let frame$1 = frame(sketch); if (status !== 'playing') { sketch.status = 'playing'; emit(sketch, 'play'); } if (status == null) { exec(sketch, sketch.construct); emit(sketch, 'construct'); attach(sketch); } if (!sketch.draw || frame$1.count >= settings.frames) { stop(sketch); return; } let target = 1000 / settings.fps; let now = window.performance.now(); let delta = frame$1.count < 0 ? target : now - frame$1.time; let epsilon = 5; if (delta >= target - epsilon) { frame$1.count++; frame$1.time = now; frame$1.rate = 1000 / delta; exec(sketch, sketch.draw); emit(sketch, 'draw'); } sketch.raf = window.requestAnimationFrame(() => { delete sketch.raf; play(sketch); }); } /** Pause the sketch's draw loop. */ function pause(sketch) { if (sketch.status !== 'playing') return; if (sketch.raf) window.cancelAnimationFrame(sketch.raf); delete sketch.raf; sketch.status = 'paused'; emit(sketch, 'pause'); } /** Get the sketch's current pointer information. */ function pointer(sketch) { return (sketch.pointer ??= { type: null, x: null, y: null, position: null, button: null, buttons: {}, }); } /** Get the sketch's seeded pseudo-random number generator. */ function prng(sketch) { return (sketch.prng ??= Sfc32(...[0, 1, 2, 3].map((n) => { let i = 2 + n * 8; return parseInt(sketch.hash.substring(i, i + 8), 16); }))); } /** Save the sketch's layers as an image. */ async function save(sketch) { if (!sketch.output) { throw new Error(`Cannot save a sketch that wasn't initialized for export!`); } switch (sketch.output.type) { case 'png': case 'jpg': case 'webp': return await saveImage(sketch); case 'svg': return await saveSvg(sketch); case 'pdf': throw new Error('not implemented!'); } } async function saveImage(sketch) { let canvas = document.createElement('canvas'); let context = canvas.getContext('2d'); if (!context) { throw new Error(`Cannot get a 2D context for a canvas!`); } let [deviceWidth, deviceHeight] = dimensions(sketch, 'device'); let [pixelHeight, pixelWidth] = dimensions(sketch, 'pixel'); canvas.width = deviceWidth; canvas.height = deviceHeight; canvas.style.width = `${pixelHeight}px`; canvas.style.height = `${pixelWidth}px`; let images = await Promise.all(Object.values(sketch.layers) .filter((layer) => layer.export != null) .map((layer) => { return new Promise((resolve, reject) => { let img = new Image(); let url = layer.export(); img.onload = () => resolve(img); img.onerror = (e, source, lineno, colno, error) => reject(error); img.src = url; }); })); for (let image of images) { context.drawImage(image, 0, 0); } let { output } = sketch; let { type } = output; let quality = 'quality' in output ? output.quality : 1; let mime = OUTPUT_MIME_TYPES[type]; let url = canvas.toDataURL(mime, quality); return url; } async function saveSvg(sketch) { let svg = document.createElementNS(SVG_NAMESPACE, 'svg'); for (let layer of Object.values(sketch.layers)) { if (!layer.export) continue; let url = layer.export(); let string = svgDataUriToString(url); let el = svgStringToElement(string); let group = document.createElementNS(SVG_NAMESPACE, 'g'); svg.setAttribute('version', el.getAttribute('version')); svg.setAttribute('xmlns', el.getAttribute('xmlns')); svg.setAttribute('xmlns:xlink', el.getAttribute('xmlns:xlink')); svg.setAttribute('width', el.getAttribute('width')); svg.setAttribute('height', el.getAttribute('height')); svg.appendChild(group); for (let node of Array.from(el.childNodes)) { group.appendChild(node); } } let string = svgElementToString(svg); let url = svgStringToDataUri(string); return url; } // Code from before: // import { jsPDF } from 'jspdf' // export async function savePdf(sketch: Sketch): Promise { // /** Export a vector PDF file from a `settings` and `module`. */ // export let exportPdf = async ( // module: Module, // settings: Settings, // traits: Traits // ) => { // let string = getSvg(module, settings, traits) // let div = document.createElement('div') // div.innerHTML = string // let el = div.firstChild as SVGSVGElement // let [width, height] = Settings.outerDimensions(settings) // let { units } = settings // let doc = new jsPDF({ // unit: units as any, // format: [width, height], // hotfixes: ['px_scaling'], // }) // await doc.svg(el, { width, height, x: 0, y: 0 }) // doc.save('download.pdf') // } // } /** Resolve a `config` object into the sketch's settings. */ function settings(sketch, config) { config = { ...config, ...sketch.config }; let { dpi = CSS_DPI, fps = 60, frames = Infinity } = config; let orientation$1 = orientation(config); let units$1 = units(config); // Convert the precision to the sketch's units. let [precision$1, pu] = precision(config); precision$1 = convertUnits(precision$1, pu, units$1, { dpi }); // Create a unit conversion helper with the sketch's default units. let [width, height, du] = dimensions$2(config); if (width === Infinity) width = sketch.container.offsetWidth; if (height === Infinity) height = sketch.container.offsetHeight; width = convertUnits(width, du, units$1, { dpi, precision: precision$1 }); height = convertUnits(height, du, units$1, { dpi, precision: precision$1 }); // Apply the orientation setting to the dimensions. if (orientation$1 != null) { [width, height] = applyOrientation(width, height, orientation$1); } // Apply a margin, so the canvas is drawn without need to know it. let [mt, mr, mb, ml, mu] = margin(config); mt = convertUnits(mt, mu, units$1, { dpi, precision: precision$1 }); mr = convertUnits(mr, mu, units$1, { dpi, precision: precision$1 }); mb = convertUnits(mb, mu, units$1, { dpi, precision: precision$1 }); ml = convertUnits(ml, mu, units$1, { dpi, precision: precision$1 }); width -= mr + ml; height -= mt + mb; let margin$1 = [mt, mr, mb, ml]; sketch.config = config; sketch.settings = { dpi, fps, frames, height, margin: margin$1, precision: precision$1 ?? null, units: units$1, width, }; return sketch.settings; } /** Stop the sketch's draw loop. */ function stop(sketch) { if (sketch.status === 'stopped') return; if (sketch.raf) window.cancelAnimationFrame(sketch.raf); delete sketch.raf; sketch.status = 'stopped'; emit(sketch, 'stop'); } /** Set a trait on a `sketch` to a new value. */ function trait(sketch, name, value, schema) { sketch.schemas ??= {}; sketch.traits[name] = value; sketch.schemas[name] = schema; } var methods = /*#__PURE__*/Object.freeze({ __proto__: null, assert: assert, attach: attach, current: current, detach: detach, dimensions: dimensions, emit: emit, exec: exec, fork: fork, frame: frame, keyboard: keyboard, layer: layer, of: of, on: on, play: play, pause: pause, pointer: pointer, prng: prng, save: save, saveImage: saveImage, saveSvg: saveSvg, settings: settings, stop: stop, trait: trait }); exports.Config = methods$2; exports.SIZES = SIZES; exports.Size = methods$1; exports.Sketch = methods; exports.Void = _void; })); //# sourceMappingURL=index.cjs.map