本项目使用使用H5 canvas绘制炫酷的数字时钟,使用ES6 class面向对象开发,目的在于练习js面向对象开发的能力。
炫酷的数字时钟,使用canvas绘制,效果如下图
案例涉及的东西不是特别多:
- canvas的基本使用,绘制圆
- 动画中的requestAnimationFrame与定时器
- 按位运算与数组构建模型
- 如何模拟物理效果(加速度,阻力,碰撞检测)
- js面向对象OOP
要掌握的东西不多,但是其中的思路还是尤为重要的。
此案例的对象有以下:
- Ball 仅指页面中的小球,该对象包含了ball的所有运动和状态参数,如:位置、速度、颜色等
- Tile 每一个数字块就是一个Tile实例,呈现一个数字0-9
- Chunk 一个Chunk实例包涵两个Tile对象,呈现两位数字,
- Sence 负责整个页面的,比如页面的刷新,页面显示的调试信息等
案例其实就是普通的时钟逻辑 + 七段数显 + 小球的物理特性
Ball关键属性
- posX, posY, vx, vy, gravity, bounce, canMove 对象位置、运动参数
- alive, birth, life 对象生命参数
- light, color, radius 对象状态参数
// 给出代码
function Ball(data) {
this.posX = data.posX || 0;
this.posY = data.posY || 0;
this.radius = data.radius || 10;
this.vx = data.vx || Math.random() - 0.5;
this.vy = data.vy || Math.random() - 0.5;
this.alive = true;
this.canMove = false;
this.birth = null;
this.light = false;
this.color = data.color || `#${Math.random().toString(16).substr(3, 6)}`;
}
Ball.prototype = {
init(data) {
this.initData(data);
this.blender();
return this;
},
initData(data) {
this.WIDTH = data.WIDTH;
this.HEIGHT = data.HEIGHT;
this.ctx = data.ctx;
this.life = 1000 * 15;
this.gravity = 0.08;
this.bounce = -0.7;
},
}initData里的属性所有Ball实例都一样,没有必要单独设置,放到原型上,算是优化吧,减少不必要的开销。
值得注意的是颜色的随机使用了截取Math.random()的16进制串,想当年还是傻傻的用
Math.ceil(Math.random() * 255)这里的canMove用了控制Ball是否可以移动,light控制是否启用颜色,还记得数字时钟的数字前后没有改变的时候是固定不动的吗,而且还是没有颜色
当然是清除无用的对象,释放内存了。比如那些小球出了画布已经没有作用了,就可以清除了,这里使用alive标记,这样可以很方便的清除这些无用对象。其实这里面还有一些对象的vx可能特别小,如果等这些对象移出画布等待的事件会很长,所以这里设置了life来记录小球的生命,生命到期就会被标记
具体实现如下:
// 标记生命到期的对象
if (new Date() - this.birth > this.life) {
this.alive = false;
}
// 标记移出画布的对象
if (this.posX - this.radius <= 0 || this.posX >= WIDTH) {
this.alive = false;
}回想高中我们学习的物理知识—抛物线运动
v = at v = v0 + at h = 1/2 * at^2
然而实际却无法用这些,因为这里我们拿不到时间t,那么我们就要换个思路了
-
匀速运动
比如x方向做匀速运动,那么vx势必是定值,然后每次跟新把当前的posX = posX + vx,这样就可以实现匀速运动了
-
加速运动
同理,y方向做加速运动,那么vy势必是变值,不仅每次跟新把当前的posY = posY + vy,还要把这个重力加速度vy = gravity + vy ,这样就可以实现加速运动了
-
阻力呢?
为了达到每次弹跳有衰减,这里引入bounce参数,通过把这个参数设置一个(-1~0)即可实现反弹和衰减,一举两得。
// collide函数中
if (this.posY - this.radius <= 0 || this.posY + this.radius >= HEIGHT) {
// 这里的min max是碰撞检测一个经典的做法
this.posY = Math.min(this.posY, HEIGHT - this.radius);
this.posY = Math.max(this.posY, 0);
this.vy *= this.bounce;
}
// update函数中
if (this.canMove) {
this.posX += this.vx;
this.posY += this.vy;
this.collide();
this.vy += this.gravity;
}好了,至此我们已经完成了Ball对象的构建,使用这个对象已经可以完成一些基本效果。
严复在《天演论》讲到:“译事三难:信、达、雅“,这也是高中英语老师素来强调的,类比一个事物逻辑和程序亦是如此,程序上单单是做到可以跑出来效果是远远不够的,一个程序的好坏也要用”信达雅“来衡量。
当看到矩阵类的图形时,我们的第一印象当然是使用二维数组来描述了,很显然这是没什么大问题的。
就拿此案例里的七段数显时钟,比如一个数字8,我们会用下面的数字来描述
// 使用1、0来描述不同的对象,当然你可以喜欢啥用啥,比如第二种
let NUM_EIGHT = [
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
]
let Eg = [
['■', '■', '■'],
['■', '□', '■'],
['■', '■', '■'],
['■', '□', '■'],
['■', '■', '■'],
]然后呢,构建好这样一个二维数组,我们可以使用双层for遍历这个二维数组来构建我们的视图,这里大家比较容易,就不给出代码了。
接触的多了,大家会发现,使用二维数组虽然简单,但是十个数字定义下来,占用了很大一部分空间(编辑器),最主要的是我们的代码看上去不是很优雅,这就好像一个,那么如何改善呢?
我们知道,七段数显这类矩阵每个点也就两种状态,其实完全可以用1bit表示,有时候使用二进制会让代码看起来更简洁、优雅。
下一步的重点就是如何获取每一位数字了,按位运算符**& >>就派上用了,&按位与的逻辑就是全一为一**,也就是
1 & 1 // 1
1 & 0 // 0
0 & 1 // 0
0 & 0 // 0
这里有些结论
- 任何数 & 同位数全1,还是这个数本身,如0b1011 & 0b1111 = 0b1011
- 取一个数中指定位,如取低位1bit,0b1011 & 0001 = 0b0001(这也是我们今天需要的)
其实这里还缺少一个东西,就是移位操作,这是个很形象的游戏,规则
- 规则是听到口令做动作,口令有两句,左移n位,右移n位
- 凳子上有人的记1,没人的记0
- 没有凳子的人淘汰
结合按位与和移位运算,我们即可完成我们的逻辑,代码也相当简单
let num = 0x7bef
while (num) {
let show = num & 1;
num = num >> 1;
if (show) {
// coding
}
}其他思路,这里再提供一种方式就是使用toString
0x7bef.toString(2).split("")
// ["1", "1", "1", "1", "0", "1", "1", "1", "1", "1", "0", "1", "1", "1", "1"]
后面要做的就是数据和视图的映射了,这里就不给赘述了。
作为刚刚入行的我,每次代码写完后都会思考下如何把代码写的符合”信达雅“,译文推崇的也是我程序所需的,我会尽量把逻辑写的简洁,过程中的思考和内在逻辑会写在PPT里作为笔记,我很少在代码里写详细的注释,即便几个月后看不懂自己写的,那我会翻看我的课件。
Tile类仅仅负责一个数字,案例中的有时、分、秒三块,而且每块都包含两个Tile对象,因此要一个额外的对象记录一些数据,比如当前数字,位置等。
Chunk的核心功能就是把记录的数字映射成对应的Tile对象,并时刻的更新
scene类相对简单很多,主要负责更新页面,调用页面中对象对应的update方法,
早起做动画一般的会用到定时器setInterval,H5 提供一个专门用于请求动画的API,requestAnimationFrame,直译就是请求动画帧。(好难听······)
-
那么两者有区别吗?
requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。屏幕刷新率是nHz,那么回调函数就每1000/n ms被执行一次,比如刷新率是60Hz,那么这个时间间隔就变成了1000/60=16.7ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次。
直接的好处有以下几个:
- 不会引起丢帧现象,也不会导致动画出现卡顿的问题(所谓的函数节流)
- 浏览器页面处理未激活的状态下,比如关闭或切出去,动画不会被执行(CPU开销小,节省资源)
--------------------------已完-----------------------


