书读的越多而不加思考,你就会觉得你知道得很多;而当你读书而思考得越多的时候,你就会越清楚地看到,你知道得很少。——伏尔泰

canvas实现绚丽的倒计时效果与动画基础(一)

前端技术 码云 291℃ 0评论
目录
[隐藏]

canvas如何实现绚丽的倒计时效果,如下图显示的效果,非常简单,利用之前我为大家整理的canvas绘图和动画基础,你就可以轻松实现这一需求,具体请看《canvas画布绘制动画基础》。倒计时电子钟的实现,通过学习弧线和圆的绘制,进而完成电子钟的绘制效果。

倒计时程序基本架构

在学习绘制电子钟时,先要弄清楚数字的矩阵排列是如何实现的,其实非常简单,对于一个点阵化的数据就是一个二维数组,二维数组中的每一个元素不是0就是1,如果是1表示他填充的是一个实心的圆,如果是0表示他是一个空白,那么这样一个二维数组经过我们绘制逻辑以后就会在画布上形成一个矩阵似的数字效果。

一个矩阵似的数字效果

我在编辑器中建立两个文件,一个叫digit.js文件,另一个是countdown.js文件,digit文件用来存放我们二维的点阵模型供countdowm来使用,而countdown就是用来完成我们这个程序的主逻辑。首先我们来看一下digit,在digit里面声明一个其实是三维数组,就叫digit。那么这个三维数组中digit[0]包含的是一个二维数组,这个二维数组就是0这个数字的二维点阵,digit[1]包含的又是一个二维数组,这个二维数组是1这个数字的二维点阵,以此类推一直到digit[9],那么digit[9]包含的就是9这个数字的二维点阵,除此之外,我们这个digit中还留了第十个位置,这个第十个二维数组其实就是冒号”:”这一个符号所对应的二维点阵。看到这儿大家觉得这个文件有点复杂,没关系这里我会提供给大家下载拿去直接使用。与此同时大家可以想象,若果我们点阵化的做一个字母ABCD也可以使用这种方法,只不过是把相应的点阵换成ABCD这样的字母的点阵表示就可以了。

这里大家还要记住一会儿具体绘制矩阵时需要的一些数据,那就是我们每一个数字的点阵表示都是10乘以7这样大小的一个二维数组,而我们这个冒号相对应的比较小是一个10乘以4这样大小的一个二维数组,看一下具体digit代码:

digit =
    [
        [
            [0,0,1,1,1,0,0],
            [0,1,1,0,1,1,0],
            [1,1,0,0,0,1,1],
            [1,1,0,0,0,1,1],
            [1,1,0,0,0,1,1],
            [1,1,0,0,0,1,1],
            [1,1,0,0,0,1,1],
            [1,1,0,0,0,1,1],
            [0,1,1,0,1,1,0],
            [0,0,1,1,1,0,0]
        ],//0
        ...
        [
            [0,1,1,1,1,1,0],
            [1,1,0,0,0,1,1],
            [1,1,0,0,0,1,1],
            [1,1,0,0,0,1,1],
            [0,1,1,1,0,1,1],
            [0,0,0,0,0,1,1],
            [0,0,0,0,0,1,1],
            [0,0,0,0,1,1,0],
            [0,0,0,1,1,0,0],
            [0,1,1,0,0,0,0]
        ],//9
        [
            [0,0,0,0],
            [0,0,0,0],
            [0,1,1,0],
            [0,1,1,0],
            [0,0,0,0],
            [0,0,0,0],
            [0,1,1,0],
            [0,1,1,0],
            [0,0,0,0],
            [0,0,0,0]
        ]//:
    ];

点击下载digit.js  digit.js (下载3)

接下来看看我们countdown处理逻辑程序,在countdown中我们使用的还是window.onload = function () {}来定义数据初始化的行为,首先进行canvas的初始化,通过document.getElementById()方法拿到canvas变量,然后通过getContext方法拿到这个上下文绘图环境。

var canvas = document.getElemntbyId('canvas');
var context = document.getContext('2d');

为canvas赋值,这里我设置两个全局变量,定义canvas大小为宽1024,高768

var WINDOW.WIDTH = 1024;
var WINDOW.HEIGHT = 768;

在window.onload中我们这样调用:

canvas.width = WINDOW.WIDTH;
canvas.height = WINDOW.HEIGHT;

