在上一篇《每周一点canvas动画》——从2D到3D中,我们讨论了要在2D的平面实现3D的效果,是一件多么复杂的事情。但是对于一些简单的3D效果,使用webGL不仅有杀鸡用牛刀的感觉,而且浏览器的兼容性也是一个很大的问题。所以,我们考虑在2D的canvas中去模拟3D的效果,将其作为我们项目中的降级方案。也许你对在2D的canvas中去模仿3D的效果保有怀疑,这里我先给一个小小的demo,让你直观的感受下,canvas模拟的3D效果到底如何?
是不是很逼真,立体效果不错吧!我记得前段时间淘宝首页有一个简单的3D效果(可是我没找到,不过有那么个印象),以为用了webGL。其实,canvas完全就可以模拟。下面我们介绍3D环境的搭建。
1.坐标系统
前面部分的动画内容之所以是2维的,是因为我们所有的动画都基于一个2维坐标系统。要实现3维的动画效果,除了x轴,y轴,我们还需要另一条坐标轴—— z轴。但是,canvas先天是不具备这条坐标轴的。所以,我们需要手工的去设定这条坐标轴。
在坐标轴的设定上,我们有两种选择,如下图所示:
第一种叫做左手坐标系(左手的食指,中指,大拇指三者垂直,大拇指指向自己),第二种叫做右手坐标系(右手的食指,中指,大拇指三者垂直,大拇指指向外面)。他们之间的区别如图所示,z轴的指向不同。以右手坐标为例,反映到物体的表现上(如果只是考虑物体的大小),当物体朝着z轴的正向运动,那么我们会看到物体变得越来越小。就如第一幅效果图中展示的那样,当物体朝着负方向运动的时候。我们可以看到物体变得越来越大,产生一种朝我们迎面飞来的感觉。另外,在本文中我们默认使用的是右手坐标系。
2.透视(perspective)
2.1 概念
不管你是叫他景深也好,透视也罢。perspective是3D场景中最重要的概念之一,如果你使用过three.js,就会发现camera中有一个参数,就是perspective。另一个你很熟悉的场景,恐怕就是css3中的perspective了吧!如果你对这其中的任何一个有了解,那么perspective的概念就很好理解了。
perspective的作用是确定物体是靠近我们,还是远离我们。因为在2维的空间中,我们只需要两个坐标就可以确定一个物体在平面上的位置。但是,在3维的环境中这是行不通的,两个物体可能具有相同的x坐标,y坐标。但是只要z坐标不相同,我们就不能判断他们两的位置是不是重合的。
那么,怎么在2维的平面去体现透视效果呢?各位看官请试想一下,当篮球从远处飞向你的时候(这里如果我们只考虑一个元素——篮球大小),篮球是不是越来越大呢,当你把篮球扔出去,它是不是越来越小呢!Ok,就是这个理。在2维的平面,我们想要让物体感觉向我们走来,就放大它,同理远离的效果就是让它变小。
除了让它的大小发生变化。另一个比较重要的点是:当物体远离直至消失的过程中,要想模拟三维的消失效果,我们必须让物体的x坐标,y坐标向消失点移动。这个消失点你可以理解为汽车向远方驶去,最终它会逐渐变成黑点消失在地平线上,这个黑点就是消失点。
所以总结下来,我们在perspective这一块要做两件事:
- 放大或者缩小物体
- 让它靠近或者远离消失点
2.2 公式
上面的概念中我们了解了要想形成透视的效果,我们需要做两件事。下图展示了一个侧面视图,人眼代表观察点,蓝色的球体代表屏幕中的物体,人眼距离屏幕的距离为fl,物体与屏幕之间有一段距离z值(这个值在成像的时候并不存在,反映到物体大小的变化上)
物体的大小与这 fl
, z
两者之间满足下面的关系:
scale = fl / (fl + z) = 1/ ( 1+ z/fl)
fl的值就如 css3 中我们设定的 perspective
值。 同理,这里也是我们自己设定的一个值,200,300,500都无所谓啦。
从公式中我们可以得出:
scale的值一般情况下范围为(0,1.0),这个值之后会用在缩放物体的比例与靠近消失点的比例。
试想两种比较极端的情况:
当z轴无限大的时候(也就是朝着z轴的正向持续运动),scale的值就会趋近于0。当z的距离趋近于-fl的时候,scale的值就会变得很大,就像是戳进我们的眼里。
以我们前面使用的小球为例,在draw方法上我们定义
context.scale(this.scaleX, this.scaleY)
当缩放比例确定后,一方面我们确定了球体大小的变化,另一方面我们用物体的坐标乘以这个比例就可以得到物体的新坐标。
可能这样说比较抽象,我们假定 fl=200 ,这时物体的z坐标等于0,由公式可得 scale=1.0 ,那么物体的大小不变,物体的位置不变。如果 z=200 ,那么 scale=0.5 。物体的大小变为原来的 1/2,同时我们要让它现在的坐标乘以缩放比例 scale,得到新的位置。如果原来的为(200,300),那么新坐标就为(100,150)。具体效果如下图:
3.代码实现
先上效果图
var canvas = document.getElementById('canvas'),
context = canvas.getContext('2d'),
ball = new Ball(40, "red"),
mouse = utils.captureMouse(canvas);
var xpos = 0,
//物体的 3D坐标
ypos = 0,
zpos = 0,
fl = 250,
//距离屏幕的距离(焦距)
vpX = canvas.width / 2,
//消失点
vpY = canvas.height / 2;
window.addEventListener('keydown', function(e) {
if (e.keyCode === 38) { //up
zpos += 5;
} else if (e.keyCode === 40) {
zpos -= 5;
}
}, false);
(function drawFrame() {
window.requestAnimationFrame(drawFrame, canvas);
context.clearRect(0, 0, canvas.width, canvas.height);
if (zpos > -fl) {
var scale = fl / (fl + zpos); //缩放比列
xpos = mouse.x - vpX;
ypos = mouse.y - vpY;
ball.scaleX = ball.scaleY = scale; //物体大小变化
ball.x = vpX + xpos * scale; //新坐标
ball.y = vpY + ypos * scale;
ball.visible = true; //物体可见
} else {
ball.visible = false
}
if (ball.visible) {
ball.draw(context);
}
}())
代码相对来说比较简单。首先,我们设定物体的3D坐标 xpos,ypos,zpos
,初始默认为0。然后设定焦距fl = 250
,最后设置消失点(vpX, vpY)
。这里需要注意的地方是,我们设置的消失点为画布的中心。如果不这样做,物体就会向画布的左上角(0,0)处汇集,这并不是我们想要的效果。
接下来,在动画循环中我们根据公式计算缩放比例 scale,然后作用于 ball 的 scale 上,最后计算物体的新位置。这里有个值得注意的点是,我们在外层加了一个判定条件(zpos > -fl)
,这样做的目的是当物体太大的时候,超出了 canvas 画布我们就不再绘制它。
这一节的内容是整个3维效果的核心,后面的所有效果都是基于此。所以请务必弄明白!