@omjs/matrix2d
Version:
CSS tranform-2D matrix
401 lines (360 loc) • 10.5 kB
JavaScript
/**
* matrix2d 工具包,包含一个Matrix2D类及若干运算方法
* @author oyzhen
* @link https://git.coding.net/oyzhen/matrix2d.git
*/
import { map, Iterable } from '@omjs/iterable';
import Angle from '@omjs/angle';
var version = "1.0.1";
/**
* 根据指定的小数位四舍五入
* @param {number} digit 小数位数
*/
const round = digit => (num, p) => (p = 10 ** digit, Math.round(num * p) / p);
/**
* Matrix2D矩阵操作函数
*/
/**
* 判断是否是类Matrix2D数据结构
* @param {*} mat
*/
function isMatrix2DLike(mat) {
return Matrix2D.isMatrix2D(mat) || Object.keys(mat || {}).join('') === '012345';
}
/**
* 简化数值,根据指定的小数位数四舍五入
* @param {number} digit 小数位数
*/
function simplify(digit = 3) {
let m = isMatrix2DLike(this) ? this : new Matrix2D(this);
return new Matrix2D(map.call(m, v => round(digit)(v)));
}
/**
* 左乘
* @param {Matrix2D} mat1 被乘矩阵
* @param {Matrix2D} mat2 乘矩阵
* @returns {Matrix2D}
*/
function lmul(mat) {
let m1 = isMatrix2DLike(this) ? this : new Matrix2D(this);
let m2 = isMatrix2DLike(mat) ? mat : new Matrix2D(mat);
return new Matrix2D(m1[0] * m2[0] + m1[1] * m2[2], m1[0] * m2[1] + m1[1] * m2[3], m1[2] * m2[0] + m1[3] * m2[2], m1[2] * m2[1] + m1[3] * m2[3], m1[4] * m2[0] + m1[5] * m2[2] + m2[4], m1[4] * m2[1] + m1[5] * m2[3] + m2[5]);
}
/**
* 右乘
* @param {Matrix2D} mat 被乘矩阵
* @returns {Matrix2D}
*/
function rmul(mat) {
return lmul.call(mat, this);
}
/**
* 乘
* @param {Matrix2D} mat 被乘矩阵
* @returns {Matrix2D}
*/
function mul(mat, dir = Matrix2D.MUL_DIR.L) {
if (dir === Matrix2D.MUL_DIR.L) {
return lmul.call(this, mat);
}
return rmul.call(this, mat);
}
/**
* 旋转变换
* @public
* @param {Angle | number | string} angle 旋转角,默认 0rad,当传入为数值时单位为deg,当传入为字符串时
* @param {Symbol} dir 旋转方向,默认 顺时针方向
* @returns {Matrix2D}
*/
function rotate(angle = new Angle(), dir = Matrix2D.ROTATE_DIR.CW) {
let rad = Angle.from(angle).rad;
let cos = Math.cos(rad);
let sin = Math.sin(rad);
let mat = [cos, sin, -sin, cos, 0, 0];
if (dir === Matrix2D.ROTATE_DIR.ACW) {
mat = [cos, -sin, sin, cos, 0, 0];
}
return lmul.call(this, mat);
}
/**
* 缩放变换
* @public
* @param {number} x x方向缩放量
* @param {number} y y方向缩放量
* @returns {Matrix2D}
*/
function scale(x = 1, y = 1) {
return lmul.call(this, [x, 0, 0, y, 0, 0]);
}
/**
* 倾斜变换
* @public
* @param {Angle | string | number} angX x轴倾斜角
* @param {Angle | string | number} angY y轴倾斜角
* @returns {Matrix2D}
*/
function skew(angX = new Angle(), angY = new Angle()) {
let radX = Angle.from(angX).rad;
let radY = Angle.from(angY).rad;
return lmul.call(this, [1, Math.tan(radY), Math.tan(radX), 1, 0, 0]);
}
/**
* 平移
* @param {number} x 水平方向平移量
* @param {number} y 垂直方向平移量
* @returns {Matrix2D}
*/
function translate(x = 0, y = 0) {
return lmul.call(this, [1, 0, 0, 1, x, y]);
}
/**
* 平移,同translate
* @param {number} x 水平方向平移量
* @param {number} y 垂直方向平移量
* @returns {Matrix2D}
*/
function move(x = 0, y = 0) {
return translate.call(this, x, y);
}
/**
* 对点应用矩阵变换
* @param {number[]} pt [x, y] 变换点坐标
* @returns {number[]} [x, y] 变换后点坐标
*/
function exec([x = 0, y = 0] = []) {
let m = isMatrix2DLike(this) ? this : new Matrix2D(this);
return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
}
/**
* 对矩形应用矩阵变换
* @param {Object} rect {x, y, w, h} 矩形描述对象
* @param {number[]} center [cx, cy] 变换中心点
* @returns {Object} {x, y, w, h} 变换后的矩形描述对象
*/
function execRect({ x = 0, y = 0, w = 0, h = 0 } = {}, [cx = 0, cy = 0] = []) {
let bfr = [[x - cx, y - cy], [x + w - cx, y - cy], [x + w - cx, y + h - cy], [x - cx, y + h - cy]]; // 变换前矩形顶点坐标(左上、右上、右下、左下)
let aft = bfr.map(v => exec.call(this, v));
let xArr = aft.map(v => v[0]);
let yArr = aft.map(v => v[1]);
let xMin = Math.min(...xArr);
let xMax = Math.max(...xArr);
let yMin = Math.min(...yArr);
let yMax = Math.max(...yArr);
return {
x: xMin + cx,
y: yMin + cy,
w: xMax - xMin,
h: yMax - yMin
};
}
/**
* 由Matrix2D推导rotate、scale、skew、translate等值
* 先rotate,再scale,再skew,再translate
* @todo 已知bug: 当rotate不是90的倍数时,计算值不准确
* @see http://www.wolframalpha.com/input/?i=%7B%7B1,0%7D,%7Bt1,1%7D%7D*%7B%7Bs1,0%7D,%7B0,s2%7D%7D*%7B%7Bcos%5Br%5D,+-sin%5Br%5D%7D,%7Bsin%5Br%5D,+cos%5Br%5D%7D%7D+%3D+%7B%7Bm0,m2%7D,%7Bm1,m3%7D%7D+and+m0*m3!%3Dm1*m2
* @returns {object} {rotate, scale, scaleX, scaleY, skew, skewX, skewY, translate, translateX, translateY}
*/
function decompose() {
let m = isMatrix2DLike(this) ? this : new Matrix2D(this);
let dm = m[0] * m[3] - m[1] * m[2];
if (dm === 0) {
throw new Error('Only an invertible matrix can be decomposed.');
}
let sm = m[0] ** 2 + m[2] ** 2;
let rotate = Angle.fromRad(2 * Math.atan2(m[0] - Math.sqrt(sm), m[2]));
let scaleX = Math.sqrt(sm);
let scaleY = dm / scaleX;
let skewX = Angle.fromRad(Math.atan2(m[0] * m[1] + m[2] * m[3], sm));
let skewY = Angle.fromRad(0);
return {
rotate,
scaleX,
scaleY,
scale: [scaleX, scaleY],
skewX,
skewY,
skew: [skewX, skewY],
translateX: m[4],
translateY: m[5],
translate: [m[4], m[5]]
};
}
/**
* CSS/Affine Transform-2D Matrix
* matrix(a, b, c, d, m, n)<br />
* ┌ a c m ┐<br />
* | b d n |<br />
* └ 0 0 1 ┘<br />
*/
const Matrix2D = (() => {
// class definition
class Matrix2D extends Iterable {
/**
* 构造函数
* @param {*} mat 矩阵初始化量,可为css transform-2d matrix、数组
* @todo 优化运算
*/
constructor(...mat) {
if (Matrix2D.isMatrix2D(mat[0])) {
super(...mat[0]);
} else {
let [a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0] = mat.toString().split(/[^\d.\-+e]/).map(v => parseFloat(v)).filter(x => !isNaN(x));
super(a, b, c, d, tx, ty);
}
let dcache;
Object.defineProperty(this, 'decompose', {
/**
* 矩阵分解,得到rotate/skew/translate/scale等值
* @returns {object}
*/
get: () => {
if (!dcache) {
dcache = decompose.call(this);
}
return dcache;
}
});
}
/**
* 判断是否是Matrix2D对象
* @param {*} mat 待判定对象
*/
static isMatrix2D(mat) {
return mat != null && mat instanceof Matrix2D;
}
/**
* 转换成字符串
* @override
* @returns {string}
*/
toString() {
return `matrix(${Array.prototype.join.call(this, ',')})`;
}
/**
* JSON.stringify预处理
* @override
* @returns {string}
*/
toJSON() {
return this.toString();
}
/**
* 简化数值,根据指定的小数位数四舍五入
* @param {number} digit
* @returns {Matrix2D}
*/
toBrief(digit) {
return simplify.call(this, digit);
}
/**
* @override
* @returns {number[]}
*/
valueOf() {
return [...this];
}
/**
* deep clone a copy
* @returns {Matrix2D}
*/
clone() {
return new Matrix2D(this);
}
/**
* 获得旋转量
* @returns {Angle}
*/
get rotate() {
return this.decompose.rotate;
}
/**
* 获得缩放量
* @returns {number[]}
*/
get scale() {
return this.decompose.scale;
}
/**
* 获得x方向缩放量
* @returns {number}
*/
get scaleX() {
return this.decompose.scaleX;
}
/**
* 获得y方向缩放量
* @returns {number}
*/
get scaleY() {
return this.decompose.scaleY;
}
/**
* 获得倾斜量
* @returns {Angle[]}
*/
get skew() {
return this.decompose.skew;
}
/**
* 获得x方向倾斜量
* @returns {Angle}
*/
get skewX() {
return this.decompose.skewX;
}
/**
* 获得y方向倾斜量
* @returns {Angle}
*/
get skewY() {
return this.decompose.skewY;
}
/**
* 获得平移量
* @returns {number[]}
*/
get translate() {
return this.decompose.translate;
}
/**
* 获得x方向平移量
* @returns {number}
*/
get translateX() {
return this.decompose.translateX;
}
/**
* 获得y方向平移量
* @returns {number}
*/
get translateY() {
return this.decompose.translateY;
}
}
/**
* @prop {string} version 版本号
*/
Matrix2D.version = version;
// Object.defineProperty(Matrix2D, 'version', {value: version});
/**
* 矩阵乘法方向
* @readOnly
* @static
* @property {Symbol} L 左乘
* @property {Symbol} R 右乘
*/
Matrix2D.MUL_DIR = Object.freeze({ L: Symbol('left'), R: Symbol('right') });
/**
* 旋转方向
* @readOnly
* @static
* @property {Symbol} CW 顺时针方向
* @property {Symbol} ACW 逆时针方向
*/
Matrix2D.ROTATE_DIR = Object.freeze({
CW: Symbol('cw'),
ACW: Symbol('acw')
}); // CW - 顺时针,ACW - 逆时针
return Matrix2D;
})();
export { Matrix2D, isMatrix2DLike, simplify, lmul, rmul, mul, rotate, scale, skew, translate, move, exec, execRect, decompose };