来自MDN上的Canvas动画例子--太阳系动画
本文将从例子源码入手,带你从我个人角度理解Canvas太阳系动画的实现,并对一些api做出介绍解释,首先说明我也是刚学习的Canvas,有出错的地方还请指教。
代码在关键处有注释说明。
初始化
<canvas id="canvas" width="300" height="300"></canvas>
<script>
// 获取canvas内容上下文(基础操作)
var ctx = document.getElementById('canvas').getContext('2d');
// 如果把每次绘画图形当成一个图层,那么globalCompositeOperation规定了图层之间的合成模式,
// 'destination-over'设置在现有的画布内容(图层)后面绘制新的图形。
// 在下方draw函数绘制图形时,是按绘画地球,月球,太阳的顺序绘画的,
// 以'destination-over'绘画太阳会放在图层最下方,这样不会覆盖地球和月球
ctx.globalCompositeOperation = 'destination-over';
var sun = new Image();
var moon = new Image();
var earth = new Image();
function init() {
sun.src = 'https://mdn.mozillademos.org/files/1456/Canvas_sun.png';
moon.src = 'https://mdn.mozillademos.org/files/1443/Canvas_moon.png';
earth.src = 'https://mdn.mozillademos.org/files/1429/Canvas_earth.png';
// 这里其实应该设置img.onload在调用绘画函数draw的,但是由于我们使用了递归方式,
// 所以图片还是能在后续被使用到,只不过会在第一帧的时候图片不展示,
// 如果有需要可以加入onload判断再调用draw函数
window.requestAnimationFrame(draw);
}
function draw() {
...
...
// canvas使用递归实现动画,每次递归函数在最开始清除上一次绘制的内容,使用rAF实现递归避免卡帧
window.requestAnimationFrame(draw);
}
init();
</script>
function draw() {
ctx.clearRect(0, 0, 300, 300); // clear canvas
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.strokeStyle = 'rgba(0,153,255,0.4)';
ctx.save();
...
...
}
为了实现动画,在每次绘制新的画面时需要清除旧画面,ctx.clearRect(x, y, width, height),清除规定矩形内的内容,这个矩形范围的左上角在 (x, y),宽度和高度分别由 width 和height确定。
ctx.fillStyle = 'rgba(0,0,0,0.4)';规定填充的样式
ctx.strokeStyle = 'rgba(0,153,255,0.4)';规定画笔的样式
ctx.save();保存在这之前的状态设置,包括填充,画笔样式和原点位置,在绘画太阳图片的时候需要用到初始状态,因为在绘画地球和月球的时候原点的位置是不同的,根据原点进行旋转,根据时间调整旋转的角度。
绘制地球
绿色线的夹角就是旋转的角度。
function draw() {
...
ctx.translate(150, 150);
var time = new Date();
ctx.rotate(
((2 * Math.PI) / 60) * time.getSeconds() +
((2 * Math.PI) / 60000) * time.getMilliseconds()
);
ctx.translate(105, 0);
ctx.fillRect(0, -12, 50, 24); // Shadow
ctx.drawImage(earth, -12, -12);
...
}
ctx.translate(150, 150);移动原点,依据目前原点进行移动,一开始为(0,0),现在为(150,150),为canvas画布的中心,也是地球围绕太阳旋转的中心,之后调用ctx.rotate进行旋转,((2 * Math.PI) / 60) * time.getSeconds() 旋转的角度根据时间进行变化,60秒旋转一周,为了更加丝滑加上了((2 * Math.PI) / 60000) * time.getMilliseconds(),不然1秒转6度看起来卡卡的,利用当前毫秒调整角度就流畅很多了,这里就是实现地球围绕太阳旋转的代码分析了。
之后绘制出地球的位置,因为月球围绕地球旋转,所以我们把原点放到地球中心ctx.translate(105, 0);通过ctx.drawImage(earth, -12, -12);绘制出地球图片,并附上阴影ctx.fillRect(0, -12, 50, 24);来模拟日照背影,由于前面设置了globalCompositeOperation模式,所以应该先绘制阴影在绘制地球,这样阴影会覆盖地球的一半。如上图就是第一次绘画的结果。
绘制月球
之后就是绘制月球了,方法其实跟绘制地球差不多,这里就不过多讲解了。
疑问一
可能这中途会有一个疑问,就是使用translate改变原点然后rotate旋转怎么不会带动地球(或者整个画布)进行旋转呢?
对canvas不太熟悉很容易产生这样的误解,会跟css中的联系起来,导致结果跟想象的不一致。在canvas中,绘制是一步一步执行的,毕竟用js写的嘛,设置样式也讲究顺序,像fillStyle,strokeStyle,当然translate,rotate也是,这两个api指规定了下方canvas代码的原点和x轴旋转的角度,它并不会绘制出什么内容,也不会对上方代码已经绘制出来的图形有影响,更不会影响原始画布,千万不要给css规则带跑了。
绘制太阳
function draw() {
...
...
ctx.restore();
ctx.beginPath();
ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // Earth orbit
ctx.stroke();
ctx.drawImage(sun, 0, 0, 300, 300);
window.requestAnimationFrame(draw);
}
这一步就很简单了,只需要在中间画上就行了,这里使用 ctx.restore()恢复之前保存的状态,这时候原点回到了(0,0),也没有进行rotate旋转,拿起画笔ctx.beginPath();,画一下中间蓝色的地球轨道线ctx.arc(150, 150, 105, 0, Math.PI * 2, false);ctx.stroke();,最后再绘制太阳图片ctx.drawImage(sun, 0, 0, 300, 300),从相对原点(0,0)的(0,0)位置开始,宽高300绘制太阳,上面几张图为了理解方便把轨道线提前绘制了。
进入递归