canvas ---- canvas API 中文网的loading菊花效果怎么实现的?

参考:canvas API 中文网

1. 前言

在canvas API 中文网学习canvas的时候,看到里面有一段示例代码,如图:


loading菊花效果示例

提取官网中的代码如下:

// 圆心坐标
var center = [20, 20];
// 线长度和距离圆心距离
var length = 8, offset = 8;
// 开始绘制
context.lineWidth = 4;
context.lineCap = 'round';
for (var angle = 0; angle < 360; angle += 45) {
  // 正余弦
  var sin = Math.sin(angle / 180 * Math.PI);
  var cos = Math.cos(angle / 180 * Math.PI);
  // 开始绘制
  context.beginPath();
  context.moveTo(center[0] + offset * cos, center[1] + offset * sin);
  context.lineTo(center[0] + (offset + length) * cos, center[1] + (offset + length) * sin);
  context.strokeStyle = 'rgba(0,0,0,'+ (0.25 + 0.75 * angle / 360) +')';
  context.stroke();
}

复制官网的这段代码,实际的效果是不会转的,就是绘制了8条透明度递增的粗线条,当时就很好奇该怎么让这朵“菊花”旋转起来。思考了一番,我觉得有两种方法:

  • (1)绘制“菊花”之后,使用setInterval不断旋转图案;
  • (2)绘制“菊花”之后,使用setInterval不断的改变花瓣的颜色。

2. 代码实现

html代码:

<!DOCTYPE html>
<html lang='zh-Hans'>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <div class="container" style="width: 300;">  
      <canvas 
        id="scene" 
        width="300" 
        height="300" 
        style="background: #f5f7f9;" 
      ></canvas>
    </div>

    <script type="text/javascript" src="./test1.js"></script>
  </body>
</html>

然后在同级目录新建test1.js书写js代码。


  • 方法(1):
window.onload = function() {
  /**
   * 绘制菊花
   * @param { Object } context canvas 2d 对象
   * @param { number } x 菊花圆心x轴坐标
   * @param { number } y 菊花圆心y轴坐标
   * @param { number } length 菊花花瓣的长度
   * @param { number } offset 菊花花瓣到圆心的距离
   * @param { number } rotateAngle 旋转角度
   */
  const loading = function(context, x, y, length, offset, rotateAngle) {
    context.lineWidth = 4;        // 花瓣宽度        
    context.lineCap = 'round';    // 花瓣圆角

    context.clearRect(x / 2, y / 2, x, y);        // 绘制前先清除画布
    context.save();                               // 保存状态画布状态
    context.translate(x, y);                      // 移动坐标系中心  
    context.rotate(rotateAngle / 180 * Math.PI);  // 以坐标系中心旋转旋转画布

    // 绘制 8 个花瓣
    for(let angle = 0; angle < 360; angle += 45) {
      // 正余弦
      let sin = Math.sin(angle / 180 * Math.PI);
      let cos = Math.cos(angle / 180 * Math.PI);
      // 开始绘制 
      context.beginPath(); 
      context.moveTo(offset * cos, offset * sin);
      context.lineTo((offset + length) * cos, (offset + length) * sin);
      context.strokeStyle = 'rgba(0,0,0,'+ (0.25 + 0.75 * angle / 360) +')';
      context.stroke();
    }

    context.restore();                            // 绘制之后重置画布状态(坐标系、旋转角度)
  }

  let canvas = document.getElementById('scene');
  let context = canvas.getContext('2d');

  // 绘制一朵静态的菊花
  loading(context,50, 50, 8, 8, 0);

  // 为菊花添加点击事件,点击后旋转
  canvas.addEventListener('click', function(event) {
    // 判断鼠标点击区域
    let rect = this.getBoundingClientRect();
    let x = event.clientX - rect.left;
    let y = event.clientY - rect.top;

    if(
      x > 50 + 8 * 2 || 
      x < 50 - 8 * 2 ||
      y > 50 + 8 * 2 ||
      y < 50 - 8 * 2 
    ) {
      return;
    }

    // 从弧度0开始旋转
    let rotateAngle = 0; 

    setInterval(() => {
      loading(50, 50, 8, 8, rotateAngle);
      rotateAngle += 45;
  
      if(rotateAngle === 36000) {
        rotateAngle = 0;
      }
    }, 150);
  })
}

