canvas实现绚丽的倒计时效果优化与扩展(三)

目录
文章目录隐藏
  1. 性能优化
  2. 屏幕自适应
  3. 改进时间管理小工具

经过之前的课程 《canvas 实现绚丽的倒计时效果与动画基础(一)》和《canvas 实现绚丽的倒计时效果与动画基础(二)》相信大家已经完全实现并掌握了倒计时效果,但是对于我们的程序还是有些问题,是可以进行优化的,或者说是更完善的。下面我将针对这些问题进行简单介绍并带领大家一起优化我们的作品。

性能优化

第一个大的问题可能大家也发现了,在我们 update 的过程中,我们在这个 addBalls 函数里不停地在往我们的 canvas 里增加新的小球,但是有增加却没有减少,这会导致什么情况呢?为此我在 update 函数里的 updateBalls()下面增加了一句代码:

console.log( balls.length);

也就是在控制台打印出来 balls 的数组的长度,大家会看到每隔 50 毫秒会调用一次 update 函数,在 update 函数里就会打印出 balls.length,数组元素是只增无减的,越来越多,这样的程序肯定是不能长时间运行的,每一个计算机都有它的极限,balls 数组在不停的吃我们的内存,但是大家可以分析一下,当这个小球已经走出这个画面的时候,此时这个小球就可以不留在这个数组里,如果我们有个机制能够维护这个数组,让走出这个画面的小球从数组里删去的话,这样这个程序就可以长时间运行了。具体如何实现呢,我们来看一下代码:

function updateBalls(){

    for( var i = 0 ; i < balls.length ; i ++ ){

        balls[i].x += balls[i].vx;
        balls[i].y += balls[i].vy;
        balls[i].vy += balls[i].g;

        if( balls[i].y >= WINDOW_HEIGHT-RADIUS ){
            balls[i].y = WINDOW_HEIGHT-RADIUS;
            balls[i].vy = - balls[i].vy*0.75;
        }
    }

    var cnt = 0
    for( var i = 0 ; i < balls.length ; i ++ )
        if( balls[i].x + RADIUS > 0 && balls[i].x -RADIUS < WINDOW_WIDTH )
            balls[cnt++] = balls[i]

    while( balls.length > cnt ){
        balls.pop();
    }
}

我把对小球数量的控制也放到 updateBalls 函数中,毕竟在这我们要删去小球数量,也是对 balls 的一个 update,思路:首先对 balls 数据进行一次遍历,在这个遍历中要对每一个小球的当前位置进行判断,看看它现在是否还在画面内,如何判断呢?就是这个小球的右边缘仍然大于 0,那么一个小球的右边缘就是它的中心坐标的位置再加上它的半径,如果这个位置仍然比 0 大的话,说明这个小球一定还有一部分留在画面里,与此同时它又不能越过整个画面,那么我们为了判断这一点呢我们还要再加上一个条件,这个条件就是小球本身的左边缘一定要比整个画面的长度还要小,也就是这个小球的中心位置减去半径一定要小于 WINDOW_WIDTH,这样这个小球还留在画面中我们可以把这个小球放在个数组里。具体对这个小球的保留我是用了一个小技巧,首先我声明一个变量 cnt,初始化为 0,这个数量将计划有多少个小球还保留在整个画布中,一旦我找到这样的小球我就就进行这样一个操作“balls[cnt++] = balls[i]”请大家想一下这样做的效果是什么样子,我们在遍历的过程当中由于并非所有的小球都满足于 if 语句,所以“[i]”的索引一定是大于等于“[cnt++]”的,由于 cnt 在不停的“++”,所以 cnt 是逐步的从 0、1、2、3 等等一直往上积累的,也就是说在这样的一个写法下一旦我们发现了符合规则的小球我们就把这样的小球挤在了前面,这样一来当整个循环结束以后,从 0 到 cnt 减 1 就都是留在画布的小球了。而从 cnt 开始一直到当前的 balls.length-1 这段长度里的小球就都不是在画布里的小球了,当然,在极端的情况下 i 和 cnt 正好相等,不过这样也没有关系,说明整个数组里的小球全部都留在画面中,请大家再体会一下这样的写法。

当我们完成了这一步后,我们就知道了在这个数组中前 cnt 个小球都是我们需要的,我们对 cnt 后面的小球都可以删掉,具体怎么删呢?我们可以使用这样一个循环:

while( balls.length > cnt ){
        balls.pop();
}

只要当前的 balls.length 比 cnt 还大的话,我们就 pop 把这个末尾数组的小球给删掉了,这样我们就成功的维护了这个数组,使得数组内的小球总是出现在画面上,减少我们的内存空间,具体实验希望大家在自己电脑上体会一下。

