canvas动画之基础知识

canvas和svg是我觉得前端领域十分有趣的知识点。在很早之前就开始学习canvas了,但是使用的机会却比较少。最近开发负责接手无线活动业务,有大量运营游戏等,因此需要回过头来整理canvas基础。

整个系列我目前也不清楚要写几篇博客,大概包括基础知识、动画思路和几个例子等~其中有部分内容是从我之前的云笔记里面摘抄过来的。

<!--more-->

参考:

1. 基本概念

1.1. canvas

canvas只是一个HTML标签,只是该DOM元素具有一些自带的方法,通过JS调用可以绘制图形

<canvas id="myCanvas" width="200" height="100" style="border: 1px solid red;"></canvas>

一般通过行内样式指定画布大小而不是css样式,因为这个大小不仅代表了画布大小,还代表了内部图形的分辨率。文档里面提到:如果CSS的尺寸与初始画布的比例不一致,它会出现扭曲

也可以直接通过canvas对象的height和width属性来设置其大小。

1.2. context

canvas创建了一个画布,并提供一个或多个渲染上下文context,其可以用来绘制和处理要展示的内容

var ctx = canvas.getContext("2d")
var ctx2 = canvas.getContext("2d")
console.log(ctx === ctx2) // true

context拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法,接下来要学习的绘图API都是基于context的。先大致了解下API文档,然后学习绘图思路。

2. API

2.1. 绘制基本图形

canvas只支持一种原生的图形绘制:矩形。所有其他的图形的绘制都至少需要生成一条路径。

绘制一个填充色为红色的矩形

ctx.fillStyle = "rgb(255,0,0)";
ctx.fillRect(10, 10, 55, 50);

绘制一个边框为半透明蓝色的矩形

ctx.strokeStyle = "rgba(0, 0, 255, 0.5)";
ctx.strokeRect(30, 30, 55, 50);

清除指定矩形区域,让清除部分完全透明。

clearRect(x, y, width, height)

上面三个绘制矩形的方法,其参数是一致的,x与y指定了在canvas画布上所绘制的矩形的左上角(相对于原点)的坐标,而width和height设置矩形的尺寸。

2.2. 绘制其他图形

从数学几何开始,我们从点、线、面的顺序开始了解。

点与坐标

绘制点,我们需要掌握的是点的位置,即canvas的坐标系统。我们知道HTML文档流是从上到下、从左到右的。web中的canvas坐标也是如此,即

Canvas的坐标以左上角为原点,水平向右为X轴,垂直向下为Y轴,以像素为单位,所以每个点都是非负整数。

当使用某些游戏引擎时可能会发现部分游戏引擎使用的是笛卡尔坐标系,即以屏幕左下角为原点,向上向右为正,一定要注意他们之间的区分。

绘制线

根据两个点就可以绘制线,一般情况下只需要通过指定起点和终点即可。

实际上,这里的“线”,在canvas中的术语叫做路径

// 移动画笔到某个起点
ctx.moveTo(75, 50);
// 从当前位置开始绘制到目标位置的一条线
ctx.lineTo(100, 75);
ctx.stroke();

此外还有一些其他的线

  • arc(x, y, radius, startAngle, endAngle, anticlockwise)绘制圆弧
  • quadraticCurveTo(cp1x, cp1y, x, y)绘制二次贝塞尔曲线
  • bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)绘制三次贝塞尔曲线

绘制面

面在canvas的术语叫做“路径”

路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。

在canvas中的面是封闭的,绘制面需要经历下面几个步骤

  • 第一步叫做beginPath()
  • 第二步就是调用函数指定绘制路径
  • 第三步就是闭合路径closePath()
// 需要创建路径起始点
ctx.beginPath();
ctx.moveTo(75, 50)
ctx.lineTo(100, 75)
ctx.lineTo(100, 25)
// 之后把路径封闭
ctx.closePath()
// 一旦路径生成,就能通过描边或填充路径区域来渲染图形
ctx.fill()

