test/matrix.spec.js
/* eslint-disable comma-spacing */
import expect from 'expect';
import { Matrix, mat2, mat3, mat4 } from '../src/matrix';
import { Vector } from '../src/vector';
describe('matrix.js', () => {
it('should be possible to instantiate and assign values using factory functions', () => {
expect(new Matrix().length).toBe(16);
expect(new Matrix(2).length).toBe(4);
expect(new Matrix(3).length).toBe(9);
expect(new Matrix(4).length).toBe(16);
let m = new Matrix(4, 2);
expect(m.length).toBe(8);
expect(m.columns).toBe(2);
expect(m.rows).toBe(4);
expect(m.isSquare).toBeFalsy();
m.copyFrom([
1, 2,
3, 4,
5, 6,
7, 8,
]);
expect(m.toArray()).toEqual([1, 3, 5, 7, 2, 4, 6, 8]);
m = new Matrix(2, 2, [1, 0, 0, 1]);
expect(m).toEqual(Matrix.identity(2));
expect(mat2(1, 2, 3, 4, 5, 6, 7).toArray()).toEqual([1, 3, 2, 4]);
m = mat4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
expect(m.length).toBe(16);
expect(m.rows).toBe(4);
expect(m.columns).toBe(4);
expect(m.toArray()).toEqual([1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
// values should be stored internally in columns-first order
// elements can be accessed (up to 4d) using the property
// syntax: a{row}{column}
expect(m.a11).toBe(m[0]);
expect(m.a21).toBe(m[1]);
expect(m.a31).toBe(m[2]);
expect(m.a41).toBe(m[3]);
expect(m.a12).toBe(m[4]);
expect(m.a22).toBe(m[5]);
expect(m.a32).toBe(m[6]);
expect(m.a42).toBe(m[7]);
expect(m.a13).toBe(m[8]);
expect(m.a23).toBe(m[9]);
expect(m.a33).toBe(m[10]);
expect(m.a43).toBe(m[11]);
expect(m.a14).toBe(m[12]);
expect(m.a24).toBe(m[13]);
expect(m.a34).toBe(m[14]);
expect(m.a44).toBe(m[15]);
// assign new value to row 3 column 2
m.a32 = 20;
expect(m[6]).toBe(20);
// asign new value to row 2 column 3
m[9] = 14;
expect(m.a23).toBe(14);
});
it('should be possible to instantiate matrices from existing arrays', () => {
// identity matrix
const I = Matrix.identity(3);
expect(I.isSquare).toBeTruthy();
expect(I.toArray()).toEqual([1, 0, 0, 0, 1, 0, 0, 0, 1]);
// from existing array
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
expect(Matrix.fromArray(arr, 2).toArray2d(true)).toEqual([
[1, 2],
[3, 4],
[5, 6],
[7, 8],
]);
expect(Matrix.fromArray(arr, 3).toArray2d(true)).toEqual([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]);
// mutates input arr by reducing its size to fit the matrix
expect(Matrix.fromArray(arr, 4, true, true).toArray2d(true)).toEqual([
[1, 2, 3, 4],
[5, 6, 7, 8],
]);
expect(arr.length).toBe(8);
});
it('should be possible to instantiate matrices from vectors', () => {
const i = [1, 0, 0.5];
const j = [0, 1, 0];
const k = [0, 0, 1];
expect(Matrix.fromVectors(i, j, k).toArray(true)).toEqual([
1, 0, 0,
0, 1, 0,
0.5, 0, 1,
]);
});
it('should be able to extract values or subsets of values', () => {
const m = mat3(1, 2, 3, 4, 5, 6, 7, 8, 9);
// columns first (as stored internally)
const columnsFirst = m.toArray();
expect(columnsFirst).toEqual([1, 4, 7, 2, 5, 8, 3, 6, 9]);
expect(columnsFirst).not.toBe(m);
expect(m.columnsFirst()).toEqual(columnsFirst);
// rows first
const rowsFirst = m.toArray(true);
expect(rowsFirst).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(rowsFirst).not.toBeInstanceOf(Matrix);
expect(m.rowsFirst()).toEqual(rowsFirst);
/* Transform to 2d array */
// columns first
expect(m.toArray2d()).toEqual([[1, 4, 7], [2, 5, 8], [3, 6, 9]]);
// rows first
expect(m.toArray2d(true)).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
/* Columns and rows */
expect(m.col(0)).toEqual([1, 4, 7]);
expect(m.col(1)).toEqual([2, 5, 8]);
expect(m.col(2)).toEqual([3, 6, 9]);
expect(m.row(0)).toEqual([1, 2, 3]);
expect(m.row(1)).toEqual([4, 5, 6]);
expect(m.row(2)).toEqual([7, 8, 9]);
/* sub-matrix */
let sm = m.submatrix(1, 0, 2, 1);
expect(sm.rows).toBe(2);
expect(sm.columns).toBe(1);
expect(sm.toArray(true)).toEqual([4, 7]);
sm = m.submatrix(1, 1, 2, 2, new Matrix(2, 2));
expect(sm.rows).toBe(2);
expect(sm.columns).toBe(2);
expect(sm.toArray(true)).toEqual([5, 6, 8, 9]);
});
it('should be possible to transpose a matrix', () => {
let m = mat3(1, 2, 3, 4, 5, 6, 7, 8, 9);
let transposed = m.transpose();
expect(m).toBe(transposed);
expect(m.toArray()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// immutable
transposed = m.transpose(new Matrix());
expect(m).not.toBe(transposed);
expect(m.toArray()).not.toEqual(transposed.toArray());
m = new Matrix(3, 2, [
1, 2,
3, 4,
5, 6,
]);
expect(m.rows).toBe(3);
expect(m.columns).toBe(2);
transposed = m.transpose(new Matrix());
expect(transposed.rows).toBe(2);
expect(transposed.columns).toBe(3);
expect(transposed.toArray2d(true)).toEqual([ // rows first
[1, 3, 5],
[2, 4, 6],
]);
});
it('should be possible to remove rows/colums from a matrix', () => {
let m = mat3(1, 2, 3, 4, 5, 6, 7, 8, 9);
m.remove(1);
expect(m.rows).toBe(2);
expect(m.columns).toBe(3);
expect(m.toArray(true)).toEqual([
1, 2, 3,
7, 8, 9,
]);
m = mat3(1, 2, 3, 4, 5, 6, 7, 8, 9);
m.remove(null, 1);
expect(m.rows).toBe(3);
expect(m.columns).toBe(2);
expect(m.toArray(true)).toEqual([
1, 3,
4, 6,
7, 9,
]);
m = mat3(1, 2, 3, 4, 5, 6, 7, 8, 9);
m.remove(2, 1);
expect(m.rows).toBe(2);
expect(m.columns).toBe(2);
expect(m.toArray(true)).toEqual([
1, 3,
4, 6,
]);
// immutable
m = mat3(1, 2, 3, 4, 5, 6, 7, 8, 9);
const m2 = m.remove(1, 1, new Matrix());
expect(m.rows).toBe(3);
expect(m.columns).toBe(3);
expect(m2.rows).toBe(2);
expect(m2.columns).toBe(2);
expect(m2.toArray(true)).toEqual([
1, 3,
7, 9,
]);
});
it('should be possible to calculate the product between two matrices as well as matrix and vector/array', () => {
const S = mat3(
2, 0, 0,
0, 2, 0,
0, 0, 1,
);
const T = mat3(
1, 0, 2,
0, 1,-3,
0, 0, 1,
);
let v = new Vector(2, -1.5);
const a = S.dot(T, mat3());
const b = S.dot(v, new Vector(2));
const c = S.dot([-3, 2]);
expect(a).toBeInstanceOf(Matrix);
expect(a.rowsFirst()).toEqual([
2, 0, 4,
0, 2, -6,
0, 0, 1,
]);
expect(b).toBeInstanceOf(Vector);
expect(b).toEqual([4, -3]);
expect(c).toBeInstanceOf(Array);
expect(c).toEqual([-6, 4]);
const M = mat4(
1, 0, 0, -4,
0, 1, 0, 2,
0, 0, 1, 1,
0, 0, 0, 1,
);
v = new Vector(-250, -250, 1);
// mutates input array
expect(M.dot([-250, -250, 1, 1])).toEqual([-254, -248, 2, 1]);
// To allow affine transformations supporting translations
// you need to use 4d matrices in 3d space and 3d matrices
// in 2d space etc. This lib will treat any vector as
// if it had the same number of components as the matrix has
// rows, i.e. it fills in the blanks or truncate its size.
// immutable: M4 * V3 = V4
expect(M.dot(v, new Vector(4))).toEqual([-254, -248, 2, 1]);
// immutable: M4 * V3 = V3
expect(M.dot(v, new Vector(3))).toEqual([-254, -248, 2]);
// mutable: M4 * V3 = V3
expect(M.dot(v)).toEqual([-254, -248, 2]);
});
it('should be able to find the inverse of a (square) matrix', () => {
expect(() => new Matrix(3, 2).inverse()).toThrow();
expect(() => Matrix.identity().inverse()).not.toThrow();
expect(Matrix.identity(4).inverse()).toEqual(Matrix.identity(4));
let actual = mat2(3, 3.5, 3.2, 3.6).inverse();
let expected = [-9, 8.75, 8, -7.5];
actual.every((a, i) => expect(a).toBeCloseTo(expected[i], 5));
actual = mat2(4, 7, 2, 6).inverse();
expected = [0.6, -0.7, -0.2, 0.4];
actual.every((a, i) => expect(a).toBeCloseTo(expected[i], 5));
// imutable
const m = mat4(
2, 0, 0, 0,
0, 1.5, 0, 0,
0, 0, 3, 0,
0, 0, 0, 1,
);
actual = m.inverse(new Matrix());
expect(m).not.toBe(actual);
expect(m.toArray(true)).toEqual([
2, 0, 0, 0,
0, 1.5, 0, 0,
0, 0, 3, 0,
0, 0, 0, 1,
]);
expected = [0.5, 0, 0, 0, 0, 0.666667, 0, 0, 0, 0, 0.333333, 0, 0, 0, 0, 1];
actual.every((a, i) => expect(a).toBeCloseTo(expected[i], 5));
expect(mat2(0, -1, 2, 4).inverse().toArray(true)).toEqual([2, 0.5, -1, -0]);
});
it('should be able to calculate its determinant', () => {
expect(() => new Matrix(3, 2).determinant()).toThrow();
expect(Matrix.identity().determinant()).toBe(1);
expect(mat2(1, 3, 2, -1).determinant()).toBe(-7);
expect(mat3(-2, 2, 3, -1, 1, 3, 2, 0, -1).determinant()).toBe(6);
expect(mat4(1, 3, 5, 9, 1, 3, 1, 7, 4, 3, 9, 7, 5, 2, 0, 9).determinant()).toBe(-376);
expect(mat4(
1, 2, 3, -4,
-5, -6, 7, 8,
9, 10, 11, 12,
-13, 14, 15, 16,
).determinant()).toBe(25344);
});
});