diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e74bf4e87..cc2e37f115a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(): Matrix util cleanup [#8894](https://github.com/fabricjs/fabric.js/pull/8894) - chore(TS): pattern cleanup + export types [#8875](https://github.com/fabricjs/fabric.js/pull/8875) - fix(): Disable offscreen check for bg and overlay when not needed [#8898](https://github.com/fabricjs/fabric.js/pull/8898) - chore(): cleanup #8888 [#8892](https://github.com/fabricjs/fabric.js/pull/8892) diff --git a/src/parser/parseTransformAttribute.ts b/src/parser/parseTransformAttribute.ts index cda34dc8dfd..8930a783dd8 100644 --- a/src/parser/parseTransformAttribute.ts +++ b/src/parser/parseTransformAttribute.ts @@ -1,12 +1,15 @@ import { iMatrix } from '../constants'; import { reNum } from './constants'; -import { multiplyTransformMatrices } from '../util/misc/matrix'; -import { rotateMatrix } from './rotateMatrix'; -import { scaleMatrix } from './scaleMatrix'; -import { translateMatrix } from './translateMatrix'; import { TMat2D } from '../typedefs'; import { cleanupSvgAttribute } from '../util/internals/cleanupSvgAttribute'; -import { skewXMatrix, skewYMatrix } from './skewMatrix'; +import { + createRotateMatrix, + createScaleMatrix, + createSkewXMatrix, + createSkewYMatrix, + createTranslateMatrix, + multiplyTransformMatrixArray, +} from '../util/misc/matrix'; // == begin transform regexp const p = `(${reNum})`; @@ -64,19 +67,19 @@ export function parseTransformAttribute(attributeValue: string): TMat2D { switch (operation) { case 'translate': - matrix = translateMatrix(arg0, arg1); + matrix = createTranslateMatrix(arg0, arg1); break; case 'rotate': - matrix = rotateMatrix(arg0, arg1, arg2); + matrix = createRotateMatrix({ angle: arg0 }, { x: arg1, y: arg2 }); break; case 'scale': - matrix = scaleMatrix(arg0, arg1); + matrix = createScaleMatrix(arg0, arg1); break; case 'skewX': - matrix = skewXMatrix(arg0); + matrix = createSkewXMatrix(arg0); break; case 'skewY': - matrix = skewYMatrix(arg0); + matrix = createSkewYMatrix(arg0); break; case 'matrix': matrix = [arg0, arg1, arg2, arg3, arg4, arg5]; @@ -87,8 +90,5 @@ export function parseTransformAttribute(attributeValue: string): TMat2D { matrices.push(matrix); } - return matrices.reduce( - (acc, matrix) => multiplyTransformMatrices(acc, matrix), - iMatrix - ); + return multiplyTransformMatrixArray(matrices); } diff --git a/src/parser/rotateMatrix.ts b/src/parser/rotateMatrix.ts deleted file mode 100644 index 27f65393c11..00000000000 --- a/src/parser/rotateMatrix.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { cos } from '../util/misc/cos'; -import { sin } from '../util/misc/sin'; -import { TDegree, TMat2D } from '../typedefs'; -import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; - -/** - * A rotation matrix - * In the form of - * [cos(a) -sin(a) -xcos(a)+ysin(a)+x] - * [sin(a) cos(a) -xsin(a)-ycos(a)+y] - * [0 0 1 ] - */ - -/** - * 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 deleted file mode 100644 index 883d7eb8927..00000000000 --- a/src/parser/scaleMatrix.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { TMat2D } from '../typedefs'; - -/** - * A scale matrix - * Takes form - * [x 0 0] - * [0 y 0] - * [0 0 1] - * For more info, see - * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#scale - */ - -/** - * 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 deleted file mode 100644 index c5ba2a6a4e3..00000000000 --- a/src/parser/skewMatrix.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; -import { TDegree, TMat2D } from '../typedefs'; - -/** - * A matrix in the form - * [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 - */ -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, -]; - -/** - * 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 deleted file mode 100644 index b9b555d2cc2..00000000000 --- a/src/parser/translateMatrix.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TMat2D } from '../typedefs'; - -/** - * A translation matrix in the form of - * [ 1 0 x ] - * [ 0 1 y ] - * [ 0 0 1 ] - * See @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#translate for more details - */ - -/** - * Generate a translation matrix - * @param {number} x translation on X axis - * @param {number} [y] translation on Y axis - * @returns {TMat2D} matrix - */ -export const translateMatrix = (x: number, y = 0): TMat2D => [1, 0, 0, 1, x, y]; diff --git a/src/shapes/Object/InteractiveObject.ts b/src/shapes/Object/InteractiveObject.ts index c696a930fa8..03fc2a00f1d 100644 --- a/src/shapes/Object/InteractiveObject.ts +++ b/src/shapes/Object/InteractiveObject.ts @@ -1,9 +1,10 @@ import { Point } from '../../Point'; -import type { AssertKeys, TCornerPoint, TDegree, TMat2D } from '../../typedefs'; +import type { AssertKeys, TCornerPoint, TDegree } from '../../typedefs'; import { FabricObject } from './Object'; import { degreesToRadians } from '../../util/misc/radiansDegreesConversion'; import { - calcRotateMatrix, + createRotateMatrix, + createTranslateMatrix, multiplyTransformMatrices, qrDecompose, TQrDecomposeOut, @@ -235,8 +236,8 @@ export class InteractiveFabricObject< calcOCoords(): Record { const vpt = this.getViewportTransform(), center = this.getCenterPoint(), - tMatrix = [1, 0, 0, 1, center.x, center.y] as TMat2D, - rMatrix = calcRotateMatrix({ + tMatrix = createTranslateMatrix(center.x, center.y), + rMatrix = createRotateMatrix({ angle: this.getTotalAngle() - (!!this.group && this.flipX ? 180 : 0), }), positionMatrix = multiplyTransformMatrices(tMatrix, rMatrix), diff --git a/src/shapes/Object/ObjectGeometry.ts b/src/shapes/Object/ObjectGeometry.ts index 96829b49f03..61c333abe3f 100644 --- a/src/shapes/Object/ObjectGeometry.ts +++ b/src/shapes/Object/ObjectGeometry.ts @@ -12,7 +12,8 @@ import { Point } from '../../Point'; import { makeBoundingBoxFromPoints } from '../../util/misc/boundingBoxFromPoints'; import { cos } from '../../util/misc/cos'; import { - calcRotateMatrix, + createRotateMatrix, + createTranslateMatrix, composeMatrix, invertTransform, multiplyTransformMatrices, @@ -26,7 +27,6 @@ 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 +662,9 @@ export class ObjectGeometry * @return {TCornerPoint} */ calcACoords(): TCornerPoint { - const rotateMatrix = calcRotateMatrix({ angle: this.angle }), + const rotateMatrix = createRotateMatrix({ angle: this.angle }), { x, y } = this.getRelativeCenterPoint(), - tMatrix = translateMatrix(x, y), + tMatrix = createTranslateMatrix(x, y), finalMatrix = multiplyTransformMatrices(tMatrix, rotateMatrix), dim = this._getTransformedDimensions(), w = dim.x / 2, diff --git a/src/util/index.ts b/src/util/index.ts index 8a34e69201a..942126b8ae6 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -18,9 +18,14 @@ export { invertTransform, composeMatrix, qrDecompose, + createTranslateMatrix, + createRotateMatrix, + createScaleMatrix, + createSkewXMatrix, + createSkewYMatrix, calcDimensionsMatrix, - calcRotateMatrix, multiplyTransformMatrices, + multiplyTransformMatrixArray, isIdentityMatrix, } from './misc/matrix'; export { diff --git a/src/util/misc/matrix.ts b/src/util/misc/matrix.ts index 3599f79db3e..2ef0407d953 100644 --- a/src/util/misc/matrix.ts +++ b/src/util/misc/matrix.ts @@ -1,8 +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 { Point, XY } from '../../Point'; +import { TDegree, TRadian, TMat2D } from '../../typedefs'; import { cos } from './cos'; import { degreesToRadians, radiansToDegrees } from './radiansDegreesConversion'; import { sin } from './sin'; @@ -57,7 +55,7 @@ export const transformPoint = ( export const invertTransform = (t: TMat2D): TMat2D => { const a = 1 / (t[0] * t[3] - t[1] * t[2]), r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0] as TMat2D, - { x, y } = transformPoint(new Point(t[4], t[5]), r, true); + { x, y } = new Point(t[4], t[5]).transform(r, true); r[4] = -x; r[5] = -y; return r; @@ -84,6 +82,25 @@ export const multiplyTransformMatrices = ( is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5], ] as TMat2D; +/** + * Multiplies {@link matrices} such that a matrix defines the plane for the rest of the matrices **after** it + * + * `multiplyTransformMatrixArray([A, B, C, D])` is equivalent to `A(B(C(D)))` + * + * @param matrices an array of matrices + * @param [is2x2] flag to multiply matrices as 2x2 matrices + * @returns the multiplication product + */ +export const multiplyTransformMatrixArray = ( + matrices: (TMat2D | undefined | null | false)[], + is2x2?: boolean +) => + matrices.reduceRight( + (product: TMat2D, curr) => + curr ? multiplyTransformMatrices(curr, product, is2x2) : product, + iMatrix + ); + /** * Decomposes standard 2x3 matrix into transform components * @param {TMat2D} a transformMatrix @@ -107,22 +124,130 @@ export const qrDecompose = (a: TMat2D): TQrDecomposeOut => { }; /** - * Returns a transform matrix starting from an object of the same kind of - * the one returned from qrDecompose, useful also if you want to calculate some - * transformations from an object that is not enlived yet - * @param {Object} options - * @param {Number} [options.angle] angle in degrees - * @return {TMat2D} transform matrix + * Generate a translation matrix + * + * A translation matrix in the form of + * [ 1 0 x ] + * [ 0 1 y ] + * [ 0 0 1 ] + * + * See @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#translate for more details + * + * @param {number} x translation on X axis + * @param {number} [y] translation on Y axis + * @returns {TMat2D} matrix */ -export const calcRotateMatrix = ({ angle }: TRotateMatrixArgs): TMat2D => { - if (!angle) { - return iMatrix; - } - const theta = degreesToRadians(angle), - cosin = cos(theta), - sinus = sin(theta); - return [cosin, sinus, -sinus, cosin, 0, 0]; -}; +export const createTranslateMatrix = (x: number, y = 0): TMat2D => [ + 1, + 0, + 0, + 1, + x, + y, +]; + +/** + * Generate a rotation matrix around around a point (x,y), defaulting to (0,0) + * + * A matrix in the form of + * [cos(a) -sin(a) -x*cos(a)+y*sin(a)+x] + * [sin(a) cos(a) -x*sin(a)-y*cos(a)+y] + * [0 0 1 ] + * + * + * @param {TDegree} angle rotation in degrees + * @param {XY} [pivotPoint] pivot point to rotate around + * @returns {TMat2D} matrix + */ +export function createRotateMatrix( + { angle = 0 }: TRotateMatrixArgs = {}, + { x = 0, y = 0 }: Partial = {} +): TMat2D { + const angleRadiant = degreesToRadians(angle), + cosValue = cos(angleRadiant), + sinValue = sin(angleRadiant); + return [ + cosValue, + sinValue, + -sinValue, + cosValue, + x ? x - (cosValue * x - sinValue * y) : 0, + y ? y - (sinValue * x + cosValue * y) : 0, + ]; +} + +/** + * Generate a scale matrix around the point (0,0) + * + * A matrix in the form of + * [x 0 0] + * [0 y 0] + * [0 0 1] + * + * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#scale + * + * @param {number} x scale on X axis + * @param {number} [y] scale on Y axis + * @returns {TMat2D} matrix + */ +export const createScaleMatrix = (x: number, y: number = x): TMat2D => [ + x, + 0, + 0, + y, + 0, + 0, +]; + +export const angleToSkew = (angle: TDegree) => + Math.tan(degreesToRadians(angle)); + +export const skewToAngle = (value: TRadian) => + radiansToDegrees(Math.atan(value)); + +/** + * Generate a skew matrix for the X axis + * + * A matrix in the form of + * [1 x 0] + * [0 1 0] + * [0 0 1] + * + * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#skewx + * + * @param {TDegree} skewValue translation on X axis + * @returns {TMat2D} matrix + */ +export const createSkewXMatrix = (skewValue: TDegree): TMat2D => [ + 1, + 0, + angleToSkew(skewValue), + 1, + 0, + 0, +]; + +/** + * Generate a skew matrix for the Y axis + * + * A matrix in the form of + * [1 0 0] + * [y 1 0] + * [0 0 1] + * + * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#skewy + * + * @param {TDegree} skewValue translation on Y axis + * @returns {TMat2D} matrix + */ +export const createSkewYMatrix = (skewValue: TDegree): TMat2D => [ + 1, + angleToSkew(skewValue), + 0, + 1, + 0, + 0, +]; /** * Returns a transform matrix starting from an object of the same kind of @@ -147,17 +272,14 @@ export const calcDimensionsMatrix = ({ skewX = 0 as TDegree, skewY = 0 as TDegree, }: TScaleMatrixArgs) => { - let scaleMat = scaleMatrix( - flipX ? -scaleX : scaleX, - flipY ? -scaleY : scaleY + return multiplyTransformMatrixArray( + [ + createScaleMatrix(flipX ? -scaleX : scaleX, flipY ? -scaleY : scaleY), + skewX && createSkewXMatrix(skewX), + skewY && createSkewYMatrix(skewY), + ], + true ); - if (skewX) { - scaleMat = multiplyTransformMatrices(scaleMat, skewXMatrix(skewX), true); - } - if (skewY) { - scaleMat = multiplyTransformMatrices(scaleMat, skewYMatrix(skewY), true); - } - return scaleMat; }; /** @@ -182,13 +304,9 @@ export const composeMatrix = ({ angle = 0 as TDegree, ...otherOptions }: TComposeMatrixArgs): TMat2D => { - let matrix = [1, 0, 0, 1, translateX, translateY] as TMat2D; - if (angle) { - matrix = multiplyTransformMatrices(matrix, calcRotateMatrix({ angle })); - } - const scaleMatrix = calcDimensionsMatrix(otherOptions); - if (scaleMatrix !== iMatrix) { - matrix = multiplyTransformMatrices(matrix, scaleMatrix); - } - return matrix; + return multiplyTransformMatrixArray([ + createTranslateMatrix(translateX, translateY), + angle && createRotateMatrix({ angle }), + calcDimensionsMatrix(otherOptions), + ]); }; diff --git a/test/unit/util.js b/test/unit/util.js index 2bd1c4b38bd..96cc89a4c55 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -94,9 +94,9 @@ assert.deepEqual(fabric.util.radiansToDegrees(), NaN); }); - QUnit.test('calcRotateMatrix', function (assert) { - assert.ok(typeof fabric.util.calcRotateMatrix === 'function', 'calcRotateMatrix should exist'); - var matrix = fabric.util.calcRotateMatrix({ angle: 90 }); + QUnit.test('createRotateMatrix', function (assert) { + assert.ok(typeof fabric.util.createRotateMatrix === 'function', 'createRotateMatrix should exist'); + var matrix = fabric.util.createRotateMatrix({ angle: 90 }); var expected = [ 0, 1, @@ -108,6 +108,20 @@ assert.deepEqual(matrix, expected, 'rotate matrix is equal'); }); + QUnit.test('createRotateMatrix with origin', function (assert) { + var matrix = fabric.util.createRotateMatrix({ angle: 90 }, { x: 100, y: 200 }); + var expected = [ + 0, + 1, + -1, + 0, + 300, + 100 + ]; + assert.deepEqual(matrix, expected, 'rotate matrix is equal'); + assert.deepEqual(new fabric.Point().rotate(Math.PI / 2, new fabric.Point(100, 200)), new fabric.Point(300, 100), 'rotating 0,0 around origin should equal the matrix translation'); + }); + QUnit.test('fabric.util.getRandomInt', function(assert) { assert.ok(typeof fabric.util.getRandomInt === 'function'); @@ -470,6 +484,29 @@ assert.deepEqual(m3, [2, 2, 2, 2, 0, 0]); }); + QUnit.test('multiplyTransformMatrixArray', function (assert) { + assert.ok(typeof fabric.util.multiplyTransformMatrixArray === 'function'); + const m1 = [1, 2, 3, 4, 10, 20], m2 = [5, 6, 7, 8, 30, 40]; + assert.deepEqual(fabric.util.multiplyTransformMatrixArray([m1, m2]), [ + 23, + 34, + 31, + 46, + 160, + 240 + ]); + assert.deepEqual(fabric.util.multiplyTransformMatrixArray([m1, m2], true), [ + 23, + 34, + 31, + 46, + 0, + 0 + ]); + assert.deepEqual(fabric.util.multiplyTransformMatrixArray([m1, m2]), fabric.util.multiplyTransformMatrices(m1, m2)); + assert.deepEqual(fabric.util.multiplyTransformMatrixArray([m1, m2], true), fabric.util.multiplyTransformMatrices(m1, m2, true)); + }); + QUnit.test('resetObjectTransform', function(assert) { assert.ok(typeof fabric.util.resetObjectTransform === 'function'); var rect = new fabric.Rect({