记录一些 Canvas 笔记

582 阅读20分钟

Canvas 笔记

简介

Canvas API 提供了一个通过JavaScript 和 HTML的<canvas>元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。

Canvas API主要聚焦于2D图形。而同样使用<canvas>元素的 WebGL API 则用于绘制硬件加速的2D和3D图形。

canvas 元素

<canvas id="canvas" width="150" height="150"></canvas>

<canvas> 看起来和 <img> 元素很相像,唯一的不同就是它并没有 src 和 alt 属性。实际上,<canvas> 标签只有两个属性—— width和height。这些都是可选的,并且同样利用 DOM properties 来设置。当没有设置宽度和高度的时候,canvas会初始化宽度为300像素和高度为150像素。该元素可以使用CSS来定义大小,但在绘制时图像会伸缩以适应它的框架尺寸:如果CSS的尺寸与初始画布的比例不一致,它会出现扭曲。
    
注意: 如果你绘制出来的图像是扭曲的, 尝试用width和height属性为<canvas>明确规定宽高,而不是使用CSS。

替换内容

<canvas> 元素与 <img> 标签的不同之处在于,就像<video><audio>,或者 <picture>元素一样,很容易定义一些替代内容。由于某些较老的浏览器(尤其是IE9之前的IE浏览器)或者文本浏览器不支持HTML元素"canvas",在这些浏览器上你应该总是能展示替代内容。

这非常简单:我们只是在<canvas>标签中提供了替换内容。不支持<canvas>的浏览器将会忽略容器并在其中渲染后备内容。而支持<canvas>的浏览器将会忽略在容器中包含的内容,并且只是正常渲染canvas。

<!-- 举个例子,我们可以提供对canvas内容的文字描述或者是提供动态生成内容相对应的静态图片,如下所示: -->
<canvas id="canvas" width="150" height="150">
  浏览器不支持canvas
</canvas>

<canvas id="canvas" width="150" height="150">
  <img src="images/浏览器不支持.png" width="150" height="150" alt=""/>
</canvas>
    
<!-- 注意点:</canvas>标签不可省略 --><img> 元素不同,<canvas> 元素需要结束标签(</canvas>)。如果结束标签不存在,则文档的其余部分会被认为是替代内容,将不会显示出来。
    
如果不需要替代内容,一个简单的 <canvas id="foo" ...></canvas> 在所有支持canvas的浏览器中都是完全兼容的。

渲染上下文

<canvas> 元素创造了一个固定大小的画布,它公开了一个或多个渲染上下文,其可以用来绘制和处理要展示的内容。我们将会将注意力放在2D渲染上下文中。其他种类的上下文也许提供了不同种类的渲染方式;比如, WebGL 使用了基于OpenGL ES的3D上下文 ("experimental-webgl") 。

canvas起初是空白的。为了展示,首先脚本需要找到渲染上下文,然后在它的上面绘制。<canvas> 元素有一个叫做 getContext() 的方法,这个方法是用来获得渲染上下文和它的绘画功能。getContext()只有一个参数,上下文的格式。对于2D图像而言,如本教程,你可以使用 CanvasRenderingContext2Dvar canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

代码的第一行通过使用 document.getElementById() 方法来为 <canvas> 元素得到DOM对象。一旦有了元素对象,你可以通过使用它的getContext() 方法来访问绘画上下文。

基础示例

// 这个简单的例子在画布绘制一条直线
// 1. 通过Document.getElementById() 方法获取HTML <canvas> 元素的引用 
// 2. 接着,HTMLCanvasElement.getContext() 方法获取这个元素的context——图像稍后将在此被渲染。 
// 3. 得到ctx对象后,绘制出从(200, 50)开始到(300, 50)结束的一条直线
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.moveTo(200, 50);
ctx.lineTo(300, 50);
ctx.stroke();

绘制基本图形

绘制矩形

// canvas提供了三种方法绘制矩形:
// 1. 绘制一个填充的矩形
fillRect(x, y, width, height)	
// 2. 绘制一个矩形的边框
strokeRect(x, y, width, height)	
// 3. 清除指定矩形区域,让清除部分完全透明。
clearRect(x, y, width, height)	

//上面提供的方法之中,每一个参数的意思:
// x,y:代表了在canvas画布上所绘制的相对于原点的坐标(左上角)。
// width,height:代表了矩形的尺寸

// 现在就来使用上面的三个函数
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.fillRect(50, 100, 100, 100);		
ctx.clearRect(75, 125, 50, 50);
ctx.strokeRect(85, 135, 30, 30);

