UNPKG

@bschlenk/mat

Version:

Functional affine transform matrix library

235 lines (234 loc) 7.09 kB
import { areClose, DEG2RAD } from '@bschlenk/util'; /** * The identity matrix. */ export const IDENTITY = mat(1, 0, 0, 1, 0, 0); /** * Create a new Matrix, passing values in column-major order. * Some reference material may refer to the values as a, b, c, d, e, f, * but I find giving these values more descriptive names makes it easier * to remember what they do. */ export function mat(xx, xy, yx, yy, tx, ty) { return { xx, xy, yx, yy, tx, ty }; } /** * Create a new matrix that translates by the given values. */ export function translate(x, y) { return mat(1, 0, 0, 1, x, y); } /** * Create a new matrix that rotates by the given angle, in radians. */ export function rotate(angle) { const cos = Math.cos(angle); const sin = Math.sin(angle); return mat(cos, sin, -sin, cos, 0, 0); } /** * Create a new matrix that rotates by the given angle, in degrees. */ export function rotateDeg(angle) { return rotate(angle * DEG2RAD); } /** * Create a new matrix that rotates by the given angle, in radians, * around the given point. */ export function rotateAt(angle, cx, cy) { return transformAt(rotate(angle), cx, cy); } /** * Create a new matrix that rotates by the given angle, in degrees, * around the given point. */ export function rotateDegAt(angle, cx, cy) { return rotateAt(angle * DEG2RAD, cx, cy); } /** * Determine the rotation of the given matrix. * * This is the angle in radians that the x axis makes with origin. */ export function getRotation(m) { return Math.atan2(m.xy, m.xx); } /** * Create a new matrix that scales by the given values. The second argument * can be omitted to scale by the same value in both dimensions. */ export function scale(x, y = x) { return mat(x, 0, 0, y, 0, 0); } /** * Create a new matrix that scales by the given values, around the given point. */ export function scaleAt(x, y, cx, cy) { return transformAt(scale(x, y), cx, cy); } /** * Multiply an arbitrary number of matrices together. */ export function mult(...matrices) { if (matrices.length === 0) return IDENTITY; let m = matrices[0]; for (let i = 1; i < matrices.length; ++i) { m = mult2(m, matrices[i]); } return m; } /** * Compute the determinant of the given matrix. */ export function determinant(m) { return m.xx * m.yy - m.xy * m.yx; } /** * Invert the given matrix, if possible. If the matrix is not invertible, * `null` is returned. */ export function invert(m) { const det = determinant(m); if (det === 0) return null; return mat(m.yy / det, -m.xy / det, -m.yx / det, m.xx / det, (m.yx * m.ty - m.yy * m.tx) / det, (m.xy * m.tx - m.xx * m.ty) / det); } /** * Rounds any values in the given matrix that are within `epsilon` of the value * obtained by calling `Math.round` on them. */ export function round(m, epsilon = Number.EPSILON) { const xx = roundEpsilon(m.xx, epsilon); const xy = roundEpsilon(m.xy, epsilon); const yx = roundEpsilon(m.yx, epsilon); const yy = roundEpsilon(m.yy, epsilon); const tx = roundEpsilon(m.tx, epsilon); const ty = roundEpsilon(m.ty, epsilon); if (xx === m.xx && xy === m.xy && yx === m.yx && yy === m.yy && tx === m.tx && ty === m.ty) { return m; } return mat(xx, xy, yx, yy, tx, ty); } /** * Transform a point by the given matrix. * * Essentially converts a "matrix space" point to "world space". */ export function transformPoint(m, v) { return { x: m.xx * v.x + m.yx * v.y + m.tx, y: m.xy * v.x + m.yy * v.y + m.ty, }; } /** * Transform a point by the inverse of the given matrix. If the matrix is not * invertible, returns `null`. * * Essentially converts a "world space" point to "matrix space". * * Note that if you have multiple points to inverse transform by the same * matrix, you're better off first storing the inverse matrix and then using * `transformPoint` on each point, to avoid inverting the matrix multiple times. */ export function inverseTransformPoint(m, v) { const mi = invert(m); return mi ? transformPoint(mi, v) : null; } export function equals(a, b, epsilon = Number.EPSILON) { return (areClose(a.xx, b.xx, epsilon) && areClose(a.xy, b.xy, epsilon) && areClose(a.yx, b.yx, epsilon) && areClose(a.yy, b.yy, epsilon) && areClose(a.tx, b.tx, epsilon) && areClose(a.ty, b.ty, epsilon)); } export function isIdentity(m) { return equals(m, IDENTITY); } /** * Check if all values in the given matrix are not NaN or Infinity. */ export function isValid(m) { return Object.values(m).every((v) => !Number.isNaN(v) && Number.isFinite(v)); } /** * Converts any -0 values in the given matrix to 0. */ export function fixNegativeZeros(m) { const xx = fixNegativeZero(m.xx); const xy = fixNegativeZero(m.xy); const yx = fixNegativeZero(m.yx); const yy = fixNegativeZero(m.yy); const tx = fixNegativeZero(m.tx); const ty = fixNegativeZero(m.ty); if (Object.is(xx, m.xx) && Object.is(xy, m.xy) && Object.is(yx, m.yx) && Object.is(yy, m.yy) && Object.is(tx, m.tx) && Object.is(ty, m.ty)) { return m; } return mat(xx, xy, yx, yy, tx, ty); } /** * Generate a css transform property value from a Matrix. * * The only difference between this and `toSvg` is that the values * are separated by commas instead of spaces. */ export function toCss(m) { return `matrix(${m.xx}, ${m.xy}, ${m.yx}, ${m.yy}, ${m.tx}, ${m.ty})`; } /** * Generate an SVG transform attribute from a Matrix. * * The only difference between this and `toCss` is that the values * are separated by spaces instead of commas. */ export function toSvg(m) { return `matrix(${m.xx} ${m.xy} ${m.yx} ${m.yy} ${m.tx} ${m.ty})`; } /** * Calls the given canvas context's `transform` method * with the values from the given matrix. */ export function toCanvas(m, ctx) { ctx.transform(m.xx, m.xy, m.yx, m.yy, m.tx, m.ty); } /** * Create a new matrix from an instance of a DOMMatrix object. These can be * obtained by calling `getTransform()` on a canvas context. */ export function fromDomMatrix(m) { return mat(m.a, m.b, m.c, m.d, m.e, m.f); } // Helpers function mult2(a, b) { return mat(a.xx * b.xx + a.yx * b.xy, a.xy * b.xx + a.yy * b.xy, a.xx * b.yx + a.yx * b.yy, a.xy * b.yx + a.yy * b.yy, a.xx * b.tx + a.yx * b.ty + a.tx, a.xy * b.tx + a.yy * b.ty + a.ty); } function transformAt(m, centerX, centerY) { return mult(translate(centerX, centerY), m, translate(-centerX, -centerY)); } /** * Rounds the given value if it is within `epsilon` of the value obtained by * calling `Math.round` on it. */ function roundEpsilon(value, epsilon = Number.EPSILON) { const r = Math.round(value); return Math.abs(value - r) < epsilon ? r : value; } function fixNegativeZero(value) { return isNegativeZero(value) ? 0 : value; } function isNegativeZero(value) { return Object.is(value, -0); } //# sourceMappingURL=index.js.map