canvas给我们提供了绘图API,这些API基于JavaScript实现,那么我们可以方便的实现一些动画,在这里我们将展示几个经典的动画绘制技巧,包括:时钟,太阳系,动态蚂蚁线,全景照片
动画绘制的基本步骤
我们知道动画是由关键帧拼接而成,在人眼的视觉暂留下连续起来从而具有动态的效果,在这篇博客中我们也介绍了一些方法,canvas动画的绘制也大同小异:
- 清空canvas:新一动画帧绘制前,我们要清除原有的状态,以避免原有渲染的污染。除非接下来要画的内容会完全充满 canvas (例如背景图),否则需要清空所有。最简单的做法就是用 clearRect 方法。
- 保存canvas状态:与前面提到的一样,保存canvas是一种优秀的品质。如果要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,需要先保存上一状态。
- 绘制动画:动画实现的关键之处,动画效果都在这一步绘制关键帧
- 恢复canvas状态:如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。
时间间隔设置
动画的实现离不开定时器,在递归函数调用时我们设置一个定时器,从而在固定时刻绘制每一帧动画。目前有三种主要方法:setInterval,setTimeout,requestAnimationFrame()
- setInterval(function,delay):按照指定的周期(以毫秒计)来调用函数或计算表达式。方法会不停地调用函数,直到 clearInterval() 被调用或窗口被关闭。
- setTimeout(function,delay):在指定的毫秒数后调用函数或计算表达式。解除方法是clearTimeout(timer).
- requestAnimationFrame(callback):告诉浏览器你希望执行一个动画,并在重绘之前,请求浏览器执行一个特定的函数来更新动画。相较于前两者这个方法提供了更加平缓并更加有效率的方式来执行动画,当系统准备好了重绘条件的时候,才调用绘制动画帧。一般每秒钟回调函数执行60次,也有可能会被降低。
- cancelAnimationFrame:取消requestAnimationFrame动画
如何选择这3者?
- 如果不需要与用户互动,可以使用setInterval()方法,它就可以定期执行指定代码。
- 如果我们需要做一个游戏,我们可以使用键盘或者鼠标事件配合上setTimeout()方法来实现。通过设置事件监听,我们可以捕捉用户的交互,并执行相应的动作。
- requestAnimationFrame(callback)方法提供了更加平缓并更加有效率的方式来执行动画,当系统准备好了重绘条件的时候,才调用绘制动画帧。
动画示例:
canvas实现的动画
(1)太阳系
var sun=new Image();
var moon=new Image();
var earth=new Image();
function init()
{
sun.src="sun.jpg";
earth.src="地球.jpg";
moon.src="moon.jpg";
window.requestAnimationFrame(draw);//确保图片平稳加载完毕,调用requestAnimationFrame
}
//设计draw函数
function draw(ctx)
{
var ctx=document.getElementById("canvas").getContext('2d');
ctx.globalCompositeOperation="destination-over";//合成为在当前画布内容后绘制新的动画
ctx.clearRect(0,0,300,300);//清除区域
ctx.strokeStyle="rgba(0,153,255,0.4)";
ctx.save();
//earth
ctx.translate(150,150);//earth 确定地球的中心
var time=new Date();//create时间对象
ctx.rotate((2*Math.PI/60)*time.getSeconds()+(2*Math.PI/60000)*time.getMilliseconds());
//getSecond返回一个0~59之间的整数表示秒,getMillioneconds返回一个0~60000之间的整数,表示毫秒
ctx.translate(105,0);//确定地球轨道上的位置
ctx.drawImage(earth,-12,-12,24,24);
//Moon
ctx.save();
ctx.rotate((2*Math.PI/6)*time.getSeconds()+(2*Math.PI/6000)*time.getMilliseconds());
ctx.translate(0,30);//确定月球的位置
ctx.drawImage(moon,-3.5,-3.5);
ctx.restore();
ctx.restore();
//轨道
ctx.beginPath();
ctx.arc(150,150,105,0,Math.PI*2,false);
ctx.stroke();
//sun
ctx.drawImage(sun,0,0,300,300);
//递归调用
window.requestAnimationFrame(draw);
}
window.onload=init();
(2)动态蚂蚁线
window.onload=march;
var timer;
var offset=0;
function draw() {
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext('2d');
ctx.strokeStyle="rgb(150,"+offset*16+",0)";
ctx.clearRect(0,0, canvas.width, canvas.height);
ctx.setLineDash([5, 2]);//线段和间隙交替
ctx.lineDashOffset = -offset;//起始偏移量
ctx.strokeRect(10,10, 400, 300);
}
function march() {
offset++;
if (offset > 16) {
offset = 0;
}
draw();
if(timer) clearTimeout(timer);
timer=setTimeout(march, 20);
}
(3)动态时钟
下面我们将会绘制一个动态的时钟,绘制过程轻车熟路,要注意两点:
- 指针旋转的角度计算:
- 时针 rotate_hour=(2Math.PI((hour%12/12)+min/720+sec/43200));
- 分针 rotate_min=(2Math.PI(min/60+sec/3600));
- 秒针 rotate_sec=(2Math.PI(sec/60));
- 注意一个save要对应一个restore,以防止出现错乱
function draw()
{
var time=new Date();
var ctx=document.getElementById("canvas").getContext('2d');
ctx.save();
ctx.clearRect(0,0,500,500);
ctx.translate(250,250);
ctx.rotate(-Math.PI/2);//旋转到起始位置
ctx.fillStyle="#5F9EA0";
ctx.strokeStyle="black";
ctx.lineWidth=5;
ctx.lineCap="round";
//背景
ctx.beginPath();
ctx.arc(0,0,201,Math.PI*2,false);
ctx.fill();
//边框
ctx.save();
ctx.strokeStyle="white";
ctx.lineWidth=1;
ctx.beginPath();
ctx.arc(0,0,195,0,Math.PI*2,false);
ctx.stroke();
ctx.beginPath();
ctx.arc(0,0,200,0,Math.PI*2,false);
ctx.stroke();
ctx.restore();
//小时刻度
ctx.save();
for(var i=0;i<12;i++)
{
ctx.beginPath();
ctx.moveTo(0,-175);
ctx.lineTo(0,-190);
ctx.stroke();
ctx.rotate(Math.PI/6);
}
ctx.restore();
//分刻度
ctx.lineWidth=1;
ctx.save();
for(var i=0;i<60;i++)
{
ctx.beginPath();
ctx.moveTo(0,-185);
ctx.lineTo(0,-190);
ctx.stroke();
ctx.rotate(Math.PI/30);
}
ctx.restore();
//画指针
var hour=time.getHours();
var min=time.getMinutes();
var sec=time.getSeconds();
var rotate_hour=(2*Math.PI*((hour%12/12)+min/720+sec/43200));
var rotate_min=(2*Math.PI*(min/60+sec/3600));
var rotate_sec=(2*Math.PI*(sec/60));
ctx.fillStyle="black";
//时针
ctx.save();
ctx.rotate(rotate_hour);
ctx.lineWidth=11;
ctx.beginPath();
ctx.moveTo(0,20);
ctx.lineTo(0,-150);
ctx.stroke();
ctx.restore();
//分针
ctx.save();
ctx.rotate(rotate_min);
ctx.lineWidth=7;
ctx.beginPath();
ctx.moveTo(0,30);
ctx.lineTo(0,-180);
ctx.stroke();
ctx.restore();
//秒针
ctx.save();
ctx.rotate(rotate_sec);
ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(0,40);
ctx.lineTo(0,-188);
ctx.stroke();
ctx.restore();
//画出中心点
ctx.save();
ctx.fillStyle=" #4B0082";
ctx.beginPath();
ctx.arc(0,0,5,0,Math.PI*2,false);
ctx.fill();
ctx.restore();
//显示数字时间
ctx.save();
ctx.rotate(Math.PI/2);
ctx.strokeStyle="white";
ctx.font="15px 幼圆";
ctx.fillText(hour+":"+min+":"+sec,50,-50);
ctx.restore();
ctx.restore();
window.requestAnimationFrame(draw);
}
window.requestAnimationFrame(draw);
(4)全景照片
还记得这篇文章中用动画增强网页吗?我们设置一张长图,这张长图将所有的图片横向包含,隐藏这张长图的绝大部分,当鼠标悬浮时,显示这张图的相应子图。这里我们仍然采用这样的做法,首先我们需要一张长图。
var img = new Image();
// User Variables - customize these to change the image being scrolled, its
// direction, and the speed.
img.src = '对称.png';
var CanvasXSize = 603;
var CanvasYSize = 300;
var speed = 30; //lower is faster
var scale = 1.05;
var y = -4.5; //vertical offset
// Main program
var imgW;
var imgH;
var x = 0;
var clearX;
var clearY;
var ctx;
img.onload = function() {
imgW = img.width*scale;
imgH = img.height*scale;
if (imgW > CanvasXSize) { x = CanvasXSize-imgW; } // image larger than canvas
if (imgW > CanvasXSize) { clearX = imgW; } // image larger than canvas
else { clearX = CanvasXSize; }
if (imgH > CanvasYSize) { clearY = imgH; } // image larger than canvas
else { clearY = CanvasYSize; }
//Get Canvas Element
ctx = document.getElementById('canvas').getContext('2d');
//Set Refresh Rate
return setInterval(draw, speed);
}
function draw() {
//Clear Canvas
ctx.clearRect(0,0,clearX,clearY);
//If image is <= Canvas Size
if (imgW <= CanvasXSize) {
//reset, start from beginning
if (x > (CanvasXSize)) { x = 0; }
//draw aditional image
if (x > (CanvasXSize-imgW)) { ctx.drawImage(img,x-CanvasXSize+1,y,imgW,imgH); }
}
//If image is > Canvas Size
else {
//reset, start from beginning
if (x > (CanvasXSize)) { x = CanvasXSize-imgW; }
//draw aditional image
if (x > (CanvasXSize-imgW)) { ctx.drawImage(img,x-imgW+1,y,imgW,imgH); }
}
//draw image
ctx.drawImage(img,x,y,imgW,imgH);
//amount to move
x += 0.75;
}
简化版:
var img = new Image();
img.src = '对称.png';
var speed = 30; //lower is faster
var y = -4.5;
var x = 0;
var ctx;
img.onload = function() {
ctx = document.getElementById('canvas').getContext('2d');
return setInterval(draw, speed);
}
function draw()
{
ctx.clearRect(0,0,ctx.width,ctx.height);
if(x>=img.width) x=0;
ctx.drawImage(img,x-img.width,y,img.width,img.height);
ctx.drawImage(img,x,y,img.width,img.height);
x+=0.75;
}