旋转动画通过线性组合变换矩阵实现,从而实现放大-缩小效果。

6

我有一个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 transform matrix3d对图像进行转换(仅限于硬件加速的3D)。这个变换是通过requestAnimationFrame实现动画效果的。

例如,我的startMatrix如下:

enter image description here

而endMatrix如下:

enter image description here

线性组合的公式如下:

enter image description here

其中t的值从0到1变化。

变换矩阵的线性组合结果(即图像位置)是正确的。但我的问题是:如果新角度与实际角度相差约180度,则endMatrix的值会从正数变为负数(或反之亦然)。这会导致图像在变换过程中出现缩放效果。

有没有一种方法可以避免这种情况,最好只使用一个变换矩阵?

4个回答

4
直接插值矩阵值会有问题,对于非常小的角度,可能无法观察到不准确性,但是长期来看,您将面临问题。即使您对矩阵进行规范化,更大的角度也会使问题在视觉上明显。
2D旋转非常简单,因此您可以在没有旋转矩阵的情况下很好地完成。最佳方法可能是使用四元数,但它们可能更适用于3D变换。
需要采取以下步骤: 1. 计算旋转、缩放和变换值。如果您已经拥有这些值,则可以跳过此步骤。对于2D矩阵变换,保持这些值分开可能是最容易的。 2. 然后对这些值进行插值。 3. 基于计算构建一个新矩阵。
在动画开始时,您必须从第1步计算值,然后在每个帧上应用第2和第3步。
第1步:获取旋转、缩放和变换 假设起始矩阵为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矩阵的比例是指该矩阵跨越的空间中任意一个基向量的长度,例如:

Scale for non-skewing 2D matrix 是指非扭曲的二维矩阵的比例,具体为该矩阵所跨越的空间中任意一个基向量的长度。

// 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]);

最难的部分是获取矩阵旋转值。好消息是,每次插值只需要计算一次。
对于两个2D矩阵,旋转角度可以基于列向量所创建的向量之间的夹角来计算。如果没有旋转,第一列具有值(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);

第三步构建矩阵

对于每个帧,构建最终矩阵只需将值放入变换矩阵即可。

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]]

然后您有一个二维矩阵,这对于任何3D硬件加速器来说都很容易输入。

免责声明:其中一些代码已经经过测试,但有些代码未经过测试,因此可能会发现错误。


2
这是第一个给我们正确提示的答案:仅使用线性组合无法保留比例。这也是我们问题的解决方案,所以我会把奖励给你。但它带来了一个新问题:旋转中心是任意且不可控的。为了解决这个问题,我们使用了@miks解决方案的调整。现在我们为每个动画步骤计算旋转中心,并通过此构建每个t的矩阵。 - Julian Habekost
调整矩阵以保留不同变换空间的旋转中心有时可能是一项稍微费脑筋的任务...很高兴你做到了! - Tero Tolonen
@JulianHabekost 在我的解决方案中,我首先尝试将所有的变换(旋转除外)合并到一个矩阵中(实际上是两个:旋转前和旋转后),但这似乎对我来说太复杂了;然后我编辑了我的答案。如果你点击“编辑...”你会看到第一个版本。 - mik

3

当动画旋转保持比例时,点不沿着直线移动,而是沿着圆形移动。因此,中间矩阵不是起始矩阵和结束矩阵的线性组合。克服这个问题最简单的方法是在每个动画帧中计算所有变换:

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);

3
此答案假设您知道x、y、角度和比例的起始值。如果您只知道矩阵,请参阅@Tero Tolonen的答案。 - mik
1
@Teron Tolons的回答解决了我们的问题,所以我授予了他赏金。虽然它带来了新的问题,即缺乏旋转中心的控制。我们可以从这个答案中推导出解决方案,为每个t计算一个旋转中心所在的“轨道”。 - Julian Habekost
@JulianHabekost 一般来说,仅凭矩阵无法猜测旋转中心路径。旋转与平移的组合实际上是围绕另一个中心进行旋转,因此当您进行平移时,矩阵会失去有关原始旋转中心的信息。 - mik

2

使用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)
};

要实现这个功能,只需创建起始和结束位置的元素,以及您想要遵循缓动路径的元素。请保留HTML标签。
<div id="start"></div>
<div id="end"></div>
<div id="animateMe"></div>

使用CSS在#start和#end添加任何transform规则。如果要隐藏它们,请设置CSS规则display:none

然后,您可以使用以下方式可视化结果:

//build transform array - 60 frames
var mxArr = document.getElementById('start').matrixTweenArray(document.getElementById('end'), 60);

使用以下方式调用animate函数:

document.getElementById('animateMe').matrixAnimmate(mxArr);

不要忘记在 div 上设置perspective-origin,并在父元素上设置 perspective

https://jsfiddle.net/tnt1/wjunsj36/2/

希望这有所帮助 )


1
但就我所见,这并不能解决旋转过程中改变比例的主要问题。 - mik
@mik,您可以应用任何CSS转换,所以不应该有问题)。 - tnt-rox
1
有趣的解决方案,但是这个 https://jsfiddle.net/s6xjjdmn/ 显示它并不是我的问题的解决方案。 - Alex

-1

你的示例中旋转矩阵的定义域为[-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);
}

注意:我不是 JavaScript 程序员。希望语法是正确的。


语法没问题,但并不是解决问题的方法。使用矩阵线性组合来实现一个保持比例的动画鞋底并非在所有情况下都可行,正如其他答案所解释的那样。 - Julian Habekost

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接