onsight
Version:
Interactive, easy to use JavaScript game framework.
275 lines (237 loc) • 8.97 kB
JavaScript
// https://stackoverflow.com/questions/45159314/decompose-2d-transformation-matrix
import { Vector2 } from './Vector2.js';
/**
* 2D 3x2 transformation matrix, values of the matrix are stored as numeric array.
* [ 0 Xx 2 Yx 4 Ox ]
* [ 1 Xy 3 Yy 5 Oy ]
*/
class Matrix2 {
constructor(...values) {
this.m = [ 1, 0, 0, 1, 0, 0 ];
if (values && values.length > 0) {
if (Array.isArray(values[0])) this.set(...values[0])
else this.set(...values);
}
}
set(m0, m1, m2, m3, m4, m5) {
this.m[0] = m0;
this.m[1] = m1;
this.m[2] = m2;
this.m[3] = m3;
this.m[4] = m4;
this.m[5] = m5;
return this;
}
copy(mat) {
if (mat && mat instanceof Matrix2) this.set(...mat.m);
return this;
}
clone() {
return new Matrix2(...this.m);
}
/** Reset this matrix to identity */
identity() {
return this.set(1, 0, 0, 1, 0, 0);
}
/** Multiply another matrix by this one and store the result */
multiply(mat) {
const m0 = this.m[0] * mat.m[0] + this.m[2] * mat.m[1];
const m1 = this.m[1] * mat.m[0] + this.m[3] * mat.m[1];
const m2 = this.m[0] * mat.m[2] + this.m[2] * mat.m[3];
const m3 = this.m[1] * mat.m[2] + this.m[3] * mat.m[3];
const m4 = this.m[0] * mat.m[4] + this.m[2] * mat.m[5] + this.m[4];
const m5 = this.m[1] * mat.m[4] + this.m[3] * mat.m[5] + this.m[5];
return this.set(m0, m1, m2, m3, m4, m5);
}
/** Premultiply another matrix by this one and store the result */
premultiply(mat) {
const m0 = mat.m[0] * this.m[0] + mat.m[2] * this.m[1];
const m1 = mat.m[1] * this.m[0] + mat.m[3] * this.m[1];
const m2 = mat.m[0] * this.m[2] + mat.m[2] * this.m[3];
const m3 = mat.m[1] * this.m[2] + mat.m[3] * this.m[3];
const m4 = mat.m[0] * this.m[4] + mat.m[2] * this.m[5] + mat.m[4];
const m5 = mat.m[1] * this.m[4] + mat.m[3] * this.m[5] + mat.m[5];
return this.set(m0, m1, m2, m3, m4, m5);
}
/**
* Compose this transformation matrix with position scale and rotation and origin point.
* @param {number} px Position X
* @param {number} py Position Y
* @param {number} sx Scale X
* @param {number} sy Scale Y
* @param {number} rot Rotation angle (in radians)
*/
compose(px, py, sx, sy, rot) {
// Reset to Identity
this.identity();
// Translation (Position)
this.multiply(_translate.set(1, 0, 0, 1, px, py));
// Rotation (Clockwise)
if (rot !== 0) {
const c = Math.cos(rot);
const s = Math.sin(rot);
this.multiply(_rotate.set(c, s, -s, c, 0, 0));
}
// Scale
if (sx !== 1 || sy !== 1) this.scale(sx, sy);
return this;
}
decompose(object) {
if (!object || typeof object !== 'object') return this;
if (object.position) this.getPosition(object.position);
object.rotation = this.getRotation();
if (object.scale) this.getScale(object.scale);
return this;
}
/** Set the position of the transformation matrix */
setPosition(x, y) {
this.m[4] = x;
this.m[5] = y;
return this;
}
/** Apply translation to this matrix (adds position to transformation already in the matrix) */
translate(x, y) {
if (typeof x === 'object') {
this.m[4] += this.m[0] * x.x + this.m[2] * x.y;
this.m[5] += this.m[1] * x.x + this.m[3] * x.y;
} else {
this.m[4] += this.m[0] * x + this.m[2] * y;
this.m[5] += this.m[1] * x + this.m[3] * y;
}
return this;
}
/** Apply rotation to this matrix in radians */
rotate(rad) {
const c = Math.cos(rad);
const s = Math.sin(rad);
const m11 = this.m[0] * c + this.m[2] * s;
const m12 = this.m[1] * c + this.m[3] * s;
const m21 = this.m[0] * -s + this.m[2] * c;
const m22 = this.m[1] * -s + this.m[3] * c;
this.m[0] = m11;
this.m[1] = m12;
this.m[2] = m21;
this.m[3] = m22;
return this;
}
/** Apply scale to this matrix */
scale(x, y) {
if (typeof x === 'object') {
this.m[0] *= x.x;
this.m[1] *= x.x;
this.m[2] *= x.y;
this.m[3] *= x.y;
} else {
if (y == undefined) y = x;
this.m[0] *= x;
this.m[1] *= x;
this.m[2] *= y;
this.m[3] *= y;
}
return this;
}
/** Apply skew to this matrix */
skew(radianX, radianY) {
return this.multiply(_skew.set(1, Math.tan(radianY), Math.tan(radianX), 1, 0, 0));
}
/** Extract the scale from the transformation matrix */
getScale(target = new Vector2()) {
const scaleX = Math.sqrt(this.m[0] * this.m[0] + this.m[1] * this.m[1]);
const scaleY = Math.sqrt(this.m[2] * this.m[2] + this.m[3] * this.m[3]);
target.set(scaleX, scaleY);
return target;
}
getPosition(target = new Vector2()) {
target.set(this.m[4], this.m[5]);
return target;
}
getRotation() {
return Math.atan2(this.m[1], this.m[0]);
}
getSkew(target = new Vector2()) {
const scaleX = Math.sqrt(this.m[0] * this.m[0] + this.m[1] * this.m[1]);
const scaleY = Math.sqrt(this.m[2] * this.m[2] + this.m[3] * this.m[3]);
const skewX = Math.atan2(-this.m[2] / scaleY, this.m[0] / scaleX);
const skewY = Math.atan2( this.m[1] / scaleX, this.m[3] / scaleY);
target.set(skewX, skewY);
return target;
}
getSign(target = new Vector2()) {
const signX = (this.m[0] < 0) ? -1 : 1;
const signY = (this.m[3] < 0) ? -1 : 1;
target.set(signX, signY);
return target;
}
/**
* The determinant of a 2D transformation matrix can indicate if the transformation includes a reflection.
* A negative determinant means the transformation includes a mirroring.
*/
determinant() {
return this.m[0] * this.m[3] - this.m[1] * this.m[2];
}
/** Get the inverse matrix */
getInverse(mat = new Matrix2()) {
const d = this.determinant();
if (d === 0) console.error(`Matrix2.getInverse(): Matrix is non-invertible`);
const invD = 1 / d;
const m0 = this.m[3] * invD;
const m1 = -this.m[1] * invD;
const m2 = -this.m[2] * invD;
const m3 = this.m[0] * invD;
const m4 = invD * (this.m[2] * this.m[5] - this.m[3] * this.m[4]);
const m5 = invD * (this.m[1] * this.m[4] - this.m[0] * this.m[5]);
return mat.set(m0, m1, m2, m3, m4, m5);
}
/** Transform target point using this matrix */
applyToVector(target) {
if (!target) console.warn(`Matrix2.applyToVector(): Missing vector target`);
const x = target.x * this.m[0] + target.y * this.m[2] + this.m[4];
const y = target.x * this.m[1] + target.y * this.m[3] + this.m[5];
return target.set(x, y);
}
/** Transform a point using this matrix, returns new transformed point */
transformPoint(x, y) {
let px, py;
if (typeof x === 'object') {
px = x.x * this.m[0] + x.y * this.m[2] + this.m[4];
py = x.x * this.m[1] + x.y * this.m[3] + this.m[5];
} else {
px = x * this.m[0] + y * this.m[2] + this.m[4];
py = x * this.m[1] + y * this.m[3] + this.m[5];
}
return new Vector2(px, py);
}
/**
* Set a canvas context to use this transformation
* @param {CanvasRenderingContext2D} context Canvas context to apply this matrix transform.
*/
setContextTransform(context) {
context.setTransform(1, 0, 0, 1, 0, 0);
this.transformContext(context);
return this;
}
/**
* Transform on top of the current context transformation
* @param {CanvasRenderingContext2D} context Canvas context to apply this matrix transform.
*/
transformContext(context) {
context.transform(this.m[0], -this.m[1], -this.m[2], this.m[3], this.m[4], -this.m[5]);
return this;
}
/** CSS transform string that can be applied to the transform style of any DOM element */
cssTransform() {
return `matrix(${this.m[0]}, ${-this.m[1]}, ${-this.m[2]}, ${this.m[3]}, ${this.m[4]}, ${-this.m[5]}`;
}
toArray() {
return [ ...this.m ];
}
fromArray(array, offset = 0) {
this.set(array[offset + 0], array[offset + 1], array[offset + 2], array[offset + 3], array[offset + 4], array[offset + 5]);
return this;
}
}
export { Matrix2 };
/******************** INTERNAL ********************/
const _translate = new Matrix2();
const _rotate = new Matrix2();
const _skew = new Matrix2();