为了实现不断旋转的效果,把官网原本的代码封装成一个loading函数,然后放到setInterval中去执行。其中新增了以下5个canvas的API:

  • context.clearRect(x, y, width, height)
    清除画布中的某一矩形区域,我一开始尝试的时候,菊花旋转了一圈之后,花瓣的颜色就全变黑了,因为setInterval在不断执行,后续绘制的花瓣不断覆盖重叠在一起,于是在每次绘制前需要清除上一次的绘制。
  • context.rotate(angle)
    旋转一定角度,这里的旋转是以画布左上角(0, 0)建立的坐标系旋转,本质上就是整个画布旋转,而这次实现的loading菊花效果是在坐标系上(50, 50)的位置绘制的,那么理论上就应该以(50, 50)为中心旋转,这时就需要搭配下面的API使用。
  • context.translate(x, y)
    对坐标系进行整体位移。
  • context.save()
    保存画布状态,类似一次游戏存档。
  • context.restore()
    恢复上次save的状态,类似游戏读取存档。本例中使用了平移和旋转,在平移和旋转前save,绘制之后直接restore恢复画布状态。如果不用save和restore,平移坐标系之后要再把坐标系平移回去,不然由于坐标系混乱,根本达不到旋转效果。不过使用平移后再平移回去这种方法,实现的旋转动画效果并不流畅,不知道是平移造成的视觉差还是什么原因,总之推荐使用save和restore配合。

  • 方法(2)
    理论上方法(2)比方法(1)性能更好,因为方法(1)在绘制之后还要旋转,下一次绘制前再清除,而方法(2)只有绘制和清除两个过程。
    观察源代码中设置透明度的那一句代码:
for (let angle = 0; angle < 360; angle += 45) {
  // dosomething -----
  context.strokeStyle = 'rgba(0,0,0,'+ (0.25 + 0.75 * angle / 360) +')';
}

canvas绘制圆形是从3点钟方向开始的,上面的代码利用rgba设置透明度。最开始从三点钟方向弧度是0,透明度最低,每增加45度颜色递增。如果把弧度45的花瓣颜色设置最浅,其他花瓣也随之递增改变,那么在视觉效果上就是顺时针旋转45度。在代码中只需要稍微调整for循环的起止条件和颜色赋值语句:

window.onload = function() {
  /**
   * 绘制菊花
   * @param { Object } context canvas 2d 对象
   * @param { number } x 菊花圆心x轴坐标
   * @param { number } y 菊花圆心y轴坐标
   * @param { number } length 菊花花瓣的长度
   * @param { number } offset 菊花花瓣到圆心的距离
   * @param { number } begin 起始角度
   * @param { number } end 结束角度
   */
  const loading = function(context, x, y, length, offset, begin, end) {
    context.lineWidth = 4;        // 花瓣宽度        
    context.lineCap = 'round';    // 花瓣圆角

    context.clearRect(x / 2, y / 2, x, y);        // 绘制前先清除画布
    context.save();                               // 保存状态画布状态

    // 绘制 8 个花瓣
    for(let angle = begin; angle < end; angle += 45) {
      // 正余弦
      let sin = Math.sin(angle / 180 * Math.PI);
      let cos = Math.cos(angle / 180 * Math.PI);
      // 开始绘制 
      context.beginPath(); 
      context.moveTo(x + offset * cos, y + offset * sin);
      context.lineTo(x + (offset + length) * cos, y + (offset + length) * sin);
      context.strokeStyle = 'rgba(0,0,0,'+ (0.25 + 0.75 * (angle - begin) / 360) +')';
      context.stroke();
    }

    context.restore();                            // 绘制之后重置画布状态
  }

  let canvas = document.getElementById('scene');
  let context = canvas.getContext('2d');

  // 绘制一朵静态的菊花
  loading(context,50, 50, 8, 8, 0, 360);

  // 为菊花添加点击事件,点击后旋转
  canvas.addEventListener('click', function(event) {
    // 判断鼠标点击区域
    let rect = this.getBoundingClientRect();
    let x = event.clientX - rect.left;
    let y = event.clientY - rect.top;

    if(
      x > 50 + 8 * 2 || 
      x < 50 - 8 * 2 ||
      y > 50 + 8 * 2 ||
      y < 50 - 8 * 2 
    ) {
      return;
    }

    // 从(0, 360)开始绘制
    let begin = 0;
    let end = 360; 

    setInterval(() => {
      loading(50, 50, 8, 8, begin, end);
      rotateAngle += 45;
  
      if(begin >= 360 || end >= 720) {
        begin = 0;
        end = 360;
      }
    }, 150);
  })
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。