PixiJs学前篇(四):Canvas进阶【动画篇】🔥🔥


theme: smartblue
highlight: a11y-dark

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

大家好我是ndz,很高兴也很荣幸成为了一名稀土掘金技术社区签约作者,在这里真的很感谢平台给予的肯定和各位读者的支持,感谢 🙏 🙏 🙏。

本文为稀土掘金技术社区签约作者专栏 – 《从Canvas到PixiJs》 的第五篇文章,喜欢的小伙伴记得点赞加关注,以防需要用时回来不迷路 😂

前言

在前面的文章中我们已经介绍了 Canvas 基础的内容:

PixiJs学前篇(一):Canvas基础【绘制篇】

PixiJs学前篇(二):Canvas基础【图形篇】

PixiJs学前篇(三):Canvas基础【文字篇】

在我们掌握了基础内容的前提下我们就可以开始学习 Canvas 的进阶内容了,进阶内容其实主要就是对Canvas内容的一个应用,比如动画、事件等。

动画

说到Canvas的动画我又想起了曾经初识Canvas和Pixi就是因为项目中需要做动画的元素太多了,导致性能性能跟不上,所以结识了Canvas、WebGL和Pixi。

在我们的日常开发中,普遍使用的还是DOM,因为DOM的使用比较简单方便,但一些特殊情况下因为元素过多啊之类的导致内存的消耗过大,甚至浏览器的一些默认行为还会导致我们无法很好的控制绘制内容。因此在一些特定情况下我们就需要Canvas或者WebGL来代替DOM。

回归正题,咱们还是来说说Canvas的动画。

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

几何变换

几何变换的类型其实和CSS动画中的类型(不知道也没关系,下面咱们一一讲解)差不多,也就是:移动、旋转、缩放,下面咱们依次介绍一下。

移动

移动就是把元素从一个地方移动到另外一个地方。

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

举个例子看一下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 动画</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    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)
    
  </script>
</body>
</html>

效果如下:

image.png
如图可以看到,原本我们以(0,0)坐标绘制了一个宽200像素,高100像素的矩形,结果我们得到的是一个以(200,200)坐标绘制的一个宽200像素,高100像素的矩形。

因此不难看出,图形整体在x轴和y轴上平移了200像素。

旋转

了解了移动以后,旋转也比较好理解,就是把元素顺时针旋转一定的弧度。

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

举个例子看一下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 动画</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    const canvas = document.getElementById('canvas'); // 获取Canvas
    const ctx = canvas.getContext('2d'); // 获取绘制上下文
    for (let i = 0; i < 9; i++) {
      ctx.fillStyle=`#${i}${i}${i}`
      // 旋转弧度设置,角度和弧度的转换公式:1° = Math.PI / 180
      ctx.rotate(i * 2 * Math.PI / 180);
      // 在(0,0)坐标点绘制一个宽:200,高:100的矩形
      ctx.fillRect(100, 0, 200, 100)
    }
  </script>
</body>
</html>

效果如下:

image.png

如图可以看出,本来是在(100,0)坐标点绘制8个宽:200,高:100的矩形,但因为每次绘制前我们都指定了旋转的弧度,因此8个绘制出来的元素不在同一个地方。

值得注意的是,旋转的原点是Canvas画布的(0,0)坐标点,而不是元素的左上角或者中心点。当然如果我们想设置为沿某个点旋转也是可以实现的,具体怎么设置,咱们之后再说。

缩放

了解了移动和旋转以后,缩放也就更好理解了,就是把元素按一定的比例整体缩小或放大。

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

举个例子看一下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 动画</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    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();
    }
  </script>
</body>
</html>

效果如下:

image.png

状态的保存和恢复

了解了几何变换以后咱们其实就可以做基本的动画了,但复杂的动画却还不够,这里咱们再介绍一下:状态的保存和恢复

那么什么是状态的保存和恢复呢?我们这么理解,当我们在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)

还不了解的没关系,咱们举个例子说明一下。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 动画</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    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);

  </script>
</body>
</html>

效果如下:

image.png

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

因此我们可以试想一下,如果我们的状态不是只有fillStyle,而是有多个,那么状态的保存和恢复的使用就显得更有价值。

动画

下面我们就正式进入Canvas动画的实现中。我们知道,Canvas呈现的东西都是绘制完了以后才能看到,因此想通过Canvas自己提供的Api来实现动画是做不到的。

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

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

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

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

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

setInterval、setTimeout

使用 Canvas 搭配任意一个方法我们都可以在Canvas中做动画。人狠话不多,咱们直接举例看一下,这里就使用 setInterval()方法实现一个元素的位移效果。

代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 动画</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    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);
      }
    })
  </script>
</body>
</html>

效果如下:

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

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

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

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

那么如何清除呢?

画布清空

在我们的Canvas中其实它本身已经提供了这样的方法:clearRect()

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

参数:

  • x为要清除的矩形区域左上角的x坐标,
  • y为要清除的矩形区域左上角的y坐标
  • width为要清除的矩形区域的宽度
  • height为要清除的矩形区域的高度

那么我们试想一下,既然我们的Canvas是实时渲染的,那么我们是不是直接清除整个画布就可以了呢?

想到咱们就试试:

代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 动画</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    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);
      }
    })
  </script>
