1.动画

在Canvas中,动画其实也就是一些基础的几何变换,因此想做动画第一步咱们需要先了解有哪些几何变换

几何变换

几何变换的类型其实和CSS动画中的类型差不多,也就是:移动、旋转、缩放

移动

语法:translate(x, y),其中 x 是左右偏移量,y 是上下偏移量。

const canvas = document.getElementById('canvas'); // 获取Canvas
const ctx = canvas.getContext('2d'); // 获取绘制上下文
ctx.fillStyle="#ff0000"
// 向x轴和y轴平移200像素(移的是画布的原点)
ctx.translate(200, 200);
// 在(0,0)坐标点绘制一个宽:200,高:100的矩形
ctx.fillRect(0, 0, 200, 100)

旋转

语法:rotate(angle),其中 angle 是旋转的角度,以弧度为单位,顺时针旋转。

const canvas = document.getElementById('canvas'); // 获取Canvas
const ctx = canvas.getContext('2d'); // 获取绘制上下文
for (let i = 0; i < 9; i++) {
// 旋转弧度设置,角度和弧度的转换公式:1° = Math.PI / 180
ctx.rotate(i * 2 * Math.PI / 180);
// 在(0,0)坐标点绘制一个宽:200,高:100的矩形
ctx.fillRect(100, 0, 200, 100)
}

每次调用 rotate() 方法都是基于当前绘图上下文的状态进行旋转,就是在上一次旋转的角度基础上再进行旋转。用closePath()也不能重置旋转的角度为0°

缩放

语法:scale(x, y),其中 x 为水平缩放的值,y 为垂直缩放得值。x和y的值小于1则为缩小,大于1则为放大。默认值为 1。

const canvas = document.getElementById('canvas'); // 获取Canvas
const ctx = canvas.getContext('2d'); // 获取绘制上下文

for (let i = 0; i < 9; i++) {
ctx.fillStyle=`#${i}${i}${i}`//颜色渐变
ctx.beginPath()
ctx.scale(2 / i, 2 / i);
// 绘制圆
ctx.arc(250, 250, 50, 0, 360 * Math.PI/180);
ctx.fill();
}

4becc49ca733443cbac3b2b70a26baa3~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.webp

状态的保存和恢复

什么是状态的保存和恢复呢?我们这么理解,当我们在Canvas中绘制时,每次绘制完都会是一个Canvas的快照,而每个快照时的状态,我们可以保存起来,当我们需要再次使用时,又把这个快照恢复。

状态的保存和恢复 用到的方法是 save()restore(), 分别是保存和恢复。方法不需要参数,直接调用就OK。

绘画的状态有哪些呢(就是我们可以保存和恢复的状态有哪些)?我们列举一下:

  • 应用的变形:移动、旋转、缩放、strokeStyle、fillStyle、globalAlpha、lineWidth、lineCap、lineJoin、miterLimit、lineDashOffset、shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor、globalCompositeOperation、font、textAlign、textBaseline、direction、imageSmoothingEnabled等。
  • 应用的裁切路径(clipping path)
const canvas = document.getElementById('canvas'); // 获取Canvas
const ctx = canvas.getContext('2d'); // 获取绘制上下文

ctx.fillStyle = "gray";
ctx.fillRect(10, 10, 200, 100);
// 保存状态
ctx.save();
ctx.fillStyle = "orange";
ctx.fillRect(10, 150, 200, 100);
// 恢复上次保存的状态
ctx.restore();
ctx.fillRect(10, 300, 200, 200);

28e1c9899a1749fab7893b7d21f057b9~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.webp

如上图我们可以看出,最开始我们设置了填充颜色为灰色,并绘制了一个矩形,然后我们执行了状态保存,上面我们已经列举了哪些状态可以保存,所以这里我们知道此次的状态保存的是:fillStyle状态,保存完以后我们又设置了填充颜色为橘色,并且又绘制了一个矩形,最后我们执行了一次状态恢复,接着直接绘制一个正方形。我们知道如果没有状态保存和恢复的方法,正常情况下正方形应该是使用橘色来填充,但正因为我们保存了fillStyle状态的灰色,又在绘制正方形之前恢复了fillStyle状态为灰色,因此绘制出来的正方形为灰色。

2.动画

Canvas呈现的东西都是绘制完了以后才能看到,因此想通过Canvas自己提供的Api来实现动画是做不到的。

那么想在 Canvas 中实现动画就得借助别的东西,那么借助啥呢?

在我们的 windows 对象上有三个方法:

  • setInterval(function, delay) :定时器,当设定好间隔时间后,function 会定期执行。
  • setTimeout(function, delay):延时器,在设定好的时间之后执行函数
  • requestAnimationFrame(callback):告诉浏览器你希望执行一个动画,并在重绘之前,请求浏览器执行一个特定的函数来更新动画。

那么这三个方法有什么区别呢?

正常情况下,当我们需要自动去展示动画而不需要和用户交互的情况下,我们会选择 setInterval()方法,因为我们只需要把执行动画的代码丢在 setInterval()方法中,他就会自动执行绘制我们想要的动画。如果我们做一些交互性的动画,那么使用 setTimeout() 方法和键盘或者鼠标事件配合会更简单一些。相对于前两个方法,requestAnimationFrame()方法可能会显得陌生一些,requestAnimationFrame()方法提供了更加平缓且有效率的方式来执行动画,当我们准备好动画以后,把动画交给requestAnimationFrame()方法就能绘制动画帧。

