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