绘制路径

图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。使用路径绘制图形需要一些额外的步骤。
    1. 首先,你需要创建路径起始点。
    2. 然后你使用画图命令去画出路径。
    3. 之后你把路径封闭。
    4. 一旦路径生成,你就能通过描边或填充路径区域来渲染图形。
    
/** 以下是所要用到的函数: */
// 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
beginPath()
// 闭合路径之后图形绘制命令又重新指向到上下文中。
closePath()
// 通过线条来绘制图形轮廓。
stroke()
// 通过填充路径的内容区域生成实心的图形。
fill()

移动笔触

一个非常有用的函数,而这个函数实际上并不能画出任何东西,也是上面所描述的路径列表的一部分,这个函数就是moveTo()。或者你可以想象一下在纸上作业,一支钢笔或者铅笔的笔尖从一个点到另一个点的移动过程。

// 将笔触移动到指定的坐标x以及y上。
moveTo(x, y)

线

绘制直线,需要用到的方法lineTo()。

// 绘制一条从当前位置到指定x以及y位置的直线。
lineTo(x, y)

圆弧

绘制圆弧或者圆,我们使用arc()方法。当然可以使用arcTo()。

// 根据给定的控制点和半径画一段圆弧,再以直线连接两个控制点。
arcTo(x1, y1, x2, y2, radius)

// 画一个以(x,y)为圆心的以radius为半径的圆弧(圆),从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成。
arc(x, y, radius, startAngle, endAngle, anticlockwise)

详细介绍一下arc方法,该方法有六个参数:
x,y为绘制圆弧所在圆上的圆心坐标。
radius为半径。
startAngle以及endAngle参数用弧度定义了开始以及结束的弧度。这些都是以x轴为基准。
参数anticlockwise为一个布尔值。为true时,是逆时针方向,否则顺时针方向。

// 示例绘制一个圆
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.arc(100, 100, 50, 0, Math.PI * 2, true);
ctx.stroke();

二次贝塞尔曲线及三次贝塞尔曲线

一个十分有用的路径类型就是贝塞尔曲线。二次及三次贝塞尔曲线都十分有用,一般用来绘制复杂有规律的图形。

// 绘制二次贝塞尔曲线,cp1x,cp1y为一个控制点,x,y为结束点。
quadraticCurveTo(cp1x, cp1y, x, y)

// 绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

二次贝塞尔曲线有一个开始点、一个结束点以及一个控制点,而三次贝塞尔曲线有两个控制点。

参数x、y在这两个方法中都是结束点坐标。cp1x,cp1y为坐标中的第一个控制点,cp2x,cp2y为坐标中的第二个控制点。

Path2D 对象

正如我们在前面例子中看到的,你可以使用一系列的路径和绘画命令来把对象“画”在画布上。为了简化代码和提高性能,Path2D对象已可以在较新版本的浏览器中使用,用来缓存或记录绘画命令,这样你将能快速地回顾路径。

// Path2D()会返回一个新初始化的Path2D对象(可能将某一个路径作为变量——创建一个它的副本,或者将一个包含SVG path数据的字符串作为变量)。
new Path2D();     // 空的Path对象
new Path2D(path); // 克隆Path对象
new Path2D(d);    // 从SVG建立Path对象

// 添加了一条路径到当前路径(可能添加了一个变换矩阵)。
Path2D.addPath(path [, transform])

// 示例
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

const rectangle = new Path2D();
rectangle.rect(10, 10, 50, 50);

ctx.stroke(rectangle);
ctx.fill(rectangle);

SVG paths

新的Path2D API有另一个强大的特点,就是使用SVG path data来初始化canvas上的路径。这将使你获取路径时可以以SVG或canvas的方式来重用它们。

这条路径将先移动到点 (M10 10) 然后再水平移动80个单位(h 80),然后下移80个单位 (v 80),接着左移80个单位 (h -80),再回到起点处 (z)。

// 示例
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

var p = new Path2D("M10 10 h 80 v 80 h -80 Z");
ctx.fill(p);

样式和颜色

色彩 Colors

如果我们想要给图形上色,有两个重要的属性可以做到:

// 设置图形的填充颜色。
fillStyle = color

// 设置图形轮廓的颜色。
strokeStyle = color

// 示例: 这些 fillStyle 的值均为 '橙色'
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)";