这样调用有两个好处,一是屏幕大小改变起来非常方便,其次在后续我们做屏幕自适应的时候只要计算这两个变量的值的大小就好,非常方便。说到这里暂时我们不处理这个逻辑过程,只处理我们绘制的过程,在后续我们将要调用我们定义的render()函数方法,注意在这里我们给它传入一个参数context,这个参数就是我们之前获得的绘图的上下文环境,下面我们来看一下这个render()函数的写法。

这个函数他要做的事情呢就是绘制当前这个canvas画布,这里我们先从绘制这个时钟开始,要想绘制这个时钟,就要知道绘制的这个时钟的具体数字是多少,为此我设置了三个变量分辨是时、分、秒,他们存放的就是当前倒计时的具体数字,由于现在不涉及到具体逻辑,我们给它附上一个常量,我们做实验使用,之后我们就要一个数字一个数字的绘制这个时间了,为此我又设计一个新的函数为renderDigit,里面包含这么几个函数,首先是从哪个位置开始来绘制这一个具体的数字,在做实验前我们先设置一个(0, 0)这个位置,之后呢就是我们要绘制哪个数字,这里大家要注意我们这个时钟的demo里头三个常量都是二位数字,但是我们在具体绘制这个数字是我们要一位数字一位数字的绘制,所以我们需要把我们定义的三个变量拆开,那么对于这个hours小时来说要想拿到这个十位数字的方法呢就是(hours/10)然后通过parseInt()方法给它转换成整型即parseInt(hours/10),这些呢就是绘制这一个数字的所有信息了,但是大家要注意要想让renderDigit可以绘制的话,一定要将上下文绘图环境传进去

function render(cxt){
    var hours = 12;
    var minutes = 34;
    var seconds = 56;
    renderDigit(0, 0, parseInt(hours/10), cxt);
}

接下来我们讨论一下renderDigit这个函数是怎么写的。这个函数又四个参数分别为x、y、num、cxt,在这个函数里首先看到之后我们一定是使用一次二维的循环的方式遍历相应的二维点阵,并且在点阵中每一个唯一的地方画一个实心的小球,为此呢我们先设置一个状态给这个cxt的fillStyle为”rgb(0,102,153)”的颜色,这个颜色为蓝色,在之后就可以写这个二维循环了,大家可以想象我们这个二维循环应该是对digit[num]这个二维数组进行遍历,那么第一层遍历呢从0一直到digit[num].length,相应的第二层遍历则是从0遍历到digit[num]的第[i]行的length,在之后则应该判断如果digit[num][i][j]这一个位置它是”1″的话,那么说明此时我们应该在这里绘制一个圆球,具体的绘制圆球方法在之前我的一篇文章介绍过了请看《canvas画布绘制动画基础》,首先我们应该beginPath(),然后调用一个arc()函数,具体参数一会儿我们在做介绍,之后我们调用closePath(),最后调用一个fill()方法对圆球进行绘制,整个rederDigit()框架就是这样。

function renderDigit (x, y,num, cxt) {
    cxt.fillStyle = "rgb(0,102,153)";
    for(var i = 0; i < digit[num].length; i++)
        for(var j < 0; j < digit[num][i].length; j++)
            if(digit[i][j] == 1){
                cxt.beginPath();
                cxt.arc();
                cxt.closePath();
                cxt.fill();
            }
}

这里还缺一个对arc()参数的计算方法,这个计算方法比较复杂一些,我们一起来看一下。

倒计时数字钟具体绘制

我们在绘制每一个数字的时候,组成这个数字的每一个小圆的原生究竟该如何计算呢?我们先来分析一下,对于一个7*10的二维数组,我们单独拿出一个圆来看,我们设计的每一个小圆在一个格子里,加入一个小圆的半径为大R,由于小圆与小圆之间有一定距离的空隙,那么圆心到包含这个圆的外边框的距离就是R+1,给它多留出一个像素,那么这个包含小圆的正方形的边长就是2*(R+1),有了这个分析就可以回想一下上面我们提到的renderDigit函数,这个函数告诉我们要从x,y顶点开始绘制这个数字,在绘制这个数字的时候遍历一个二维数组,其中”i”代表的是行数,”j”代表的是列数这样一个方向,大家一定要分清这个方向,此时我们要解决的是第(i,j)这个小圆它的圆心位置 究竟是哪儿呢,大家看一下这个式子分析的是不是这么回事:

