src/matrix.js
import { rowsToColumns } from './utils';
import { scale } from './functions';
/**
* A class for storing values in rows and columns and for doing
* common matrix operations. As the Vector class, this extends the
* js Array class.
*
* Values are stores columns-first internally, but is normally
* instantiated by supplying values in rows-first order.
*/
export class Matrix extends Array {
/**
* Creates an identity matrix with rows and columns equal to size.
* @param {number} size dimension
* @return {Matrix}
*/
static identity(size = 4) {
const m = new Matrix(size).fill(0);
for (let i = 0; i < m.length; i += size + 1) {
m[i] = 1;
}
return m;
}
/**
* Traverse an array as a matrix, either rows-first or columns first.
* @param {number[]} arr array to traverse as a matrix
* @param {number} cols number of columns to split the array into
* @param {function} callback function to be executed for each element
* @param {boolean} rowsFirst traverse order
*/
static traverse(arr, cols = 1, callback = () => ({}), rowsFirst = true) {
const rows = arr.rows || ~~(arr.length / cols);
if (rowsFirst) {
let n = 0;
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
const idx = c * rows + r;
callback(arr[idx], n++, r, c, idx);
}
}
} else {
arr.forEach((v, i) => callback(v, i, i % cols, ~~(i / rows), i));
}
}
/**
* Instantiate a matrix from an array.
* @param {number[]} arr array of numbers
* @param {number} columns number of columns to split the array into
* @param {boolean} rowsFirst traverse order
* @param {boolean} mutateArgs set if the input array should be mutated or not
* @return {Matrix}
*/
static fromArray(arr, columns = 1, rowsFirst = true, mutateArgs = false) {
const rows = columns === 1 ? arr.length : ~~(arr.length / columns);
return new Matrix(rows, columns, arr, rowsFirst, mutateArgs);
}
/**
* Create a matrix from vectors. Each vector will be a column in the matrix
* and the number of rows are determined by the vector dimension.
* @param {...number[]} vectors
* @return {Matrix}
*/
static fromVectors(...vectors) {
const cols = vectors.length;
const rows = vectors[0].length;
const m = new Matrix(rows, cols);
let n = 0;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
m[n++] = vectors[i][j];
}
}
return m;
}
/**
* Constructs a new instance of Matrix. If cols is omitted, it will mirror the rows
* parameter, making a square matrix.
* @param {number} rows number of rows
* @param {number} cols number of columns
* @param {number[]} values array of numbers to assign to the matrix elements
* @param {boolean} rowsFirst if values should be read rows-first or columns-first
* @param {boolean} mutateArgs set if the input array should be mutated or not
*/
constructor(rows = 4, cols, values, rowsFirst = true, mutateArgs = false) {
cols = cols || rows;
super(rows * cols);
/** internal property to hold number of columns of this matrix */
this._c = cols;
/** internal property to hold number of rows of this matrix */
this._r = rows;
if (values) {
if (values.length === 1 && Array.isArray(values[0])) {
[values] = values;
}
this.copyFrom(values, rowsFirst, mutateArgs);
}
}
/**
* Set values of this matrix from an array
* @param {number[]} values array of numbers
* @param {boolean} rowsFirst if values should be read rows-first or columns-first
* @param {number} mutateArgs
*/
copyFrom(values, rowsFirst = true, mutateArgs = false) {
if (!rowsFirst) {
for (let i = 0; i < Math.min(this.length, values.length); i++) {
/** assign value */
this[i] = values[i];
}
} else {
if (values.length > this.length) {
if (mutateArgs) {
values.length = this.length;
} else {
values = values.slice(0, this.length);
}
}
rowsToColumns(values, this.columns, this);
}
return this;
}
/**
* Traverse this matrix
* @param {function} callback function to be executed for each element
* @param {boolean} rowsFirst if values should be read rows-first or columns-first
* @return {Matrix} returns itself for chaining
*/
traverse(callback = () => ({}), rowsFirst = true) {
Matrix.traverse(this, this.columns, callback, rowsFirst);
return this;
}
/**
* Clone/copy this matrix
* @return {Matrix}
*/
clone() {
return new Matrix(this.rows, this.columns, this, false);
}
/**
* Calculate the internal 1d array index
* @param {number} row row index (zero-based)
* @param {number} col column index (zero-based)
*/
index(row, col) {
return this.rows * col + row;
}
/**
* Get a matrix element value by row and column
* @param {number} row row index (zero-based)
* @param {number} col column index (zero-based)
* @return {number}
*/
get(row, col) {
const idx = this.index(row, col);
return this[idx];
}
/**
* Set a value at the specified row and column
* @param {number} row row index (zero-based)
* @param {number} col column index (zero-based)
* @param {number} value value to set
* @return {Matrix} returns itself for chaining
*/
set(row, col, value) {
const idx = this.index(row, col);
/** assign value */
this[idx] = value;
return this;
}
/**
* Calculate the matrix matrix product between this matrix and the passed matrices.
* in the order they appear.
*
* The left hand matrix number of columns must always match the right hand matrix number of rows.
* @param {...Matrix} matrices
* @return {Matrix}
*/
dotMat(...matrices) {
let m = this;
let target = this;
matrices.forEach((other) => {
target = m.clone();
if (m.columns !== other.rows) throw Error('Columns of left hand matrix must match rows of right hand matrix!');
let idx = 0;
for (let c = 0; c < other.columns; c++) {
for (let r = 0; r < m.rows; r++) {
let sum = 0;
for (let n = 0; n < m.columns; n++) {
sum += m.get(r, n) * other.get(n, c);
}
target[idx++] = sum;
}
}
m = target;
});
return target;
}
/**
* Calculate the matrix vector product between this matrix and the passed arrays/vectors.
*
* Since it is common to use homogeneous coordinates when doing matrix-vector multiplication,
* i.e. use 4x4 matrix for transforming 3d vectors, the vectors will be treated as if they had
* the same number of components as the matrix has columns. For vectors with a lower dimension
* than the number of columns in the matrix, the last component will be iterpreted as 1 and any
* in between as 0. In the opposite case, it will simply neglect excessive components.
* @param {number[]} vec vector (as Array/Vector)
* @param {Vector|number[]} target optional array/vector to avoid mutating the vec argument
* @return {number[]} resulting vector (type depending on vec/target argument)
*/
dotVec(vec, target = null) {
if (target) { // optimized for immutable input vector
for (let c = 0; c < this.columns; c++) {
const n = c * this.rows;
for (let r = 0; r < target.length; r++) {
if (c === 0) target[r] = 0;
let compVal = 0;
if (c < vec.length) {
compVal = vec[c];
} else if (c === this.rows - 1) {
compVal = 1;
}
target[r] += this[n + r] * compVal;
}
}
return target;
}
// optimized for mutation of input vector
const comp = new Array(vec.length);
for (let i = 0; i < vec.length; i++) {
comp[i] = vec[i];
vec[i] = 0;
}
for (let c = 0; c < this.columns; c++) {
const n = c * this.rows;
for (let r = 0; r < vec.length; r++) {
let compVal = 0;
if (c < comp.length) {
compVal = comp[c];
} else if (c === this.rows - 1) {
compVal = 1;
}
vec[r] += this[n + r] * compVal;
}
}
if (vec.length > this.rows) {
vec.length = this.rows;
}
return vec;
}
/**
* Calculate the matrix-matrix or matrix-vector product, depending on the type of
* the argument passed to the other-parameter.
* @param {number[]|Vector|Matrix} other {Array/Vector/Matrix}
* @param {Vector|number[]} target optional array/vector to avoid mutating input argument
* @return {Matrix|Vector|number[]} depending on input.
*/
dot(other, target = null) {
if (other instanceof Matrix) {
return this.dotMat(other);
}
return this.dotVec(other, target);
}
/**
* Get the values from a specific column.
* @param {number} j zero-based column index
* @param {Vector|number[]} target optional array/vector to write values to
* @return {number[]} column values
*/
col(j, target = null) {
target = target || new Array(this.rows);
const idx = j * this.rows;
for (let i = idx; i < idx + this.rows; i++) {
target[i - idx] = this[i];
}
return target;
}
/**
* Get the values from a specific row.
* @param {number} i zero-based row index
* @param {Vector|number[]} target optional array/vector to write values to
* @return {number[]} row values
*/
row(i, target = null) {
target = target || new Array(this.columns);
for (let j = 0; j < this.columns; j++) {
target[j] = this[i + j * this.rows];
}
return target;
}
/**
* Create a submatrix from this matrix.
* @param {number} row zero-based row index
* @param {number} col zero-based column index
* @param {number} rows number of rows to include
* @param {number} cols number of columns to include
* @param {Matrix|number[]} target optional array/matrix to write values to
* @return {Matrix} submatrix
*/
submatrix(row = 0, col = 0, rows = 2, cols = 2, target = null) {
const start = this.index(row, col);
const end = start + cols * this.rows;
let subm;
if (target) {
if (target instanceof Matrix) {
target._c = cols;
target._r = rows;
target.length = rows * cols;
}
subm = target;
} else {
subm = new Matrix(rows, cols);
}
let k = 0;
for (let i = start; i < end; i += this.rows) {
for (let j = 0; j < rows; j++) {
subm[k++] = this[i + j];
}
}
return subm;
}
/**
* Remove a row and/or column from this matrix
* @param {number} row zero-based row index (-1 or null to omit)
* @param {number} col zero-based column index (-1 or null to omit)
* @param {Matrix} target optional matrix to avoid mutating this matrix
* @return {Matrix} reduced matrix
*/
remove(row = null, col = null, target = null) {
if (target && target instanceof Matrix) {
target._r = this._r;
target._c = this._c;
target.length = this.length;
target.copyFrom(this, false);
} else {
target = this;
}
let idx;
if (Number.isFinite(col) && col >= 0 && col < target.columns) {
idx = col * target.rows;
target.splice(idx, target.rows);
target._c--;
}
if (Number.isFinite(row) && row >= 0 && row < target.rows) {
for (let r = 0; r < target.columns; r++) {
idx = r * target.rows + row - r;
target.splice(idx, 1);
}
target._r--;
}
return target;
}
/**
* Transpose this matrix (switch rows and columns)
* @param {Matrix|number[]} target optional array/matrix to avoid mutating this matrix
* @return {Matrix} transposed matrix
*/
transpose(target = null) {
const cols = this.rows;
if (!target) {
target = this;
} else {
target.length = this.length;
}
if (target instanceof Matrix) {
target._r = this._c;
target._c = cols;
}
rowsToColumns(this, cols, target);
return target;
}
/**
* Invert this matrix (where applicable)
* @param {Matrix} target optional matrix to avoid mutating this matrix
* @return {Matrix} transposed matrix
*/
inverse(target = null) {
if (!this.isSquare) throw Error('Matrix must be square!');
const dim = this.rows;
if (target && target instanceof Matrix) {
target._c = this.columns;
target._r = this.rows;
target.length = this.length;
target.copyFrom(this, false);
} else {
target = this;
}
const src = Matrix.identity(dim);
for (let i = 0; i < dim; i++) {
const d = target.index(i, i);
let e = target[d];
// if we have a 0 on the diagonal (we'll need to swap with a lower row)
if (e === 0) {
// look through every row below the i'th row
for (let ii = i + 1; ii < dim; ii += 1) {
// if the ii'th row has a non-0 in the i'th col
if (target.get(ii, i) !== 0) {
// it would make the diagonal have a non-0 so swap it
for (let j = 0; j < dim; j++) {
const ij = target.index(i, j);
const iij = target.index(ii, j);
e = target[ij]; // temp store i'th row
target[ij] = target[iij]; // replace i'th row by ii'th
target[iij] = e; // repace ii'th by temp
e = src[ij]; // temp store i'th row
src[ij] = src[iij]; // replace i'th row by ii'th
src[iij] = e; // repace ii'th by temp
}
// don't bother checking other rows since we've swapped
break;
}
}
// get the new diagonal
e = target[d];
// if it's still 0, not srcertable (error)
if (e === 0) return undefined;
}
// Scale this row down by e (so we have a 1 on the diagonal)
for (let j = 0; j < dim; j++) {
const ij = target.index(i, j);
target[ij] /= e; // apply to original thisrix
src[ij] /= e; // apply to identity
}
// Subtract this row (scaled appropriately for each row) from ALL of
// the other rows so that there will be 0's in this column in the
// rows above and below this one
for (let ii = 0; ii < dim; ii++) {
// Only apply to other rows (we want a 1 on the diagonal)
if (ii === i) continue;
// We want to change this element to 0
e = target.get(ii, i);
// Subtract (the row above(or below) scaled by e) from (the
// current row) but start at the i'th column and assume all the
// stuff left of diagonal is 0 (which it should be if we made this
// algorithm correctly)
for (let j = 0; j < dim; j++) {
const ij = target.index(i, j);
const iij = target.index(ii, j);
target[iij] -= e * target[ij]; // apply to original matrix
src[iij] -= e * src[ij]; // apply to identity
}
}
}
return src;
}
/**
* Calculate this matrix's determinant (where applicable)
* @return {number} the determinant
*/
determinant() {
if (!this.isSquare) throw Error('Matrix must be square!');
if (this.rows === 2) {
return this[0] * this[3] - this[2] * this[1];
} else if (this.rows === 3) {
return this[0] * this[4] * this[8] - this[6] * this[4] * this[2] +
this[3] * this[7] * this[2] - this[0] * this[7] * this[5] +
this[6] * this[1] * this[5] - this[3] * this[1] * this[8];
}
function determinantRec(m) {
if (m.length === 1) return m[0];
let d = 0;
for (let c = 0; c < m.columns; c++) {
const v = m[c * m.rows];
if (v === 0) continue;
const sm = m.remove(0, c, new Matrix());
let cofactor = determinantRec(sm);
if (c % 2 === 1) {
cofactor = -cofactor;
}
d += v * cofactor;
}
return d;
}
return determinantRec(this);
}
/**
* Scale this vector by a factor
* @param {number} factor scaling factor
* @param {Matrix|number[]} target optional array/matrix to avoid mutating this matrix
* @return {Matrix}
*/
scale(factor, target = null) {
return scale(this, factor, target || this);
}
/**
* Returns a native javascript Array representation of this matrix, in rows-first order
* @return {number[]}
*/
columnsFirst() {
return [...this];
}
/**
* Returns a native javascript Array representation of this matrix, in column-first order
* @return {number[]}
*/
rowsFirst() {
const res = new Array(this.length);
rowsToColumns(this, this.rows, res);
return res;
}
/**
* Return a native javascript Array representation of this matrix.
* @param {boolean} rowsFirst if the output should be rows-first or columns-first
* @return {number[]}
*/
toArray(rowsFirst = false) {
return rowsFirst ? this.rowsFirst() : this.columnsFirst();
}
/**
* Returns a 2d native javascript Array representation of this matrix.
* @param {boolean} rowsFirst if the output should be rows-first or columns-first
* @return {Array<Array>}
*/
toArray2d(rowsFirst = false) {
const res = new Array(rowsFirst ? this.rows : this.columns);
const cf = rowsFirst ?
(v, i, r, c) => {
if (!res[r]) res[r] = new Array(this.columns);
res[r][c] = v;
} :
(v, i, r, c) => {
if (!res[c]) res[c] = new Array(this.rows);
res[c][r] = v;
};
this.traverse(cf);
return res;
}
/**
* Get the number of columns in this matrix.
* @return {number}
*/
get columns() {
return this._c;
}
/**
* Get the number of rows in this matrix.
* @return {number}
*/
get rows() {
return this._r;
}
/**
* This function returns true if this matrix has the same number of rows as columns,
* otherwise false.
* @return {boolan}
*/
get isSquare() {
return this.rows === this.columns;
}
}
// add extra accessors
for (let i = 1; i <= 4; i++) {
for (let j = 1; j <= 4; j++) {
Object.defineProperty(Matrix.prototype, `a${i}${j}`, {
get() {
return this.get(i - 1, j - 1);
},
set(v) {
this.set(i - 1, j - 1, v);
},
});
}
}
/**
* Factory function for creating and assigning a matrix from an array.
* The array must be 1 dimensional, and will be split into rows and columns based
* on the specified cols parameter. If array length divided by cols doesn't add up
* to a whole number, then only floor(args.length / cols) values will be read from
* the input array.
*
* Initial values are passed in row-first order by default, but can optionally
* be passed in columns-first by setting the rowFirst argument to false.
* @param {...number} arr initial values
* @param {number} cols number of columns to split the array into
* @param {boolean} rowsFirst read arr in rows-first or column-first order
* @return {Matrix}
*/
export function mat(arr, cols, rowsFirst = true) {
return Matrix.fromArray(arr, cols, rowsFirst, true);
}
/**
* Factory function for creating and assigning a 2x2 matrix.
* Initial values are passed in row-first order.
* @param {...number} args initial values
* @return {Matrix}
*/
export function mat2(...args) {
return new Matrix(2, 2, args, true, true);
}
/**
* Factory function for creating and assigning a 3x3 matrix.
* Initial values are passed in row-first order.
* @param {...number} args initial values
* @return {Matrix}
*/
export function mat3(...args) {
return new Matrix(3, 3, args, true, true);
}
/**
* Factory function for creating and assigning a 4x4 matrix.
* Initial values are passed in row-first order.
* @param {...number} args initial values
* @return {Matrix}
*/
export function mat4(...args) {
return new Matrix(4, 4, args, true, true);
}