UNPKG

canvg

Version:

JavaScript SVG parser and renderer on Canvas.

1,520 lines (1,499 loc) 191 kB
import requestAnimationFrame from 'raf'; import RGBColor from 'rgbcolor'; import { SVGPathData } from 'svg-pathdata'; import { canvasRGBA } from 'stackblur-canvas'; /** * Options preset for `OffscreenCanvas`. * @param config - Preset requirements. * @param config.DOMParser - XML/HTML parser from string into DOM Document. * @returns Preset object. */ function offscreen() { let { DOMParser: DOMParserFallback } = arguments.length > 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; } /** * 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 = 0.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); } class Property { static empty(document) { return new Property(document, 'EMPTY', ''); } 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; } 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 // 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 * Math.min(viewPort.computeSize('x'), viewPort.computeSize('y')); case this.isString(/vmax$/): return this.getNumber() / 100 * Math.max(viewPort.computeSize('x'), viewPort.computeSize('y')); case this.isString(/vw$/): return this.getNumber() / 100 * viewPort.computeSize('x'); case this.isString(/vh$/): return this.getNumber() / 100 * 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; case this.isString(/px$/): return this.getNumber(); case this.isString(/pt$/): return this.getNumber() * this.getDpi() * (1 / 72); 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) { 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); case this.isString(/grad$/): return this.getNumber() * (Math.PI / 200); case this.isString(/rad$/): return this.getNumber(); default: return this.getNumber() * (Math.PI / 180); } } getDefinition() { const asString = this.getString(); const match = /#([^)'"]+)/.exec(asString); const name = (match === null || match === void 0 ? void 0 : 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); } constructor(document, name, value){ this.document = document; this.name = name; this.value = value; this.isNormalizedColor = false; } } Property.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' }; class ViewPort { 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); } constructor(){ this.viewPorts = []; } } ViewPort.DEFAULT_VIEWPORT_WIDTH = 800; ViewPort.DEFAULT_VIEWPORT_HEIGHT = 600; function getDefault() { return { width: ViewPort.DEFAULT_VIEWPORT_WIDTH, height: ViewPort.DEFAULT_VIEWPORT_HEIGHT }; } class Point { 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; } 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; } constructor(x, y){ this.x = x; this.y = y; } } class Mouse { 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 === null || window === void 0 ? void 0 : window.scrollX) { point.x += window.scrollX; } if (window === null || window === void 0 ? void 0 : 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(); } } }); } constructor(screen){ this.screen = screen; this.working = false; this.events = []; this.eventElements = []; this.onClick = this.onClick.bind(this); this.onMouseMove = this.onMouseMove.bind(this); } } 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 { 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 - finalDesiredWidth / 2, 0); } if (align.endsWith('YMid') && (isMeetMinX || isSliceMaxX)) { ctx.translate(0, height / 2 - finalDesiredHeight / 2); } 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 shouldUpdate1 = this.animations.reduce((shouldUpdate, animation)=>animation.update(frameDuration) || shouldUpdate , false); if (shouldUpdate1) { 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 = "".concat(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 = "".concat(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("".concat(transformStyle.getString(), " scale(").concat(1 / xRatio, ", ").concat(1 / yRatio, ")")); } // clear and render if (!ignoreClear) { ctx.clearRect(0, 0, cWidth, cHeight); } element.render(ctx); if (isFirstRender) { this.isFirstRender = false; } } constructor(ctx, { fetch =defaultFetch$1 , window =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 = window; if (!fetch) { throw new Error("Can't find 'fetch' in 'globalThis', please provide it via options"); } this.fetch = fetch; } } Screen.defaultWindow = defaultWindow; Screen.defaultFetch = defaultFetch$1; Screen.FRAMERATE = 30; Screen.MAX_VIRTUAL_PIXELS = 30000; const { defaultFetch } = Screen; const DefaultDOMParser = typeof DOMParser !== 'undefined' ? DOMParser : undefined; class Parser { 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); } constructor({ fetch =defaultFetch , DOMParser =DefaultDOMParser } = {}){ if (!fetch) { throw new Error("Can't find 'fetch' in 'globalThis', please provide it via options"); } if (!DOMParser) { throw new Error("Can't find 'DOMParser' in 'globalThis', please provide it via options"); } this.fetch = fetch; this.DOMParser = DOMParser; } } class Translate { apply(ctx) { const { x , y } = this.point; ctx.translate(x || 0, y || 0); } unapply(ctx) { const { x , y } = this.point; ctx.translate(-1 * x || 0, -1 * y || 0); } applyToPoint(point) { const { x , y } = this.point; point.applyTransform([ 1, 0, 0, 1, x || 0, y || 0 ]); } constructor(_, point){ this.type = 'translate'; this.point = Point.parse(point); } } class Rotate { 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 * 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, cy || 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, -cy || 0 // -this.p.y ]); } constructor(document, rotate, transformOrigin){ this.type = 'rotate'; 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; } } class Scale { 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 / x, 1 / y || x); ctx.translate(-tx, -ty); } applyToPoint(point) { const { x , y } = this.scale; point.applyTransform([ x || 0, 0, 0, y || 0, 0, 0 ]); } constructor(_, scale, transformOrigin){ this.type = 'scale'; 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]; } } class Matrix { 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; const h = 0; const i = 1; 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); } constructor(_, matrix, transformOrigin){ this.type = 'matrix'; this.matrix = toMatrixValue(matrix); this.originX = transformOrigin[0]; this.originY = transformOrigin[1]; } } class Skew extends Matrix { constructor(document, skew, transformOrigin){ super(document, skew, transformOrigin); this.type = 'skew'; this.angle = new Property(document, 'angle', skew); } } class SkewX extends Skew { constructor(document, skew, transformOrigin){ super(document, skew, transformOrigin); this.type = 'skewX'; this.matrix = [ 1, 0, Math.tan(this.angle.getRadians()), 1, 0, 0 ]; } } class SkewY extends Skew { constructor(document, skew, transformOrigin){ super(document, skew, transformOrigin); this.type = 'skewY'; 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 { 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; } 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) ); } constructor(document, transform1, transformOrigin){ this.document = document; this.transforms = []; const data = parseTransforms(transform1); 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)); } }); } } Transform.transformTypes = { translate: Translate, rotate: Rotate, scale: Scale, matrix: Matrix, skewX: SkewX, skewY: SkewY }; class Element { 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) { var ref; const { node } = this; if (typeof node.matches === 'function') { return node.matches(selector); } const styleClasses = (ref = node.getAttribute) === null || ref === void 0 ? void 0 : ref.call(node, 'class'); if (!styleClasses || styleClasses === '') { return false; } return styleClasses.split(' ').some((styleClass)=>".".concat(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 toRestore1 = 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 toRestore1; } restoreStyles(element, styles) { styles.forEach((param)=>{ let [name, value] = param; element.getStyle(name, true).setValue(value); }); } isFirstChild() { var ref; return ((ref = this.parent) === null || ref === void 0 ? void 0 : ref.children.indexOf(this)) === 0; } 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.addChi