CenterX:x + j*2*(R+1) + (R+1);
CenterY:Y + i*2*(R+1) + (R+1);

其中CenterX应该是初始x加上它是第几列的用j乘以2乘以(R+1),也就是乘以这个包围盒的边长,之后才是挪到了我们该绘制的那一个包围盒的前面,然后在加上一个(R+1),使得这个横坐标挪在了该绘制的圆心的位置,这是横坐标。

纵坐标是类似的,用y加上i*2*(R=1),在这里纵坐标要用行数i来表示,这样的话挪到了将要绘制的这个圆的包围盒的前面,之后再加上一个(R+1)挪到了这个圆心的位置,这里大家要注意(i,j)都是从0开始计算的,所以这个式子是成立的,大家下去再仔细的研究一下这个式子。

倒计时数字钟具体绘制

事实上我们在制作游戏相关或者动画相关的时候经常使用这种格子系统,在使用这种格子系统的时候都免不了的使用这种方式,来计算某一个具体的格子的位置,希望大家能够熟悉这种计算方式,以后我们还少不了和这种方式打交道,比如说对图像进行处理,每一个图像就可以看做是一个格子系统,其中每一个点就是一个像素,每一个像素存储的是这一个点上的颜色值,我们还是要使用这种方式来具体看操作那种像素,那么我们再讲相关内容在做介绍。

回到我们代码中,定义一下我们想要画的这些小球的半径,我们声明一个新的变量RADIUS=8,之后来看一下arc(),根据刚才的分析,圆心的位置就应该是”x+j*2*(RADIUS+1)+(RADIUS+1) , y+i*2*(RADIUS+1)+(RADIUS+1)”大家可以比较一下和刚才的分析是否一致,第三个参数是半径就是我们新建的变量RADIUS,最后呢因为我们要画的是圆从零开始到2*Math.PI,这样我们这个rederDigit函数就完成了,代码如下:

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

window.onload = function(){

    var canvas = document.getElementById('canvas');
    var context = canvas.getContext("2d");

    canvas.width = WINDOW_WIDTH;
    canvas.height = WINDOW_HEIGHT;

    render( context )
}

function render( cxt ){

    var hours = 12
    var minutes = 34
    var seconds = 56

    renderDigit(0, 0, parseInt(hours/10), cxt);
}

function renderDigit( x , y , num , cxt ){

    cxt.fillStyle = "rgb(0,102,153)";

    for( var i = 0 ; i < digit[num].length ; i ++ )
        for(var j = 0 ; j < digit[num][i].length ; j ++ )
            if( digit[num][i][j] == 1 ){
                cxt.beginPath();
                cxt.arc( x+j*2*(RADIUS+1)+(RADIUS+1) , y+i*2*(RADIUS+1)+(RADIUS+1) , RADIUS , 0 , 2*Math.PI )
                cxt.closePath()

                cxt.fill()
            }
}

此时程序的运行结果:

倒计时数字钟具体绘制

还记得最初我们给hours赋值12,现在就把12中的第一位给画出来了,在render函数中,我们把小时数字的十位数显示出来,在renderDigit函数继续绘制后面的数字之前呢我们要整理一下传入的x,y,为了让显示过程更美观,我在设置两个全局变量:

var MARGIN_TOP = 60;//第一个数字距离我们画布上边距的距离
var MARGIN_LEFT = 30;//第一个数字距离我们画布左边距的距离

那么在render函数中调用的renderDigit函数传入的x,y就是这样写:

renderDigit(MARGIN_TOP, MARGIN_LEFT, parseInt(hours/10), cxt);

下面我么来绘制小时hours的第二个数字,也就是数字的个位数,任然需要我们调用renderDigit函数,那么此时x是什么呢?此时的x就应该是在MARGIN_LEFT的基础上加上第一个数字所使用的距离,那么第一个数字在横向上使用的距离是多少呢,大家可以想象,其实应该是14*(RADIUS + 1),这是因为我们的数组是7*10的也就是在横向上7的位置,相当于就是14个半径加1这么长,但是呢为了和右侧数字留有一定间隔,所以我们把14改成15,那么这样在我们的程序中,每一个数字在横方向上所占的这个长度就是15倍的半径长度加1即(RADIUS+1)这么长,之后距离画布上边距的距离任然是MARGIN_TOP,数字具体应该是多少呢,我们计算个位数只需要(hours % 10)来进行一次整型转换即parseInt(hours % 10),最后不要忘了把绘图的上下文环境cxt传进去,现在我们把整个小时这两个数字绘制完成了:

