「FunFace」用一张粉嫩的小脸,来谈谈HTML5 Canvas的某些玩意儿

这年头,HTML5火遍大江南北呀,神马?火遍全球?对!(在国内跟着火的,还有一个叫“H5”的名儿,具体你懂的)

跟着它顺便火的,还有个HTML5的新标签,叫Canvas,专门用于Web界面画画图,搞点交互,模拟,游戏啥的。虽然大家都称Canvas为HTML5的新标签,看起来好像跟HTML语言有什么关联似的,但其实Canvas画图是通过JavaScript来实现的。所以,如果你想学习Canvas画图,你最好要有一定的JavaScript基础。

前言


提到画图,现实下对于没啥美术天赋及基础滴人来说,那简直不易,就是再怎么画,明明你心目中想画的是白龙马,结果画完画风一变成了草泥马-_-| 而程序里头,这画图涉及到图形学,也不易啊,若还想在静态图形的基础上配个动画,啧啧,必须用上各种小学中学乃至大学各种数学物理知识,那个时候,瞬间发现,原来学了介么多年的数理化,总算有用武之地了,那个内牛满面。

“说了这么多不相干的啰嗦,你说,那今天我们到底要整啥?!” ——路人甲

“来,别急嘛,我们先上一张可爱的效果图”

一张粉嫩的欢乐脸



我嘞个去,有点意思嘛!

How to do it 肿么实现?

想看整个实现的所有源文件?请访问全球最大“同性”交友平台GitHub FunFace了解,想切身体验一下?请不必客气地点击此Demo

光这样就行?那我一些关键点还是看不懂啊啊啊~好吧,俺把源码重点部分该注释的都重新注释助于理解之外,并抽取其中的一些核心代码来谈谈HTML5 Canvas的一些东东。

核心一

先定义一个init函数进行一个初始化的工作,将画布的宽高、鼠标的位置坐标、瞳孔的归位控制、眼睛的位置及半径等等变量做初始化。这里会涉及到一个重要的核心点——requestAnimationFrame

起初,我们要完成一些动画效果的话,会经常使用window.setTimout()或者window.setInterval()来定时不断更新元素的DOM状态位置等来实现动画(画面的更新频率必须要达到每秒60次才能让肉眼看到流畅的动画效果)。不过,这样实现动画的方式极为耗费资源,经常出现这样的情况,刚开始比较流畅,几分钟之后动画可能就不行了。

如今,HTML5/CSS3时代,我们要想在Web里实现一些动画,可选择性已经丰富了很多。借助requestAnimationFrame,CSS3的transition,CSS3的animattion+keyframes等等都能实现想要的一些动画。但是,CSS3动画还是有不少局限性,比如不是所有属性都能参与动画、动画缓动效果太少、无法完全控制动画过程等等,所以有的时候我们还是不得不使用setTimeout或setInterval的方式来实现动画,可setTimeout和setInterval有着严重的性能问题,虽然某些现代浏览器对这两函数进行了一些优化,但还是无法跟CSS3的动画性能相提并论。这个时候,就该requestAnimationFrame上场了,这次这个案例主要就是通过requestAnimationFrame来实现。

关于requestAnimationFrame

来看下Mozilla MDN给出的诠释:

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes as an argument a callback to be invoked before the repaint.

也就说,window.requestAnimationFrame()这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数,以满足开发者操作动画的需求。这个方法接受一个函数为参,该函数会在重绘前进行调用。

这个方法的原理,其实大致跟setTimeout/setInterval差不多,通过递归调用同一方法来不断更新画面以达到动起来的效果,但比setTimeout/setInterval更具优势:

  • 浏览器自动专门优化,且重绘的时间间隔紧紧跟随浏览器的刷新频率,动画更流畅;
  • 若页面不是激活状态(隐藏或不可见)下,动画会自动暂停,有效节省CPU、GPU及内存开销;

使用语法:

1
window.requestAnimationFrame(callback);

浏览器最新支持情况

就目前来说,主流现代浏览器都对它提供了较好的支持,包括IE10+,Firefox,Chrome,Safari,Opera等,在移动设备上,除了Opera Mini之外也都支持requestAnimationFrame,如下图所示:



浏览器Polyfill兼容延伸