还值得一提的是,我们现在的动画相对比较简单,对这个小球运动位置的计算量比较低,所以我们把所有能融在画面上的小球全都渲染出来也是没问题的,但在某些情况下可能由于我们计算机的限制或者运算量的限制,我们只能保留有限个小球,这种时候我们还可以把 cnt 做一个改动,如下:

while( balls.length > Math.min(300,cnt) ){
        balls.pop();
}

可以看到,我现在把 cnt 改写成 Math.min(300,cnt)的意思是这个 balls.length 的长度保留 cut 和 300 之间的最小值,如何理解这个意思呢?大家想象一下,如果我们算出来的 cnt 比 300 小,那我们 balls 里就取 cnt 个小球,但是如果我们算出来的 cnt 比 300 还大,比如说 500,那我们就取 300 个小球,因为我们可能通过其他的测验知道计算 300 个小球的位置是当前我们计算机的极限了,当然这只是一个小技巧,说起动画的性能还是一个很大的话题,其中有额很多技术、技巧,相应的工具可以探讨,我们后续有时间在和大家一一介绍。

屏幕自适应

接下来我们探讨第二个问题即屏幕自适应的问题,在我们这个程序里做屏幕自适应其实非常简单,因为我们所有物体尺寸都是由下面变量控制的:

var WINDOW_WIDTH = 1024;
var WINDOW_HEIGHT = 768;
var RADIUS = 8;
var MARGIN_TOP = 60;
var MARGIN_LEFT = 30;

之前我们把这些变量写死,如果想做屏幕自适应呢,只需要将这些变量根据当前的屏幕状态而变化就好了,在这里我举个简单的例子。比如说我们画布的 WINDOW_WIDTH 和 WINDOW_HEIGHT 就可以用 js 的方法来赋值:

WINDOW_WIDTH = document.body.clientWidth
WINDOW_HEIGHT = document.body.clientHeight

这里有一个 css 的小技巧,如果我们这样调用 body.clientWidth 是得不到整个屏幕的高度,我们必须在 HTML 里面把这个屏幕的高度整个撑起来,为此我在 body 加一个 style,height 是 100%,同时,我们在做自适应 canvas 中之前的 display:block;margin:50px auto;这些 css 就不需要了,但是我们需要 canvas 这个元素把整个屏幕给撑起来,所以也写一个 height 等于 100%。

接下来我们看一下 MARGIN_LEFT,如果我希望这一些倒计时钟表上的文字占整个屏幕大约五分之四左右,这样两侧空白就要各占五分之一,那么左侧一侧的空白大概就要占整个屏幕宽度的十分之一,有了这样的计算,我 MARGIN_LEFT 就可以这样写:

MARGIN_LEFT = Math.round(WINDOW_WIDTH /10);

MARGIN_LEFT 就等于我刚刚得到的 WINDOW_WIDTH 除以 10 十分之一,之后我再进行一个四舍五入取整。计算出这些之后我们就可以据此计算出我们画的每一个小球半径 RADIUS 了, 如下计算方法:

RADIUS = Math.round(WINDOW_WIDTH * 4 / 5 / 108)-1

解释一下上面代码,我们用 WINDOW_WIDTH 乘以 4 除以 5 也就是乘以一个五分之四,那我们算出的是整个时钟的这一些文字它们一共占得宽度是多少,是 WINDOW_WIDTH 乘以五分之四,之后这里我为什么要除以 108 呢,不知道大家还记不记得之前我们的那些计算,我们在 render 的时候计算过每个数字距离左边距的位置,如下:

renderDigit( MARGIN_LEFT , MARGIN_TOP , parseInt(hours/10) , cxt )
    renderDigit( MARGIN_LEFT + 15*(RADIUS+1) , MARGIN_TOP , parseInt(hours%10) , cxt )
    renderDigit( MARGIN_LEFT + 30*(RADIUS + 1) , MARGIN_TOP , 10 , cxt )
    renderDigit( MARGIN_LEFT + 39*(RADIUS+1) , MARGIN_TOP , parseInt(minutes/10) , cxt);
    renderDigit( MARGIN_LEFT + 54*(RADIUS+1) , MARGIN_TOP , parseInt(minutes%10) , cxt);
    renderDigit( MARGIN_LEFT + 69*(RADIUS+1) , MARGIN_TOP , 10 , cxt);
    renderDigit( MARGIN_LEFT + 78*(RADIUS+1) , MARGIN_TOP , parseInt(seconds/10) , cxt);
    renderDigit( MARGIN_LEFT + 93*(RADIUS+1) , MARGIN_TOP , parseInt(seconds%10) , cxt);

