简单的立体旋转特效DEMO实现

1,507 阅读6分钟

写在最前:

 前两天我们这边接到公司做教育那块的一个活儿,要做一个比较炫酷的3D旋转特效,拿到网页一看,别人是用Three.js去实现的,确实比较狂拽。但是我对这个只是了解一点点,还不到能用它做出成熟DEMO的地步(学习的道路任重而道远啊...)。所以老大建议用css transform特性加上perspective试试。嗯,活儿都来了,上呗。所以有了这边分享的诞生,但是我的实现肯定比不了Three那么炫酷,将就着看吧,里面有很多数学方面的推导是我请教部门清华的同事完成的,再次感谢他,下面我们来看看吧。

 对了,惯例,我的博客点赞哈~

成果展示:

Git地址:

github.com/ry928330/tr…

功能说明:

  1. 以屏幕中心为球心,屏幕横向为X轴,纵向为Y轴,垂直屏幕为Z轴,横向滑动屏幕,页面中的图标会绕着Y轴旋转。纵向滑动,图标会绕着X轴旋转,斜着滑,图标就会斜着转。
  2. 点击页面中任意一个图标,被点中的图标会旋转到页面的中间(受perspective属性的影响,这个中间是一个近似的中间的位置)。

实现细节:

 按照功能我们从两个方面讲解DEMO的实现,一个是滑动屏幕,图标相应的滚动,另一个是点击图标,旋转到某一正确的位置。

滑屏转动图标:

 因为画图比较麻烦,所以我借助草稿一起来分析下图标的转动,如下图所示:


从图中我们可以清楚的得到绕X、Y轴分别旋转后所在位置的X、Y、Z坐标,但是我要想要斜着滑动,的话就存在空间中的点同时绕X和Y轴的旋转,此时我们只需要要讲两个旋转矩阵相乘即可,即AB或者BA,如果是前者,表示先绕X轴旋转,然后再绕Y轴旋转,如果是后者则表示先绕Y轴旋转,再绕X轴旋转。因为每转动一个很小的角度我们都计算一下空间点的位置,所以你看不出先后顺序,给人的感觉是同时进行的。这里给出代码如下:

/**
 * 旋转函数
 * @param xAng  绕x轴旋转角度,轴向顺时针
 * @param yAng  绕y轴旋转角度,轴向顺时针
 * @param vector  待旋转向量
 */
function rotate(xAng, yAng, vector) {
    xRotMat = [[1, 0, 0], [0, Math.cos(xAng), Math.sin(xAng)], [0, -Math.sin(xAng), Math.cos(xAng)]];

    yRotMat = [[Math.cos(yAng), 0, -Math.sin(yAng)],
                [0, 1, 0],
                [Math.sin(yAng), 0, Math.cos(yAng)]];
    vector = matmul(yRotMat, vector);
    vector = matmul(xRotMat, vector);
    return vector;
}
/**
 * 简单矩阵乘法,不做任何合法性判断
 * A为3*3矩阵  B为1*3 返回1*3矩阵
 * */
function matmul(matA, matB) {
    var result = [];
    for (var i = 0; i < 3; i++) {
        result[i] = 0;
        for (var j = 0; j < 3; j++) {
            result[i] += matA[i][j] * matB[j];
        }
    }
    return result;
}

 得到各个坐标点的值后,通过transform的3d设置,即可完成图标在页面的绘制:

    /**
     * 绘制所有图标
     */
    function printBalls() {
        for (var i = 0; i < objNum; i++) {
            index = i + 1;
            x = objPos[i][0];
            y = objPos[i][1];
            z = objPos[i][2];
            $('.num' + index).css({
                'transform': 'translate3d(' + x + 'px,' + y + 'px,' + z + 'px)'
            });
        }
    }

 这里我们得提一下关于页面结构,如下所示,在css里面我们得把图标的父元素container的perspective熟悉设置一个比图标运动半径大的值,不能太接近。因为perspective 属性用于定义 3D 元素距视图的距离,以像素计。该属性允许你改变 3D 元素查看 3D 元素的视图。当为元素定义 perspective 属性时,其子元素会获得透视效果,而不是元素本身。而且,当子元素的Z值越接近父元素的perspective值时,图标呈现的大小越大,因为此时就相当于图标就在你的眼前,当超出perspective值后,图标就看不见了,这时就相当于是图标成的像落在了你的视网膜后面,你没法儿看见图像了。关于perspective的详解,建议你看看张鑫旭的文章

 <div id="stage">
    <div id="container">
        <div class="num1 word"></div>
        <div class="num2 word"></div>
        <div class="num3 word"></div>
        <div class="num4 word"></div>
    </div>
</div>