注意: 一旦您设置了 strokeStyle 或者 fillStyle 的值,那么这个新值就会成为新绘制的图形的默认值。如果你要给每个图形上不同的颜色,你需要重新设置 fillStyle 或 strokeStyle 的值。

透明度 Transparency

除了可以绘制实色图形,我们还可以用 canvas 来绘制半透明的图形。通过设置 globalAlpha 属性或者使用一个半透明颜色作为轮廓或填充的样式。

// 这个属性影响到 canvas 里所有图形的透明度,有效的值范围是 0.0 (完全透明)到 1.0(完全不透明),默认是 1.0
globalAlpha = 1.0

线型 Line styles

可以通过一系列属性来设置线的样式

// 设置线条宽度。
lineWidth = value

// 设置线条末端样式。
lineCap = type

// 设定线条与线条间接合处的样式。
lineJoin = type

// 限制当两条线相交时交接处最大长度;所谓交接处长度(斜接长度)是指线条交接处内角顶点到外角顶点的长度。
miterLimit = value

// 返回一个包含当前虚线样式,长度为非负偶数的数组。
getLineDash()

// 设置当前虚线样式。
setLineDash(segments)

// 设置虚线样式的起始偏移量。
lineDashOffset = value

lineWidth 属性

这个属性设置当前绘线的粗细。属性值必须为正数。默认值是1.0。

线宽是指给定路径的中心到两边的粗细。换句话说就是在路径的两边各绘制线宽的一半。因为画布的坐标并不和像素直接对应,当需要获得精确的水平或垂直线的时候要特别注意。

lineCap 属性

属性 lineCap 的值决定了线段端点显示的样子。
它可以为下面的三种的其中之一:butt,round 和 square。
默认是 butt。

lineJoin属性

lineJoin 的属性值决定了图形中两线段连接处所显示的样子。
它可以是这三种之一:round, bevel 和 miter。
默认是 miter。

miterLimit 属性

miterLimit 属性就是用来设定外延交点与连接点的最大距离,如果交点距离大于此值,连接效果会变成了 bevel。注意,最大斜接长度(即交点距离)是当前坐标系测量线宽与此miterLimit属性值(HTML <canvas>默认为10.0)的乘积,所以miterLimit可以单独设置,不受显示比例改变或任何仿射变换的影响:它只影响线条边缘的有效绘制形状

使用虚线

用 setLineDash 方法和 lineDashOffset 属性来制定虚线样式. setLineDash 方法接受一个数组,来指定线段与间隙的交替;lineDashOffset 属性设置起始偏移量.

// 4表示线条长度,2表示间距
ctx.setLineDash([4, 2]);	

渐变 Gradients

就好像一般的绘图软件一样,我们可以用线性或者径向的渐变来填充或描边。我们用下面的方法新建一个 canvasGradient 对象,并且赋给图形的 fillStyle 或 strokeStyle 属性。

// createLinearGradient 方法接受 4 个参数,表示渐变的起点 (x1,y1) 与终点 (x2,y2)。
createLinearGradient(x1, y1, x2, y2)

// createRadialGradient 方法接受 6 个参数,前三个定义一个以 (x1,y1) 为原点,半径为 r1 的圆,后三个参数则定义另一个以 (x2,y2) 为原点,半径为 r2 的圆。
createRadialGradient(x1, y1, r1, x2, y2, r2)

// 方法接受 2 个参数,position 参数必须是一个 0.0 与 1.0 之间的数值,表示渐变中颜色所在的相对位置。例如,0.5 表示颜色会出现在正中间。color 参数必须是一个有效的 CSS 颜色值(如 #FFF, rgba(0,0,0,1),等等)。
gradient.addColorStop 

// 示例
 const lineargradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 50);
lineargradient.addColorStop(0, '#fff');
lineargradient.addColorStop(1, '#50c5f1');

ctx.fillStyle = lineargradient;
ctx.arc(100, 100, 50, 0, Math.PI * 2, true);
ctx.fill();

图案样式 Patterns

// 该方法接受两个参数。Image 可以是一个 Image 对象的引用,或者另一个 canvas 对象。Type 必须是下面的字符串值之一:repeat,repeat-x,repeat-y 和 no-repeat。
createPattern(image, type)

注意: 用 canvas 对象作为 Image 参数在 Firefox 1.5 (Gecko 1.8) 中是无效的。
注意:与 drawImage 有点不同,你需要确认 image 对象已经装载完毕,否则图案可能效果不对的。

// 示例

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