renderDigit(MARGIN_LEFT, MARGIN_TOP, parseInt(hours/10), cxt);
renderDigit(MARGIN_LEFT + 15*(RADIUS + 1), MARGIN_TOP, parseInt(hours%10), cxt);

接下来我们绘制冒号这个特殊符号,这个方法也是大同小异,首先找到他距离左边距的位置,那就是MAGIN_LEFT加上30倍的半径加上1(RADIUS + 1),这是因为我们之前说过每一个数字占15,那么他就应该在上一个数字基础上占到30的位置,距离画布上边距的距离任然是MARGIN_TOP,接下来要注意我们绘制的不是一个数字,其实在这里边传入的参数是一个索引,只不过是这个索引建立的很巧妙,传入的0调用的就是digit[0],遍历的就是0所对应的二维数组,如果是1呢就是遍历的digit[1]所对应的二维数组,大家回想一下我们在上面对digit[10]设置的是“:”冒号,在这里我们就传入10,那么它对应的就是digit[10],digit[10]对应的不是数字,而是冒号,然后把上下文环境cxt传入就可:

renderDigit(MARGIN_LEFT + 30*(RADIUS + 1), MARGIN_TOP, 10, cxt);

运行效果:

倒计时数字钟具体绘制

可以看出hours赋值12被成功地渲染出来了,同时还有一个冒号。

通过之前的分析后面的数字就变得非常简单了,绘制的分钟秒钟完全类似,照着做就可以了,特别注意我们开始绘制分钟的第一位数字是从39开始的,这是因为冒号那个数组是4*10,宽度是4位的这么一个数组,那么根据刚才的分析我们用4*2得到8倍的边长再留一点空隙加上1,以此类推绘制一下内容,之后呢大家来看一下数字是否计算正确,代码如下:

//时
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);

一起来看一下效果:

倒计时数字钟具体绘制

当然目前我们只是将样式绘制出来了,如何获得当前距离我们要倒计时的那个时间之间的这个时间呢,我们需要研究一下JavaScript时间函数。

倒计时时间的设置

在上面我们定义的时间都是写死的,如果要做成动态的效果这样的写法肯定是不行的,那么如何计算倒计时所需要的时间呢?首先我们需要顶一个截止的时间,我声明一个新的全局变量endTime,使用 date的方式新创建一个时间,

const endTime = new date(2014,6,11,18,47,52);

这里需要注意两点:

(1)在JavaScript中月数这个参数是比较特殊的,它是从0开始一直到11,也就是如果传入的是0就代表着1月,11代表的是12月,这里我想要把这个月份设置成7月那么我传入的就是6,这里大家一定要注意。

(2)大家可能注意到我们demo倒计时呢在消失这个位置只有两位数字,换句话说,一天有24个小时,最多也就是倒计时4天,这是我们程序在设计时的一个限制,如果大家想要打破这个限制,学完这个大家可以将小时扩充到3位数,甚至增加到天的倒计时,月的倒计时,年的倒计时,思路大致一样的,我们的demo暂时设计成这样。所以我在设计日期的时候距离现在不会超过4天。

当我们有了这个截止日期的时候,如何在render函数中获取当前时间距离截止日期具体有多少个小时,多少个分钟,多少个秒钟呢,JavaScript的date()对象提供给我们getTime()方法,他返回了当前时间距1970年1月1日0时0分0秒的毫秒数,用这个方法减去截止日期endTime的getTime得到的这个毫秒数,就得到了中间的差值,也就是之间要经过多少毫秒,那么通过之间的这个毫秒数的判断我们就可以计算出来倒计时的具体小时、分钟和秒数。那么在这里由于我们这个倒计时由于一秒一秒的变化之后要再加上动画的效果,这个动画的效果要不停地和当前的时间作一个比较,为此我们设计一个全局的标量来表示现在倒计时需要多少秒:

var ourShowTimeSeconds = 0;

这里我把毫秒在一会儿的计算中再转化成秒,因为毫秒这个细节我们是不需要的,这里我定义为ourShowTimeSeconds初始化0,然后再render函数前进行一个计算,这个计算我封装到一个新的函数getCurrentShowTimeSeconds中,代码如下