支持requestAnimationFrame的浏览器有些还是自己的私有实现,所以有些还得加前缀,对于不支持requestAnimationFrame的浏览器,我们只能使用setTimeout,因为两者的使用方式几近相同,所以这两者的兼容并不难。对于支持requestAnimationFrame的浏览器,我们使用requestAnimationFrame,而不支持的我们优雅降级使用传统的setTimeout。以下为封装后统一能兼容各大浏览器的API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var lastTime = 0;
var prefixes = 'webkit moz ms o'.split(' '); //各浏览器前缀
var requestAnimationFrame = window.requestAnimationFrame;
var cancelAnimationFrame = window.cancelAnimationFrame;
var prefix;
//通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
for( var i = 0; i < prefixes.length; i++ ) {
if ( requestAnimationFrame && cancelAnimationFrame ) {
break;
}
prefix = prefixes[i];
requestAnimationFrame = requestAnimationFrame || window[ prefix + 'RequestAnimationFrame' ];
cancelAnimationFrame = cancelAnimationFrame || window[ prefix + 'CancelAnimationFrame' ] || window[ prefix + 'CancelRequestAnimationFrame' ];
}
//如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
if ( !requestAnimationFrame || !cancelAnimationFrame ) {
requestAnimationFrame = function( callback, element ) {
var currTime = new Date().getTime();
//为了使setTimteout的尽可能的接近每秒60帧的效果
var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) );
var id = window.setTimeout( function() {
callback( currTime + timeToCall );
}, timeToCall );
lastTime = currTime + timeToCall;
return id;
};
cancelAnimationFrame = function( id ) {
window.clearTimeout( id );
};
}
//得到兼容各浏览器的API
window.requestAnimationFrame = requestAnimationFrame;
window.cancelAnimationFrame = cancelAnimationFrame;

核心二

我们画画以及实现动画的基础,是确定好每一个画中的元素的坐标,半径,移动方式,移动速度等等,只有确定好了这些数据,才能进行下一步的绘画过程,这就好比炒一个菜,你得先准备好食材,才能进行下一步的加工烹饪。

我们这个案例最重要的就是确定跟随鼠标位置移动的瞳孔如何绘制,这就涉及到瞳孔的中心点(pupilX, pupilY)的实时位置,涉及到如何计算两点之间距离,为了有动画效果还要计算出加速度及偏移量的弹动等等。

比如这次案例里当鼠标移动时所要计算的“鼠标所在点位置”与“眼睛中心点”两点的距离。一般来说,如果是计算任意两点的距离,有两种方法:一种利用勾股定理计算,适用于两点距离很近的情况;一种按标准的球面大圆劣弧长度计算,适用于距离较远的情况。我们这次主要采用前者进行计算。

1
2
3
var dx = mx - this.x,
dy = my - this.y,
dist = Math.sqrt(dx * dx + dy * dy); //计算“鼠标所在点位置”与“眼睛中心点”两点的距离

通过以上计算,就可以算出两点的距离。除了距离,我们还得计算出偏移的角度来,可通过下面计算得出:

1
this.angle = Math.atan2(dy, dx); //偏移角度

偏移角度拿来做什么呢?主要是用来进行下一步计算得到加速度

1
Math.cos(this.angle) * this.magnitude //magnitude为移动距离,大概的范围是:0 =< 移动距离 <= magnitudeMax

最终我们计算出加速度下的偏移量弹动值 + 瞳孔原坐标值 = 最新瞳孔的中心坐标

1
2
3
4
this.pupilX += ((this.x + Math.cos(this.angle) * this.magnitude) - this.pupilX) * 0.1;
//X轴瞳孔中心偏移量的弹动 0.1为弹性系数
this.pupilY += ((this.y + Math.sin(this.angle) * this.magnitude) - this.pupilY) * 0.1;
//Y轴瞳孔中心偏移量的弹动

结尾

待所有的数据都确定准备好了,就可以完成最终的简单绘画过程了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Eye.prototype.draw = function () {
//画眼睛及眼眶
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, 2 * PI);
ctx.fillStyle = '#FFFFFF';
ctx.fill();
ctx.lineWidth = 5;
ctx.strokeStyle = '#424031';
ctx.stroke();
//画瞳孔
ctx.beginPath();
ctx.arc(this.pupilX, this.pupilY, this.pupilRadius, 0, 2 * PI);
ctx.fillStyle = '#424031';
ctx.fill();
};
1
2
3
4
5
//画嘴巴
ctx.beginPath();
ctx.arc(width / 2, height * 0.65, 100, 0, PI);
ctx.fillStyle = '#424031';
ctx.fill();

此刻,一个粉嫩的欢乐小脸,出现在了你的面前 :)