变换矩阵线性组合的旋转动画导致放大缩小
Rotational Animation by Linear Combination of Transformation Matrices leads to Zoom-In-Zoom-Out
我有一个 3x3 矩阵 (startMatrix),它表示图像的实际视图(平移、旋转和缩放)。现在我创建一个新矩阵 (endMatrix),它有一个恒等矩阵、新的 x 和 y 坐标、新的角度和新的比例,例如:
endMatrix = translate(identityMatrix, -x, -y);
endMatrix = rotate(endMatrix, angle);
endMatrix = scale(endMatrix, scale);
endMatrix = translate(endMatrix,(screen.width/2)/scale,screen.height/2)/scale);
和功能(标准的东西)
function scale(m,s) {
var n = new Matrix([
[s, 0, 0],
[0, s, 0],
[0, 0, s]
]);
return n.multiply(m);
}
function rotate(m, theta) {
var n = new Matrix([
[Math.cos(theta), -Math.sin(theta), 0],
[Math.sin(theta), Math.cos(theta), 0],
[0, 0, 1]
]);
return n.multiply(m);
}
function translate(m, x, y) {
var n = new Matrix([
[1, 0, x],
[0, 1, y],
[0, 0, 1]
]);
return n.multiply(m);
}
之后,我使用 css 变换矩阵 3d 变换图像(3d 仅用于硬件加速)。此转换使用 requestAnimationFrame.
进行动画处理
比如我的startMatrix是
和 endMatrix
线性组合如下所示:
t 从 0 到 1
变换矩阵线性组合的结果(得到的图像位置)是正确的,我现在的问题是:如果新角度与实际角度相差180度左右,则endMatrix值由正变为负(或相反)。这会导致转换图像的动画中出现放大缩小效果。
有没有一种方法可以最好地使用一个矩阵进行转换来防止这种情况?
您示例中的旋转矩阵的定义域为 [-180°,180°]。大于 180° 的度数在函数域之外。
您可以通过以下方式将旋转角度映射到正确的域:
function MapToDomain(theta){
/* mapping abitraty rotation-angle to [0,2*PI] */
var beta = theta % (2*Math.PI);
/* mapping [0,2*PI] -> [-PI,PI] */
if (beta > (Math.PI/2) ) { beta = Math.PI/2-beta; }
return beta;
}
必须在计算旋转函数中的矩阵元素之前调用此函数:
function rotate(m, theta) {
var beta = MapToDomain(theta);
var n = new Matrix([
[Math.cos(beta), -Math.sin(beta), 0],
[Math.sin(beta), Math.cos(beta), 0],
[0, 0, 1]
]);
return n.multiply(m);
}
注意:我不是 java-script 程序员。我希望语法是正确的。
使用JavaScript计算变换是有效绕过了硬件加速的核心功能之一。使用 CSS 进行转换的效率要高得多。
从你的问题中不清楚你试图用这种技术实现什么,但这种方法可能会有所帮助。我用它来创建用于矩阵转换的补间数组。
HTMLElement.prototype.get3dMatrixArray = function () {
var st = window.getComputedStyle(this, null),
mx = (st.getPropertyValue("transform") || st.getPropertyValue("-o-transform") || st.getPropertyValue("-ms-transform") || st.getPropertyValue("-moz-transform") || st.getPropertyValue("-webkit-transform") || 'none').replace(/\(|\)| |"/g, ''),
arr = [];
if (mx.indexOf('matrix3d') > -1) {
arr = mx.replace('matrix3d', '').split(',');
} else if (mx.indexOf('matrix') > -1) {
arr = mx.replace('matrix', '').split(',');
arr.push(0, 1);
[2, 3, 6, 7, 8, 9, 10, 11].map(function (i) {
arr.splice(i, 0, 0);
});
} else return mx;
return arr.map(function (v) {
return parseFloat(v)
});
};
HTMLElement.prototype.set3dMatrix = function (mx) {
if (Object.prototype.toString.call(mx) === '[object Array]')
this.style.webkitTransform = this.style.msTransform = this.style.MozTransform = this.style.OTransform = this.style.transform = 'matrix3d(' + mx.join(",") + ')';
return this;
};
HTMLElement.prototype.matrixTweenArray = function (endEl, steps) {
function _tween(b, a, e) {
b = b.get3dMatrixArray();
var f = a.get3dMatrixArray();
a = [];
for (var c = 1; c < e + 1; c++) {
var d = -1;
a.push(b.map(function (v) {
d++;
return v != f[d] ? v - (v - f[d]) / e * c : v;
}));
}
return a;
}
return _tween(this, endEl, steps);
};
HTMLElement.prototype.matrixAnimmate = function (matrixArr) {
var that = this,
pointer = 0;
function _frameloop() {
that.set3dMatrix(matrixArr[pointer]);
pointer++;
pointer === matrixArr.length && (pointer = 0);
requestAnimationFrame(_frameloop);
}
requestAnimationFrame(_frameloop)
};
要实现这一点,只需为开始位置和结束位置创建一个元素,以及要遵循补间路径的元素。
<div id="start"></div>
<div id="end"></div>
<div id="animateMe"></div>
使用 css 添加任何 transform
规则到#start 和#end。如果你想隐藏它们,请设置 css 规则 display:none
。
然后您可以使用以下方法可视化结果:
//build transform array - 60 frames
var mxArr = document.getElementById('start').matrixTweenArray(document.getElementById('end'), 60);
并使用 :
调用动画函数
document.getElementById('animateMe').matrixAnimmate(mxArr);
不要忘记在 div 上设置 perspective-origin
并在其父元素上设置 perspective
。
https://jsfiddle.net/tnt1/wjunsj36/2/
希望对您有所帮助)
如果直接对矩阵值进行插值会出现问题,对于非常小的角度,无法观察到不准确,但在较长的 运行 中,您会遇到问题。即使您对矩阵进行归一化,更大的角度也会使问题在视觉上变得明显。
二维旋转很简单,不用旋转矩阵也能搞定。最好的方法可能是使用四元数,但四元数可能更适合 3D 转换。
要采取的步骤是:
- 计算旋转、缩放和变换值。如果你已经有了这些,你可以跳过这一步。对于 2D 矩阵变换,将这些值分开可能是最简单的。
- 然后对这些值应用插值
- 根据计算构建新矩阵
在动画的开头,您必须计算一次第 1 步的值,然后在每一帧应用第 2 步和第 3 步。
第一步:获取旋转、缩放、变换
假设起始矩阵为S,结束矩阵为E。
转换值只是最后一列,例如
var start_tx = S[0][2];
var start_ty = S[1][2];
var end_tx = E[0][2];
var end_ty = E[1][2];
非倾斜 2D 矩阵的比例只是 space 矩阵所跨越的基本向量之一的长度,例如
// scale is just the length of the rotation matrixes vector
var startScale = Math.sqrt( S[0][0]*S[0][0] + S[1][0]*S[1][0]);
var endScale = Math.sqrt( E[0][0]*E[0][0] + E[1][0]*E[1][0]);
最难的部分是获取矩阵的旋转值。好处是每次插值只需计算一次。
两个二维矩阵的旋转角度可以根据矩阵列所创建的向量之间的角度来计算。如果没有旋转,第一列的值 (1,0) 表示 x 轴,第二列的值 (0,1) 表示 y 轴。
一般矩阵S的x轴位置表示为
(S[0][0], S[0][1])
y轴指向方向
(S[1][0], S[1][1])
对于任何 2D 3x3 矩阵都是一样的,比如 E.
使用此信息,您可以仅使用标准向量数学来确定两个矩阵之间的旋转角度 - 如果我们假设没有倾斜。
// normalize column vectors
var s00 = S[0][0]/ startScale; // x-component
var s01 = S[0][1]/ startScale; // y-component
var e00 = E[0][0]/ endScale; // x-component
var e01 = E[0][1]/ endScale; // y-component
// calculate dot product which is the cos of the angle
var dp_start = s00*1 + s01*0; // base rotation, dot prod against x-axis
var dp_between = s00*e00 + s01*e01; // between matrices
var startRotation = Math.acos( dp_start );
var deltaRotation = Math.acos( dp_between );
// if detect clockwise rotation, the y -comp of x-axis < 0
if(S[0][1]<0) startRotation = -1*startRotation;
// for the delta rotation calculate cross product
var cp_between = s00*e01 - s01*e00;
if(cp_between<0) deltaRotation = deltaRotation*-1;
var endRotation = startRotation + deltaRotation;
此处 startRotation 仅根据矩阵第一个值的 acos 计算得出。然而,第二列第一个值,即 -sin(angle) 大于零,则矩阵已顺时针旋转,角度必须为负。必须这样做,因为 acos 只给出正值。
另一种思考方式是考虑叉积 s00*e01 - s01*e00,其中起始位置 (s00,s01) 是 x 轴,其中 s00 == 1 和 s01 == 0 以及结束位置 (e00 , e01) 是 ( S[0][0], S[0][1] ) 创建叉积
1 * S[0][1] - 0 * S[0][0]
即S[0][1]。如果该值为负,则 x 轴已转向顺时针方向。
对于 endRotation 我们需要从 S 到 E 的增量旋转。这可以通过矩阵跨越的向量之间的点积类似地计算。同样,我们做叉积测试,看旋转方向是否为顺时针(负角)。
第 2 步:插值
在动画期间获取新值是微不足道的插值:
var scale = startScale + t*(endScale-startScale);
var rotation = startRotation + t*(endRotation-startRotation);
var tx = start_tx + t*(end_tx-start_tx);
var ty = start_ty + t*(end_ty-start_ty);
步骤3构建矩阵
为每一帧构造最终矩阵,您只需将值放入变换矩阵矩阵
var cs = Math.cos(rotation);
var sn = Math.sin(rotation);
var matrix_values = [[scale*cs, -scale*sn, tx], [scale*sn, scale*cs, ty], [0,0,1]]
然后你有一个 2D 矩阵,它也很容易提供给任何 3D 硬件加速器。
免责声明:部分代码已经过测试,部分代码尚未经过测试,因此很可能会发现错误。
当动画旋转保留比例时,点不会沿着直线移动,而是沿着圆圈移动。因此,中间矩阵不是起始矩阵和结束矩阵的线性组合。克服这个问题的最简单方法是计算每个动画帧期间的所有变换:
scale = startScale*(1-t)+endScale*t;
transformMatrix = translate(identityMatrix, -startX*(1-t)-endX*t, -startY*(1-t)-endY*t);
transformMatrix = rotate(transformMatrix, startAngle*(1-t)+endAngle*t);
transformMatrix = scale(transformMatrix, scale);
transformMatrix = translate(transformMatrix,screen.width/2/scale,screen.height/2/scale);
我有一个 3x3 矩阵 (startMatrix),它表示图像的实际视图(平移、旋转和缩放)。现在我创建一个新矩阵 (endMatrix),它有一个恒等矩阵、新的 x 和 y 坐标、新的角度和新的比例,例如:
endMatrix = translate(identityMatrix, -x, -y);
endMatrix = rotate(endMatrix, angle);
endMatrix = scale(endMatrix, scale);
endMatrix = translate(endMatrix,(screen.width/2)/scale,screen.height/2)/scale);
和功能(标准的东西)
function scale(m,s) {
var n = new Matrix([
[s, 0, 0],
[0, s, 0],
[0, 0, s]
]);
return n.multiply(m);
}
function rotate(m, theta) {
var n = new Matrix([
[Math.cos(theta), -Math.sin(theta), 0],
[Math.sin(theta), Math.cos(theta), 0],
[0, 0, 1]
]);
return n.multiply(m);
}
function translate(m, x, y) {
var n = new Matrix([
[1, 0, x],
[0, 1, y],
[0, 0, 1]
]);
return n.multiply(m);
}
之后,我使用 css 变换矩阵 3d 变换图像(3d 仅用于硬件加速)。此转换使用 requestAnimationFrame.
进行动画处理比如我的startMatrix是
和 endMatrix
线性组合如下所示:
t 从 0 到 1
变换矩阵线性组合的结果(得到的图像位置)是正确的,我现在的问题是:如果新角度与实际角度相差180度左右,则endMatrix值由正变为负(或相反)。这会导致转换图像的动画中出现放大缩小效果。
有没有一种方法可以最好地使用一个矩阵进行转换来防止这种情况?
您示例中的旋转矩阵的定义域为 [-180°,180°]。大于 180° 的度数在函数域之外。
您可以通过以下方式将旋转角度映射到正确的域:
function MapToDomain(theta){
/* mapping abitraty rotation-angle to [0,2*PI] */
var beta = theta % (2*Math.PI);
/* mapping [0,2*PI] -> [-PI,PI] */
if (beta > (Math.PI/2) ) { beta = Math.PI/2-beta; }
return beta;
}
必须在计算旋转函数中的矩阵元素之前调用此函数:
function rotate(m, theta) {
var beta = MapToDomain(theta);
var n = new Matrix([
[Math.cos(beta), -Math.sin(beta), 0],
[Math.sin(beta), Math.cos(beta), 0],
[0, 0, 1]
]);
return n.multiply(m);
}
注意:我不是 java-script 程序员。我希望语法是正确的。
使用JavaScript计算变换是有效绕过了硬件加速的核心功能之一。使用 CSS 进行转换的效率要高得多。
从你的问题中不清楚你试图用这种技术实现什么,但这种方法可能会有所帮助。我用它来创建用于矩阵转换的补间数组。
HTMLElement.prototype.get3dMatrixArray = function () {
var st = window.getComputedStyle(this, null),
mx = (st.getPropertyValue("transform") || st.getPropertyValue("-o-transform") || st.getPropertyValue("-ms-transform") || st.getPropertyValue("-moz-transform") || st.getPropertyValue("-webkit-transform") || 'none').replace(/\(|\)| |"/g, ''),
arr = [];
if (mx.indexOf('matrix3d') > -1) {
arr = mx.replace('matrix3d', '').split(',');
} else if (mx.indexOf('matrix') > -1) {
arr = mx.replace('matrix', '').split(',');
arr.push(0, 1);
[2, 3, 6, 7, 8, 9, 10, 11].map(function (i) {
arr.splice(i, 0, 0);
});
} else return mx;
return arr.map(function (v) {
return parseFloat(v)
});
};
HTMLElement.prototype.set3dMatrix = function (mx) {
if (Object.prototype.toString.call(mx) === '[object Array]')
this.style.webkitTransform = this.style.msTransform = this.style.MozTransform = this.style.OTransform = this.style.transform = 'matrix3d(' + mx.join(",") + ')';
return this;
};
HTMLElement.prototype.matrixTweenArray = function (endEl, steps) {
function _tween(b, a, e) {
b = b.get3dMatrixArray();
var f = a.get3dMatrixArray();
a = [];
for (var c = 1; c < e + 1; c++) {
var d = -1;
a.push(b.map(function (v) {
d++;
return v != f[d] ? v - (v - f[d]) / e * c : v;
}));
}
return a;
}
return _tween(this, endEl, steps);
};
HTMLElement.prototype.matrixAnimmate = function (matrixArr) {
var that = this,
pointer = 0;
function _frameloop() {
that.set3dMatrix(matrixArr[pointer]);
pointer++;
pointer === matrixArr.length && (pointer = 0);
requestAnimationFrame(_frameloop);
}
requestAnimationFrame(_frameloop)
};
要实现这一点,只需为开始位置和结束位置创建一个元素,以及要遵循补间路径的元素。
<div id="start"></div>
<div id="end"></div>
<div id="animateMe"></div>
使用 css 添加任何 transform
规则到#start 和#end。如果你想隐藏它们,请设置 css 规则 display:none
。
然后您可以使用以下方法可视化结果:
//build transform array - 60 frames
var mxArr = document.getElementById('start').matrixTweenArray(document.getElementById('end'), 60);
并使用 :
调用动画函数document.getElementById('animateMe').matrixAnimmate(mxArr);
不要忘记在 div 上设置 perspective-origin
并在其父元素上设置 perspective
。
https://jsfiddle.net/tnt1/wjunsj36/2/
希望对您有所帮助)
如果直接对矩阵值进行插值会出现问题,对于非常小的角度,无法观察到不准确,但在较长的 运行 中,您会遇到问题。即使您对矩阵进行归一化,更大的角度也会使问题在视觉上变得明显。
二维旋转很简单,不用旋转矩阵也能搞定。最好的方法可能是使用四元数,但四元数可能更适合 3D 转换。
要采取的步骤是:
- 计算旋转、缩放和变换值。如果你已经有了这些,你可以跳过这一步。对于 2D 矩阵变换,将这些值分开可能是最简单的。
- 然后对这些值应用插值
- 根据计算构建新矩阵
在动画的开头,您必须计算一次第 1 步的值,然后在每一帧应用第 2 步和第 3 步。
第一步:获取旋转、缩放、变换
假设起始矩阵为S,结束矩阵为E。
转换值只是最后一列,例如
var start_tx = S[0][2];
var start_ty = S[1][2];
var end_tx = E[0][2];
var end_ty = E[1][2];
非倾斜 2D 矩阵的比例只是 space 矩阵所跨越的基本向量之一的长度,例如
// scale is just the length of the rotation matrixes vector
var startScale = Math.sqrt( S[0][0]*S[0][0] + S[1][0]*S[1][0]);
var endScale = Math.sqrt( E[0][0]*E[0][0] + E[1][0]*E[1][0]);
最难的部分是获取矩阵的旋转值。好处是每次插值只需计算一次。
两个二维矩阵的旋转角度可以根据矩阵列所创建的向量之间的角度来计算。如果没有旋转,第一列的值 (1,0) 表示 x 轴,第二列的值 (0,1) 表示 y 轴。
一般矩阵S的x轴位置表示为
(S[0][0], S[0][1])
y轴指向方向
(S[1][0], S[1][1])
对于任何 2D 3x3 矩阵都是一样的,比如 E.
使用此信息,您可以仅使用标准向量数学来确定两个矩阵之间的旋转角度 - 如果我们假设没有倾斜。
// normalize column vectors
var s00 = S[0][0]/ startScale; // x-component
var s01 = S[0][1]/ startScale; // y-component
var e00 = E[0][0]/ endScale; // x-component
var e01 = E[0][1]/ endScale; // y-component
// calculate dot product which is the cos of the angle
var dp_start = s00*1 + s01*0; // base rotation, dot prod against x-axis
var dp_between = s00*e00 + s01*e01; // between matrices
var startRotation = Math.acos( dp_start );
var deltaRotation = Math.acos( dp_between );
// if detect clockwise rotation, the y -comp of x-axis < 0
if(S[0][1]<0) startRotation = -1*startRotation;
// for the delta rotation calculate cross product
var cp_between = s00*e01 - s01*e00;
if(cp_between<0) deltaRotation = deltaRotation*-1;
var endRotation = startRotation + deltaRotation;
此处 startRotation 仅根据矩阵第一个值的 acos 计算得出。然而,第二列第一个值,即 -sin(angle) 大于零,则矩阵已顺时针旋转,角度必须为负。必须这样做,因为 acos 只给出正值。
另一种思考方式是考虑叉积 s00*e01 - s01*e00,其中起始位置 (s00,s01) 是 x 轴,其中 s00 == 1 和 s01 == 0 以及结束位置 (e00 , e01) 是 ( S[0][0], S[0][1] ) 创建叉积
1 * S[0][1] - 0 * S[0][0]
即S[0][1]。如果该值为负,则 x 轴已转向顺时针方向。
对于 endRotation 我们需要从 S 到 E 的增量旋转。这可以通过矩阵跨越的向量之间的点积类似地计算。同样,我们做叉积测试,看旋转方向是否为顺时针(负角)。
第 2 步:插值
在动画期间获取新值是微不足道的插值:
var scale = startScale + t*(endScale-startScale);
var rotation = startRotation + t*(endRotation-startRotation);
var tx = start_tx + t*(end_tx-start_tx);
var ty = start_ty + t*(end_ty-start_ty);
步骤3构建矩阵
为每一帧构造最终矩阵,您只需将值放入变换矩阵矩阵
var cs = Math.cos(rotation);
var sn = Math.sin(rotation);
var matrix_values = [[scale*cs, -scale*sn, tx], [scale*sn, scale*cs, ty], [0,0,1]]
然后你有一个 2D 矩阵,它也很容易提供给任何 3D 硬件加速器。
免责声明:部分代码已经过测试,部分代码尚未经过测试,因此很可能会发现错误。
当动画旋转保留比例时,点不会沿着直线移动,而是沿着圆圈移动。因此,中间矩阵不是起始矩阵和结束矩阵的线性组合。克服这个问题的最简单方法是计算每个动画帧期间的所有变换:
scale = startScale*(1-t)+endScale*t;
transformMatrix = translate(identityMatrix, -startX*(1-t)-endX*t, -startY*(1-t)-endY*t);
transformMatrix = rotate(transformMatrix, startAngle*(1-t)+endAngle*t);
transformMatrix = scale(transformMatrix, scale);
transformMatrix = translate(transformMatrix,screen.width/2/scale,screen.height/2/scale);