You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1120 lines
30 KiB
1120 lines
30 KiB
/* eslint-disable no-undef */ |
|
/*! |
|
2D Transformation Matrix v2.7.5 |
|
(c) Epistemex.com 2014-2018 |
|
License: MIT |
|
*/ |
|
|
|
/** |
|
* 2D transformation matrix object initialized with identity matrix. |
|
* |
|
* The matrix can synchronize a canvas 2D context by supplying the context |
|
* as an argument, or later apply current absolute transform to an |
|
* existing context. |
|
* |
|
* To synchronize a DOM element you can use [`toCSS()`]{@link Matrix#toCSS} or [`toCSS3D()`]{@link Matrix#toCSS3D}. |
|
* together with for example the `style.transform` property. |
|
* |
|
* @param {CanvasRenderingContext2D} [context] - Optional context to sync with Matrix |
|
* @param {HTMLElement} [element=null] - DOM Element to synchronize |
|
* @prop {number} a - scale x |
|
* @prop {number} b - shear y |
|
* @prop {number} c - shear x |
|
* @prop {number} d - scale y |
|
* @prop {number} e - translate x |
|
* @prop {number} f - translate y |
|
* @prop {CanvasRenderingContext2D} [context] - set or get current synchronized 2D context |
|
* @prop {HTMLElement} [element] - get current synchronized DOM element |
|
* @prop {boolean} [useCSS3D=false] - is a DOM element is defined for sync., choose whether to use 2D (false) or 3D (true) matrix to sync it. |
|
* @constructor |
|
* @license MIT license |
|
* @copyright Epistemex.com 2014-2018 |
|
*/ |
|
function Matrix(context, element) { |
|
var me = this, |
|
_el; |
|
me._t = me.transform; |
|
|
|
me.a = me.d = 1; |
|
me.b = me.c = me.e = me.f = 0; |
|
|
|
// sync context |
|
if (context) (me.context = context).setTransform(1, 0, 0, 1, 0, 0); |
|
|
|
// sync DOM element |
|
Object.defineProperty(me, 'element', { |
|
get: function () { |
|
return _el; |
|
}, |
|
set: function (el) { |
|
if (!_el) { |
|
me._px = me._getPX(); |
|
me.useCSS3D = false; |
|
} |
|
_el = el; |
|
(me._st = _el.style)[me._px] = me.toCSS(); |
|
}, |
|
}); |
|
|
|
if (element) me.element = element; |
|
} |
|
|
|
/** |
|
* Returns a new matrix that transforms a triangle `t1` into another triangle |
|
* `t2`, or throws an exception if it is impossible. |
|
* |
|
* Note: the method can take both arrays as well as literal objects. |
|
* Just make sure that both arguments (`t1`, `t2`) are of the same type. |
|
* |
|
* @param {{px: number, py: number, qx: number, qy: number, rx: number, ry: number}|Array} t1 - Object or array containing the three points for the triangle. |
|
* For object use obj.px, obj.py, obj.qx, obj.qy, obj.rx and obj.ry. For arrays provide the points in the order [px, py, qx, qy, rx, ry], or as point array [{x:,y:}, {x:,y:}, {x:,y:}] |
|
* @param {{px: number, py: number, qx: number, qy: number, rx: number, ry: number}|Array} t2 - See description for t1. |
|
* @param {CanvasRenderingContext2D} [context] - optional canvas 2D context to use for the matrix |
|
* @returns {Matrix} |
|
* @throws Exception is matrix becomes not invertible |
|
* @static |
|
*/ |
|
Matrix.fromTriangles = function (t1, t2, context) { |
|
var m1 = new Matrix(), |
|
m2 = new Matrix(context), |
|
r1, |
|
r2, |
|
rx1, |
|
ry1, |
|
rx2, |
|
ry2; |
|
|
|
if (Array.isArray(t1)) { |
|
if (typeof t1[0] === 'number') { |
|
rx1 = t1[4]; |
|
ry1 = t1[5]; |
|
rx2 = t2[4]; |
|
ry2 = t2[5]; |
|
r1 = [t1[0] - rx1, t1[1] - ry1, t1[2] - rx1, t1[3] - ry1, rx1, ry1]; |
|
r2 = [t2[0] - rx2, t2[1] - ry2, t2[2] - rx2, t2[3] - ry2, rx2, ry2]; |
|
} else { |
|
rx1 = t1[2].x; |
|
ry1 = t1[2].y; |
|
rx2 = t2[2].x; |
|
ry2 = t2[2].y; |
|
r1 = [ |
|
t1[0].x - rx1, |
|
t1[0].y - ry1, |
|
t1[1].x - rx1, |
|
t1[1].y - ry1, |
|
rx1, |
|
ry1, |
|
]; |
|
r2 = [ |
|
t2[0].x - rx2, |
|
t2[0].y - ry2, |
|
t2[1].x - rx2, |
|
t2[1].y - ry2, |
|
rx2, |
|
ry2, |
|
]; |
|
} |
|
} else { |
|
r1 = [ |
|
t1.px - t1.rx, |
|
t1.py - t1.ry, |
|
t1.qx - t1.rx, |
|
t1.qy - t1.ry, |
|
t1.rx, |
|
t1.ry, |
|
]; |
|
r2 = [ |
|
t2.px - t2.rx, |
|
t2.py - t2.ry, |
|
t2.qx - t2.rx, |
|
t2.qy - t2.ry, |
|
t2.rx, |
|
t2.ry, |
|
]; |
|
} |
|
|
|
m1.setTransform.apply(m1, r1); |
|
m2.setTransform.apply(m2, r2); |
|
|
|
return m2.multiply(m1.inverse()); |
|
}; |
|
|
|
/** |
|
* Create a matrix from a transform list from an SVG shape. The list |
|
* can be for example baseVal (i.e. `shape.transform.baseVal`). |
|
* |
|
* The resulting matrix has all transformations from that list applied |
|
* in the same order as the list. |
|
* |
|
* @param {SVGTransformList} tList - transform list from an SVG shape. |
|
* @param {CanvasRenderingContext2D} [context] - optional canvas 2D context to use for the matrix |
|
* @param {HTMLElement} [dom] - optional DOM element to use for the matrix |
|
* @returns {Matrix} |
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGTransformList|MDN / SVGTransformList} |
|
*/ |
|
Matrix.fromSVGTransformList = function (tList, context, dom) { |
|
var m = new Matrix(context, dom), |
|
i = 0; |
|
|
|
while (i < tList.length) m.multiply(tList[i++].matrix); |
|
|
|
return m; |
|
}; |
|
|
|
/** |
|
* Create and transform a new matrix based on given matrix values, or |
|
* provide SVGMatrix or a (2D) DOMMatrix, WebKitCSSMatrix or another |
|
* instance of a generic Matrix. |
|
* |
|
* @example |
|
* |
|
* var m = Matrix.from(1, 0.2, 0, 2, 120, 97); |
|
* var m = Matrix.from(domMatrix, ctx); |
|
* var m = Matrix.from(svgMatrix); |
|
* var m = Matrix.from(cssMatrix); |
|
* var m = Matrix.from(matrix); |
|
* var m = Matrix.from(vector [,pre-x] [,pre-y] [,doScale]); |
|
* |
|
* @param {*} a - number representing a in [a-f], or a Matrix object containing properties a-f. Vector is given as an object with properties x and y. |
|
* @param {*} [b] - b property if a is not a matrix object, or optional canvas 2D context. |
|
* If vector is input this will be pre-translate for x. |
|
* @param {number} [c] - If vector is input this will be pre-translate for y. |
|
* @param {number} [d] - If vector is input, set this to true to use scale and translate of 1, |
|
* false to use hypotenuse as translate distance instead and no scale. |
|
* @param {number} [e] |
|
* @param {number} [f] |
|
* @param {CanvasRenderingContext2D} [context] - optional canvas context to synchronize |
|
* @param {HTMLElement} [dom] - optional DOM element to use for the matrix |
|
* @returns {Matrix} |
|
* @static |
|
*/ |
|
Matrix.from = function (a, b, c, d, e, f, context, dom) { |
|
var m = new Matrix(context, dom), |
|
scale, |
|
dist, |
|
q; |
|
|
|
if (typeof a === 'number') m.setTransform(a, b, c, d, e, f); |
|
else if (typeof a.x === 'number') { |
|
// vector |
|
|
|
q = Math.sqrt(a.x * a.x + a.y * a.y); |
|
scale = dist = 1; |
|
|
|
if (d) scale = q; |
|
else dist = q; |
|
|
|
m.translate(b || 0, c || 0) |
|
.rotateFromVector(a) |
|
.scaleU(scale) |
|
.translate(dist, 0); |
|
} else { |
|
if (typeof a.is2D === 'boolean' && !a.is2D) |
|
throw 'Cannot use 3D DOMMatrix.'; |
|
if (b) m.context = b; |
|
if (c) m.element = c; |
|
m.multiply(a); |
|
} |
|
|
|
return m; |
|
}; |
|
|
|
Matrix.prototype = { |
|
_getPX: function () { |
|
var lst = ['t', 'oT', 'msT', 'mozT', 'webkitT', 'khtmlT'], |
|
i = 0, |
|
p, |
|
style = document.createElement('div').style; |
|
|
|
while ((p = lst[i++])) |
|
if (typeof style[p + 'ransform'] !== 'undefined') return p + 'ransform'; |
|
}, |
|
|
|
/** |
|
* Concatenates transforms of this matrix onto the given child matrix and |
|
* returns a new matrix. This instance is used on left side. |
|
* |
|
* @param {Matrix|SVGMatrix} cm - child matrix to apply concatenation to |
|
* @returns {Matrix} - new Matrix instance |
|
*/ |
|
concat: function (cm) { |
|
return this.clone().multiply(cm); |
|
}, |
|
|
|
/** |
|
* Flips the horizontal values. |
|
* @returns {Matrix} |
|
*/ |
|
flipX: function () { |
|
return this._t(-1, 0, 0, 1, 0, 0); |
|
}, |
|
|
|
/** |
|
* Flips the vertical values. |
|
* @returns {Matrix} |
|
*/ |
|
flipY: function () { |
|
return this._t(1, 0, 0, -1, 0, 0); |
|
}, |
|
|
|
/** |
|
* Reflects incoming (velocity) vector on the normal which will be the |
|
* current transformed x axis. Call when a trigger condition is met. |
|
* |
|
* @param {number} x - vector end point for x (start = 0) |
|
* @param {number} y - vector end point for y (start = 0) |
|
* @returns {{x: number, y: number}} |
|
*/ |
|
reflectVector: function (x, y) { |
|
var v = this.applyToPoint(0, 1), |
|
d = (v.x * x + v.y * y) * 2; |
|
|
|
x -= d * v.x; |
|
y -= d * v.y; |
|
|
|
return { x: x, y: y }; |
|
}, |
|
|
|
/** |
|
* Short-hand to reset current matrix to an identity matrix. |
|
* @returns {Matrix} |
|
*/ |
|
reset: function () { |
|
return this.setTransform(1, 0, 0, 1, 0, 0); |
|
}, |
|
|
|
/** |
|
* Rotates current matrix by angle (accumulative). |
|
* @param {number} angle - angle in radians |
|
* @returns {Matrix} |
|
*/ |
|
rotate: function (angle) { |
|
var cos = Math.cos(angle), |
|
sin = Math.sin(angle); |
|
return this._t(cos, sin, -sin, cos, 0, 0); |
|
}, |
|
|
|
/** |
|
* Converts a vector given as `x` and `y` to angle, and |
|
* rotates (accumulative). x can instead contain an object with |
|
* properties x and y and if so, y parameter will be ignored. |
|
* @param {number|*} x |
|
* @param {number} [y] |
|
* @returns {Matrix} |
|
*/ |
|
rotateFromVector: function (x, y) { |
|
return this.rotate( |
|
typeof x === 'number' ? Math.atan2(y, x) : Math.atan2(x.y, x.x) |
|
); |
|
}, |
|
|
|
/** |
|
* Helper method to make a rotation based on an angle in degrees. |
|
* @param {number} angle - angle in degrees |
|
* @returns {Matrix} |
|
*/ |
|
rotateDeg: function (angle) { |
|
return this.rotate((angle * Math.PI) / 180); |
|
}, |
|
|
|
/** |
|
* Scales current matrix uniformly and accumulative. |
|
* @param {number} f - scale factor for both x and y (1 does nothing) |
|
* @returns {Matrix} |
|
*/ |
|
scaleU: function (f) { |
|
return this._t(f, 0, 0, f, 0, 0); |
|
}, |
|
|
|
/** |
|
* Scales current matrix accumulative. |
|
* @param {number} sx - scale factor x (1 does nothing) |
|
* @param {number} sy - scale factor y (1 does nothing) |
|
* @returns {Matrix} |
|
*/ |
|
scale: function (sx, sy) { |
|
return this._t(sx, 0, 0, sy, 0, 0); |
|
}, |
|
|
|
/** |
|
* Scales current matrix on x axis accumulative. |
|
* @param {number} sx - scale factor x (1 does nothing) |
|
* @returns {Matrix} |
|
*/ |
|
scaleX: function (sx) { |
|
return this._t(sx, 0, 0, 1, 0, 0); |
|
}, |
|
|
|
/** |
|
* Scales current matrix on y axis accumulative. |
|
* @param {number} sy - scale factor y (1 does nothing) |
|
* @returns {Matrix} |
|
*/ |
|
scaleY: function (sy) { |
|
return this._t(1, 0, 0, sy, 0, 0); |
|
}, |
|
|
|
/** |
|
* Converts a vector given as `x` and `y` to normalized scale. |
|
* @param x |
|
* @param y |
|
* @returns {Matrix} |
|
*/ |
|
scaleFromVector: function (x, y) { |
|
return this.scaleU(Math.sqrt(x * x + y * y)); |
|
}, |
|
|
|
/** |
|
* Apply shear to the current matrix accumulative. |
|
* @param {number} sx - amount of shear for x |
|
* @param {number} sy - amount of shear for y |
|
* @returns {Matrix} |
|
*/ |
|
shear: function (sx, sy) { |
|
return this._t(1, sy, sx, 1, 0, 0); |
|
}, |
|
|
|
/** |
|
* Apply shear for x to the current matrix accumulative. |
|
* @param {number} sx - amount of shear for x |
|
* @returns {Matrix} |
|
*/ |
|
shearX: function (sx) { |
|
return this._t(1, 0, sx, 1, 0, 0); |
|
}, |
|
|
|
/** |
|
* Apply shear for y to the current matrix accumulative. |
|
* @param {number} sy - amount of shear for y |
|
* @returns {Matrix} |
|
*/ |
|
shearY: function (sy) { |
|
return this._t(1, sy, 0, 1, 0, 0); |
|
}, |
|
|
|
/** |
|
* Apply skew to the current matrix accumulative. Angles in radians. |
|
* Also see [`skewDeg()`]{@link Matrix#skewDeg}. |
|
* @param {number} ax - angle of skew for x |
|
* @param {number} ay - angle of skew for y |
|
* @returns {Matrix} |
|
*/ |
|
skew: function (ax, ay) { |
|
return this.shear(Math.tan(ax), Math.tan(ay)); |
|
}, |
|
|
|
/** |
|
* Apply skew to the current matrix accumulative. Angles in degrees. |
|
* Also see [`skew()`]{@link Matrix#skew}. |
|
* @param {number} ax - angle of skew for x |
|
* @param {number} ay - angle of skew for y |
|
* @returns {Matrix} |
|
*/ |
|
skewDeg: function (ax, ay) { |
|
return this.shear( |
|
Math.tan((ax / 180) * Math.PI), |
|
Math.tan((ay / 180) * Math.PI) |
|
); |
|
}, |
|
|
|
/** |
|
* Apply skew for x to the current matrix accumulative. Angles in radians. |
|
* Also see [`skewDeg()`]{@link Matrix#skewDeg}. |
|
* @param {number} ax - angle of skew for x |
|
* @returns {Matrix} |
|
*/ |
|
skewX: function (ax) { |
|
return this.shearX(Math.tan(ax)); |
|
}, |
|
|
|
/** |
|
* Apply skew for y to the current matrix accumulative. Angles in radians. |
|
* Also see [`skewDeg()`]{@link Matrix#skewDeg}. |
|
* @param {number} ay - angle of skew for y |
|
* @returns {Matrix} |
|
*/ |
|
skewY: function (ay) { |
|
return this.shearY(Math.tan(ay)); |
|
}, |
|
|
|
/** |
|
* Set current matrix to new absolute matrix. |
|
* @param {number} a - scale x |
|
* @param {number} b - shear y |
|
* @param {number} c - shear x |
|
* @param {number} d - scale y |
|
* @param {number} e - translate x |
|
* @param {number} f - translate y |
|
* @returns {Matrix} |
|
*/ |
|
setTransform: function (a, b, c, d, e, f) { |
|
var me = this; |
|
me.a = a; |
|
me.b = b; |
|
me.c = c; |
|
me.d = d; |
|
me.e = e; |
|
me.f = f; |
|
return me._x(); |
|
}, |
|
|
|
/** |
|
* Translate current matrix accumulative. |
|
* @param {number} tx - translation for x |
|
* @param {number} ty - translation for y |
|
* @returns {Matrix} |
|
*/ |
|
translate: function (tx, ty) { |
|
return this._t(1, 0, 0, 1, tx, ty); |
|
}, |
|
|
|
/** |
|
* Translate current matrix on x axis accumulative. |
|
* @param {number} tx - translation for x |
|
* @returns {Matrix} |
|
*/ |
|
translateX: function (tx) { |
|
return this._t(1, 0, 0, 1, tx, 0); |
|
}, |
|
|
|
/** |
|
* Translate current matrix on y axis accumulative. |
|
* @param {number} ty - translation for y |
|
* @returns {Matrix} |
|
*/ |
|
translateY: function (ty) { |
|
return this._t(1, 0, 0, 1, 0, ty); |
|
}, |
|
|
|
/** |
|
* Multiplies current matrix with new matrix values. Also see [`multiply()`]{@link Matrix#multiply}. |
|
* |
|
* @param {number} a2 - scale x |
|
* @param {number} b2 - skew y |
|
* @param {number} c2 - skew x |
|
* @param {number} d2 - scale y |
|
* @param {number} e2 - translate x |
|
* @param {number} f2 - translate y |
|
* @returns {Matrix} |
|
*/ |
|
transform: function (a2, b2, c2, d2, e2, f2) { |
|
var me = this, |
|
a1 = me.a, |
|
b1 = me.b, |
|
c1 = me.c, |
|
d1 = me.d, |
|
e1 = me.e, |
|
f1 = me.f; |
|
|
|
/* matrix column order is: |
|
* a c e |
|
* b d f |
|
* 0 0 1 |
|
*/ |
|
me.a = a1 * a2 + c1 * b2; |
|
me.b = b1 * a2 + d1 * b2; |
|
me.c = a1 * c2 + c1 * d2; |
|
me.d = b1 * c2 + d1 * d2; |
|
me.e = a1 * e2 + c1 * f2 + e1; |
|
me.f = b1 * e2 + d1 * f2 + f1; |
|
|
|
return me._x(); |
|
}, |
|
|
|
/** |
|
* Multiplies current matrix with source matrix. |
|
* @param {Matrix|DOMMatrix|SVGMatrix} m - source matrix to multiply with. |
|
* @returns {Matrix} |
|
*/ |
|
multiply: function (m) { |
|
return this._t(m.a, m.b, m.c, m.d, m.e, m.f); |
|
}, |
|
|
|
/** |
|
* Divide this matrix on input matrix which must be invertible. |
|
* @param {Matrix} m - matrix to divide on (divisor) |
|
* @throws Exception if input matrix is not invertible |
|
* @returns {Matrix} |
|
*/ |
|
divide: function (m) { |
|
return this.multiply(m.inverse()); |
|
}, |
|
|
|
/** |
|
* Divide current matrix on scalar value != 0. |
|
* @param {number} d - divisor |
|
* @throws Exception if divisor is zero |
|
* @returns {Matrix} |
|
*/ |
|
divideScalar: function (d) { |
|
var me = this; |
|
|
|
if (!d) throw 'Division on zero'; |
|
|
|
me.a /= d; |
|
me.b /= d; |
|
me.c /= d; |
|
me.d /= d; |
|
me.e /= d; |
|
me.f /= d; |
|
|
|
return me._x(); |
|
}, |
|
|
|
/** |
|
* Get an inverse matrix of current matrix. The method returns a new |
|
* matrix with values you need to use to get to an identity matrix. |
|
* Context from parent matrix is not applied to the returned matrix. |
|
* |
|
* @param {boolean} [cloneContext=false] - clone current context to resulting matrix |
|
* @param {boolean} [cloneDOM=false] - clone current DOM element to resulting matrix |
|
* @throws Exception is input matrix is not invertible |
|
* @returns {Matrix} - new Matrix instance |
|
*/ |
|
inverse: function (cloneContext, cloneDOM) { |
|
var me = this, |
|
m = new Matrix( |
|
cloneContext ? me.context : null, |
|
cloneDOM ? me.element : null |
|
), |
|
dt = me.determinant(); |
|
|
|
if (!dt) throw 'Matrix not invertible.'; |
|
|
|
m.a = me.d / dt; |
|
m.b = -me.b / dt; |
|
m.c = -me.c / dt; |
|
m.d = me.a / dt; |
|
m.e = (me.c * me.f - me.d * me.e) / dt; |
|
m.f = -(me.a * me.f - me.b * me.e) / dt; |
|
|
|
return m; |
|
}, |
|
|
|
/** |
|
* Interpolate this matrix with another and produce a new matrix. |
|
* `t` is a value in the range [0.0, 1.0] where 0 is this instance and |
|
* 1 is equal to the second matrix. The `t` value is not clamped. |
|
* |
|
* Context from parent matrix is not applied to the returned matrix. |
|
* |
|
* Note: this interpolation is naive. For animation containing rotation, |
|
* shear or skew use the [`interpolateAnim()`]{@link Matrix#interpolateAnim} method instead |
|
* to avoid unintended flipping. |
|
* |
|
* @param {Matrix|SVGMatrix} m2 - the matrix to interpolate with. |
|
* @param {number} t - interpolation [0.0, 1.0] |
|
* @param {CanvasRenderingContext2D} [context] - optional context to affect |
|
* @param {HTMLElement} [dom] - optional DOM element to use for the matrix |
|
* @returns {Matrix} - new Matrix instance with the interpolated result |
|
*/ |
|
interpolate: function (m2, t, context, dom) { |
|
var me = this, |
|
m = new Matrix(context, dom); |
|
|
|
m.a = me.a + (m2.a - me.a) * t; |
|
m.b = me.b + (m2.b - me.b) * t; |
|
m.c = me.c + (m2.c - me.c) * t; |
|
m.d = me.d + (m2.d - me.d) * t; |
|
m.e = me.e + (m2.e - me.e) * t; |
|
m.f = me.f + (m2.f - me.f) * t; |
|
|
|
return m._x(); |
|
}, |
|
|
|
/** |
|
* Interpolate this matrix with another and produce a new matrix. |
|
* `t` is a value in the range [0.0, 1.0] where 0 is this instance and |
|
* 1 is equal to the second matrix. The `t` value is not constrained. |
|
* |
|
* Context from parent matrix is not applied to the returned matrix. |
|
* |
|
* To obtain easing `t` can be preprocessed using easing-functions |
|
* before being passed to this method. |
|
* |
|
* Note: this interpolation method uses decomposition which makes |
|
* it suitable for animations (in particular where rotation takes |
|
* places). |
|
* |
|
* @param {Matrix} m2 - the matrix to interpolate with. |
|
* @param {number} t - interpolation [0.0, 1.0] |
|
* @param {CanvasRenderingContext2D} [context] - optional context to affect |
|
* @param {HTMLElement} [dom] - optional DOM element to use for the matrix |
|
* @returns {Matrix} - new Matrix instance with the interpolated result |
|
*/ |
|
interpolateAnim: function (m2, t, context, dom) { |
|
var m = new Matrix(context, dom), |
|
d1 = this.decompose(), |
|
d2 = m2.decompose(), |
|
t1 = d1.translate, |
|
t2 = d2.translate, |
|
s1 = d1.scale; |
|
|
|
// QR order (t-r-s-sk) |
|
m.translate(t1.x + (t2.x - t1.x) * t, t1.y + (t2.y - t1.y) * t); |
|
m.rotate(d1.rotation + (d2.rotation - d1.rotation) * t); |
|
m.scale(s1.x + (d2.scale.x - s1.x) * t, s1.y + (d2.scale.y - s1.y) * t); |
|
//todo test skew scenarios |
|
|
|
return m._x(); |
|
}, |
|
|
|
/** |
|
* Decompose the current matrix into simple transforms using either |
|
* QR (default) or LU decomposition. |
|
* |
|
* @param {boolean} [useLU=false] - set to true to use LU rather than QR decomposition |
|
* @returns {*} - an object containing current decomposed values (translate, rotation, scale, skew) |
|
* @see {@link https://en.wikipedia.org/wiki/QR_decomposition|More on QR decomposition} |
|
* @see {@link https://en.wikipedia.org/wiki/LU_decomposition|More on LU decomposition} |
|
*/ |
|
decompose: function (useLU) { |
|
var me = this, |
|
a = me.a, |
|
b = me.b, |
|
c = me.c, |
|
d = me.d, |
|
acos = Math.acos, |
|
atan = Math.atan, |
|
sqrt = Math.sqrt, |
|
pi = Math.PI, |
|
translate = { x: me.e, y: me.f }, |
|
rotation = 0, |
|
scale = { x: 1, y: 1 }, |
|
skew = { x: 0, y: 0 }, |
|
determ = a * d - b * c, // determinant(), skip DRY here... |
|
r, |
|
s; |
|
|
|
if (useLU) { |
|
if (a) { |
|
skew = { x: atan(c / a), y: atan(b / a) }; |
|
scale = { x: a, y: determ / a }; |
|
} else if (b) { |
|
rotation = pi * 0.5; |
|
scale = { x: b, y: determ / b }; |
|
skew.x = atan(d / b); |
|
} else { |
|
// a = b = 0 |
|
scale = { x: c, y: d }; |
|
skew.x = pi * 0.25; |
|
} |
|
} else { |
|
// Apply the QR-like decomposition. |
|
if (a || b) { |
|
r = sqrt(a * a + b * b); |
|
rotation = b > 0 ? acos(a / r) : -acos(a / r); |
|
scale = { x: r, y: determ / r }; |
|
skew.x = atan((a * c + b * d) / (r * r)); |
|
} else if (c || d) { |
|
s = sqrt(c * c + d * d); |
|
rotation = pi * 0.5 - (d > 0 ? acos(-c / s) : -acos(c / s)); |
|
scale = { x: determ / s, y: s }; |
|
skew.y = atan((a * c + b * d) / (s * s)); |
|
} else { |
|
// a = b = c = d = 0 |
|
scale = { x: 0, y: 0 }; |
|
} |
|
} |
|
|
|
return { |
|
translate: translate, |
|
rotation: rotation, |
|
scale: scale, |
|
skew: skew, |
|
}; |
|
}, |
|
|
|
/** |
|
* Returns the determinant of the current matrix. |
|
* @returns {number} |
|
*/ |
|
determinant: function () { |
|
return this.a * this.d - this.b * this.c; |
|
}, |
|
|
|
/** |
|
* Apply current matrix to `x` and `y` of a point. |
|
* Returns a point object. |
|
* |
|
* @param {number} x - value for x |
|
* @param {number} y - value for y |
|
* @returns {{x: number, y: number}} A new transformed point object |
|
*/ |
|
applyToPoint: function (x, y) { |
|
var me = this; |
|
return { |
|
x: x * me.a + y * me.c + me.e, |
|
y: x * me.b + y * me.d + me.f, |
|
}; |
|
}, |
|
|
|
/** |
|
* Apply current matrix to array with point objects or point pairs. |
|
* Returns a new array with points in the same format as the input array. |
|
* |
|
* A point object is an object literal: |
|
* |
|
* {x: x, y: y} |
|
* |
|
* so an array would contain either: |
|
* |
|
* [{x: x1, y: y1}, {x: x2, y: y2}, ... {x: xn, y: yn}] |
|
* |
|
* or |
|
* |
|
* [x1, y1, x2, y2, ... xn, yn] |
|
* |
|
* @param {Array} points - array with point objects or pairs |
|
* @returns {Array} A new array with transformed points |
|
*/ |
|
applyToArray: function (points) { |
|
var i = 0, |
|
p, |
|
l, |
|
mxPoints = []; |
|
|
|
if (typeof points[0] === 'number') { |
|
l = points.length; |
|
|
|
while (i < l) { |
|
p = this.applyToPoint(points[i++], points[i++]); |
|
mxPoints.push(p.x, p.y); |
|
} |
|
} else { |
|
while ((p = points[i++])) { |
|
mxPoints.push(this.applyToPoint(p.x, p.y)); |
|
} |
|
} |
|
|
|
return mxPoints; |
|
}, |
|
|
|
/** |
|
* Apply current matrix to a typed array with point pairs. Although |
|
* the input array may be an ordinary array, this method is intended |
|
* for more performant use where typed arrays are used. The returned |
|
* array is regardless always returned as a `Float32Array`. |
|
* |
|
* @param {*} points - (typed) array with point pairs [x1, y1, ..., xn, yn] |
|
* @param {boolean} [use64=false] - use Float64Array instead of Float32Array |
|
* @returns {*} A new typed array with transformed points |
|
*/ |
|
applyToTypedArray: function (points, use64) { |
|
var i = 0, |
|
p, |
|
l = points.length, |
|
mxPoints = use64 ? new Float64Array(l) : new Float32Array(l); |
|
|
|
while (i < l) { |
|
p = this.applyToPoint(points[i], points[i + 1]); |
|
mxPoints[i++] = p.x; |
|
mxPoints[i++] = p.y; |
|
} |
|
|
|
return mxPoints; |
|
}, |
|
|
|
/** |
|
* Apply to any canvas 2D context object. This does not affect the |
|
* context that optionally was referenced in constructor unless it is |
|
* the same context. |
|
* |
|
* @param {CanvasRenderingContext2D} context - target context |
|
* @returns {Matrix} |
|
*/ |
|
applyToContext: function (context) { |
|
var me = this; |
|
context.setTransform(me.a, me.b, me.c, me.d, me.e, me.f); |
|
return me; |
|
}, |
|
|
|
/** |
|
* Apply to any DOM element. This does not affect the DOM element |
|
* that optionally was referenced in constructor unless it is |
|
* the same element. |
|
* |
|
* The method will auto-detect the correct browser prefix if any. |
|
* |
|
* @param {HTMLElement} element - target DOM element |
|
* @param {boolean} [use3D=false] - use 3D transformation matrix instead of 2D |
|
* @returns {Matrix} |
|
*/ |
|
applyToElement: function (element, use3D) { |
|
var me = this; |
|
if (!me._px) me._px = me._getPX(); |
|
element.style[me._px] = use3D ? me.toCSS3D() : me.toCSS(); |
|
return me; |
|
}, |
|
|
|
/** |
|
* Instead of creating a new instance of a Matrix, DOMMatrix or SVGMatrix |
|
* the current settings of this instance can be applied to an external |
|
* object of a different (or same) type. You can also pass in an |
|
* empty literal object. |
|
* |
|
* Note that the properties a-f will be set regardless of if they |
|
* already exist or not. |
|
* |
|
* @param {*} obj - target object. |
|
* @returns {Matrix} |
|
*/ |
|
applyToObject: function (obj) { |
|
var me = this; |
|
obj.a = me.a; |
|
obj.b = me.b; |
|
obj.c = me.c; |
|
obj.d = me.d; |
|
obj.e = me.e; |
|
obj.f = me.f; |
|
return me; |
|
}, |
|
|
|
/** |
|
* Returns true if matrix is an identity matrix (no transforms applied). |
|
* @returns {boolean} |
|
*/ |
|
isIdentity: function () { |
|
var me = this; |
|
return me.a === 1 && !me.b && !me.c && me.d === 1 && !me.e && !me.f; |
|
}, |
|
|
|
/** |
|
* Returns true if matrix is invertible |
|
* @returns {boolean} |
|
*/ |
|
isInvertible: function () { |
|
return !this._q(this.determinant(), 0); |
|
}, |
|
|
|
/** |
|
* The method is intended for situations where scale is accumulated |
|
* via multiplications, to detect situations where scale becomes |
|
* "trapped" with a value of zero. And in which case scale must be |
|
* set explicitly to a non-zero value. |
|
* |
|
* @returns {boolean} |
|
*/ |
|
isValid: function () { |
|
return !(this.a * this.d); |
|
}, |
|
|
|
/** |
|
* Compares current matrix with another matrix. Returns true if equal |
|
* (within epsilon tolerance). |
|
* @param {Matrix|SVGMatrix} m - matrix to compare this matrix with |
|
* @returns {boolean} |
|
*/ |
|
isEqual: function (m) { |
|
var me = this, |
|
q = me._q; |
|
|
|
return ( |
|
q(me.a, m.a) && |
|
q(me.b, m.b) && |
|
q(me.c, m.c) && |
|
q(me.d, m.d) && |
|
q(me.e, m.e) && |
|
q(me.f, m.f) |
|
); |
|
}, |
|
|
|
/** |
|
* Clones current instance and returning a new matrix. |
|
* @param {boolean} [noContext=false] don't clone context reference if true |
|
* @returns {Matrix} - a new Matrix instance with identical transformations as this instance |
|
*/ |
|
clone: function (noContext) { |
|
return new Matrix(noContext ? null : this.context).multiply(this); |
|
}, |
|
|
|
/** |
|
* Returns an array with current matrix values. |
|
* @returns {Array} |
|
*/ |
|
toArray: function () { |
|
var me = this; |
|
return [me.a, me.b, me.c, me.d, me.e, me.f]; |
|
}, |
|
|
|
/** |
|
* Returns a binary 32-bit floating point typed array. |
|
* @returns {*} |
|
*/ |
|
toTypedArray: function () { |
|
var me = this; |
|
return new Float32Array([me.a, me.b, me.c, me.d, me.e, me.f]); |
|
}, |
|
|
|
/** |
|
* Generates a string that can be used with CSS `transform`. |
|
* @example |
|
* element.style.transform = m.toCSS(); |
|
* @returns {string} |
|
*/ |
|
toCSS: function () { |
|
return 'matrix(' + this.toArray() + ')'; |
|
}, |
|
|
|
/** |
|
* Generates a `matrix3d()` string that can be used with CSS `transform`. |
|
* Although the matrix is for 2D use you may see performance benefits |
|
* on some devices using a 3D CSS transform instead of a 2D. |
|
* @example |
|
* element.style.transform = m.toCSS3D(); |
|
* @returns {string} |
|
*/ |
|
toCSS3D: function () { |
|
var me = this, |
|
n2 = ',0,0,'; |
|
return ( |
|
'matrix3d(' + |
|
me.a + |
|
',' + |
|
me.b + |
|
n2 + |
|
me.c + |
|
',' + |
|
me.d + |
|
n2 + |
|
n2 + |
|
',1,0,' + |
|
me.e + |
|
',' + |
|
me.f + |
|
',0,1)' |
|
); |
|
}, |
|
|
|
/** |
|
* Returns a JSON compatible string of current matrix. |
|
* @returns {string} |
|
*/ |
|
toJSON: function () { |
|
var me = this; |
|
return ( |
|
'{"a":' + |
|
me.a + |
|
',"b":' + |
|
me.b + |
|
',"c":' + |
|
me.c + |
|
',"d":' + |
|
me.d + |
|
',"e":' + |
|
me.e + |
|
',"f":' + |
|
me.f + |
|
'}' |
|
); |
|
}, |
|
|
|
/** |
|
* Returns a string with current matrix as comma-separated list. |
|
* @param {number} [fixLen=4] - truncate decimal values to number of digits |
|
* @returns {string} |
|
*/ |
|
toString: function (fixLen) { |
|
var me = this; |
|
fixLen = fixLen || 4; |
|
return ( |
|
'a=' + |
|
me.a.toFixed(fixLen) + |
|
' b=' + |
|
me.b.toFixed(fixLen) + |
|
' c=' + |
|
me.c.toFixed(fixLen) + |
|
' d=' + |
|
me.d.toFixed(fixLen) + |
|
' e=' + |
|
me.e.toFixed(fixLen) + |
|
' f=' + |
|
me.f.toFixed(fixLen) |
|
); |
|
}, |
|
|
|
/** |
|
* Returns a string with current matrix as comma-separated values |
|
* string with line-end (CR+LF). |
|
* @returns {string} |
|
*/ |
|
toCSV: function () { |
|
return this.toArray().join() + '\r\n'; |
|
}, |
|
|
|
/** |
|
* Convert current matrix into a `DOMMatrix`. If `DOMMatrix` is not |
|
* supported, a `null` is returned. |
|
* |
|
* @returns {DOMMatrix} |
|
* @see {@link https://drafts.fxtf.org/geometry/#dommatrix|MDN / SVGMatrix} |
|
*/ |
|
toDOMMatrix: function () { |
|
var m = null; |
|
if ('DOMMatrix' in window) { |
|
m = new DOMMatrix(); |
|
m.a = this.a; |
|
m.b = this.b; |
|
m.c = this.c; |
|
m.d = this.d; |
|
m.e = this.e; |
|
m.f = this.f; |
|
} |
|
return m; |
|
}, |
|
|
|
/** |
|
* Convert current matrix into a `SVGMatrix`. If `SVGMatrix` is not |
|
* supported, a `null` is returned. |
|
* |
|
* @returns {SVGMatrix} |
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGMatrix|MDN / SVGMatrix} |
|
*/ |
|
toSVGMatrix: function () { |
|
var me = this, |
|
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), |
|
svgMatrix = null; |
|
|
|
if (svg) { |
|
svgMatrix = svg.createSVGMatrix(); |
|
svgMatrix.a = me.a; |
|
svgMatrix.b = me.b; |
|
svgMatrix.c = me.c; |
|
svgMatrix.d = me.d; |
|
svgMatrix.e = me.e; |
|
svgMatrix.f = me.f; |
|
} |
|
|
|
return svgMatrix; |
|
}, |
|
|
|
/** |
|
* Compares floating point values with some tolerance (epsilon) |
|
* @param {number} f1 - float 1 |
|
* @param {number} f2 - float 2 |
|
* @returns {boolean} |
|
* @private |
|
*/ |
|
_q: function (f1, f2) { |
|
return Math.abs(f1 - f2) < 1e-14; |
|
}, |
|
|
|
/** |
|
* Apply current absolute matrix to context if defined, to sync it. |
|
* Apply current absolute matrix to element if defined, to sync it. |
|
* @returns {Matrix} |
|
* @private |
|
*/ |
|
_x: function () { |
|
var me = this; |
|
|
|
if (me.context) me.context.setTransform(me.a, me.b, me.c, me.d, me.e, me.f); |
|
|
|
if (me._st) me._st[me._px] = me.useCSS3D ? me.toCSS3D() : me.toCSS(); // can be optimized pre-storing func ref. |
|
|
|
return me; |
|
}, |
|
}; |
|
|
|
// Node support |
|
if (typeof exports !== 'undefined') exports.Matrix = Matrix;
|
|
|