首先,安装一个 Matrix 库,因为自己写的话,一是麻烦易错,二是性能不佳:
npm install gl-mat3
封装一个仿射矩阵类
const mat3 = require("gl-mat3");
class AffineMatrix {
#values = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
/**
* @param {[number, number, number, number, number, number]} values
*/
constructor(values) {
for (const i in values) {
this.#values[i] = values[i];
}
}
/**
* @methd getValues 读取矩阵的值
* @returns {Float32Array}
*/
getValues() {
return new Float32Array(this.#values);
}
/**
* @method rotate 旋转
* @param {number|string} [rad] 旋转的角度,如果是数字,默认单位是弧度(rad)
* @returns {AffineMatrix} 返回一个全新矩阵,即不在原矩阵上修改
*/
// rotate(rad = 0) {
// let radius = rad;
// if (typeof rad === "string") {
// if (/rad$/.test(rad)) {
// radius = parseFloat(rad.replace(/rad$/, ""));
// } else if (/deg/.test(rad)) {
// radius = parseFloat(rad.replace(/deg$/, "") / Math.PI);
// } else {
// radius = parseFloat(rad);
// }
// }
// return new AffineMatrix(
// mat3.rotate(new Float32Array(9), this.#values),
// radius
// );
// }
/**
* @method scale - 缩放
* @param {number} scaleX
* @param {number} scaleY
* @returns {AffineMatrix}
*
* @overload
* @param {*} scaleX
* @returns {AffineMatrix}
*/
scale(scaleX, scaleY) {
if (scaleY === undefined) {
return new AffineMatrix(
mat3.scale(new Float32Array(9), this.#values, [scaleX, scaleX, 1])
);
} else {
return new AffineMatrix(
mat3.scale(new Float32Array(9), this.#values, [scaleX, scaleY, 1])
);
}
}
/**
* @method multi 乘以一个向量或另外的矩阵
* @param {AffineMatrix} obj
* @returns {AffineMatrix}
*
* @overload
* @param {ArrayLike} obj
* @returns {Float32Array}
*/
multi(obj) {
if (obj instanceof AffineMatrix) {
return new AffineMatrix(
mat3.multiply(new Float32Array(9), this.#values, obj.getValues())
);
} else {
const v = this.#values;
const [x, y, z] = obj;
return new Float32Array([
v[0] * x + v[1] * y + v[2] * z,
v[3] * x + v[4] * y + v[5] * z,
v[6] * x + v[7] * y + v[8] * z,
]);
}
}
/**
* @method invert 求逆矩阵
*/
invert() {
return new AffineMatrix(mat3.invert(new Float32Array(9), this.#values));
}
toCSSMatrix() {
const v = this.#values;
return `matrix(${v[0]},${v[1]},${v[2]},${v[3]},${v[4]},${v[5]})`;
}
/**
* @method fromCSSMatrix toCSSMatrix 的逆运算
* @param {string} matrixString
*/
static fromCSSMatrix(matrixString) {
return new AffineMatrix([
...matrixString
.replace(/^matrix\(/, "")
.split(",")
.map((i) => parseFloat(i)),
0,
0,
1,
]);
}
}
"jQuery" 上新增一个对应的方法:
$.fn.transform = function(affine){
this.style.transform = affine.toCSSMatrix();
return $(this);
}
给父元素设置"transform"的时候,一律不准使用 "$.fn.css",转而使用上述方法:
const fnCss = $.fn.css;
const $background = $("#background");
$.fn.css = function(...args){
const [val] = args;
if($background.eq(this) && (val === "transfrom" || "transform" in val)){
throw new Error("禁止对 #playground 使用 .css('transfrom') ,应使用 transform()")
}
return $.fn.css.call(this, ...args);
}
// 在所有变换开始前,获取父元素位置和变换中心位置坐标,后面回来计算很麻烦
const {top, left, width, height} = $background[0].getBoundingClientRect();
const pOrgin = [top + width / 2, left + height / 2];
$background
.transform(
(new AffineMatrix([1, 0, 0, 0, 1, 0, 0, 0, 1]))
.scale(new_scale)
)
子元素设置 "offset" 的时候究竟遇到什么问题,题目描述得不是很清楚,因此这里假设是期望子元素的位置免受"scale"影响。
$("#ball").click(function(){
const $this = $(this);
const offsetX = 300;
const offsetY = 300;
// 计算目标 offset 在变换前的坐标
const originOffset = [offsetX + left - pOrgin[0], offsetY + top - pOrgin[1], 1];
// 计算目标 offset 在变换后的坐标
const scaledOffset =
AffineMatrix
.fromCSSMatrix($background[0].style.transform)
.multi(originOffset);
// 计算该坐标点变换前后的移动路径(向量)
const offsetVector = [
scaledOffset[0] - originOffset[0],
scaledOffset[1] - originOffset[1]
]
// 沿着向量反向移动,就可以“回到”变化前应有的位置
$this.offset({
offsetX - offsetVector[0],
offsetY - offsetVector[1]
})
});
写到这里发现想复杂了,这个方法可以扩展到任意 "transform" 2D 变换,实际上如果只考虑 "scale"
的话所有的计算可以进一步简化,计算难度降低到小学高年级数学水平。
因为真正的“干货”其实只在于计算那个"offsetVector",它的计算公式不过是把矩阵运算展开为线性方程组,很简单:
$$ vx = (x + left - (left + width \div 2)) \times scale - (x + left - (left +
width \div 2))\\ vy = (y + top - (top + height \div 2)) \times scale - (x +
top - (top + height \div 2)) $$
然后让变换后的点“移动”回去就行了:
$$ x' = (x + left - (left + width \div 2)) \times scale - vx \\ y' = (y + top
- (top + height \div 2)) \times scale - vy $$