setInterval setTimeout

这里先使用 setInterval()方法实现一个元素的位移效果。

const canvas = document.getElementById('canvas'); // 获取Canvas
const ctx = canvas.getContext('2d'); // 获取绘制上下文
ctx.fillStyle = "#ccc";
let num = 0
setInterval(()=>{
num += 1
if(num <= 400) {
ctx.fillRect(num, 0, 100, 100);
}
})

7fc69c0842034fe6a423d696e6a365f0~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.webp1.gif

如图我们可以看出,元素确实动了,但是似乎不是我们想要的那个样子,我们想实现的是元素的位移,但看样子实现的是元素的变宽。

那么我们看一下问题出在哪里?

经过我们的一番思考,我们发现,Canvas 绘制时把元素一帧一帧的绘制到画布上,比如上面的例子我们把一个元素从(0,0)移动到(400,0),也就是横向移动400像素。既然是一帧一帧绘制的,那么我们看到的就是连续的从(0,0)绘制到(400,0)的效果,也就是我们看到的是所有的帧组合在一起的效果,而不是从(0,0)移动到(400,0)的效果。

那么想要看到移动的效果就需要我们只看此时此刻的那一帧,而不看之前的帧。因此我们在绘制下一帧的同时我们需要把上一帧清除掉。

画布清空

语法:clearRect(x, y, width, height)

参数:

  • x为要清除的矩形区域左上角的x坐标,
  • y为要清除的矩形区域左上角的y坐标
  • width为要清除的矩形区域的宽度
  • height为要清除的矩形区域的高度
const canvas = document.getElementById('canvas'); // 获取Canvas
const ctx = canvas.getContext('2d'); // 获取绘制上下文
ctx.fillStyle = "#ccc";
const width = canvas.width
const height = canvas.height
let num = 0
setInterval(()=>{
num += 1
if(num <= 400) {
ctx.clearRect(0, 0, width, height) //清除整张画布
ctx.fillRect(num, 0, 100, 100);
}
})

4f3b8056d42d4e5dade50336be33da54~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.webp

requestAnimationFrame

requestAnimationFrame()方法的整体性能要比setInterval()方法好很多,打个比方,当我们用setInterval()方法来做动画,我们需要设置一下多长时间执行一次setInterval()方法里面的代码块,而这个时间我们只要设定了,那么就会强行这个时间执行,而如果我们的浏览器显示频率和setInterval()方法执行的绘制请求不一致,就会导致一些帧率消失,从而造成卡顿的效果。因此使用requestAnimationFrame()方法做动画会更加平缓且有效率。

同时在requestAnimationFrame()方法的使用中我们需要注意,一般每秒钟回调函数执行次数为60次,但也可能会被降低,因为通常情况下requestAnimationFrame()方法会遵循W3C的建议,在浏览器中的回调函数执行次数需要和浏览器屏幕刷新次数相匹配。还有就是为了提高性能和电池使用寿命,requestAnimationFrame() 方法运行在后台标签页或者隐藏在 <iframe>标签里时,requestAnimationFrame()方法会暂停调用以提升性能和电池使用寿命。

requestAnimationFrame()方法不能自循环,那怎么让他实时触发渲染呢?

function callbackFn() {
// 放入需要执行的代码块
requestAnimationFrame(callbackFn);
}
requestAnimationFrame(callbackFn);

这样就形成一个递归,当执行完以后会自动调用它自己。

    const canvas = document.getElementById('canvas'); 
// 获取绘制上下文
const ctx = canvas.getContext('2d');
// globalCompositeOperation 属性设置或返回如何将一个源(新的)图像绘制到目标(已有的)的图像上。
// 这里主要是为了让飞机压在运行轨迹上
ctx.globalCompositeOperation = 'destination-over';
//globalCompositeOperation 设置或返回如何将一个源(新的)图像绘制到目标(已有的)的图像上
//destination-over 把源图像绘制到目标图像的上面(也就是源图像盖到目标图像的上面)
const width = canvas.width
const height = canvas.height
let num = 0
ctx.strokeStyle = "#ccc"
const img = new Image()
img.src="../images/plane.png"
img.onload = ()=>{
requestAnimationFrame(planeRun);
}
function planeRun(){
// 清空画布
ctx.clearRect(0, 0, width, height)

// 保存画布状态
ctx.save();

// 把圆心移到画布中间
ctx.translate(250, 250);

// 绘制飞机和飞机动画
num += 0.01
ctx.rotate(-num);
ctx.translate(0, 200);
ctx.drawImage(img, -20, -25, 40, 40);

// 恢复状态
ctx.restore();

// 飞机运行的轨迹
ctx.beginPath();
ctx.arc(250, 250, 200, 0, Math.PI * 2, false);
ctx.stroke();

// 执行完以后继续调用
requestAnimationFrame(planeRun);
}

d4ccea40300842079c5755adb3dea5fa~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.webp