首页 文章

通过线性组合转换矩阵的旋转动画可以实现放大缩小

提问于
浏览
5

我有一个3x3矩阵(startMatrix),它代表图像的实际视图(平移,旋转和缩放) . 现在我创建一个新的矩阵(endMatrix),其中包含一个identitymatrix,新的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

    当一个动画旋转保留比例,点不沿直线移动,而是沿圆圈移动 . 结果,中间矩阵不是起始矩阵和末端矩阵的线性组合 . 克服这个问题的最简单方法是计算每个动画帧期间的所有变换:

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

    如果直接插值矩阵值会出现问题,对于非常小的角度,不能观察到不准确,但从长时间来看,您将遇到问题 . 即使你对矩阵进行归一化,更大的角度也会使问题在视觉上变得明显 .

    2D旋转非常简单,因此无需旋转矩阵方法即可完成 . 最好的方法可能是使用四元数,但它们可能更适合3D转换 .

    要采取的步骤是:

    • 计算旋转,缩放和变换值 . 如果您已经拥有这些,则可以跳过此步骤 . 对于2D矩阵变换,保持这些值是分开的可能是最容易的 .

    • 然后应用插值到这些值

    • 根据计算构造新矩阵

    在动画开始时,您必须计算步骤1中的值一次,然后在每帧处应用步骤2和3 .

    Step 1: get the rotation, scale, transform

    假设起始矩阵为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 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矩阵的旋转角度 . 如果没有旋转,则第一列具有表示x轴的值(1,0),第二列具有表示y轴的值(0,1) .

    通常,矩阵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的Δ旋转 . 这可以类似地从矩阵跨越的矢量之间的点积计算 . 类似地,我们测试交叉乘积以查看旋转方向是否是顺时针(负角度) .

    Step 2: interpolate

    在动画期间获取新值是微不足道的插值:

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

    Step 3 construct the matrix

    对于每个帧构造最终矩阵,您只需将值放入变换矩阵矩阵中

    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硬件加速器提供 .

    免责声明:部分代码已经过测试,其中一些尚未经过测试,因此最有可能发现错误 .

  • 3

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

    并使用以下命令调用animate函数:

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

    别忘了在div上设置 perspective-origin ,在父元素上设置 perspective .

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

    希望这可以帮助 )

  • 2

    示例中的旋转矩阵具有域[-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;
    }
    

    在评估rotation-function中的matrix-elements之前,必须调用此函数:

    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脚本程序员 . 我希望语法是正确的 .

相关问题