import * as PropertySymbol from '../PropertySymbol.js'; import BrowserWindow from '../window/BrowserWindow.js'; import DOMMatrix from '../dom/dom-matrix/DOMMatrix.js'; const TRANSFORM_REGEXP = /([a-zA-Z0-9]+)\(([^)]+)\)/; const TRANSFORM_PARAMETER_SPLIT_REGEXP = /[\s,]+/; /** * SVG Matrix. * * Documentation missing at developer.mozilla.org. */ export default class SVGMatrix { // Internal properties public [PropertySymbol.window]: BrowserWindow; public [PropertySymbol.getAttribute]: () => string | null = null; public [PropertySymbol.setAttribute]: (value: string) => void | null = null; public [PropertySymbol.attributeValue]: string | null = null; public [PropertySymbol.readOnly]: boolean = false; /** * Constructor. * * @param illegalConstructorSymbol Illegal constructor symbol. * @param window Window. * @param [options] Options. * @param [options.readOnly] Read only. * @param [options.getAttribute] Get attribute. * @param [options.setAttribute] Set attribute. */ constructor( illegalConstructorSymbol: symbol, window: BrowserWindow, options?: { readOnly?: boolean; getAttribute?: () => string | null; setAttribute?: (value: string) => void; } ) { if (illegalConstructorSymbol !== PropertySymbol.illegalConstructor) { throw new TypeError('Illegal constructor'); } this[PropertySymbol.window] = window; if (options) { this[PropertySymbol.readOnly] = !!options.readOnly; this[PropertySymbol.getAttribute] = options.getAttribute || null; this[PropertySymbol.setAttribute] = options.setAttribute || null; } } /** * Returns the `a` value of the matrix. */ public get a(): number { return this[PropertySymbol.getDOMMatrix]().a; } /** * Sets the `a` value of the matrix. */ public set a(value: number) { if (this[PropertySymbol.readOnly]) { return; } const domMatrix = this[PropertySymbol.getDOMMatrix](); domMatrix.a = value; this[PropertySymbol.setDOMMatrix](domMatrix); } /** * Returns the `b` value of the matrix. */ public get b(): number { return this[PropertySymbol.getDOMMatrix]().b; } /** * Sets the `b` value of the matrix. */ public set b(value: number) { if (this[PropertySymbol.readOnly]) { return; } const domMatrix = this[PropertySymbol.getDOMMatrix](); domMatrix.b = value; this[PropertySymbol.setDOMMatrix](domMatrix); } /** * Returns the `c` value of the matrix. */ public get c(): number { return this[PropertySymbol.getDOMMatrix]().c; } /** * Sets the `c` value of the matrix. */ public set c(value: number) { if (this[PropertySymbol.readOnly]) { return; } const domMatrix = this[PropertySymbol.getDOMMatrix](); domMatrix.c = value; this[PropertySymbol.setDOMMatrix](domMatrix); } /** * Returns the `d` value of the matrix. */ public get d(): number { return this[PropertySymbol.getDOMMatrix]().d; } /** * Sets the `d` value of the matrix. */ public set d(value: number) { if (this[PropertySymbol.readOnly]) { return; } const domMatrix = this[PropertySymbol.getDOMMatrix](); domMatrix.d = value; this[PropertySymbol.setDOMMatrix](domMatrix); } /** * Returns the `e` value of the matrix. */ public get e(): number { return this[PropertySymbol.getDOMMatrix]().e; } /** * Sets the `e` value of the matrix. */ public set e(value: number) { if (this[PropertySymbol.readOnly]) { return; } const domMatrix = this[PropertySymbol.getDOMMatrix](); domMatrix.e = value; this[PropertySymbol.setDOMMatrix](domMatrix); } /** * Returns the `f` value of the matrix. */ public get f(): number { return this[PropertySymbol.getDOMMatrix]().f; } /** * Sets the `f` value of the matrix. */ public set f(value: number) { if (this[PropertySymbol.readOnly]) { return; } const domMatrix = this[PropertySymbol.getDOMMatrix](); domMatrix.f = value; this[PropertySymbol.setDOMMatrix](domMatrix); } /** * Returns a new SVGMatrix instance which is the result of this matrix multiplied by the passed matrix. * * @param secondMatrix Matrix object. * @returns A new SVGMatrix object. */ public multiply(secondMatrix: SVGMatrix): SVGMatrix { if (!(secondMatrix instanceof SVGMatrix)) { throw new this[PropertySymbol.window].TypeError( "Failed to execute 'multiply' on 'SVGMatrix': parameter 1 is not of type 'SVGMatrix'." ); } const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.multiplySelf(secondMatrix[PropertySymbol.getDOMMatrix]()); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance which is this matrix post multiplied by a translation matrix containing the passed values. * * @param [x=0] X component of the translation value. * @param [y=0] Y component of the translation value. * @returns The resulted matrix */ public translate(x: number = 0, y: number = 0): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.translateSelf(x, y); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance which is this matrix post multiplied by a scale 2D matrix containing the passed values. * * @param scale The scale factor. * @returns The resulted matrix */ public scale(scale: number): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.scaleSelf(scale); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance which is this matrix post multiplied by a scale 3D matrix containing the passed values. * * @param [scaleX] X-Axis scale. * @param [scaleY] Y-Axis scale. * @returns The resulted matrix */ public scaleNonUniform(scaleX = 1, scaleY = 1): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.scaleNonUniformSelf(scaleX, scaleY); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance which is this matrix post multiplied by each of 3 rotation matrices about the major axes, first X, then Y, then Z. * * @param angle Angle of rotation in degrees. * @returns The resulted matrix */ public rotate(angle: number): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.rotateSelf(angle); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance which is this matrix post multiplied by a skew matrix along the X axis by the given angle. * * Not implemented in Happy DOM yet. * * @param [x] X-Axis skew. * @param [y] Y-Axis skew. */ public rotateFromVector(x: number = 0, y: number = 0): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.rotateFromVectorSelf(x, y); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance that specifies a skew transformation along X-Axis by the given angle. * * @param angle Angle amount in degrees to skew. * @returns The resulted matrix */ public skewX(angle: number): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.skewXSelf(angle); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance that specifies a skew transformation along Y-Axis by the given angle. * * @param angle Angle amount in degrees to skew. * @returns The resulted matrix */ public skewY(angle: number): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.skewYSelf(angle); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance which is this matrix flipped on X-axis. */ public flipX(): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.flipXSelf(); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance which is this matrix flipped on Y-axis. */ public flipY(): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.flipYSelf(); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns a new SVGMatrix instance which is this matrix inverted. */ public inverse(): SVGMatrix { const domMatrix = this[PropertySymbol.getDOMMatrix](); const svgMatrix = new SVGMatrix(PropertySymbol.illegalConstructor, this[PropertySymbol.window]); domMatrix.invertSelf(); svgMatrix[PropertySymbol.setDOMMatrix](domMatrix); return svgMatrix; } /** * Returns DOM matrix. * * @returns DOM matrix. */ public [PropertySymbol.getDOMMatrix](): DOMMatrix { const attribute = this[PropertySymbol.getAttribute] ? this[PropertySymbol.getAttribute]() : this[PropertySymbol.attributeValue]; if (!attribute) { return new DOMMatrix(); } const match = attribute.match(TRANSFORM_REGEXP); if (!match) { return new DOMMatrix(); } const parameters: number[] = []; for (const parameter of match[2].trim().split(TRANSFORM_PARAMETER_SPLIT_REGEXP)) { const value = Number(parameter); if (isNaN(value)) { throw new this[PropertySymbol.window].TypeError( `Failed to parse transform attribute: Expected number, but got "${parameter}" in "${attribute}".` ); } parameters.push(value); } switch (match[1]) { case 'matrix': if (parameters.length !== 6) { throw new this[PropertySymbol.window].TypeError( `Failed to parse transform attribute: Expected 6 parameters in "${attribute}".` ); } return DOMMatrix[PropertySymbol.fromString](attribute); case 'scale': case 'translate': if (parameters.length !== 1 && parameters.length !== 2) { throw new this[PropertySymbol.window].TypeError( `Failed to parse transform attribute: Expected 1 or 2 parameters in "${attribute}".` ); } return DOMMatrix[PropertySymbol.fromString](attribute); case 'skewY': case 'skewX': if (parameters.length !== 1) { throw new this[PropertySymbol.window].TypeError( `Failed to parse transform attribute: Expected 1 parameter in "${attribute}".` ); } return DOMMatrix[PropertySymbol.fromString](attribute); case 'rotate': const domMatrix = new DOMMatrix(); if (parameters.length !== 1 && parameters.length !== 3) { throw new this[PropertySymbol.window].TypeError( `Failed to parse transform attribute: Expected 1 or 3 parameters in "${attribute}".` ); } const [angle, x, y] = parameters; if (x || y) { domMatrix.translateSelf(x, y); } const radian = (angle * Math.PI) / 180; /** * @see https://www.w3.org/TR/SVG11/coords.html#TransformAttribute **/ domMatrix.multiplySelf( // prettier-ignore new DOMMatrix([ Math.cos(radian), Math.sin(radian), -Math.sin(radian), Math.cos(radian), 0, 0 ]) ); if (x || y) { domMatrix.translateSelf(-x, -y); } return domMatrix; default: throw new this[PropertySymbol.window].TypeError( `Failed to parse transform attribute: Unknown transformation "${attribute}".` ); } } /** * Sets DOM matrix. * * @param domMatrix DOM matrix. */ public [PropertySymbol.setDOMMatrix](domMatrix: DOMMatrix): void { this[PropertySymbol.attributeValue] = domMatrix.toString().replace(/, /g, ' '); if (this[PropertySymbol.setAttribute]) { this[PropertySymbol.setAttribute](this[PropertySymbol.attributeValue]); } } }