我们最后一个数字距离左边距是 MARGIN_LEFT + 93*(RADIUS+1),我们又说过每一个数字呢我们给他占的空间呢是 15 个 RADIUS+1,所以这些数字加起来总共是 108 个 RADIUS+1 ,所以在这里我们得到了整个数字所花的空间以后又除以了一个 108,即 Math.round(WINDOW_WIDTH * 4 / 5 / 108),注意这里头我们得出的结果是半径加 1,我们给它取整之后再减 1,最后就计算出每个小球的半径了。

最后给 MARGIN_TOP 赋值是比较简单的了,因为 MARGIN_TOP 和前面计算的东西基本上没有关系,比如说我想要 MARGIN_TOP 是整个屏幕高度的五分之一,我只要这么些就好了:

MARGIN_TOP = Math.round(WINDOW_HEIGHT /5);

讲到这儿,我就把和位置相关的变量都进行了赋值,这样赋值以后我们的程序对当前的屏幕进行了一次自适应,我们来看一下效果:

倒计时屏幕自适应优化

此时程序运行起来数字更加大些了,因为它更加匹配我现在使用的大屏幕,是不是非常的酷

改进时间管理小工具

很多小伙伴在自己的电脑上运行不能正确的执行,如下效果:

改进时间管理小工具

这是为什么呢?接下来我就为大家讲解一下这个问题,同时也对这个源码进行一次改造,看看这样一个程序还能有怎样的扩展,其实只要大家仔细看了前面的课程介绍就会知道,现在的这个程序这样设计有一定的局限性,因为对于小时这个数字的显示,我只给出了两位数字的位置,也就是最多能够显示 99 小时,所以我的 endTime 超出了 99 个小时以外的时间就会出现 bug。而另一方面,如果大家有印象的话,我们在 getCurrentShowTimeSeconds 函数里我们会计算结束的时间距离现在的时间的距离,如果这个距离小于 0 也就是说结束的时间比现在的时间还要早的话,那么就会返回 0,这个返回的 0 就是我们为什么会在屏幕上显示 00:00:00 的原因,那么具体怎么改呢,很简单,我们要把 endTime 改成距离现在之后 99 个小时之内的一个时间,在这里大家注意一点 javascript 的 Date 函数在创建的时候有一个小的陷阱,以我们写的这个例子为例,我传入的参数是 new Date(2018,6,11,18,47,52),那代表的是 2014 年 6 月 11 日 18 点 47 分 52 秒呢,答案是否定的,问题的关键是月份,在 javascript 中月份是从 0 开始的,依次采用 0~11,这 12 个数字表示从 1 月到 12 月,所以 6 代表的是 7 月份,很多小伙伴修改了 endTime 最后还是失败,就是因为这个月份有问题,没有从 0 开始计算。在这里我举个例子,我现在写这篇文章的时间是 2018 年 12 月 21 日,如果我想设置倒计时的时间到计时到 2018 年 12 月 22 日 18 点 47 分 52 秒,在修改上月份就应该填 11,因为 11 代表的是 12 月,而日子呢就要填 22,这样修改就好了。

另外有的小伙伴认为这样每次修改 endTime 很麻烦的,那么有没有什么方式可以自动的让 endTime 就距离现在的时间之后比如之后的一个小时呢,答案是肯定的,想要修改这个代码只需要对 javascript 的 Date 对象有一定的了解即可,我们一起来看一下,此时 endTime 还是 Date 对象,但是不需要我们指定这个具体的时间了,所以我们直接使用 new Date()来声明这个对象,这样一来 endTime 其实此时指定的是当前时间,如果想设定它成为当前之后一个小时这个时间呢,在具体使用时需要调用 endTime 的 setTime()方法,那么 setTime()方法传入一个数值,这个数值表示的是从 1970 年 1 月 1 日开始的一个毫秒数,就是所谓的时间戳的概念。那么用这样一个时间戳表示 endTime 这个时间,那么具体设置时这个时间戳设置多少呢?也就是 endTime 当前的毫秒数时间 getTime()方法调用一下,再加上一个小时的毫秒数即 3600*1000,这样的话,endTime 设置就是距离当前的这个时间再向后推一个小时,这里大家要注意 endTime 还是要声明称 var 而不是 const,这样就可以实现从一个小时开始倒计时。

var endTime = new Date();
endTime.setTime(endTime.getTime() + 3600*1000);

这个程序呢其实就可以作为一个时间管理的工具,比如说大家希望没一个小时都进行工作之后进行休息,如果大家愿意还可以设置成一个提醒,播放一段音乐提醒大家已经学习一个小时了。当然了大家还是要根据自己的情景来使用了。

具体源码请到git 码云下载

「点点赞赏,手留余香」

6

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » canvas实现绚丽的倒计时效果优化与扩展(三)

1 评论

  1. 很不错的倒计时。

发表回复