写在最前:
前两天我们这边接到公司做教育那块的一个活儿,要做一个比较炫酷的3D旋转特效,拿到网页一看,别人是用Three.js去实现的,确实比较狂拽。但是我对这个只是了解一点点,还不到能用它做出成熟DEMO的地步(学习的道路任重而道远啊...)。所以老大建议用css transform特性加上perspective试试。嗯,活儿都来了,上呗。所以有了这边分享的诞生,但是我的实现肯定比不了Three那么炫酷,将就着看吧,里面有很多数学方面的推导是我请教部门清华的同事完成的,再次感谢他,下面我们来看看吧。
对了,惯例,我的博客点赞哈~
成果展示:
Git地址:
功能说明:
- 以屏幕中心为球心,屏幕横向为X轴,纵向为Y轴,垂直屏幕为Z轴,横向滑动屏幕,页面中的图标会绕着Y轴旋转。纵向滑动,图标会绕着X轴旋转,斜着滑,图标就会斜着转。
- 点击页面中任意一个图标,被点中的图标会旋转到页面的中间(受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变化的效果,说白了就是加上了一个景深的效果,使你的图标看起来有种近大远小的感觉而是数学上的一些空间变化,深感自己高数没有学好啊,是时候复习一波高数的知识了。