const img = new Image();
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
img.onload = () => {
    const ptrn = ctx.createPattern(img, 'repeat');
    ctx.fillStyle = ptrn;
    ctx.fillRect(0, 0, 400, 400);
}

阴影 Shadows

// shadowOffsetX 和 shadowOffsetY 用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为 0。
shadowOffsetX = float
shadowOffsetY = float

// shadowBlur 用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响,默认为 0。
shadowBlur = float

// shadowColor 是标准的 CSS 颜色值,用于设定阴影颜色效果,默认是全透明的黑色。
shadowColor = color

// 文字阴影例子
ctx.shadowOffsetX = 4;
ctx.shadowOffsetY = 4;
ctx.shadowColor = 'rgb(193, 193, 193)';
ctx.shadowBlur = 3;
ctx.font = "40px 微软雅黑";
ctx.fillText('Hello Canvas', 100, 100);

绘制文本

canvas提供了两种方法来渲染文本

// 在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的.
fillText(text, x, y [, maxWidth])

// 在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的.
strokeText(text, x, y [, maxWidth])

有样式的文本

// 当前我们用来绘制文本的样式. 这个字符串使用和 CSS font 属性相同的语法. 默认的字体是 10px sans-serif。
font = value

// 文本对齐选项. 可选的值包括:start, end, left, right or center. 默认值是 start。
textAlign = value

// 基线对齐选项. 可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom。默认值是 alphabetic。
textBaseline = value

// 文本方向。可能的值包括:ltr, rtl, inherit。默认值是 inherit。
direction = value

预测量文本宽度

当你需要获得更多的文本细节时,下面的方法可以给你测量文本的方法。

// 将返回一个 TextMetrics对象的宽度、所在像素,这些体现文本特性的属性。
measureText()

// 下面的代码段将展示如何测量文本来获得它的宽度:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

const text = ctx.measureText("Hello");
console.log(text.width);

图像

canvas更有意思的一项特性就是图像操作能力。可以用于动态的图像合成或者作为图形的背景,以及游戏界面(Sprites)等等。浏览器支持的任意格式的外部图片都可以使用,比如PNG、GIF或者JPEG。 你甚至可以将同一个页面中其他canvas元素生成的图片作为图片源。

获取需要绘制的图片

canvas的API可以使用下面这些类型中的一种作为图片的源:

// 这些图片是由Image()函数构造出来的,或者任何的<img>元素
HTMLImageElement

// 用一个HTML的 <video>元素作为你的图片源,可以从视频中抓取当前帧作为一个图像
HTMLVideoElement

// 可以使用另一个 <canvas> 元素作为你的图片源。
HTMLCanvasElement

// 这是一个高性能的位图,可以低延迟地绘制,它可以从上述的所有源以及其它几种源中生成。
ImageBitmap

// 这些源统一由 CanvasImageSource类型来引用。

绘制图片

一旦获得了源图对象,我们就可以使用 drawImage 方法将它渲染到 canvas 里。
drawImage 方法有三种形态:

// 其中 image 是 image 或者 canvas 对象,x 和 y 是其在目标 canvas 里的起始坐标。
drawImage(image, x, y)

// 这个方法多了2个参数:width 和 height,这两个参数用来控制 当向canvas画入时应该缩放的大小
drawImage(image, x, y, width, height)

// 第一个参数和其它的是相同的,都是一个图像或者另一个 canvas 的引用。
// 其它8个参数最好是参照右边的图解,
// 前4个是定义图像源的切片位置和大小,后4个则是定义切片的目标显示位置和大小。
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

控制图像的缩放行为

过度缩放图像可能会导致图像模糊或像素化。
您可以通过使用绘图环境的imageSmoothingEnabled属性来控制是否在缩放图像时使用平滑算法。
默认值为true,即启用平滑缩放。
您也可以像这样禁用此功能:

ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;

变形 Transformations

状态的保存和恢复

// 保存画布(canvas)的所有状态
save()

// save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。
restore()


你可以调用任意多次 save方法。每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。

移动 Translating

// translate 方法接受两个参数。x 是左右偏移量,y 是上下偏移量,如右图所示。
translate(x, y)

旋转 Rotating

// 这个方法只接受一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。
rotate(angle)

旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到 translate 方法。

缩放 Scaing

我们用它来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大。

// scale  方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子,如果比1小,会缩小图形, 如果比1大会放大图形。默认值为1, 为实际大小。
scale(x, y)

变形 Transforms