</body>
</html>

效果如下:

2.gif

nice!如图我们就把我们想要的效果实现了,

requestAnimationFrame

接下来咱们再单独说一下:requestAnimationFrame()方法吧。

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

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

有的小伙伴可能会有疑问,requestAnimationFrame()方法不能自循环,那怎么让他实时触发渲染呢?

很简单,我们这样一下你就懂了

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

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

下面咱们举个例子看一下:

代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 动画</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    // 获取Canvas
    const canvas = document.getElementById('canvas'); 
    // 获取绘制上下文
    const ctx = canvas.getContext('2d'); 
    // globalCompositeOperation 属性设置或返回如何将一个源(新的)图像绘制到目标(已有的)的图像上。
    // 这里主要是为了让飞机压在运行轨迹上
    ctx.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);
    }
  </script>
</body>
</html>

效果如下:

4.gif

如图小飞机就飞起来了,其中代码中 globalCompositeOperation 属性之前咱们没有说过,其实就是设置或返回如何将一个源(新的)图像绘制到目标(已有的)的图像上,而属性值 destination-over 就是把源图像绘制到目标图像的上面(也就是源图像盖到目标图像的上面)。

动画咱们到此就算说完了,但在上面的 几何变换 中,咱们其实漏了点东西,下面咱们再补充一点。

transform

在咱们的 Canvas 动画中,其实很多时候我们会看到这样的效果:

image.png

这种效果叫做:斜切或者倾斜。然而在上面的几何变换中咱们只说了Canvas自己本身提供的:移动旋转缩放。那么想实现上面这种斜切的效果,就需要用矩阵来实现。

矩阵如果展开来说内容太多,这里咱们就简单说一下transform

transform不仅能实现移动旋转缩放,还能实现斜切

语法:

transform(a, b, c, d, e, f)

参数:

  • a:水平缩放,不缩放为1
  • b:水平倾斜,不倾斜为0
  • c:垂直倾斜,不倾斜为0
  • d:垂直缩放,不缩放为1
  • e:水平移动,不移动为0
  • f:垂直移动,不移动为0

因此不设置参数的时候,默认参数为transform(1, 0, 0, 1, 0, 0)

下面咱们看一下如何用transform实现移动旋转缩放斜切

移动

通过上面的参数我们可以看出,在transform中,控制移动的参数是ef

简单举个例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - transform</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    // 获取Canvas
    const canvas = document.getElementById('canvas'); 
    // 获取绘制上下文
    const ctx = canvas.getContext('2d'); 
    // 填充颜色
    ctx.fillStyle="orange"

    // x轴和y轴都移动了100
    ctx.transform(1, 0, 0, 1, 100, 100);

    ctx.fillRect(0, 0, 200, 200);
   
  </script>
</body>
</html>

效果如下:

image.png

如图可以看出,原本想在(0, 0)坐标上绘制一个宽高都为200的矩形,通过transform在x轴和y轴上都移动了200像素

缩放

通过上面的参数我们也可以看出,在transform中,控制缩放的参数是ad

也简单举个例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - transform</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    // 获取Canvas
    const canvas = document.getElementById('canvas'); 
    // 获取绘制上下文
    const ctx = canvas.getContext('2d'); 
    // 填充颜色
    ctx.fillStyle="orange"

    // x轴和y轴都放大1.5倍
    ctx.transform(1.5, 0, 0, 1.5, 0, 0);
    ctx.fillRect(0, 0, 200, 200);
   
  </script>
</body>
</html>

效果如下:

image.png

如图可以看出,原本想在(0, 0)坐标上绘制一个宽高都为200的矩形,通过transform都放大了1.5倍,当然这里也可以设置为缩小,比如缩小一半则为:0.5

斜切

通过上面的参数我们也可以看出,在transform中,控制斜切的参数是bc

也简单举个例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - transform</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    // 获取Canvas
    const canvas = document.getElementById('canvas'); 
    // 获取绘制上下文
    const ctx = canvas.getContext('2d'); 
    // 填充颜色
    ctx.fillStyle="orange"

    //Y轴逐渐拉伸,得到上图右边状态
    ctx.transform(1, Math.PI/20, 0, 1, 0, 0);

    ctx.fillRect(150, 100, 200, 200);
   
  </script>
</body>
</html>

效果如下:

image.png

如图可以看出,原本的一个宽高都为200的矩形,通过transform拉伸得到上面的效果

旋转

通过上面的参数我们看来没有任何一个参数是设置旋转的,但其实并不是这样,我们可以通过设置transform中的四个参数来实现旋转,具体哪四个参数呢?其实就是abcd

也简单举个例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - transform</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    // 获取Canvas
    const canvas = document.getElementById('canvas'); 
    // 获取绘制上下文
    const ctx = canvas.getContext('2d'); 
    // 填充颜色
    ctx.fillStyle="orange"

    // 旋转30度
    const deg = Math.PI/180;
    const c3 = Math.cos(30*deg)
    const s3 = Math.sin(30*deg)
    ctx.transform(c3, s3, -s3, c3, 0, 0)
    ctx.fillRect(0, 0, 200, 200);
  </script>
</body>
</html>

效果如下:

image.png

如图可以看出,通过设置transformabcd四个参数,可以实现元素的旋转。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容