canvg
Version:
JavaScript SVG parser and renderer on Canvas.
1,520 lines (1,494 loc) • 193 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var requestAnimationFrame = require('raf');
var RGBColor = require('rgbcolor');
var svgPathdata = require('svg-pathdata');
var stackblurCanvas = require('stackblur-canvas');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var requestAnimationFrame__default = /*#__PURE__*/_interopDefaultLegacy(requestAnimationFrame);
var RGBColor__default = /*#__PURE__*/_interopDefaultLegacy(RGBColor);
/**
* 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__default["default"](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__default["default"](tick);
};
if (!ignoreMouse) {
mouse.start();
}
this.intervalId = requestAnimationFrame__default["default"](tick);
}
stop() {
if (this.intervalId) {
requestAnimationFrame__default["default"].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;