diff --git a/src/canvg.js b/src/canvg.js new file mode 100644 index 0000000..70f8298 --- /dev/null +++ b/src/canvg.js @@ -0,0 +1,4093 @@ +/* eslint-disable */ +/* + * canvg.js - Javascript SVG parser and renderer on Canvas + * version 2.0.0 + * MIT Licensed + * Gabe Lerner (gabelerner@gmail.com) + * https://github.com/canvg/canvg + * + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('rgbcolor'), require('stackblur-canvas')) : + typeof define === 'function' && define.amd ? define(['rgbcolor', 'stackblur-canvas'], factory) : + (global.canvgv2 = factory(global.RGBColor,global.StackBlur)); +}(this, (function (rgbcolor,stackblurCanvas) { 'use strict'; + + rgbcolor = rgbcolor && rgbcolor.hasOwnProperty('default') ? rgbcolor['default'] : rgbcolor; + stackblurCanvas = stackblurCanvas && stackblurCanvas.hasOwnProperty('default') ? stackblurCanvas['default'] : stackblurCanvas; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var canvg_1 = createCommonjsModule(function (module) { + /* eslint-env browser, node */ + /* globals $, Sizzle, Windows, ActiveXObject */ + /* eslint-disable no-redeclare */ + + + var isNode = (module.exports && typeof window === 'undefined'), + nodeEnv = false; + var windowEnv; + + { + windowEnv = window; + windowEnv.DOMParser = window.DOMParser; + } + + var doc = windowEnv.document, + defaultClientWidth = 800, + defaultClientHeight = 600; + + function createCanvas(width, height) { + var c; + { + c = doc.createElement('canvas'); + c.width = width; + c.height = height; + } + return c; + } + + // canvg(target, s) + // empty parameters: replace all 'svg' elements on page with 'canvas' elements + // target: canvas element or the id of a canvas element + // s: svg string, url to svg file, or xml document + // opts: optional hash of options + // ignoreMouse: true => ignore mouse events + // ignoreAnimation: true => ignore animations + // ignoreDimensions: true => does not try to resize canvas + // ignoreClear: true => does not clear canvas + // offsetX: int => draws at a x offset + // offsetY: int => draws at a y offset + // scaleWidth: int => scales horizontally to width + // scaleHeight: int => scales vertically to height + // renderCallback: function => will call the function after the first render is completed + // enableRedraw: function => whether enable the redraw interval in node environment + // forceRedraw: function => will call the function on every frame, if it returns true, will redraw + var canvg = function (target, s, opts) { + // no parameters + if (target == null && s == null && opts == null) { + var svgTags = doc.querySelectorAll('svg'); + for (var i = 0; i < svgTags.length; i++) { + var svgTag = svgTags[i]; + var c = doc.createElement('canvas'); + if (typeof(svgTag.clientWidth) !== 'undefined' && typeof(svgTag.clientHeight) !== 'undefined') { + c.width = svgTag.clientWidth; + c.height = svgTag.clientHeight; + } else { + var rect = svgTag.getBoundingClientRect(); + c.width = rect.width; + c.height = rect.height; + } + svgTag.parentNode.insertBefore(c, svgTag); + svgTag.parentNode.removeChild(svgTag); + var div = doc.createElement('div'); + div.appendChild(svgTag); + canvg(c, div.innerHTML); + } + return; + } + + var svg = build(opts || {}); + + if (typeof target == 'string') { + target = doc.getElementById(target); + } + + // store class on canvas + if (target.svg != null) target.svg.stop(); + + // on i.e. 8 for flash canvas, we can't assign the property so check for it + if (!(target.childNodes && target.childNodes.length == 1 && target.childNodes[0].nodeName == 'OBJECT')) target.svg = svg; + + var ctx = target.getContext('2d'); + + if (typeof s.documentElement != 'undefined') { + // load from xml doc + svg.loadXmlDoc(ctx, s); + } else if (s.substr(0, 1) == '<') { + // load from xml string + svg.loadXml(ctx, s); + } else { + // load from url + svg.load(ctx, s); + } + }; + + var matchesSelector; + { + // see https://developer.mozilla.org/en-US/docs/Web/API/Element.matches + if (typeof Element == 'undefined') ; else if (typeof Element.prototype.matches != 'undefined') { + matchesSelector = function (node, selector) { + return node.matches(selector); + }; + } else if (typeof Element.prototype.webkitMatchesSelector != 'undefined') { + matchesSelector = function (node, selector) { + return node.webkitMatchesSelector(selector); + }; + } else if (typeof Element.prototype.mozMatchesSelector != 'undefined') { + matchesSelector = function (node, selector) { + return node.mozMatchesSelector(selector); + }; + } else if (typeof Element.prototype.msMatchesSelector != 'undefined') { + matchesSelector = function (node, selector) { + return node.msMatchesSelector(selector); + }; + } else if (typeof Element.prototype.oMatchesSelector != 'undefined') { + matchesSelector = function (node, selector) { + return node.oMatchesSelector(selector); + }; + } else { + // requires Sizzle: https://github.com/jquery/sizzle/wiki/Sizzle-Documentation + // or jQuery: http://jquery.com/download/ + // or Zepto: http://zeptojs.com/# + // without it, this is a ReferenceError + + if (typeof jQuery === 'function' || typeof Zepto === 'function') { + matchesSelector = function (node, selector) { + return $(node).is(selector); + }; + } + + if (typeof matchesSelector === 'undefined' && typeof Sizzle !== 'undefined') { + matchesSelector = Sizzle.matchesSelector; + } + } + } + + // slightly modified version of https://github.com/keeganstreet/specificity/blob/master/specificity.js + var attributeRegex = /(\[[^\]]+\])/g; + var idRegex = /(#[^\s\+>~\.\[:]+)/g; + var classRegex = /(\.[^\s\+>~\.\[:]+)/g; + var pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi; + var pseudoClassWithBracketsRegex = /(:[\w-]+\([^\)]*\))/gi; + var pseudoClassRegex = /(:[^\s\+>~\.\[:]+)/g; + var elementRegex = /([^\s\+>~\.\[:]+)/g; + + function getSelectorSpecificity(selector) { + var typeCount = [0, 0, 0]; + var findMatch = function (regex, type) { + var matches = selector.match(regex); + if (matches == null) { + return; + } + typeCount[type] += matches.length; + selector = selector.replace(regex, ' '); + }; + + selector = selector.replace(/:not\(([^\)]*)\)/g, ' $1 '); + selector = selector.replace(/{[\s\S]*/gm, ' '); + findMatch(attributeRegex, 1); + findMatch(idRegex, 0); + findMatch(classRegex, 1); + findMatch(pseudoElementRegex, 2); + findMatch(pseudoClassWithBracketsRegex, 1); + findMatch(pseudoClassRegex, 1); + selector = selector.replace(/[\*\s\+>~]/g, ' '); + selector = selector.replace(/[#\.]/g, ' '); + findMatch(elementRegex, 2); + return typeCount.join(''); + } + + function build(opts) { + var svg = { opts: opts }; + + svg.FRAMERATE = 30; + svg.MAX_VIRTUAL_PIXELS = 30000; + + svg.rootEmSize = 12; + svg.emSize = 12; + + svg.log = function (/* msg */) { }; + if (svg.opts['log'] == true && typeof console != 'undefined') { + svg.log = function (msg) { console.log(msg); }; // eslint-disable-line no-console + } + + // globals + svg.init = function (ctx) { + var uniqueId = 0; + svg.UniqueId = function () { uniqueId++; return 'canvg' + uniqueId; }; + svg.Definitions = {}; + svg.Styles = {}; + svg.StylesSpecificity = {}; + svg.Animations = []; + svg.Images = []; + svg.ctx = ctx; + svg.ViewPort = new (function () { + + this.viewPorts = []; + this.Clear = function () { this.viewPorts = []; }; + this.SetCurrent = function (width, height) { this.viewPorts.push({ width: width, height: height }); }; + this.RemoveCurrent = function () { this.viewPorts.pop(); }; + this.Current = function () { return this.viewPorts[this.viewPorts.length - 1]; }; + this.width = function () { return this.Current().width; }; + this.height = function () { return this.Current().height; }; + this.ComputeSize = function (d) { + if (d != null && typeof d == 'number') return d; + if (d == 'x') return this.width(); + if (d == 'y') return this.height(); + return Math.sqrt(Math.pow(this.width(), 2) + Math.pow(this.height(), 2)) / Math.sqrt(2); + }; + }); + }; + svg.init(); + + // images loaded + svg.ImagesLoaded = function () { + for (var i = 0; i < svg.Images.length; i++) { + if (!svg.Images[i].loaded) return false; + } + return true; + }; + + // trim + svg.trim = function (s) { return s.replace(/^\s+|\s+$/g, ''); }; + + // compress non-ideographic spaces + svg.compressSpaces = function (s) { return s.replace(/(?!\u3000)\s+/gm, ' '); }; + + // ajax + svg.ajax = function (url) { + var AJAX; + if (windowEnv.XMLHttpRequest) { AJAX = new windowEnv.XMLHttpRequest(); } else { AJAX = new ActiveXObject('Microsoft.XMLHTTP'); } + if (AJAX) { + AJAX.open('GET', url, false); + AJAX.send(null); + return AJAX.responseText; + } + return null; + }; + + // parse xml + svg.parseXml = function (xml) { + if (typeof Windows != 'undefined' && typeof Windows.Data != 'undefined' && typeof Windows.Data.Xml != 'undefined') { + var xmlDoc = new Windows.Data.Xml.Dom.XmlDocument(); + var settings = new Windows.Data.Xml.Dom.XmlLoadSettings(); + settings.prohibitDtd = false; + xmlDoc.loadXml(xml, settings); + return xmlDoc; + } else if (windowEnv.DOMParser) { + try { + var parser = opts.xmldom ? new windowEnv.DOMParser(opts.xmldom) : new windowEnv.DOMParser(); + return parser.parseFromString(xml, 'image/svg+xml'); + } catch (e) { + parser = opts.xmldom ? new windowEnv.DOMParser(opts.xmldom) : new windowEnv.DOMParser(); + return parser.parseFromString(xml, 'text/xml'); + } + } else { + xml = xml.replace(/]*>/, ''); + var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + } + }; + + svg.Property = function (name, value) { + this.name = name; + this.value = value; + }; + svg.Property.prototype.getValue = function () { + return this.value; + }; + + svg.Property.prototype.hasValue = function () { + return (this.value != null && this.value !== ''); + }; + + // return the numerical value of the property + svg.Property.prototype.numValue = function () { + if (!this.hasValue()) return 0; + + var n = parseFloat(this.value); + if ((this.value + '').match(/%$/)) { + n = n / 100.0; + } + return n; + }; + + svg.Property.prototype.valueOrDefault = function (def) { + if (this.hasValue()) return this.value; + return def; + }; + + svg.Property.prototype.numValueOrDefault = function (def) { + if (this.hasValue()) return this.numValue(); + return parseFloat(def); + }; + + // color extensions + // augment the current color value with the opacity + svg.Property.prototype.addOpacity = function (opacityProp) { + var newValue = this.value; + if (opacityProp.value != null && opacityProp.value != '' && typeof this.value == 'string') { // can only add opacity to colors, not patterns + var color = new rgbcolor(this.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')'; + } + } + return new svg.Property(this.name, newValue); + }; + + // definition extensions + // get the definition from the definitions table + svg.Property.prototype.getDefinition = function () { + var name = this.value.match(/#([^\)'"]+)/); + if (name) { name = name[1]; } + if (!name) { name = this.value; } + return svg.Definitions[name]; + }; + + svg.Property.prototype.isUrlDefinition = function () { + return this.value.indexOf('url(') == 0 + }; + + svg.Property.prototype.getFillStyleDefinition = function (e, opacityProp) { + var def = this.getDefinition(); + + // gradient + if (def != null && def.createGradient) { + return def.createGradient(svg.ctx, e, opacityProp); + } + + // pattern + if (def != null && def.createPattern) { + if (def.getHrefAttribute().hasValue()) { + var pt = def.attribute('patternTransform'); + def = def.getHrefAttribute().getDefinition(); + if (pt.hasValue()) { def.attribute('patternTransform', true).value = pt.value; } + } + return def.createPattern(svg.ctx, e, opacityProp); + } + + return null; + }; + + // length extensions + svg.Property.prototype.getDPI = function (/* viewPort */) { + return 96.0; // TODO: compute? + }; + + svg.Property.prototype.getREM = function (/* viewPort */) { + return svg.rootEmSize; + }; + + svg.Property.prototype.getEM = function (/* viewPort */) { + return svg.emSize; + }; + + svg.Property.prototype.getUnits = function () { + var s = this.value + ''; + return s.replace(/[0-9\.\-]/g, ''); + }; + + svg.Property.prototype.isPixels = function () { + if (!this.hasValue()) return false; + var s = this.value + ''; + if (s.match(/px$/)) return true; + if (s.match(/^[0-9]+$/)) return true; + return false; + }; + + // get the length as pixels + svg.Property.prototype.toPixels = function (viewPort, processPercent) { + if (!this.hasValue()) return 0; + var s = this.value + ''; + if (s.match(/rem$/)) return this.numValue() * this.getREM(viewPort); + if (s.match(/em$/)) return this.numValue() * this.getEM(viewPort); + if (s.match(/ex$/)) return this.numValue() * this.getEM(viewPort) / 2.0; + if (s.match(/px$/)) return this.numValue(); + if (s.match(/pt$/)) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); + if (s.match(/pc$/)) return this.numValue() * 15; + if (s.match(/cm$/)) return this.numValue() * this.getDPI(viewPort) / 2.54; + if (s.match(/mm$/)) return this.numValue() * this.getDPI(viewPort) / 25.4; + if (s.match(/in$/)) return this.numValue() * this.getDPI(viewPort); + if (s.match(/%$/)) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); + var n = this.numValue(); + if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); + return n; + }; + + // time extensions + // get the time as milliseconds + // https://svgwg.org/specs/animations/#ClockValueSyntax + svg.Property.prototype.toMilliseconds = function () { + if (!this.hasValue()) return 0; + var s = this.value + ''; + if (s.match(/ms$/)) return this.numValue(); + return this.numValue() * 1000; + }; + + // angle extensions + // get the angle as radians + svg.Property.prototype.toRadians = function () { + if (!this.hasValue()) return 0; + var s = this.value + ''; + if (s.match(/deg$/)) return this.numValue() * (Math.PI / 180.0); + if (s.match(/grad$/)) return this.numValue() * (Math.PI / 200.0); + if (s.match(/rad$/)) return this.numValue(); + return this.numValue() * (Math.PI / 180.0); + }; + + // text extensions + // get the text baseline + var textBaselineMapping = { + 'baseline': 'alphabetic', + 'before-edge': 'top', + 'text-before-edge': 'top', + 'middle': 'middle', + 'central': 'middle', + 'after-edge': 'bottom', + 'text-after-edge': 'bottom', + 'ideographic': 'ideographic', + 'alphabetic': 'alphabetic', + 'hanging': 'hanging', + 'mathematical': 'alphabetic' + }; + svg.Property.prototype.toTextBaseline = function () { + if (!this.hasValue()) return null; + return textBaselineMapping[this.value]; + }; + + // fonts + svg.Font = new (function () { + this.Styles = 'normal|italic|oblique|inherit'; + this.Variants = 'normal|small-caps|inherit'; + this.Weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit'; + + this.CreateFont = function (fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { + var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + fontFamily = fontFamily || f.fontFamily; + return { + fontFamily: fontFamily, + fontSize: fontSize || f.fontSize, + fontStyle: fontStyle || f.fontStyle, + fontWeight: fontWeight || f.fontWeight, + fontVariant: fontVariant || f.fontVariant, + toString: function () { return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' ') } + } + }; + + var that = this; + this.Parse = function (s) { + var f = {}; + var d = svg.trim(svg.compressSpaces(s || '')).split(' '); + var set = { fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false }; + var ff = ''; + for (var i = 0; i < d.length; i++) { + if (!set.fontStyle && that.Styles.indexOf(d[i]) != -1) { + if (d[i] != 'inherit') f.fontStyle = d[i]; + set.fontStyle = true; + } else if (!set.fontVariant && that.Variants.indexOf(d[i]) != -1) { + if (d[i] != 'inherit') f.fontVariant = d[i]; + set.fontStyle = set.fontVariant = true; + } else if (!set.fontWeight && that.Weights.indexOf(d[i]) != -1) { + if (d[i] != 'inherit') f.fontWeight = d[i]; + set.fontStyle = set.fontVariant = set.fontWeight = true; + } else if (!set.fontSize) { + if (d[i] != 'inherit') f.fontSize = d[i].split('/')[0]; + set.fontStyle = set.fontVariant = set.fontWeight = set.fontSize = true; + } else { if (d[i] != 'inherit') ff += d[i]; } + } + if (ff != '') f.fontFamily = ff; + return f; + }; + }); + + // points and paths + svg.ToNumberArray = function (s) { + var a = (s || '').match(/-?(\d+(?:\.\d*(?:[eE][+-]?\d+)?)?|\.\d+)(?=\D|$)/gm) || []; + for (var i = 0; i < a.length; i++) { + a[i] = parseFloat(a[i]); + } + return a; + }; + svg.Point = function (x, y) { + this.x = x; + this.y = y; + }; + svg.Point.prototype.angleTo = function (p) { + return Math.atan2(p.y - this.y, p.x - this.x); + }; + + svg.Point.prototype.applyTransform = function (v) { + var xp = this.x * v[0] + this.y * v[2] + v[4]; + var yp = this.x * v[1] + this.y * v[3] + v[5]; + this.x = xp; + this.y = yp; + }; + + svg.CreatePoint = function (s) { + var a = svg.ToNumberArray(s); + return new svg.Point(a[0], a[1]); + }; + svg.CreatePath = function (s) { + var a = svg.ToNumberArray(s); + var path = []; + for (var i = 0; i < a.length; i += 2) { + path.push(new svg.Point(a[i], a[i + 1])); + } + return path; + }; + + // bounding box + svg.BoundingBox = function (x1, y1, x2, y2) { // pass in initial points if you want + this.x1 = Number.NaN; + this.y1 = Number.NaN; + this.x2 = Number.NaN; + this.y2 = Number.NaN; + + this.x = function () { return this.x1; }; + this.y = function () { return this.y1; }; + this.width = function () { return this.x2 - this.x1; }; + this.height = function () { return this.y2 - this.y1; }; + + this.addPoint = function (x, y) { + if (x != null) { + if (isNaN(this.x1) || isNaN(this.x2)) { + this.x1 = x; + this.x2 = x; + } + if (x < this.x1) this.x1 = x; + if (x > this.x2) this.x2 = x; + } + + if (y != null) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + }; + this.addX = function (x) { this.addPoint(x, null); }; + this.addY = function (y) { this.addPoint(null, y); }; + + this.addBoundingBox = function (bb) { + this.addPoint(bb.x1, bb.y1); + this.addPoint(bb.x2, bb.y2); + }; + + this.addQuadraticCurve = function (p0x, p0y, p1x, p1y, p2x, p2y) { + var cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + var cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + }; + + this.addBezierCurve = function (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + var p0 = [p0x, p0y], + p1 = [p1x, p1y], + p2 = [p2x, p2y], + p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + for (var i = 0; i <= 1; i++) { + var f = function (t) { + return Math.pow(1 - t, 3) * p0[i] + + 3 * Math.pow(1 - t, 2) * t * p1[i] + + 3 * (1 - t) * Math.pow(t, 2) * p2[i] + + Math.pow(t, 3) * p3[i]; + }; + + var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + var c = 3 * p1[i] - 3 * p0[i]; + + if (a == 0) { + if (b == 0) continue; + var t = -c / b; + if (0 < t && t < 1) { + if (i == 0) this.addX(f(t)); + if (i == 1) this.addY(f(t)); + } + continue; + } + + var b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) continue; + var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (0 < t1 && t1 < 1) { + if (i == 0) this.addX(f(t1)); + if (i == 1) this.addY(f(t1)); + } + var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (0 < t2 && t2 < 1) { + if (i == 0) this.addX(f(t2)); + if (i == 1) this.addY(f(t2)); + } + } + }; + + this.isPointInBox = function (x, y) { + return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); + }; + + this.addPoint(x1, y1); + this.addPoint(x2, y2); + }; + + // transforms + svg.Transform = function (v) { + var that = this; + this.Type = {}; + + // translate + this.Type.translate = function (s) { + this.p = svg.CreatePoint(s); + this.apply = function (ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + }; + this.unapply = function (ctx) { + ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0); + }; + this.applyToPoint = function (p) { + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + }; + }; + + // rotate + this.Type.rotate = function (s) { + var a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + this.apply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + this.unapply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(-1.0 * this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + this.applyToPoint = function (p) { + var a = this.angle.toRadians(); + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); + p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); + }; + }; + + this.Type.scale = function (s) { + this.p = svg.CreatePoint(s); + this.apply = function (ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + }; + this.unapply = function (ctx) { + ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0); + }; + this.applyToPoint = function (p) { + p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); + }; + }; + + this.Type.matrix = function (s) { + this.m = svg.ToNumberArray(s); + this.apply = function (ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + }; + this.unapply = function (ctx) { + var a = this.m[0]; + var b = this.m[2]; + var c = this.m[4]; + var d = this.m[1]; + var e = this.m[3]; + var f = this.m[5]; + var g = 0.0; + var h = 0.0; + var i = 1.0; + var det = 1 / (a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)); + ctx.transform( + det * (e * i - f * h), + det * (f * g - d * i), + det * (c * h - b * i), + det * (a * i - c * g), + det * (b * f - c * e), + det * (c * d - a * f) + ); + }; + this.applyToPoint = function (p) { + p.applyTransform(this.m); + }; + }; + + this.Type.SkewBase = function (s) { + this.base = that.Type.matrix; + this.base(s); + this.angle = new svg.Property('angle', s); + }; + this.Type.SkewBase.prototype = new this.Type.matrix; + + this.Type.skewX = function (s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0]; + }; + this.Type.skewX.prototype = new this.Type.SkewBase; + + this.Type.skewY = function (s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0]; + }; + this.Type.skewY.prototype = new this.Type.SkewBase; + + this.transforms = []; + + this.apply = function (ctx) { + for (var i = 0; i < this.transforms.length; i++) { + this.transforms[i].apply(ctx); + } + }; + + this.unapply = function (ctx) { + for (var i = this.transforms.length - 1; i >= 0; i--) { + this.transforms[i].unapply(ctx); + } + }; + + // TODO: applyToPoint unused ... remove? + this.applyToPoint = function (p) { + for (var i = 0; i < this.transforms.length; i++) { + this.transforms[i].applyToPoint(p); + } + }; + + var data = svg.trim(svg.compressSpaces(v)).replace(/\)([a-zA-Z])/g, ') $1').replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/); + for (var i = 0; i < data.length; i++) { + if (data[i] === 'none') { + continue; + } + + var type = svg.trim(data[i].split('(')[0]); + var s = data[i].split('(')[1].replace(')', ''); + var transformType = this.Type[type]; + if (typeof transformType != 'undefined') { + var transform = new transformType(s); + transform.type = type; + this.transforms.push(transform); + } + } + }; + + // aspect ratio + svg.AspectRatio = function (ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX, minY, refX, refY) { + // aspect ratio - http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + aspectRatio = svg.compressSpaces(aspectRatio); + aspectRatio = aspectRatio.replace(/^defer\s/, ''); // ignore defer + var align = aspectRatio.split(' ')[0] || 'xMidYMid'; + var meetOrSlice = aspectRatio.split(' ')[1] || 'meet'; + + // calculate scale + var scaleX = width / desiredWidth; + var scaleY = height / desiredHeight; + var scaleMin = Math.min(scaleX, scaleY); + var scaleMax = Math.max(scaleX, scaleY); + if (meetOrSlice == 'meet') { + desiredWidth *= scaleMin; + desiredHeight *= scaleMin; + } + if (meetOrSlice == 'slice') { + desiredWidth *= scaleMax; + desiredHeight *= scaleMax; + } + + refX = new svg.Property('refX', refX); + refY = new svg.Property('refY', refY); + if (refX.hasValue() && refY.hasValue()) { + ctx.translate(-scaleMin * refX.toPixels('x'), -scaleMin * refY.toPixels('y')); + } else { + // align + if (align.match(/^xMid/) && ((meetOrSlice == 'meet' && scaleMin == scaleY) || (meetOrSlice == 'slice' && scaleMax == scaleY))) ctx.translate(width / 2.0 - desiredWidth / 2.0, 0); + if (align.match(/YMid$/) && ((meetOrSlice == 'meet' && scaleMin == scaleX) || (meetOrSlice == 'slice' && scaleMax == scaleX))) ctx.translate(0, height / 2.0 - desiredHeight / 2.0); + if (align.match(/^xMax/) && ((meetOrSlice == 'meet' && scaleMin == scaleY) || (meetOrSlice == 'slice' && scaleMax == scaleY))) ctx.translate(width - desiredWidth, 0); + if (align.match(/YMax$/) && ((meetOrSlice == 'meet' && scaleMin == scaleX) || (meetOrSlice == 'slice' && scaleMax == scaleX))) ctx.translate(0, height - desiredHeight); + } + + // scale + if (align == 'none') ctx.scale(scaleX, scaleY); + else if (meetOrSlice == 'meet') ctx.scale(scaleMin, scaleMin); + else if (meetOrSlice == 'slice') ctx.scale(scaleMax, scaleMax); + + // translate + ctx.translate(minX == null ? 0 : -minX, minY == null ? 0 : -minY); + }; + + // elements + svg.Element = {}; + + svg.EmptyProperty = new svg.Property('EMPTY', ''); + + svg.Element.ElementBase = function (node) { + this.attributes = {}; + this.styles = {}; + this.stylesSpecificity = {}; + this.children = []; + + // get or create attribute + this.attribute = function (name, createIfNotExists) { + var a = this.attributes[name]; + if (a != null) return a; + + if (createIfNotExists == true) { + a = new svg.Property(name, ''); + this.attributes[name] = a; + } + return a || svg.EmptyProperty; + }; + + this.getHrefAttribute = function () { + for (var a in this.attributes) { + if (a == 'href' || a.match(/:href$/)) { + return this.attributes[a]; + } + } + return svg.EmptyProperty; + }; + + // get or create style, crawls up node tree + this.style = function (name, createIfNotExists, skipAncestors) { + var s = this.styles[name]; + if (s != null) return s; + + var a = this.attribute(name); + if (a != null && a.hasValue()) { + this.styles[name] = a; // move up to me to cache + return a; + } + + if (skipAncestors != true) { + var p = this.parent; + if (p != null) { + var ps = p.style(name); + if (ps != null && ps.hasValue()) { + return ps; + } + } + } + + if (createIfNotExists == true) { + s = new svg.Property(name, ''); + this.styles[name] = s; + } + return s || svg.EmptyProperty; + }; + + // base render + this.render = function (ctx) { + // don't render display=none + if (this.style('display').value == 'none') return; + + // don't render visibility=hidden + if (this.style('visibility').value == 'hidden') return; + + ctx.save(); + if (this.style('mask').hasValue()) { // mask + var mask = this.style('mask').getDefinition(); + if (mask != null) mask.apply(ctx, this); + } else if (this.style('filter').hasValue()) { // filter + var filter = this.style('filter').getDefinition(); + if (filter != null) filter.apply(ctx, this); + } else { + this.setContext(ctx); + this.renderChildren(ctx); + this.clearContext(ctx); + } + ctx.restore(); + }; + + // base set context + this.setContext = function (/* ctx */) { + // OVERRIDE ME! + }; + + // base clear context + this.clearContext = function (/* ctx */) { + // OVERRIDE ME! + }; + + // base render children + this.renderChildren = function (ctx) { + for (var i = 0; i < this.children.length; i++) { + this.children[i].render(ctx); + } + }; + + this.addChild = function (childNode, create) { + var child = childNode; + if (create) child = svg.CreateElement(childNode); + child.parent = this; + if (child.type != 'title') { this.children.push(child); } + }; + + this.addStylesFromStyleDefinition = function () { + // add styles + for (var selector in svg.Styles) { + if (selector[0] != '@' && matchesSelector(node, selector)) { + var styles = svg.Styles[selector]; + var specificity = svg.StylesSpecificity[selector]; + if (styles != null) { + for (var name in styles) { + var existingSpecificity = this.stylesSpecificity[name]; + if (typeof existingSpecificity == 'undefined') { + existingSpecificity = '000'; + } + if (specificity >= existingSpecificity) { + this.styles[name] = styles[name]; + this.stylesSpecificity[name] = specificity; + } + } + } + } + } + }; + + // Microsoft Edge fix + var allUppercase = new RegExp('^[A-Z\-]+$'); + var normalizeAttributeName = function (name) { + if (allUppercase.test(name)) { + return name.toLowerCase(); + } + return name; + }; + + if (node != null && node.nodeType == 1) { //ELEMENT_NODE + // add attributes + for (var i = 0; i < node.attributes.length; i++) { + var attribute = node.attributes[i]; + var nodeName = normalizeAttributeName(attribute.nodeName); + this.attributes[nodeName] = new svg.Property(nodeName, attribute.value); + } + + this.addStylesFromStyleDefinition(); + + // add inline styles + if (this.attribute('style').hasValue()) { + var styles = this.attribute('style').value.split(';'); + for (var i = 0; i < styles.length; i++) { + if (svg.trim(styles[i]) != '') { + var style = styles[i].split(':'); + var name = svg.trim(style[0]); + var value = svg.trim(style[1]); + this.styles[name] = new svg.Property(name, value); + } + } + } + + // add id + if (this.attribute('id').hasValue()) { + if (svg.Definitions[this.attribute('id').value] == null) { + svg.Definitions[this.attribute('id').value] = this; + } + } + + // add children + for (var i = 0; i < node.childNodes.length; i++) { + var childNode = node.childNodes[i]; + if (childNode.nodeType == 1) this.addChild(childNode, true); //ELEMENT_NODE + if (this.captureTextNodes && (childNode.nodeType == 3 || childNode.nodeType == 4)) { + var text = childNode.value || childNode.text || childNode.textContent || ''; + if (svg.compressSpaces(text) != '') { + this.addChild(new svg.Element.tspan(childNode), false); // TEXT_NODE + } + } + } + } + }; + + svg.Element.RenderedElementBase = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.calculateOpacity = function() { + var opacity = 1.0; + + var el = this; + while (el != null) { + var opacityStyle = el.style('opacity', false, true); // no ancestors on style call + if (opacityStyle.hasValue()) { + opacity = opacity * opacityStyle.numValue(); + } + el = el.parent; + } + + return opacity; + }; + + this.setContext = function (ctx, fromMeasure) { + if (!fromMeasure) { // causes stack overflow when measuring text with gradients + // fill + if (this.style('fill').isUrlDefinition()) { + var fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity')); + if (fs != null) ctx.fillStyle = fs; + } else if (this.style('fill').hasValue()) { + var fillStyle = this.style('fill'); + if (fillStyle.value == 'currentColor') fillStyle.value = this.style('color').value; + if (fillStyle.value != 'inherit') ctx.fillStyle = (fillStyle.value == 'none' ? 'rgba(0,0,0,0)' : fillStyle.value); + } + if (this.style('fill-opacity').hasValue()) { + var fillStyle = new svg.Property('fill', ctx.fillStyle); + fillStyle = fillStyle.addOpacity(this.style('fill-opacity')); + ctx.fillStyle = fillStyle.value; + } + + // stroke + if (this.style('stroke').isUrlDefinition()) { + var fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity')); + if (fs != null) ctx.strokeStyle = fs; + } else if (this.style('stroke').hasValue()) { + var strokeStyle = this.style('stroke'); + if (strokeStyle.value == 'currentColor') strokeStyle.value = this.style('color').value; + if (strokeStyle.value != 'inherit') ctx.strokeStyle = (strokeStyle.value == 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value); + } + if (this.style('stroke-opacity').hasValue()) { + var strokeStyle = new svg.Property('stroke', ctx.strokeStyle); + strokeStyle = strokeStyle.addOpacity(this.style('stroke-opacity')); + ctx.strokeStyle = strokeStyle.value; + } + if (this.style('stroke-width').hasValue()) { + var newLineWidth = this.style('stroke-width').toPixels(); + ctx.lineWidth = newLineWidth == 0 ? 0.001 : newLineWidth; // browsers don't respect 0 + } + if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value; + if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value; + if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value; + if (this.style('paint-order').hasValue()) ctx.paintOrder = this.style('paint-order').value; + if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value != 'none') { + var gaps = svg.ToNumberArray(this.style('stroke-dasharray').value); + if (typeof ctx.setLineDash != 'undefined') { ctx.setLineDash(gaps); } else if (typeof ctx.webkitLineDash != 'undefined') { ctx.webkitLineDash = gaps; } else if (typeof ctx.mozDash != 'undefined' && !(gaps.length == 1 && gaps[0] == 0)) { ctx.mozDash = gaps; } + + var offset = this.style('stroke-dashoffset').toPixels(); + if (typeof ctx.lineDashOffset != 'undefined') { ctx.lineDashOffset = offset; } else if (typeof ctx.webkitLineDashOffset != 'undefined') { ctx.webkitLineDashOffset = offset; } else if (typeof ctx.mozDashOffset != 'undefined') { ctx.mozDashOffset = offset; } + } + } + + // font + if (typeof ctx.font != 'undefined') { + if (this.style('font').hasValue()) { + ctx.font = this.style('font').value; + } else { + ctx.font = svg.Font.CreateFont( + this.style('font-style').value, + this.style('font-variant').value, + this.style('font-weight').value, + this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '', + this.style('font-family').value).toString(); + + // update em size if needed + var currentFontSize = this.style('font-size', false, false); + if (currentFontSize.isPixels()) { + svg.emSize = currentFontSize.toPixels(); + } + } + } + + // transform + if (this.style('transform', false, true).hasValue()) { + var transform = new svg.Transform(this.style('transform', false, true).value); + transform.apply(ctx); + } + + // clip + if (this.style('clip-path', false, true).hasValue()) { + var clip = this.style('clip-path', false, true).getDefinition(); + if (clip != null) clip.apply(ctx); + } + + // opacity + ctx.globalAlpha = this.calculateOpacity(); + }; + }; + svg.Element.RenderedElementBase.prototype = new svg.Element.ElementBase; + + svg.Element.PathElementBase = function (node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.path = function (ctx) { + if (ctx != null) ctx.beginPath(); + return new svg.BoundingBox(); + }; + + this.renderChildren = function (ctx) { + this.path(ctx); + svg.Mouse.checkPath(this, ctx); + if (ctx.fillStyle != '') { + if (this.style('fill-rule').valueOrDefault('inherit') != 'inherit') { ctx.fill(this.style('fill-rule').value); } else { ctx.fill(); } + } + if (ctx.strokeStyle != '') ctx.stroke(); + + var markers = this.getMarkers(); + if (markers != null) { + if (this.style('marker-start').isUrlDefinition()) { + var marker = this.style('marker-start').getDefinition(); + marker.render(ctx, markers[0][0], markers[0][1]); + } + if (this.style('marker-mid').isUrlDefinition()) { + var marker = this.style('marker-mid').getDefinition(); + for (var i = 1; i < markers.length - 1; i++) { + marker.render(ctx, markers[i][0], markers[i][1]); + } + } + if (this.style('marker-end').isUrlDefinition()) { + var marker = this.style('marker-end').getDefinition(); + marker.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]); + } + } + }; + + this.getBoundingBox = function () { + return this.path(); + }; + + this.getMarkers = function () { + return null; + }; + }; + svg.Element.PathElementBase.prototype = new svg.Element.RenderedElementBase; + + svg.SetDefaults = function (ctx) { + // initial values and defaults + ctx.strokeStyle = 'rgba(0,0,0,0)'; + ctx.lineCap = 'butt'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 4; + }; + + // svg element + svg.Element.svg = function (node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.baseClearContext = this.clearContext; + this.clearContext = function (ctx) { + this.baseClearContext(ctx); + svg.ViewPort.RemoveCurrent(); + }; + + this.baseSetContext = this.setContext; + this.setContext = function (ctx) { + svg.SetDefaults(ctx); + if (ctx.canvas.style && typeof ctx.font != 'undefined' && typeof windowEnv.getComputedStyle != 'undefined') { + ctx.font = windowEnv.getComputedStyle(ctx.canvas).getPropertyValue('font'); + + var fontSize = new svg.Property('fontSize', svg.Font.Parse(ctx.font).fontSize); + if (fontSize.hasValue()) svg.rootEmSize = svg.emSize = fontSize.toPixels('y'); + } + + this.baseSetContext(ctx); + + // create new view port + if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0; + if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0; + ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y')); + + var width = svg.ViewPort.width(); + var height = svg.ViewPort.height(); + + if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%'; + if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%'; + if (typeof this.root == 'undefined') { + width = this.attribute('width').toPixels('x'); + height = this.attribute('height').toPixels('y'); + + var x = 0; + var y = 0; + if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) { + x = -this.attribute('refX').toPixels('x'); + y = -this.attribute('refY').toPixels('y'); + } + + if (this.attribute('overflow').valueOrDefault('hidden') != 'visible') { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(width, y); + ctx.lineTo(width, height); + ctx.lineTo(x, height); + ctx.closePath(); + ctx.clip(); + } + } + svg.ViewPort.SetCurrent(width, height); + + // viewbox + if (this.attribute('viewBox').hasValue()) { + var viewBox = svg.ToNumberArray(this.attribute('viewBox').value); + var minX = viewBox[0]; + var minY = viewBox[1]; + width = viewBox[2]; + height = viewBox[3]; + + svg.AspectRatio(ctx, + this.attribute('preserveAspectRatio').value, + svg.ViewPort.width(), + width, + svg.ViewPort.height(), + height, + minX, + minY, + this.attribute('refX').value, + this.attribute('refY').value); + + svg.ViewPort.RemoveCurrent(); + svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); + } + }; + }; + svg.Element.svg.prototype = new svg.Element.RenderedElementBase; + + // rect element + svg.Element.rect = function (node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.path = function (ctx) { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + var rx = this.attribute('rx').toPixels('x'); + var ry = this.attribute('ry').toPixels('y'); + if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx; + if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry; + rx = Math.min(rx, width / 2.0); + ry = Math.min(ry, height / 2.0); + + if (ctx != null) { + var KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + ctx.beginPath(); // always start the path so we don't fill prior paths + if (height > 0 && width > 0) { + ctx.moveTo(x + rx, y); + ctx.lineTo(x + width - rx, y); + ctx.bezierCurveTo(x + width - rx + (KAPPA * rx), y, x + width, y + ry - (KAPPA * ry), x + width, y + ry); + ctx.lineTo(x + width, y + height - ry); + ctx.bezierCurveTo(x + width, y + height - ry + (KAPPA * ry), x + width - rx + (KAPPA * rx), y + height, x + width - rx, y + height); + ctx.lineTo(x + rx, y + height); + ctx.bezierCurveTo(x + rx - (KAPPA * rx), y + height, x, y + height - ry + (KAPPA * ry), x, y + height - ry); + ctx.lineTo(x, y + ry); + ctx.bezierCurveTo(x, y + ry - (KAPPA * ry), x + rx - (KAPPA * rx), y, x + rx, y); + ctx.closePath(); + } + } + + return new svg.BoundingBox(x, y, x + width, y + height); + }; + }; + svg.Element.rect.prototype = new svg.Element.PathElementBase; + + // circle element + svg.Element.circle = function (node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.path = function (ctx) { + var cx = this.attribute('cx').toPixels('x'); + var cy = this.attribute('cy').toPixels('y'); + var r = this.attribute('r').toPixels(); + + if (ctx != null && r > 0) { + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2, false); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r); + }; + }; + svg.Element.circle.prototype = new svg.Element.PathElementBase; + + // ellipse element + svg.Element.ellipse = function (node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.path = function (ctx) { + var KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + var rx = this.attribute('rx').toPixels('x'); + var ry = this.attribute('ry').toPixels('y'); + var cx = this.attribute('cx').toPixels('x'); + var cy = this.attribute('cy').toPixels('y'); + + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(cx + rx, cy); + ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry); + ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy); + ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry); + ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); + }; + }; + svg.Element.ellipse.prototype = new svg.Element.PathElementBase; + + // line element + svg.Element.line = function (node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.getPoints = function () { + return [ + new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')), + new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y')) + ]; + }; + + this.path = function (ctx) { + var points = this.getPoints(); + + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + ctx.lineTo(points[1].x, points[1].y); + } + + return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y); + }; + + this.getMarkers = function () { + var points = this.getPoints(); + var a = points[0].angleTo(points[1]); + return [ + [points[0], a], + [points[1], a] + ]; + }; + }; + svg.Element.line.prototype = new svg.Element.PathElementBase; + + // polyline element + svg.Element.polyline = function (node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.points = svg.CreatePath(this.attribute('points').value); + this.path = function (ctx) { + var bb = new svg.BoundingBox(this.points[0].x, this.points[0].y); + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + } + for (var i = 1; i < this.points.length; i++) { + bb.addPoint(this.points[i].x, this.points[i].y); + if (ctx != null) ctx.lineTo(this.points[i].x, this.points[i].y); + } + return bb; + }; + + this.getMarkers = function () { + var markers = []; + for (var i = 0; i < this.points.length - 1; i++) { + markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]); + } + if (markers.length > 0) { + markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]); + } + return markers; + }; + }; + svg.Element.polyline.prototype = new svg.Element.PathElementBase; + + // polygon element + svg.Element.polygon = function (node) { + this.base = svg.Element.polyline; + this.base(node); + + this.basePath = this.path; + this.path = function (ctx) { + var bb = this.basePath(ctx); + if (ctx != null) { + ctx.lineTo(this.points[0].x, this.points[0].y); + ctx.closePath(); + } + return bb; + }; + }; + svg.Element.polygon.prototype = new svg.Element.polyline; + + // path element + svg.Element.path = function (node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + var d = this.attribute('d').value; + // TODO: convert to real lexer based on http://www.w3.org/TR/SVG11/paths.html#PathDataBNF + d = d.replace(/,/gm, ' '); // get rid of all commas + // As the end of a match can also be the start of the next match, we need to run this replace twice. + for (var i = 0; i < 2; i++) + d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([^\s])/gm, '$1 $2'); // suffix commands with spaces + d = d.replace(/([^\s])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // prefix commands with spaces + d = d.replace(/([0-9])([+\-])/gm, '$1 $2'); // separate digits on +- signs + // Again, we need to run this twice to find all occurances + for (var i = 0; i < 2; i++) + d = d.replace(/(\.[0-9]*)(\.)/gm, '$1 $2'); // separate digits when they start with a comma + d = d.replace(/([Aa](\s+[0-9]+){3})\s+([01])\s*([01])/gm, '$1 $3 $4 '); // shorthand elliptical arc path syntax + d = svg.compressSpaces(d); // compress multiple spaces + d = svg.trim(d); + this.PathParser = new (function (d) { + this.tokens = d.split(' '); + + this.reset = function () { + this.i = -1; + this.command = ''; + this.previousCommand = ''; + this.start = new svg.Point(0, 0); + this.control = new svg.Point(0, 0); + this.current = new svg.Point(0, 0); + this.points = []; + this.angles = []; + }; + + this.isEnd = function () { + return this.i >= this.tokens.length - 1; + }; + + this.isCommandOrEnd = function () { + if (this.isEnd()) return true; + return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; + }; + + this.isRelativeCommand = function () { + switch (this.command) { + case 'm': + case 'l': + case 'h': + case 'v': + case 'c': + case 's': + case 'q': + case 't': + case 'a': + case 'z': + return true; + } + return false; + }; + + this.getToken = function () { + this.i++; + return this.tokens[this.i]; + }; + + this.getScalar = function () { + return parseFloat(this.getToken()); + }; + + this.nextCommand = function () { + this.previousCommand = this.command; + this.command = this.getToken(); + }; + + this.getPoint = function () { + var p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + }; + + this.getAsControlPoint = function () { + var p = this.getPoint(); + this.control = p; + return p; + }; + + this.getAsCurrentPoint = function () { + var p = this.getPoint(); + this.current = p; + return p; + }; + + this.getReflectedControlPoint = function () { + if (this.previousCommand.toLowerCase() != 'c' && + this.previousCommand.toLowerCase() != 's' && + this.previousCommand.toLowerCase() != 'q' && + this.previousCommand.toLowerCase() != 't') { + return this.current; + } + + // reflect point + var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + }; + + this.makeAbsolute = function (p) { + if (this.isRelativeCommand()) { + p.x += this.current.x; + p.y += this.current.y; + } + return p; + }; + + this.addMarker = function (p, from, priorTo) { + // if the last angle isn't filled in because we didn't have this point yet ... + if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length - 1] == null) { + this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo); + } + this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); + }; + + this.addMarkerAngle = function (p, a) { + this.points.push(p); + this.angles.push(a); + }; + + this.getMarkerPoints = function () { return this.points; }; + this.getMarkerAngles = function () { + for (var i = 0; i < this.angles.length; i++) { + if (this.angles[i] == null) { + for (var j = i + 1; j < this.angles.length; j++) { + if (this.angles[j] != null) { + this.angles[i] = this.angles[j]; + break; + } + } + } + } + return this.angles; + }; + })(d); + + this.path = function (ctx) { + var pp = this.PathParser; + pp.reset(); + + var bb = new svg.BoundingBox(); + if (ctx != null) ctx.beginPath(); + while (!pp.isEnd()) { + pp.nextCommand(); + switch (pp.command) { + case 'M': + case 'm': + var p = pp.getAsCurrentPoint(); + pp.addMarker(p); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.moveTo(p.x, p.y); + pp.start = pp.current; + while (!pp.isCommandOrEnd()) { + var p = pp.getAsCurrentPoint(); + pp.addMarker(p, pp.start); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.lineTo(p.x, p.y); + } + break; + case 'L': + case 'l': + while (!pp.isCommandOrEnd()) { + var c = pp.current; + var p = pp.getAsCurrentPoint(); + pp.addMarker(p, c); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.lineTo(p.x, p.y); + } + break; + case 'H': + case 'h': + while (!pp.isCommandOrEnd()) { + var newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'V': + case 'v': + while (!pp.isCommandOrEnd()) { + var newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'C': + case 'c': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var p1 = pp.getPoint(); + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'S': + case 's': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var p1 = pp.getReflectedControlPoint(); + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'Q': + case 'q': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'T': + case 't': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var cntrl = pp.getReflectedControlPoint(); + pp.control = cntrl; + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'A': + case 'a': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var rx = pp.getScalar(); + var ry = pp.getScalar(); + var xAxisRotation = pp.getScalar() * (Math.PI / 180.0); + var largeArcFlag = pp.getScalar(); + var sweepFlag = pp.getScalar(); + var cp = pp.getAsCurrentPoint(); + + // Conversion from endpoint to center parameterization + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + // x1', y1' + var currp = new svg.Point( + Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0 + ); + // adjust radii + var l = Math.pow(currp.x, 2) / Math.pow(rx, 2) + Math.pow(currp.y, 2) / Math.pow(ry, 2); + if (l > 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + var s = (largeArcFlag == sweepFlag ? -1 : 1) * Math.sqrt( + ((Math.pow(rx, 2) * Math.pow(ry, 2)) - (Math.pow(rx, 2) * Math.pow(currp.y, 2)) - (Math.pow(ry, 2) * Math.pow(currp.x, 2))) / + (Math.pow(rx, 2) * Math.pow(currp.y, 2) + Math.pow(ry, 2) * Math.pow(currp.x, 2)) + ); + if (isNaN(s)) s = 0; + var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + var centp = new svg.Point( + (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, + (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y + ); + // vector magnitude + var m = function (v) { return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); }; + // ratio between two vectors + var r = function (u, v) { return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)) }; + // angle between two vectors + var a = function (u, v) { return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); }; + // initial angle + var a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]); + // angle delta + var u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]; + var v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry]; + var ad = a(u, v); + if (r(u, v) <= -1) ad = Math.PI; + if (r(u, v) >= 1) ad = 0; + + // for markers + var dir = 1 - sweepFlag ? 1.0 : -1.0; + var ah = a1 + dir * (ad / 2.0); + var halfWay = new svg.Point( + centp.x + rx * Math.cos(ah), + centp.y + ry * Math.sin(ah) + ); + pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); + pp.addMarkerAngle(cp, ah - dir * Math.PI); + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (ctx != null && !isNaN(a1) && !isNaN(ad)) { + var r = rx > ry ? rx : ry; + var sx = rx > ry ? 1 : rx / ry; + var sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1 / sx, 1 / sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + break; + case 'Z': + case 'z': + if (ctx != null) { + // only close path if it is not a straight line + if (bb.x1 !== bb.x2 && bb.y1 !== bb.y2) { + ctx.closePath(); + } + } + pp.current = pp.start; + } + } + + return bb; + }; + + this.getMarkers = function () { + var points = this.PathParser.getMarkerPoints(); + var angles = this.PathParser.getMarkerAngles(); + + var markers = []; + for (var i = 0; i < points.length; i++) { + markers.push([points[i], angles[i]]); + } + return markers; + }; + }; + svg.Element.path.prototype = new svg.Element.PathElementBase; + + // pattern element + svg.Element.pattern = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.createPattern = function (ctx, element, parentOpacityProp) { + var width = this.attribute('width').toPixels('x', true); + var height = this.attribute('height').toPixels('y', true); + + // render me using a temporary svg element + var tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['width'] = new svg.Property('width', width + 'px'); + tempSvg.attributes['height'] = new svg.Property('height', height + 'px'); + tempSvg.attributes['transform'] = new svg.Property('transform', this.attribute('patternTransform').value); + tempSvg.children = this.children; + + var c = createCanvas(width, height); + var cctx = c.getContext('2d'); + if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) { + cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true)); + } + + if (parentOpacityProp.hasValue()) { + this.styles['fill-opacity'] = parentOpacityProp; + } else { + delete this.styles['fill-opacity']; + } + + // render 3x3 grid so when we transform there's no white space on edges + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + cctx.save(); + tempSvg.attributes['x'] = new svg.Property('x', x * c.width); + tempSvg.attributes['y'] = new svg.Property('y', y * c.height); + tempSvg.render(cctx); + cctx.restore(); + } + } + var pattern = ctx.createPattern(c, 'repeat'); + return pattern; + }; + }; + svg.Element.pattern.prototype = new svg.Element.ElementBase; + + // marker element + svg.Element.marker = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.baseRender = this.render; + this.render = function (ctx, point, angle) { + if (!point) { return; } + ctx.translate(point.x, point.y); + if (this.attribute('orient').valueOrDefault('auto') == 'auto') ctx.rotate(angle); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') == 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth); + ctx.save(); + + // render me using a temporary svg element + var tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['refX'] = new svg.Property('refX', this.attribute('refX').value); + tempSvg.attributes['refY'] = new svg.Property('refY', this.attribute('refY').value); + tempSvg.attributes['width'] = new svg.Property('width', this.attribute('markerWidth').value); + tempSvg.attributes['height'] = new svg.Property('height', this.attribute('markerHeight').value); + tempSvg.attributes['fill'] = new svg.Property('fill', this.attribute('fill').valueOrDefault('black')); + tempSvg.attributes['stroke'] = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none')); + tempSvg.children = this.children; + tempSvg.render(ctx); + + ctx.restore(); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') == 'strokeWidth') ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth); + if (this.attribute('orient').valueOrDefault('auto') == 'auto') ctx.rotate(-angle); + ctx.translate(-point.x, -point.y); + }; + }; + svg.Element.marker.prototype = new svg.Element.ElementBase; + + // definitions element + svg.Element.defs = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.render = function (/* ctx */) { + // NOOP + }; + }; + svg.Element.defs.prototype = new svg.Element.ElementBase; + + // base for gradients + svg.Element.GradientBase = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.stops = []; + for (var i = 0; i < this.children.length; i++) { + var child = this.children[i]; + if (child.type == 'stop') this.stops.push(child); + } + + this.getGradient = function () { + // OVERRIDE ME! + }; + + this.gradientUnits = function () { + return this.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); + }; + + this.attributesToInherit = ['gradientUnits']; + + this.inheritStopContainer = function (stopsContainer) { + for (var i = 0; i < this.attributesToInherit.length; i++) { + var attributeToInherit = this.attributesToInherit[i]; + if (!this.attribute(attributeToInherit).hasValue() && stopsContainer.attribute(attributeToInherit).hasValue()) { + this.attribute(attributeToInherit, true).value = stopsContainer.attribute(attributeToInherit).value; + } + } + }; + + this.createGradient = function (ctx, element, parentOpacityProp) { + var stopsContainer = this; + if (this.getHrefAttribute().hasValue()) { + stopsContainer = this.getHrefAttribute().getDefinition(); + this.inheritStopContainer(stopsContainer); + } + + var addParentOpacity = function (color) { + if (parentOpacityProp.hasValue()) { + var p = new svg.Property('color', color); + return p.addOpacity(parentOpacityProp).value; + } + return color; + }; + + var g = this.getGradient(ctx, element); + if (g == null) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color); + for (var i = 0; i < stopsContainer.stops.length; i++) { + g.addColorStop(stopsContainer.stops[i].offset, addParentOpacity(stopsContainer.stops[i].color)); + } + + if (this.attribute('gradientTransform').hasValue()) { + // render as transformed pattern on temporary canvas + var rootView = svg.ViewPort.viewPorts[0]; + + var rect = new svg.Element.rect(); + rect.attributes['x'] = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes['y'] = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes['width'] = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS); + rect.attributes['height'] = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS); + + var group = new svg.Element.g(); + group.attributes['transform'] = new svg.Property('transform', this.attribute('gradientTransform').value); + group.children = [rect]; + + var tempSvg = new svg.Element.svg(); + tempSvg.attributes['x'] = new svg.Property('x', 0); + tempSvg.attributes['y'] = new svg.Property('y', 0); + tempSvg.attributes['width'] = new svg.Property('width', rootView.width); + tempSvg.attributes['height'] = new svg.Property('height', rootView.height); + tempSvg.children = [group]; + var c = createCanvas(rootView.width, rootView.height); + var tempCtx = c.getContext('2d'); + tempCtx.fillStyle = g; + tempSvg.render(tempCtx); + return tempCtx.createPattern(c, 'no-repeat'); + } + + return g; + }; + }; + svg.Element.GradientBase.prototype = new svg.Element.ElementBase; + + // linear gradient element + svg.Element.linearGradient = function (node) { + this.base = svg.Element.GradientBase; + this.base(node); + + this.attributesToInherit.push('x1'); + this.attributesToInherit.push('y1'); + this.attributesToInherit.push('x2'); + this.attributesToInherit.push('y2'); + + this.getGradient = function (ctx, element) { + var bb = this.gradientUnits() == 'objectBoundingBox' ? element.getBoundingBox(ctx) : null; + + if (!this.attribute('x1').hasValue() && + !this.attribute('y1').hasValue() && + !this.attribute('x2').hasValue() && + !this.attribute('y2').hasValue()) { + this.attribute('x1', true).value = 0; + this.attribute('y1', true).value = 0; + this.attribute('x2', true).value = 1; + this.attribute('y2', true).value = 0; + } + + var x1 = (this.gradientUnits() == 'objectBoundingBox' ? + bb.x() + bb.width() * this.attribute('x1').numValue() : + this.attribute('x1').toPixels('x')); + var y1 = (this.gradientUnits() == 'objectBoundingBox' ? + bb.y() + bb.height() * this.attribute('y1').numValue() : + this.attribute('y1').toPixels('y')); + var x2 = (this.gradientUnits() == 'objectBoundingBox' ? + bb.x() + bb.width() * this.attribute('x2').numValue() : + this.attribute('x2').toPixels('x')); + var y2 = (this.gradientUnits() == 'objectBoundingBox' ? + bb.y() + bb.height() * this.attribute('y2').numValue() : + this.attribute('y2').toPixels('y')); + + if (x1 == x2 && y1 == y2) return null; + return ctx.createLinearGradient(x1, y1, x2, y2); + }; + }; + svg.Element.linearGradient.prototype = new svg.Element.GradientBase; + + // radial gradient element + svg.Element.radialGradient = function (node) { + this.base = svg.Element.GradientBase; + this.base(node); + + this.attributesToInherit.push('cx'); + this.attributesToInherit.push('cy'); + this.attributesToInherit.push('r'); + this.attributesToInherit.push('fx'); + this.attributesToInherit.push('fy'); + this.attributesToInherit.push('fr'); + + this.getGradient = function (ctx, element) { + var bb = element.getBoundingBox(ctx); + + if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%'; + if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%'; + if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%'; + + var cx = (this.gradientUnits() == 'objectBoundingBox' ? + bb.x() + bb.width() * this.attribute('cx').numValue() : + this.attribute('cx').toPixels('x')); + var cy = (this.gradientUnits() == 'objectBoundingBox' ? + bb.y() + bb.height() * this.attribute('cy').numValue() : + this.attribute('cy').toPixels('y')); + + var fx = cx; + var fy = cy; + if (this.attribute('fx').hasValue()) { + fx = (this.gradientUnits() == 'objectBoundingBox' ? + bb.x() + bb.width() * this.attribute('fx').numValue() : + this.attribute('fx').toPixels('x')); + } + if (this.attribute('fy').hasValue()) { + fy = (this.gradientUnits() == 'objectBoundingBox' ? + bb.y() + bb.height() * this.attribute('fy').numValue() : + this.attribute('fy').toPixels('y')); + } + + var r = (this.gradientUnits() == 'objectBoundingBox' ? + (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue() : + this.attribute('r').toPixels()); + + var fr = this.attribute('fr').toPixels(); + return ctx.createRadialGradient(fx, fy, fr, cx, cy, r); + }; + }; + svg.Element.radialGradient.prototype = new svg.Element.GradientBase; + + // gradient stop element + svg.Element.stop = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.offset = this.attribute('offset').numValue(); + if (this.offset < 0) this.offset = 0; + if (this.offset > 1) this.offset = 1; + + var stopColor = this.style('stop-color', true); + if (stopColor.value === '') stopColor.value = '#000'; + if (this.style('stop-opacity').hasValue()) stopColor = stopColor.addOpacity(this.style('stop-opacity')); + this.color = stopColor.value; + }; + svg.Element.stop.prototype = new svg.Element.ElementBase; + + // animation base element + svg.Element.AnimateBase = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + svg.Animations.push(this); + + this.duration = 0.0; + this.begin = this.attribute('begin').toMilliseconds(); + this.maxDuration = this.begin + this.attribute('dur').toMilliseconds(); + + this.getProperty = function () { + var attributeType = this.attribute('attributeType').value; + var attributeName = this.attribute('attributeName').value; + + if (attributeType == 'CSS') { + return this.parent.style(attributeName, true); + } + return this.parent.attribute(attributeName, true); + }; + + this.initialValue = null; + this.initialUnits = ''; + this.removed = false; + + this.calcValue = function () { + // OVERRIDE ME! + return ''; + }; + + this.update = function (delta) { + // set initial value + if (this.initialValue == null) { + this.initialValue = this.getProperty().value; + this.initialUnits = this.getProperty().getUnits(); + } + + // if we're past the end time + if (this.duration > this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value == 'indefinite' || + this.attribute('repeatDur').value == 'indefinite') { + this.duration = 0.0; + } else if (this.attribute('fill').valueOrDefault('remove') == 'freeze' && !this.frozen) { + this.frozen = true; + this.parent.animationFrozen = true; + this.parent.animationFrozenValue = this.getProperty().value; + } else if (this.attribute('fill').valueOrDefault('remove') == 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue; + return true; + } + return false; + } + this.duration = this.duration + delta; + + // if we're past the begin time + var updated = false; + if (this.begin < this.duration) { + var newValue = this.calcValue(); // tween + + if (this.attribute('type').hasValue()) { + // for transform, etc. + var type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } + + this.getProperty().value = newValue; + updated = true; + } + + return updated; + }; + + this.from = this.attribute('from'); + this.to = this.attribute('to'); + this.values = this.attribute('values'); + if (this.values.hasValue()) this.values.value = this.values.value.split(';'); + + // fraction of duration we've covered + this.progress = function () { + var ret = { progress: (this.duration - this.begin) / (this.maxDuration - this.begin) }; + if (this.values.hasValue()) { + var p = ret.progress * (this.values.value.length - 1); + var lb = Math.floor(p), + ub = Math.ceil(p); + ret.from = new svg.Property('from', parseFloat(this.values.value[lb])); + ret.to = new svg.Property('to', parseFloat(this.values.value[ub])); + ret.progress = (p - lb) / (ub - lb); + } else { + ret.from = this.from; + ret.to = this.to; + } + return ret; + }; + }; + svg.Element.AnimateBase.prototype = new svg.Element.ElementBase; + + // animate element + svg.Element.animate = function (node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function () { + var p = this.progress(); + + // tween value linearly + var newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; + if (this.initialUnits === '%') { + newValue *= 100.0; // numValue() returns 0-1 whereas properties are 0-100 + } + return newValue + this.initialUnits; + }; + }; + svg.Element.animate.prototype = new svg.Element.AnimateBase; + + // animate color element + svg.Element.animateColor = function (node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function () { + var p = this.progress(); + var from = new rgbcolor(p.from.value); + var to = new rgbcolor(p.to.value); + + if (from.ok && to.ok) { + // tween color linearly + var r = from.r + (to.r - from.r) * p.progress; + var g = from.g + (to.g - from.g) * p.progress; + var b = from.b + (to.b - from.b) * p.progress; + return 'rgb(' + parseInt(r, 10) + ',' + parseInt(g, 10) + ',' + parseInt(b, 10) + ')'; + } + return this.attribute('from').value; + }; + }; + svg.Element.animateColor.prototype = new svg.Element.AnimateBase; + + // animate transform element + svg.Element.animateTransform = function (node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function () { + var p = this.progress(); + + // tween value linearly + var from = svg.ToNumberArray(p.from.value); + var to = svg.ToNumberArray(p.to.value); + var newValue = ''; + for (var i = 0; i < from.length; i++) { + newValue += from[i] + (to[i] - from[i]) * p.progress + ' '; + } + return newValue; + }; + }; + svg.Element.animateTransform.prototype = new svg.Element.animate; + + // font element + svg.Element.font = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + + this.isRTL = false; + this.isArabic = false; + this.fontFace = null; + this.missingGlyph = null; + this.glyphs = []; + for (var i = 0; i < this.children.length; i++) { + var child = this.children[i]; + if (child.type == 'font-face') { + this.fontFace = child; + if (child.style('font-family').hasValue()) { + svg.Definitions[child.style('font-family').value] = this; + } + } else if (child.type == 'missing-glyph') this.missingGlyph = child; + else if (child.type == 'glyph') { + if (child.arabicForm != '') { + this.isRTL = true; + this.isArabic = true; + if (typeof this.glyphs[child.unicode] == 'undefined') this.glyphs[child.unicode] = []; + this.glyphs[child.unicode][child.arabicForm] = child; + } else { + this.glyphs[child.unicode] = child; + } + } + } + + this.render = function(/* ctx */) { + // NO RENDER + }; + }; + svg.Element.font.prototype = new svg.Element.ElementBase; + + // font-face element + svg.Element.fontface = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.ascent = this.attribute('ascent').value; + this.descent = this.attribute('descent').value; + this.unitsPerEm = this.attribute('units-per-em').numValue(); + }; + svg.Element.fontface.prototype = new svg.Element.ElementBase; + + // missing-glyph element + svg.Element.missingglyph = function (node) { + this.base = svg.Element.path; + this.base(node); + + this.horizAdvX = 0; + }; + svg.Element.missingglyph.prototype = new svg.Element.path; + + // glyph element + svg.Element.glyph = function (node) { + this.base = svg.Element.path; + this.base(node); + + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + this.unicode = this.attribute('unicode').value; + this.arabicForm = this.attribute('arabic-form').value; + }; + svg.Element.glyph.prototype = new svg.Element.path; + + // text element + svg.Element.text = function (node) { + this.captureTextNodes = true; + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.baseSetContext = this.setContext; + this.setContext = function (ctx) { + this.baseSetContext(ctx); + + var textBaseline = this.style('dominant-baseline').toTextBaseline(); + if (textBaseline == null) textBaseline = this.style('alignment-baseline').toTextBaseline(); + if (textBaseline != null) ctx.textBaseline = textBaseline; + }; + + this.initializeCoordinates = function (ctx) { + this.x = this.attribute('x').toPixels('x'); + this.y = this.attribute('y').toPixels('y'); + if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); + if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); + this.x += this.getAnchorDelta(ctx, this, 0); + }; + + this.getBoundingBox = function (ctx) { + this.initializeCoordinates(ctx); + var bb = null; + for (var i = 0; i < this.children.length; i++) { + var childBB = this.getChildBoundingBox(ctx, this, this, i); + if (bb == null) bb = childBB; + else bb.addBoundingBox(childBB); + } + return bb; + }; + + this.renderChildren = function (ctx) { + this.initializeCoordinates(ctx); + for (var i = 0; i < this.children.length; i++) { + this.renderChild(ctx, this, this, i); + } + svg.Mouse.checkBoundingBox(this, this.getBoundingBox(ctx)); + }; + + this.getAnchorDelta = function (ctx, parent, startI) { + var textAnchor = this.style('text-anchor').valueOrDefault('start'); + if (textAnchor != 'start') { + var width = 0; + for (var i = startI; i < parent.children.length; i++) { + var child = parent.children[i]; + if (i > startI && child.attribute('x').hasValue()) break; // new group + width += child.measureTextRecursive(ctx); + } + return -1 * (textAnchor == 'end' ? width : width / 2.0); + } + return 0; + }; + + this.adjustChildCoordinates = function(ctx, textParent, parent, i) { + var child = parent.children[i]; + if (typeof(child.measureText) !== 'function') { + return child; + } + + if (child.attribute('x').hasValue()) { + child.x = child.attribute('x').toPixels('x') + textParent.getAnchorDelta(ctx, parent, i); + + // local text-anchor + var textAnchor = child.attribute('text-anchor').valueOrDefault('start'); + if (textAnchor !== 'start') { + var width = child.measureTextRecursive(ctx); + child.x += -1 * (textAnchor == 'end' ? width : width / 2.0); + } + + if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x'); + } else { + if (child.attribute('dx').hasValue()) textParent.x += child.attribute('dx').toPixels('x'); + child.x = textParent.x; + } + textParent.x = child.x + child.measureText(ctx); + + if (child.attribute('y').hasValue()) { + child.y = child.attribute('y').toPixels('y'); + if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y'); + } else { + if (child.attribute('dy').hasValue()) textParent.y += child.attribute('dy').toPixels('y'); + child.y = textParent.y; + } + textParent.y = child.y; + + return child; + }; + + this.getChildBoundingBox = function (ctx, textParent, parent, i) { + var child = this.adjustChildCoordinates(ctx, textParent, parent, i); + var bb = child.getBoundingBox(ctx); + + for (var i = 0; i < child.children.length; i++) { + var childBB = textParent.getChildBoundingBox(ctx, textParent, child, i); + bb.addBoundingBox(childBB); + } + + return bb; + }; + + this.renderChild = function (ctx, textParent, parent, i) { + var child = this.adjustChildCoordinates(ctx, textParent, parent, i); + child.render(ctx); + + for (var i = 0; i < child.children.length; i++) { + textParent.renderChild(ctx, textParent, child, i); + } + }; + }; + svg.Element.text.prototype = new svg.Element.RenderedElementBase; + + // text base + svg.Element.TextElementBase = function (node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.getGlyph = function (font, text, i) { + var c = text[i]; + var glyph = null; + if (font.isArabic) { + var arabicForm = 'isolated'; + if ((i == 0 || text[i - 1] == ' ') && i < text.length - 2 && text[i + 1] != ' ') arabicForm = 'terminal'; + if (i > 0 && text[i - 1] != ' ' && i < text.length - 2 && text[i + 1] != ' ') arabicForm = 'medial'; + if (i > 0 && text[i - 1] != ' ' && (i == text.length - 1 || text[i + 1] == ' ')) arabicForm = 'initial'; + if (typeof font.glyphs[c] != 'undefined') { + glyph = font.glyphs[c][arabicForm]; + if (glyph == null && font.glyphs[c].type == 'glyph') glyph = font.glyphs[c]; + } + } else { + glyph = font.glyphs[c]; + } + if (glyph == null) glyph = font.missingGlyph; + return glyph; + }; + + this.renderChildren = function (ctx) { + var customFont = this.parent.style('font-family').getDefinition(); + if (customFont != null) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); + var text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (var i = 0; i < text.length; i++) { + var glyph = this.getGlyph(customFont, text, i); + var scale = fontSize / customFont.fontFace.unitsPerEm; + ctx.translate(this.x, this.y); + ctx.scale(scale, -scale); + var lw = ctx.lineWidth; + ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize; + if (fontStyle == 'italic') ctx.transform(1, 0, .4, 1, 0, 0); + glyph.render(ctx); + if (fontStyle == 'italic') ctx.transform(1, 0, -.4, 1, 0, 0); + ctx.lineWidth = lw; + ctx.scale(1 / scale, -1 / scale); + ctx.translate(-this.x, -this.y); + + this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm; + if (typeof dx[i] != 'undefined' && !isNaN(dx[i])) { + this.x += dx[i]; + } + } + return; + } + if (ctx.paintOrder == 'stroke') { + if (ctx.strokeStyle != '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y); + if (ctx.fillStyle != '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); + } else { + if (ctx.fillStyle != '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); + if (ctx.strokeStyle != '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y); + } + }; + + this.getText = function () { + // OVERRIDE ME + }; + + this.measureTextRecursive = function (ctx) { + var width = this.measureText(ctx); + for (var i = 0; i < this.children.length; i++) { + width += this.children[i].measureTextRecursive(ctx); + } + return width; + }; + + this.measureText = function (ctx) { + var customFont = this.parent.style('font-family').getDefinition(); + if (customFont != null) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + var measure = 0; + var text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (var i = 0; i < text.length; i++) { + var glyph = this.getGlyph(customFont, text, i); + measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; + if (typeof dx[i] != 'undefined' && !isNaN(dx[i])) { + measure += dx[i]; + } + } + return measure; + } + + var textToMeasure = svg.compressSpaces(this.getText()); + if (!ctx.measureText) return textToMeasure.length * 10; + + ctx.save(); + this.setContext(ctx, true); + var width = ctx.measureText(textToMeasure).width; + ctx.restore(); + return width; + }; + + this.getBoundingBox = function (ctx) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + return new svg.BoundingBox(this.x, this.y - fontSize, this.x + this.measureText(ctx), this.y); + }; + }; + svg.Element.TextElementBase.prototype = new svg.Element.RenderedElementBase; + + // tspan + svg.Element.tspan = function (node) { + this.captureTextNodes = true; + this.base = svg.Element.TextElementBase; + this.base(node); + + this.text = svg.compressSpaces(node.value || node.text || node.textContent || ''); + this.getText = function () { + // if this node has children, then they own the text + if (this.children.length > 0) { return ''; } + return this.text; + }; + }; + svg.Element.tspan.prototype = new svg.Element.TextElementBase; + + // tref + svg.Element.tref = function (node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + this.getText = function () { + var element = this.getHrefAttribute().getDefinition(); + if (element != null) return element.children[0].getText(); + }; + }; + svg.Element.tref.prototype = new svg.Element.TextElementBase; + + // a element + svg.Element.a = function (node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + this.hasText = node.childNodes.length > 0; + for (var i = 0; i < node.childNodes.length; i++) { + if (node.childNodes[i].nodeType != 3) this.hasText = false; + } + + // this might contain text + this.text = this.hasText ? node.childNodes[0].value || node.childNodes[0].data : ''; + this.getText = function () { + return this.text; + }; + + this.baseRenderChildren = this.renderChildren; + this.renderChildren = function (ctx) { + if (this.hasText) { + // render as text element + this.baseRenderChildren(ctx); + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.toPixels('y'), this.x + this.measureText(ctx), this.y)); + } else if (this.children.length > 0) { + // render as temporary group + var g = new svg.Element.g(); + g.children = this.children; + g.parent = this; + g.render(ctx); + } + }; + + this.onclick = function () { + windowEnv.open(this.getHrefAttribute().value); + }; + + this.onmousemove = function () { + svg.ctx.canvas.style.cursor = 'pointer'; + }; + }; + svg.Element.a.prototype = new svg.Element.TextElementBase; + + // textPath + svg.Element.textPath = function (node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + var pathElement = this.getHrefAttribute().getDefinition(); + + this.text = svg.compressSpaces(node.value || node.text || node.textContent || ''); + + this.renderChildren = function (ctx) { + this.setTextData(ctx); + + ctx.save(); + var textDecoration = this.parent.style('text-decoration').value; + var fontSize = this.fontSize(); + var glyphInfo = this.glyphInfo; + var fill = ctx.fillStyle; + if (textDecoration === 'underline') { + ctx.beginPath(); + } + for (var i = 0; i < glyphInfo.length; i++) { + var p0 = glyphInfo[i].p0; + var p1 = glyphInfo[i].p1; + var partialText = glyphInfo[i].text; + + ctx.save(); + ctx.translate(p0.x, p0.y); + ctx.rotate(glyphInfo[i].rotation); + if (ctx.fillStyle != '') ctx.fillText(svg.compressSpaces(partialText), 0, 0); + if (ctx.strokeStyle != '') ctx.strokeText(svg.compressSpaces(partialText), 0, 0); + ctx.restore(); + if (textDecoration === 'underline') { + if (i === 0) { + ctx.moveTo(p0.x, p0.y + fontSize / 8); + } + ctx.lineTo(p1.x, p1.y + fontSize / 5); + } + + //// To assist with debugging visually, uncomment following + // + // ctx.beginPath(); + // if (i % 2) + // ctx.strokeStyle = 'red'; + // else + // ctx.strokeStyle = 'green'; + // ctx.moveTo(p0.x, p0.y); + // ctx.lineTo(p1.x, p1.y); + // ctx.stroke(); + // ctx.closePath(); + } + + if (textDecoration === 'underline') { + ctx.lineWidth = fontSize / 20; + ctx.strokeStyle = fill; + ctx.stroke(); + ctx.closePath(); + } + ctx.restore(); + }; + + this.path = function (ctx) { + var ca = this.dataArray; + if (ctx != null) { + ctx.beginPath(); + } + for (var n = 0; n < ca.length; n++) { + var c = ca[n].command; + var p = ca[n].points; + switch (c) { + case 'L': + if (ctx != null) ctx.lineTo(p[0], p[1]); + break; + case 'M': + if (ctx != null) ctx.moveTo(p[0], p[1]); + break; + case 'C': + if (ctx != null) ctx.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]); + break; + case 'Q': + if (ctx != null) ctx.quadraticCurveTo(p[0], p[1], p[2], p[3]); + break; + case 'A': + var cx = p[0], cy = p[1], rx = p[2], ry = p[3], + theta = p[4], dTheta = p[5], psi = p[6], fs = p[7]; + + var r = (rx > ry) ? rx : ry; + var scaleX = (rx > ry) ? 1 : rx / ry; + var scaleY = (rx > ry) ? ry / rx : 1; + + if (ctx != null) { + ctx.translate(cx, cy); + ctx.rotate(psi); + ctx.scale(scaleX, scaleY); + ctx.arc(0, 0, r, theta, theta + dTheta, 1 - fs); + ctx.scale(1 / scaleX, 1 / scaleY); + ctx.rotate(-psi); + ctx.translate(-cx, -cy); + } + break; + case 'z': + if (ctx != null) ctx.closePath(); + break; + } + } + }; + + this.getText = function () { + return this.text; + }; + + this.fontSize = function () { + return this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + }; + + this.measureText = function (ctx, text) { + var customFont = this.parent.style('font-family').getDefinition(); + text = text || this.getText(); + if (customFont != null) { + var fontSize = this.fontSize(); + var measure = 0; + if (customFont.isRTL) text = text.split("").reverse().join(""); + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (var i = 0; i < text.length; i++) { + var glyph = this.getGlyph(customFont, text, i); + measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; + if (typeof dx[i] != 'undefined' && !isNaN(dx[i])) { + measure += dx[i]; + } + } + return measure; + } + + var textToMeasure = svg.compressSpaces(text); + if (!ctx.measureText) return textToMeasure.length * 10; + + ctx.save(); + this.setContext(ctx); + var width = ctx.measureText(textToMeasure).width; + ctx.restore(); + return width; + }; + + // This method supposes what all custom fonts already loaded. + // If some font will be loaded after this method call, will not be rendered correctly. + // You need to call this method manually to update glyphs cache. + this.setTextData = function (ctx) { + if (this.hasOwnProperty('glyphInfo')) return; + + var that = this; + var charArr = this.getText().split(''); + var spacesNumber = this.getText().split(' ').length - 1; + var dx = svg.ToNumberArray(this.parent.attribute('dx').valueOrDefault('0')); + var letterSpacing = 0; + var anchor = this.parent.style('text-anchor').valueOrDefault('start'); + + var thisSpacing = this.style('letter-spacing'); + var parentSpacing = this.parent.style('letter-spacing'); + if (!thisSpacing.hasValue() || thisSpacing.getValue() === 'inherit') { + letterSpacing = parentSpacing.toPixels(); + } else if (thisSpacing.hasValue()) { + if (thisSpacing.getValue() !== 'initial' && thisSpacing.getValue() !== 'unset') { + letterSpacing = thisSpacing.toPixels(); + } + } + + // fill letter-spacing cache + this.letterSpacingCache = []; + for (var i = 0; i < this.getText().length; i++) { + this.letterSpacingCache.push(dx[i] !== undefined ? dx[i] : letterSpacing); + } + + var dxSum = this.letterSpacingCache.reduce(function (acc, cur) { return acc + cur || 0 }, 0); + + this.textWidth = this.measureText(ctx); + this.textHeight = this.fontSize(); + + var textFullWidth = Math.max(this.textWidth + dxSum, 0); + + this.glyphInfo = []; + + var fullPathWidth = this.getPathLength(); + + var startOffset = this.style('startOffset').numValueOrDefault(0) * fullPathWidth; + var offset = 0; + if (anchor === 'middle' || anchor === 'center') { + offset = -textFullWidth / 2; + } + if (anchor === 'end' || anchor === 'right') { + offset = -textFullWidth; + } + offset += startOffset; + + var getGetterSpacingAt = function (idx) { + idx = idx || 0; + return that.letterSpacingCache[idx] || 0; + }; + + var findSegmentToFitChar = function (c, charI) { + var glyphWidth = that.measureText(ctx, c); + + if (c === ' ' && anchor === 'justify' && textFullWidth < fullPathWidth) { + glyphWidth += (fullPathWidth - textFullWidth) / spacesNumber; + } + + if (charI > -1) { + offset += getGetterSpacingAt(charI); + } + + var splineStep = that.textHeight / 20; + var segment = { + p0: that.getEquidistantPointOnPath(offset, splineStep), + p1: that.getEquidistantPointOnPath(offset + glyphWidth, splineStep) + }; + + offset += glyphWidth; + + return segment; + }; + + for (var i = 0; i < charArr.length; i++) { + + // Find such segment what distance between p0 and p1 is approx. width of glyph + var segment = findSegmentToFitChar(charArr[i], i); + + if (segment.p0 === undefined || segment.p1 === undefined) { + continue; + } + + var width = that.getLineLength(segment.p0.x, segment.p0.y, segment.p1.x, segment.p1.y); + + // Note: Since glyphs are rendered one at a time, any kerning pair data built into the font will not be used. + // Can foresee having a rough pair table built in that the developer can override as needed. + // Or use "dx" attribute of the node as a naive replacement + + var kern = 0; + // placeholder for future implementation + + var midpoint = that.getPointOnLine(kern + width / 2.0, segment.p0.x, segment.p0.y, segment.p1.x, segment.p1.y); + + var rotation = Math.atan2((segment.p1.y - segment.p0.y), (segment.p1.x - segment.p0.x)); + this.glyphInfo.push({ + transposeX: midpoint.x, + transposeY: midpoint.y, + text: charArr[i], + rotation: rotation, + p0: segment.p0, + p1: segment.p1 + }); + } + }; + + this.parsePathData = function (path) { + this.pathLength = undefined; // reset path length + + if (!path) { + return []; + } + + var ca = []; + var pp = path.PathParser; + pp.reset(); + + // convert l, H, h, V, and v to L + while (!pp.isEnd()) { + var points = []; + var cmd = null; + var startX = pp.current ? pp.current.x : 0; + var startY = pp.current ? pp.current.y : 0; + + pp.nextCommand(); + var C = pp.command.toUpperCase(); + switch (pp.command) { + case 'M': + case 'm': + var p = pp.getAsCurrentPoint(); + // pp.addMarker(p); + points.push(p.x, p.y); + + pp.start = pp.current; + while (!pp.isCommandOrEnd()) { + var p = pp.getAsCurrentPoint(); + points.push(p.x, p.y); + cmd = 'L'; + } + break; + case 'L': + case 'l': + while (!pp.isCommandOrEnd()) { + var p = pp.getAsCurrentPoint(); + points.push(p.x, p.y); + } + cmd = 'L'; + break; + case 'H': + case 'h': + while (!pp.isCommandOrEnd()) { + var newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), + pp.current.y); + points.push(newP.x, newP.y); + pp.current = newP; + } + cmd = 'L'; + break; + case 'V': + case 'v': + while (!pp.isCommandOrEnd()) { + var newP = new svg.Point(pp.current.x, + (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); + points.push(newP.x, newP.y); + pp.current = newP; + } + cmd = 'L'; + break; + case 'C': + case 'c': + while (!pp.isCommandOrEnd()) { + var p1 = pp.getPoint(); + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + points.push(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'S': + case 's': + while (!pp.isCommandOrEnd()) { + var p1 = pp.getReflectedControlPoint(); + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + points.push(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + cmd = 'C'; + break; + case 'Q': + case 'q': + while (!pp.isCommandOrEnd()) { + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + points.push(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'T': + case 't': + while (!pp.isCommandOrEnd()) { + var cntrl = pp.getReflectedControlPoint(); + pp.control = cntrl; + var cp = pp.getAsCurrentPoint(); + points.push(cntrl.x, cntrl.y, cp.x, cp.y); + } + cmd = 'Q'; + break; + case 'A': + case 'a': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; // x1, y1 + var rx = pp.getScalar(); + var ry = pp.getScalar(); + var xAxisRotation = pp.getScalar() * (Math.PI / 180.0); // φ + var largeArcFlag = pp.getScalar(); // fA + var sweepFlag = pp.getScalar(); // fS + var cp = pp.getAsCurrentPoint(); // x2, y2 + + // Conversion from endpoint to center parameterization + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + // x1', y1' + var currp = new svg.Point( + Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, + -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0 + ); + // adjust radii + var l = Math.pow(currp.x, 2) / Math.pow(rx, 2) + Math.pow(currp.y, 2) / Math.pow(ry, 2); + if (l > 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + var s = (largeArcFlag == sweepFlag ? -1 : 1) * Math.sqrt( + ((Math.pow(rx, 2) * Math.pow(ry, 2)) - (Math.pow(rx, 2) * Math.pow(currp.y, 2)) - (Math.pow(ry, 2) * Math.pow(currp.x, 2))) / + (Math.pow(rx, 2) * Math.pow(currp.y, 2) + Math.pow(ry, 2) * Math.pow(currp.x, 2)) + ); + if (isNaN(s)) s = 0; + var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + var centp = new svg.Point( + (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, + (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y + ); + // vector magnitude + var m = function (v) { + return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); + }; + // ratio between two vectors + var r = function (u, v) { + return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)) + }; + // angle between two vectors + var a = function (u, v) { + return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); + }; + // initial angle + var a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]); // θ1 + // angle delta + var u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]; + var v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry]; + var ad = a(u, v); // Δθ + if (r(u, v) <= -1) ad = Math.PI; + if (r(u, v) >= 1) ad = 0; + if (sweepFlag === 0 && ad > 0) ad = ad - 2 * Math.PI; + if (sweepFlag === 1 && ad < 0) ad = ad + 2 * Math.PI; + + points = [centp.x, centp.y, rx, ry, a1, ad, xAxisRotation, sweepFlag]; + } + break; + case 'Z': + case 'z': + pp.current = pp.start; + } + + if (C !== 'Z') { + ca.push({ + command: cmd || C, + points: points, + start: { + x: startX, + y: startY + }, + pathLength: this.calcLength(startX, startY, cmd || C, points) + }); + } else { + ca.push({ + command: 'z', + points: [], + start: undefined, + pathLength: 0 + }); + } + } + return ca; + }; + + this.getPathLength = function() { + if (this.pathLength === undefined || this.pathLength === null || isNaN(this.pathLength)) { + this.pathLength = 0; + for (var l = 0; l < this.dataArray.length; l++) { + if (this.dataArray[l].pathLength > 0) { + this.pathLength += this.dataArray[l].pathLength; + } + } + } + return this.pathLength; + }; + + this.getPointOnPath = function(distance) { + var cumulativePathLength = 0; + var fullLen = this.getPathLength(); + var p = undefined; + + if (distance < -0.00005 || distance - 0.00005 > fullLen) return undefined; + + for(var i = 0; i < this.dataArray.length; i++) { + var pathCmd = this.dataArray[i]; + + if (pathCmd + && ( + pathCmd.pathLength < 0.00005 + || cumulativePathLength + pathCmd.pathLength + 0.00005 < distance + ) + ) { + cumulativePathLength += pathCmd.pathLength; + continue; + } + + var delta = distance - cumulativePathLength; + var currentT = undefined; + switch (pathCmd.command) { + case 'L': + p = this.getPointOnLine(delta, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.start.x, pathCmd.start.y); + break; + case 'A': + var start = pathCmd.points[4]; + // 4 = theta + var dTheta = pathCmd.points[5]; + // 5 = dTheta + var end = pathCmd.points[4] + dTheta; + + currentT = start + delta / pathCmd.pathLength * dTheta; + if(dTheta < 0 && currentT < end || dTheta >= 0 && currentT > end) { + break; + } + p = this.getPointOnEllipticalArc(pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], currentT, pathCmd.points[6]); + break; + case 'C': + currentT = delta / pathCmd.pathLength; + if (currentT > 1) { + currentT = 1; + } + p = this.getPointOnCubicBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], pathCmd.points[4], pathCmd.points[5]); + break; + case 'Q': + currentT = delta / pathCmd.pathLength; + if (currentT > 1) { + currentT = 1; + } + p = this.getPointOnQuadraticBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3]); + break; + } + + if (p !== undefined && p !== {}) { + return p; + } + + break; + } + }; + + // TODO need some optimisations. possibly build cache only for curved segments? + this.buildEquidistantCache = function(step, precision) { + var fullLen = this.getPathLength(); + precision = precision || 0.25; // accuracy vs performance + step = step || fullLen / 100; + this.equidistantCache = this.equidistantCache || {}; + if ( + !this.equidistantCache.hasOwnProperty('points') + || this.equidistantCache.step != step + || this.equidistantCache.precision != precision + ) { + // Prepare cache + this.equidistantCache = { + step: step, + precision: precision, + points: [] + }; + // Calculate points + var s = 0; + for (var l = 0; l <= fullLen; l += precision) { + var p0 = this.getPointOnPath(l), + p1 = this.getPointOnPath(l + precision); + + if (p0 === undefined || p1 === undefined) continue; + + s += this.getLineLength(p0.x, p0.y, p1.x, p1.y); + if (s >= step) { + this.equidistantCache.points.push({ + x: p0.x, + y: p0.y, + distance: l + }); + s -= step; + } + } + } + }; + + this.getEquidistantPointOnPath = function (targetDistance, step, precision) { + this.buildEquidistantCache(step, precision); + + if (targetDistance < 0 || targetDistance - this.getPathLength() > 0.00005) return undefined; + + var idx = Math.round(targetDistance / this.getPathLength() * (this.equidistantCache.points.length - 1)); + return this.equidistantCache.points[idx] || undefined; + }; + + this.getLineLength = function (x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + }; + + this.getPointOnLine = function (dist, P1x, P1y, P2x, P2y, fromX, fromY) { + if (fromX === undefined) { + fromX = P1x; + } + if (fromY === undefined) { + fromY = P1y; + } + + var m = (P2y - P1y) / ((P2x - P1x) + 0.00000001); + var run = Math.sqrt(dist * dist / (1 + m * m)); + if (P2x < P1x) { + run *= -1; + } + var rise = m * run; + var pt; + + if (P2x === P1x) { // vertical line + pt = { + x: fromX, + y: fromY + rise + }; + } else if ((fromY - P1y) / ((fromX - P1x) + 0.00000001) === m) { + pt = { + x: fromX + run, + y: fromY + rise + }; + } + else { + var ix, iy; + + var len = this.getLineLength(P1x, P1y, P2x, P2y); + if (len < 0.00000001) { + return undefined; + } + var u = (((fromX - P1x) * (P2x - P1x)) + ((fromY - P1y) * (P2y - P1y))); + u = u / (len * len); + ix = P1x + u * (P2x - P1x); + iy = P1y + u * (P2y - P1y); + + var pRise = this.getLineLength(fromX, fromY, ix, iy); + var pRun = Math.sqrt(dist * dist - pRise * pRise); + run = Math.sqrt(pRun * pRun / (1 + m * m)); + if (P2x < P1x) { + run *= -1; + } + rise = m * run; + pt = { + x: ix + run, + y: iy + rise + }; + } + + return pt; + }; + + this.getPointOnCubicBezier = function (pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) { + function CB1(t) { + return t * t * t; + } + + function CB2(t) { + return 3 * t * t * (1 - t); + } + + function CB3(t) { + return 3 * t * (1 - t) * (1 - t); + } + + function CB4(t) { + return (1 - t) * (1 - t) * (1 - t); + } + + var x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct); + var y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct); + + return { + x: x, + y: y + }; + }; + + this.getPointOnQuadraticBezier = function (pct, P1x, P1y, P2x, P2y, P3x, P3y) { + function QB1(t) { + return t * t; + } + + function QB2(t) { + return 2 * t * (1 - t); + } + + function QB3(t) { + return (1 - t) * (1 - t); + } + + var x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct); + var y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct); + + return { + x: x, + y: y + }; + }; + + this.getPointOnEllipticalArc = function (cx, cy, rx, ry, theta, psi) { + var cosPsi = Math.cos(psi), sinPsi = Math.sin(psi); + var pt = { + x: rx * Math.cos(theta), + y: ry * Math.sin(theta) + }; + return { + x: cx + (pt.x * cosPsi - pt.y * sinPsi), + y: cy + (pt.x * sinPsi + pt.y * cosPsi) + }; + }; + + this.calcLength = function (x, y, cmd, points) { + var len, p1, p2, t; + + switch (cmd) { + case 'L': + return this.getLineLength(x, y, points[0], points[1]); + case 'C': + // Approximates by breaking curve into 100 line segments + len = 0.0; + p1 = this.getPointOnCubicBezier(0, x, y, points[0], points[1], points[2], points[3], points[4], points[5]); + for (t = 0.01; t <= 1; t += 0.01) { + p2 = this.getPointOnCubicBezier(t, x, y, points[0], points[1], points[2], points[3], points[4], points[5]); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + return len; + case 'Q': + // Approximates by breaking curve into 100 line segments + len = 0.0; + p1 = this.getPointOnQuadraticBezier(0, x, y, points[0], points[1], points[2], points[3]); + for (t = 0.01; t <= 1; t += 0.01) { + p2 = this.getPointOnQuadraticBezier(t, x, y, points[0], points[1], points[2], points[3]); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + return len; + case 'A': + // Approximates by breaking curve into line segments + len = 0.0; + var start = points[4]; + // 4 = theta + var dTheta = points[5]; + // 5 = dTheta + var end = points[4] + dTheta; + var inc = Math.PI / 180.0; + // 1 degree resolution + if (Math.abs(start - end) < inc) { + inc = Math.abs(start - end); + } + // Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi + p1 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0); + if (dTheta < 0) {// clockwise + for (t = start - inc; t > end; t -= inc) { + p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + else {// counter-clockwise + for (t = start + inc; t < end; t += inc) { + p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + + return len; + } + + return 0; + }; + + this.dataArray = this.parsePathData(pathElement); + }; + svg.Element.textPath.prototype = new svg.Element.TextElementBase; + + // image element + svg.Element.image = function (node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + var href = this.getHrefAttribute().value; + if (href == '') { return; } + var isSvg = href.match(/\.svg$/); + + svg.Images.push(this); + this.loaded = false; + if (!isSvg) { + this.img = doc.createElement('img'); + if (svg.opts['useCORS'] == true) { this.img.crossOrigin = 'Anonymous'; } + var self = this; + this.img.onload = function () { self.loaded = true; }; + this.img.onerror = function () { + svg.log('ERROR: image "' + href + '" not found'); + self.loaded = true; + }; + this.img.src = href; + } else { + this.img = svg.ajax(href); + this.loaded = true; + } + + this.renderChildren = function (ctx) { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + if (width == 0 || height == 0) return; + + ctx.save(); + if (isSvg) { + ctx.drawSvg(this.img, x, y, width, height); + } else { + ctx.translate(x, y); + svg.AspectRatio(ctx, + this.attribute('preserveAspectRatio').value, + width, + this.img.width, + height, + this.img.height, + 0, + 0); + if (self.loaded) { + if (this.img.complete === undefined || this.img.complete) { + ctx.drawImage(this.img, 0, 0); + } + } + } + ctx.restore(); + }; + + this.getBoundingBox = function () { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + return new svg.BoundingBox(x, y, x + width, y + height); + }; + }; + svg.Element.image.prototype = new svg.Element.RenderedElementBase; + + // group element + svg.Element.g = function (node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.getBoundingBox = function (ctx) { + var bb = new svg.BoundingBox(); + for (var i = 0; i < this.children.length; i++) { + bb.addBoundingBox(this.children[i].getBoundingBox(ctx)); + } + return bb; + }; + }; + svg.Element.g.prototype = new svg.Element.RenderedElementBase; + + // symbol element + svg.Element.symbol = function (node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.render = function (/* ctx */) { + // NO RENDER + }; + }; + svg.Element.symbol.prototype = new svg.Element.RenderedElementBase; + + svg.ParseExternalUrl = function(url) { + // single quotes [2] + // v double quotes [3] + // v v no quotes [4] + // v v v + var urlMatch = url.match(/url\(('([^']+)'|"([^"]+)"|([^'"\)]+))\)/) || []; + return urlMatch[2] || urlMatch[3] || urlMatch[4]; + }; + + // style element + svg.Element.style = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + // text, or spaces then CDATA + var css = ''; + for (var i = 0; i < node.childNodes.length; i++) { + css += node.childNodes[i].data; + } + css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // remove comments + css = svg.compressSpaces(css); // replace whitespace + var cssDefs = css.split('}'); + for (var i = 0; i < cssDefs.length; i++) { + if (svg.trim(cssDefs[i]) != '') { + var cssDef = cssDefs[i].split('{'); + var cssClasses = cssDef[0].split(','); + var cssProps = cssDef[1].split(';'); + for (var j = 0; j < cssClasses.length; j++) { + var cssClass = svg.trim(cssClasses[j]); + if (cssClass != '') { + var props = svg.Styles[cssClass] || {}; + for (var k = 0; k < cssProps.length; k++) { + var prop = cssProps[k].indexOf(':'); + var name = cssProps[k].substr(0, prop); + var value = cssProps[k].substr(prop + 1, cssProps[k].length - prop); + if (name != null && value != null) { + props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value)); + } + } + svg.Styles[cssClass] = props; + svg.StylesSpecificity[cssClass] = getSelectorSpecificity(cssClass); + if (cssClass == '@font-face' && !nodeEnv) { + var fontFamily = props['font-family'].value.replace(/"/g, ''); + var srcs = props['src'].value.split(','); + for (var s = 0; s < srcs.length; s++) { + if (srcs[s].indexOf('format("svg")') > 0) { + var url = svg.ParseExternalUrl(srcs[s]); + if (url) { + var doc = svg.parseXml(svg.ajax(url)); + var fonts = doc.getElementsByTagName('font'); + for (var f = 0; f < fonts.length; f++) { + var font = svg.CreateElement(fonts[f]); + svg.Definitions[fontFamily] = font; + } + } + } + } + } + } + } + } + } + }; + svg.Element.style.prototype = new svg.Element.ElementBase; + + // use element + svg.Element.use = function (node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.baseSetContext = this.setContext; + this.setContext = function (ctx) { + this.baseSetContext(ctx); + if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0); + if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y')); + }; + + var element = this.getHrefAttribute().getDefinition(); + + this.path = function (ctx) { + if (element != null) element.path(ctx); + }; + + this.elementTransform = function () { + if (element != null && element.style('transform', false, true).hasValue()) { + return new svg.Transform(element.style('transform', false, true).value); + } + }; + + this.getBoundingBox = function (ctx) { + if (element != null) return element.getBoundingBox(ctx); + }; + + this.renderChildren = function (ctx) { + if (element != null) { + var tempSvg = element; + if (element.type == 'symbol') { + // render me using a temporary svg element in symbol cases (http://www.w3.org/TR/SVG/struct.html#UseElement) + tempSvg = new svg.Element.svg(); + tempSvg.type = 'svg'; + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', element.attribute('viewBox').value); + tempSvg.attributes['preserveAspectRatio'] = new svg.Property('preserveAspectRatio', element.attribute('preserveAspectRatio').value); + tempSvg.attributes['overflow'] = new svg.Property('overflow', element.attribute('overflow').value); + tempSvg.children = element.children; + } + if (tempSvg.type == 'svg') { + // if symbol or svg, inherit width/height from me + if (this.attribute('width').hasValue()) tempSvg.attributes['width'] = new svg.Property('width', this.attribute('width').value); + if (this.attribute('height').hasValue()) tempSvg.attributes['height'] = new svg.Property('height', this.attribute('height').value); + } + var oldParent = tempSvg.parent; + tempSvg.parent = null; + tempSvg.render(ctx); + tempSvg.parent = oldParent; + } + }; + }; + svg.Element.use.prototype = new svg.Element.RenderedElementBase; + + // mask element + svg.Element.mask = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.apply = function (ctx, element) { + // render as temp svg + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + + if (width == 0 && height == 0) { + var bb = new svg.BoundingBox(); + for (var i = 0; i < this.children.length; i++) { + bb.addBoundingBox(this.children[i].getBoundingBox(ctx)); + } + var x = Math.floor(bb.x1); + var y = Math.floor(bb.y1); + var width = Math.floor(bb.width()); + var height = Math.floor(bb.height()); + } + + // temporarily remove mask to avoid recursion + var mask = element.style('mask').value; + element.style('mask').value = ''; + + var cMask = createCanvas(x + width, y + height); + var maskCtx = cMask.getContext('2d'); + svg.SetDefaults(maskCtx); + this.renderChildren(maskCtx); + + // convert mask to alpha with a fake node + // TODO: refactor out apply from feColorMatrix + var cm = new svg.Element.feColorMatrix({ + nodeType: 1, + childNodes: [], + attributes: [ + { nodeName: 'type', value: 'luminanceToAlpha' }, + { nodeName: 'includeOpacity', value: 'true' }, + ] + }); + cm.apply(maskCtx, 0, 0, x + width, y + height); + + var c = createCanvas(x + width, y + height); + var tempCtx = c.getContext('2d'); + svg.SetDefaults(tempCtx); + element.render(tempCtx); + tempCtx.globalCompositeOperation = 'destination-in'; + tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat'); + tempCtx.fillRect(0, 0, x + width, y + height); + + ctx.fillStyle = tempCtx.createPattern(c, 'no-repeat'); + ctx.fillRect(0, 0, x + width, y + height); + + // reassign mask + element.style('mask').value = mask; + }; + + this.render = function (/* ctx */) { + // NO RENDER + }; + }; + svg.Element.mask.prototype = new svg.Element.ElementBase; + + // clip element + svg.Element.clipPath = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.apply = function (ctx) { + var hasContext2D = (typeof CanvasRenderingContext2D !== 'undefined'); + var oldBeginPath = ctx.beginPath; + var oldClosePath = ctx.closePath; + if (hasContext2D) { + CanvasRenderingContext2D.prototype.beginPath = function () { }; + CanvasRenderingContext2D.prototype.closePath = function () { }; + } + + oldBeginPath.call(ctx); + for (var i = 0; i < this.children.length; i++) { + var child = this.children[i]; + if (typeof child.path != 'undefined') { + var transform = typeof child.elementTransform != 'undefined' && child.elementTransform(); // handle + if (!transform && child.style('transform', false, true).hasValue()) { + transform = new svg.Transform(child.style('transform', false, true).value); + } + if (transform) { + transform.apply(ctx); + } + child.path(ctx); + if (hasContext2D) { + CanvasRenderingContext2D.prototype.closePath = oldClosePath; + } + if (transform) { transform.unapply(ctx); } + } + } + oldClosePath.call(ctx); + ctx.clip(); + if (hasContext2D) { + CanvasRenderingContext2D.prototype.beginPath = oldBeginPath; + CanvasRenderingContext2D.prototype.closePath = oldClosePath; + } + }; + + this.render = function (/* ctx */) { + // NO RENDER + }; + }; + svg.Element.clipPath.prototype = new svg.Element.ElementBase; + + // filters + svg.Element.filter = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.apply = function (ctx, element) { + // render as temp svg + var bb = element.getBoundingBox(ctx); + var x = Math.floor(bb.x1); + var y = Math.floor(bb.y1); + var width = Math.floor(bb.width()); + var height = Math.floor(bb.height()); + + // temporarily remove filter to avoid recursion + var filter = element.style('filter').value; + element.style('filter').value = ''; + + var px = 0, + py = 0; + for (var i = 0; i < this.children.length; i++) { + var efd = this.children[i].extraFilterDistance || 0; + px = Math.max(px, efd); + py = Math.max(py, efd); + } + + var c = createCanvas(width + 2 * px, height + 2 * py); + var tempCtx = c.getContext('2d'); + svg.SetDefaults(tempCtx); + tempCtx.translate(-x + px, -y + py); + element.render(tempCtx); + + // apply filters + for (var i = 0; i < this.children.length; i++) { + if (typeof this.children[i].apply == 'function') { + this.children[i].apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py); + } + } + + // render on me + ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py); + + // reassign filter + element.style('filter', true).value = filter; + }; + + this.render = function (/* ctx */) { + // NO RENDER + }; + }; + svg.Element.filter.prototype = new svg.Element.ElementBase; + + svg.Element.feDropShadow = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + this.addStylesFromStyleDefinition(); + + this.apply = function (/* ctx, x, y, width, height */) { + // TODO: implement + }; + }; + svg.Element.feDropShadow.prototype = new svg.Element.ElementBase; + + svg.Element.feMorphology = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.apply = function (/* ctx, x, y, width, height */) { + // TODO: implement + }; + }; + svg.Element.feMorphology.prototype = new svg.Element.ElementBase; + + svg.Element.feComposite = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.apply = function (/* ctx, x, y, width, height */) { + // TODO: implement + }; + }; + svg.Element.feComposite.prototype = new svg.Element.ElementBase; + + svg.Element.feColorMatrix = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + var matrix = svg.ToNumberArray(this.attribute('values').value); + switch (this.attribute('type').valueOrDefault('matrix')) { // http://www.w3.org/TR/SVG/filters.html#feColorMatrixElement + case 'saturate': + var s = matrix[0]; + matrix = [0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0, + 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0, + 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1 + ]; + break; + case 'hueRotate': + var a = matrix[0] * Math.PI / 180.0; + var c = function (m1, m2, m3) { return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; }; + matrix = [c(0.213, 0.787, -0.213), c(0.715, -0.715, -0.715), c(0.072, -0.072, 0.928), 0, 0, + c(0.213, -0.213, 0.143), c(0.715, 0.285, 0.140), c(0.072, -0.072, -0.283), 0, 0, + c(0.213, -0.213, -0.787), c(0.715, -0.715, 0.715), c(0.072, 0.928, 0.072), 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1 + ]; + break; + case 'luminanceToAlpha': + matrix = [0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0.2125, 0.7154, 0.0721, 0, 0, + 0, 0, 0, 0, 1 + ]; + break; + } + + function imGet(img, x, y, width, height, rgba) { + return img[y * width * 4 + x * 4 + rgba]; + } + + function imSet(img, x, y, width, height, rgba, val) { + img[y * width * 4 + x * 4 + rgba] = val; + } + + function m(i, v) { + var mi = matrix[i]; + return mi * (mi < 0 ? v - 255 : v); + } + + var includeOpacity = this.attribute('includeOpacity').hasValue(); + this.apply = function (ctx, x, y, width, height) { + // assuming x==0 && y==0 for now + var srcData = ctx.getImageData(0, 0, width, height); + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + var r = imGet(srcData.data, x, y, width, height, 0); + var g = imGet(srcData.data, x, y, width, height, 1); + var b = imGet(srcData.data, x, y, width, height, 2); + var a = imGet(srcData.data, x, y, width, height, 3); + var nr = m(0, r) + m(1, g) + m(2, b) + m(3, a) + m(4, 1); + var ng = m(5, r) + m(6, g) + m(7, b) + m(8, a) + m(9, 1); + var nb = m(10, r) + m(11, g) + m(12, b) + m(13, a) + m(14, 1); + var na = m(15, r) + m(16, g) + m(17, b) + m(18, a) + m(19, 1); + if (includeOpacity) { + nr = ng = nb = 0; + na *= a / 255; + } + imSet(srcData.data, x, y, width, height, 0, nr); + imSet(srcData.data, x, y, width, height, 1, ng); + imSet(srcData.data, x, y, width, height, 2, nb); + imSet(srcData.data, x, y, width, height, 3, na); + } + } + ctx.clearRect(0, 0, width, height); + ctx.putImageData(srcData, 0, 0); + }; + }; + svg.Element.feColorMatrix.prototype = new svg.Element.ElementBase; + + svg.Element.feGaussianBlur = function (node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.blurRadius = Math.floor(this.attribute('stdDeviation').numValue()); + this.extraFilterDistance = this.blurRadius; + + this.apply = function (ctx, x, y, width, height) { + if (!stackblurCanvas || typeof stackblurCanvas.canvasRGBA === 'undefined') { + svg.log('ERROR: StackBlur.js must be included for blur to work'); + return; + } + + // StackBlur requires canvas be on document + ctx.canvas.id = svg.UniqueId(); + { + ctx.canvas.style.display = 'none'; + doc.body.appendChild(ctx.canvas); + } + stackblurCanvas.canvasRGBA(ctx.canvas, x, y, width, height, this.blurRadius); + { + doc.body.removeChild(ctx.canvas); + } + }; + }; + svg.Element.feGaussianBlur.prototype = new svg.Element.ElementBase; + + // title element, do nothing + svg.Element.title = function (/* node */) { }; + svg.Element.title.prototype = new svg.Element.ElementBase; + + // desc element, do nothing + svg.Element.desc = function (/* node */) { }; + svg.Element.desc.prototype = new svg.Element.ElementBase; + + svg.Element.MISSING = function (node) { + svg.log('ERROR: Element \'' + node.nodeName + '\' not yet implemented.'); + }; + svg.Element.MISSING.prototype = new svg.Element.ElementBase; + + // element factory + svg.CreateElement = function (node) { + var className = node.nodeName.replace(/^[^:]+:/, ''); // remove namespace + className = className.replace(/\-/g, ''); // remove dashes + var e = null; + if (typeof svg.Element[className] != 'undefined') { + e = new svg.Element[className](node); + } else { + e = new svg.Element.MISSING(node); + } + + e.type = node.nodeName; + return e; + }; + + // load from url + svg.load = function (ctx, url) { + svg.loadXml(ctx, svg.ajax(url)); + }; + + // load from xml + svg.loadXml = function (ctx, xml) { + svg.loadXmlDoc(ctx, svg.parseXml(xml)); + }; + + svg.loadXmlDoc = function (ctx, dom) { + svg.init(ctx); + + var mapXY = function (p) { + var e = ctx.canvas; + while (e) { + p.x -= e.offsetLeft; + p.y -= e.offsetTop; + e = e.offsetParent; + } + if (windowEnv.scrollX) p.x += windowEnv.scrollX; + if (windowEnv.scrollY) p.y += windowEnv.scrollY; + return p; + }; + + // bind mouse + if (svg.opts['ignoreMouse'] != true) { + ctx.canvas.onclick = function (e) { + var p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY)); + svg.Mouse.onclick(p.x, p.y); + }; + ctx.canvas.onmousemove = function (e) { + var p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY)); + svg.Mouse.onmousemove(p.x, p.y); + }; + } + + var e = svg.CreateElement(dom.documentElement); + e.root = true; + e.addStylesFromStyleDefinition(); + + // render loop + var isFirstRender = true; + var draw = function () { + svg.ViewPort.Clear(); + if (ctx.canvas.parentNode) { + svg.ViewPort.SetCurrent(ctx.canvas.parentNode.clientWidth, ctx.canvas.parentNode.clientHeight); + } else { + svg.ViewPort.SetCurrent(defaultClientWidth, defaultClientHeight); + } + + if (svg.opts['ignoreDimensions'] != true && (isFirstRender || (svg.opts['scaleWidth'] == null && svg.opts['scaleHeight'] == null))) { + // set canvas size + if (e.style('width').hasValue()) { + ctx.canvas.width = e.style('width').toPixels('x'); + if (ctx.canvas.style) { ctx.canvas.style.width = ctx.canvas.width + 'px'; } + } + if (e.style('height').hasValue()) { + ctx.canvas.height = e.style('height').toPixels('y'); + if (ctx.canvas.style) { ctx.canvas.style.height = ctx.canvas.height + 'px'; } + } + } + var cWidth = ctx.canvas.clientWidth || ctx.canvas.width; + var cHeight = ctx.canvas.clientHeight || ctx.canvas.height; + if (svg.opts['ignoreDimensions'] == true && e.style('width').hasValue() && e.style('height').hasValue()) { + cWidth = e.style('width').toPixels('x'); + cHeight = e.style('height').toPixels('y'); + } + svg.ViewPort.SetCurrent(cWidth, cHeight); + + if (svg.opts['scaleWidth'] != null || svg.opts['scaleHeight'] != null) { + var xRatio = null, + yRatio = null, + viewBox = svg.ToNumberArray(e.attribute('viewBox').value); + + if (svg.opts['scaleWidth'] != null) { + if (e.attribute('width').hasValue()) xRatio = e.attribute('width').toPixels('x') / svg.opts['scaleWidth']; + else if (!isNaN(viewBox[2])) xRatio = viewBox[2] / svg.opts['scaleWidth']; + } + + if (svg.opts['scaleHeight'] != null) { + if (e.attribute('height').hasValue()) yRatio = e.attribute('height').toPixels('y') / svg.opts['scaleHeight']; + else if (!isNaN(viewBox[3])) yRatio = viewBox[3] / svg.opts['scaleHeight']; + } + + if (xRatio == null) { xRatio = yRatio; } + if (yRatio == null) { yRatio = xRatio; } + + e.attribute('width', true).value = svg.opts['scaleWidth']; + e.attribute('height', true).value = svg.opts['scaleHeight']; + e.style('transform', true, true).value += ' scale(' + (1.0 / xRatio) + ',' + (1.0 / yRatio) + ')'; + } + if (svg.opts['offsetX'] != null) e.attribute('x', true).value = svg.opts['offsetX'] * xRatio; + if (svg.opts['offsetY'] != null) e.attribute('y', true).value = svg.opts['offsetY'] * yRatio; + + // clear and render + if (svg.opts['ignoreClear'] != true) { + ctx.clearRect(0, 0, cWidth, cHeight); + } + e.render(ctx); + if (isFirstRender) { + isFirstRender = false; + if (typeof svg.opts['renderCallback'] == 'function') svg.opts['renderCallback'](dom); + } + }; + + var waitingForImages = true; + if (svg.ImagesLoaded()) { + waitingForImages = false; + draw(); + } + { + //In node, in the most cases, we don't need the animation listener. + svg.intervalID = setInterval(function () { + var needUpdate = false; + + if (waitingForImages && svg.ImagesLoaded()) { + waitingForImages = false; + needUpdate = true; + } + + // need update from mouse events? + if (svg.opts['ignoreMouse'] != true) { + needUpdate = needUpdate || svg.Mouse.hasEvents(); + } + + // need update from animations? + if (svg.opts['ignoreAnimation'] != true) { + for (var i = 0; i < svg.Animations.length; i++) { + var needAnimationUpdate = svg.Animations[i].update(1000 / svg.FRAMERATE); + needUpdate = needUpdate || needAnimationUpdate; + } + } + + // need update from redraw? + if (typeof svg.opts['forceRedraw'] == 'function') { + if (svg.opts['forceRedraw']() == true) needUpdate = true; + } + + // render if needed + if (needUpdate) { + draw(); + svg.Mouse.runEvents(); // run and clear our events + } + }, 1000 / svg.FRAMERATE); + } + }; + + svg.stop = function () { + if (svg.intervalID) { + clearInterval(svg.intervalID); + } + }; + + svg.Mouse = new (function () { + this.events = []; + this.hasEvents = function () { return this.events.length != 0; }; + + this.onclick = function (x, y) { + this.events.push({ + type: 'onclick', + x: x, + y: y, + run: function (e) { if (e.onclick) e.onclick(); } + }); + }; + + this.onmousemove = function (x, y) { + this.events.push({ + type: 'onmousemove', + x: x, + y: y, + run: function (e) { if (e.onmousemove) e.onmousemove(); } + }); + }; + + this.eventElements = []; + + this.checkPath = function (element, ctx) { + for (var i = 0; i < this.events.length; i++) { + var e = this.events[i]; + if (ctx.isPointInPath && ctx.isPointInPath(e.x, e.y)) this.eventElements[i] = element; + } + }; + + this.checkBoundingBox = function (element, bb) { + if (!bb) { + return; + } + + for (var i = 0; i < this.events.length; i++) { + var e = this.events[i]; + if (bb.isPointInBox(e.x, e.y)) this.eventElements[i] = element; + } + }; + + this.runEvents = function () { + svg.ctx.canvas.style.cursor = ''; + + for (var i = 0; i < this.events.length; i++) { + var e = this.events[i]; + var element = this.eventElements[i]; + while (element) { + e.run(element); + element = element.parent; + } + } + + // done running, clear + this.events = []; + this.eventElements = []; + }; + }); + + return svg; + } + + if (typeof CanvasRenderingContext2D != 'undefined') { + CanvasRenderingContext2D.prototype.drawSvg = function (s, dx, dy, dw, dh, opts) { + var cOpts = { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true, + ignoreClear: true, + offsetX: dx, + offsetY: dy, + scaleWidth: dw, + scaleHeight: dh + }; + + for (var prop in opts) { + if (opts.hasOwnProperty(prop)) { + cOpts[prop] = opts[prop]; + } + } + canvg(this.canvas, s, cOpts); + }; + } + + // for tests + canvg._build = build; + + module.exports = canvg; + }); + + return canvg_1; + +}))); diff --git a/src/icon.js b/src/icon.js new file mode 100644 index 0000000..6f49db5 --- /dev/null +++ b/src/icon.js @@ -0,0 +1,30 @@ +class Icon { + constructor(url) { + this.url = url; + this.svgContent = ''; + this.svgLoaded = false; + this.load() + } + + async load() { + return fetch(this.url) + .then(response => response.text()) + .then(svgContent => { + this.svgContent = svgContent; + this.svgLoaded = true; + }) + .catch(error => { + console.error('Error loading SVG:', error); + this.svgLoaded = false; + }); + } + + render(ctx, x, y, w, h, color) { + if (this.svgLoaded) { + ctx.fillStyle = color + ctx.drawSvg(this.svgContent, x, y, w, h) + } + } +} + +export { Icon }; \ No newline at end of file diff --git a/src/index.html b/src/index.html index 545adc4..aa67aa8 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,10 @@ + + + + diff --git a/src/main.js b/src/main.js index 95ff781..427bdd6 100644 --- a/src/main.js +++ b/src/main.js @@ -4,7 +4,8 @@ import { Bezier } from "/bezier.js"; import { Quadtree } from './quadtree.js'; import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js'; import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint, hslToRgb, drawCheckerboardBackground, hexToHsl, hsvToRgb, hexToHsv, rgbToHex, clamp, drawBorderedRect, drawCenteredText, drawHorizontallyCenteredText } from './utils.js'; -import { backgroundColor, darkMode, foregroundColor, frameWidth, gutterHeight, highlight, layerHeight, layerWidth, scrubberColor, shade, shadow } from './styles.js'; +import { backgroundColor, darkMode, foregroundColor, frameWidth, gutterHeight, highlight, iconSize, labelColor, layerHeight, layerWidth, scrubberColor, shade, shadow } from './styles.js'; +import { Icon } from './icon.js'; const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs; const { open: openFileDialog, @@ -404,8 +405,6 @@ let actions = { }, execute: async (action) => { let imageObject = new GraphicsObject(action.objectUuid) - // let img = pointerList[action.img] - let img = new Image(); function loadImage(src) { return new Promise((resolve, reject) => { let img = new Image(); @@ -414,7 +413,7 @@ let actions = { img.src = src; // Start loading the image }); } - img = await loadImage(action.src) + let img = await loadImage(action.src) console.log(img.crossOrigin) // img.onload = function() { let ct = { @@ -3597,6 +3596,11 @@ function timeline() { let timeline_cvs = document.createElement("canvas") timeline_cvs.className = "timeline" + // Load icons for show/hide layer + timeline_cvs.icons = {} + timeline_cvs.icons.eye_fill = new Icon('assets/eye-fill.svg'); + timeline_cvs.icons.eye_slash = new Icon('assets/eye-slash.svg'); + // Variable to store the last time updateTimelineCanvasSize was called let lastResizeTime = 0; @@ -3651,7 +3655,20 @@ function timeline() { mouse.y -= gutterHeight let l = Math.floor(mouse.y / layerHeight) if (l < context.activeObject.layers.length) { - context.activeObject.currentLayer = context.activeObject.layers.length - (l+1) + let i = context.activeObject.layers.length - (l+1) + mouse.y -= l*layerHeight + if ( + mouse.x > layerWidth - iconSize - 5 && + mouse.x < layerWidth - 5 && + mouse.y > 0.5 * (layerHeight - iconSize) && + mouse.y < 0.5 * (layerHeight + iconSize) + ) { + context.activeObject.layers[i].visible = !context.activeObject.layers[i].visible + updateUI() + updateMenu() + } else { + context.activeObject.currentLayer = i + } } } updateLayers() @@ -3975,7 +3992,7 @@ function updateLayers() { ctx.rect(layerWidth,0,width-layerWidth,height) ctx.clip() ctx.translate(layerWidth - offsetX, 0) - ctx.fillStyle = darkMode ? "white" : "black" + ctx.fillStyle = labelColor for (let j=Math.floor(offsetX / (5 * frameWidth)) * 5; j255?255:this.r,this.g=this.g<0||isNaN(this.g)?0:this.g>255?255:this.g,this.b=this.b<0||isNaN(this.b)?0:this.b>255?255:this.b,this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"},this.toHex=function(){var e=this.r.toString(16),f=this.g.toString(16),a=this.b.toString(16);return 1==e.length&&(e="0"+e),1==f.length&&(f="0"+f),1==a.length&&(a="0"+a),"#"+e+f+a},this.getHelpXML=function(){for(var e=new Array,r=0;r "+s.toRGB()+" -> "+s.toHex());o.appendChild(c),o.appendChild(b),l.appendChild(o)}catch(e){}return l}}"undefined"!=typeof define&&define.amd?define(function(){return f}):"undefined"!=typeof module&&module.exports&&(module.exports=f),e.RGBColor=f}("undefined"!=typeof window?window:this); \ No newline at end of file diff --git a/src/stackblur.min.js b/src/stackblur.min.js new file mode 100644 index 0000000..8d273ba --- /dev/null +++ b/src/stackblur.min.js @@ -0,0 +1 @@ +!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.StackBlur=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g>T,0!=F?(F=255/F,H[o]=(q*S>>T)*F,H[o+1]=(r*S>>T)*F,H[o+2]=(s*S>>T)*F):H[o]=H[o+1]=H[o+2]=0,q-=u,r-=v,s-=w,t-=x,u-=Q.r,v-=Q.g,w-=Q.b,x-=Q.a,m=p+((m=g+f+1)>T,F>0?(F=255/F,H[m]=(q*S>>T)*F,H[m+1]=(r*S>>T)*F,H[m+2]=(s*S>>T)*F):H[m]=H[m+1]=H[m+2]=0,q-=u,r-=v,s-=w,t-=x,u-=Q.r,v-=Q.g,w-=Q.b,x-=Q.a,m=g+((m=h+L)>P,D[o+1]=r*O>>P,D[o+2]=s*O>>P,q-=t,r-=u,s-=v,t-=M.r,u-=M.g,v-=M.b,m=p+((m=g+f+1)>P,D[m+1]=r*O>>P,D[m+2]=s*O>>P,q-=t,r-=u,s-=v,t-=M.r,u-=M.g,v-=M.b,m=g+((m=h+H) 0 && arguments[0] !== void 0 ? arguments[0] : {}; + const preset = { + window: null, + ignoreAnimation: true, + ignoreMouse: true, + DOMParser: DOMParserFallback, + createCanvas (width, height) { + return new OffscreenCanvas(width, height); + }, + async createImage (url) { + const response = await fetch(url); + const blob = await response.blob(); + const img = await createImageBitmap(blob); + return img; + } + }; + if (typeof globalThis.DOMParser !== 'undefined' || typeof DOMParserFallback === 'undefined') { + Reflect.deleteProperty(preset, 'DOMParser'); + } + return preset; + } + + /* eslint-disable @typescript-eslint/no-explicit-any */ /** + * Options preset for `node-canvas`. + * @param config - Preset requirements. + * @param config.DOMParser - XML/HTML parser from string into DOM Document. + * @param config.canvas - `node-canvas` exports. + * @param config.fetch - WHATWG-compatible `fetch` function. + * @returns Preset object. + */ function node(param) { + let { DOMParser, canvas, fetch } = param; + return { + window: null, + ignoreAnimation: true, + ignoreMouse: true, + DOMParser, + fetch, + createCanvas: canvas.createCanvas, + createImage: canvas.loadImage + }; + } + + var index = /*#__PURE__*/Object.freeze({ + __proto__: null, + offscreen: offscreen, + node: node + }); + + /** + * HTML-safe compress white-spaces. + * @param str - String to compress. + * @returns String. + */ function compressSpaces(str) { + return str.replace(/(?!\u3000)\s+/gm, ' '); + } + /** + * HTML-safe left trim. + * @param str - String to trim. + * @returns String. + */ function trimLeft(str) { + return str.replace(/^[\n \t]+/, ''); + } + /** + * HTML-safe right trim. + * @param str - String to trim. + * @returns String. + */ function trimRight(str) { + return str.replace(/[\n \t]+$/, ''); + } + /** + * String to numbers array. + * @param str - Numbers string. + * @returns Numbers array. + */ function toNumbers(str) { + const matches = str.match(/-?(\d+(?:\.\d*(?:[eE][+-]?\d+)?)?|\.\d+)(?=\D|$)/gm); + return matches ? matches.map(parseFloat) : []; + } + /** + * String to matrix value. + * @param str - Numbers string. + * @returns Matrix value. + */ function toMatrixValue(str) { + const numbers = toNumbers(str); + const matrix = [ + numbers[0] || 0, + numbers[1] || 0, + numbers[2] || 0, + numbers[3] || 0, + numbers[4] || 0, + numbers[5] || 0 + ]; + return matrix; + } + // Microsoft Edge fix + const allUppercase = /^[A-Z-]+$/; + /** + * Normalize attribute name. + * @param name - Attribute name. + * @returns Normalized attribute name. + */ function normalizeAttributeName(name) { + if (allUppercase.test(name)) { + return name.toLowerCase(); + } + return name; + } + /** + * Parse external URL. + * @param url - CSS url string. + * @returns Parsed URL. + */ function parseExternalUrl(url) { + // single quotes [2] + // v double quotes [3] + // v v no quotes [4] + // v v v + const urlMatch = /url\(('([^']+)'|"([^"]+)"|([^'")]+))\)/.exec(url); + if (!urlMatch) { + return ''; + } + return urlMatch[2] || urlMatch[3] || urlMatch[4] || ''; + } + /** + * Transform floats to integers in rgb colors. + * @param color - Color to normalize. + * @returns Normalized color. + */ function normalizeColor(color) { + if (!color.startsWith('rgb')) { + return color; + } + let rgbParts = 3; + const normalizedColor = color.replace(/\d+(\.\d+)?/g, (num, isFloat)=>rgbParts-- && isFloat ? String(Math.round(parseFloat(num))) : num); + return normalizedColor; + } + + // slightly modified version of https://github.com/keeganstreet/specificity/blob/master/specificity.js + const attributeRegex = /(\[[^\]]+\])/g; + const idRegex = /(#[^\s+>~.[:]+)/g; + const classRegex = /(\.[^\s+>~.[:]+)/g; + const pseudoElementRegex = /(::[^\s+>~.[:]+|:first-line|:first-letter|:before|:after)/gi; + const pseudoClassWithBracketsRegex = /(:[\w-]+\([^)]*\))/gi; + const pseudoClassRegex = /(:[^\s+>~.[:]+)/g; + const elementRegex = /([^\s+>~.[:]+)/g; + function findSelectorMatch(selector, regex) { + const matches = regex.exec(selector); + if (!matches) { + return [ + selector, + 0 + ]; + } + return [ + selector.replace(regex, ' '), + matches.length + ]; + } + /** + * Measure selector specificity. + * @param selector - Selector to measure. + * @returns Specificity. + */ function getSelectorSpecificity(selector) { + const specificity = [ + 0, + 0, + 0 + ]; + let currentSelector = selector.replace(/:not\(([^)]*)\)/g, ' $1 ').replace(/{[\s\S]*/gm, ' '); + let delta = 0; + [currentSelector, delta] = findSelectorMatch(currentSelector, attributeRegex); + specificity[1] += delta; + [currentSelector, delta] = findSelectorMatch(currentSelector, idRegex); + specificity[0] += delta; + [currentSelector, delta] = findSelectorMatch(currentSelector, classRegex); + specificity[1] += delta; + [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoElementRegex); + specificity[2] += delta; + [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoClassWithBracketsRegex); + specificity[1] += delta; + [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoClassRegex); + specificity[1] += delta; + currentSelector = currentSelector.replace(/[*\s+>~]/g, ' ').replace(/[#.]/g, ' '); + [currentSelector, delta] = findSelectorMatch(currentSelector, elementRegex) // lgtm [js/useless-assignment-to-local] + ; + specificity[2] += delta; + return specificity.join(''); + } + + const PSEUDO_ZERO = .00000001; + /** + * Vector magnitude. + * @param v + * @returns Number result. + */ function vectorMagnitude(v) { + return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); + } + /** + * Ratio between two vectors. + * @param u + * @param v + * @returns Number result. + */ function vectorsRatio(u, v) { + return (u[0] * v[0] + u[1] * v[1]) / (vectorMagnitude(u) * vectorMagnitude(v)); + } + /** + * Angle between two vectors. + * @param u + * @param v + * @returns Number result. + */ function vectorsAngle(u, v) { + return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vectorsRatio(u, v)); + } + function CB1(t) { + return t * t * t; + } + function CB2(t) { + return 3 * t * t * (1 - t); + } + function CB3(t) { + return 3 * t * (1 - t) * (1 - t); + } + function CB4(t) { + return (1 - t) * (1 - t) * (1 - t); + } + function QB1(t) { + return t * t; + } + function QB2(t) { + return 2 * t * (1 - t); + } + function QB3(t) { + return (1 - t) * (1 - t); + } + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + var raf$1 = {exports: {}}; + + var performanceNow = {exports: {}}; + + // Generated by CoffeeScript 1.12.2 + (function() { + var getNanoSeconds, hrtime, loadTime, moduleLoadTime, nodeLoadTime, upTime; + if (typeof performance !== "undefined" && performance !== null && performance.now) { + performanceNow.exports = function() { + return performance.now(); + }; + } else if (typeof process !== "undefined" && process !== null && process.hrtime) { + performanceNow.exports = function() { + return (getNanoSeconds() - nodeLoadTime) / 1e6; + }; + hrtime = process.hrtime; + getNanoSeconds = function() { + var hr; + hr = hrtime(); + return hr[0] * 1e9 + hr[1]; + }; + moduleLoadTime = getNanoSeconds(); + upTime = process.uptime() * 1e9; + nodeLoadTime = moduleLoadTime - upTime; + } else if (Date.now) { + performanceNow.exports = function() { + return Date.now() - loadTime; + }; + loadTime = Date.now(); + } else { + performanceNow.exports = function() { + return new Date().getTime() - loadTime; + }; + loadTime = new Date().getTime(); + } + }).call(commonjsGlobal); + + var now = performanceNow.exports, root = typeof window === 'undefined' ? commonjsGlobal : window, vendors = [ + 'moz', + 'webkit' + ], suffix = 'AnimationFrame', raf = root['request' + suffix], caf = root['cancel' + suffix] || root['cancelRequest' + suffix]; + for(var i$1 = 0; !raf && i$1 < vendors.length; i$1++){ + raf = root[vendors[i$1] + 'Request' + suffix]; + caf = root[vendors[i$1] + 'Cancel' + suffix] || root[vendors[i$1] + 'CancelRequest' + suffix]; + } + // Some versions of FF have rAF but not cAF + if (!raf || !caf) { + var last = 0, id = 0, queue = [], frameDuration = 1000 / 60; + raf = function(callback) { + if (queue.length === 0) { + var _now = now(), next = Math.max(0, frameDuration - (_now - last)); + last = next + _now; + setTimeout(function() { + var cp = queue.slice(0); + // Clear queue here to prevent + // callbacks from appending listeners + // to the current frame's queue + queue.length = 0; + for(var i = 0; i < cp.length; i++){ + if (!cp[i].cancelled) { + try { + cp[i].callback(last); + } catch (e) { + setTimeout(function() { + throw e; + }, 0); + } + } + } + }, Math.round(next)); + } + queue.push({ + handle: ++id, + callback: callback, + cancelled: false + }); + return id; + }; + caf = function(handle) { + for(var i = 0; i < queue.length; i++){ + if (queue[i].handle === handle) { + queue[i].cancelled = true; + } + } + }; + } + raf$1.exports = function(fn) { + // Wrap in a new function to prevent + // `cancel` potentially being assigned + // to the native rAF function + return raf.call(root, fn); + }; + raf$1.exports.cancel = function() { + caf.apply(root, arguments); + }; + raf$1.exports.polyfill = function(object) { + if (!object) { + object = root; + } + object.requestAnimationFrame = raf; + object.cancelAnimationFrame = caf; + }; + + var requestAnimationFrame = raf$1.exports; + + /* + Based on rgbcolor.js by Stoyan Stefanov + http://www.phpied.com/rgb-color-parser-in-javascript/ + */ + + var rgbcolor = function(color_string) { + this.ok = false; + this.alpha = 1.0; + // strip any leading # + if (color_string.charAt(0) == '#') { + color_string = color_string.substr(1, 6); + } + color_string = color_string.replace(/ /g, ''); + color_string = color_string.toLowerCase(); + // before getting into regexps, try simple matches + // and overwrite the input + var simple_colors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred: 'cd5c5c', + indigo: '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + rebeccapurple: '663399', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' + }; + color_string = simple_colors[color_string] || color_string; + // emd of simple type-in colors + // array of color definition objects + var color_defs = [ + { + re: /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*((?:\d?\.)?\d)\)$/, + example: [ + 'rgba(123, 234, 45, 0.8)', + 'rgba(255,234,245,1.0)' + ], + process: function(bits) { + return [ + parseInt(bits[1]), + parseInt(bits[2]), + parseInt(bits[3]), + parseFloat(bits[4]) + ]; + } + }, + { + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + example: [ + 'rgb(123, 234, 45)', + 'rgb(255,234,245)' + ], + process: function(bits) { + return [ + parseInt(bits[1]), + parseInt(bits[2]), + parseInt(bits[3]) + ]; + } + }, + { + re: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, + example: [ + '#00ff00', + '336699' + ], + process: function(bits) { + return [ + parseInt(bits[1], 16), + parseInt(bits[2], 16), + parseInt(bits[3], 16) + ]; + } + }, + { + re: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, + example: [ + '#fb0', + 'f0f' + ], + process: function(bits) { + return [ + parseInt(bits[1] + bits[1], 16), + parseInt(bits[2] + bits[2], 16), + parseInt(bits[3] + bits[3], 16) + ]; + } + } + ]; + // search through the definitions to find a match + for(var i = 0; i < color_defs.length; i++){ + var re = color_defs[i].re; + var processor = color_defs[i].process; + var bits = re.exec(color_string); + if (bits) { + var channels = processor(bits); + this.r = channels[0]; + this.g = channels[1]; + this.b = channels[2]; + if (channels.length > 3) { + this.alpha = channels[3]; + } + this.ok = true; + } + } + // validate/cleanup values + this.r = this.r < 0 || isNaN(this.r) ? 0 : this.r > 255 ? 255 : this.r; + this.g = this.g < 0 || isNaN(this.g) ? 0 : this.g > 255 ? 255 : this.g; + this.b = this.b < 0 || isNaN(this.b) ? 0 : this.b > 255 ? 255 : this.b; + this.alpha = this.alpha < 0 ? 0 : this.alpha > 1.0 || isNaN(this.alpha) ? 1.0 : this.alpha; + // some getters + this.toRGB = function() { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + }; + this.toRGBA = function() { + return 'rgba(' + this.r + ', ' + this.g + ', ' + this.b + ', ' + this.alpha + ')'; + }; + this.toHex = function() { + var r = this.r.toString(16); + var g = this.g.toString(16); + var b = this.b.toString(16); + if (r.length == 1) r = '0' + r; + if (g.length == 1) g = '0' + g; + if (b.length == 1) b = '0' + b; + return '#' + r + g + b; + }; + // help + this.getHelpXML = function() { + var examples = new Array(); + // add regexps + for(var i = 0; i < color_defs.length; i++){ + var example = color_defs[i].example; + for(var j = 0; j < example.length; j++){ + examples[examples.length] = example[j]; + } + } + // add type-in colors + for(var sc in simple_colors){ + examples[examples.length] = sc; + } + var xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + for(var i = 0; i < examples.length; i++){ + try { + var list_item = document.createElement('li'); + var list_color = new RGBColor(examples[i]); + var example_div = document.createElement('div'); + example_div.style.cssText = 'margin: 3px; ' + 'border: 1px solid black; ' + 'background:' + list_color.toHex() + '; ' + 'color:' + list_color.toHex(); + example_div.appendChild(document.createTextNode('test')); + var list_item_value = document.createTextNode(' ' + examples[i] + ' -> ' + list_color.toRGB() + ' -> ' + list_color.toHex()); + list_item.appendChild(example_div); + list_item.appendChild(list_item_value); + xml.appendChild(list_item); + } catch (e) {} + } + return xml; + }; + }; + + class Property { + document; + name; + value; + static empty(document) { + return new Property(document, 'EMPTY', ''); + } + static textBaselineMapping = { + 'baseline': 'alphabetic', + 'before-edge': 'top', + 'text-before-edge': 'top', + 'middle': 'middle', + 'central': 'middle', + 'after-edge': 'bottom', + 'text-after-edge': 'bottom', + 'ideographic': 'ideographic', + 'alphabetic': 'alphabetic', + 'hanging': 'hanging', + 'mathematical': 'alphabetic' + }; + isNormalizedColor; + constructor(document, name, value){ + this.document = document; + this.name = name; + this.value = value; + this.isNormalizedColor = false; + } + split() { + let separator = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : ' '; + const { document, name } = this; + return compressSpaces(this.getString()).trim().split(separator).map((value)=>new Property(document, name, value)); + } + hasValue(zeroIsValue) { + const value = this.value; + return value !== null && value !== '' && (zeroIsValue || value !== 0) && typeof value !== 'undefined'; + } + isString(regexp) { + const { value } = this; + const result = typeof value === 'string'; + if (!result || !regexp) { + return result; + } + return regexp.test(value); + } + isUrlDefinition() { + return this.isString(/^url\(/); + } + isPixels() { + if (!this.hasValue()) { + return false; + } + const asString = this.getString(); + switch(true){ + case asString.endsWith('px'): + case /^[0-9]+$/.test(asString): + return true; + default: + return false; + } + } + setValue(value) { + this.value = value; + return this; + } + getValue(def) { + if (typeof def === 'undefined' || this.hasValue()) { + return this.value; + } + return def; + } + getNumber(def) { + if (!this.hasValue()) { + if (typeof def === 'undefined') { + return 0; + } + // @ts-expect-error Parse unknown value. + return parseFloat(def); + } + const { value } = this; + // @ts-expect-error Parse unknown value. + let n = parseFloat(value); + if (this.isString(/%$/)) { + n /= 100.0; + } + return n; + } + getString(def) { + if (typeof def === 'undefined' || this.hasValue()) { + return typeof this.value === 'undefined' ? '' : String(this.value); + } + return String(def); + } + getColor(def) { + let color = this.getString(def); + if (this.isNormalizedColor) { + return color; + } + this.isNormalizedColor = true; + color = normalizeColor(color); + this.value = color; + return color; + } + getDpi() { + return 96.0 // TODO: compute? + ; + } + getRem() { + return this.document.rootEmSize; + } + getEm() { + return this.document.emSize; + } + getUnits() { + return this.getString().replace(/[0-9.-]/g, ''); + } + getPixels(axisOrIsFontSize) { + let processPercent = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + if (!this.hasValue()) { + return 0; + } + const [axis, isFontSize] = typeof axisOrIsFontSize === 'boolean' ? [ + undefined, + axisOrIsFontSize + ] : [ + axisOrIsFontSize + ]; + const { viewPort } = this.document.screen; + switch(true){ + case this.isString(/vmin$/): + return this.getNumber() / 100.0 * Math.min(viewPort.computeSize('x'), viewPort.computeSize('y')); + case this.isString(/vmax$/): + return this.getNumber() / 100.0 * Math.max(viewPort.computeSize('x'), viewPort.computeSize('y')); + case this.isString(/vw$/): + return this.getNumber() / 100.0 * viewPort.computeSize('x'); + case this.isString(/vh$/): + return this.getNumber() / 100.0 * viewPort.computeSize('y'); + case this.isString(/rem$/): + return this.getNumber() * this.getRem(); + case this.isString(/em$/): + return this.getNumber() * this.getEm(); + case this.isString(/ex$/): + return this.getNumber() * this.getEm() / 2.0; + case this.isString(/px$/): + return this.getNumber(); + case this.isString(/pt$/): + return this.getNumber() * this.getDpi() * (1.0 / 72.0); + case this.isString(/pc$/): + return this.getNumber() * 15; + case this.isString(/cm$/): + return this.getNumber() * this.getDpi() / 2.54; + case this.isString(/mm$/): + return this.getNumber() * this.getDpi() / 25.4; + case this.isString(/in$/): + return this.getNumber() * this.getDpi(); + case this.isString(/%$/) && isFontSize: + return this.getNumber() * this.getEm(); + case this.isString(/%$/): + return this.getNumber() * viewPort.computeSize(axis); + default: + { + const n = this.getNumber(); + if (processPercent && n < 1.0) { + return n * viewPort.computeSize(axis); + } + return n; + } + } + } + getMilliseconds() { + if (!this.hasValue()) { + return 0; + } + if (this.isString(/ms$/)) { + return this.getNumber(); + } + return this.getNumber() * 1000; + } + getRadians() { + if (!this.hasValue()) { + return 0; + } + switch(true){ + case this.isString(/deg$/): + return this.getNumber() * (Math.PI / 180.0); + case this.isString(/grad$/): + return this.getNumber() * (Math.PI / 200.0); + case this.isString(/rad$/): + return this.getNumber(); + default: + return this.getNumber() * (Math.PI / 180.0); + } + } + getDefinition() { + const asString = this.getString(); + const match = /#([^)'"]+)/.exec(asString); + const name = match?.[1] || asString; + return this.document.definitions[name]; + } + getFillStyleDefinition(element, opacity) { + let def = this.getDefinition(); + if (!def) { + return null; + } + // gradient + if (typeof def.createGradient === 'function' && 'getBoundingBox' in element) { + return def.createGradient(this.document.ctx, element, opacity); + } + // pattern + if (typeof def.createPattern === 'function') { + if (def.getHrefAttribute().hasValue()) { + const patternTransform = def.getAttribute('patternTransform'); + def = def.getHrefAttribute().getDefinition(); + if (def && patternTransform.hasValue()) { + def.getAttribute('patternTransform', true).setValue(patternTransform.value); + } + } + if (def) { + return def.createPattern(this.document.ctx, element, opacity); + } + } + return null; + } + getTextBaseline() { + if (!this.hasValue()) { + return null; + } + const key = this.getString(); + return Property.textBaselineMapping[key] || null; + } + addOpacity(opacity) { + let value = this.getColor(); + const len = value.length; + let commas = 0; + // Simulate old RGBColor version, which can't parse rgba. + for(let i = 0; i < len; i++){ + if (value[i] === ',') { + commas++; + } + if (commas === 3) { + break; + } + } + if (opacity.hasValue() && this.isString() && commas !== 3) { + const color = new rgbcolor(value); + if (color.ok) { + color.alpha = opacity.getNumber(); + value = color.toRGBA(); + } + } + return new Property(this.document, this.name, value); + } + } + + class ViewPort { + static DEFAULT_VIEWPORT_WIDTH = 800; + static DEFAULT_VIEWPORT_HEIGHT = 600; + viewPorts = []; + clear() { + this.viewPorts = []; + } + setCurrent(width, height) { + this.viewPorts.push({ + width, + height + }); + } + removeCurrent() { + this.viewPorts.pop(); + } + getRoot() { + const [root] = this.viewPorts; + if (!root) { + return getDefault(); + } + return root; + } + getCurrent() { + const { viewPorts } = this; + const current = viewPorts[viewPorts.length - 1]; + if (!current) { + return getDefault(); + } + return current; + } + get width() { + return this.getCurrent().width; + } + get height() { + return this.getCurrent().height; + } + computeSize(d) { + if (typeof d === 'number') { + return d; + } + if (d === 'x') { + return this.width; + } + if (d === 'y') { + return this.height; + } + return Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2)) / Math.sqrt(2); + } + } + function getDefault() { + return { + width: ViewPort.DEFAULT_VIEWPORT_WIDTH, + height: ViewPort.DEFAULT_VIEWPORT_HEIGHT + }; + } + + class Point { + x; + y; + static parse(point) { + let defaultValue = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 0; + const [x = defaultValue, y = defaultValue] = toNumbers(point); + return new Point(x, y); + } + static parseScale(scale) { + let defaultValue = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 1; + const [x = defaultValue, y = x] = toNumbers(scale); + return new Point(x, y); + } + static parsePath(path) { + const points = toNumbers(path); + const len = points.length; + const pathPoints = []; + for(let i = 0; i < len; i += 2){ + pathPoints.push(new Point(points[i], points[i + 1])); + } + return pathPoints; + } + constructor(x, y){ + this.x = x; + this.y = y; + } + angleTo(point) { + return Math.atan2(point.y - this.y, point.x - this.x); + } + applyTransform(transform) { + const { x, y } = this; + const xp = x * transform[0] + y * transform[2] + transform[4]; + const yp = x * transform[1] + y * transform[3] + transform[5]; + this.x = xp; + this.y = yp; + } + } + + class Mouse { + screen; + working; + events; + eventElements; + constructor(screen){ + this.screen = screen; + this.working = false; + this.events = []; + this.eventElements = []; + this.onClick = this.onClick.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + } + isWorking() { + return this.working; + } + start() { + if (this.working) { + return; + } + const { screen, onClick, onMouseMove } = this; + const canvas = screen.ctx.canvas; + canvas.onclick = onClick; + canvas.onmousemove = onMouseMove; + this.working = true; + } + stop() { + if (!this.working) { + return; + } + const canvas = this.screen.ctx.canvas; + this.working = false; + canvas.onclick = null; + canvas.onmousemove = null; + } + hasEvents() { + return this.working && this.events.length > 0; + } + runEvents() { + if (!this.working) { + return; + } + const { screen: document, events, eventElements } = this; + const { style } = document.ctx.canvas; + let element; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (style) { + style.cursor = ''; + } + events.forEach((param, i)=>{ + let { run } = param; + element = eventElements[i]; + while(element){ + run(element); + element = element.parent; + } + }); + // done running, clear + this.events = []; + this.eventElements = []; + } + checkPath(element, ctx) { + if (!this.working || !ctx) { + return; + } + const { events, eventElements } = this; + events.forEach((param, i)=>{ + let { x, y } = param; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!eventElements[i] && ctx.isPointInPath && ctx.isPointInPath(x, y)) { + eventElements[i] = element; + } + }); + } + checkBoundingBox(element, boundingBox) { + if (!this.working || !boundingBox) { + return; + } + const { events, eventElements } = this; + events.forEach((param, i)=>{ + let { x, y } = param; + if (!eventElements[i] && boundingBox.isPointInBox(x, y)) { + eventElements[i] = element; + } + }); + } + mapXY(x, y) { + const { window, ctx } = this.screen; + const point = new Point(x, y); + let element = ctx.canvas; + while(element){ + point.x -= element.offsetLeft; + point.y -= element.offsetTop; + element = element.offsetParent; + } + if (window?.scrollX) { + point.x += window.scrollX; + } + if (window?.scrollY) { + point.y += window.scrollY; + } + return point; + } + onClick(event) { + const { x, y } = this.mapXY(event.clientX, event.clientY); + this.events.push({ + type: 'onclick', + x, + y, + run (eventTarget) { + if (eventTarget.onClick) { + eventTarget.onClick(); + } + } + }); + } + onMouseMove(event) { + const { x, y } = this.mapXY(event.clientX, event.clientY); + this.events.push({ + type: 'onmousemove', + x, + y, + run (eventTarget) { + if (eventTarget.onMouseMove) { + eventTarget.onMouseMove(); + } + } + }); + } + } + + const defaultWindow = typeof window !== 'undefined' ? window : null; + const defaultFetch$1 = typeof fetch !== 'undefined' ? fetch.bind(undefined) // `fetch` depends on context: `someObject.fetch(...)` will throw error. + : undefined; + class Screen { + ctx; + static defaultWindow = defaultWindow; + static defaultFetch = defaultFetch$1; + static FRAMERATE = 30; + static MAX_VIRTUAL_PIXELS = 30000; + window; + fetch; + viewPort; + mouse; + animations; + readyPromise; + resolveReady; + waits; + frameDuration; + isReadyLock; + isFirstRender; + intervalId; + constructor(ctx, { fetch: fetch1 = defaultFetch$1, window: window1 = defaultWindow } = {}){ + this.ctx = ctx; + this.viewPort = new ViewPort(); + this.mouse = new Mouse(this); + this.animations = []; + this.waits = []; + this.frameDuration = 0; + this.isReadyLock = false; + this.isFirstRender = true; + this.intervalId = null; + this.window = window1; + if (!fetch1) { + throw new Error(`Can't find 'fetch' in 'globalThis', please provide it via options`); + } + this.fetch = fetch1; + } + wait(checker) { + this.waits.push(checker); + } + ready() { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + if (!this.readyPromise) { + return Promise.resolve(); + } + return this.readyPromise; + } + isReady() { + if (this.isReadyLock) { + return true; + } + const isReadyLock = this.waits.every((_)=>_()); + if (isReadyLock) { + this.waits = []; + if (this.resolveReady) { + this.resolveReady(); + } + } + this.isReadyLock = isReadyLock; + return isReadyLock; + } + setDefaults(ctx) { + // initial values and defaults + ctx.strokeStyle = 'rgba(0,0,0,0)'; + ctx.lineCap = 'butt'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 4; + } + setViewBox(param) { + let { document, ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX = 0, minY = 0, refX, refY, clip = false, clipX = 0, clipY = 0 } = param; + // aspect ratio - http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + const cleanAspectRatio = compressSpaces(aspectRatio).replace(/^defer\s/, '') // ignore defer + ; + const [aspectRatioAlign, aspectRatioMeetOrSlice] = cleanAspectRatio.split(' '); + const align = aspectRatioAlign || 'xMidYMid'; + const meetOrSlice = aspectRatioMeetOrSlice || 'meet'; + // calculate scale + const scaleX = width / desiredWidth; + const scaleY = height / desiredHeight; + const scaleMin = Math.min(scaleX, scaleY); + const scaleMax = Math.max(scaleX, scaleY); + let finalDesiredWidth = desiredWidth; + let finalDesiredHeight = desiredHeight; + if (meetOrSlice === 'meet') { + finalDesiredWidth *= scaleMin; + finalDesiredHeight *= scaleMin; + } + if (meetOrSlice === 'slice') { + finalDesiredWidth *= scaleMax; + finalDesiredHeight *= scaleMax; + } + const refXProp = new Property(document, 'refX', refX); + const refYProp = new Property(document, 'refY', refY); + const hasRefs = refXProp.hasValue() && refYProp.hasValue(); + if (hasRefs) { + ctx.translate(-scaleMin * refXProp.getPixels('x'), -scaleMin * refYProp.getPixels('y')); + } + if (clip) { + const scaledClipX = scaleMin * clipX; + const scaledClipY = scaleMin * clipY; + ctx.beginPath(); + ctx.moveTo(scaledClipX, scaledClipY); + ctx.lineTo(width, scaledClipY); + ctx.lineTo(width, height); + ctx.lineTo(scaledClipX, height); + ctx.closePath(); + ctx.clip(); + } + if (!hasRefs) { + const isMeetMinY = meetOrSlice === 'meet' && scaleMin === scaleY; + const isSliceMaxY = meetOrSlice === 'slice' && scaleMax === scaleY; + const isMeetMinX = meetOrSlice === 'meet' && scaleMin === scaleX; + const isSliceMaxX = meetOrSlice === 'slice' && scaleMax === scaleX; + if (align.startsWith('xMid') && (isMeetMinY || isSliceMaxY)) { + ctx.translate(width / 2.0 - finalDesiredWidth / 2.0, 0); + } + if (align.endsWith('YMid') && (isMeetMinX || isSliceMaxX)) { + ctx.translate(0, height / 2.0 - finalDesiredHeight / 2.0); + } + if (align.startsWith('xMax') && (isMeetMinY || isSliceMaxY)) { + ctx.translate(width - finalDesiredWidth, 0); + } + if (align.endsWith('YMax') && (isMeetMinX || isSliceMaxX)) { + ctx.translate(0, height - finalDesiredHeight); + } + } + // scale + switch(true){ + case align === 'none': + ctx.scale(scaleX, scaleY); + break; + case meetOrSlice === 'meet': + ctx.scale(scaleMin, scaleMin); + break; + case meetOrSlice === 'slice': + ctx.scale(scaleMax, scaleMax); + break; + } + // translate + ctx.translate(-minX, -minY); + } + start(element) { + let { enableRedraw = false, ignoreMouse = false, ignoreAnimation = false, ignoreDimensions = false, ignoreClear = false, forceRedraw, scaleWidth, scaleHeight, offsetX, offsetY } = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}; + const { mouse } = this; + const frameDuration = 1000 / Screen.FRAMERATE; + this.isReadyLock = false; + this.frameDuration = frameDuration; + this.readyPromise = new Promise((resolve)=>{ + this.resolveReady = resolve; + }); + if (this.isReady()) { + this.render(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY); + } + if (!enableRedraw) { + return; + } + let now = Date.now(); + let then = now; + let delta = 0; + const tick = ()=>{ + now = Date.now(); + delta = now - then; + if (delta >= frameDuration) { + then = now - delta % frameDuration; + if (this.shouldUpdate(ignoreAnimation, forceRedraw)) { + this.render(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY); + mouse.runEvents(); + } + } + this.intervalId = requestAnimationFrame(tick); + }; + if (!ignoreMouse) { + mouse.start(); + } + this.intervalId = requestAnimationFrame(tick); + } + stop() { + if (this.intervalId) { + requestAnimationFrame.cancel(this.intervalId); + this.intervalId = null; + } + this.mouse.stop(); + } + shouldUpdate(ignoreAnimation, forceRedraw) { + // need update from animations? + if (!ignoreAnimation) { + const { frameDuration } = this; + const shouldUpdate = this.animations.reduce((shouldUpdate, animation)=>animation.update(frameDuration) || shouldUpdate, false); + if (shouldUpdate) { + return true; + } + } + // need update from redraw? + if (typeof forceRedraw === 'function' && forceRedraw()) { + return true; + } + if (!this.isReadyLock && this.isReady()) { + return true; + } + // need update from mouse events? + if (this.mouse.hasEvents()) { + return true; + } + return false; + } + render(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY) { + const { viewPort, ctx, isFirstRender } = this; + const canvas = ctx.canvas; + viewPort.clear(); + if (canvas.width && canvas.height) { + viewPort.setCurrent(canvas.width, canvas.height); + } + const widthStyle = element.getStyle('width'); + const heightStyle = element.getStyle('height'); + if (!ignoreDimensions && (isFirstRender || typeof scaleWidth !== 'number' && typeof scaleHeight !== 'number')) { + // set canvas size + if (widthStyle.hasValue()) { + canvas.width = widthStyle.getPixels('x'); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (canvas.style) { + canvas.style.width = `${canvas.width}px`; + } + } + if (heightStyle.hasValue()) { + canvas.height = heightStyle.getPixels('y'); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (canvas.style) { + canvas.style.height = `${canvas.height}px`; + } + } + } + let cWidth = canvas.clientWidth || canvas.width; + let cHeight = canvas.clientHeight || canvas.height; + if (ignoreDimensions && widthStyle.hasValue() && heightStyle.hasValue()) { + cWidth = widthStyle.getPixels('x'); + cHeight = heightStyle.getPixels('y'); + } + viewPort.setCurrent(cWidth, cHeight); + if (typeof offsetX === 'number') { + element.getAttribute('x', true).setValue(offsetX); + } + if (typeof offsetY === 'number') { + element.getAttribute('y', true).setValue(offsetY); + } + if (typeof scaleWidth === 'number' || typeof scaleHeight === 'number') { + const viewBox = toNumbers(element.getAttribute('viewBox').getString()); + let xRatio = 0; + let yRatio = 0; + if (typeof scaleWidth === 'number') { + const widthStyle = element.getStyle('width'); + if (widthStyle.hasValue()) { + xRatio = widthStyle.getPixels('x') / scaleWidth; + } else if (viewBox[2] && !isNaN(viewBox[2])) { + xRatio = viewBox[2] / scaleWidth; + } + } + if (typeof scaleHeight === 'number') { + const heightStyle = element.getStyle('height'); + if (heightStyle.hasValue()) { + yRatio = heightStyle.getPixels('y') / scaleHeight; + } else if (viewBox[3] && !isNaN(viewBox[3])) { + yRatio = viewBox[3] / scaleHeight; + } + } + if (!xRatio) { + xRatio = yRatio; + } + if (!yRatio) { + yRatio = xRatio; + } + element.getAttribute('width', true).setValue(scaleWidth); + element.getAttribute('height', true).setValue(scaleHeight); + const transformStyle = element.getStyle('transform', true, true); + transformStyle.setValue(`${transformStyle.getString()} scale(${1.0 / xRatio}, ${1.0 / yRatio})`); + } + // clear and render + if (!ignoreClear) { + ctx.clearRect(0, 0, cWidth, cHeight); + } + element.render(ctx); + if (isFirstRender) { + this.isFirstRender = false; + } + } + } + + const { defaultFetch } = Screen; + const DefaultDOMParser = typeof DOMParser !== 'undefined' ? DOMParser : undefined; + class Parser { + fetch; + DOMParser; + constructor({ fetch = defaultFetch, DOMParser: DOMParser1 = DefaultDOMParser } = {}){ + if (!fetch) { + throw new Error(`Can't find 'fetch' in 'globalThis', please provide it via options`); + } + if (!DOMParser1) { + throw new Error(`Can't find 'DOMParser' in 'globalThis', please provide it via options`); + } + this.fetch = fetch; + this.DOMParser = DOMParser1; + } + async parse(resource) { + if (resource.startsWith('<')) { + return this.parseFromString(resource); + } + return this.load(resource); + } + parseFromString(xml) { + const parser = new this.DOMParser(); + try { + return this.checkDocument(parser.parseFromString(xml, 'image/svg+xml')); + } catch (err) { + return this.checkDocument(parser.parseFromString(xml, 'text/xml')); + } + } + checkDocument(document) { + const parserError = document.getElementsByTagName('parsererror')[0]; + if (parserError) { + throw new Error(parserError.textContent || 'Unknown parse error'); + } + return document; + } + async load(url) { + const response = await this.fetch(url); + const xml = await response.text(); + return this.parseFromString(xml); + } + } + + class Translate { + type = 'translate'; + point; + constructor(_, point){ + this.point = Point.parse(point); + } + apply(ctx) { + const { x, y } = this.point; + ctx.translate(x || 0.0, y || 0.0); + } + unapply(ctx) { + const { x, y } = this.point; + ctx.translate(-1.0 * x || 0.0, -1.0 * y || 0.0); + } + applyToPoint(point) { + const { x, y } = this.point; + point.applyTransform([ + 1, + 0, + 0, + 1, + x || 0.0, + y || 0.0 + ]); + } + } + + class Rotate { + type = 'rotate'; + angle; + originX; + originY; + cx; + cy; + constructor(document, rotate, transformOrigin){ + const numbers = toNumbers(rotate); + this.angle = new Property(document, 'angle', numbers[0]); + this.originX = transformOrigin[0]; + this.originY = transformOrigin[1]; + this.cx = numbers[1] || 0; + this.cy = numbers[2] || 0; + } + apply(ctx) { + const { cx, cy, originX, originY, angle } = this; + const tx = cx + originX.getPixels('x'); + const ty = cy + originY.getPixels('y'); + ctx.translate(tx, ty); + ctx.rotate(angle.getRadians()); + ctx.translate(-tx, -ty); + } + unapply(ctx) { + const { cx, cy, originX, originY, angle } = this; + const tx = cx + originX.getPixels('x'); + const ty = cy + originY.getPixels('y'); + ctx.translate(tx, ty); + ctx.rotate(-1.0 * angle.getRadians()); + ctx.translate(-tx, -ty); + } + applyToPoint(point) { + const { cx, cy, angle } = this; + const rad = angle.getRadians(); + point.applyTransform([ + 1, + 0, + 0, + 1, + cx || 0.0, + cy || 0.0 // this.p.y + ]); + point.applyTransform([ + Math.cos(rad), + Math.sin(rad), + -Math.sin(rad), + Math.cos(rad), + 0, + 0 + ]); + point.applyTransform([ + 1, + 0, + 0, + 1, + -cx || 0.0, + -cy || 0.0 // -this.p.y + ]); + } + } + + class Scale { + type = 'scale'; + scale; + originX; + originY; + constructor(_, scale, transformOrigin){ + const scaleSize = Point.parseScale(scale); + // Workaround for node-canvas + if (scaleSize.x === 0 || scaleSize.y === 0) { + scaleSize.x = PSEUDO_ZERO; + scaleSize.y = PSEUDO_ZERO; + } + this.scale = scaleSize; + this.originX = transformOrigin[0]; + this.originY = transformOrigin[1]; + } + apply(ctx) { + const { scale: { x, y }, originX, originY } = this; + const tx = originX.getPixels('x'); + const ty = originY.getPixels('y'); + ctx.translate(tx, ty); + ctx.scale(x, y || x); + ctx.translate(-tx, -ty); + } + unapply(ctx) { + const { scale: { x, y }, originX, originY } = this; + const tx = originX.getPixels('x'); + const ty = originY.getPixels('y'); + ctx.translate(tx, ty); + ctx.scale(1.0 / x, 1.0 / y || x); + ctx.translate(-tx, -ty); + } + applyToPoint(point) { + const { x, y } = this.scale; + point.applyTransform([ + x || 0.0, + 0, + 0, + y || 0.0, + 0, + 0 + ]); + } + } + + class Matrix { + type = 'matrix'; + matrix; + originX; + originY; + constructor(_, matrix, transformOrigin){ + this.matrix = toMatrixValue(matrix); + this.originX = transformOrigin[0]; + this.originY = transformOrigin[1]; + } + apply(ctx) { + const { originX, originY, matrix } = this; + const tx = originX.getPixels('x'); + const ty = originY.getPixels('y'); + ctx.translate(tx, ty); + ctx.transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]); + ctx.translate(-tx, -ty); + } + unapply(ctx) { + const { originX, originY, matrix } = this; + const a = matrix[0]; + const b = matrix[2]; + const c = matrix[4]; + const d = matrix[1]; + const e = matrix[3]; + const f = matrix[5]; + const g = 0.0; + const h = 0.0; + const i = 1.0; + const det = 1 / (a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)); + const tx = originX.getPixels('x'); + const ty = originY.getPixels('y'); + ctx.translate(tx, ty); + ctx.transform(det * (e * i - f * h), det * (f * g - d * i), det * (c * h - b * i), det * (a * i - c * g), det * (b * f - c * e), det * (c * d - a * f)); + ctx.translate(-tx, -ty); + } + applyToPoint(point) { + point.applyTransform(this.matrix); + } + } + + class Skew extends Matrix { + type = 'skew'; + angle; + constructor(document, skew, transformOrigin){ + super(document, skew, transformOrigin); + this.angle = new Property(document, 'angle', skew); + } + } + + class SkewX extends Skew { + type = 'skewX'; + constructor(document, skew, transformOrigin){ + super(document, skew, transformOrigin); + this.matrix = [ + 1, + 0, + Math.tan(this.angle.getRadians()), + 1, + 0, + 0 + ]; + } + } + + class SkewY extends Skew { + type = 'skewY'; + constructor(document, skew, transformOrigin){ + super(document, skew, transformOrigin); + this.matrix = [ + 1, + Math.tan(this.angle.getRadians()), + 0, + 1, + 0, + 0 + ]; + } + } + + function parseTransforms(transform) { + return compressSpaces(transform).trim().replace(/\)([a-zA-Z])/g, ') $1').replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/); + } + function parseTransform(transform) { + const [type = '', value = ''] = transform.split('('); + return [ + type.trim(), + value.trim().replace(')', '') + ]; + } + class Transform { + document; + static fromElement(document, element) { + const transformStyle = element.getStyle('transform', false, true); + if (transformStyle.hasValue()) { + const [transformOriginXProperty, transformOriginYProperty = transformOriginXProperty] = element.getStyle('transform-origin', false, true).split(); + if (transformOriginXProperty && transformOriginYProperty) { + const transformOrigin = [ + transformOriginXProperty, + transformOriginYProperty + ]; + return new Transform(document, transformStyle.getString(), transformOrigin); + } + } + return null; + } + static transformTypes = { + translate: Translate, + rotate: Rotate, + scale: Scale, + matrix: Matrix, + skewX: SkewX, + skewY: SkewY + }; + transforms; + constructor(document, transform, transformOrigin){ + this.document = document; + this.transforms = []; + const data = parseTransforms(transform); + data.forEach((transform)=>{ + if (transform === 'none') { + return; + } + const [type, value] = parseTransform(transform); + const TransformType = Transform.transformTypes[type]; + if (TransformType) { + this.transforms.push(new TransformType(this.document, value, transformOrigin)); + } + }); + } + apply(ctx) { + this.transforms.forEach((transform)=>transform.apply(ctx)); + } + unapply(ctx) { + this.transforms.forEach((transform)=>transform.unapply(ctx)); + } + // TODO: applyToPoint unused ... remove? + applyToPoint(point) { + this.transforms.forEach((transform)=>transform.applyToPoint(point)); + } + } + + class Element { + document; + node; + captureTextNodes; + static ignoreChildTypes = [ + 'title' + ]; + type; + attributes; + styles; + stylesSpecificity; + animationFrozen; + animationFrozenValue; + parent; + children; + constructor(document, node, captureTextNodes = false){ + this.document = document; + this.node = node; + this.captureTextNodes = captureTextNodes; + this.type = ''; + this.attributes = {}; + this.styles = {}; + this.stylesSpecificity = {}; + this.animationFrozen = false; + this.animationFrozenValue = ''; + this.parent = null; + this.children = []; + if (!node || node.nodeType !== 1) { + return; + } + // add attributes + Array.from(node.attributes).forEach((attribute)=>{ + const nodeName = normalizeAttributeName(attribute.nodeName); + this.attributes[nodeName] = new Property(document, nodeName, attribute.value); + }); + this.addStylesFromStyleDefinition(); + // add inline styles + if (this.getAttribute('style').hasValue()) { + const styles = this.getAttribute('style').getString().split(';').map((_)=>_.trim()); + styles.forEach((style)=>{ + if (!style) { + return; + } + const [name, value] = style.split(':').map((_)=>_.trim()); + if (name) { + this.styles[name] = new Property(document, name, value); + } + }); + } + const { definitions } = document; + const id = this.getAttribute('id'); + // add id + if (id.hasValue()) { + if (!definitions[id.getString()]) { + definitions[id.getString()] = this; + } + } + Array.from(node.childNodes).forEach((childNode)=>{ + if (childNode.nodeType === 1) { + this.addChild(childNode) // ELEMENT_NODE + ; + } else if (captureTextNodes && (childNode.nodeType === 3 || childNode.nodeType === 4)) { + const textNode = document.createTextNode(childNode); + if (textNode.getText().length > 0) { + this.addChild(textNode) // TEXT_NODE + ; + } + } + }); + } + getAttribute(name) { + let createIfNotExists = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + const attr = this.attributes[name]; + if (!attr && createIfNotExists) { + const attr = new Property(this.document, name, ''); + this.attributes[name] = attr; + return attr; + } + return attr || Property.empty(this.document); + } + getHrefAttribute() { + let href; + for(const key in this.attributes){ + if (key === 'href' || key.endsWith(':href')) { + href = this.attributes[key]; + break; + } + } + return href || Property.empty(this.document); + } + getStyle(name) { + let createIfNotExists = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false, skipAncestors = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false; + const style = this.styles[name]; + if (style) { + return style; + } + const attr = this.getAttribute(name); + if (attr.hasValue()) { + this.styles[name] = attr // move up to me to cache + ; + return attr; + } + if (!skipAncestors) { + const { parent } = this; + if (parent) { + const parentStyle = parent.getStyle(name); + if (parentStyle.hasValue()) { + return parentStyle; + } + } + } + if (createIfNotExists) { + const style = new Property(this.document, name, ''); + this.styles[name] = style; + return style; + } + return Property.empty(this.document); + } + render(ctx) { + // don't render display=none + // don't render visibility=hidden + if (this.getStyle('display').getString() === 'none' || this.getStyle('visibility').getString() === 'hidden') { + return; + } + ctx.save(); + if (this.getStyle('mask').hasValue()) { + const mask = this.getStyle('mask').getDefinition(); + if (mask) { + this.applyEffects(ctx); + mask.apply(ctx, this); + } + } else if (this.getStyle('filter').getValue('none') !== 'none') { + const filter = this.getStyle('filter').getDefinition(); + if (filter) { + this.applyEffects(ctx); + filter.apply(ctx, this); + } + } else { + this.setContext(ctx); + this.renderChildren(ctx); + this.clearContext(ctx); + } + ctx.restore(); + } + setContext(_) { + // NO RENDER + } + applyEffects(ctx) { + // transform + const transform = Transform.fromElement(this.document, this); + if (transform) { + transform.apply(ctx); + } + // clip + const clipPathStyleProp = this.getStyle('clip-path', false, true); + if (clipPathStyleProp.hasValue()) { + const clip = clipPathStyleProp.getDefinition(); + if (clip) { + clip.apply(ctx); + } + } + } + clearContext(_) { + // NO RENDER + } + renderChildren(ctx) { + this.children.forEach((child)=>{ + child.render(ctx); + }); + } + addChild(childNode) { + const child = childNode instanceof Element ? childNode : this.document.createElement(childNode); + child.parent = this; + if (!Element.ignoreChildTypes.includes(child.type)) { + this.children.push(child); + } + } + matchesSelector(selector) { + const { node } = this; + if (typeof node.matches === 'function') { + return node.matches(selector); + } + const styleClasses = node.getAttribute?.('class'); + if (!styleClasses || styleClasses === '') { + return false; + } + return styleClasses.split(' ').some((styleClass)=>`.${styleClass}` === selector); + } + addStylesFromStyleDefinition() { + const { styles, stylesSpecificity } = this.document; + let styleProp; + for(const selector in styles){ + if (!selector.startsWith('@') && this.matchesSelector(selector)) { + const style = styles[selector]; + const specificity = stylesSpecificity[selector]; + if (style) { + for(const name in style){ + let existingSpecificity = this.stylesSpecificity[name]; + if (typeof existingSpecificity === 'undefined') { + existingSpecificity = '000'; + } + if (specificity && specificity >= existingSpecificity) { + styleProp = style[name]; + if (styleProp) { + this.styles[name] = styleProp; + } + this.stylesSpecificity[name] = specificity; + } + } + } + } + } + } + removeStyles(element, ignoreStyles) { + const toRestore = ignoreStyles.reduce((toRestore, name)=>{ + const styleProp = element.getStyle(name); + if (!styleProp.hasValue()) { + return toRestore; + } + const value = styleProp.getString(); + styleProp.setValue(''); + return [ + ...toRestore, + [ + name, + value + ] + ]; + }, []); + return toRestore; + } + restoreStyles(element, styles) { + styles.forEach((param)=>{ + let [name, value] = param; + element.getStyle(name, true).setValue(value); + }); + } + isFirstChild() { + return this.parent?.children.indexOf(this) === 0; + } + } + + class UnknownElement extends Element { + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + { + console.warn(`Element ${node.nodeName} not yet implemented.`); + } + } + } + + function wrapFontFamily(fontFamily) { + const trimmed = fontFamily.trim(); + return /^('|")/.test(trimmed) ? trimmed : `"${trimmed}"`; + } + function prepareFontFamily(fontFamily) { + return typeof process === 'undefined' ? fontFamily : fontFamily.trim().split(',').map(wrapFontFamily).join(','); + } + /** + * https://developer.mozilla.org/en-US/docs/Web/CSS/font-style + * @param fontStyle + * @returns CSS font style. + */ function prepareFontStyle(fontStyle) { + if (!fontStyle) { + return ''; + } + const targetFontStyle = fontStyle.trim().toLowerCase(); + switch(targetFontStyle){ + case 'normal': + case 'italic': + case 'oblique': + case 'inherit': + case 'initial': + case 'unset': + return targetFontStyle; + default: + if (/^oblique\s+(-|)\d+deg$/.test(targetFontStyle)) { + return targetFontStyle; + } + return ''; + } + } + /** + * https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight + * @param fontWeight + * @returns CSS font weight. + */ function prepareFontWeight(fontWeight) { + if (!fontWeight) { + return ''; + } + const targetFontWeight = fontWeight.trim().toLowerCase(); + switch(targetFontWeight){ + case 'normal': + case 'bold': + case 'lighter': + case 'bolder': + case 'inherit': + case 'initial': + case 'unset': + return targetFontWeight; + default: + if (/^[\d.]+$/.test(targetFontWeight)) { + return targetFontWeight; + } + return ''; + } + } + class Font { + static parse() { + let font = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : '', inherit = arguments.length > 1 ? arguments[1] : void 0; + let fontStyle = ''; + let fontVariant = ''; + let fontWeight = ''; + let fontSize = ''; + let fontFamily = ''; + const parts = compressSpaces(font).trim().split(' '); + const set = { + fontSize: false, + fontStyle: false, + fontWeight: false, + fontVariant: false + }; + parts.forEach((part)=>{ + switch(true){ + case !set.fontStyle && Font.styles.includes(part): + if (part !== 'inherit') { + fontStyle = part; + } + set.fontStyle = true; + break; + case !set.fontVariant && Font.variants.includes(part): + if (part !== 'inherit') { + fontVariant = part; + } + set.fontStyle = true; + set.fontVariant = true; + break; + case !set.fontWeight && Font.weights.includes(part): + if (part !== 'inherit') { + fontWeight = part; + } + set.fontStyle = true; + set.fontVariant = true; + set.fontWeight = true; + break; + case !set.fontSize: + if (part !== 'inherit') { + fontSize = part.split('/')[0] || ''; + } + set.fontStyle = true; + set.fontVariant = true; + set.fontWeight = true; + set.fontSize = true; + break; + default: + if (part !== 'inherit') { + fontFamily += part; + } + } + }); + return new Font(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit); + } + static styles = 'normal|italic|oblique|inherit'; + static variants = 'normal|small-caps|inherit'; + static weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit'; + fontFamily; + fontSize; + fontStyle; + fontWeight; + fontVariant; + constructor(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit){ + const inheritFont = inherit ? typeof inherit === 'string' ? Font.parse(inherit) : inherit : {}; + this.fontFamily = fontFamily || inheritFont.fontFamily; + this.fontSize = fontSize || inheritFont.fontSize; + this.fontStyle = fontStyle || inheritFont.fontStyle; + this.fontWeight = fontWeight || inheritFont.fontWeight; + this.fontVariant = fontVariant || inheritFont.fontVariant; + } + toString() { + return [ + prepareFontStyle(this.fontStyle), + this.fontVariant, + prepareFontWeight(this.fontWeight), + this.fontSize, + // Wrap fontFamily only on nodejs and only for canvas.ctx + prepareFontFamily(this.fontFamily) + ].join(' ').trim(); + } + } + + class BoundingBox { + x1; + y1; + x2; + y2; + constructor(x1 = Number.NaN, y1 = Number.NaN, x2 = Number.NaN, y2 = Number.NaN){ + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + get x() { + return this.x1; + } + get y() { + return this.y1; + } + get width() { + return this.x2 - this.x1; + } + get height() { + return this.y2 - this.y1; + } + addPoint(x, y) { + if (typeof x !== 'undefined') { + if (isNaN(this.x1) || isNaN(this.x2)) { + this.x1 = x; + this.x2 = x; + } + if (x < this.x1) { + this.x1 = x; + } + if (x > this.x2) { + this.x2 = x; + } + } + if (typeof y !== 'undefined') { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) { + this.y1 = y; + } + if (y > this.y2) { + this.y2 = y; + } + } + } + addX(x) { + this.addPoint(x, 0); + } + addY(y) { + this.addPoint(0, y); + } + addBoundingBox(boundingBox) { + if (!boundingBox) { + return; + } + const { x1, y1, x2, y2 } = boundingBox; + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + sumCubic(t, p0, p1, p2, p3) { + return Math.pow(1 - t, 3) * p0 + 3 * Math.pow(1 - t, 2) * t * p1 + 3 * (1 - t) * Math.pow(t, 2) * p2 + Math.pow(t, 3) * p3; + } + bezierCurveAdd(forX, p0, p1, p2, p3) { + const b = 6 * p0 - 12 * p1 + 6 * p2; + const a = -3 * p0 + 9 * p1 - 9 * p2 + 3 * p3; + const c = 3 * p1 - 3 * p0; + if (a === 0) { + if (b === 0) { + return; + } + const t = -c / b; + if (0 < t && t < 1) { + if (forX) { + this.addX(this.sumCubic(t, p0, p1, p2, p3)); + } else { + this.addY(this.sumCubic(t, p0, p1, p2, p3)); + } + } + return; + } + const b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) { + return; + } + const t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (0 < t1 && t1 < 1) { + if (forX) { + this.addX(this.sumCubic(t1, p0, p1, p2, p3)); + } else { + this.addY(this.sumCubic(t1, p0, p1, p2, p3)); + } + } + const t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (0 < t2 && t2 < 1) { + if (forX) { + this.addX(this.sumCubic(t2, p0, p1, p2, p3)); + } else { + this.addY(this.sumCubic(t2, p0, p1, p2, p3)); + } + } + } + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + addBezierCurve(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + this.addPoint(p0x, p0y); + this.addPoint(p3x, p3y); + this.bezierCurveAdd(true, p0x, p1x, p2x, p3x); + this.bezierCurveAdd(false, p0y, p1y, p2y, p3y); + } + addQuadraticCurve(p0x, p0y, p1x, p1y, p2x, p2y) { + const cp1x = p0x + 2 / 3 * (p1x - p0x // CP1 = QP0 + 2/3 *(QP1-QP0) + ); + const cp1y = p0y + 2 / 3 * (p1y - p0y // CP1 = QP0 + 2/3 *(QP1-QP0) + ); + const cp2x = cp1x + 1 / 3 * (p2x - p0x // CP2 = CP1 + 1/3 *(QP2-QP0) + ); + const cp2y = cp1y + 1 / 3 * (p2y - p0y // CP2 = CP1 + 1/3 *(QP2-QP0) + ); + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + isPointInBox(x, y) { + const { x1, y1, x2, y2 } = this; + return x1 <= x && x <= x2 && y1 <= y && y <= y2; + } + } + + class RenderedElement extends Element { + modifiedEmSizeStack = false; + calculateOpacity() { + let opacity = 1.0; + // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this + let element = this; + while(element){ + const opacityStyle = element.getStyle('opacity', false, true) // no ancestors on style call + ; + if (opacityStyle.hasValue(true)) { + opacity *= opacityStyle.getNumber(); + } + element = element.parent; + } + return opacity; + } + setContext(ctx) { + let fromMeasure = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + if (!fromMeasure) { + // fill + const fillStyleProp = this.getStyle('fill'); + const fillOpacityStyleProp = this.getStyle('fill-opacity'); + const strokeStyleProp = this.getStyle('stroke'); + const strokeOpacityProp = this.getStyle('stroke-opacity'); + if (fillStyleProp.isUrlDefinition()) { + const fillStyle = fillStyleProp.getFillStyleDefinition(this, fillOpacityStyleProp); + if (fillStyle) { + ctx.fillStyle = fillStyle; + } + } else if (fillStyleProp.hasValue()) { + if (fillStyleProp.getString() === 'currentColor') { + fillStyleProp.setValue(this.getStyle('color').getColor()); + } + const fillStyle = fillStyleProp.getColor(); + if (fillStyle !== 'inherit') { + ctx.fillStyle = fillStyle === 'none' ? 'rgba(0,0,0,0)' : fillStyle; + } + } + if (fillOpacityStyleProp.hasValue()) { + const fillStyle = new Property(this.document, 'fill', ctx.fillStyle).addOpacity(fillOpacityStyleProp).getColor(); + ctx.fillStyle = fillStyle; + } + // stroke + if (strokeStyleProp.isUrlDefinition()) { + const strokeStyle = strokeStyleProp.getFillStyleDefinition(this, strokeOpacityProp); + if (strokeStyle) { + ctx.strokeStyle = strokeStyle; + } + } else if (strokeStyleProp.hasValue()) { + if (strokeStyleProp.getString() === 'currentColor') { + strokeStyleProp.setValue(this.getStyle('color').getColor()); + } + const strokeStyle = strokeStyleProp.getString(); + if (strokeStyle !== 'inherit') { + ctx.strokeStyle = strokeStyle === 'none' ? 'rgba(0,0,0,0)' : strokeStyle; + } + } + if (strokeOpacityProp.hasValue()) { + const strokeStyle = new Property(this.document, 'stroke', ctx.strokeStyle).addOpacity(strokeOpacityProp).getString(); + ctx.strokeStyle = strokeStyle; + } + const strokeWidthStyleProp = this.getStyle('stroke-width'); + if (strokeWidthStyleProp.hasValue()) { + const newLineWidth = strokeWidthStyleProp.getPixels(); + ctx.lineWidth = !newLineWidth ? PSEUDO_ZERO // browsers don't respect 0 (or node-canvas? :-) + : newLineWidth; + } + const strokeLinecapStyleProp = this.getStyle('stroke-linecap'); + const strokeLinejoinStyleProp = this.getStyle('stroke-linejoin'); + const strokeMiterlimitProp = this.getStyle('stroke-miterlimit'); + // NEED TEST + // const pointOrderStyleProp = this.getStyle('paint-order'); + const strokeDasharrayStyleProp = this.getStyle('stroke-dasharray'); + const strokeDashoffsetProp = this.getStyle('stroke-dashoffset'); + if (strokeLinecapStyleProp.hasValue()) { + ctx.lineCap = strokeLinecapStyleProp.getString(); + } + if (strokeLinejoinStyleProp.hasValue()) { + ctx.lineJoin = strokeLinejoinStyleProp.getString(); + } + if (strokeMiterlimitProp.hasValue()) { + ctx.miterLimit = strokeMiterlimitProp.getNumber(); + } + // NEED TEST + // if (pointOrderStyleProp.hasValue()) { + // // ? + // ctx.paintOrder = pointOrderStyleProp.getValue(); + // } + if (strokeDasharrayStyleProp.hasValue() && strokeDasharrayStyleProp.getString() !== 'none') { + const gaps = toNumbers(strokeDasharrayStyleProp.getString()); + if (typeof ctx.setLineDash !== 'undefined') { + ctx.setLineDash(gaps); + } else // @ts-expect-error Handle browser prefix. + if (typeof ctx.webkitLineDash !== 'undefined') { + // @ts-expect-error Handle browser prefix. + ctx.webkitLineDash = gaps; + } else // @ts-expect-error Handle browser prefix. + if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) { + // @ts-expect-error Handle browser prefix. + ctx.mozDash = gaps; + } + const offset = strokeDashoffsetProp.getPixels(); + if (typeof ctx.lineDashOffset !== 'undefined') { + ctx.lineDashOffset = offset; + } else // @ts-expect-error Handle browser prefix. + if (typeof ctx.webkitLineDashOffset !== 'undefined') { + // @ts-expect-error Handle browser prefix. + ctx.webkitLineDashOffset = offset; + } else // @ts-expect-error Handle browser prefix. + if (typeof ctx.mozDashOffset !== 'undefined') { + // @ts-expect-error Handle browser prefix. + ctx.mozDashOffset = offset; + } + } + } + // font + this.modifiedEmSizeStack = false; + if (typeof ctx.font !== 'undefined') { + const fontStyleProp = this.getStyle('font'); + const fontStyleStyleProp = this.getStyle('font-style'); + const fontVariantStyleProp = this.getStyle('font-variant'); + const fontWeightStyleProp = this.getStyle('font-weight'); + const fontSizeStyleProp = this.getStyle('font-size'); + const fontFamilyStyleProp = this.getStyle('font-family'); + const font = new Font(fontStyleStyleProp.getString(), fontVariantStyleProp.getString(), fontWeightStyleProp.getString(), fontSizeStyleProp.hasValue() ? `${fontSizeStyleProp.getPixels(true)}px` : '', fontFamilyStyleProp.getString(), Font.parse(fontStyleProp.getString(), ctx.font)); + fontStyleStyleProp.setValue(font.fontStyle); + fontVariantStyleProp.setValue(font.fontVariant); + fontWeightStyleProp.setValue(font.fontWeight); + fontSizeStyleProp.setValue(font.fontSize); + fontFamilyStyleProp.setValue(font.fontFamily); + ctx.font = font.toString(); + if (fontSizeStyleProp.isPixels()) { + this.document.emSize = fontSizeStyleProp.getPixels(); + this.modifiedEmSizeStack = true; + } + } + if (!fromMeasure) { + // effects + this.applyEffects(ctx); + // opacity + ctx.globalAlpha = this.calculateOpacity(); + } + } + clearContext(ctx) { + super.clearContext(ctx); + if (this.modifiedEmSizeStack) { + this.document.popEmSize(); + } + } + } + + class TextElement extends RenderedElement { + type = 'text'; + x = 0; + y = 0; + leafTexts = []; + textChunkStart = 0; + minX = Number.POSITIVE_INFINITY; + maxX = Number.NEGATIVE_INFINITY; + measureCache = -1; + constructor(document, node, captureTextNodes){ + super(document, node, new.target === TextElement ? true : captureTextNodes); + } + setContext(ctx) { + let fromMeasure = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + super.setContext(ctx, fromMeasure); + const textBaseline = this.getStyle('dominant-baseline').getTextBaseline() || this.getStyle('alignment-baseline').getTextBaseline(); + if (textBaseline) { + ctx.textBaseline = textBaseline; + } + } + initializeCoordinates() { + this.x = 0; + this.y = 0; + this.leafTexts = []; + this.textChunkStart = 0; + this.minX = Number.POSITIVE_INFINITY; + this.maxX = Number.NEGATIVE_INFINITY; + } + getBoundingBox(ctx) { + if (this.type !== 'text') { + return this.getTElementBoundingBox(ctx); + } + // first, calculate child positions + this.initializeCoordinates(); + this.adjustChildCoordinatesRecursive(ctx); + let boundingBox = null; + // then calculate bounding box + this.children.forEach((_, i)=>{ + const childBoundingBox = this.getChildBoundingBox(ctx, this, this, i); + if (!boundingBox) { + boundingBox = childBoundingBox; + } else { + boundingBox.addBoundingBox(childBoundingBox); + } + }); + return boundingBox; + } + getFontSize() { + const { document, parent } = this; + const inheritFontSize = Font.parse(document.ctx.font).fontSize; + const fontSize = parent.getStyle('font-size').getNumber(inheritFontSize); + return fontSize; + } + getTElementBoundingBox(ctx) { + const fontSize = this.getFontSize(); + return new BoundingBox(this.x, this.y - fontSize, this.x + this.measureText(ctx), this.y); + } + getGlyph(font, text, i) { + const char = text[i]; + let glyph; + if (font.isArabic) { + const len = text.length; + const prevChar = text[i - 1]; + const nextChar = text[i + 1]; + let arabicForm = 'isolated'; + if ((i === 0 || prevChar === ' ') && i < len - 1 && nextChar !== ' ') { + arabicForm = 'terminal'; + } + if (i > 0 && prevChar !== ' ' && i < len - 1 && nextChar !== ' ') { + arabicForm = 'medial'; + } + if (i > 0 && prevChar !== ' ' && (i === len - 1 || nextChar === ' ')) { + arabicForm = 'initial'; + } + glyph = font.arabicGlyphs[char]?.[arabicForm] || font.glyphs[char]; + } else { + glyph = font.glyphs[char]; + } + if (!glyph) { + glyph = font.missingGlyph; + } + return glyph; + } + getText() { + return ''; + } + getTextFromNode(node) { + const textNode = node || this.node; + const childNodes = Array.from(textNode.parentNode.childNodes); + const index = childNodes.indexOf(textNode); + const lastIndex = childNodes.length - 1; + let text = compressSpaces(// textNode.value + // || textNode.text + textNode.textContent || ''); + if (index === 0) { + text = trimLeft(text); + } + if (index === lastIndex) { + text = trimRight(text); + } + return text; + } + renderChildren(ctx) { + if (this.type !== 'text') { + this.renderTElementChildren(ctx); + return; + } + // first, calculate child positions + this.initializeCoordinates(); + this.adjustChildCoordinatesRecursive(ctx); + // then render + this.children.forEach((_, i)=>{ + this.renderChild(ctx, this, this, i); + }); + const { mouse } = this.document.screen; + // Do not calc bounding box if mouse is not working. + if (mouse.isWorking()) { + mouse.checkBoundingBox(this, this.getBoundingBox(ctx)); + } + } + renderTElementChildren(ctx) { + const { document, parent } = this; + const renderText = this.getText(); + const customFont = parent.getStyle('font-family').getDefinition(); + if (customFont) { + const { unitsPerEm } = customFont.fontFace; + const ctxFont = Font.parse(document.ctx.font); + const fontSize = parent.getStyle('font-size').getNumber(ctxFont.fontSize); + const fontStyle = parent.getStyle('font-style').getString(ctxFont.fontStyle); + const scale = fontSize / unitsPerEm; + const text = customFont.isRTL ? renderText.split('').reverse().join('') : renderText; + const dx = toNumbers(parent.getAttribute('dx').getString()); + const len = text.length; + for(let i = 0; i < len; i++){ + const glyph = this.getGlyph(customFont, text, i); + ctx.translate(this.x, this.y); + ctx.scale(scale, -scale); + const lw = ctx.lineWidth; + ctx.lineWidth = ctx.lineWidth * unitsPerEm / fontSize; + if (fontStyle === 'italic') { + ctx.transform(1, 0, .4, 1, 0, 0); + } + glyph.render(ctx); + if (fontStyle === 'italic') { + ctx.transform(1, 0, -.4, 1, 0, 0); + } + ctx.lineWidth = lw; + ctx.scale(1 / scale, -1 / scale); + ctx.translate(-this.x, -this.y); + this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + this.x += dx[i]; + } + } + return; + } + const { x, y } = this; + // NEED TEST + // if (ctx.paintOrder === 'stroke') { + // if (ctx.strokeStyle) { + // ctx.strokeText(renderText, x, y); + // } + // if (ctx.fillStyle) { + // ctx.fillText(renderText, x, y); + // } + // } else { + if (ctx.fillStyle) { + ctx.fillText(renderText, x, y); + } + if (ctx.strokeStyle) { + ctx.strokeText(renderText, x, y); + } + // } + } + applyAnchoring() { + if (this.textChunkStart >= this.leafTexts.length) { + return; + } + // This is basically the "Apply anchoring" part of https://www.w3.org/TR/SVG2/text.html#TextLayoutAlgorithm. + // The difference is that we apply the anchoring as soon as a chunk is finished. This saves some extra looping. + // Vertical text is not supported. + const firstElement = this.leafTexts[this.textChunkStart]; + const textAnchor = firstElement.getStyle('text-anchor').getString('start'); + const isRTL = false // we treat RTL like LTR + ; + let shift = 0; + if (textAnchor === 'start' && !isRTL || textAnchor === 'end' && isRTL) { + shift = firstElement.x - this.minX; + } else if (textAnchor === 'end' && !isRTL || textAnchor === 'start' && isRTL) { + shift = firstElement.x - this.maxX; + } else { + shift = firstElement.x - (this.minX + this.maxX) / 2; + } + for(let i = this.textChunkStart; i < this.leafTexts.length; i++){ + this.leafTexts[i].x += shift; + } + // start new chunk + this.minX = Number.POSITIVE_INFINITY; + this.maxX = Number.NEGATIVE_INFINITY; + this.textChunkStart = this.leafTexts.length; + } + adjustChildCoordinatesRecursive(ctx) { + this.children.forEach((_, i)=>{ + this.adjustChildCoordinatesRecursiveCore(ctx, this, this, i); + }); + this.applyAnchoring(); + } + adjustChildCoordinatesRecursiveCore(ctx, textParent, parent, i) { + const child = parent.children[i]; + if (child.children.length > 0) { + child.children.forEach((_, i)=>{ + textParent.adjustChildCoordinatesRecursiveCore(ctx, textParent, child, i); + }); + } else { + // only leafs are relevant + this.adjustChildCoordinates(ctx, textParent, parent, i); + } + } + adjustChildCoordinates(ctx, textParent, parent, i) { + const child = parent.children[i]; + if (typeof child.measureText !== 'function') { + return child; + } + ctx.save(); + child.setContext(ctx, true); + const xAttr = child.getAttribute('x'); + const yAttr = child.getAttribute('y'); + const dxAttr = child.getAttribute('dx'); + const dyAttr = child.getAttribute('dy'); + const customFont = child.getStyle('font-family').getDefinition(); + const isRTL = Boolean(customFont?.isRTL); + if (i === 0) { + // First children inherit attributes from parent(s). Positional attributes + // are only inherited from a parent to it's first child. + if (!xAttr.hasValue()) { + xAttr.setValue(child.getInheritedAttribute('x')); + } + if (!yAttr.hasValue()) { + yAttr.setValue(child.getInheritedAttribute('y')); + } + if (!dxAttr.hasValue()) { + dxAttr.setValue(child.getInheritedAttribute('dx')); + } + if (!dyAttr.hasValue()) { + dyAttr.setValue(child.getInheritedAttribute('dy')); + } + } + const width = child.measureText(ctx); + if (isRTL) { + textParent.x -= width; + } + if (xAttr.hasValue()) { + // an "x" attribute marks the start of a new chunk + textParent.applyAnchoring(); + child.x = xAttr.getPixels('x'); + if (dxAttr.hasValue()) { + child.x += dxAttr.getPixels('x'); + } + } else { + if (dxAttr.hasValue()) { + textParent.x += dxAttr.getPixels('x'); + } + child.x = textParent.x; + } + textParent.x = child.x; + if (!isRTL) { + textParent.x += width; + } + if (yAttr.hasValue()) { + child.y = yAttr.getPixels('y'); + if (dyAttr.hasValue()) { + child.y += dyAttr.getPixels('y'); + } + } else { + if (dyAttr.hasValue()) { + textParent.y += dyAttr.getPixels('y'); + } + child.y = textParent.y; + } + textParent.y = child.y; + // update the current chunk and it's bounds + textParent.leafTexts.push(child); + textParent.minX = Math.min(textParent.minX, child.x, child.x + width); + textParent.maxX = Math.max(textParent.maxX, child.x, child.x + width); + child.clearContext(ctx); + ctx.restore(); + return child; + } + getChildBoundingBox(ctx, textParent, parent, i) { + const child = parent.children[i]; + // not a text node? + if (typeof child.getBoundingBox !== 'function') { + return null; + } + const boundingBox = child.getBoundingBox(ctx); + if (boundingBox) { + child.children.forEach((_, i)=>{ + const childBoundingBox = textParent.getChildBoundingBox(ctx, textParent, child, i); + boundingBox.addBoundingBox(childBoundingBox); + }); + } + return boundingBox; + } + renderChild(ctx, textParent, parent, i) { + const child = parent.children[i]; + child.render(ctx); + child.children.forEach((_, i)=>{ + textParent.renderChild(ctx, textParent, child, i); + }); + } + measureText(ctx) { + const { measureCache } = this; + if (~measureCache) { + return measureCache; + } + const renderText = this.getText(); + const measure = this.measureTargetText(ctx, renderText); + this.measureCache = measure; + return measure; + } + measureTargetText(ctx, targetText) { + if (!targetText.length) { + return 0; + } + const { parent } = this; + const customFont = parent.getStyle('font-family').getDefinition(); + if (customFont) { + const fontSize = this.getFontSize(); + const text = customFont.isRTL ? targetText.split('').reverse().join('') : targetText; + const dx = toNumbers(parent.getAttribute('dx').getString()); + const len = text.length; + let measure = 0; + for(let i = 0; i < len; i++){ + const glyph = this.getGlyph(customFont, text, i); + measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + measure += dx[i]; + } + } + return measure; + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!ctx.measureText) { + return targetText.length * 10; + } + ctx.save(); + this.setContext(ctx, true); + const { width: measure } = ctx.measureText(targetText); + this.clearContext(ctx); + ctx.restore(); + return measure; + } + /** + * Inherits positional attributes from {@link TextElement} parent(s). Attributes + * are only inherited from a parent to its first child. + * @param name - The attribute name. + * @returns The attribute value or null. + */ getInheritedAttribute(name) { + // eslint-disable-next-line @typescript-eslint/no-this-alias,consistent-this + let current = this; + while(current instanceof TextElement && current.isFirstChild() && current.parent){ + const parentAttr = current.parent.getAttribute(name); + if (parentAttr.hasValue(true)) { + return parentAttr.getString('0'); + } + current = current.parent; + } + return null; + } + } + + class TSpanElement extends TextElement { + type = 'tspan'; + text; + constructor(document, node, captureTextNodes){ + super(document, node, new.target === TSpanElement ? true : captureTextNodes); + // if this node has children, then they own the text + this.text = this.children.length > 0 ? '' : this.getTextFromNode(); + } + getText() { + return this.text; + } + } + + class TextNode extends TSpanElement { + type = 'textNode'; + } + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ var t = function(r, e) { + return (t = Object.setPrototypeOf || ({ + __proto__: [] + }) instanceof Array && function(t, r) { + t.__proto__ = r; + } || function(t, r) { + for(var e in r)Object.prototype.hasOwnProperty.call(r, e) && (t[e] = r[e]); + })(r, e); + }; + function r(r, e) { + if ("function" != typeof e && null !== e) throw new TypeError("Class extends value " + String(e) + " is not a constructor or null"); + function i() { + this.constructor = r; + } + t(r, e), r.prototype = null === e ? Object.create(e) : (i.prototype = e.prototype, new i); + } + function e(t) { + var r = ""; + Array.isArray(t) || (t = [ + t + ]); + for(var e = 0; e < t.length; e++){ + var i = t[e]; + if (i.type === _.CLOSE_PATH) r += "z"; + else if (i.type === _.HORIZ_LINE_TO) r += (i.relative ? "h" : "H") + i.x; + else if (i.type === _.VERT_LINE_TO) r += (i.relative ? "v" : "V") + i.y; + else if (i.type === _.MOVE_TO) r += (i.relative ? "m" : "M") + i.x + " " + i.y; + else if (i.type === _.LINE_TO) r += (i.relative ? "l" : "L") + i.x + " " + i.y; + else if (i.type === _.CURVE_TO) r += (i.relative ? "c" : "C") + i.x1 + " " + i.y1 + " " + i.x2 + " " + i.y2 + " " + i.x + " " + i.y; + else if (i.type === _.SMOOTH_CURVE_TO) r += (i.relative ? "s" : "S") + i.x2 + " " + i.y2 + " " + i.x + " " + i.y; + else if (i.type === _.QUAD_TO) r += (i.relative ? "q" : "Q") + i.x1 + " " + i.y1 + " " + i.x + " " + i.y; + else if (i.type === _.SMOOTH_QUAD_TO) r += (i.relative ? "t" : "T") + i.x + " " + i.y; + else { + if (i.type !== _.ARC) throw new Error('Unexpected command type "' + i.type + '" at index ' + e + "."); + r += (i.relative ? "a" : "A") + i.rX + " " + i.rY + " " + i.xRot + " " + +i.lArcFlag + " " + +i.sweepFlag + " " + i.x + " " + i.y; + } + } + return r; + } + function i(t, r) { + var e = t[0], i = t[1]; + return [ + e * Math.cos(r) - i * Math.sin(r), + e * Math.sin(r) + i * Math.cos(r) + ]; + } + function a() { + for(var t = [], r = 0; r < arguments.length; r++)t[r] = arguments[r]; + for(var e = 0; e < t.length; e++)if ("number" != typeof t[e]) throw new Error("assertNumbers arguments[" + e + "] is not a number. " + typeof t[e] + " == typeof " + t[e]); + return !0; + } + var n = Math.PI; + function o(t, r, e) { + t.lArcFlag = 0 === t.lArcFlag ? 0 : 1, t.sweepFlag = 0 === t.sweepFlag ? 0 : 1; + var a = t.rX, o = t.rY, s = t.x, u = t.y; + a = Math.abs(t.rX), o = Math.abs(t.rY); + var h = i([ + (r - s) / 2, + (e - u) / 2 + ], -t.xRot / 180 * n), c = h[0], y = h[1], p = Math.pow(c, 2) / Math.pow(a, 2) + Math.pow(y, 2) / Math.pow(o, 2); + 1 < p && (a *= Math.sqrt(p), o *= Math.sqrt(p)), t.rX = a, t.rY = o; + var m = Math.pow(a, 2) * Math.pow(y, 2) + Math.pow(o, 2) * Math.pow(c, 2), O = (t.lArcFlag !== t.sweepFlag ? 1 : -1) * Math.sqrt(Math.max(0, (Math.pow(a, 2) * Math.pow(o, 2) - m) / m)), l = a * y / o * O, T = -o * c / a * O, v = i([ + l, + T + ], t.xRot / 180 * n); + t.cX = v[0] + (r + s) / 2, t.cY = v[1] + (e + u) / 2, t.phi1 = Math.atan2((y - T) / o, (c - l) / a), t.phi2 = Math.atan2((-y - T) / o, (-c - l) / a), 0 === t.sweepFlag && t.phi2 > t.phi1 && (t.phi2 -= 2 * n), 1 === t.sweepFlag && t.phi2 < t.phi1 && (t.phi2 += 2 * n), t.phi1 *= 180 / n, t.phi2 *= 180 / n; + } + function s(t, r, e) { + a(t, r, e); + var i = t * t + r * r - e * e; + if (0 > i) return []; + if (0 === i) return [ + [ + t * e / (t * t + r * r), + r * e / (t * t + r * r) + ] + ]; + var n = Math.sqrt(i); + return [ + [ + (t * e + r * n) / (t * t + r * r), + (r * e - t * n) / (t * t + r * r) + ], + [ + (t * e - r * n) / (t * t + r * r), + (r * e + t * n) / (t * t + r * r) + ] + ]; + } + var u, h = Math.PI / 180; + function c$1(t, r, e) { + return (1 - e) * t + e * r; + } + function y(t, r, e, i) { + return t + Math.cos(i / 180 * n) * r + Math.sin(i / 180 * n) * e; + } + function p(t, r, e, i) { + var a = 1e-6, n = r - t, o = e - r, s = 3 * n + 3 * (i - e) - 6 * o, u = 6 * (o - n), h = 3 * n; + return Math.abs(s) < a ? [ + -h / u + ] : function(t, r, e) { + void 0 === e && (e = 1e-6); + var i = t * t / 4 - r; + if (i < -e) return []; + if (i <= e) return [ + -t / 2 + ]; + var a = Math.sqrt(i); + return [ + -t / 2 - a, + -t / 2 + a + ]; + }(u / s, h / s, a); + } + function m$1(t, r, e, i, a) { + var n = 1 - a; + return t * (n * n * n) + r * (3 * n * n * a) + e * (3 * n * a * a) + i * (a * a * a); + } + !function(t) { + function r() { + return u(function(t, r, e) { + return t.relative && (void 0 !== t.x1 && (t.x1 += r), void 0 !== t.y1 && (t.y1 += e), void 0 !== t.x2 && (t.x2 += r), void 0 !== t.y2 && (t.y2 += e), void 0 !== t.x && (t.x += r), void 0 !== t.y && (t.y += e), t.relative = !1), t; + }); + } + function e() { + var t = NaN, r = NaN, e = NaN, i = NaN; + return u(function(a, n, o) { + return a.type & _.SMOOTH_CURVE_TO && (a.type = _.CURVE_TO, t = isNaN(t) ? n : t, r = isNaN(r) ? o : r, a.x1 = a.relative ? n - t : 2 * n - t, a.y1 = a.relative ? o - r : 2 * o - r), a.type & _.CURVE_TO ? (t = a.relative ? n + a.x2 : a.x2, r = a.relative ? o + a.y2 : a.y2) : (t = NaN, r = NaN), a.type & _.SMOOTH_QUAD_TO && (a.type = _.QUAD_TO, e = isNaN(e) ? n : e, i = isNaN(i) ? o : i, a.x1 = a.relative ? n - e : 2 * n - e, a.y1 = a.relative ? o - i : 2 * o - i), a.type & _.QUAD_TO ? (e = a.relative ? n + a.x1 : a.x1, i = a.relative ? o + a.y1 : a.y1) : (e = NaN, i = NaN), a; + }); + } + function n() { + var t = NaN, r = NaN; + return u(function(e, i, a) { + if (e.type & _.SMOOTH_QUAD_TO && (e.type = _.QUAD_TO, t = isNaN(t) ? i : t, r = isNaN(r) ? a : r, e.x1 = e.relative ? i - t : 2 * i - t, e.y1 = e.relative ? a - r : 2 * a - r), e.type & _.QUAD_TO) { + t = e.relative ? i + e.x1 : e.x1, r = e.relative ? a + e.y1 : e.y1; + var n = e.x1, o = e.y1; + e.type = _.CURVE_TO, e.x1 = ((e.relative ? 0 : i) + 2 * n) / 3, e.y1 = ((e.relative ? 0 : a) + 2 * o) / 3, e.x2 = (e.x + 2 * n) / 3, e.y2 = (e.y + 2 * o) / 3; + } else t = NaN, r = NaN; + return e; + }); + } + function u(t) { + var r = 0, e = 0, i = NaN, a = NaN; + return function(n) { + if (isNaN(i) && !(n.type & _.MOVE_TO)) throw new Error("path must start with moveto"); + var o = t(n, r, e, i, a); + return n.type & _.CLOSE_PATH && (r = i, e = a), void 0 !== n.x && (r = n.relative ? r + n.x : n.x), void 0 !== n.y && (e = n.relative ? e + n.y : n.y), n.type & _.MOVE_TO && (i = r, a = e), o; + }; + } + function O(t, r, e, i, n, o) { + return a(t, r, e, i, n, o), u(function(a, s, u, h) { + var c = a.x1, y = a.x2, p = a.relative && !isNaN(h), m = void 0 !== a.x ? a.x : p ? 0 : s, O = void 0 !== a.y ? a.y : p ? 0 : u; + function l(t) { + return t * t; + } + a.type & _.HORIZ_LINE_TO && 0 !== r && (a.type = _.LINE_TO, a.y = a.relative ? 0 : u), a.type & _.VERT_LINE_TO && 0 !== e && (a.type = _.LINE_TO, a.x = a.relative ? 0 : s), void 0 !== a.x && (a.x = a.x * t + O * e + (p ? 0 : n)), void 0 !== a.y && (a.y = m * r + a.y * i + (p ? 0 : o)), void 0 !== a.x1 && (a.x1 = a.x1 * t + a.y1 * e + (p ? 0 : n)), void 0 !== a.y1 && (a.y1 = c * r + a.y1 * i + (p ? 0 : o)), void 0 !== a.x2 && (a.x2 = a.x2 * t + a.y2 * e + (p ? 0 : n)), void 0 !== a.y2 && (a.y2 = y * r + a.y2 * i + (p ? 0 : o)); + var T = t * i - r * e; + if (void 0 !== a.xRot && (1 !== t || 0 !== r || 0 !== e || 1 !== i)) if (0 === T) delete a.rX, delete a.rY, delete a.xRot, delete a.lArcFlag, delete a.sweepFlag, a.type = _.LINE_TO; + else { + var v = a.xRot * Math.PI / 180, f = Math.sin(v), N = Math.cos(v), x = 1 / l(a.rX), d = 1 / l(a.rY), E = l(N) * x + l(f) * d, A = 2 * f * N * (x - d), C = l(f) * x + l(N) * d, M = E * i * i - A * r * i + C * r * r, R = A * (t * i + r * e) - 2 * (E * e * i + C * t * r), g = E * e * e - A * t * e + C * t * t, I = (Math.atan2(R, M - g) + Math.PI) % Math.PI / 2, S = Math.sin(I), L = Math.cos(I); + a.rX = Math.abs(T) / Math.sqrt(M * l(L) + R * S * L + g * l(S)), a.rY = Math.abs(T) / Math.sqrt(M * l(S) - R * S * L + g * l(L)), a.xRot = 180 * I / Math.PI; + } + return void 0 !== a.sweepFlag && 0 > T && (a.sweepFlag = +!a.sweepFlag), a; + }); + } + function l() { + return function(t) { + var r = {}; + for(var e in t)r[e] = t[e]; + return r; + }; + } + t.ROUND = function(t) { + function r(r) { + return Math.round(r * t) / t; + } + return void 0 === t && (t = 1e13), a(t), function(t) { + return void 0 !== t.x1 && (t.x1 = r(t.x1)), void 0 !== t.y1 && (t.y1 = r(t.y1)), void 0 !== t.x2 && (t.x2 = r(t.x2)), void 0 !== t.y2 && (t.y2 = r(t.y2)), void 0 !== t.x && (t.x = r(t.x)), void 0 !== t.y && (t.y = r(t.y)), void 0 !== t.rX && (t.rX = r(t.rX)), void 0 !== t.rY && (t.rY = r(t.rY)), t; + }; + }, t.TO_ABS = r, t.TO_REL = function() { + return u(function(t, r, e) { + return t.relative || (void 0 !== t.x1 && (t.x1 -= r), void 0 !== t.y1 && (t.y1 -= e), void 0 !== t.x2 && (t.x2 -= r), void 0 !== t.y2 && (t.y2 -= e), void 0 !== t.x && (t.x -= r), void 0 !== t.y && (t.y -= e), t.relative = !0), t; + }); + }, t.NORMALIZE_HVZ = function(t, r, e) { + return void 0 === t && (t = !0), void 0 === r && (r = !0), void 0 === e && (e = !0), u(function(i, a, n, o, s) { + if (isNaN(o) && !(i.type & _.MOVE_TO)) throw new Error("path must start with moveto"); + return r && i.type & _.HORIZ_LINE_TO && (i.type = _.LINE_TO, i.y = i.relative ? 0 : n), e && i.type & _.VERT_LINE_TO && (i.type = _.LINE_TO, i.x = i.relative ? 0 : a), t && i.type & _.CLOSE_PATH && (i.type = _.LINE_TO, i.x = i.relative ? o - a : o, i.y = i.relative ? s - n : s), i.type & _.ARC && (0 === i.rX || 0 === i.rY) && (i.type = _.LINE_TO, delete i.rX, delete i.rY, delete i.xRot, delete i.lArcFlag, delete i.sweepFlag), i; + }); + }, t.NORMALIZE_ST = e, t.QT_TO_C = n, t.INFO = u, t.SANITIZE = function(t) { + void 0 === t && (t = 0), a(t); + var r = NaN, e = NaN, i = NaN, n = NaN; + return u(function(a, o, s, u, h) { + var c = Math.abs, y = !1, p = 0, m = 0; + if (a.type & _.SMOOTH_CURVE_TO && (p = isNaN(r) ? 0 : o - r, m = isNaN(e) ? 0 : s - e), a.type & (_.CURVE_TO | _.SMOOTH_CURVE_TO) ? (r = a.relative ? o + a.x2 : a.x2, e = a.relative ? s + a.y2 : a.y2) : (r = NaN, e = NaN), a.type & _.SMOOTH_QUAD_TO ? (i = isNaN(i) ? o : 2 * o - i, n = isNaN(n) ? s : 2 * s - n) : a.type & _.QUAD_TO ? (i = a.relative ? o + a.x1 : a.x1, n = a.relative ? s + a.y1 : a.y2) : (i = NaN, n = NaN), a.type & _.LINE_COMMANDS || a.type & _.ARC && (0 === a.rX || 0 === a.rY || !a.lArcFlag) || a.type & _.CURVE_TO || a.type & _.SMOOTH_CURVE_TO || a.type & _.QUAD_TO || a.type & _.SMOOTH_QUAD_TO) { + var O = void 0 === a.x ? 0 : a.relative ? a.x : a.x - o, l = void 0 === a.y ? 0 : a.relative ? a.y : a.y - s; + p = isNaN(i) ? void 0 === a.x1 ? p : a.relative ? a.x : a.x1 - o : i - o, m = isNaN(n) ? void 0 === a.y1 ? m : a.relative ? a.y : a.y1 - s : n - s; + var T = void 0 === a.x2 ? 0 : a.relative ? a.x : a.x2 - o, v = void 0 === a.y2 ? 0 : a.relative ? a.y : a.y2 - s; + c(O) <= t && c(l) <= t && c(p) <= t && c(m) <= t && c(T) <= t && c(v) <= t && (y = !0); + } + return a.type & _.CLOSE_PATH && c(o - u) <= t && c(s - h) <= t && (y = !0), y ? [] : a; + }); + }, t.MATRIX = O, t.ROTATE = function(t, r, e) { + void 0 === r && (r = 0), void 0 === e && (e = 0), a(t, r, e); + var i = Math.sin(t), n = Math.cos(t); + return O(n, i, -i, n, r - r * n + e * i, e - r * i - e * n); + }, t.TRANSLATE = function(t, r) { + return void 0 === r && (r = 0), a(t, r), O(1, 0, 0, 1, t, r); + }, t.SCALE = function(t, r) { + return void 0 === r && (r = t), a(t, r), O(t, 0, 0, r, 0, 0); + }, t.SKEW_X = function(t) { + return a(t), O(1, 0, Math.atan(t), 1, 0, 0); + }, t.SKEW_Y = function(t) { + return a(t), O(1, Math.atan(t), 0, 1, 0, 0); + }, t.X_AXIS_SYMMETRY = function(t) { + return void 0 === t && (t = 0), a(t), O(-1, 0, 0, 1, t, 0); + }, t.Y_AXIS_SYMMETRY = function(t) { + return void 0 === t && (t = 0), a(t), O(1, 0, 0, -1, 0, t); + }, t.A_TO_C = function() { + return u(function(t, r, e) { + return _.ARC === t.type ? function(t, r, e) { + var a, n, s, u; + t.cX || o(t, r, e); + for(var y = Math.min(t.phi1, t.phi2), p = Math.max(t.phi1, t.phi2) - y, m = Math.ceil(p / 90), O = new Array(m), l = r, T = e, v = 0; v < m; v++){ + var f = c$1(t.phi1, t.phi2, v / m), N = c$1(t.phi1, t.phi2, (v + 1) / m), x = N - f, d = 4 / 3 * Math.tan(x * h / 4), E = [ + Math.cos(f * h) - d * Math.sin(f * h), + Math.sin(f * h) + d * Math.cos(f * h) + ], A = E[0], C = E[1], M = [ + Math.cos(N * h), + Math.sin(N * h) + ], R = M[0], g = M[1], I = [ + R + d * Math.sin(N * h), + g - d * Math.cos(N * h) + ], S = I[0], L = I[1]; + O[v] = { + relative: t.relative, + type: _.CURVE_TO + }; + var H = function(r, e) { + var a = i([ + r * t.rX, + e * t.rY + ], t.xRot), n = a[0], o = a[1]; + return [ + t.cX + n, + t.cY + o + ]; + }; + a = H(A, C), O[v].x1 = a[0], O[v].y1 = a[1], n = H(S, L), O[v].x2 = n[0], O[v].y2 = n[1], s = H(R, g), O[v].x = s[0], O[v].y = s[1], t.relative && (O[v].x1 -= l, O[v].y1 -= T, O[v].x2 -= l, O[v].y2 -= T, O[v].x -= l, O[v].y -= T), l = (u = [ + O[v].x, + O[v].y + ])[0], T = u[1]; + } + return O; + }(t, t.relative ? 0 : r, t.relative ? 0 : e) : t; + }); + }, t.ANNOTATE_ARCS = function() { + return u(function(t, r, e) { + return t.relative && (r = 0, e = 0), _.ARC === t.type && o(t, r, e), t; + }); + }, t.CLONE = l, t.CALCULATE_BOUNDS = function() { + var t = function(t) { + var r = {}; + for(var e in t)r[e] = t[e]; + return r; + }, i = r(), a = n(), h = e(), c = u(function(r, e, n) { + var u = h(a(i(t(r)))); + function O(t) { + t > c.maxX && (c.maxX = t), t < c.minX && (c.minX = t); + } + function l(t) { + t > c.maxY && (c.maxY = t), t < c.minY && (c.minY = t); + } + if (u.type & _.DRAWING_COMMANDS && (O(e), l(n)), u.type & _.HORIZ_LINE_TO && O(u.x), u.type & _.VERT_LINE_TO && l(u.y), u.type & _.LINE_TO && (O(u.x), l(u.y)), u.type & _.CURVE_TO) { + O(u.x), l(u.y); + for(var T = 0, v = p(e, u.x1, u.x2, u.x); T < v.length; T++){ + 0 < (w = v[T]) && 1 > w && O(m$1(e, u.x1, u.x2, u.x, w)); + } + for(var f = 0, N = p(n, u.y1, u.y2, u.y); f < N.length; f++){ + 0 < (w = N[f]) && 1 > w && l(m$1(n, u.y1, u.y2, u.y, w)); + } + } + if (u.type & _.ARC) { + O(u.x), l(u.y), o(u, e, n); + for(var x = u.xRot / 180 * Math.PI, d = Math.cos(x) * u.rX, E = Math.sin(x) * u.rX, A = -Math.sin(x) * u.rY, C = Math.cos(x) * u.rY, M = u.phi1 < u.phi2 ? [ + u.phi1, + u.phi2 + ] : -180 > u.phi2 ? [ + u.phi2 + 360, + u.phi1 + 360 + ] : [ + u.phi2, + u.phi1 + ], R = M[0], g = M[1], I = function(t) { + var r = t[0], e = t[1], i = 180 * Math.atan2(e, r) / Math.PI; + return i < R ? i + 360 : i; + }, S = 0, L = s(A, -d, 0).map(I); S < L.length; S++){ + (w = L[S]) > R && w < g && O(y(u.cX, d, A, w)); + } + for(var H = 0, U = s(C, -E, 0).map(I); H < U.length; H++){ + var w; + (w = U[H]) > R && w < g && l(y(u.cY, E, C, w)); + } + } + return r; + }); + return c.minX = 1 / 0, c.maxX = -1 / 0, c.minY = 1 / 0, c.maxY = -1 / 0, c; + }; + }(u || (u = {})); + var O, l = function() { + function t() {} + return t.prototype.round = function(t) { + return this.transform(u.ROUND(t)); + }, t.prototype.toAbs = function() { + return this.transform(u.TO_ABS()); + }, t.prototype.toRel = function() { + return this.transform(u.TO_REL()); + }, t.prototype.normalizeHVZ = function(t, r, e) { + return this.transform(u.NORMALIZE_HVZ(t, r, e)); + }, t.prototype.normalizeST = function() { + return this.transform(u.NORMALIZE_ST()); + }, t.prototype.qtToC = function() { + return this.transform(u.QT_TO_C()); + }, t.prototype.aToC = function() { + return this.transform(u.A_TO_C()); + }, t.prototype.sanitize = function(t) { + return this.transform(u.SANITIZE(t)); + }, t.prototype.translate = function(t, r) { + return this.transform(u.TRANSLATE(t, r)); + }, t.prototype.scale = function(t, r) { + return this.transform(u.SCALE(t, r)); + }, t.prototype.rotate = function(t, r, e) { + return this.transform(u.ROTATE(t, r, e)); + }, t.prototype.matrix = function(t, r, e, i, a, n) { + return this.transform(u.MATRIX(t, r, e, i, a, n)); + }, t.prototype.skewX = function(t) { + return this.transform(u.SKEW_X(t)); + }, t.prototype.skewY = function(t) { + return this.transform(u.SKEW_Y(t)); + }, t.prototype.xSymmetry = function(t) { + return this.transform(u.X_AXIS_SYMMETRY(t)); + }, t.prototype.ySymmetry = function(t) { + return this.transform(u.Y_AXIS_SYMMETRY(t)); + }, t.prototype.annotateArcs = function() { + return this.transform(u.ANNOTATE_ARCS()); + }, t; + }(), T = function(t) { + return " " === t || "\t" === t || "\r" === t || "\n" === t; + }, v = function(t) { + return "0".charCodeAt(0) <= t.charCodeAt(0) && t.charCodeAt(0) <= "9".charCodeAt(0); + }, f = function(t) { + function e() { + var r = t.call(this) || this; + return r.curNumber = "", r.curCommandType = -1, r.curCommandRelative = !1, r.canParseCommandOrComma = !0, r.curNumberHasExp = !1, r.curNumberHasExpDigits = !1, r.curNumberHasDecimal = !1, r.curArgs = [], r; + } + return r(e, t), e.prototype.finish = function(t) { + if (void 0 === t && (t = []), this.parse(" ", t), 0 !== this.curArgs.length || !this.canParseCommandOrComma) throw new SyntaxError("Unterminated command at the path end."); + return t; + }, e.prototype.parse = function(t, r) { + var e = this; + void 0 === r && (r = []); + for(var i = function(t) { + r.push(t), e.curArgs.length = 0, e.canParseCommandOrComma = !0; + }, a = 0; a < t.length; a++){ + var n = t[a], o = !(this.curCommandType !== _.ARC || 3 !== this.curArgs.length && 4 !== this.curArgs.length || 1 !== this.curNumber.length || "0" !== this.curNumber && "1" !== this.curNumber), s = v(n) && ("0" === this.curNumber && "0" === n || o); + if (!v(n) || s) if ("e" !== n && "E" !== n) if ("-" !== n && "+" !== n || !this.curNumberHasExp || this.curNumberHasExpDigits) if ("." !== n || this.curNumberHasExp || this.curNumberHasDecimal || o) { + if (this.curNumber && -1 !== this.curCommandType) { + var u = Number(this.curNumber); + if (isNaN(u)) throw new SyntaxError("Invalid number ending at " + a); + if (this.curCommandType === _.ARC) { + if (0 === this.curArgs.length || 1 === this.curArgs.length) { + if (0 > u) throw new SyntaxError('Expected positive number, got "' + u + '" at index "' + a + '"'); + } else if ((3 === this.curArgs.length || 4 === this.curArgs.length) && "0" !== this.curNumber && "1" !== this.curNumber) throw new SyntaxError('Expected a flag, got "' + this.curNumber + '" at index "' + a + '"'); + } + this.curArgs.push(u), this.curArgs.length === N[this.curCommandType] && (_.HORIZ_LINE_TO === this.curCommandType ? i({ + type: _.HORIZ_LINE_TO, + relative: this.curCommandRelative, + x: u + }) : _.VERT_LINE_TO === this.curCommandType ? i({ + type: _.VERT_LINE_TO, + relative: this.curCommandRelative, + y: u + }) : this.curCommandType === _.MOVE_TO || this.curCommandType === _.LINE_TO || this.curCommandType === _.SMOOTH_QUAD_TO ? (i({ + type: this.curCommandType, + relative: this.curCommandRelative, + x: this.curArgs[0], + y: this.curArgs[1] + }), _.MOVE_TO === this.curCommandType && (this.curCommandType = _.LINE_TO)) : this.curCommandType === _.CURVE_TO ? i({ + type: _.CURVE_TO, + relative: this.curCommandRelative, + x1: this.curArgs[0], + y1: this.curArgs[1], + x2: this.curArgs[2], + y2: this.curArgs[3], + x: this.curArgs[4], + y: this.curArgs[5] + }) : this.curCommandType === _.SMOOTH_CURVE_TO ? i({ + type: _.SMOOTH_CURVE_TO, + relative: this.curCommandRelative, + x2: this.curArgs[0], + y2: this.curArgs[1], + x: this.curArgs[2], + y: this.curArgs[3] + }) : this.curCommandType === _.QUAD_TO ? i({ + type: _.QUAD_TO, + relative: this.curCommandRelative, + x1: this.curArgs[0], + y1: this.curArgs[1], + x: this.curArgs[2], + y: this.curArgs[3] + }) : this.curCommandType === _.ARC && i({ + type: _.ARC, + relative: this.curCommandRelative, + rX: this.curArgs[0], + rY: this.curArgs[1], + xRot: this.curArgs[2], + lArcFlag: this.curArgs[3], + sweepFlag: this.curArgs[4], + x: this.curArgs[5], + y: this.curArgs[6] + })), this.curNumber = "", this.curNumberHasExpDigits = !1, this.curNumberHasExp = !1, this.curNumberHasDecimal = !1, this.canParseCommandOrComma = !0; + } + if (!T(n)) if ("," === n && this.canParseCommandOrComma) this.canParseCommandOrComma = !1; + else if ("+" !== n && "-" !== n && "." !== n) if (s) this.curNumber = n, this.curNumberHasDecimal = !1; + else { + if (0 !== this.curArgs.length) throw new SyntaxError("Unterminated command at index " + a + "."); + if (!this.canParseCommandOrComma) throw new SyntaxError('Unexpected character "' + n + '" at index ' + a + ". Command cannot follow comma"); + if (this.canParseCommandOrComma = !1, "z" !== n && "Z" !== n) if ("h" === n || "H" === n) this.curCommandType = _.HORIZ_LINE_TO, this.curCommandRelative = "h" === n; + else if ("v" === n || "V" === n) this.curCommandType = _.VERT_LINE_TO, this.curCommandRelative = "v" === n; + else if ("m" === n || "M" === n) this.curCommandType = _.MOVE_TO, this.curCommandRelative = "m" === n; + else if ("l" === n || "L" === n) this.curCommandType = _.LINE_TO, this.curCommandRelative = "l" === n; + else if ("c" === n || "C" === n) this.curCommandType = _.CURVE_TO, this.curCommandRelative = "c" === n; + else if ("s" === n || "S" === n) this.curCommandType = _.SMOOTH_CURVE_TO, this.curCommandRelative = "s" === n; + else if ("q" === n || "Q" === n) this.curCommandType = _.QUAD_TO, this.curCommandRelative = "q" === n; + else if ("t" === n || "T" === n) this.curCommandType = _.SMOOTH_QUAD_TO, this.curCommandRelative = "t" === n; + else { + if ("a" !== n && "A" !== n) throw new SyntaxError('Unexpected character "' + n + '" at index ' + a + "."); + this.curCommandType = _.ARC, this.curCommandRelative = "a" === n; + } + else r.push({ + type: _.CLOSE_PATH + }), this.canParseCommandOrComma = !0, this.curCommandType = -1; + } + else this.curNumber = n, this.curNumberHasDecimal = "." === n; + } else this.curNumber += n, this.curNumberHasDecimal = !0; + else this.curNumber += n; + else this.curNumber += n, this.curNumberHasExp = !0; + else this.curNumber += n, this.curNumberHasExpDigits = this.curNumberHasExp; + } + return r; + }, e.prototype.transform = function(t) { + return Object.create(this, { + parse: { + value: function(r, e) { + void 0 === e && (e = []); + for(var i = 0, a = Object.getPrototypeOf(this).parse.call(this, r); i < a.length; i++){ + var n = a[i], o = t(n); + Array.isArray(o) ? e.push.apply(e, o) : e.push(o); + } + return e; + } + } + }); + }, e; + }(l), _ = function(t) { + function i(r) { + var e = t.call(this) || this; + return e.commands = "string" == typeof r ? i.parse(r) : r, e; + } + return r(i, t), i.prototype.encode = function() { + return i.encode(this.commands); + }, i.prototype.getBounds = function() { + var t = u.CALCULATE_BOUNDS(); + return this.transform(t), t; + }, i.prototype.transform = function(t) { + for(var r = [], e = 0, i = this.commands; e < i.length; e++){ + var a = t(i[e]); + Array.isArray(a) ? r.push.apply(r, a) : r.push(a); + } + return this.commands = r, this; + }, i.encode = function(t) { + return e(t); + }, i.parse = function(t) { + var r = new f, e = []; + return r.parse(t, e), r.finish(e), e; + }, i.CLOSE_PATH = 1, i.MOVE_TO = 2, i.HORIZ_LINE_TO = 4, i.VERT_LINE_TO = 8, i.LINE_TO = 16, i.CURVE_TO = 32, i.SMOOTH_CURVE_TO = 64, i.QUAD_TO = 128, i.SMOOTH_QUAD_TO = 256, i.ARC = 512, i.LINE_COMMANDS = i.LINE_TO | i.HORIZ_LINE_TO | i.VERT_LINE_TO, i.DRAWING_COMMANDS = i.HORIZ_LINE_TO | i.VERT_LINE_TO | i.LINE_TO | i.CURVE_TO | i.SMOOTH_CURVE_TO | i.QUAD_TO | i.SMOOTH_QUAD_TO | i.ARC, i; + }(l), N = ((O = {})[_.MOVE_TO] = 2, O[_.LINE_TO] = 2, O[_.HORIZ_LINE_TO] = 1, O[_.VERT_LINE_TO] = 1, O[_.CLOSE_PATH] = 0, O[_.QUAD_TO] = 4, O[_.SMOOTH_QUAD_TO] = 2, O[_.CURVE_TO] = 6, O[_.SMOOTH_CURVE_TO] = 4, O[_.ARC] = 7, O); + + class PathParser extends _ { + control = new Point(0, 0); + start = new Point(0, 0); + current = new Point(0, 0); + command = null; + commands = this.commands; + i = -1; + previousCommand = null; + points = []; + angles = []; + constructor(path){ + super(path// Fix spaces after signs. + .replace(/([+\-.])\s+/gm, '$1')// Remove invalid part. + .replace(/[^MmZzLlHhVvCcSsQqTtAae\d\s.,+-].*/g, '')); + } + reset() { + this.i = -1; + this.command = null; + this.previousCommand = null; + this.start = new Point(0, 0); + this.control = new Point(0, 0); + this.current = new Point(0, 0); + this.points = []; + this.angles = []; + } + isEnd() { + const { i, commands } = this; + return i >= commands.length - 1; + } + next() { + const command = this.commands[++this.i]; + this.previousCommand = this.command; + this.command = command; + return command; + } + getPoint() { + let xProp = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : 'x', yProp = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 'y'; + const point = new Point(this.command[xProp], this.command[yProp]); + return this.makeAbsolute(point); + } + getAsControlPoint(xProp, yProp) { + const point = this.getPoint(xProp, yProp); + this.control = point; + return point; + } + getAsCurrentPoint(xProp, yProp) { + const point = this.getPoint(xProp, yProp); + this.current = point; + return point; + } + getReflectedControlPoint() { + const previousCommand = this.previousCommand.type; + if (previousCommand !== _.CURVE_TO && previousCommand !== _.SMOOTH_CURVE_TO && previousCommand !== _.QUAD_TO && previousCommand !== _.SMOOTH_QUAD_TO) { + return this.current; + } + // reflect point + const { current: { x: cx, y: cy }, control: { x: ox, y: oy } } = this; + const point = new Point(2 * cx - ox, 2 * cy - oy); + return point; + } + makeAbsolute(point) { + if (this.command.relative) { + const { x, y } = this.current; + point.x += x; + point.y += y; + } + return point; + } + addMarker(point, from, priorTo) { + const { points, angles } = this; + // if the last angle isn't filled in because we didn't have this point yet ... + if (priorTo && angles.length > 0 && !angles[angles.length - 1]) { + angles[angles.length - 1] = points[points.length - 1].angleTo(priorTo); + } + this.addMarkerAngle(point, from ? from.angleTo(point) : null); + } + addMarkerAngle(point, angle) { + this.points.push(point); + this.angles.push(angle); + } + getMarkerPoints() { + return this.points; + } + getMarkerAngles() { + const { angles } = this; + const len = angles.length; + for(let i = 0; i < len; i++){ + if (!angles[i]) { + for(let j = i + 1; j < len; j++){ + if (angles[j]) { + angles[i] = angles[j]; + break; + } + } + } + } + return angles; + } + } + + class PathElement extends RenderedElement { + type = 'path'; + pathParser; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + this.pathParser = new PathParser(this.getAttribute('d').getString()); + } + path(ctx) { + const { pathParser } = this; + const boundingBox = new BoundingBox(); + pathParser.reset(); + if (ctx) { + ctx.beginPath(); + } + while(!pathParser.isEnd()){ + switch(pathParser.next().type){ + case PathParser.MOVE_TO: + this.pathM(ctx, boundingBox); + break; + case PathParser.LINE_TO: + this.pathL(ctx, boundingBox); + break; + case PathParser.HORIZ_LINE_TO: + this.pathH(ctx, boundingBox); + break; + case PathParser.VERT_LINE_TO: + this.pathV(ctx, boundingBox); + break; + case PathParser.CURVE_TO: + this.pathC(ctx, boundingBox); + break; + case PathParser.SMOOTH_CURVE_TO: + this.pathS(ctx, boundingBox); + break; + case PathParser.QUAD_TO: + this.pathQ(ctx, boundingBox); + break; + case PathParser.SMOOTH_QUAD_TO: + this.pathT(ctx, boundingBox); + break; + case PathParser.ARC: + this.pathA(ctx, boundingBox); + break; + case PathParser.CLOSE_PATH: + this.pathZ(ctx, boundingBox); + break; + } + } + return boundingBox; + } + getBoundingBox(_ctx) { + return this.path(); + } + getMarkers() { + const { pathParser } = this; + const points = pathParser.getMarkerPoints(); + const angles = pathParser.getMarkerAngles(); + const markers = points.map((point, i)=>[ + point, + angles[i] + ]); + return markers; + } + renderChildren(ctx) { + this.path(ctx); + this.document.screen.mouse.checkPath(this, ctx); + const fillRuleStyleProp = this.getStyle('fill-rule'); + if (ctx.fillStyle !== '') { + if (fillRuleStyleProp.getString('inherit') !== 'inherit') { + ctx.fill(fillRuleStyleProp.getString()); + } else { + ctx.fill(); + } + } + if (ctx.strokeStyle !== '') { + if (this.getAttribute('vector-effect').getString() === 'non-scaling-stroke') { + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.stroke(); + ctx.restore(); + } else { + ctx.stroke(); + } + } + const markers = this.getMarkers(); + if (markers) { + const markersLastIndex = markers.length - 1; + const markerStartStyleProp = this.getStyle('marker-start'); + const markerMidStyleProp = this.getStyle('marker-mid'); + const markerEndStyleProp = this.getStyle('marker-end'); + if (markerStartStyleProp.isUrlDefinition()) { + const marker = markerStartStyleProp.getDefinition(); + const [point, angle] = markers[0]; + marker.render(ctx, point, angle); + } + if (markerMidStyleProp.isUrlDefinition()) { + const marker = markerMidStyleProp.getDefinition(); + for(let i = 1; i < markersLastIndex; i++){ + const [point, angle] = markers[i]; + marker.render(ctx, point, angle); + } + } + if (markerEndStyleProp.isUrlDefinition()) { + const marker = markerEndStyleProp.getDefinition(); + const [point, angle] = markers[markersLastIndex]; + marker.render(ctx, point, angle); + } + } + } + static pathM(pathParser) { + const point = pathParser.getAsCurrentPoint(); + pathParser.start = pathParser.current; + return { + point + }; + } + pathM(ctx, boundingBox) { + const { pathParser } = this; + const { point } = PathElement.pathM(pathParser); + const { x, y } = point; + pathParser.addMarker(point); + boundingBox.addPoint(x, y); + if (ctx) { + ctx.moveTo(x, y); + } + } + static pathL(pathParser) { + const { current } = pathParser; + const point = pathParser.getAsCurrentPoint(); + return { + current, + point + }; + } + pathL(ctx, boundingBox) { + const { pathParser } = this; + const { current, point } = PathElement.pathL(pathParser); + const { x, y } = point; + pathParser.addMarker(point, current); + boundingBox.addPoint(x, y); + if (ctx) { + ctx.lineTo(x, y); + } + } + static pathH(pathParser) { + const { current, command } = pathParser; + const point = new Point((command.relative ? current.x : 0) + command.x, current.y); + pathParser.current = point; + return { + current, + point + }; + } + pathH(ctx, boundingBox) { + const { pathParser } = this; + const { current, point } = PathElement.pathH(pathParser); + const { x, y } = point; + pathParser.addMarker(point, current); + boundingBox.addPoint(x, y); + if (ctx) { + ctx.lineTo(x, y); + } + } + static pathV(pathParser) { + const { current, command } = pathParser; + const point = new Point(current.x, (command.relative ? current.y : 0) + command.y); + pathParser.current = point; + return { + current, + point + }; + } + pathV(ctx, boundingBox) { + const { pathParser } = this; + const { current, point } = PathElement.pathV(pathParser); + const { x, y } = point; + pathParser.addMarker(point, current); + boundingBox.addPoint(x, y); + if (ctx) { + ctx.lineTo(x, y); + } + } + static pathC(pathParser) { + const { current } = pathParser; + const point = pathParser.getPoint('x1', 'y1'); + const controlPoint = pathParser.getAsControlPoint('x2', 'y2'); + const currentPoint = pathParser.getAsCurrentPoint(); + return { + current, + point, + controlPoint, + currentPoint + }; + } + pathC(ctx, boundingBox) { + const { pathParser } = this; + const { current, point, controlPoint, currentPoint } = PathElement.pathC(pathParser); + pathParser.addMarker(currentPoint, controlPoint, point); + boundingBox.addBezierCurve(current.x, current.y, point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + if (ctx) { + ctx.bezierCurveTo(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + } + } + static pathS(pathParser) { + const { current } = pathParser; + const point = pathParser.getReflectedControlPoint(); + const controlPoint = pathParser.getAsControlPoint('x2', 'y2'); + const currentPoint = pathParser.getAsCurrentPoint(); + return { + current, + point, + controlPoint, + currentPoint + }; + } + pathS(ctx, boundingBox) { + const { pathParser } = this; + const { current, point, controlPoint, currentPoint } = PathElement.pathS(pathParser); + pathParser.addMarker(currentPoint, controlPoint, point); + boundingBox.addBezierCurve(current.x, current.y, point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + if (ctx) { + ctx.bezierCurveTo(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + } + } + static pathQ(pathParser) { + const { current } = pathParser; + const controlPoint = pathParser.getAsControlPoint('x1', 'y1'); + const currentPoint = pathParser.getAsCurrentPoint(); + return { + current, + controlPoint, + currentPoint + }; + } + pathQ(ctx, boundingBox) { + const { pathParser } = this; + const { current, controlPoint, currentPoint } = PathElement.pathQ(pathParser); + pathParser.addMarker(currentPoint, controlPoint, controlPoint); + boundingBox.addQuadraticCurve(current.x, current.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + if (ctx) { + ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + } + } + static pathT(pathParser) { + const { current } = pathParser; + const controlPoint = pathParser.getReflectedControlPoint(); + pathParser.control = controlPoint; + const currentPoint = pathParser.getAsCurrentPoint(); + return { + current, + controlPoint, + currentPoint + }; + } + pathT(ctx, boundingBox) { + const { pathParser } = this; + const { current, controlPoint, currentPoint } = PathElement.pathT(pathParser); + pathParser.addMarker(currentPoint, controlPoint, controlPoint); + boundingBox.addQuadraticCurve(current.x, current.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + if (ctx) { + ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + } + } + static pathA(pathParser) { + const { current, command } = pathParser; + let { rX, rY, xRot, lArcFlag, sweepFlag } = command; + const xAxisRotation = xRot * (Math.PI / 180.0); + const currentPoint = pathParser.getAsCurrentPoint(); + // Conversion from endpoint to center parameterization + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + // x1', y1' + const currp = new Point(Math.cos(xAxisRotation) * (current.x - currentPoint.x) / 2.0 + Math.sin(xAxisRotation) * (current.y - currentPoint.y) / 2.0, -Math.sin(xAxisRotation) * (current.x - currentPoint.x) / 2.0 + Math.cos(xAxisRotation) * (current.y - currentPoint.y) / 2.0); + // adjust radii + const l = Math.pow(currp.x, 2) / Math.pow(rX, 2) + Math.pow(currp.y, 2) / Math.pow(rY, 2); + if (l > 1) { + rX *= Math.sqrt(l); + rY *= Math.sqrt(l); + } + // cx', cy' + let s = (lArcFlag === sweepFlag ? -1 : 1) * Math.sqrt((Math.pow(rX, 2) * Math.pow(rY, 2) - Math.pow(rX, 2) * Math.pow(currp.y, 2) - Math.pow(rY, 2) * Math.pow(currp.x, 2)) / (Math.pow(rX, 2) * Math.pow(currp.y, 2) + Math.pow(rY, 2) * Math.pow(currp.x, 2))); + if (isNaN(s)) { + s = 0; + } + const cpp = new Point(s * rX * currp.y / rY, s * -rY * currp.x / rX); + // cx, cy + const centp = new Point((current.x + currentPoint.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, (current.y + currentPoint.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y); + // initial angle + const a1 = vectorsAngle([ + 1, + 0 + ], [ + (currp.x - cpp.x) / rX, + (currp.y - cpp.y) / rY + ]) // θ1 + ; + // angle delta + const u = [ + (currp.x - cpp.x) / rX, + (currp.y - cpp.y) / rY + ]; + const v = [ + (-currp.x - cpp.x) / rX, + (-currp.y - cpp.y) / rY + ]; + let ad = vectorsAngle(u, v) // Δθ + ; + if (vectorsRatio(u, v) <= -1) { + ad = Math.PI; + } + if (vectorsRatio(u, v) >= 1) { + ad = 0; + } + return { + currentPoint, + rX, + rY, + sweepFlag, + xAxisRotation, + centp, + a1, + ad + }; + } + pathA(ctx, boundingBox) { + const { pathParser } = this; + const { currentPoint, rX, rY, sweepFlag, xAxisRotation, centp, a1, ad } = PathElement.pathA(pathParser); + // for markers + const dir = 1 - sweepFlag ? 1.0 : -1.0; + const ah = a1 + dir * (ad / 2.0); + const halfWay = new Point(centp.x + rX * Math.cos(ah), centp.y + rY * Math.sin(ah)); + pathParser.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); + pathParser.addMarkerAngle(currentPoint, ah - dir * Math.PI); + boundingBox.addPoint(currentPoint.x, currentPoint.y) // TODO: this is too naive, make it better + ; + if (ctx && !isNaN(a1) && !isNaN(ad)) { + const r = rX > rY ? rX : rY; + const sx = rX > rY ? 1 : rX / rY; + const sy = rX > rY ? rY / rX : 1; + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, r, a1, a1 + ad, Boolean(1 - sweepFlag)); + ctx.scale(1 / sx, 1 / sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + static pathZ(pathParser) { + pathParser.current = pathParser.start; + } + pathZ(ctx, boundingBox) { + PathElement.pathZ(this.pathParser); + if (ctx) { + // only close path if it is not a straight line + if (boundingBox.x1 !== boundingBox.x2 && boundingBox.y1 !== boundingBox.y2) { + ctx.closePath(); + } + } + } + } + + class SVGElement extends RenderedElement { + type = 'svg'; + root = false; + setContext(ctx) { + const { document } = this; + const { screen, window } = document; + const canvas = ctx.canvas; + screen.setDefaults(ctx); + if ('style' in canvas && typeof ctx.font !== 'undefined' && window && typeof window.getComputedStyle !== 'undefined') { + ctx.font = window.getComputedStyle(canvas).getPropertyValue('font'); + const fontSizeProp = new Property(document, 'fontSize', Font.parse(ctx.font).fontSize); + if (fontSizeProp.hasValue()) { + document.rootEmSize = fontSizeProp.getPixels('y'); + document.emSize = document.rootEmSize; + } + } + // create new view port + if (!this.getAttribute('x').hasValue()) { + this.getAttribute('x', true).setValue(0); + } + if (!this.getAttribute('y').hasValue()) { + this.getAttribute('y', true).setValue(0); + } + let { width, height } = screen.viewPort; + if (!this.getStyle('width').hasValue()) { + this.getStyle('width', true).setValue('100%'); + } + if (!this.getStyle('height').hasValue()) { + this.getStyle('height', true).setValue('100%'); + } + if (!this.getStyle('color').hasValue()) { + this.getStyle('color', true).setValue('black'); + } + const refXAttr = this.getAttribute('refX'); + const refYAttr = this.getAttribute('refY'); + const viewBoxAttr = this.getAttribute('viewBox'); + const viewBox = viewBoxAttr.hasValue() ? toNumbers(viewBoxAttr.getString()) : null; + const clip = !this.root && this.getStyle('overflow').getValue('hidden') !== 'visible'; + let minX = 0; + let minY = 0; + let clipX = 0; + let clipY = 0; + if (viewBox) { + minX = viewBox[0]; + minY = viewBox[1]; + } + if (!this.root) { + width = this.getStyle('width').getPixels('x'); + height = this.getStyle('height').getPixels('y'); + if (this.type === 'marker') { + clipX = minX; + clipY = minY; + minX = 0; + minY = 0; + } + } + screen.viewPort.setCurrent(width, height); + // Default value of transform-origin is center only for root SVG elements + // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform-origin + if (this.node // is not temporary SVGElement + && (!this.parent || this.node.parentNode?.nodeName === 'foreignObject') && this.getStyle('transform', false, true).hasValue() && !this.getStyle('transform-origin', false, true).hasValue()) { + this.getStyle('transform-origin', true, true).setValue('50% 50%'); + } + super.setContext(ctx); + ctx.translate(this.getAttribute('x').getPixels('x'), this.getAttribute('y').getPixels('y')); + if (viewBox) { + width = viewBox[2]; + height = viewBox[3]; + } + document.setViewBox({ + ctx, + aspectRatio: this.getAttribute('preserveAspectRatio').getString(), + width: screen.viewPort.width, + desiredWidth: width, + height: screen.viewPort.height, + desiredHeight: height, + minX, + minY, + refX: refXAttr.getValue(), + refY: refYAttr.getValue(), + clip, + clipX, + clipY + }); + if (viewBox) { + screen.viewPort.removeCurrent(); + screen.viewPort.setCurrent(width, height); + } + } + clearContext(ctx) { + super.clearContext(ctx); + this.document.screen.viewPort.removeCurrent(); + } + /** + * Resize SVG to fit in given size. + * @param width + * @param height + * @param preserveAspectRatio + */ resize(width) { + let height = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : width, preserveAspectRatio = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false; + const widthAttr = this.getAttribute('width', true); + const heightAttr = this.getAttribute('height', true); + const viewBoxAttr = this.getAttribute('viewBox'); + const styleAttr = this.getAttribute('style'); + const originWidth = widthAttr.getNumber(0); + const originHeight = heightAttr.getNumber(0); + if (preserveAspectRatio) { + if (typeof preserveAspectRatio === 'string') { + this.getAttribute('preserveAspectRatio', true).setValue(preserveAspectRatio); + } else { + const preserveAspectRatioAttr = this.getAttribute('preserveAspectRatio'); + if (preserveAspectRatioAttr.hasValue()) { + preserveAspectRatioAttr.setValue(preserveAspectRatioAttr.getString().replace(/^\s*(\S.*\S)\s*$/, '$1')); + } + } + } + widthAttr.setValue(width); + heightAttr.setValue(height); + if (!viewBoxAttr.hasValue()) { + viewBoxAttr.setValue(`0 0 ${originWidth || width} ${originHeight || height}`); + } + if (styleAttr.hasValue()) { + const widthStyle = this.getStyle('width'); + const heightStyle = this.getStyle('height'); + if (widthStyle.hasValue()) { + widthStyle.setValue(`${width}px`); + } + if (heightStyle.hasValue()) { + heightStyle.setValue(`${height}px`); + } + } + } + } + + class RectElement extends PathElement { + type = 'rect'; + path(ctx) { + const x = this.getAttribute('x').getPixels('x'); + const y = this.getAttribute('y').getPixels('y'); + const width = this.getStyle('width', false, true).getPixels('x'); + const height = this.getStyle('height', false, true).getPixels('y'); + const rxAttr = this.getAttribute('rx'); + const ryAttr = this.getAttribute('ry'); + let rx = rxAttr.getPixels('x'); + let ry = ryAttr.getPixels('y'); + if (rxAttr.hasValue() && !ryAttr.hasValue()) { + ry = rx; + } + if (ryAttr.hasValue() && !rxAttr.hasValue()) { + rx = ry; + } + rx = Math.min(rx, width / 2.0); + ry = Math.min(ry, height / 2.0); + if (ctx) { + const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + ctx.beginPath() // always start the path so we don't fill prior paths + ; + if (height > 0 && width > 0) { + ctx.moveTo(x + rx, y); + ctx.lineTo(x + width - rx, y); + ctx.bezierCurveTo(x + width - rx + KAPPA * rx, y, x + width, y + ry - KAPPA * ry, x + width, y + ry); + ctx.lineTo(x + width, y + height - ry); + ctx.bezierCurveTo(x + width, y + height - ry + KAPPA * ry, x + width - rx + KAPPA * rx, y + height, x + width - rx, y + height); + ctx.lineTo(x + rx, y + height); + ctx.bezierCurveTo(x + rx - KAPPA * rx, y + height, x, y + height - ry + KAPPA * ry, x, y + height - ry); + ctx.lineTo(x, y + ry); + ctx.bezierCurveTo(x, y + ry - KAPPA * ry, x + rx - KAPPA * rx, y, x + rx, y); + ctx.closePath(); + } + } + return new BoundingBox(x, y, x + width, y + height); + } + getMarkers() { + return null; + } + } + + class CircleElement extends PathElement { + type = 'circle'; + path(ctx) { + const cx = this.getAttribute('cx').getPixels('x'); + const cy = this.getAttribute('cy').getPixels('y'); + const r = this.getAttribute('r').getPixels(); + if (ctx && r > 0) { + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2, false); + ctx.closePath(); + } + return new BoundingBox(cx - r, cy - r, cx + r, cy + r); + } + getMarkers() { + return null; + } + } + + class EllipseElement extends PathElement { + type = 'ellipse'; + path(ctx) { + const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + const rx = this.getAttribute('rx').getPixels('x'); + const ry = this.getAttribute('ry').getPixels('y'); + const cx = this.getAttribute('cx').getPixels('x'); + const cy = this.getAttribute('cy').getPixels('y'); + if (ctx && rx > 0 && ry > 0) { + ctx.beginPath(); + ctx.moveTo(cx + rx, cy); + ctx.bezierCurveTo(cx + rx, cy + KAPPA * ry, cx + KAPPA * rx, cy + ry, cx, cy + ry); + ctx.bezierCurveTo(cx - KAPPA * rx, cy + ry, cx - rx, cy + KAPPA * ry, cx - rx, cy); + ctx.bezierCurveTo(cx - rx, cy - KAPPA * ry, cx - KAPPA * rx, cy - ry, cx, cy - ry); + ctx.bezierCurveTo(cx + KAPPA * rx, cy - ry, cx + rx, cy - KAPPA * ry, cx + rx, cy); + ctx.closePath(); + } + return new BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); + } + getMarkers() { + return null; + } + } + + class LineElement extends PathElement { + type = 'line'; + getPoints() { + return [ + new Point(this.getAttribute('x1').getPixels('x'), this.getAttribute('y1').getPixels('y')), + new Point(this.getAttribute('x2').getPixels('x'), this.getAttribute('y2').getPixels('y')) + ]; + } + path(ctx) { + const [{ x: x0, y: y0 }, { x: x1, y: y1 }] = this.getPoints(); + if (ctx) { + ctx.beginPath(); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y1); + } + return new BoundingBox(x0, y0, x1, y1); + } + getMarkers() { + const [p0, p1] = this.getPoints(); + const a = p0.angleTo(p1); + return [ + [ + p0, + a + ], + [ + p1, + a + ] + ]; + } + } + + class PolylineElement extends PathElement { + type = 'polyline'; + points = []; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + this.points = Point.parsePath(this.getAttribute('points').getString()); + } + path(ctx) { + const { points } = this; + const [{ x: x0, y: y0 }] = points; + const boundingBox = new BoundingBox(x0, y0); + if (ctx) { + ctx.beginPath(); + ctx.moveTo(x0, y0); + } + points.forEach((param)=>{ + let { x, y } = param; + boundingBox.addPoint(x, y); + if (ctx) { + ctx.lineTo(x, y); + } + }); + return boundingBox; + } + getMarkers() { + const { points } = this; + const lastIndex = points.length - 1; + const markers = []; + points.forEach((point, i)=>{ + if (i === lastIndex) { + return; + } + markers.push([ + point, + point.angleTo(points[i + 1]) + ]); + }); + if (markers.length > 0) { + markers.push([ + points[points.length - 1], + markers[markers.length - 1][1] + ]); + } + return markers; + } + } + + class PolygonElement extends PolylineElement { + type = 'polygon'; + path(ctx) { + const boundingBox = super.path(ctx); + const [{ x, y }] = this.points; + if (ctx) { + ctx.lineTo(x, y); + ctx.closePath(); + } + return boundingBox; + } + } + + class PatternElement extends Element { + type = 'pattern'; + createPattern(ctx, _, parentOpacityProp) { + const width = this.getStyle('width').getPixels('x', true); + const height = this.getStyle('height').getPixels('y', true); + // render me using a temporary svg element + const patternSvg = new SVGElement(this.document, null); + patternSvg.attributes.viewBox = new Property(this.document, 'viewBox', this.getAttribute('viewBox').getValue()); + patternSvg.attributes.width = new Property(this.document, 'width', `${width}px`); + patternSvg.attributes.height = new Property(this.document, 'height', `${height}px`); + patternSvg.attributes.transform = new Property(this.document, 'transform', this.getAttribute('patternTransform').getValue()); + patternSvg.children = this.children; + const patternCanvas = this.document.createCanvas(width, height); + const patternCtx = patternCanvas.getContext('2d'); + const xAttr = this.getAttribute('x'); + const yAttr = this.getAttribute('y'); + if (xAttr.hasValue() && yAttr.hasValue()) { + patternCtx.translate(xAttr.getPixels('x', true), yAttr.getPixels('y', true)); + } + if (parentOpacityProp.hasValue()) { + this.styles['fill-opacity'] = parentOpacityProp; + } else { + Reflect.deleteProperty(this.styles, 'fill-opacity'); + } + // render 3x3 grid so when we transform there's no white space on edges + for(let x = -1; x <= 1; x++){ + for(let y = -1; y <= 1; y++){ + patternCtx.save(); + patternSvg.attributes.x = new Property(this.document, 'x', x * patternCanvas.width); + patternSvg.attributes.y = new Property(this.document, 'y', y * patternCanvas.height); + patternSvg.render(patternCtx); + patternCtx.restore(); + } + } + const pattern = ctx.createPattern(patternCanvas, 'repeat'); + return pattern; + } + } + + class MarkerElement extends Element { + type = 'marker'; + render(ctx, point, angle) { + if (!point) { + return; + } + const { x, y } = point; + const orient = this.getAttribute('orient').getString('auto'); + const markerUnits = this.getAttribute('markerUnits').getString('strokeWidth'); + ctx.translate(x, y); + if (orient === 'auto') { + ctx.rotate(angle); + } + if (markerUnits === 'strokeWidth') { + ctx.scale(ctx.lineWidth, ctx.lineWidth); + } + ctx.save(); + // render me using a temporary svg element + const markerSvg = new SVGElement(this.document); + markerSvg.type = this.type; + markerSvg.attributes.viewBox = new Property(this.document, 'viewBox', this.getAttribute('viewBox').getValue()); + markerSvg.attributes.refX = new Property(this.document, 'refX', this.getAttribute('refX').getValue()); + markerSvg.attributes.refY = new Property(this.document, 'refY', this.getAttribute('refY').getValue()); + markerSvg.attributes.width = new Property(this.document, 'width', this.getAttribute('markerWidth').getValue()); + markerSvg.attributes.height = new Property(this.document, 'height', this.getAttribute('markerHeight').getValue()); + markerSvg.attributes.overflow = new Property(this.document, 'overflow', this.getAttribute('overflow').getValue()); + markerSvg.attributes.fill = new Property(this.document, 'fill', this.getAttribute('fill').getColor('black')); + markerSvg.attributes.stroke = new Property(this.document, 'stroke', this.getAttribute('stroke').getValue('none')); + markerSvg.children = this.children; + markerSvg.render(ctx); + ctx.restore(); + if (markerUnits === 'strokeWidth') { + ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth); + } + if (orient === 'auto') { + ctx.rotate(-angle); + } + ctx.translate(-x, -y); + } + } + + class DefsElement extends Element { + type = 'defs'; + render() { + // NOOP + } + } + + class GElement extends RenderedElement { + type = 'g'; + getBoundingBox(ctx) { + const boundingBox = new BoundingBox(); + this.children.forEach((child)=>{ + boundingBox.addBoundingBox(child.getBoundingBox(ctx)); + }); + return boundingBox; + } + } + + class GradientElement extends Element { + attributesToInherit = [ + 'gradientUnits' + ]; + stops = []; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + const { stops, children } = this; + children.forEach((child)=>{ + if (child.type === 'stop') { + stops.push(child); + } + }); + } + getGradientUnits() { + return this.getAttribute('gradientUnits').getString('objectBoundingBox'); + } + createGradient(ctx, element, parentOpacityProp) { + // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this + let stopsContainer = this; + if (this.getHrefAttribute().hasValue()) { + stopsContainer = this.getHrefAttribute().getDefinition(); + this.inheritStopContainer(stopsContainer); + } + const { stops } = stopsContainer; + const gradient = this.getGradient(ctx, element); + if (!gradient) { + return this.addParentOpacity(parentOpacityProp, stops[stops.length - 1].color); + } + stops.forEach((stop)=>{ + gradient.addColorStop(stop.offset, this.addParentOpacity(parentOpacityProp, stop.color)); + }); + if (this.getAttribute('gradientTransform').hasValue()) { + // render as transformed pattern on temporary canvas + const { document } = this; + const { MAX_VIRTUAL_PIXELS } = Screen; + const { viewPort } = document.screen; + const rootView = viewPort.getRoot(); + const rect = new RectElement(document); + rect.attributes.x = new Property(document, 'x', -MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes.y = new Property(document, 'y', -MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes.width = new Property(document, 'width', MAX_VIRTUAL_PIXELS); + rect.attributes.height = new Property(document, 'height', MAX_VIRTUAL_PIXELS); + const group = new GElement(document); + group.attributes.transform = new Property(document, 'transform', this.getAttribute('gradientTransform').getValue()); + group.children = [ + rect + ]; + const patternSvg = new SVGElement(document); + patternSvg.attributes.x = new Property(document, 'x', 0); + patternSvg.attributes.y = new Property(document, 'y', 0); + patternSvg.attributes.width = new Property(document, 'width', rootView.width); + patternSvg.attributes.height = new Property(document, 'height', rootView.height); + patternSvg.children = [ + group + ]; + const patternCanvas = document.createCanvas(rootView.width, rootView.height); + const patternCtx = patternCanvas.getContext('2d'); + patternCtx.fillStyle = gradient; + patternSvg.render(patternCtx); + return patternCtx.createPattern(patternCanvas, 'no-repeat'); + } + return gradient; + } + inheritStopContainer(stopsContainer) { + this.attributesToInherit.forEach((attributeToInherit)=>{ + if (!this.getAttribute(attributeToInherit).hasValue() && stopsContainer.getAttribute(attributeToInherit).hasValue()) { + this.getAttribute(attributeToInherit, true).setValue(stopsContainer.getAttribute(attributeToInherit).getValue()); + } + }); + } + addParentOpacity(parentOpacityProp, color) { + if (parentOpacityProp.hasValue()) { + const colorProp = new Property(this.document, 'color', color); + return colorProp.addOpacity(parentOpacityProp).getColor(); + } + return color; + } + } + + class LinearGradientElement extends GradientElement { + type = 'linearGradient'; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + this.attributesToInherit.push('x1', 'y1', 'x2', 'y2'); + } + getGradient(ctx, element) { + const isBoundingBoxUnits = this.getGradientUnits() === 'objectBoundingBox'; + const boundingBox = isBoundingBoxUnits ? element.getBoundingBox(ctx) : null; + if (isBoundingBoxUnits && !boundingBox) { + return null; + } + if (!this.getAttribute('x1').hasValue() && !this.getAttribute('y1').hasValue() && !this.getAttribute('x2').hasValue() && !this.getAttribute('y2').hasValue()) { + this.getAttribute('x1', true).setValue(0); + this.getAttribute('y1', true).setValue(0); + this.getAttribute('x2', true).setValue(1); + this.getAttribute('y2', true).setValue(0); + } + const x1 = isBoundingBoxUnits ? boundingBox.x + boundingBox.width * this.getAttribute('x1').getNumber() : this.getAttribute('x1').getPixels('x'); + const y1 = isBoundingBoxUnits ? boundingBox.y + boundingBox.height * this.getAttribute('y1').getNumber() : this.getAttribute('y1').getPixels('y'); + const x2 = isBoundingBoxUnits ? boundingBox.x + boundingBox.width * this.getAttribute('x2').getNumber() : this.getAttribute('x2').getPixels('x'); + const y2 = isBoundingBoxUnits ? boundingBox.y + boundingBox.height * this.getAttribute('y2').getNumber() : this.getAttribute('y2').getPixels('y'); + if (x1 === x2 && y1 === y2) { + return null; + } + return ctx.createLinearGradient(x1, y1, x2, y2); + } + } + + class RadialGradientElement extends GradientElement { + type = 'radialGradient'; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + this.attributesToInherit.push('cx', 'cy', 'r', 'fx', 'fy', 'fr'); + } + getGradient(ctx, element) { + const isBoundingBoxUnits = this.getGradientUnits() === 'objectBoundingBox'; + const boundingBox = element.getBoundingBox(ctx); + if (isBoundingBoxUnits && !boundingBox) { + return null; + } + if (!this.getAttribute('cx').hasValue()) { + this.getAttribute('cx', true).setValue('50%'); + } + if (!this.getAttribute('cy').hasValue()) { + this.getAttribute('cy', true).setValue('50%'); + } + if (!this.getAttribute('r').hasValue()) { + this.getAttribute('r', true).setValue('50%'); + } + const cx = isBoundingBoxUnits ? boundingBox.x + boundingBox.width * this.getAttribute('cx').getNumber() : this.getAttribute('cx').getPixels('x'); + const cy = isBoundingBoxUnits ? boundingBox.y + boundingBox.height * this.getAttribute('cy').getNumber() : this.getAttribute('cy').getPixels('y'); + let fx = cx; + let fy = cy; + if (this.getAttribute('fx').hasValue()) { + fx = isBoundingBoxUnits ? boundingBox.x + boundingBox.width * this.getAttribute('fx').getNumber() : this.getAttribute('fx').getPixels('x'); + } + if (this.getAttribute('fy').hasValue()) { + fy = isBoundingBoxUnits ? boundingBox.y + boundingBox.height * this.getAttribute('fy').getNumber() : this.getAttribute('fy').getPixels('y'); + } + const r = isBoundingBoxUnits ? (boundingBox.width + boundingBox.height) / 2.0 * this.getAttribute('r').getNumber() : this.getAttribute('r').getPixels(); + const fr = this.getAttribute('fr').getPixels(); + return ctx.createRadialGradient(fx, fy, fr, cx, cy, r); + } + } + + class StopElement extends Element { + type = 'stop'; + offset; + color; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + const offset = Math.max(0, Math.min(1, this.getAttribute('offset').getNumber())); + const stopOpacity = this.getStyle('stop-opacity'); + let stopColor = this.getStyle('stop-color', true); + if (stopColor.getString() === '') { + stopColor.setValue('#000'); + } + if (stopOpacity.hasValue()) { + stopColor = stopColor.addOpacity(stopOpacity); + } + this.offset = offset; + this.color = stopColor.getColor(); + } + } + + class AnimateElement extends Element { + type = 'animate'; + begin; + maxDuration; + from; + to; + values; + duration = 0; + initialValue; + initialUnits = ''; + removed = false; + frozen = false; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + document.screen.animations.push(this); + this.begin = this.getAttribute('begin').getMilliseconds(); + this.maxDuration = this.begin + this.getAttribute('dur').getMilliseconds(); + this.from = this.getAttribute('from'); + this.to = this.getAttribute('to'); + this.values = new Property(document, 'values', null); + const valuesAttr = this.getAttribute('values'); + if (valuesAttr.hasValue()) { + this.values.setValue(valuesAttr.getString().split(';')); + } + } + getProperty() { + const attributeType = this.getAttribute('attributeType').getString(); + const attributeName = this.getAttribute('attributeName').getString(); + if (attributeType === 'CSS') { + return this.parent.getStyle(attributeName, true); + } + return this.parent.getAttribute(attributeName, true); + } + calcValue() { + const { initialUnits } = this; + const { progress, from, to } = this.getProgress(); + // tween value linearly + let newValue = from.getNumber() + (to.getNumber() - from.getNumber()) * progress; + if (initialUnits === '%') { + newValue *= 100.0 // numValue() returns 0-1 whereas properties are 0-100 + ; + } + return `${newValue}${initialUnits}`; + } + update(delta) { + const { parent } = this; + const prop = this.getProperty(); + // set initial value + if (!this.initialValue) { + this.initialValue = prop.getString(); + this.initialUnits = prop.getUnits(); + } + // if we're past the end time + if (this.duration > this.maxDuration) { + const fill = this.getAttribute('fill').getString('remove'); + // loop for indefinitely repeating animations + if (this.getAttribute('repeatCount').getString() === 'indefinite' || this.getAttribute('repeatDur').getString() === 'indefinite') { + this.duration = 0; + } else if (fill === 'freeze' && !this.frozen) { + this.frozen = true; + if (parent && prop) { + parent.animationFrozen = true; + parent.animationFrozenValue = prop.getString(); + } + } else if (fill === 'remove' && !this.removed) { + this.removed = true; + if (parent && prop) { + prop.setValue(parent.animationFrozen ? parent.animationFrozenValue : this.initialValue); + } + return true; + } + return false; + } + this.duration += delta; + // if we're past the begin time + let updated = false; + if (this.begin < this.duration) { + let newValue = this.calcValue() // tween + ; + const typeAttr = this.getAttribute('type'); + if (typeAttr.hasValue()) { + // for transform, etc. + const type = typeAttr.getString(); + newValue = `${type}(${newValue})`; + } + prop.setValue(newValue); + updated = true; + } + return updated; + } + getProgress() { + const { document, values } = this; + let progress = (this.duration - this.begin) / (this.maxDuration - this.begin); + let from; + let to; + if (values.hasValue()) { + const p = progress * (values.getValue().length - 1); + const lb = Math.floor(p); + const ub = Math.ceil(p); + let value; + value = values.getValue()[lb]; + from = new Property(document, 'from', value ? parseFloat(value) : 0); + value = values.getValue()[ub]; + to = new Property(document, 'to', value ? parseFloat(value) : 0); + progress = (p - lb) / (ub - lb); + } else { + from = this.from; + to = this.to; + } + return { + progress, + from, + to + }; + } + } + + class AnimateColorElement extends AnimateElement { + type = 'animateColor'; + calcValue() { + const { progress, from, to } = this.getProgress(); + const colorFrom = new rgbcolor(from.getColor()); + const colorTo = new rgbcolor(to.getColor()); + if (colorFrom.ok && colorTo.ok) { + // tween color linearly + const r = colorFrom.r + (colorTo.r - colorFrom.r) * progress; + const g = colorFrom.g + (colorTo.g - colorFrom.g) * progress; + const b = colorFrom.b + (colorTo.b - colorFrom.b) * progress; + // ? alpha + return `rgb(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(b)})`; + } + return this.getAttribute('from').getColor(); + } + } + + class AnimateTransformElement extends AnimateElement { + type = 'animateTransform'; + calcValue() { + const { progress, from, to } = this.getProgress(); + // tween value linearly + const transformFrom = toNumbers(from.getString()); + const transformTo = toNumbers(to.getString()); + const newValue = transformFrom.map((from, i)=>{ + const to = transformTo[i]; + return from + (to - from) * progress; + }).join(' '); + return newValue; + } + } + + class FontFaceElement extends Element { + type = 'font-face'; + ascent; + descent; + unitsPerEm; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + this.ascent = this.getAttribute('ascent').getNumber(); + this.descent = this.getAttribute('descent').getNumber(); + this.unitsPerEm = this.getAttribute('units-per-em').getNumber(); + } + } + + class GlyphElement extends PathElement { + type = 'glyph'; + horizAdvX; + unicode; + arabicForm; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + this.horizAdvX = this.getAttribute('horiz-adv-x').getNumber(); + this.unicode = this.getAttribute('unicode').getString(); + this.arabicForm = this.getAttribute('arabic-form').getString(); + } + } + + class MissingGlyphElement extends GlyphElement { + type = 'missing-glyph'; + horizAdvX = 0; + } + + class FontElement extends Element { + type = 'font'; + isArabic = false; + missingGlyph; + glyphs = {}; + arabicGlyphs = {}; + horizAdvX; + isRTL = false; + fontFace; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + this.horizAdvX = this.getAttribute('horiz-adv-x').getNumber(); + const { definitions } = document; + const { children } = this; + for (const child of children){ + if (child instanceof FontFaceElement) { + this.fontFace = child; + const fontFamilyStyle = child.getStyle('font-family'); + if (fontFamilyStyle.hasValue()) { + definitions[fontFamilyStyle.getString()] = this; + } + } else if (child instanceof MissingGlyphElement) { + this.missingGlyph = child; + } else if (child instanceof GlyphElement) { + if (child.arabicForm) { + this.isRTL = true; + this.isArabic = true; + const arabicGlyph = this.arabicGlyphs[child.unicode]; + if (typeof arabicGlyph === 'undefined') { + this.arabicGlyphs[child.unicode] = { + [child.arabicForm]: child + }; + } else { + arabicGlyph[child.arabicForm] = child; + } + } else { + this.glyphs[child.unicode] = child; + } + } + } + } + render() { + // NO RENDER + } + } + + class TRefElement extends TextElement { + type = 'tref'; + getText() { + const element = this.getHrefAttribute().getDefinition(); + if (element) { + const firstChild = element.children[0]; + if (firstChild) { + return firstChild.getText(); + } + } + return ''; + } + } + + class AElement extends TextElement { + type = 'a'; + hasText; + text; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + const { childNodes } = node; + const firstChild = childNodes[0]; + const hasText = childNodes.length > 0 && Array.from(childNodes).every((node)=>node.nodeType === 3); + this.hasText = hasText; + this.text = hasText ? this.getTextFromNode(firstChild) : ''; + } + getText() { + return this.text; + } + renderChildren(ctx) { + if (this.hasText) { + // render as text element + super.renderChildren(ctx); + const { document, x, y } = this; + const { mouse } = document.screen; + const fontSize = new Property(document, 'fontSize', Font.parse(document.ctx.font).fontSize); + // Do not calc bounding box if mouse is not working. + if (mouse.isWorking()) { + mouse.checkBoundingBox(this, new BoundingBox(x, y - fontSize.getPixels('y'), x + this.measureText(ctx), y)); + } + } else if (this.children.length > 0) { + // render as temporary group + const g = new GElement(this.document); + g.children = this.children; + g.parent = this; + g.render(ctx); + } + } + onClick() { + const { window } = this.document; + if (window) { + window.open(this.getHrefAttribute().getString()); + } + } + onMouseMove() { + const ctx = this.document.ctx; + ctx.canvas.style.cursor = 'pointer'; + } + } + + class TextPathElement extends TextElement { + type = 'textPath'; + textWidth = 0; + textHeight = 0; + pathLength = -1; + glyphInfo = null; + text; + dataArray; + letterSpacingCache = []; + equidistantCache; + measuresCache = new Map([ + [ + '', + 0 + ] + ]); + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + const pathElement = this.getHrefAttribute().getDefinition(); + this.text = this.getTextFromNode(); + this.dataArray = this.parsePathData(pathElement); + } + getText() { + return this.text; + } + path(ctx) { + const { dataArray } = this; + if (ctx) { + ctx.beginPath(); + } + dataArray.forEach((param)=>{ + let { type, points } = param; + switch(type){ + case PathParser.LINE_TO: + if (ctx) { + ctx.lineTo(points[0], points[1]); + } + break; + case PathParser.MOVE_TO: + if (ctx) { + ctx.moveTo(points[0], points[1]); + } + break; + case PathParser.CURVE_TO: + if (ctx) { + ctx.bezierCurveTo(points[0], points[1], points[2], points[3], points[4], points[5]); + } + break; + case PathParser.QUAD_TO: + if (ctx) { + ctx.quadraticCurveTo(points[0], points[1], points[2], points[3]); + } + break; + case PathParser.ARC: + { + const [cx, cy, rx, ry, theta, dTheta, psi, fs] = points; + const r = rx > ry ? rx : ry; + const scaleX = rx > ry ? 1 : rx / ry; + const scaleY = rx > ry ? ry / rx : 1; + if (ctx) { + ctx.translate(cx, cy); + ctx.rotate(psi); + ctx.scale(scaleX, scaleY); + ctx.arc(0, 0, r, theta, theta + dTheta, Boolean(1 - fs)); + ctx.scale(1 / scaleX, 1 / scaleY); + ctx.rotate(-psi); + ctx.translate(-cx, -cy); + } + break; + } + case PathParser.CLOSE_PATH: + if (ctx) { + ctx.closePath(); + } + break; + } + }); + } + renderChildren(ctx) { + this.setTextData(ctx); + ctx.save(); + const textDecoration = this.parent.getStyle('text-decoration').getString(); + const fontSize = this.getFontSize(); + const { glyphInfo } = this; + const fill = ctx.fillStyle; + if (textDecoration === 'underline') { + ctx.beginPath(); + } + glyphInfo.forEach((glyph, i)=>{ + const { p0, p1, rotation, text: partialText } = glyph; + ctx.save(); + ctx.translate(p0.x, p0.y); + ctx.rotate(rotation); + if (ctx.fillStyle) { + ctx.fillText(partialText, 0, 0); + } + if (ctx.strokeStyle) { + ctx.strokeText(partialText, 0, 0); + } + ctx.restore(); + if (textDecoration === 'underline') { + if (i === 0) { + ctx.moveTo(p0.x, p0.y + fontSize / 8); + } + ctx.lineTo(p1.x, p1.y + fontSize / 5); + } + // // To assist with debugging visually, uncomment following + // + // ctx.beginPath(); + // if (i % 2) + // ctx.strokeStyle = 'red'; + // else + // ctx.strokeStyle = 'green'; + // ctx.moveTo(p0.x, p0.y); + // ctx.lineTo(p1.x, p1.y); + // ctx.stroke(); + // ctx.closePath(); + }); + if (textDecoration === 'underline') { + ctx.lineWidth = fontSize / 20; + ctx.strokeStyle = fill; + ctx.stroke(); + ctx.closePath(); + } + ctx.restore(); + } + getLetterSpacingAt() { + let idx = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : 0; + return this.letterSpacingCache[idx] || 0; + } + findSegmentToFitChar(ctx, anchor, textFullWidth, fullPathWidth, spacesNumber, inputOffset, dy, c, charI) { + let offset = inputOffset; + let glyphWidth = this.measureText(ctx, c); + if (c === ' ' && anchor === 'justify' && textFullWidth < fullPathWidth) { + glyphWidth += (fullPathWidth - textFullWidth) / spacesNumber; + } + if (charI > -1) { + offset += this.getLetterSpacingAt(charI); + } + const splineStep = this.textHeight / 20; + const p0 = this.getEquidistantPointOnPath(offset, splineStep, 0); + const p1 = this.getEquidistantPointOnPath(offset + glyphWidth, splineStep, 0); + const segment = { + p0, + p1 + }; + const rotation = p0 && p1 ? Math.atan2(p1.y - p0.y, p1.x - p0.x) : 0; + if (dy) { + const dyX = Math.cos(Math.PI / 2 + rotation) * dy; + const dyY = Math.cos(-rotation) * dy; + segment.p0 = { + ...p0, + x: p0.x + dyX, + y: p0.y + dyY + }; + segment.p1 = { + ...p1, + x: p1.x + dyX, + y: p1.y + dyY + }; + } + offset += glyphWidth; + return { + offset, + segment, + rotation + }; + } + measureText(ctx, text) { + const { measuresCache } = this; + const targetText = text || this.getText(); + if (measuresCache.has(targetText)) { + return measuresCache.get(targetText); + } + const measure = this.measureTargetText(ctx, targetText); + measuresCache.set(targetText, measure); + return measure; + } + // This method supposes what all custom fonts already loaded. + // If some font will be loaded after this method call, will not be rendered correctly. + // You need to call this method manually to update glyphs cache. + setTextData(ctx) { + if (this.glyphInfo) { + return; + } + const renderText = this.getText(); + const chars = renderText.split(''); + const spacesNumber = renderText.split(' ').length - 1; + const dx = this.parent.getAttribute('dx').split().map((_)=>_.getPixels('x')); + const dy = this.parent.getAttribute('dy').getPixels('y'); + const anchor = this.parent.getStyle('text-anchor').getString('start'); + const thisSpacing = this.getStyle('letter-spacing'); + const parentSpacing = this.parent.getStyle('letter-spacing'); + let letterSpacing = 0; + if (!thisSpacing.hasValue() || thisSpacing.getValue() === 'inherit') { + letterSpacing = parentSpacing.getPixels(); + } else if (thisSpacing.hasValue()) { + if (thisSpacing.getValue() !== 'initial' && thisSpacing.getValue() !== 'unset') { + letterSpacing = thisSpacing.getPixels(); + } + } + // fill letter-spacing cache + const letterSpacingCache = []; + const textLen = renderText.length; + this.letterSpacingCache = letterSpacingCache; + for(let i = 0; i < textLen; i++){ + letterSpacingCache.push(typeof dx[i] !== 'undefined' ? dx[i] : letterSpacing); + } + const dxSum = letterSpacingCache.reduce((acc, cur, i)=>i === 0 ? 0 : acc + cur || 0, 0); + const textWidth = this.measureText(ctx); + const textFullWidth = Math.max(textWidth + dxSum, 0); + this.textWidth = textWidth; + this.textHeight = this.getFontSize(); + this.glyphInfo = []; + const fullPathWidth = this.getPathLength(); + const startOffset = this.getStyle('startOffset').getNumber(0) * fullPathWidth; + let offset = 0; + if (anchor === 'middle' || anchor === 'center') { + offset = -textFullWidth / 2; + } + if (anchor === 'end' || anchor === 'right') { + offset = -textFullWidth; + } + offset += startOffset; + chars.forEach((char, i)=>{ + // Find such segment what distance between p0 and p1 is approx. width of glyph + const { offset: nextOffset, segment, rotation } = this.findSegmentToFitChar(ctx, anchor, textFullWidth, fullPathWidth, spacesNumber, offset, dy, char, i); + offset = nextOffset; + if (!segment.p0 || !segment.p1) { + return; + } + // const width = this.getLineLength( + // segment.p0.x, + // segment.p0.y, + // segment.p1.x, + // segment.p1.y + // ); + // Note: Since glyphs are rendered one at a time, any kerning pair data built into the font will not be used. + // Can foresee having a rough pair table built in that the developer can override as needed. + // Or use "dx" attribute of the node as a naive replacement + // const kern = 0; + // placeholder for future implementation + // const midpoint = this.getPointOnLine( + // kern + width / 2.0, + // segment.p0.x, segment.p0.y, segment.p1.x, segment.p1.y + // ); + this.glyphInfo.push({ + // transposeX: midpoint.x, + // transposeY: midpoint.y, + text: chars[i], + p0: segment.p0, + p1: segment.p1, + rotation + }); + }); + } + parsePathData(path) { + this.pathLength = -1 // reset path length + ; + if (!path) { + return []; + } + const pathCommands = []; + const { pathParser } = path; + pathParser.reset(); + // convert l, H, h, V, and v to L + while(!pathParser.isEnd()){ + const { current } = pathParser; + const startX = current ? current.x : 0; + const startY = current ? current.y : 0; + const command = pathParser.next(); + let nextCommandType = command.type; + let points = []; + switch(command.type){ + case PathParser.MOVE_TO: + this.pathM(pathParser, points); + break; + case PathParser.LINE_TO: + nextCommandType = this.pathL(pathParser, points); + break; + case PathParser.HORIZ_LINE_TO: + nextCommandType = this.pathH(pathParser, points); + break; + case PathParser.VERT_LINE_TO: + nextCommandType = this.pathV(pathParser, points); + break; + case PathParser.CURVE_TO: + this.pathC(pathParser, points); + break; + case PathParser.SMOOTH_CURVE_TO: + nextCommandType = this.pathS(pathParser, points); + break; + case PathParser.QUAD_TO: + this.pathQ(pathParser, points); + break; + case PathParser.SMOOTH_QUAD_TO: + nextCommandType = this.pathT(pathParser, points); + break; + case PathParser.ARC: + points = this.pathA(pathParser); + break; + case PathParser.CLOSE_PATH: + PathElement.pathZ(pathParser); + break; + } + if (command.type !== PathParser.CLOSE_PATH) { + pathCommands.push({ + type: nextCommandType, + points, + start: { + x: startX, + y: startY + }, + pathLength: this.calcLength(startX, startY, nextCommandType, points) + }); + } else { + pathCommands.push({ + type: PathParser.CLOSE_PATH, + points: [], + pathLength: 0 + }); + } + } + return pathCommands; + } + pathM(pathParser, points) { + const { x, y } = PathElement.pathM(pathParser).point; + points.push(x, y); + } + pathL(pathParser, points) { + const { x, y } = PathElement.pathL(pathParser).point; + points.push(x, y); + return PathParser.LINE_TO; + } + pathH(pathParser, points) { + const { x, y } = PathElement.pathH(pathParser).point; + points.push(x, y); + return PathParser.LINE_TO; + } + pathV(pathParser, points) { + const { x, y } = PathElement.pathV(pathParser).point; + points.push(x, y); + return PathParser.LINE_TO; + } + pathC(pathParser, points) { + const { point, controlPoint, currentPoint } = PathElement.pathC(pathParser); + points.push(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + } + pathS(pathParser, points) { + const { point, controlPoint, currentPoint } = PathElement.pathS(pathParser); + points.push(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + return PathParser.CURVE_TO; + } + pathQ(pathParser, points) { + const { controlPoint, currentPoint } = PathElement.pathQ(pathParser); + points.push(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + } + pathT(pathParser, points) { + const { controlPoint, currentPoint } = PathElement.pathT(pathParser); + points.push(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y); + return PathParser.QUAD_TO; + } + pathA(pathParser) { + let { rX, rY, sweepFlag, xAxisRotation, centp, a1, ad } = PathElement.pathA(pathParser); + if (sweepFlag === 0 && ad > 0) { + ad -= 2 * Math.PI; + } + if (sweepFlag === 1 && ad < 0) { + ad += 2 * Math.PI; + } + return [ + centp.x, + centp.y, + rX, + rY, + a1, + ad, + xAxisRotation, + sweepFlag + ]; + } + calcLength(x, y, commandType, points) { + let len = 0; + let p1 = null; + let p2 = null; + let t = 0; + switch(commandType){ + case PathParser.LINE_TO: + return this.getLineLength(x, y, points[0], points[1]); + case PathParser.CURVE_TO: + // Approximates by breaking curve into 100 line segments + len = 0.0; + p1 = this.getPointOnCubicBezier(0, x, y, points[0], points[1], points[2], points[3], points[4], points[5]); + for(t = 0.01; t <= 1; t += 0.01){ + p2 = this.getPointOnCubicBezier(t, x, y, points[0], points[1], points[2], points[3], points[4], points[5]); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + return len; + case PathParser.QUAD_TO: + // Approximates by breaking curve into 100 line segments + len = 0.0; + p1 = this.getPointOnQuadraticBezier(0, x, y, points[0], points[1], points[2], points[3]); + for(t = 0.01; t <= 1; t += 0.01){ + p2 = this.getPointOnQuadraticBezier(t, x, y, points[0], points[1], points[2], points[3]); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + return len; + case PathParser.ARC: + { + // Approximates by breaking curve into line segments + len = 0.0; + const start = points[4]; + // 4 = theta + const dTheta = points[5]; + // 5 = dTheta + const end = points[4] + dTheta; + let inc = Math.PI / 180.0; + // 1 degree resolution + if (Math.abs(start - end) < inc) { + inc = Math.abs(start - end); + } + // Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi + p1 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0); + if (dTheta < 0) { + for(t = start - inc; t > end; t -= inc){ + p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } else { + for(t = start + inc; t < end; t += inc){ + p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0); + len += this.getLineLength(p1.x, p1.y, p2.x, p2.y); + return len; + } + } + return 0; + } + getPointOnLine(dist, p1x, p1y, p2x, p2y) { + let fromX = arguments.length > 5 && arguments[5] !== void 0 ? arguments[5] : p1x, fromY = arguments.length > 6 && arguments[6] !== void 0 ? arguments[6] : p1y; + const m = (p2y - p1y) / (p2x - p1x + PSEUDO_ZERO); + let run = Math.sqrt(dist * dist / (1 + m * m)); + if (p2x < p1x) { + run *= -1; + } + let rise = m * run; + let pt = null; + if (p2x === p1x) { + pt = { + x: fromX, + y: fromY + rise + }; + } else if ((fromY - p1y) / (fromX - p1x + PSEUDO_ZERO) === m) { + pt = { + x: fromX + run, + y: fromY + rise + }; + } else { + let ix = 0; + let iy = 0; + const len = this.getLineLength(p1x, p1y, p2x, p2y); + if (len < PSEUDO_ZERO) { + return null; + } + let u = (fromX - p1x) * (p2x - p1x) + (fromY - p1y) * (p2y - p1y); + u /= len * len; + ix = p1x + u * (p2x - p1x); + iy = p1y + u * (p2y - p1y); + const pRise = this.getLineLength(fromX, fromY, ix, iy); + const pRun = Math.sqrt(dist * dist - pRise * pRise); + run = Math.sqrt(pRun * pRun / (1 + m * m)); + if (p2x < p1x) { + run *= -1; + } + rise = m * run; + pt = { + x: ix + run, + y: iy + rise + }; + } + return pt; + } + getPointOnPath(distance) { + const fullLen = this.getPathLength(); + let cumulativePathLength = 0; + let p = null; + if (distance < -0.00005 || distance - 0.00005 > fullLen) { + return null; + } + const { dataArray } = this; + for (const command of dataArray){ + if (command && (command.pathLength < 0.00005 || cumulativePathLength + command.pathLength + 0.00005 < distance)) { + cumulativePathLength += command.pathLength; + continue; + } + const delta = distance - cumulativePathLength; + let currentT = 0; + switch(command.type){ + case PathParser.LINE_TO: + p = this.getPointOnLine(delta, command.start.x, command.start.y, command.points[0], command.points[1], command.start.x, command.start.y); + break; + case PathParser.ARC: + { + const start = command.points[4]; + // 4 = theta + const dTheta = command.points[5]; + // 5 = dTheta + const end = command.points[4] + dTheta; + currentT = start + delta / command.pathLength * dTheta; + if (dTheta < 0 && currentT < end || dTheta >= 0 && currentT > end) { + break; + } + p = this.getPointOnEllipticalArc(command.points[0], command.points[1], command.points[2], command.points[3], currentT, command.points[6]); + break; + } + case PathParser.CURVE_TO: + currentT = delta / command.pathLength; + if (currentT > 1) { + currentT = 1; + } + p = this.getPointOnCubicBezier(currentT, command.start.x, command.start.y, command.points[0], command.points[1], command.points[2], command.points[3], command.points[4], command.points[5]); + break; + case PathParser.QUAD_TO: + currentT = delta / command.pathLength; + if (currentT > 1) { + currentT = 1; + } + p = this.getPointOnQuadraticBezier(currentT, command.start.x, command.start.y, command.points[0], command.points[1], command.points[2], command.points[3]); + break; + } + if (p) { + return p; + } + break; + } + return null; + } + getLineLength(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + } + getPathLength() { + if (this.pathLength === -1) { + this.pathLength = this.dataArray.reduce((length, command)=>command.pathLength > 0 ? length + command.pathLength : length, 0); + } + return this.pathLength; + } + getPointOnCubicBezier(pct, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { + const x = p4x * CB1(pct) + p3x * CB2(pct) + p2x * CB3(pct) + p1x * CB4(pct); + const y = p4y * CB1(pct) + p3y * CB2(pct) + p2y * CB3(pct) + p1y * CB4(pct); + return { + x, + y + }; + } + getPointOnQuadraticBezier(pct, p1x, p1y, p2x, p2y, p3x, p3y) { + const x = p3x * QB1(pct) + p2x * QB2(pct) + p1x * QB3(pct); + const y = p3y * QB1(pct) + p2y * QB2(pct) + p1y * QB3(pct); + return { + x, + y + }; + } + getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi) { + const cosPsi = Math.cos(psi); + const sinPsi = Math.sin(psi); + const pt = { + x: rx * Math.cos(theta), + y: ry * Math.sin(theta) + }; + return { + x: cx + (pt.x * cosPsi - pt.y * sinPsi), + y: cy + (pt.x * sinPsi + pt.y * cosPsi) + }; + } + // TODO need some optimisations. possibly build cache only for curved segments? + buildEquidistantCache(inputStep, inputPrecision) { + const fullLen = this.getPathLength(); + const precision = inputPrecision || 0.25 // accuracy vs performance + ; + const step = inputStep || fullLen / 100; + if (!this.equidistantCache || this.equidistantCache.step !== step || this.equidistantCache.precision !== precision) { + // Prepare cache + this.equidistantCache = { + step, + precision, + points: [] + }; + // Calculate points + let s = 0; + for(let l = 0; l <= fullLen; l += precision){ + const p0 = this.getPointOnPath(l); + const p1 = this.getPointOnPath(l + precision); + if (!p0 || !p1) { + continue; + } + s += this.getLineLength(p0.x, p0.y, p1.x, p1.y); + if (s >= step) { + this.equidistantCache.points.push({ + x: p0.x, + y: p0.y, + distance: l + }); + s -= step; + } + } + } + } + getEquidistantPointOnPath(targetDistance, step, precision) { + this.buildEquidistantCache(step, precision); + if (targetDistance < 0 || targetDistance - this.getPathLength() > 0.00005) { + return null; + } + const idx = Math.round(targetDistance / this.getPathLength() * (this.equidistantCache.points.length - 1)); + return this.equidistantCache.points[idx] || null; + } + } + + // groups: 1: mime-type (+ charset), 2: mime-type (w/o charset), 3: charset, 4: base64?, 5: body + const dataUriRegex = /^\s*data:(([^/,;]+\/[^/,;]+)(?:;([^,;=]+=[^,;=]+))?)?(?:;(base64))?,(.*)$/i; + class ImageElement extends RenderedElement { + type = 'image'; + loaded = false; + image; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + const href = this.getHrefAttribute().getString(); + if (!href) { + return; + } + const isSvg = href.endsWith('.svg') || /^\s*data:image\/svg\+xml/i.test(href); + document.images.push(this); + if (!isSvg) { + void this.loadImage(href); + } else { + void this.loadSvg(href); + } + } + async loadImage(href) { + try { + const image = await this.document.createImage(href); + this.image = image; + } catch (err) { + console.error(`Error while loading image "${href}":`, err); + } + this.loaded = true; + } + async loadSvg(href) { + const match = dataUriRegex.exec(href); + if (match) { + const data = match[5]; + if (data) { + if (match[4] === 'base64') { + this.image = atob(data); + } else { + this.image = decodeURIComponent(data); + } + } + } else { + try { + const response = await this.document.fetch(href); + const svg = await response.text(); + this.image = svg; + } catch (err) { + console.error(`Error while loading image "${href}":`, err); + } + } + this.loaded = true; + } + renderChildren(ctx) { + const { document, image, loaded } = this; + const x = this.getAttribute('x').getPixels('x'); + const y = this.getAttribute('y').getPixels('y'); + const width = this.getStyle('width').getPixels('x'); + const height = this.getStyle('height').getPixels('y'); + if (!loaded || !image || !width || !height) { + return; + } + ctx.save(); + ctx.translate(x, y); + if (typeof image === 'string') { + const subDocument = document.canvg.forkString(ctx, image, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true, + ignoreClear: true, + offsetX: 0, + offsetY: 0, + scaleWidth: width, + scaleHeight: height + }); + const { documentElement } = subDocument.document; + if (documentElement) { + documentElement.parent = this; + } + void subDocument.render(); + } else { + document.setViewBox({ + ctx, + aspectRatio: this.getAttribute('preserveAspectRatio').getString(), + width, + desiredWidth: image.width, + height, + desiredHeight: image.height + }); + if (this.loaded) { + if (!('complete' in image) || image.complete) { + ctx.drawImage(image, 0, 0); + } + } + } + ctx.restore(); + } + getBoundingBox() { + const x = this.getAttribute('x').getPixels('x'); + const y = this.getAttribute('y').getPixels('y'); + const width = this.getStyle('width').getPixels('x'); + const height = this.getStyle('height').getPixels('y'); + return new BoundingBox(x, y, x + width, y + height); + } + } + + class SymbolElement extends RenderedElement { + type = 'symbol'; + render(_) { + // NO RENDER + } + } + + class SVGFontLoader { + document; + loaded; + constructor(document){ + this.document = document; + this.loaded = false; + document.fonts.push(this); + } + async load(fontFamily, url) { + try { + const { document } = this; + const svgDocument = await document.canvg.parser.load(url); + const fonts = svgDocument.getElementsByTagName('font'); + Array.from(fonts).forEach((fontNode)=>{ + const font = document.createElement(fontNode); + document.definitions[fontFamily] = font; + }); + } catch (err) { + console.error(`Error while loading font "${url}":`, err); + } + this.loaded = true; + } + } + + class StyleElement extends Element { + static parseExternalUrl = parseExternalUrl; + type = 'style'; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + const css = compressSpaces(Array.from(node.childNodes)// NEED TEST + .map((_)=>_.textContent).join('').replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, '') // remove comments + .replace(/@import.*;/g, '') // remove imports + ); + const cssDefs = css.split('}'); + cssDefs.forEach((_)=>{ + const def = _.trim(); + if (!def) { + return; + } + const cssParts = def.split('{'); + const cssClasses = cssParts[0].split(','); + const cssProps = cssParts[1].split(';'); + cssClasses.forEach((_)=>{ + const cssClass = _.trim(); + if (!cssClass) { + return; + } + const props = document.styles[cssClass] || {}; + cssProps.forEach((cssProp)=>{ + const prop = cssProp.indexOf(':'); + const name = cssProp.substr(0, prop).trim(); + const value = cssProp.substr(prop + 1, cssProp.length - prop).trim(); + if (name && value) { + props[name] = new Property(document, name, value); + } + }); + document.styles[cssClass] = props; + document.stylesSpecificity[cssClass] = getSelectorSpecificity(cssClass); + if (cssClass === '@font-face') { + const fontFamily = props['font-family'].getString().replace(/"|'/g, ''); + const srcs = props.src.getString().split(','); + srcs.forEach((src)=>{ + if (src.indexOf('format("svg")') > 0) { + const url = parseExternalUrl(src); + if (url) { + void new SVGFontLoader(document).load(fontFamily, url); + } + } + }); + } + }); + }); + } + } + + class UseElement extends RenderedElement { + type = 'use'; + cachedElement; + setContext(ctx) { + super.setContext(ctx); + const xAttr = this.getAttribute('x'); + const yAttr = this.getAttribute('y'); + if (xAttr.hasValue()) { + ctx.translate(xAttr.getPixels('x'), 0); + } + if (yAttr.hasValue()) { + ctx.translate(0, yAttr.getPixels('y')); + } + } + path(ctx) { + const { element } = this; + if (element) { + element.path(ctx); + } + } + renderChildren(ctx) { + const { document, element } = this; + if (element) { + let tempSvg = element; + if (element.type === 'symbol') { + // render me using a temporary svg element in symbol cases (http://www.w3.org/TR/SVG/struct.html#UseElement) + tempSvg = new SVGElement(document); + tempSvg.attributes.viewBox = new Property(document, 'viewBox', element.getAttribute('viewBox').getString()); + tempSvg.attributes.preserveAspectRatio = new Property(document, 'preserveAspectRatio', element.getAttribute('preserveAspectRatio').getString()); + tempSvg.attributes.overflow = new Property(document, 'overflow', element.getAttribute('overflow').getString()); + tempSvg.children = element.children; + // element is still the parent of the children + element.styles.opacity = new Property(document, 'opacity', this.calculateOpacity()); + } + if (tempSvg.type === 'svg') { + const widthStyle = this.getStyle('width', false, true); + const heightStyle = this.getStyle('height', false, true); + // if symbol or svg, inherit width/height from me + if (widthStyle.hasValue()) { + tempSvg.attributes.width = new Property(document, 'width', widthStyle.getString()); + } + if (heightStyle.hasValue()) { + tempSvg.attributes.height = new Property(document, 'height', heightStyle.getString()); + } + } + const oldParent = tempSvg.parent; + tempSvg.parent = this; + tempSvg.render(ctx); + tempSvg.parent = oldParent; + } + } + getBoundingBox(ctx) { + const { element } = this; + if (element) { + return element.getBoundingBox(ctx); + } + return null; + } + elementTransform() { + const { document, element } = this; + if (!element) { + return null; + } + return Transform.fromElement(document, element); + } + get element() { + if (!this.cachedElement) { + this.cachedElement = this.getHrefAttribute().getDefinition(); + } + return this.cachedElement; + } + } + + function imGet(img, x, y, width, _height, rgba) { + return img[y * width * 4 + x * 4 + rgba]; + } + function imSet(img, x, y, width, _height, rgba, val) { + img[y * width * 4 + x * 4 + rgba] = val; + } + function m(matrix, i, v) { + const mi = matrix[i]; + return mi * v; + } + function c(a, m1, m2, m3) { + return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; + } + class FeColorMatrixElement extends Element { + type = 'feColorMatrix'; + matrix; + includeOpacity; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + let matrix = toNumbers(this.getAttribute('values').getString()); + switch(this.getAttribute('type').getString('matrix')){ + case 'saturate': + { + const s = matrix[0]; + /* eslint-disable array-element-newline */ matrix = [ + 0.213 + 0.787 * s, + 0.715 - 0.715 * s, + 0.072 - 0.072 * s, + 0, + 0, + 0.213 - 0.213 * s, + 0.715 + 0.285 * s, + 0.072 - 0.072 * s, + 0, + 0, + 0.213 - 0.213 * s, + 0.715 - 0.715 * s, + 0.072 + 0.928 * s, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1 + ]; + break; + } + case 'hueRotate': + { + const a = matrix[0] * Math.PI / 180.0; + /* eslint-disable array-element-newline */ matrix = [ + c(a, 0.213, 0.787, -0.213), + c(a, 0.715, -0.715, -0.715), + c(a, 0.072, -0.072, 0.928), + 0, + 0, + c(a, 0.213, -0.213, 0.143), + c(a, 0.715, 0.285, 0.140), + c(a, 0.072, -0.072, -0.283), + 0, + 0, + c(a, 0.213, -0.213, -0.787), + c(a, 0.715, -0.715, 0.715), + c(a, 0.072, 0.928, 0.072), + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1 + ]; + break; + } + case 'luminanceToAlpha': + /* eslint-disable array-element-newline */ matrix = [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.2125, + 0.7154, + 0.0721, + 0, + 0, + 0, + 0, + 0, + 0, + 1 + ]; + break; + } + this.matrix = matrix; + this.includeOpacity = this.getAttribute('includeOpacity').hasValue(); + } + apply(ctx, _x, _y, width, height) { + // assuming x==0 && y==0 for now + const { includeOpacity, matrix } = this; + const srcData = ctx.getImageData(0, 0, width, height); + for(let y = 0; y < height; y++){ + for(let x = 0; x < width; x++){ + const r = imGet(srcData.data, x, y, width, height, 0); + const g = imGet(srcData.data, x, y, width, height, 1); + const b = imGet(srcData.data, x, y, width, height, 2); + const a = imGet(srcData.data, x, y, width, height, 3); + let nr = m(matrix, 0, r) + m(matrix, 1, g) + m(matrix, 2, b) + m(matrix, 3, a) + m(matrix, 4, 1); + let ng = m(matrix, 5, r) + m(matrix, 6, g) + m(matrix, 7, b) + m(matrix, 8, a) + m(matrix, 9, 1); + let nb = m(matrix, 10, r) + m(matrix, 11, g) + m(matrix, 12, b) + m(matrix, 13, a) + m(matrix, 14, 1); + let na = m(matrix, 15, r) + m(matrix, 16, g) + m(matrix, 17, b) + m(matrix, 18, a) + m(matrix, 19, 1); + if (includeOpacity) { + nr = 0; + ng = 0; + nb = 0; + na *= a / 255; + } + imSet(srcData.data, x, y, width, height, 0, nr); + imSet(srcData.data, x, y, width, height, 1, ng); + imSet(srcData.data, x, y, width, height, 2, nb); + imSet(srcData.data, x, y, width, height, 3, na); + } + } + ctx.clearRect(0, 0, width, height); + ctx.putImageData(srcData, 0, 0); + } + } + + class MaskElement extends Element { + static ignoreStyles = [ + 'mask', + 'transform', + 'clip-path' + ]; + type = 'mask'; + apply(ctx, element) { + const { document } = this; + // render as temp svg + let x = this.getAttribute('x').getPixels('x'); + let y = this.getAttribute('y').getPixels('y'); + let width = this.getStyle('width').getPixels('x'); + let height = this.getStyle('height').getPixels('y'); + if (!width && !height) { + const boundingBox = new BoundingBox(); + this.children.forEach((child)=>{ + boundingBox.addBoundingBox(child.getBoundingBox(ctx)); + }); + x = Math.floor(boundingBox.x1); + y = Math.floor(boundingBox.y1); + width = Math.floor(boundingBox.width); + height = Math.floor(boundingBox.height); + } + const ignoredStyles = this.removeStyles(element, MaskElement.ignoreStyles); + const maskCanvas = document.createCanvas(x + width, y + height); + const maskCtx = maskCanvas.getContext('2d'); + document.screen.setDefaults(maskCtx); + this.renderChildren(maskCtx); + // convert mask to alpha with a fake node + // TODO: refactor out apply from feColorMatrix + new FeColorMatrixElement(document, { + nodeType: 1, + childNodes: [], + attributes: [ + { + nodeName: 'type', + value: 'luminanceToAlpha' + }, + { + nodeName: 'includeOpacity', + value: 'true' + } + ] + }).apply(maskCtx, 0, 0, x + width, y + height); + const tmpCanvas = document.createCanvas(x + width, y + height); + const tmpCtx = tmpCanvas.getContext('2d'); + document.screen.setDefaults(tmpCtx); + element.render(tmpCtx); + tmpCtx.globalCompositeOperation = 'destination-in'; + tmpCtx.fillStyle = maskCtx.createPattern(maskCanvas, 'no-repeat'); + tmpCtx.fillRect(0, 0, x + width, y + height); + ctx.fillStyle = tmpCtx.createPattern(tmpCanvas, 'no-repeat'); + ctx.fillRect(0, 0, x + width, y + height); + // reassign mask + this.restoreStyles(element, ignoredStyles); + } + render(_) { + // NO RENDER + } + } + + const noop = ()=>{ + // NOOP + }; + class ClipPathElement extends Element { + type = 'clipPath'; + apply(ctx) { + const { document } = this; + const contextProto = Reflect.getPrototypeOf(ctx); + const { beginPath, closePath } = ctx; + if (contextProto) { + contextProto.beginPath = noop; + contextProto.closePath = noop; + } + Reflect.apply(beginPath, ctx, []); + this.children.forEach((child)=>{ + if (!('path' in child)) { + return; + } + let transform = 'elementTransform' in child ? child.elementTransform() : null // handle + ; + if (!transform) { + transform = Transform.fromElement(document, child); + } + if (transform) { + transform.apply(ctx); + } + child.path(ctx); + if (contextProto) { + contextProto.closePath = closePath; + } + if (transform) { + transform.unapply(ctx); + } + }); + Reflect.apply(closePath, ctx, []); + ctx.clip(); + if (contextProto) { + contextProto.beginPath = beginPath; + contextProto.closePath = closePath; + } + } + render(_) { + // NO RENDER + } + } + + class FilterElement extends Element { + static ignoreStyles = [ + 'filter', + 'transform', + 'clip-path' + ]; + type = 'filter'; + apply(ctx, element) { + // render as temp svg + const { document, children } = this; + const boundingBox = 'getBoundingBox' in element ? element.getBoundingBox(ctx) : null; + if (!boundingBox) { + return; + } + let px = 0; + let py = 0; + children.forEach((child)=>{ + const efd = child.extraFilterDistance || 0; + px = Math.max(px, efd); + py = Math.max(py, efd); + }); + const width = Math.floor(boundingBox.width); + const height = Math.floor(boundingBox.height); + const tmpCanvasWidth = width + 2 * px; + const tmpCanvasHeight = height + 2 * py; + if (tmpCanvasWidth < 1 || tmpCanvasHeight < 1) { + return; + } + const x = Math.floor(boundingBox.x); + const y = Math.floor(boundingBox.y); + const ignoredStyles = this.removeStyles(element, FilterElement.ignoreStyles); + const tmpCanvas = document.createCanvas(tmpCanvasWidth, tmpCanvasHeight); + const tmpCtx = tmpCanvas.getContext('2d'); + document.screen.setDefaults(tmpCtx); + tmpCtx.translate(-x + px, -y + py); + element.render(tmpCtx); + // apply filters + children.forEach((child)=>{ + if (typeof child.apply === 'function') { + child.apply(tmpCtx, 0, 0, tmpCanvasWidth, tmpCanvasHeight); + } + }); + // render on me + ctx.drawImage(tmpCanvas, 0, 0, tmpCanvasWidth, tmpCanvasHeight, x - px, y - py, tmpCanvasWidth, tmpCanvasHeight); + this.restoreStyles(element, ignoredStyles); + } + render(_) { + // NO RENDER + } + } + + class FeDropShadowElement extends Element { + type = 'feDropShadow'; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + this.addStylesFromStyleDefinition(); + } + apply(_, _x, _y, _width, _height) { + // TODO: implement + } + } + + class FeMorphologyElement extends Element { + type = 'feMorphology'; + apply(_, _x, _y, _width, _height) { + // TODO: implement + } + } + + class FeCompositeElement extends Element { + type = 'feComposite'; + apply(_, _x, _y, _width, _height) { + // TODO: implement + } + } + + function _typeof(obj) { + "@babel/helpers - typeof"; + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function(obj) { + return typeof obj; + }; + } else { + _typeof = function(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + return _typeof(obj); + } + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + /* eslint-disable no-bitwise -- used for calculations */ /* eslint-disable unicorn/prefer-query-selector -- aiming at + backward-compatibility */ /** + * StackBlur - a fast almost Gaussian Blur For Canvas + * + * In case you find this class useful - especially in commercial projects - + * I am not totally unhappy for a small donation to my PayPal account + * mario@quasimondo.de + * + * Or support me on flattr: + * {@link https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript}. + * + * @module StackBlur + * @author Mario Klingemann + * Contact: mario@quasimondo.com + * Website: {@link http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html} + * Twitter: @quasimondo + * + * @copyright (c) 2010 Mario Klingemann + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ var mulTable = [ + 512, + 512, + 456, + 512, + 328, + 456, + 335, + 512, + 405, + 328, + 271, + 456, + 388, + 335, + 292, + 512, + 454, + 405, + 364, + 328, + 298, + 271, + 496, + 456, + 420, + 388, + 360, + 335, + 312, + 292, + 273, + 512, + 482, + 454, + 428, + 405, + 383, + 364, + 345, + 328, + 312, + 298, + 284, + 271, + 259, + 496, + 475, + 456, + 437, + 420, + 404, + 388, + 374, + 360, + 347, + 335, + 323, + 312, + 302, + 292, + 282, + 273, + 265, + 512, + 497, + 482, + 468, + 454, + 441, + 428, + 417, + 405, + 394, + 383, + 373, + 364, + 354, + 345, + 337, + 328, + 320, + 312, + 305, + 298, + 291, + 284, + 278, + 271, + 265, + 259, + 507, + 496, + 485, + 475, + 465, + 456, + 446, + 437, + 428, + 420, + 412, + 404, + 396, + 388, + 381, + 374, + 367, + 360, + 354, + 347, + 341, + 335, + 329, + 323, + 318, + 312, + 307, + 302, + 297, + 292, + 287, + 282, + 278, + 273, + 269, + 265, + 261, + 512, + 505, + 497, + 489, + 482, + 475, + 468, + 461, + 454, + 447, + 441, + 435, + 428, + 422, + 417, + 411, + 405, + 399, + 394, + 389, + 383, + 378, + 373, + 368, + 364, + 359, + 354, + 350, + 345, + 341, + 337, + 332, + 328, + 324, + 320, + 316, + 312, + 309, + 305, + 301, + 298, + 294, + 291, + 287, + 284, + 281, + 278, + 274, + 271, + 268, + 265, + 262, + 259, + 257, + 507, + 501, + 496, + 491, + 485, + 480, + 475, + 470, + 465, + 460, + 456, + 451, + 446, + 442, + 437, + 433, + 428, + 424, + 420, + 416, + 412, + 408, + 404, + 400, + 396, + 392, + 388, + 385, + 381, + 377, + 374, + 370, + 367, + 363, + 360, + 357, + 354, + 350, + 347, + 344, + 341, + 338, + 335, + 332, + 329, + 326, + 323, + 320, + 318, + 315, + 312, + 310, + 307, + 304, + 302, + 299, + 297, + 294, + 292, + 289, + 287, + 285, + 282, + 280, + 278, + 275, + 273, + 271, + 269, + 267, + 265, + 263, + 261, + 259 + ]; + var shgTable = [ + 9, + 11, + 12, + 13, + 13, + 14, + 14, + 15, + 15, + 15, + 15, + 16, + 16, + 16, + 16, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24 + ]; + /** + * @param {string|HTMLCanvasElement} canvas + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @throws {Error|TypeError} + * @returns {ImageData} See {@link https://html.spec.whatwg.org/multipage/canvas.html#imagedata} + */ function getImageDataFromCanvas(canvas, topX, topY, width, height) { + if (typeof canvas === 'string') { + canvas = document.getElementById(canvas); + } + if (!canvas || _typeof(canvas) !== 'object' || !('getContext' in canvas)) { + throw new TypeError('Expecting canvas with `getContext` method ' + 'in processCanvasRGB(A) calls!'); + } + var context = canvas.getContext('2d'); + try { + return context.getImageData(topX, topY, width, height); + } catch (e) { + throw new Error('unable to access image data: ' + e); + } + } + /** + * @param {HTMLCanvasElement} canvas + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {undefined} + */ function processCanvasRGBA(canvas, topX, topY, width, height, radius) { + if (isNaN(radius) || radius < 1) { + return; + } + radius |= 0; + var imageData = getImageDataFromCanvas(canvas, topX, topY, width, height); + imageData = processImageDataRGBA(imageData, topX, topY, width, height, radius); + canvas.getContext('2d').putImageData(imageData, topX, topY); + } + /** + * @param {ImageData} imageData + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {ImageData} + */ function processImageDataRGBA(imageData, topX, topY, width, height, radius) { + var pixels = imageData.data; + var div = 2 * radius + 1; // const w4 = width << 2; + var widthMinus1 = width - 1; + var heightMinus1 = height - 1; + var radiusPlus1 = radius + 1; + var sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2; + var stackStart = new BlurStack(); + var stack = stackStart; + var stackEnd; + for(var i = 1; i < div; i++){ + stack = stack.next = new BlurStack(); + if (i === radiusPlus1) { + stackEnd = stack; + } + } + stack.next = stackStart; + var stackIn = null, stackOut = null, yw = 0, yi = 0; + var mulSum = mulTable[radius]; + var shgSum = shgTable[radius]; + for(var y = 0; y < height; y++){ + stack = stackStart; + var pr = pixels[yi], pg = pixels[yi + 1], pb = pixels[yi + 2], pa = pixels[yi + 3]; + for(var _i = 0; _i < radiusPlus1; _i++){ + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + var rInSum = 0, gInSum = 0, bInSum = 0, aInSum = 0, rOutSum = radiusPlus1 * pr, gOutSum = radiusPlus1 * pg, bOutSum = radiusPlus1 * pb, aOutSum = radiusPlus1 * pa, rSum = sumFactor * pr, gSum = sumFactor * pg, bSum = sumFactor * pb, aSum = sumFactor * pa; + for(var _i2 = 1; _i2 < radiusPlus1; _i2++){ + var p = yi + ((widthMinus1 < _i2 ? widthMinus1 : _i2) << 2); + var r = pixels[p], g = pixels[p + 1], b = pixels[p + 2], a = pixels[p + 3]; + var rbs = radiusPlus1 - _i2; + rSum += (stack.r = r) * rbs; + gSum += (stack.g = g) * rbs; + bSum += (stack.b = b) * rbs; + aSum += (stack.a = a) * rbs; + rInSum += r; + gInSum += g; + bInSum += b; + aInSum += a; + stack = stack.next; + } + stackIn = stackStart; + stackOut = stackEnd; + for(var x = 0; x < width; x++){ + var paInitial = aSum * mulSum >>> shgSum; + pixels[yi + 3] = paInitial; + if (paInitial !== 0) { + var _a2 = 255 / paInitial; + pixels[yi] = (rSum * mulSum >>> shgSum) * _a2; + pixels[yi + 1] = (gSum * mulSum >>> shgSum) * _a2; + pixels[yi + 2] = (bSum * mulSum >>> shgSum) * _a2; + } else { + pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0; + } + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + aSum -= aOutSum; + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + aOutSum -= stackIn.a; + var _p = x + radius + 1; + _p = yw + (_p < widthMinus1 ? _p : widthMinus1) << 2; + rInSum += stackIn.r = pixels[_p]; + gInSum += stackIn.g = pixels[_p + 1]; + bInSum += stackIn.b = pixels[_p + 2]; + aInSum += stackIn.a = pixels[_p + 3]; + rSum += rInSum; + gSum += gInSum; + bSum += bInSum; + aSum += aInSum; + stackIn = stackIn.next; + var _stackOut = stackOut, _r = _stackOut.r, _g = _stackOut.g, _b = _stackOut.b, _a = _stackOut.a; + rOutSum += _r; + gOutSum += _g; + bOutSum += _b; + aOutSum += _a; + rInSum -= _r; + gInSum -= _g; + bInSum -= _b; + aInSum -= _a; + stackOut = stackOut.next; + yi += 4; + } + yw += width; + } + for(var _x = 0; _x < width; _x++){ + yi = _x << 2; + var _pr = pixels[yi], _pg = pixels[yi + 1], _pb = pixels[yi + 2], _pa = pixels[yi + 3], _rOutSum = radiusPlus1 * _pr, _gOutSum = radiusPlus1 * _pg, _bOutSum = radiusPlus1 * _pb, _aOutSum = radiusPlus1 * _pa, _rSum = sumFactor * _pr, _gSum = sumFactor * _pg, _bSum = sumFactor * _pb, _aSum = sumFactor * _pa; + stack = stackStart; + for(var _i3 = 0; _i3 < radiusPlus1; _i3++){ + stack.r = _pr; + stack.g = _pg; + stack.b = _pb; + stack.a = _pa; + stack = stack.next; + } + var yp = width; + var _gInSum = 0, _bInSum = 0, _aInSum = 0, _rInSum = 0; + for(var _i4 = 1; _i4 <= radius; _i4++){ + yi = yp + _x << 2; + var _rbs = radiusPlus1 - _i4; + _rSum += (stack.r = _pr = pixels[yi]) * _rbs; + _gSum += (stack.g = _pg = pixels[yi + 1]) * _rbs; + _bSum += (stack.b = _pb = pixels[yi + 2]) * _rbs; + _aSum += (stack.a = _pa = pixels[yi + 3]) * _rbs; + _rInSum += _pr; + _gInSum += _pg; + _bInSum += _pb; + _aInSum += _pa; + stack = stack.next; + if (_i4 < heightMinus1) { + yp += width; + } + } + yi = _x; + stackIn = stackStart; + stackOut = stackEnd; + for(var _y = 0; _y < height; _y++){ + var _p2 = yi << 2; + pixels[_p2 + 3] = _pa = _aSum * mulSum >>> shgSum; + if (_pa > 0) { + _pa = 255 / _pa; + pixels[_p2] = (_rSum * mulSum >>> shgSum) * _pa; + pixels[_p2 + 1] = (_gSum * mulSum >>> shgSum) * _pa; + pixels[_p2 + 2] = (_bSum * mulSum >>> shgSum) * _pa; + } else { + pixels[_p2] = pixels[_p2 + 1] = pixels[_p2 + 2] = 0; + } + _rSum -= _rOutSum; + _gSum -= _gOutSum; + _bSum -= _bOutSum; + _aSum -= _aOutSum; + _rOutSum -= stackIn.r; + _gOutSum -= stackIn.g; + _bOutSum -= stackIn.b; + _aOutSum -= stackIn.a; + _p2 = _x + ((_p2 = _y + radiusPlus1) < heightMinus1 ? _p2 : heightMinus1) * width << 2; + _rSum += _rInSum += stackIn.r = pixels[_p2]; + _gSum += _gInSum += stackIn.g = pixels[_p2 + 1]; + _bSum += _bInSum += stackIn.b = pixels[_p2 + 2]; + _aSum += _aInSum += stackIn.a = pixels[_p2 + 3]; + stackIn = stackIn.next; + _rOutSum += _pr = stackOut.r; + _gOutSum += _pg = stackOut.g; + _bOutSum += _pb = stackOut.b; + _aOutSum += _pa = stackOut.a; + _rInSum -= _pr; + _gInSum -= _pg; + _bInSum -= _pb; + _aInSum -= _pa; + stackOut = stackOut.next; + yi += width; + } + } + return imageData; + } + /** + * + */ var BlurStack = /** + * Set properties. + */ function BlurStack() { + _classCallCheck(this, BlurStack); + this.r = 0; + this.g = 0; + this.b = 0; + this.a = 0; + this.next = null; + }; + + class FeGaussianBlurElement extends Element { + type = 'feGaussianBlur'; + extraFilterDistance; + blurRadius; + constructor(document, node, captureTextNodes){ + super(document, node, captureTextNodes); + this.blurRadius = Math.floor(this.getAttribute('stdDeviation').getNumber()); + this.extraFilterDistance = this.blurRadius; + } + apply(ctx, x, y, width, height) { + const { document, blurRadius } = this; + const body = document.window ? document.window.document.body : null; + const canvas = ctx.canvas; + // StackBlur requires canvas be on document + canvas.id = document.getUniqueId(); + if (body) { + canvas.style.display = 'none'; + body.appendChild(canvas); + } + processCanvasRGBA(canvas, x, y, width, height, blurRadius); + if (body) { + body.removeChild(canvas); + } + } + } + + class TitleElement extends Element { + type = 'title'; + } + + class DescElement extends Element { + type = 'desc'; + } + + const elements = { + 'svg': SVGElement, + 'rect': RectElement, + 'circle': CircleElement, + 'ellipse': EllipseElement, + 'line': LineElement, + 'polyline': PolylineElement, + 'polygon': PolygonElement, + 'path': PathElement, + 'pattern': PatternElement, + 'marker': MarkerElement, + 'defs': DefsElement, + 'linearGradient': LinearGradientElement, + 'radialGradient': RadialGradientElement, + 'stop': StopElement, + 'animate': AnimateElement, + 'animateColor': AnimateColorElement, + 'animateTransform': AnimateTransformElement, + 'font': FontElement, + 'font-face': FontFaceElement, + 'missing-glyph': MissingGlyphElement, + 'glyph': GlyphElement, + 'text': TextElement, + 'tspan': TSpanElement, + 'tref': TRefElement, + 'a': AElement, + 'textPath': TextPathElement, + 'image': ImageElement, + 'g': GElement, + 'symbol': SymbolElement, + 'style': StyleElement, + 'use': UseElement, + 'mask': MaskElement, + 'clipPath': ClipPathElement, + 'filter': FilterElement, + 'feDropShadow': FeDropShadowElement, + 'feMorphology': FeMorphologyElement, + 'feComposite': FeCompositeElement, + 'feColorMatrix': FeColorMatrixElement, + 'feGaussianBlur': FeGaussianBlurElement, + 'title': TitleElement, + 'desc': DescElement + }; + + function createCanvas(width, height) { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + } + async function createImage(src) { + let anonymousCrossOrigin = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + const image = document.createElement('img'); + if (anonymousCrossOrigin) { + image.crossOrigin = 'Anonymous'; + } + return new Promise((resolve, reject)=>{ + image.onload = ()=>{ + resolve(image); + }; + image.onerror = (_event, _source, _lineno, _colno, error)=>{ + reject(error); + }; + image.src = src; + }); + } + const DEFAULT_EM_SIZE = 12; + class Document { + canvg; + static createCanvas = createCanvas; + static createImage = createImage; + static elementTypes = elements; + rootEmSize; + documentElement; + screen; + createCanvas; + createImage; + definitions; + styles; + stylesSpecificity; + images; + fonts; + emSizeStack; + uniqueId; + constructor(canvg, { rootEmSize = DEFAULT_EM_SIZE, emSize = DEFAULT_EM_SIZE, createCanvas = Document.createCanvas, createImage = Document.createImage, anonymousCrossOrigin } = {}){ + this.canvg = canvg; + this.definitions = {}; + this.styles = {}; + this.stylesSpecificity = {}; + this.images = []; + this.fonts = []; + this.emSizeStack = []; + this.uniqueId = 0; + this.screen = canvg.screen; + this.rootEmSize = rootEmSize; + this.emSize = emSize; + this.createCanvas = createCanvas; + this.createImage = this.bindCreateImage(createImage, anonymousCrossOrigin); + this.screen.wait(()=>this.isImagesLoaded()); + this.screen.wait(()=>this.isFontsLoaded()); + } + bindCreateImage(createImage, anonymousCrossOrigin) { + if (typeof anonymousCrossOrigin === 'boolean') { + return (source, forceAnonymousCrossOrigin)=>createImage(source, typeof forceAnonymousCrossOrigin === 'boolean' ? forceAnonymousCrossOrigin : anonymousCrossOrigin); + } + return createImage; + } + get window() { + return this.screen.window; + } + get fetch() { + return this.screen.fetch; + } + get ctx() { + return this.screen.ctx; + } + get emSize() { + const { emSizeStack } = this; + return emSizeStack[emSizeStack.length - 1] || DEFAULT_EM_SIZE; + } + set emSize(value) { + const { emSizeStack } = this; + emSizeStack.push(value); + } + popEmSize() { + const { emSizeStack } = this; + emSizeStack.pop(); + } + getUniqueId() { + return `canvg${++this.uniqueId}`; + } + isImagesLoaded() { + return this.images.every((_)=>_.loaded); + } + isFontsLoaded() { + return this.fonts.every((_)=>_.loaded); + } + createDocumentElement(document1) { + const documentElement = this.createElement(document1.documentElement); + documentElement.root = true; + documentElement.addStylesFromStyleDefinition(); + this.documentElement = documentElement; + return documentElement; + } + createElement(node) { + const elementType = node.nodeName.replace(/^[^:]+:/, ''); + const ElementType = Document.elementTypes[elementType]; + if (ElementType) { + return new ElementType(this, node); + } + return new UnknownElement(this, node); + } + createTextNode(node) { + return new TextNode(this, node); + } + setViewBox(config) { + this.screen.setViewBox({ + document: this, + ...config + }); + } + } + + /** + * SVG renderer on canvas. + */ class Canvg { + /** + * Create Canvg instance from SVG source string or URL. + * @param ctx - Rendering context. + * @param svg - SVG source string or URL. + * @param options - Rendering options. + * @returns Canvg instance. + */ static async from(ctx, svg) { + let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}; + const parser = new Parser(options); + const svgDocument = await parser.parse(svg); + return new Canvg(ctx, svgDocument, options); + } + /** + * Create Canvg instance from SVG source string. + * @param ctx - Rendering context. + * @param svg - SVG source string. + * @param options - Rendering options. + * @returns Canvg instance. + */ static fromString(ctx, svg) { + let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}; + const parser = new Parser(options); + const svgDocument = parser.parseFromString(svg); + return new Canvg(ctx, svgDocument, options); + } + /** + * XML/HTML parser instance. + */ parser; + /** + * Screen instance. + */ screen; + /** + * Canvg Document. + */ document; + documentElement; + options; + /** + * Main constructor. + * @param ctx - Rendering context. + * @param svg - SVG Document. + * @param options - Rendering options. + */ constructor(ctx, svg, options = {}){ + this.parser = new Parser(options); + this.screen = new Screen(ctx, options); + this.options = options; + const document = new Document(this, options); + const documentElement = document.createDocumentElement(svg); + this.document = document; + this.documentElement = documentElement; + } + /** + * Create new Canvg instance with inherited options. + * @param ctx - Rendering context. + * @param svg - SVG source string or URL. + * @param options - Rendering options. + * @returns Canvg instance. + */ fork(ctx, svg) { + let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}; + return Canvg.from(ctx, svg, { + ...this.options, + ...options + }); + } + /** + * Create new Canvg instance with inherited options. + * @param ctx - Rendering context. + * @param svg - SVG source string. + * @param options - Rendering options. + * @returns Canvg instance. + */ forkString(ctx, svg) { + let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}; + return Canvg.fromString(ctx, svg, { + ...this.options, + ...options + }); + } + /** + * Document is ready promise. + * @returns Ready promise. + */ ready() { + return this.screen.ready(); + } + /** + * Document is ready value. + * @returns Is ready or not. + */ isReady() { + return this.screen.isReady(); + } + /** + * Render only first frame, ignoring animations and mouse. + * @param options - Rendering options. + */ async render() { + let options = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; + this.start({ + enableRedraw: true, + ignoreAnimation: true, + ignoreMouse: true, + ...options + }); + await this.ready(); + this.stop(); + } + /** + * Start rendering. + * @param options - Render options. + */ start() { + let options = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; + const { documentElement, screen, options: baseOptions } = this; + screen.start(documentElement, { + enableRedraw: true, + ...baseOptions, + ...options + }); + } + /** + * Stop rendering. + */ stop() { + this.screen.stop(); + } + /** + * Resize SVG to fit in given size. + * @param width + * @param height + * @param preserveAspectRatio + */ resize(width) { + let height = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : width, preserveAspectRatio = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false; + this.documentElement.resize(width, height, preserveAspectRatio); + } + } + + exports.AElement = AElement; + exports.AnimateColorElement = AnimateColorElement; + exports.AnimateElement = AnimateElement; + exports.AnimateTransformElement = AnimateTransformElement; + exports.BoundingBox = BoundingBox; + exports.CB1 = CB1; + exports.CB2 = CB2; + exports.CB3 = CB3; + exports.CB4 = CB4; + exports.Canvg = Canvg; + exports.CircleElement = CircleElement; + exports.ClipPathElement = ClipPathElement; + exports.DefsElement = DefsElement; + exports.DescElement = DescElement; + exports.Document = Document; + exports.Element = Element; + exports.EllipseElement = EllipseElement; + exports.FeColorMatrixElement = FeColorMatrixElement; + exports.FeCompositeElement = FeCompositeElement; + exports.FeDropShadowElement = FeDropShadowElement; + exports.FeGaussianBlurElement = FeGaussianBlurElement; + exports.FeMorphologyElement = FeMorphologyElement; + exports.FilterElement = FilterElement; + exports.Font = Font; + exports.FontElement = FontElement; + exports.FontFaceElement = FontFaceElement; + exports.GElement = GElement; + exports.GlyphElement = GlyphElement; + exports.GradientElement = GradientElement; + exports.ImageElement = ImageElement; + exports.LineElement = LineElement; + exports.LinearGradientElement = LinearGradientElement; + exports.MarkerElement = MarkerElement; + exports.MaskElement = MaskElement; + exports.Matrix = Matrix; + exports.MissingGlyphElement = MissingGlyphElement; + exports.Mouse = Mouse; + exports.PSEUDO_ZERO = PSEUDO_ZERO; + exports.Parser = Parser; + exports.PathElement = PathElement; + exports.PathParser = PathParser; + exports.PatternElement = PatternElement; + exports.Point = Point; + exports.PolygonElement = PolygonElement; + exports.PolylineElement = PolylineElement; + exports.Property = Property; + exports.QB1 = QB1; + exports.QB2 = QB2; + exports.QB3 = QB3; + exports.RadialGradientElement = RadialGradientElement; + exports.RectElement = RectElement; + exports.RenderedElement = RenderedElement; + exports.Rotate = Rotate; + exports.SVGElement = SVGElement; + exports.SVGFontLoader = SVGFontLoader; + exports.Scale = Scale; + exports.Screen = Screen; + exports.Skew = Skew; + exports.SkewX = SkewX; + exports.SkewY = SkewY; + exports.StopElement = StopElement; + exports.StyleElement = StyleElement; + exports.SymbolElement = SymbolElement; + exports.TRefElement = TRefElement; + exports.TSpanElement = TSpanElement; + exports.TextElement = TextElement; + exports.TextPathElement = TextPathElement; + exports.TitleElement = TitleElement; + exports.Transform = Transform; + exports.Translate = Translate; + exports.UnknownElement = UnknownElement; + exports.UseElement = UseElement; + exports.ViewPort = ViewPort; + exports.compressSpaces = compressSpaces; + exports.elements = elements; + exports.getSelectorSpecificity = getSelectorSpecificity; + exports.normalizeAttributeName = normalizeAttributeName; + exports.normalizeColor = normalizeColor; + exports.parseExternalUrl = parseExternalUrl; + exports.presets = index; + exports.toMatrixValue = toMatrixValue; + exports.toNumbers = toNumbers; + exports.trimLeft = trimLeft; + exports.trimRight = trimRight; + exports.vectorMagnitude = vectorMagnitude; + exports.vectorsAngle = vectorsAngle; + exports.vectorsRatio = vectorsRatio; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); +//# sourceMappingURL=umd.js.map