// 这个方法是将当前的变形矩阵乘上一个基于自身参数的矩阵
transform(a, b, c, d, e, f)
	// 这个函数的参数各自代表如下:
    // 水平方向的缩放
    a (m11)
    // 竖直方向的倾斜偏移
    b(m12)
    // 水平方向的倾斜偏移
    c(m21)
    // 竖直方向的缩放
    d(m22)
    // 水平方向的移动
    e(dx)
    // 竖直方向的移动
    f(dy)

// 这个方法会将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用 transform 方法。如果任意一个参数是无限大,那么变形矩阵也必须被标记为无限大,否则会抛出异常。从根本上来说,该方法是取消了当前变形,然后设置为指定的变形,一步完成。
setTransform(a, b, c, d, e, f)

// 重置当前变形为单位矩阵,它和调用以下语句是一样的:ctx.setTransform(1, 0, 0, 1, 0, 0);
resetTransform()

合成与裁切

组合 Compositing

// 这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识12种遮盖方式的字符串。
globalCompositeOperation = type

// 可选参数:
// 这是默认设置,并在现有画布上下文之上绘制新图形。
'source-over'

// 新图形只在新图形和目标画布重叠的地方绘制。其他的都是透明的。
'source-in'

// 在不与现有画布内容重叠的地方绘制新图形。
'source-out'

// 新图形只在与现有画布内容重叠的地方绘制。
'source-atop'

// 在现有的画布内容后面绘制新的图形。
'destination-over'

// 现有的画布内容保持在新图形和现有画布内容重叠的位置。其他的都是透明的。
'destination-in'

// 现有内容保持在新图形不重叠的地方。
'destination-out'

// 现有的画布只保留与新图形重叠的部分,新的图形是在画布内容后面绘制的。
'destination-atop'

// 两个重叠图形的颜色是通过颜色值相加来确定的。
'lighter'

// 只显示新图形。
'copy'

// 图像中,那些重叠和正常绘制之外的其他地方是透明的。
'xor' 
    
// 将顶层像素与底层相应像素相乘,结果是一幅更黑暗的图片。
'multiply'

// 像素被倒转,相乘,再倒转,结果是一幅更明亮的图片。
'screen'
    
// multiply和screen的结合,原本暗的地方更暗,原本亮的地方更亮。
'overlay'

// 保留两个图层中最暗的像素。
'darken'

// 保留两个图层中最亮的像素。
'lighten'

// 将底层除以顶层的反置。
'color-dodge'

// 将反置的底层除以顶层,然后将结果反过来。
'color-burn'

// 屏幕相乘(A combination of multiply and screen)类似于叠加,但上下图层互换了。
'hard-light'

// 用顶层减去底层或者相反来得到一个正值。
'soft-light'

// 一个柔和版本的强光(hard-light)。纯黑或纯白不会导致纯黑或纯白。
'difference'

// 和difference相似,但对比度较低。
'exclusion'

// 保留了底层的亮度(luma)和色度(chroma),同时采用了顶层的色调(hue)。
'hue'

// 保留底层的亮度(luma)和色调(hue),同时采用顶层的色度(chroma)。
'saturation'

// 保留了底层的亮度(luma),同时采用了顶层的色调(hue)和色度(chroma)。
'color'

// 保持底层的色调(hue)和色度(chroma),同时采用顶层的亮度(luma)。
'luminosity'

裁切路径

裁切路径和普通的 canvas 图形差不多,不同的是它的作用是遮罩,用来隐藏不需要的部分。

// 将当前正在构建的路径转换为当前的裁剪路径。
clip()

// 示例
ctx.fillRect(100, 50, 200, 200);
ctx.arc(200, 150, 75, 0, Math.PI * 2, true);
ctx.clip();

ctx.fillStyle = 'green';
ctx.fillRect(100, 50, 200, 200);

基本动画

动画的基本步骤

/** 你可以通过以下的步骤来画出一帧: */

1. 清空 canvas
除非接下来要画的内容会完全充满 canvas (例如背景图),否则你需要清空所有。最简单的做法就是用 clearRect 方法。

2. 保存 canvas 状态
如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。

3. 绘制动画图形(animated shapes)
这一步才是重绘动画帧。

4. 恢复 canvas 状态
如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。

更新画布

// 当设定好间隔时间后,function会定期执行。
setInterval(function, delay) (en-US)

// 在设定好的时间之后执行函数
setTimeout(function, delay) (en-US)

// 告诉浏览器你希望执行一个动画,并在重绘之前,请求浏览器执行一个特定的函数来更新动画。
requestAnimationFrame(callback)