有下面几个需要注意的地方

  • 当你调用fill()函数时,所有没有闭合的形状都会自动闭合,所以你不需要手动调用closePath()函数,但是调用stroke()时不会自动闭合哦。
  • 可以通过moveTo绘制一些不连续的路径
  • 路径描述完毕,记得要使用fill或stroke进行绘制,并将控制权交回context
  • fill可以指定一个填充规则:nonzero或者evenodd
    ctx.beginPath();
    ctx.arc(50, 50, 30, 0, Math.PI * 2, true);
    ctx.arc(50, 50, 15, 0, Math.PI * 2, true);
    ctx.fill("evenodd"); // 填充的是一个圆环
  • Path2D对象*

使用上述方法绘制图形的一个弊端是,如果图形比较复杂,则需要绘制许多路径,整个代码看起来就像"面条一样"。

新版的浏览器提供了Path2D对象,下面直接使用官方的demo了

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

var circle = new Path2D();
circle.moveTo(125, 35);
circle.arc(100, 35, 25, 0, 2 * Math.PI);

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

这样的代码看起来逻辑要清晰得多。Path2D对象一个非常有用的使用方式就是:支持SVG路径参数。

比如下面代码绘制了一个心形图案

var p = new Path2D(
    "M0 10 L10 10 L10 0 L30 0 L30 10 L40 10 L40 0 L60 0 L60 10 L70 10 L70 30 L60 30 L60 40 L50 40 L50 50 L40 50 L40 60 L30 60 L30 50 L20 50 L20 40 L10 40 L10 30 L0 30 Z"
);

ctx.fillStyle = 'red'
ctx.fill(p);

2.3. 图形样式

可以通过调整下面的context属性设置绘制的样式

颜色和透明度

  • strokeStylefillStyle,设置画笔的颜色,其值可以是CSS 颜色值的字符串,渐变对象或者图案对象等
  • globalAlpha设置透明度,这个属性影响canvas中所有图形的透明度

线型

canvas的线型样式包括如下方面

  • lineWidth,设置线宽,线宽是指给定路径的中心到两边的粗细
  • lineCap,设置线段末尾样式,是圆角还是矩形
  • lineJoin,设置两个线条结合处的样式
  • setLineDash(),设置线类型
  • lineDashOffset,设置起始偏移量

渐变 渐变在canvas由canvasGradient对象控制

  • 首先通过createLinearGradientcreateRadialGradient创建渐变对象
  • 然后通过渐变对象的addColorStop进行上色
  • 将渐变对象作为画笔的颜色进行使用即可
// 指定渐变方向为竖直向下,距离为(0,0)到(0,150)
var lingrad = ctx.createLinearGradient(0, 0, 0, 150);
// 0 表示起始位置,起始颜色为00ABEB
lingrad.addColorStop(0, '#00ABEB');
// 0.5表示150*0.5位置处的颜色为 fff
lingrad.addColorStop(0.5, '#fff');
// 可以同时指定多个颜色,
// 此处也为150*0.5,颜色26C000,视觉上会出现断层
lingrad.addColorStop(0.5, '#26C000');
// 1表示终点位置
lingrad.addColorStop(1, '#fff');

同理,径向渐变也可以按照上面的方式进行理解。

阴影 canvas也可以绘制阴影,可以参考CSS的box-shadow

  • shadowOffsetXshadowOffsetY用来设定阴影在 X 和 Y 轴的延伸距离
  • shadowBlur用来设定阴影的模糊程度
  • shadowColor用来设定阴影颜色效果

2.4. 绘制文本

在canvas中绘制文本也是十分常见的需求,跟绘制闭合路径一样,也有下面这两种接口

  • fillText(text, x, y [, maxWidth])
  • strokeText(text, x, y [, maxWidth])

同样地,文本也具备相应的样式

  • font,绘制字体样式,与CSS中的font属性值可设定的值相同
  • textAlign,字体对齐方式
  • textBaseline,字体基线
  • direction,字体方向,用的比较少

2.5. 绘制图片

引入图像到canvas里需要以下两步基本操作

  • 获得一个img DOM对象或者另一个canvas元素的引用作为源,也可以通过提供一个URL的方式来使用图片
  • 使用drawImage()函数将图片绘制到画布上
var img = new Image()
img.onload = function(){
    ctx.drawImage(this, 0, 0)
}

img.src = './img/dragon_1.png'

图片预加载 可以看见,我们需要等待图片加载完成,调用的drawImage才有效,如果是下面的代码,则绘制可能会失败,因为图片加载的请求在浏览器中是异步的。

var img = new Image()
img.src = './img/dragon_1.png'
ctx.drawImage(this, 0, 0)

