From 12f87d58bf7401af11c9d67789f2ffeda52f0372 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Wed, 29 Apr 2020 14:43:13 +1000 Subject: [PATCH] finish tests for Matrix.js --- CHANGELOG.md | 4 + spec/spec/types/Matrix.js | 195 +++++++++++++++++++++++++++++++++++--- src/types/Matrix.js | 45 ++------- 3 files changed, 193 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9024c27..d89ea91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http: - fixed `reference()` which correctly returns `null` instead of throwing when specifying an attribute which holds a number - fixed `flatten()` which correctly flattens now but doesnt accept parameters anymore (makes no sense) - fixed `ungroup()` which now inserts the elements at the correct position in the correct order and has position as second argument now + - fixed `position` for `transform()` to also allow a position of 0 ### Added - added second Parameter to `SVG(el, isHTML)` which allows to explicitely create elements in the HTML namespace (#1058) @@ -42,6 +43,9 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http: - added index and array parameter when passing a function to `List.each()` so that it mostly behaves like map - added lots of tests in es6 format +### Deleted + - deleted undocumented `Matrix.compose()` method which did the same as `new Matrix()` or `Matrix.transform()` + ## [3.0.16] - 2019-11-12 ### Fixed diff --git a/spec/spec/types/Matrix.js b/spec/spec/types/Matrix.js index 33500fa..0ef5273 100644 --- a/spec/spec/types/Matrix.js +++ b/spec/spec/types/Matrix.js @@ -1,8 +1,9 @@ -/* globals describe, expect, it, jasmine */ +/* globals describe, expect, it, spyOn, jasmine */ -import { Matrix, Rect } from '../../../src/main.js' +import { Matrix, Rect, SVG } from '../../../src/main.js' +import { getWindow } from '../../../src/utils/window.js' -const { objectContaining } = jasmine +const { any, objectContaining } = jasmine describe('Matrix.js', () => { const comp = { a: 2, b: 0, c: 0, d: 2, e: 100, f: 50 } @@ -46,6 +47,11 @@ describe('Matrix.js', () => { const matrix = new Matrix(2, 0, 0, 2, 100, 50) expect(matrix).toEqual(objectContaining(comp)) }) + + it('falls back to base if source is missing values', () => { + const matrix = new Matrix([]) + expect(matrix).toEqual(new Matrix()) + }) }) describe('toString()', () => { @@ -54,14 +60,20 @@ describe('Matrix.js', () => { }) }) - describe('compose()', () => { - it('composes a matrix to form the correct result', () => { - const composed = new Matrix().compose({ - scaleX: 3, scaleY: 20, shear: 4, rotate: 50, translateX: 23, translateY: 52 - }) + describe('transform()', () => { + it('does simple left matrix multiplication if matrixlike object is passed', () => { + const matrix = new Matrix().transform(new Matrix().scale(2)) + expect(matrix).toEqual(new Matrix().lmultiplyO(new Matrix().scale(2))) + }) - const expected = new Matrix().scale(3, 20).shear(4).rotate(50).translate(23, 52) - expect(composed).toEqual(expected) + it('forces the origin to a specific place if position.x is passed', () => { + const matrix = new Matrix().transform({ px: 10 }) + expect(matrix.e).toBe(10) + }) + + it('forces the origin to a specific place if position.y is passed', () => { + const matrix = new Matrix().transform({ py: 10 }) + expect(matrix.f).toBe(10) }) }) @@ -80,7 +92,11 @@ describe('Matrix.js', () => { it('can be recomposed to the same matrix', () => { var matrix = new Matrix().scale(3, 2.5).shear(4).rotate(30).translate(20, 30) var decomposed = matrix.decompose() - var composed = new Matrix().compose(decomposed) + + // Get rid of the matrix values before recomposing with the matrix constructor + for (const prop in 'abcdef') delete decomposed[prop] + + var composed = new Matrix(decomposed) expect(matrix.a).toBeCloseTo(composed.a) expect(matrix.b).toBeCloseTo(composed.b) expect(matrix.c).toBeCloseTo(composed.c) @@ -123,7 +139,6 @@ describe('Matrix.js', () => { describe('inverse()', () => { it('inverses matrix', () => { - var matrix1 = new Matrix(2, 0, 0, 5, 4, 3) var matrix2 = matrix1.inverse() var abcdef = [ 0.5, 0, 0, 0.2, -2, -0.6 ] @@ -132,6 +147,11 @@ describe('Matrix.js', () => { expect(matrix2['abcdef'[i]]).toBeCloseTo(abcdef[i]) } }) + + it('throws if matrix is not inversable', () => { + const matrix = new Matrix(0, 0, 0, 0, 0, 0) + expect(() => matrix.inverse()).toThrowError('Cannot invert matrix(0,0,0,0,0,0)') + }) }) describe('translate()', () => { @@ -157,6 +177,7 @@ describe('Matrix.js', () => { expect(matrix.e).toBe(4 * 3) expect(matrix.f).toBe(3 * 3) }) + it('performs a non-uniformal scale with two values', () => { var matrix = new Matrix(1, 0, 0, 1, 4, 3).scale(2.5, 3.5) @@ -165,6 +186,7 @@ describe('Matrix.js', () => { expect(matrix.e).toBe(4 * 2.5) expect(matrix.f).toBe(3 * 3.5) }) + it('performs a uniformal scale at a given center point with three values', () => { var matrix = new Matrix(1, 3, 2, 3, 4, 3).scale(3, 2, 3) @@ -175,6 +197,7 @@ describe('Matrix.js', () => { expect(matrix.e).toBe(8) expect(matrix.f).toBe(3) }) + it('performs a non-uniformal scale at a given center point with four values', () => { var matrix = new Matrix(1, 3, 2, 3, 4, 3).scale(3, 2, 2, 3) @@ -198,6 +221,7 @@ describe('Matrix.js', () => { expect(matrix.e).toBeCloseTo(1.96410162) expect(matrix.f).toBeCloseTo(4.59807621) }) + it('performs a rotation around a given point with three arguments', () => { var matrix = new Matrix(1, 3, 2, 3, 4, 3).rotate(30, 2, 3) @@ -220,6 +244,7 @@ describe('Matrix.js', () => { expect(matrix.e).toBe(-4) expect(matrix.f).toBe(3) }) + it('performs a flip over the horizontal axis over a given point with two arguments', () => { var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip('x', 150) @@ -229,6 +254,7 @@ describe('Matrix.js', () => { expect(matrix.f).toBe(3) }) }) + describe('with y given', () => { it('performs a flip over the vertical axis with one argument', () => { var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip('y') @@ -238,6 +264,7 @@ describe('Matrix.js', () => { expect(matrix.e).toBe(4) expect(matrix.f).toBe(-3) }) + it('performs a flip over the vertical axis over a given point with two arguments', () => { var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip('y', 100) @@ -247,6 +274,7 @@ describe('Matrix.js', () => { expect(matrix.f).toBe(197) }) }) + describe('with no axis given', () => { it('performs a flip over the horizontal and vertical axis with no argument', () => { var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip() @@ -256,6 +284,7 @@ describe('Matrix.js', () => { expect(matrix.e).toBe(-4) expect(matrix.f).toBe(-3) }) + it('performs a flip over the horizontal and vertical axis over a given point with one argument that represent both coordinates', () => { var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip(100) @@ -264,6 +293,7 @@ describe('Matrix.js', () => { expect(matrix.e).toBe(196) expect(matrix.f).toBe(197) }) + it('performs a flip over the horizontal and vertical axis over a given point with two arguments', () => { var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip(50, 100) @@ -378,4 +408,145 @@ describe('Matrix.js', () => { expect(matrix.f).toBeCloseTo(-81.2931393017) }) }) + + describe('around()', () => { + it('performs a matrix operation around an origin by shifting the origin to 0,0', () => { + const matrix = new Matrix(1, 0, 0, 1, 0, 0).around(10, 10, new Matrix().scale(2)) + + expect(matrix).toEqual(new Matrix(2, 0, 0, 2, -10, -10)) + }) + + it('defaults to around center of 0,0', () => { + const matrix = new Matrix(1, 0, 0, 1, 0, 0).around(0, 0, new Matrix().scale(2)) + + expect(matrix).toEqual(new Matrix(2, 0, 0, 2, 0, 0)) + }) + }) + + describe('equals()', () => { + it('returns true if the same matrix is passed', () => { + const matrix = new Matrix() + expect(matrix.equals(matrix)).toBe(true) + }) + + it('returns true if the components match', () => { + const matrix = new Matrix() + expect(matrix.equals(matrix.clone())).toBe(true) + }) + + it('returns false if the components do not match', () => { + const matrix = new Matrix() + expect(matrix.equals(matrix.scale(2))).toBe(false) + }) + }) + + describe('valueOf()', () => { + it('returns an object containing the matrix components', () => { + const matrix = new Matrix().valueOf() + expect(matrix).not.toEqual(any(Matrix)) + expect(matrix).toEqual({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }) + }) + }) + + describe('toArray', () => { + it('converts matrix to array', () => { + const arr = new Matrix().toArray() + expect(arr).toEqual([ 1, 0, 0, 1, 0, 0 ]) + }) + }) + + describe('static', () => { + describe('fromArray()', () => { + it('creates a matrix like object from an array', () => { + const matrix = Matrix.fromArray([ 1, 2, 3, 4, 5, 6 ]) + expect(matrix).not.toEqual(any(Matrix)) + expect(matrix).toEqual(new Matrix(1, 2, 3, 4, 5, 6).valueOf()) + }) + }) + + describe('isMatrixLike', () => { + it('returns true if object contains all components', () => { + expect(Matrix.isMatrixLike(new Matrix())).toBe(true) + expect(Matrix.isMatrixLike(new Matrix().valueOf())).toBe(true) + expect(Matrix.isMatrixLike({ f: 0 })).toBe(true) + }) + + it('returns false if no component is found', () => { + expect(Matrix.isMatrixLike({ foo: 'bar' })).toBe(false) + }) + }) + + describe('formatTransforms()', () => { + it('formats all transform input varieties to a canonical form', () => { + expect(Matrix.formatTransforms({ + flip: true, + skew: 5, + scale: 5, + originX: 5, + originY: 5, + positionX: 5, + positionY: 5, + translateX: 5, + translateY: 5, + relativeX: 5, + relativeY: 5 + })).toEqual({ scaleX: -5, scaleY: -5, skewX: 5, skewY: 5, shear: 0, theta: 0, rx: 5, ry: 5, tx: 5, ty: 5, ox: 5, oy: 5, px: 5, py: 5 }) + }) + + it('respects flip=x', () => { + expect(Matrix.formatTransforms({ + flip: 'x', + scale: [ 1, 2 ], + skew: [ 1, 2 ] + })).toEqual(objectContaining({ scaleX: -1, scaleY: 2, skewX: 1, skewY: 2 })) + }) + + it('respects flip=y', () => { + expect(Matrix.formatTransforms({ + flip: 'y', + scaleX: 1, + scaleY: 2, + skewX: 1, + skewY: 2 + })).toEqual(objectContaining({ scaleX: 1, scaleY: -2, skewX: 1, skewY: 2 })) + }) + + it('makes position NaN if not passed', () => { + expect(Matrix.formatTransforms({ + flip: 'y', + scaleX: 1, + scaleY: 2, + skewX: 1, + skewY: 2 + })).toEqual(objectContaining({ px: NaN, py: NaN })) + }) + }) + }) + + describe('Element', () => { + describe('ctm()', () => { + it('returns the native ctm wrapped into a matrix', () => { + const rect = new Rect() + const spy = spyOn(rect.node, 'getCTM') + rect.ctm() + expect(spy).toHaveBeenCalled() + }) + }) + + describe('screenCTM()', () => { + it('returns the native screenCTM wrapped into a matrix for a normal element', () => { + const rect = new Rect() + const spy = spyOn(rect.node, 'getScreenCTM') + rect.screenCTM() + expect(spy).toHaveBeenCalled() + }) + + it('does extra work for nested svgs because firefox needs it', () => { + const spy = spyOn(getWindow().SVGGraphicsElement.prototype, 'getScreenCTM') + const svg = SVG().nested() + svg.screenCTM() + expect(spy).toHaveBeenCalled() + }) + }) + }) }) diff --git a/src/types/Matrix.js b/src/types/Matrix.js index c42adf7..9b783da 100644 --- a/src/types/Matrix.js +++ b/src/types/Matrix.js @@ -70,8 +70,9 @@ export default class Matrix { if (isFinite(t.px) || isFinite(t.py)) { const origin = new Point(ox, oy).transform(transformer) // TODO: Replace t.px with isFinite(t.px) - const dx = t.px ? t.px - origin.x : 0 - const dy = t.py ? t.py - origin.y : 0 + // Doesnt work because t.px is also 0 if it wasnt passed + const dx = isFinite(t.px) ? t.px - origin.x : 0 + const dy = isFinite(t.py) ? t.py - origin.y : 0 transformer.translateO(dx, dy) } @@ -80,34 +81,6 @@ export default class Matrix { return transformer } - // Applies a matrix defined by its affine parameters - compose (o) { - if (o.origin) { - o.originX = o.origin[0] - o.originY = o.origin[1] - } - // Get the parameters - var ox = o.originX || 0 - var oy = o.originY || 0 - var sx = o.scaleX || 1 - var sy = o.scaleY || 1 - var lam = o.shear || 0 - var theta = o.rotate || 0 - var tx = o.translateX || 0 - var ty = o.translateY || 0 - - // Apply the standard matrix - var result = new Matrix() - .translateO(-ox, -oy) - .scaleO(sx, sy) - .shearO(lam) - .rotateO(theta) - .translateO(tx, ty) - .lmultiplyO(this) - .translateO(ox, oy) - return result - } - // Decomposes this matrix into its affine parameters decompose (cx = 0, cy = 0) { // Get the parameters from the matrix @@ -351,19 +324,11 @@ export default class Matrix { return this.skew(x, 0, cx, cy) } - skewXO (x, cx, cy) { - return this.skewO(x, 0, cx, cy) - } - // SkewY skewY (y, cx, cy) { return this.skew(0, y, cx, cy) } - skewYO (y, cx, cy) { - return this.skewO(0, y, cx, cy) - } - // Transform around a center point aroundO (cx, cy, matrix) { var dx = cx || 0 @@ -377,6 +342,7 @@ export default class Matrix { // Check if two matrices are equal equals (other) { + if (other === this) return true var comp = new Matrix(other) return closeEnough(this.a, comp.a) && closeEnough(this.b, comp.b) && closeEnough(this.c, comp.c) && closeEnough(this.d, comp.d) @@ -444,7 +410,8 @@ export default class Matrix { var origin = new Point(o.origin || o.around || o.ox || o.originX, o.oy || o.originY) var ox = origin.x var oy = origin.y - var position = new Point(o.position || o.px || o.positionX, o.py || o.positionY) + // We need Point to be invalid if nothing was passed because we cannot default to 0 here. Thats why NaN + var position = new Point(o.position || o.px || o.positionX || NaN, o.py || o.positionY || NaN) var px = position.x var py = position.y var translate = new Point(o.translate || o.tx || o.translateX, o.ty || o.translateY) -- 2.39.5