点击图标,旋转到特定位置:

 鉴于之前建立的空间坐标,你可能说只要给theta(空间中和Y轴的夹角)和phi(空间中和Z轴的夹角)两个角度设置一个固定的值,就能很容易的转到你想要的位置。但是现在有一个问题,绕Y轴旋转可以得到phi角的变化,但是绕哪个轴的旋转可以得到theta夹角的变化呢。这时请教了完美的清华同事给出了解决方案。以空间所在位置的向量和Y轴的向量构成的平面,做一个法向量,经验证可以得出该法向量的坐标为([z, 0, -x])然后绕着该轴旋转,即可得到theta的变化。但是,新的问题又来了,空间中怎么得到绕某一向量旋转一定角度该如何去计算呢,查阅相关资料,得到了最后的旋转矩阵,代码如下:

/**
 * 绕指定转轴进行旋转,要求转轴必须过原点
 * @param vector 转轴对应的向量
 * @param ang  旋转的角度
 */
function rotateByArbitraryVec(vector, ang) {
    var cos = Math.cos(ang);
    var sin = Math.sin(ang);
    var a = vector[0];
    var b = vector[1];
    var c = vector[2];
    var r = Math.sqrt(a*a+b*b+c*c);
    a = a/r;
    b = b/r;
    c = c/r;
    var rotMat = [[a * a + (1 - a * a) * cos, a * b * (1 - cos) + c * sin, a * c * (1 - cos) - b * sin],
                [a * b * (1 - cos) - c * sin, b * b + (1 - b * b) * cos, b * c * (1 - cos) + a * sin],
                [a * c * (1 - cos) + b * sin, b * c * (1 - cos) - a * sin, c * c + (1 - c * c) * cos]];
    //对所有物体一次操作
    for (var i = 0, len = objNum; i < len; i++) {
       var result = matmul(rotMat, objPos[i]);
       objPos[i][0] = result[0];
       objPos[i][1] = result[1];
       objPos[i][2] = result[2];
    }
}

 这里我们得注意必须对空间旋转向量做归一化,这样旋转后的向量其模值才不会发生改变。有了旋转方程,我们就可以得出最后的计算结果,代码如下:

function moveToCenter(index) {
        // moveToTargetAngAdvance(index, Math.PI / 2, Math.PI / 2, Math.PI / 30);
        var fastMoveId = setInterval((function(index){
                //目标移动角度
                var theta = Math.PI/4; 
                var phi = Math.PI/2;
                //每步需要旋转的度数
                var delta = Math.PI/30;
                var oriTheta, oriPhi;
                var tempArr = calPosAng(index);
                oriTheta = tempArr[0];
                oriPhi = tempArr[1];
                //计算需要旋转的方位角差值
                var theta2Rot = theta - oriTheta;
                var phi2Rot = phi - oriPhi;
                //计算需要的步数
                var step = Math.floor(Math.max(Math.abs(theta2Rot / delta), Math.abs(phi2Rot / delta)));
                if (step==0) {
                    clearInterval(fastMoveId);
                    return;
                }
                //每步需要旋转的方位角
                var dTheta = theta2Rot / step;
                var dPhi = phi2Rot / step;
                // console.log(dTheta - delta, dPhi - delta);
                var x, z;
                var k = 0;
                return function() {
                    k++;
                    // console.log(k, step);
                    x = objPos[index][0];
                    z = objPos[index][2];
                    rotateByArbitraryVec([z, 0, -x], -dTheta);
                    var tempAng = calPosAng(index);
                    var degTheta = tempAng[0]/Math.PI*180;
                    var degPhi = tempAng[1]/Math.PI*180;
                    var alterPhi = tempAng[1] - oriPhi;
                    //绕y轴旋转角度
                    var yAng = (dPhi-alterPhi)/Math.PI*180;
                    rotateByArbitraryVec([0, 1, 0], dPhi-alterPhi);
                    tempAng = calPosAng(index);
                    degTheta = tempAng[0]/Math.PI*180;
                    degPhi = tempAng[1]/Math.PI*180;
                    oriPhi = tempAng[1];
                    // console.info("方位角[", oriTheta, ",", oriPhi, "]");
                    printBalls();
                    if (Math.abs(k - step) <= 0.001 ) {
                        console.log('here');
                        clearInterval(fastMoveId);
                    }
                }
        })(index), 30)
    }

 注意,因为theta的变化会导致phi的变化,所以theta变化过程中导致phi变化的部分,我们再计算phi时得提前减出来。

写在最后:

 其实说起来这篇文章涉及的前端知识并不是很多,主要就是transform搭配perspective能够实现3D变化的效果,说白了就是加上了一个景深的效果,使你的图标看起来有种近大远小的感觉而是数学上的一些空间变化,深感自己高数没有学好啊,是时候复习一波高数的知识了。