canvg
Version:
JavaScript SVG parser and renderer on Canvas.
1 lines • 397 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/presets/offscreen.ts","../src/presets/node.ts","../src/util/string.ts","../src/util/styles.ts","../src/util/math.ts","../src/Property.ts","../src/ViewPort.ts","../src/Point.ts","../src/Mouse.ts","../src/Screen.ts","../src/Parser.ts","../src/Transform/Translate.ts","../src/Transform/Rotate.ts","../src/Transform/Scale.ts","../src/Transform/Matrix.ts","../src/Transform/Skew.ts","../src/Transform/SkewX.ts","../src/Transform/SkewY.ts","../src/Transform/Transform.ts","../src/Document/Element.ts","../src/Document/UnknownElement.ts","../src/Font.ts","../src/BoundingBox.ts","../src/Document/RenderedElement.ts","../src/Document/TextElement.ts","../src/Document/TSpanElement.ts","../src/Document/TextNode.ts","../src/PathParser.ts","../src/Document/PathElement.ts","../src/Document/SVGElement.ts","../src/Document/RectElement.ts","../src/Document/CircleElement.ts","../src/Document/EllipseElement.ts","../src/Document/LineElement.ts","../src/Document/PolylineElement.ts","../src/Document/PolygonElement.ts","../src/Document/PatternElement.ts","../src/Document/MarkerElement.ts","../src/Document/DefsElement.ts","../src/Document/GElement.ts","../src/Document/GradientElement.ts","../src/Document/LinearGradientElement.ts","../src/Document/RadialGradientElement.ts","../src/Document/StopElement.ts","../src/Document/AnimateElement.ts","../src/Document/AnimateColorElement.ts","../src/Document/AnimateTransformElement.ts","../src/Document/FontFaceElement.ts","../src/Document/GlyphElement.ts","../src/Document/MissingGlyphElement.ts","../src/Document/FontElement.ts","../src/Document/TRefElement.ts","../src/Document/AElement.ts","../src/Document/TextPathElement.ts","../src/Document/ImageElement.ts","../src/Document/SymbolElement.ts","../src/SVGFontLoader.ts","../src/Document/StyleElement.ts","../src/Document/UseElement.ts","../src/Document/FeColorMatrixElement.ts","../src/Document/MaskElement.ts","../src/Document/ClipPathElement.ts","../src/Document/FilterElement.ts","../src/Document/FeDropShadowElement.ts","../src/Document/FeMorphologyElement.ts","../src/Document/FeCompositeElement.ts","../src/Document/FeGaussianBlurElement.ts","../src/Document/TitleElement.ts","../src/Document/DescElement.ts","../src/Document/elements.ts","../src/Document/Document.ts","../src/Canvg.ts"],"sourcesContent":["import { DOMParser } from './types'\n\ninterface IConfig {\n /**\n * XML/HTML parser from string into DOM Document.\n */\n DOMParser?: DOMParser\n}\n\n/**\n * Options preset for `OffscreenCanvas`.\n * @param config - Preset requirements.\n * @param config.DOMParser - XML/HTML parser from string into DOM Document.\n * @returns Preset object.\n */\nexport function offscreen({ DOMParser: DOMParserFallback }: IConfig = {}) {\n const preset = {\n window: null,\n ignoreAnimation: true,\n ignoreMouse: true,\n DOMParser: DOMParserFallback,\n createCanvas(width: number, height: number) {\n return new OffscreenCanvas(width, height)\n },\n async createImage(url: string) {\n const response = await fetch(url)\n const blob = await response.blob()\n const img = await createImageBitmap(blob)\n\n return img\n }\n }\n\n if (typeof globalThis.DOMParser !== 'undefined'\n || typeof DOMParserFallback === 'undefined'\n ) {\n Reflect.deleteProperty(preset, 'DOMParser')\n }\n\n return preset\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { DOMParser } from './types'\n\n/**\n * `node-canvas` exports.\n */\ninterface ICanvas {\n createCanvas(width: number, height: number): any\n loadImage(src: string): Promise<any>\n}\n\n/**\n * WHATWG-compatible `fetch` function.\n */\ntype Fetch = (input: any, config?: any) => Promise<any>\n\ninterface IConfig {\n /**\n * XML/HTML parser from string into DOM Document.\n */\n DOMParser: DOMParser\n /**\n * `node-canvas` exports.\n */\n canvas: ICanvas\n /**\n * WHATWG-compatible `fetch` function.\n */\n fetch: Fetch\n}\n\n/**\n * Options preset for `node-canvas`.\n * @param config - Preset requirements.\n * @param config.DOMParser - XML/HTML parser from string into DOM Document.\n * @param config.canvas - `node-canvas` exports.\n * @param config.fetch - WHATWG-compatible `fetch` function.\n * @returns Preset object.\n */\nexport function node({\n DOMParser,\n canvas,\n fetch\n}: IConfig) {\n return {\n window: null,\n ignoreAnimation: true,\n ignoreMouse: true,\n DOMParser,\n fetch,\n createCanvas: canvas.createCanvas,\n createImage: canvas.loadImage\n }\n}\n","import { MatrixValue } from '../types'\n\n/**\n * HTML-safe compress white-spaces.\n * @param str - String to compress.\n * @returns String.\n */\nexport function compressSpaces(str: string) {\n return str.replace(/(?!\\u3000)\\s+/gm, ' ')\n}\n\n/**\n * HTML-safe left trim.\n * @param str - String to trim.\n * @returns String.\n */\nexport function trimLeft(str: string) {\n return str.replace(/^[\\n \\t]+/, '')\n}\n\n/**\n * HTML-safe right trim.\n * @param str - String to trim.\n * @returns String.\n */\nexport function trimRight(str: string) {\n return str.replace(/[\\n \\t]+$/, '')\n}\n\n/**\n * String to numbers array.\n * @param str - Numbers string.\n * @returns Numbers array.\n */\nexport function toNumbers(str: string) {\n const matches = str.match(/-?(\\d+(?:\\.\\d*(?:[eE][+-]?\\d+)?)?|\\.\\d+)(?=\\D|$)/gm)\n\n return matches ? matches.map(parseFloat) : []\n}\n\n/**\n * String to matrix value.\n * @param str - Numbers string.\n * @returns Matrix value.\n */\nexport function toMatrixValue(str: string): MatrixValue {\n const numbers = toNumbers(str)\n const matrix = [\n numbers[0] || 0,\n numbers[1] || 0,\n numbers[2] || 0,\n numbers[3] || 0,\n numbers[4] || 0,\n numbers[5] || 0\n ] as const\n\n return matrix\n}\n\n// Microsoft Edge fix\nconst allUppercase = /^[A-Z-]+$/\n\n/**\n * Normalize attribute name.\n * @param name - Attribute name.\n * @returns Normalized attribute name.\n */\nexport function normalizeAttributeName(name: string) {\n if (allUppercase.test(name)) {\n return name.toLowerCase()\n }\n\n return name\n}\n\n/**\n * Parse external URL.\n * @param url - CSS url string.\n * @returns Parsed URL.\n */\nexport function parseExternalUrl(url: string): string {\n // single quotes [2]\n // v double quotes [3]\n // v v no quotes [4]\n // v v v\n const urlMatch = /url\\(('([^']+)'|\"([^\"]+)\"|([^'\")]+))\\)/.exec(url)\n\n if (!urlMatch) {\n return ''\n }\n\n return urlMatch[2] || urlMatch[3] || urlMatch[4] || ''\n}\n\n/**\n * Transform floats to integers in rgb colors.\n * @param color - Color to normalize.\n * @returns Normalized color.\n */\nexport function normalizeColor(color: string) {\n if (!color.startsWith('rgb')) {\n return color\n }\n\n let rgbParts = 3\n const normalizedColor = color.replace(\n /\\d+(\\.\\d+)?/g,\n (num, isFloat) => (rgbParts-- && isFloat\n ? String(Math.round(parseFloat(num)))\n : num)\n )\n\n return normalizedColor\n}\n","\n// slightly modified version of https://github.com/keeganstreet/specificity/blob/master/specificity.js\nconst attributeRegex = /(\\[[^\\]]+\\])/g\nconst idRegex = /(#[^\\s+>~.[:]+)/g\nconst classRegex = /(\\.[^\\s+>~.[:]+)/g\nconst pseudoElementRegex = /(::[^\\s+>~.[:]+|:first-line|:first-letter|:before|:after)/gi\nconst pseudoClassWithBracketsRegex = /(:[\\w-]+\\([^)]*\\))/gi\nconst pseudoClassRegex = /(:[^\\s+>~.[:]+)/g\nconst elementRegex = /([^\\s+>~.[:]+)/g\n\nfunction findSelectorMatch(selector: string, regex: RegExp): [string, number] {\n const matches = regex.exec(selector)\n\n if (!matches) {\n return [selector, 0]\n }\n\n return [selector.replace(regex, ' '), matches.length]\n}\n\n/**\n * Measure selector specificity.\n * @param selector - Selector to measure.\n * @returns Specificity.\n */\nexport function getSelectorSpecificity(selector: string) {\n const specificity = [\n 0,\n 0,\n 0\n ]\n let currentSelector = selector\n .replace(/:not\\(([^)]*)\\)/g, ' $1 ')\n .replace(/{[\\s\\S]*/gm, ' ')\n let delta = 0;\n\n [currentSelector, delta] = findSelectorMatch(currentSelector, attributeRegex)\n specificity[1] += delta;\n\n [currentSelector, delta] = findSelectorMatch(currentSelector, idRegex)\n specificity[0] += delta;\n\n [currentSelector, delta] = findSelectorMatch(currentSelector, classRegex)\n specificity[1] += delta;\n\n [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoElementRegex)\n specificity[2] += delta;\n\n [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoClassWithBracketsRegex)\n specificity[1] += delta;\n\n [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoClassRegex)\n specificity[1] += delta\n\n currentSelector = currentSelector\n .replace(/[*\\s+>~]/g, ' ')\n .replace(/[#.]/g, ' ');\n\n [currentSelector, delta] = findSelectorMatch(currentSelector, elementRegex) // lgtm [js/useless-assignment-to-local]\n specificity[2] += delta\n\n return specificity.join('')\n}\n","import { VectorValue } from '../types'\n\nexport const PSEUDO_ZERO = .00000001\n\n/**\n * Vector magnitude.\n * @param v\n * @returns Number result.\n */\nexport function vectorMagnitude(v: VectorValue) {\n return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2))\n}\n\n/**\n * Ratio between two vectors.\n * @param u\n * @param v\n * @returns Number result.\n */\nexport function vectorsRatio(u: VectorValue, v: VectorValue) {\n return (u[0] * v[0] + u[1] * v[1]) / (vectorMagnitude(u) * vectorMagnitude(v))\n}\n\n/**\n * Angle between two vectors.\n * @param u\n * @param v\n * @returns Number result.\n */\nexport function vectorsAngle(u: VectorValue, v: VectorValue) {\n return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vectorsRatio(u, v))\n}\n\nexport function CB1(t: number) {\n return t * t * t\n}\n\nexport function CB2(t: number) {\n return 3 * t * t * (1 - t)\n}\n\nexport function CB3(t: number) {\n return 3 * t * (1 - t) * (1 - t)\n}\n\nexport function CB4(t: number) {\n return (1 - t) * (1 - t) * (1 - t)\n}\n\nexport function QB1(t: number) {\n return t * t\n}\n\nexport function QB2(t: number) {\n return 2 * t * (1 - t)\n}\n\nexport function QB3(t: number) {\n return (1 - t) * (1 - t)\n}\n","import RGBColor from 'rgbcolor'\nimport {\n compressSpaces,\n normalizeColor\n} from './util'\nimport { Axis } from './ViewPort'\nimport {\n Document,\n Element,\n PathElement,\n PatternElement,\n GradientElement\n} from './Document'\n\nexport class Property<T = unknown> {\n static empty(document: Document) {\n return new Property(document, 'EMPTY', '')\n }\n\n static readonly textBaselineMapping: Record<string, string> = {\n 'baseline': 'alphabetic',\n 'before-edge': 'top',\n 'text-before-edge': 'top',\n 'middle': 'middle',\n 'central': 'middle',\n 'after-edge': 'bottom',\n 'text-after-edge': 'bottom',\n 'ideographic': 'ideographic',\n 'alphabetic': 'alphabetic',\n 'hanging': 'hanging',\n 'mathematical': 'alphabetic'\n }\n\n private isNormalizedColor = false\n\n constructor(\n private readonly document: Document,\n private readonly name: string,\n private value: T\n ) {}\n\n split(separator = ' ') {\n const {\n document,\n name\n } = this\n\n return compressSpaces(this.getString())\n .trim()\n .split(separator)\n .map(value => new Property<string>(document, name, value))\n }\n\n hasValue(zeroIsValue?: boolean) {\n const value = this.value as unknown\n\n return value !== null\n && value !== ''\n && (zeroIsValue || value !== 0)\n && typeof value !== 'undefined'\n }\n\n isString(regexp?: RegExp) {\n const { value } = this\n const result = typeof value === 'string'\n\n if (!result || !regexp) {\n return result\n }\n\n return regexp.test(value)\n }\n\n isUrlDefinition() {\n return this.isString(/^url\\(/)\n }\n\n isPixels() {\n if (!this.hasValue()) {\n return false\n }\n\n const asString = this.getString()\n\n switch (true) {\n case asString.endsWith('px'):\n case /^[0-9]+$/.test(asString):\n return true\n\n default:\n return false\n }\n }\n\n setValue(value: T) {\n this.value = value\n return this\n }\n\n getValue(def?: T) {\n if (typeof def === 'undefined' || this.hasValue()) {\n return this.value\n }\n\n return def\n }\n\n getNumber(def?: T) {\n if (!this.hasValue()) {\n if (typeof def === 'undefined') {\n return 0\n }\n\n // @ts-expect-error Parse unknown value.\n return parseFloat(def)\n }\n\n const { value } = this\n // @ts-expect-error Parse unknown value.\n let n = parseFloat(value)\n\n if (this.isString(/%$/)) {\n n /= 100.0\n }\n\n return n\n }\n\n getString(def?: T) {\n if (typeof def === 'undefined' || this.hasValue()) {\n return typeof this.value === 'undefined'\n ? ''\n : String(this.value)\n }\n\n return String(def)\n }\n\n getColor(def?: T) {\n let color = this.getString(def)\n\n if (this.isNormalizedColor) {\n return color\n }\n\n this.isNormalizedColor = true\n color = normalizeColor(color)\n this.value = color as unknown as T\n\n return color\n }\n\n getDpi() {\n return 96.0 // TODO: compute?\n }\n\n getRem() {\n return this.document.rootEmSize\n }\n\n getEm() {\n return this.document.emSize\n }\n\n getUnits() {\n return this.getString().replace(/[0-9.-]/g, '')\n }\n\n getPixels(axis?: Axis, processPercent?: boolean): number\n getPixels(isFontSize?: boolean): number\n getPixels(axisOrIsFontSize?: Axis | boolean, processPercent = false): number {\n if (!this.hasValue()) {\n return 0\n }\n\n const [axis, isFontSize] = typeof axisOrIsFontSize === 'boolean'\n ? [undefined, axisOrIsFontSize]\n : [axisOrIsFontSize]\n const { viewPort } = this.document.screen\n\n switch (true) {\n case this.isString(/vmin$/):\n return this.getNumber()\n / 100.0\n * Math.min(\n viewPort.computeSize('x'),\n viewPort.computeSize('y')\n )\n\n case this.isString(/vmax$/):\n return this.getNumber()\n / 100.0\n * Math.max(\n viewPort.computeSize('x'),\n viewPort.computeSize('y')\n )\n\n case this.isString(/vw$/):\n return this.getNumber()\n / 100.0\n * viewPort.computeSize('x')\n\n case this.isString(/vh$/):\n return this.getNumber()\n / 100.0\n * viewPort.computeSize('y')\n\n case this.isString(/rem$/):\n return this.getNumber() * this.getRem(/* viewPort */)\n\n case this.isString(/em$/):\n return this.getNumber() * this.getEm(/* viewPort */)\n\n case this.isString(/ex$/):\n return this.getNumber() * this.getEm(/* viewPort */) / 2.0\n\n case this.isString(/px$/):\n return this.getNumber()\n\n case this.isString(/pt$/):\n return this.getNumber() * this.getDpi(/* viewPort */) * (1.0 / 72.0)\n\n case this.isString(/pc$/):\n return this.getNumber() * 15\n\n case this.isString(/cm$/):\n return this.getNumber() * this.getDpi(/* viewPort */) / 2.54\n\n case this.isString(/mm$/):\n return this.getNumber() * this.getDpi(/* viewPort */) / 25.4\n\n case this.isString(/in$/):\n return this.getNumber() * this.getDpi(/* viewPort */)\n\n case this.isString(/%$/) && isFontSize:\n return this.getNumber() * this.getEm(/* viewPort */)\n\n case this.isString(/%$/):\n return this.getNumber() * viewPort.computeSize(axis)\n\n default: {\n const n = this.getNumber()\n\n if (processPercent && n < 1.0) {\n return n * viewPort.computeSize(axis)\n }\n\n return n\n }\n }\n }\n\n getMilliseconds() {\n if (!this.hasValue()) {\n return 0\n }\n\n if (this.isString(/ms$/)) {\n return this.getNumber()\n }\n\n return this.getNumber() * 1000\n }\n\n getRadians() {\n if (!this.hasValue()) {\n return 0\n }\n\n switch (true) {\n case this.isString(/deg$/):\n return this.getNumber() * (Math.PI / 180.0)\n\n case this.isString(/grad$/):\n return this.getNumber() * (Math.PI / 200.0)\n\n case this.isString(/rad$/):\n return this.getNumber()\n\n default:\n return this.getNumber() * (Math.PI / 180.0)\n }\n }\n\n getDefinition<T extends Element>() {\n const asString = this.getString()\n const match = /#([^)'\"]+)/.exec(asString)\n const name = match?.[1] || asString\n\n return this.document.definitions.get(name) as T | undefined\n }\n\n getFillStyleDefinition(element: Element | PathElement, opacity: Property) {\n let def = this.getDefinition<PatternElement & GradientElement>()\n\n if (!def) {\n return null\n }\n\n // gradient\n if (typeof def.createGradient === 'function' && 'getBoundingBox' in element) {\n return def.createGradient(\n this.document.ctx,\n element,\n opacity\n )\n }\n\n // pattern\n if (typeof def.createPattern === 'function') {\n if (def.getHrefAttribute().hasValue()) {\n const patternTransform = def.getAttribute('patternTransform')\n\n def = def.getHrefAttribute().getDefinition()\n\n if (def && patternTransform.hasValue()) {\n def.getAttribute('patternTransform', true).setValue(patternTransform.value)\n }\n }\n\n if (def) {\n return def.createPattern(this.document.ctx, element, opacity)\n }\n }\n\n return null\n }\n\n getTextBaseline() {\n if (!this.hasValue()) {\n return null\n }\n\n const key = this.getString()\n\n return Property.textBaselineMapping[key] || null\n }\n\n addOpacity(opacity: Property) {\n let value = this.getColor()\n const len = value.length\n let commas = 0\n\n // Simulate old RGBColor version, which can't parse rgba.\n for (let i = 0; i < len; i++) {\n if (value[i] === ',') {\n commas++\n }\n\n if (commas === 3) {\n break\n }\n }\n\n if (opacity.hasValue() && this.isString() && commas !== 3) {\n const color = new RGBColor(value)\n\n if (color.ok) {\n color.alpha = opacity.getNumber()\n value = color.toRGBA()\n }\n }\n\n return new Property<string>(this.document, this.name, value)\n }\n}\n","\nexport interface IViewPortSize {\n width: number\n height: number\n}\n\nexport type Axis = 'x' | 'y'\n\nexport class ViewPort {\n static DEFAULT_VIEWPORT_WIDTH = 800\n static DEFAULT_VIEWPORT_HEIGHT = 600\n\n viewPorts: IViewPortSize[] = []\n\n clear() {\n this.viewPorts = []\n }\n\n setCurrent(width: number, height: number) {\n this.viewPorts.push({\n width,\n height\n })\n }\n\n removeCurrent() {\n this.viewPorts.pop()\n }\n\n getRoot() {\n const [root] = this.viewPorts\n\n if (!root) {\n return getDefault()\n }\n\n return root\n }\n\n getCurrent() {\n const { viewPorts } = this\n const current = viewPorts[viewPorts.length - 1]\n\n if (!current) {\n return getDefault()\n }\n\n return current\n }\n\n get width() {\n return this.getCurrent().width\n }\n\n get height() {\n return this.getCurrent().height\n }\n\n computeSize(d?: number|Axis) {\n if (typeof d === 'number') {\n return d\n }\n\n if (d === 'x') {\n return this.width\n }\n\n if (d === 'y') {\n return this.height\n }\n\n return Math.sqrt(\n Math.pow(this.width, 2) + Math.pow(this.height, 2)\n ) / Math.sqrt(2)\n }\n}\n\nfunction getDefault() {\n return {\n width: ViewPort.DEFAULT_VIEWPORT_WIDTH,\n height: ViewPort.DEFAULT_VIEWPORT_HEIGHT\n }\n}\n","import { MatrixValue } from './types'\nimport { toNumbers } from './util'\n\nexport class Point {\n static parse(point: string, defaultValue = 0) {\n const [x = defaultValue, y = defaultValue] = toNumbers(point)\n\n return new Point(x, y)\n }\n\n static parseScale(scale: string, defaultValue = 1) {\n const [x = defaultValue, y = x] = toNumbers(scale)\n\n return new Point(x, y)\n }\n\n static parsePath(path: string) {\n const points = toNumbers(path)\n const len = points.length\n const pathPoints: Point[] = []\n\n for (let i = 0; i < len; i += 2) {\n pathPoints.push(new Point(points[i], points[i + 1]))\n }\n\n return pathPoints\n }\n\n constructor(\n public x: number,\n public y: number\n ) {}\n\n angleTo(point: Point) {\n return Math.atan2(point.y - this.y, point.x - this.x)\n }\n\n applyTransform(transform: MatrixValue) {\n const {\n x,\n y\n } = this\n const xp = x * transform[0] + y * transform[2] + transform[4]\n const yp = x * transform[1] + y * transform[3] + transform[5]\n\n this.x = xp\n this.y = yp\n }\n}\n","import { RenderingContext2D } from './types'\nimport { BoundingBox } from './BoundingBox'\nimport { Point } from './Point'\nimport { Screen } from './Screen'\nimport { Element } from './Document'\n\ninterface IEventTarget {\n onClick?(): void\n onMouseMove?(): void\n}\n\nexport interface IEvent {\n type: string\n x: number\n y: number\n run(eventTarget: IEventTarget): void\n}\n\nexport class Mouse {\n private working = false\n private events: IEvent[] = []\n private eventElements: Element[] = []\n\n constructor(\n private readonly screen: Screen\n ) {\n this.onClick = this.onClick.bind(this)\n this.onMouseMove = this.onMouseMove.bind(this)\n }\n\n isWorking() {\n return this.working\n }\n\n start() {\n if (this.working) {\n return\n }\n\n const {\n screen,\n onClick,\n onMouseMove\n } = this\n const canvas = screen.ctx.canvas as HTMLCanvasElement\n\n canvas.onclick = onClick\n canvas.onmousemove = onMouseMove\n this.working = true\n }\n\n stop() {\n if (!this.working) {\n return\n }\n\n const canvas = this.screen.ctx.canvas as HTMLCanvasElement\n\n this.working = false\n canvas.onclick = null\n canvas.onmousemove = null\n }\n\n hasEvents() {\n return this.working && this.events.length > 0\n }\n\n runEvents() {\n if (!this.working) {\n return\n }\n\n const {\n screen: document,\n events,\n eventElements\n } = this\n const { style } = document.ctx.canvas as HTMLCanvasElement\n let element: Element | null | undefined\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (style) {\n style.cursor = ''\n }\n\n events.forEach(({ run }, i) => {\n element = eventElements[i]\n\n while (element) {\n run(element as IEventTarget)\n element = element.parent\n }\n })\n\n // done running, clear\n this.events = []\n this.eventElements = []\n }\n\n checkPath(element: Element, ctx: RenderingContext2D | null) {\n if (!this.working || !ctx) {\n return\n }\n\n const {\n events,\n eventElements\n } = this\n\n events.forEach(({ x, y }, i) => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!eventElements[i] && ctx.isPointInPath && ctx.isPointInPath(x, y)) {\n eventElements[i] = element\n }\n })\n }\n\n checkBoundingBox(element: Element, boundingBox: BoundingBox | null) {\n if (!this.working || !boundingBox) {\n return\n }\n\n const {\n events,\n eventElements\n } = this\n\n events.forEach(({ x, y }, i) => {\n if (!eventElements[i] && boundingBox.isPointInBox(x, y)) {\n eventElements[i] = element\n }\n })\n }\n\n private mapXY(x: number, y: number) {\n const {\n window,\n ctx\n } = this.screen\n const point = new Point(x, y)\n let element = ctx.canvas as HTMLElement | null\n\n while (element) {\n point.x -= element.offsetLeft\n point.y -= element.offsetTop\n element = element.offsetParent as HTMLElement | null\n }\n\n if (window?.scrollX) {\n point.x += window.scrollX\n }\n\n if (window?.scrollY) {\n point.y += window.scrollY\n }\n\n return point\n }\n\n private onClick(event: MouseEvent) {\n const {\n x,\n y\n } = this.mapXY(\n event.clientX,\n event.clientY\n )\n\n this.events.push({\n type: 'onclick',\n x,\n y,\n run(eventTarget) {\n if (eventTarget.onClick) {\n eventTarget.onClick()\n }\n }\n })\n }\n\n private onMouseMove(event: MouseEvent) {\n const {\n x,\n y\n } = this.mapXY(\n event.clientX,\n event.clientY\n )\n\n this.events.push({\n type: 'onmousemove',\n x,\n y,\n run(eventTarget) {\n if (eventTarget.onMouseMove) {\n eventTarget.onMouseMove()\n }\n }\n })\n }\n}\n","import requestAnimationFrame from 'raf'\nimport {\n RenderingContext2D,\n Fetch\n} from './types'\nimport {\n compressSpaces,\n toNumbers\n} from './util'\nimport { Property } from './Property'\nimport { ViewPort } from './ViewPort'\nimport { Mouse } from './Mouse'\nimport {\n Document,\n Element,\n AnimateElement\n} from './Document'\n\nexport interface IScreenOptions {\n /**\n * Window object.\n */\n window?: Window | null\n /**\n * WHATWG-compatible `fetch` function.\n */\n fetch?: Fetch\n}\n\nexport interface IScreenStartOptions {\n /**\n * Whether enable the redraw.\n */\n enableRedraw?: boolean\n /**\n * Ignore mouse events.\n */\n ignoreMouse?: boolean\n /**\n * Ignore animations.\n */\n ignoreAnimation?: boolean\n /**\n * Does not try to resize canvas.\n */\n ignoreDimensions?: boolean\n /**\n * Does not clear canvas.\n */\n ignoreClear?: boolean\n /**\n * Scales horizontally to width.\n */\n scaleWidth?: number\n /**\n * Scales vertically to height.\n */\n scaleHeight?: number\n /**\n * Draws at a x offset.\n */\n offsetX?: number\n /**\n * Draws at a y offset.\n */\n offsetY?: number\n /**\n * Will call the function on every frame, if it returns true, will redraw.\n */\n forceRedraw?(): boolean\n}\n\nexport interface IScreenViewBoxConfig {\n document: Document\n ctx: RenderingContext2D\n aspectRatio: string\n width: number\n desiredWidth: number\n height: number\n desiredHeight: number\n minX?: number\n minY?: number\n refX?: number\n refY?: number\n clip?: boolean\n clipX?: number\n clipY?: number\n}\n\nconst defaultWindow = typeof window !== 'undefined'\n ? window\n : null\nconst defaultFetch = typeof fetch !== 'undefined'\n ? fetch.bind(undefined) // `fetch` depends on context: `someObject.fetch(...)` will throw error.\n : undefined\n\nexport class Screen {\n static readonly defaultWindow = defaultWindow\n static readonly defaultFetch = defaultFetch\n static FRAMERATE = 30\n static MAX_VIRTUAL_PIXELS = 30000\n\n readonly window: Window | null\n readonly fetch: Fetch\n readonly viewPort = new ViewPort()\n readonly mouse = new Mouse(this)\n readonly animations: AnimateElement[] = []\n private readyPromise: Promise<void> | undefined\n private resolveReady: (() => void) | undefined\n private waits: (() => boolean)[] = []\n private frameDuration = 0\n private isReadyLock = false\n private isFirstRender = true\n private intervalId: number | null = null\n\n constructor(\n readonly ctx: RenderingContext2D,\n {\n fetch = defaultFetch,\n window = defaultWindow\n }: IScreenOptions = {}\n ) {\n this.window = window\n\n if (!fetch) {\n throw new Error(`Can't find 'fetch' in 'globalThis', please provide it via options`)\n }\n\n this.fetch = fetch\n }\n\n wait(checker: () => boolean) {\n this.waits.push(checker)\n }\n\n ready() {\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n if (!this.readyPromise) {\n return Promise.resolve()\n }\n\n return this.readyPromise\n }\n\n isReady() {\n if (this.isReadyLock) {\n return true\n }\n\n const isReadyLock = this.waits.every(_ => _())\n\n if (isReadyLock) {\n this.waits = []\n\n if (this.resolveReady) {\n this.resolveReady()\n }\n }\n\n this.isReadyLock = isReadyLock\n\n return isReadyLock\n }\n\n setDefaults(ctx: RenderingContext2D) {\n // initial values and defaults\n ctx.strokeStyle = 'rgba(0,0,0,0)'\n ctx.lineCap = 'butt'\n ctx.lineJoin = 'miter'\n ctx.miterLimit = 4\n }\n\n setViewBox({\n document,\n ctx,\n aspectRatio,\n width,\n desiredWidth,\n height,\n desiredHeight,\n minX = 0,\n minY = 0,\n refX,\n refY,\n clip = false,\n clipX = 0,\n clipY = 0\n }: IScreenViewBoxConfig) {\n // aspect ratio - http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute\n const cleanAspectRatio = compressSpaces(aspectRatio).replace(/^defer\\s/, '') // ignore defer\n const [aspectRatioAlign, aspectRatioMeetOrSlice] = cleanAspectRatio.split(' ')\n const align = aspectRatioAlign || 'xMidYMid'\n const meetOrSlice = aspectRatioMeetOrSlice || 'meet'\n // calculate scale\n const scaleX = width / desiredWidth\n const scaleY = height / desiredHeight\n const scaleMin = Math.min(scaleX, scaleY)\n const scaleMax = Math.max(scaleX, scaleY)\n let finalDesiredWidth = desiredWidth\n let finalDesiredHeight = desiredHeight\n\n if (meetOrSlice === 'meet') {\n finalDesiredWidth *= scaleMin\n finalDesiredHeight *= scaleMin\n }\n\n if (meetOrSlice === 'slice') {\n finalDesiredWidth *= scaleMax\n finalDesiredHeight *= scaleMax\n }\n\n const refXProp = new Property(document, 'refX', refX)\n const refYProp = new Property(document, 'refY', refY)\n const hasRefs = refXProp.hasValue() && refYProp.hasValue()\n\n if (hasRefs) {\n ctx.translate(\n -scaleMin * refXProp.getPixels('x'),\n -scaleMin * refYProp.getPixels('y')\n )\n }\n\n if (clip) {\n const scaledClipX = scaleMin * clipX\n const scaledClipY = scaleMin * clipY\n\n ctx.beginPath()\n ctx.moveTo(scaledClipX, scaledClipY)\n ctx.lineTo(width, scaledClipY)\n ctx.lineTo(width, height)\n ctx.lineTo(scaledClipX, height)\n ctx.closePath()\n ctx.clip()\n }\n\n if (!hasRefs) {\n const isMeetMinY = meetOrSlice === 'meet' && scaleMin === scaleY\n const isSliceMaxY = meetOrSlice === 'slice' && scaleMax === scaleY\n const isMeetMinX = meetOrSlice === 'meet' && scaleMin === scaleX\n const isSliceMaxX = meetOrSlice === 'slice' && scaleMax === scaleX\n\n if (align.startsWith('xMid') && (\n isMeetMinY || isSliceMaxY\n )) {\n ctx.translate(width / 2.0 - finalDesiredWidth / 2.0, 0)\n }\n\n if (align.endsWith('YMid') && (\n isMeetMinX || isSliceMaxX\n )) {\n ctx.translate(0, height / 2.0 - finalDesiredHeight / 2.0)\n }\n\n if (align.startsWith('xMax') && (\n isMeetMinY || isSliceMaxY\n )) {\n ctx.translate(width - finalDesiredWidth, 0)\n }\n\n if (align.endsWith('YMax') && (\n isMeetMinX || isSliceMaxX\n )) {\n ctx.translate(0, height - finalDesiredHeight)\n }\n }\n\n // scale\n switch (true) {\n case align === 'none':\n ctx.scale(scaleX, scaleY)\n break\n\n case meetOrSlice === 'meet':\n ctx.scale(scaleMin, scaleMin)\n break\n\n case meetOrSlice === 'slice':\n ctx.scale(scaleMax, scaleMax)\n break\n }\n\n // translate\n ctx.translate(-minX, -minY)\n }\n\n start(\n element: Element,\n {\n enableRedraw = false,\n ignoreMouse = false,\n ignoreAnimation = false,\n ignoreDimensions = false,\n ignoreClear = false,\n forceRedraw,\n scaleWidth,\n scaleHeight,\n offsetX,\n offsetY\n }: IScreenStartOptions = {}\n ) {\n const { mouse } = this\n const frameDuration = 1000 / Screen.FRAMERATE\n\n this.isReadyLock = false\n this.frameDuration = frameDuration\n this.readyPromise = new Promise((resolve) => {\n this.resolveReady = resolve\n })\n\n if (this.isReady()) {\n this.render(\n element,\n ignoreDimensions,\n ignoreClear,\n scaleWidth,\n scaleHeight,\n offsetX,\n offsetY\n )\n }\n\n if (!enableRedraw) {\n return\n }\n\n let now = Date.now()\n let then = now\n let delta = 0\n const tick = () => {\n now = Date.now()\n delta = now - then\n\n if (delta >= frameDuration) {\n then = now - (delta % frameDuration)\n\n if (this.shouldUpdate(\n ignoreAnimation,\n forceRedraw\n )) {\n this.render(\n element,\n ignoreDimensions,\n ignoreClear,\n scaleWidth,\n scaleHeight,\n offsetX,\n offsetY\n )\n mouse.runEvents()\n }\n }\n\n this.intervalId = requestAnimationFrame(tick)\n }\n\n if (!ignoreMouse) {\n mouse.start()\n }\n\n this.intervalId = requestAnimationFrame(tick)\n }\n\n stop() {\n if (this.intervalId) {\n requestAnimationFrame.cancel(this.intervalId)\n this.intervalId = null\n }\n\n this.mouse.stop()\n }\n\n private shouldUpdate(\n ignoreAnimation: boolean,\n forceRedraw: (() => boolean) | undefined\n ) {\n // need update from animations?\n if (!ignoreAnimation) {\n const { frameDuration } = this\n const shouldUpdate = this.animations.reduce(\n (shouldUpdate, animation) => animation.update(frameDuration) || shouldUpdate,\n false\n )\n\n if (shouldUpdate) {\n return true\n }\n }\n\n // need update from redraw?\n if (typeof forceRedraw === 'function' && forceRedraw()) {\n return true\n }\n\n if (!this.isReadyLock && this.isReady()) {\n return true\n }\n\n // need update from mouse events?\n if (this.mouse.hasEvents()) {\n return true\n }\n\n return false\n }\n\n private render(\n element: Element,\n ignoreDimensions: boolean,\n ignoreClear: boolean,\n scaleWidth: number | undefined,\n scaleHeight: number | undefined,\n offsetX: number | undefined,\n offsetY: number | undefined\n ) {\n const {\n viewPort,\n ctx,\n isFirstRender\n } = this\n const canvas = ctx.canvas as HTMLCanvasElement\n\n viewPort.clear()\n\n if (canvas.width && canvas.height) {\n viewPort.setCurrent(canvas.width, canvas.height)\n }\n\n const widthStyle = element.getStyle('width')\n const heightStyle = element.getStyle('height')\n\n if (!ignoreDimensions && (\n isFirstRender\n || typeof scaleWidth !== 'number' && typeof scaleHeight !== 'number'\n )) {\n // set canvas size\n if (widthStyle.hasValue()) {\n canvas.width = widthStyle.getPixels('x')\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (canvas.style) {\n canvas.style.width = `${canvas.width}px`\n }\n }\n\n if (heightStyle.hasValue()) {\n canvas.height = heightStyle.getPixels('y')\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (canvas.style) {\n canvas.style.height = `${canvas.height}px`\n }\n }\n }\n\n let cWidth = canvas.clientWidth || canvas.width\n let cHeight = canvas.clientHeight || canvas.height\n\n if (ignoreDimensions && widthStyle.hasValue() && heightStyle.hasValue()) {\n cWidth = widthStyle.getPixels('x')\n cHeight = heightStyle.getPixels('y')\n }\n\n viewPort.setCurrent(cWidth, cHeight)\n\n if (typeof offsetX === 'number') {\n element.getAttribute('x', true).setValue(offsetX)\n }\n\n if (typeof offsetY === 'number') {\n element.getAttribute('y', true).setValue(offsetY)\n }\n\n if (typeof scaleWidth === 'number'\n || typeof scaleHeight === 'number'\n ) {\n const viewBox = toNumbers(element.getAttribute('viewBox').getString())\n let xRatio = 0\n let yRatio = 0\n\n if (typeof scaleWidth === 'number') {\n const widthStyle = element.getStyle('width')\n\n if (widthStyle.hasValue()) {\n xRatio = widthStyle.getPixels('x') / scaleWidth\n } else\n if (viewBox[2] && !isNaN(viewBox[2])) {\n xRatio = viewBox[2] / scaleWidth\n }\n }\n\n if (typeof scaleHeight === 'number') {\n const heightStyle = element.getStyle('height')\n\n if (heightStyle.hasValue()) {\n yRatio = heightStyle.getPixels('y') / scaleHeight\n } else\n if (viewBox[3] && !isNaN(viewBox[3])) {\n yRatio = viewBox[3] / scaleHeight\n }\n }\n\n if (!xRatio) {\n xRatio = yRatio\n }\n\n if (!yRatio) {\n yRatio = xRatio\n }\n\n element.getAttribute('width', true).setValue(scaleWidth)\n element.getAttribute('height', true).setValue(scaleHeight)\n\n const transformStyle = element.getStyle('transform', true, true)\n\n transformStyle.setValue(`${transformStyle.getString()} scale(${1.0 / xRatio}, ${1.0 / yRatio})`)\n }\n\n // clear and render\n if (!ignoreClear) {\n ctx.clearRect(0, 0, cWidth, cHeight)\n }\n\n element.render(ctx)\n\n if (isFirstRender) {\n this.isFirstRender = false\n }\n }\n}\n","import { Fetch } from './types'\nimport { Screen } from './Screen'\n\ntype DOMParserConstructor = typeof DOMParser\n\nexport interface IParserOptions {\n /**\n * WHATWG-compatible `fetch` function.\n */\n fetch?: Fetch\n /**\n * XML/HTML parser from string into DOM Document.\n */\n DOMParser?: DOMParserConstructor\n}\n\nconst { defaultFetch } = Screen\nconst DefaultDOMParser = typeof DOMParser !== 'undefined'\n ? DOMParser\n : undefined\n\nexport class Parser {\n private readonly fetch: Fetch\n private readonly DOMParser: DOMParserConstructor\n\n constructor({\n fetch = defaultFetch,\n DOMParser = DefaultDOMParser\n }: IParserOptions = {}) {\n if (!fetch) {\n throw new Error(`Can't find 'fetch' in 'globalThis', please provide it via options`)\n }\n\n if (!DOMParser) {\n throw new Error(`Can't find 'DOMParser' in 'globalThis', please provide it via options`)\n }\n\n this.fetch = fetch\n this.DOMParser = DOMParser\n }\n\n async parse(resource: string) {\n if (resource.startsWith('<')) {\n return this.parseFromString(resource)\n }\n\n return this.load(resource)\n }\n\n parseFromString(xml: string) {\n const parser = new this.DOMParser()\n\n try {\n return this.checkDocument(\n parser.parseFromString(xml, 'image/svg+xml')\n )\n } catch (err) {\n return this.checkDocument(\n parser.parseFromString(xml, 'text/xml')\n )\n }\n }\n\n private checkDocument(document: Document) {\n const parserError = document.getElementsByTagName('parsererror')[0]\n\n if (parserError) {\n throw new Error(parserError.textContent || 'Unknown parse error')\n }\n\n return document\n }\n\n async load(url: string) {\n const response = await this.fetch(url)\n const xml = await response.text()\n\n return this.parseFromString(xml)\n }\n}\n","import { RenderingContext2D } from '../types'\nimport { Document } from '../Document'\nimport { Point } from '../Point'\n\nexport class Translate {\n type = 'translate'\n private readonly point: Point\n\n constructor(\n _: Document,\n point: string\n ) {\n this.point = Point.parse(point)\n }\n\n apply(ctx: RenderingContext2D) {\n const {\n x,\n y\n } = this.point\n\n ctx.translate(\n x || 0.0,\n y || 0.0\n )\n }\n\n unapply(ctx: RenderingContext2D) {\n const {\n x,\n y\n } = this.point\n\n ctx.translate(\n -1.0 * x || 0.0,\n -1.0 * y || 0.0\n )\n }\n\n applyToPoint(point: Point) {\n const {\n x,\n y\n } = this.point\n\n point.applyTransform([\n 1,\n 0,\n 0,\n 1,\n x || 0.0,\n y || 0.0\n ])\n }\n}\n","import { RenderingContext2D } from '../types'\nimport { toNumbers } from '../util'\nimport { Document } from '../Document'\nimport { Property } from '../Property'\nimport { Point } from '../Point'\n\nexport class Rotate {\n type = 'rotate'\n private readonly angle: Property\n private readonly originX: Property\n private readonly originY: Property\n private readonly cx: number\n private readonly cy: number\n\n constructor(\n document: Document,\n rotate: string,\n transformOrigin: readonly [Property<string>, Property<string>]\n ) {\n const numbers = toNumbers(rotate)\n\n this.angle = new Property(document, 'angle', numbers[0])\n this.originX = transformOrigin[0]\n this.originY = transformOrigin[1]\n this.cx = numbers[1] || 0\n this.cy = numbers[2] || 0\n }\n\n apply(ctx: RenderingContext2D) {\n const {\n cx,\n cy,\n originX,\n originY,\n angle\n } = this\n const tx = cx + originX.getPixels('x')\n const ty = cy + originY.getPixels('y')\n\n ctx.translate(tx, ty)\n ctx.rotate(angle.getRadians())\n ctx.translate(-tx, -ty)\n }\n\n unapply(ctx: RenderingContext2D) {\n const {\n cx,\n cy,\n originX,\n originY,\n angle\n } = this\n const tx = cx + originX.getPixels('x')\n const ty = cy + originY.getPixels('y')\n\n ctx.translate(tx, ty)\n ctx.rotate(-1.0 * angle.getRadians())\n ctx.translate(-tx, -ty)\n }\n\n applyToPoint(point: Point) {\n const {\n cx,\n cy,\n angle\n } = this\n const rad = angle.getRadians()\n\n point.applyTransform([\n 1,\n 0,\n 0,\n 1,\n cx || 0.0, // this.p.x\n cy || 0.0 // this.p.y\n ])\n point.applyTransform([\n Math.cos(rad),\n Math.sin(rad),\n -Math.sin(rad),\n Math.cos(rad),\n 0,\n 0\n ])\n point.applyTransform([\n 1,\n 0,\n 0,\n 1,\n -cx || 0.0, // -this.p.x\n -cy || 0.0 // -this.p.y\n ])\n }\n}\n","import { RenderingContext2D } from '../types'\nimport { PSEUDO_ZERO } from '../util'\nimport { Document } from '../Document'\nimport { Point } from '../Point'\nimport { Property } from '../Property'\n\nexport class Scale {\n type = 'scale'\n private readonly scale: Point\n private readonly originX: Property\n private readonly originY: Property\n\n constructor(\n _: Document,\n scale: string,\n transformOrigin: readonly [Property<string>, Property<string>]\n ) {\n const scaleSize = Point.parseScale(scale)\n\n // Workaround for node-canvas\n if (scaleSize.x === 0\n || scaleSize.y === 0\n ) {\n scaleSize.x = PSEUDO_ZERO\n scaleSize.y = PSEUDO_ZERO\n }\n\n this.scale = scaleSize\n this.originX = transformOrigin[0]\n this.originY = transformOrigin[1]\n }\n\n apply(ctx: RenderingContext2D) {\n const {\n scale: {\n x,\n y\n },\n originX,\n originY\n } = this\n const tx = originX.getPixels('x')\n const ty = originY.getPixels('y')\n\n ctx.translate(tx, ty)\n ctx.scale(x, y || x)\n ctx.translate(-tx, -ty)\n }\n\n unapply(ctx: RenderingContext2D) {\n const {\n scale: {\n x,\n y\n },\n originX,\n originY\n } = this\n const tx = originX.getPixels('x')\n const ty = originY.getPixels('y')\n\n ctx.translate(tx, ty)\n ctx.scale(1.0 / x, 1.0 / y || x)\n ctx.translate(-tx, -ty)\n }\n\n applyToPoint(point: Point) {\n const {\n x,\n y\n } = this.scale\n\n point.applyTransform([\n x || 0.0,\n 0,\n 0,\n y || 0.0,\n 0,\n 0\n ])\n }\n}\n","import { MatrixValue, RenderingContext2D } from '../types'\nimport { toMatrixValue } from '../util'\nimport { Document } from '../Document'\nimport { Point } from '../Point'\nimport { Property } from '../Property'\nimport { ITransform } from './types'\n\nexport class Matrix implements ITransform {\n type = 'matrix'\n protected matrix: MatrixValue\n private readonly originX: Property\n private readonly originY: Property\n\n constructor(\n _: Document,\n matrix: string,\n transformOrigin: readonly [Property<string>, Property<string>]\n ) {\n this.matrix = toMatrixValue(matrix)\n this.originX = transformOrigin[0]\n this.originY = transformOrigin[1]\n }\n\n apply(ctx: RenderingContext2D) {\n const {\n originX,\n originY,\n matrix\n } = this\n const tx = originX.getPixels('x')\n const ty = originY.getPixels('y')\n\n ctx.translate(tx, ty)\n ctx.transform(\n matrix[0],\n matrix[1],\n matrix[2],\n matrix[3],\n matrix[4],\n matrix[5]\n )\n ctx.translate(-tx, -ty)\n }\n\n unapply(ctx: RenderingContext2D) {\n const {\n originX,\n originY,\n matrix\n } = this\n const a = matrix[0]\n const b = matrix[2]\n const c = matrix[4]\n const d = matrix[1]\n const e = matrix[3]\n const f = matrix[5]\n const g = 0.0\n const h = 0.0\n const i = 1.0\n const det = 1 / (a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g))\n const tx = originX.getPixels('x')\n const ty = originY.getPixels('y')\n\n ctx.translate(tx, ty)\n ctx.transform(\n det * (e * i - f * h),\n det * (f * g - d * i),\n det * (c * h - b * i),\n det * (a * i - c * g),\n det * (b * f - c * e),\n det * (c * d - a * f)\n )\n ctx.translate(-tx, -ty)\n }\n\n applyToPoint(point: Point) {\n point.applyTransform(this.matrix)\n }\n}\n","import { Document } from '../Document'\nimport { Property } from '../Property'\nimport { Matrix } from './Matrix'\n\nexport class Skew extends Matrix {\n override type = 'skew'\n protected readonly angle: Property\n\n constructor(\n document: Document,\n skew: string,\n transformOrigin: readonly [Property<string>, Property<string>]\n ) {\n super(document, skew, transformOrigin)\n\n this.angle = new Property(document, 'angle', skew)\n }\n}\n","import { Document } from '../Document'\nimport { Property } from '../Property'\nimport { Skew } from './Skew'\n\nexport class SkewX extends Skew {\n override type = 'skewX'\n\n constructor(\n document: Document,\n skew: string,\n transformOrigin: readonly [Property<string>, Property<string>]\n ) {\n super(document, skew, transformOrigin)\n\n this.matrix = [\n 1,\n 0,\n Math.tan(this.angle.getRadians()),\n 1,\n 0,\n 0\n ]\n }\n}\n","import { Document } from '../Document'\nimport { Property } from '../Property'\nimport { Skew } from './Skew'\n\nexport class SkewY extends Skew {\n override type = 'skewY'\n\n constructor(\n document: Document,\n skew: string,\n transformOrigin: readonly [Property<string>, Property<string>]\n ) {\n super(document, skew, transformOrigin)\n\n this.matrix = [\n 1,\n Math.tan(this.angle.getRadians()),\n 0,\n 1,\n 0,\n 0\n ]\n }\n}\n","import { RenderingContext2D } from '../types'\nimport { compressSpaces } from '../util'\nimport { Property } from '../Property'\nimport { Point } from '../Point'\nimport { Document, Element } from '../Document'\nimport { ITransform } from './types'\nimport { Translate } from './Translate'\nimport { Rotate } from './Rotate'\nimport { Scale } from './Scale'\nimport { Matrix } from './Matrix'\nimport { SkewX } from './SkewX'\nimport { SkewY } from './SkewY'\n\nfunction parseTransforms(transform: string) {\n return compressSpaces(transform)\n .trim()\n .replace(/\\)([a-zA-Z])/g, ') $1')\n .replace(/\\)(\\s?,\\s?)/g, ') ')\n .split(/\\s(?=[a-z])/)\n}\n\nfunction parseTransform(transform: string) {\n const [type = '', value = ''] = transform.split('(')\n\n return [type.trim(), value.trim().replace(')', '')] as const\n}\n\ninterface ITransformConstructor {\n prototype: ITransform\n new (\n document: Document,\n value: string,\n transformOrigin: readonly [Property<string>, Property<string>]\n ): ITransform\n}\n\nexport class Transform {\n static fromElement(document: Document, element: Element) {\n const transformStyle = element.getStyle('transform', false, true)\n\n if (transformStyle.hasValue()) {\n const [transformOriginXProperty, transformOriginYProperty = transformOriginXProperty] = element.getStyle('transform-origin', false, true).split()\n\n if (transformOriginXProperty && transformOriginYProperty) {\n const transformOrigin = [transformOriginXProperty, transformOriginYProperty] as const\n\n return new Transform(\n document,\n transformStyle.getString(),\n transformOrigin\n )\n }\n }\n\n return null\n }\n\n static transformTypes: Record<string, ITransformConstructor> = {\n translate: Translate,\n rotate: Rotate,\n scale: Scale,\n matrix: Matrix,\n skewX: SkewX,\n skewY: SkewY\n }\n\n private readonly transforms: ITransform[] = []\n\n constructor(\n private readonly document: Document,\n transform: string,\n transformOrigin: readonly [Property<string>, Property<string>]\n ) {\n const data = parseTransforms(transform)\n\n data.forEach((transform) => {\n if (transform === '