window.onload = function(){

    var canvas = document.getElementById('canvas');
    var context = canvas.getContext("2d");

    canvas.width = WINDOW_WIDTH;
    canvas.height = WINDOW_HEIGHT;

    curShowTimeSeconds = getCurrentShowTimeSeconds()
    render( context )
}

下面我们来看一下getCurrentShowTimeSeconds如何实现,首先我们声明一个变量curTime获取当前的时间,其次,我设置一个ret变量拿到截止的时间endTime的getTime()方法取得这个毫秒数减去当前的时间curTime的getTime()得到毫秒数,之后我给ret除以1000就是把毫秒数转化成了秒,同时用Math.round()给它转换成整数,最后ret就可以return返回去但是在具体的返回时我还是要判断一下这个ret是否大于等于0,如果大于等于0则返回ret,否者的话返回0就可以了,这样如果倒计时结束的话,屏幕的显示是0,这样的话我们函数就写好了,一起看一下代码:

function getCurrentShowTimeSeconds() {
    var curTime = new Date();
    var ret = endTime.getTime() - curTime.getTime();
    ret = Math.round( ret/1000 )

    return ret >= 0 ? ret : 0;
}

下面我们就可以在render函数中根据我们刚才得到的这个curShowTimeSeconds来计算具体的小时分钟秒钟就好了,这个计算就当对于简单了,对于小时的计算我们用当前的总共的秒数除以3600就是一共需要多少个小时,对于分钟的计算相对比较复杂些,我们需要将总共的秒数减去这些小时所代表的秒数,那么剩下的时间就是由多少分钟多少秒钟这个数除以60,经过强制类型转换得到的就是具体有多少分钟,最后我们计算有多少秒是最容易的,我们只需要把总共的这个秒数curShowTimeSeconds对60取余得到的这个余数就是多少毫秒数,具体代码:

function render( cxt ){
    //时、分、秒计算
    var hours = parseInt( curShowTimeSeconds / 3600);
    var minutes = parseInt( (curShowTimeSeconds - hours * 3600)/60 )
    var seconds = curShowTimeSeconds % 60

    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);
}

我们来看一下效果:

倒计时时间的设置

此时程序运行显示的时间就是距离我们刚才设定的endTime还剩下多少个时间,而且呢虽然我们没有加动画,但是每次我们刷新浏览器的话这个时间就会变化,来反映出我们刷新的那一刻距离我们设计好的endTime还有多少时间,我们离我们的目标越来越近了,大家加油!

完整代码:

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

const endTime = new Date(2014,6,11,18,47,52);
var curShowTimeSeconds = 0

window.onload = function(){

    var canvas = document.getElementById('canvas');
    var context = canvas.getContext("2d");

    canvas.width = WINDOW_WIDTH;
    canvas.height = WINDOW_HEIGHT;

    curShowTimeSeconds = getCurrentShowTimeSeconds()
    render( context )
}

function getCurrentShowTimeSeconds() {
    var curTime = new Date();
    var ret = endTime.getTime() - curTime.getTime();
    ret = Math.round( ret/1000 )

    return ret >= 0 ? ret : 0;
}

function render( cxt ){

    var hours = parseInt( curShowTimeSeconds / 3600);
    var minutes = parseInt( (curShowTimeSeconds - hours * 3600)/60 )
    var seconds = curShowTimeSeconds % 60

    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);
}

function renderDigit( x , y , num , cxt ){

    cxt.fillStyle = "rgb(0,102,153)";

    for( var i = 0 ; i < digit[num].length ; i ++ )
        for(var j = 0 ; j < digit[num][i].length ; j ++ )
            if( digit[num][i][j] == 1 ){
                cxt.beginPath();
                cxt.arc( x+j*2*(RADIUS+1)+(RADIUS+1) , y+i*2*(RADIUS+1)+(RADIUS+1) , RADIUS , 0 , 2*Math.PI )
                cxt.closePath()

                cxt.fill()
            }
}

因为文章内容比较多,如果都在一篇文章上写完会给大家观看带来疲劳感,所以我分三篇文章将这个canvas倒计时实现教程写完,本篇是第一篇文章,下一篇请访问《canvas实现绚丽的倒计时效果与动画基础(二)》,未完待续。。。

转载请注明:码云笔记 » canvas实现绚丽的倒计时效果与动画基础(一)

喜欢 (1)or分享 (0)
发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址