From 38ffb8ecb82b4e5c858aee945e6d887b8ca959db Mon Sep 17 00:00:00 2001 From: Yonatan Kra Date: Wed, 20 Oct 2021 05:38:38 +0300 Subject: [PATCH] Add translate, translateSelf, scale and scaleSelf to DOMMatrix (#83) * feat: add scale and translate to dommatrix * docs: update readme * refactor: prettier fixes * feat(dommatrix): added translateSelf * refactor: converted translate to use translateSelf * feat(dommatrix): added scaleSelf * refactor: use scaleSelf in scale * refactor: cleanup unused functions * refactor: change the matrix multiplication The calculation function now deals with arrays instead of the properties directly * refactor: prettier --- README.md | 1 + __tests__/classes/DOMMatrix.js | 171 +++++++++++++++++++++--- __tests__/classes/Path2D.js | 1 - src/classes/CanvasRenderingContext2D.js | 22 +-- src/classes/DOMMatrix.js | 111 +++++++++++++++ src/classes/Path2D.js | 3 +- 6 files changed, 274 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index a5190f1..9a70c76 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,7 @@ canvas.toDataURL.mockReturnValueOnce( - [@jtenner](https://github.com/jtenner) - [@evanoc0](https://github.com/evanoc0) - [@lekha](https://github.com/lekha) +- [@yonatankra](https://github.com/yonatankra) ## License diff --git a/__tests__/classes/DOMMatrix.js b/__tests__/classes/DOMMatrix.js index 38b42a3..062b4c0 100644 --- a/__tests__/classes/DOMMatrix.js +++ b/__tests__/classes/DOMMatrix.js @@ -32,22 +32,7 @@ describe('DOMMatrix class', () => { it('should accept an array of 16 length', () => { const matrix = new DOMMatrix([ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, ]); expect(matrix).toBeInstanceOf(DOMMatrix); }); @@ -160,4 +145,158 @@ describe('DOMMatrix class', () => { matrix.m42 = 2; expect(matrix.f).toBe(2); }); + + describe(`translate`, function () { + it(`should return a new DOMMatrix instance`, function () { + const matrix = new DOMMatrix(); + const translatedMatrix = matrix.translate(100, 100); + expect(translatedMatrix instanceof DOMMatrix).toBeTruthy(); + expect(translatedMatrix === matrix).toBeFalsy(); + }); + + it(`should apply 2d changes`, function () { + const x = 100; + const y = 200; + const matrix = new DOMMatrix([4, 5, 1, 3, 10, 9]); + const expectedMatrix = new DOMMatrix([ + 4, 5, 0, 0, 1, 3, 0, 0, 0, 0, 1, 0, 610, 1109, 0, 1, + ]); + const translatedMatrix = matrix.translate(x, y); + expect(translatedMatrix.toFloat32Array()).toEqual( + expectedMatrix.toFloat32Array() + ); + expect(translatedMatrix.is2D).toEqual(true); + }); + + it(`should apply 3d changes`, function () { + const x = 100; + const y = 200; + const z = 300; + const matrix = new DOMMatrix(); + const expectedMatrix = new DOMMatrix([ + 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 100, 200, 300, 1, + ]); + const translatedMatrix = matrix.translate(x, y, z); + expect(translatedMatrix).toEqual(expectedMatrix); + expect(translatedMatrix.is2D).toEqual(false); + }); + }); + + describe(`scale`, function () { + it(`should return a new DOMMatrix instance`, function () { + const matrix = new DOMMatrix(); + const scaledMatrix = matrix.scale(0.5, 0.7); + expect(scaledMatrix instanceof DOMMatrix).toBeTruthy(); + expect(scaledMatrix === matrix).toBeFalsy(); + }); + + it(`should apply 2d changes`, function () { + const scaleX = 0.75; + const scaleY = 0.5; + const matrix = new DOMMatrix([7, 8, 9, 20, 4, 7]); + const expectedMatrix = new DOMMatrix([5.25, 6, 4.5, 10, 4, 7]); + const scaledMatrix = matrix.scale(scaleX, scaleY); + expect(scaledMatrix).toEqual(expectedMatrix); + }); + + it(`should apply 3d changes`, function () { + const scaleX = 0.65; + const scaleY = 0.55; + const scaleZ = 0.9; + const matrix = new DOMMatrix(); + const expectedMatrix = new DOMMatrix([ + 0.65, 0, 0, 0, 0, 0.55, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 1, + ]); + const scaledMatrix = matrix.scale(scaleX, scaleY, scaleZ); + expect(scaledMatrix).toEqual(expectedMatrix); + }); + }); + + describe(`translateSelf`, function () { + it(`should return dot product of a 2d matrix multiplication`, function () { + const matrix2D = new DOMMatrix([1, 2, 3, 4, 5, 6]); + const tx = 2, + ty = 3; + const expectedMatrix = new DOMMatrix([1, 2, 3, 4, 16, 22]); + matrix2D.translateSelf(tx, ty); + expect(matrix2D.toFloat32Array()).toEqual( + expectedMatrix.toFloat32Array() + ); + expect(matrix2D.is2D).toEqual(true); + }); + + it(`should return do product of a 3d matrix`, function () { + const matrix3D = new DOMMatrix([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + ]); + const tx = 2, + ty = 3, + tz = 4; + const expectedMatrix = new DOMMatrix([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 66, 76, 86, 96, + ]); + matrix3D.translateSelf(tx, ty, tz); + expect(matrix3D.toFloat32Array()).toEqual( + expectedMatrix.toFloat32Array() + ); + expect(matrix3D.is2D).toEqual(false); + }); + + it(`should convert 2d matrix to 3d matrix when sent tz`, function () { + const matrix2D = new DOMMatrix([1, 2, 3, 4, 5, 6]); + const tx = 2, + ty = 3, + tz = 4; + const expectedMatrix = new DOMMatrix([ + 1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 1, 0, 16, 22, 4, 1, + ]); + matrix2D.translateSelf(tx, ty, tz); + expect(matrix2D.toFloat32Array()).toEqual( + expectedMatrix.toFloat32Array() + ); + expect(matrix2D.is2D).toEqual(false); + }); + }); + + describe(`scaleSelf`, function () { + it(`should return dot product of a 2d translated matrix multiplication`, function () { + const matrix2D = new DOMMatrix([1, 2, 3, 4, 5, 6]); + const scaleX = 2, + scaleY = 3; + const expectedMatrix = new DOMMatrix([2, 4, 9, 12, 5, 6]); + matrix2D.scaleSelf(scaleX, scaleY); + expect(matrix2D).toEqual(expectedMatrix); + }); + + it(`should return dot product of a 3d translated matrix multiplication`, function () { + const matrix3D = new DOMMatrix([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + ]); + const sx = 2, + sy = 3, + sz = 4; + const expectedMatrix = new DOMMatrix([ + 2, 4, 6, 8, 15, 18, 21, 24, 36, 40, 44, 48, 13, 14, 15, 16, + ]); + matrix3D.scaleSelf(sx, sy, sz); + expect(matrix3D).toEqual(expectedMatrix); + }); + + it(`should return dot product of a 3d translated matrix multiplication with origin`, function () { + const matrix3D = new DOMMatrix([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + ]); + const sx = 2, + sy = 3, + sz = 4, + ox = 5, + oy = 6, + oz = 7; + const expectedMatrix = new DOMMatrix([ + 2, 4, 6, 8, 15, 18, 21, 24, 36, 40, 44, 48, -241, -278, -315, -352, + ]); + matrix3D.scaleSelf(sx, sy, sz, ox, oy, oz); + expect(matrix3D).toEqual(expectedMatrix); + }); + }); }); diff --git a/__tests__/classes/Path2D.js b/__tests__/classes/Path2D.js index 95fe505..b3a5ecc 100644 --- a/__tests__/classes/Path2D.js +++ b/__tests__/classes/Path2D.js @@ -72,5 +72,4 @@ describe('Path2D', () => { expect(path1._path[2]).toBe(path2._path[0]); expect(path1._path[3]).toBe(path2._path[1]); }); - }); diff --git a/src/classes/CanvasRenderingContext2D.js b/src/classes/CanvasRenderingContext2D.js index 08cfddf..477d245 100644 --- a/src/classes/CanvasRenderingContext2D.js +++ b/src/classes/CanvasRenderingContext2D.js @@ -184,16 +184,8 @@ export default class CanvasRenderingContext2D { } addHitRegion(options = {}) { - const { - path, - fillRule, - id, - parentID, - cursor, - control, - label, - role, - } = options; + const { path, fillRule, id, parentID, cursor, control, label, role } = + options; if (!path && !id) throw new DOMException( 'ConstraintError', @@ -1724,9 +1716,8 @@ export default class CanvasRenderingContext2D { if (typeof value === 'string') { try { const result = new MooColor(value); - value = this._shadowColorStack[this._stackIndex] = serializeColor( - result - ); + value = this._shadowColorStack[this._stackIndex] = + serializeColor(result); } catch (e) { return; } @@ -1829,9 +1820,8 @@ export default class CanvasRenderingContext2D { try { const result = new MooColor(value); valid = true; - value = this._strokeStyleStack[this._stackIndex] = serializeColor( - result - ); + value = this._strokeStyleStack[this._stackIndex] = + serializeColor(result); } catch (e) { return; } diff --git a/src/classes/DOMMatrix.js b/src/classes/DOMMatrix.js index 3e6e389..cd4860e 100644 --- a/src/classes/DOMMatrix.js +++ b/src/classes/DOMMatrix.js @@ -1,3 +1,27 @@ +function sumMultipleOfMatricesCells(matrix1Array, matrix2Array, { i, j }) { + let sum = 0; + for (let k = 0; k < 4; k++) { + const matrix1Index = j - 1 + k * 4; + const matrix2Index = (i - 1) * 4 + k; + sum += matrix1Array[matrix1Index] * matrix2Array[matrix2Index]; + } + return sum; +} + +function multiplyMatrices(leftMatrix, rightMatrix) { + const leftMatrixArray = leftMatrix.toFloat64Array(); + const rightMatrixArray = rightMatrix.toFloat64Array(); + for (let i = 1; i <= 4; i++) { + for (let j = 1; j <= 4; j++) { + leftMatrix[`m${i}${j}`] = sumMultipleOfMatricesCells( + leftMatrixArray, + rightMatrixArray, + { i, j } + ); + } + } +} + export default class DOMMatrix { _is2D = true; m11 = 1.0; @@ -182,4 +206,91 @@ export default class DOMMatrix { this.m44, ]); } + + translateSelf(x, y, z) { + const tx = Number(x), + ty = Number(y), + tz = isNaN(Number(z)) ? 0 : Number(z); + + const translationMatrix = new DOMMatrix(); + translationMatrix.m41 = tx; + translationMatrix.m42 = ty; + translationMatrix.m43 = tz; + + multiplyMatrices(this, translationMatrix); + + if (tz) { + this._is2D = false; + } + return this; + } + + translate(x, y, z) { + let translatedMatrix; + if (this.is2D) { + translatedMatrix = new DOMMatrix([ + this.a, + this.b, + this.c, + this.d, + this.e, + this.f, + ]); + } else { + translatedMatrix = new DOMMatrix(this.toFloat32Array()); + } + + return translatedMatrix.translateSelf(x, y, z); + } + + scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) { + const sx = Number(scaleX), + sy = isNaN(Number(scaleY)) ? sx : Number(scaleY), + sz = isNaN(Number(scaleZ)) ? 1 : Number(scaleZ); + + const ox = isNaN(Number(originX)) ? 0 : Number(originX), + oy = isNaN(Number(originY)) ? 0 : Number(originY), + oz = isNaN(Number(originZ)) ? 0 : Number(originZ); + + this.translateSelf(ox, oy, oz); + + const scaleMatrix = new DOMMatrix(); + scaleMatrix.m11 = sx; + scaleMatrix.m22 = sy; + scaleMatrix.m33 = sz; + + multiplyMatrices(this, scaleMatrix); + + this.translateSelf(-ox, -oy, -oz); + + if (Math.abs(sz) !== 1) { + this._is2D = false; + } + return this; + } + + scale(scaleX, scaleY, scaleZ, originX, originY, originZ) { + let scaledMatrix; + if (this.is2D) { + scaledMatrix = new DOMMatrix([ + this.a, + this.b, + this.c, + this.d, + this.e, + this.f, + ]); + } else { + scaledMatrix = new DOMMatrix(this.toFloat32Array()); + } + + return scaledMatrix.scaleSelf( + scaleX, + scaleY, + scaleZ, + originX, + originY, + originZ + ); + } } diff --git a/src/classes/Path2D.js b/src/classes/Path2D.js index 01dc12d..99d23e0 100644 --- a/src/classes/Path2D.js +++ b/src/classes/Path2D.js @@ -39,7 +39,6 @@ export default class Path2D { throw new TypeError( "Failed to execute 'addPath' on 'Path2D': parameter 1 is not of type 'Path2D'." ); - for (let i = 0; i < path._path.length; i++) - this._path.push(path._path[i]); + for (let i = 0; i < path._path.length; i++) this._path.push(path._path[i]); } }