图片预加载在优化用户体验方面是一个常用的手段,如隐藏图片,css背景图加载等方法,提前加载需要的图片。

这里的一个细节是注意先注册onload事件,然后再设置img.src属性,因为如果图片从缓存中加载,速度非常快以至于没来得及绑定事件就加载完毕,就不会触发绑定事件。

drawImage重载方法 drawImage方法有多种重载形式,根据参数个数的不同,其行为也会发生变化

// x,y表示图片在画布中的起始坐标
ctx.drawImage(img, x, y)


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

// 切片,这个方法还是参考MDN上的图片理解比较合适
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

drawImage

可以通过切片,在canvas中使用精灵图~

图片跨域 需要注意的是,canvas的图片来源也受浏览器的同源策略限制,这意味着跨域的图片资源可能会污染画布

污染的画布:一旦画布被污染,你就无法读取其数据。例如,你不能再使用画布的 toBlob(), toDataURL() 或 getImageData() 方法,调用它们会抛出安全错误。 这种机制可以避免未经许可拉取远程网站信息而导致的用户隐私泄露

可以通过CORS解决这些问题。

2.6. 变形

canvas的变形是一种很强大的方法,可以将原点移动到另一点、对网格进行旋转和缩放。

平移translate

translate(x, y)用来将canvas原点移动到新的位置,注意这里的x是左右偏移量,y是上下偏移量,而不是原canvas坐标系中的坐标位置哦~

ctx.fillRect(0,0, 50, 100)

ctx.translate(100, 10);
ctx.fillRect(0, 0, 50, 100)

ctx.translate(100, 10);
ctx.fillRect(0, 0, 50, 100);

旋转rotate

rotate(angle)用于以原点为中心旋转 canvas,注意其参数单位是弧度而不是度,方向顺时针

ctx.rotate(Math.PI * 30 / 180)
ctx.fillRect(0, 0, 100, 50)

这里需要注意的是,旋转的是整个画布,这意味着旋转之后整个canvas坐标实际上都发生了变化。

一个常见的需求是将图案绕其中心进行旋转(而不是画布原点),可以通过先平移再旋转的操作实现

  • 先将canvas原点平移到图案中心
  • 然后再旋转
  • 最后还原canvas原点到初识位置
var width = 100,
    height = 50,
    x = 0,
    y = 0
var offsetX = x + width / 2,
    offsetY = y + height / 2

var angle = 30;
ctx.translate(offsetX, offsetY);
ctx.rotate((Math.PI * angle) / 180);
ctx.translate(-offsetX, -offsetY);

ctx.fillRect(0, 0, width, height);

缩放scale

缩放用来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大,这个跟CSS中比较类似

变形transform

上面的几种形变方法,都可以通过对变形矩阵直接修改来实现

transform(m11, m12, m21, m22, dx, dy)

再次对没有好好学习线性代数深表遗憾....

save和restore

通过上面我们可以认为绘图一般分成两步:

  • 设置对象的状态,如lineWidth,strokeStyle,fillStyle
  • 调用方法绘制图形,如stroke(),fill()

可见canvas绘图是基于状态的,因此后面的状态会覆盖前面的状态,可以使用saverestore来恢复到canvas的某个绘制状态

  • save将当前的canvas状态保存在一个栈中
  • restore将上一个保存的状态从栈中弹出,并为canvas应用该保存的的状态

在对canvas进行变形之前先保存状态是一个良好的习惯,这比手动恢复到变形前的状态要可靠得多。

2.7. 合成与裁剪

合成

可以通过绘制多张图片来合成新的图片,对于多张图片,在css中我们通过z-index属性来控制元素的层级关系,在canvas中,可以通过globalCompositeOperation来设定在画新图形时采用的遮盖策略,其值是一个标识12种遮盖方式的字符串。

ctx.globalCompositeOperation = type

type的具体取值可以参考文档

裁剪

clip方法创建一个新的裁切路径,裁切路径创建之后所有出现在它里面的东西才会画出来

ctx.beginPath();
ctx.arc(0,0,60,0,Math.PI*2,true);
ctx.clip();

3. 小结

这里整理了canvas基本的api,基本上都是跟着文档梳理了一遍,接下来就是学习canvas更高级一点的东西了,比如动画和粒子效果等~