MDN上的Canvas太阳系例子我终于彻底看懂了!!

951 阅读3分钟

来自MDN上的Canvas动画例子--太阳系动画

本文将从例子源码入手,带你从我个人角度理解Canvas太阳系动画的实现,并对一些api做出介绍解释,首先说明我也是刚学习的Canvas,有出错的地方还请指教。

下载.png

代码在关键处有注释说明。

初始化

<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();保存在这之前的状态设置,包括填充,画笔样式和原点位置,在绘画太阳图片的时候需要用到初始状态,因为在绘画地球和月球的时候原点的位置是不同的,根据原点进行旋转,根据时间调整旋转的角度。

绘制地球

下载 (1).png

下载 (4).png 绿色线的夹角就是旋转的角度。

 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模式,所以应该先绘制阴影在绘制地球,这样阴影会覆盖地球的一半。如上图就是第一次绘画的结果。

绘制月球

下载 (2).png 下载 (3).png

之后就是绘制月球了,方法其实跟绘制地球差不多,这里就不过多讲解了。

疑问一

可能这中途会有一个疑问,就是使用translate改变原点然后rotate旋转怎么不会带动地球(或者整个画布)进行旋转呢?

对canvas不太熟悉很容易产生这样的误解,会跟css中的联系起来,导致结果跟想象的不一致。在canvas中,绘制是一步一步执行的,毕竟用js写的嘛,设置样式也讲究顺序,像fillStyle,strokeStyle,当然translate,rotate也是,这两个api指规定了下方canvas代码的原点和x轴旋转的角度,它并不会绘制出什么内容,也不会对上方代码已经绘制出来的图形有影响,更不会影响原始画布,千万不要给css规则带跑了。

绘制太阳

下载 (5).png

 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绘制太阳,上面几张图为了理解方便把轨道线提前绘制了。

进入递归