diff --git a/CHANGELOG.md b/CHANGELOG.md index e490b50d353..d64b300eea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [next] +## [6.0.0-beta4] + +- chore(): Code cleanup and reuse of code in svg-parsing code [#8881](https://github.com/fabricjs/fabric.js/pull/8881) - chore(TS): Parse transform attribute typing [#8878](https://github.com/fabricjs/fabric.js/pull/8878) - chore(TS): Fix typing for DOMParser [#8871](https://github.com/fabricjs/fabric.js/pull/8871) - fix(Path, polyline): fix for SVG import [#8879](https://github.com/fabricjs/fabric.js/pull/8879) diff --git a/src/parser/parseTransformAttribute.ts b/src/parser/parseTransformAttribute.ts index a5bba2ceb4d..83607f7dd7d 100644 --- a/src/parser/parseTransformAttribute.ts +++ b/src/parser/parseTransformAttribute.ts @@ -1,12 +1,12 @@ import { iMatrix } from '../constants'; import { reNum } from './constants'; import { multiplyTransformMatrices } from '../util/misc/matrix'; -import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; import { rotateMatrix } from './rotateMatrix'; import { scaleMatrix } from './scaleMatrix'; -import { skewMatrix } from './skewMatrix'; import { translateMatrix } from './translateMatrix'; import { TMat2D } from '../typedefs'; +import { cleanupSvgAttribute } from '../util/internals/cleanupSvAttribute'; +import { skewXMatrix, skewYMatrix } from './skewMatrix'; // == begin transform regexp const p = `(${reNum})`; @@ -34,16 +34,11 @@ const reTransform = new RegExp(transform, 'g'); */ export function parseTransformAttribute(attributeValue: string): TMat2D { // first we clean the string - attributeValue = attributeValue - .replace(new RegExp(`(${reNum})`, 'gi'), ' $1 ') - // replace annoying commas and arbitrary whitespace with single spaces - .replace(/,/gi, ' ') - .replace(/\s+/gi, ' ') + attributeValue = cleanupSvgAttribute(attributeValue) // remove spaces around front parentheses .replace(/\s*([()])\s*/gi, '$1'); // start with identity matrix - let matrix: TMat2D = [...iMatrix]; const matrices: TMat2D[] = []; // return if no argument was given or @@ -52,7 +47,7 @@ export function parseTransformAttribute(attributeValue: string): TMat2D { !attributeValue || (attributeValue && !reTransformList.test(attributeValue)) ) { - return matrix; + return [...iMatrix]; } for (const match of attributeValue.matchAll(reTransform)) { @@ -60,42 +55,40 @@ export function parseTransformAttribute(attributeValue: string): TMat2D { if (!transformMatch) { continue; } + let matrix: TMat2D = iMatrix; const matchedParams = transformMatch.filter((m) => !!m); - const operation = matchedParams[1]; - const args = matchedParams.slice(2).map(parseFloat); + const [, operation, ...rawArgs] = matchedParams; + const [arg0, arg1, arg2, arg3, arg4, arg5] = rawArgs.map((arg) => + parseFloat(arg) + ); switch (operation) { case 'translate': - translateMatrix(matrix, args); + matrix = translateMatrix(arg0, arg1); break; case 'rotate': - args[0] = degreesToRadians(args[0]); - rotateMatrix(matrix, args); + matrix = rotateMatrix(arg0, arg1, arg2); break; case 'scale': - scaleMatrix(matrix, args); + matrix = scaleMatrix(arg0, arg1); break; case 'skewX': - skewMatrix(matrix, args, 2); + matrix = skewXMatrix(arg0); break; case 'skewY': - skewMatrix(matrix, args, 1); + matrix = skewYMatrix(arg0); break; case 'matrix': - matrix = args as TMat2D; + matrix = [arg0, arg1, arg2, arg3, arg4, arg5]; break; } // snapshot current matrix into matrices array - matrices.push([...matrix]); - // reset - matrix = [...iMatrix]; + matrices.push(matrix); } - let combinedMatrix = matrices[0]; - while (matrices.length > 1) { - matrices.shift(); - combinedMatrix = multiplyTransformMatrices(combinedMatrix, matrices[0]); - } - return combinedMatrix; + return matrices.reduce( + (acc, matrix) => multiplyTransformMatrices(acc, matrix), + iMatrix + ); } diff --git a/src/parser/rotateMatrix.ts b/src/parser/rotateMatrix.ts index e4d0facdef8..27f65393c11 100644 --- a/src/parser/rotateMatrix.ts +++ b/src/parser/rotateMatrix.ts @@ -1,6 +1,7 @@ import { cos } from '../util/misc/cos'; import { sin } from '../util/misc/sin'; -import { TMat2D } from '../typedefs'; +import { TDegree, TMat2D } from '../typedefs'; +import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; /** * A rotation matrix @@ -9,25 +10,24 @@ import { TMat2D } from '../typedefs'; * [sin(a) cos(a) -xsin(a)-ycos(a)+y] * [0 0 1 ] */ -type TMatRotate = [a: number] | [a: number, x: number, y: number]; -export function rotateMatrix( - matrix: TMat2D, - args: TMatRotate | number[] -): void { - const cosValue = cos(args[0]), - sinValue = sin(args[0]); - let x = 0, - y = 0; - if (args.length === 3) { - x = args[1]; - y = args[2]; - } - - matrix[0] = cosValue; - matrix[1] = sinValue; - matrix[2] = -sinValue; - matrix[3] = cosValue; - matrix[4] = x - (cosValue * x - sinValue * y); - matrix[5] = y - (sinValue * x + cosValue * y); +/** + * Generate a rotation matrix around the center or around a point x,y + * @param {TDegree} angle rotation in degrees + * @param {number} [x] translation on X axis for the pivot point + * @param {number} [y] translation on Y axis for the pivot point + * @returns {TMat2D} matrix + */ +export function rotateMatrix(angle: TDegree, x = 0, y = 0): TMat2D { + const angleRadiant = degreesToRadians(angle), + cosValue = cos(angleRadiant), + sinValue = sin(angleRadiant); + return [ + cosValue, + sinValue, + -sinValue, + cosValue, + x - (cosValue * x - sinValue * y), + y - (sinValue * x + cosValue * y), + ]; } diff --git a/src/parser/scaleMatrix.ts b/src/parser/scaleMatrix.ts index 4241fa024e0..883d7eb8927 100644 --- a/src/parser/scaleMatrix.ts +++ b/src/parser/scaleMatrix.ts @@ -9,12 +9,18 @@ import { TMat2D } from '../typedefs'; * For more info, see * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#scale */ -type TMatScale = [x: number] | [x: number, y: number]; -export function scaleMatrix(matrix: TMat2D, args: TMatScale | number[]) { - const multiplierX = args[0], - multiplierY = args.length === 2 ? args[1] : args[0]; - - matrix[0] = multiplierX; - matrix[3] = multiplierY; -} +/** + * Generate a scale matrix around the point 0,0 + * @param {number} x scale on X axis + * @param {number} [y] scale on Y axis + * @returns {TMat2D} matrix + */ +export const scaleMatrix = (x: number, y: number = x): TMat2D => [ + x, + 0, + 0, + y, + 0, + 0, +]; diff --git a/src/parser/skewMatrix.ts b/src/parser/skewMatrix.ts index 58846ab3696..c5ba2a6a4e3 100644 --- a/src/parser/skewMatrix.ts +++ b/src/parser/skewMatrix.ts @@ -1,20 +1,47 @@ import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; -import { TMat2D } from '../typedefs'; +import { TDegree, TMat2D } from '../typedefs'; /** * A matrix in the form - * [0 x 0] - * [y 0 0] + * [1 x 0] + * [0 1 0] * [0 0 1] + * + * or + * + * [1 0 0] + * [y 1 0] + * [0 0 1] + * * For more info, see * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#skewx */ -type TMatSkew = [xy: number]; +const fromAngleToSkew = (angle: TDegree) => Math.tan(degreesToRadians(angle)); + +/** + * Generate a skew matrix for the X axis + * @param {TDegree} skewValue translation on X axis + * @returns {TMat2D} matrix + */ +export const skewXMatrix = (skewValue: TDegree): TMat2D => [ + 1, + 0, + fromAngleToSkew(skewValue), + 1, + 0, + 0, +]; -export function skewMatrix( - matrix: TMat2D, - args: TMatSkew | number[], - pos: 2 | 1 -): void { - matrix[pos] = Math.tan(degreesToRadians(args[0])); -} +/** + * Generate a skew matrix for the Y axis + * @param {TDegree} skewValue translation on Y axis + * @returns {TMat2D} matrix + */ +export const skewYMatrix = (skewValue: TDegree): TMat2D => [ + 1, + fromAngleToSkew(skewValue), + 0, + 1, + 0, + 0, +]; diff --git a/src/parser/translateMatrix.ts b/src/parser/translateMatrix.ts index 2d1265be9a9..b9b555d2cc2 100644 --- a/src/parser/translateMatrix.ts +++ b/src/parser/translateMatrix.ts @@ -7,19 +7,11 @@ import { TMat2D } from '../typedefs'; * [ 0 0 1 ] * See @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#translate for more details */ -type TMatTranslate = [x: number] | [x: number, y: number]; /** - * Force the translation to be this - * @param matrix - * @param args + * Generate a translation matrix + * @param {number} x translation on X axis + * @param {number} [y] translation on Y axis + * @returns {TMat2D} matrix */ -export function translateMatrix( - matrix: TMat2D, - args: TMatTranslate | number[] -): void { - matrix[4] = args[0]; - if (args.length === 2) { - matrix[5] = args[1]; - } -} +export const translateMatrix = (x: number, y = 0): TMat2D => [1, 0, 0, 1, x, y]; diff --git a/src/shapes/Object/ObjectGeometry.ts b/src/shapes/Object/ObjectGeometry.ts index 21329f79bfd..96829b49f03 100644 --- a/src/shapes/Object/ObjectGeometry.ts +++ b/src/shapes/Object/ObjectGeometry.ts @@ -26,6 +26,7 @@ import type { StaticCanvas } from '../../canvas/StaticCanvas'; import { ObjectOrigin } from './ObjectOrigin'; import { ObjectEvents } from '../../EventTypeDefs'; import { ControlProps } from './types/ControlProps'; +import { translateMatrix } from '../../parser/translateMatrix'; type TLineDescriptor = { o: Point; @@ -662,9 +663,9 @@ export class ObjectGeometry */ calcACoords(): TCornerPoint { const rotateMatrix = calcRotateMatrix({ angle: this.angle }), - center = this.getRelativeCenterPoint(), - translateMatrix = [1, 0, 0, 1, center.x, center.y] as TMat2D, - finalMatrix = multiplyTransformMatrices(translateMatrix, rotateMatrix), + { x, y } = this.getRelativeCenterPoint(), + tMatrix = translateMatrix(x, y), + finalMatrix = multiplyTransformMatrices(tMatrix, rotateMatrix), dim = this._getTransformedDimensions(), w = dim.x / 2, h = dim.y / 2; diff --git a/src/util/internals/cleanupSvAttribute.ts b/src/util/internals/cleanupSvAttribute.ts new file mode 100644 index 00000000000..9225484005c --- /dev/null +++ b/src/util/internals/cleanupSvAttribute.ts @@ -0,0 +1,8 @@ +import { reNum } from '../../parser/constants'; + +export const cleanupSvgAttribute = (attributeValue: string) => + attributeValue + .replace(new RegExp(`(${reNum})`, 'gi'), ' $1 ') + // replace annoying commas and arbitrary whitespace with single spaces + .replace(/,/gi, ' ') + .replace(/\s+/gi, ' '); diff --git a/src/util/misc/matrix.ts b/src/util/misc/matrix.ts index 37b6db31deb..3599f79db3e 100644 --- a/src/util/misc/matrix.ts +++ b/src/util/misc/matrix.ts @@ -1,4 +1,6 @@ import { iMatrix } from '../../constants'; +import { scaleMatrix } from '../../parser/scaleMatrix'; +import { skewXMatrix, skewYMatrix } from '../../parser/skewMatrix'; import { XY, Point } from '../../Point'; import { TDegree, TMat2D } from '../../typedefs'; import { cos } from './cos'; @@ -145,32 +147,17 @@ export const calcDimensionsMatrix = ({ skewX = 0 as TDegree, skewY = 0 as TDegree, }: TScaleMatrixArgs) => { - let scaleMatrix = iMatrix; - if (scaleX !== 1 || scaleY !== 1 || flipX || flipY) { - scaleMatrix = [ - flipX ? -scaleX : scaleX, - 0, - 0, - flipY ? -scaleY : scaleY, - 0, - 0, - ] as TMat2D; - } + let scaleMat = scaleMatrix( + flipX ? -scaleX : scaleX, + flipY ? -scaleY : scaleY + ); if (skewX) { - scaleMatrix = multiplyTransformMatrices( - scaleMatrix, - [1, 0, Math.tan(degreesToRadians(skewX)), 1] as unknown as TMat2D, - true - ); + scaleMat = multiplyTransformMatrices(scaleMat, skewXMatrix(skewX), true); } if (skewY) { - scaleMatrix = multiplyTransformMatrices( - scaleMatrix, - [1, Math.tan(degreesToRadians(skewY)), 0, 1] as unknown as TMat2D, - true - ); + scaleMat = multiplyTransformMatrices(scaleMat, skewYMatrix(skewY), true); } - return scaleMatrix; + return scaleMat; }; /** diff --git a/src/util/path/index.ts b/src/util/path/index.ts index daed44a87a2..e4ec8797c5c 100644 --- a/src/util/path/index.ts +++ b/src/util/path/index.ts @@ -23,7 +23,7 @@ import { } from './typedefs'; import { XY, Point } from '../../Point'; import { rePathCommand } from './regex'; -import { reNum } from '../../parser/constants'; +import { cleanupSvgAttribute } from '../internals/cleanupSvAttribute'; /** * Commands that may be repeated @@ -843,11 +843,7 @@ export const getPointOnPath = ( export const parsePath = (pathString: string): TComplexPathData => { // clean the string // add spaces around the numbers - pathString = pathString - .replace(new RegExp(`(${reNum})`, 'gi'), ' $1 ') - // replace annoying commas and arbitrary whitespace with single spaces - .replace(/,/gi, ' ') - .replace(/\s+/gi, ' '); + pathString = cleanupSvgAttribute(pathString); const res: TComplexPathData = []; for (const match of pathString.matchAll(new RegExp(rePathCommand, 'gi'))) {