Canvas的使用
1.基本用例
<canvas id="my-canvas" height="150px" width="150px">canvas</canvas>
上面定义了一个canvas画布,我们通过js脚本和web api能够在上面绘画。这就是canvas的一个作用。
当没有设置宽度和高度的时候,canvas 会初始化宽度为 300 像素和高度为 150 像素。该元素可以使用CSS来定义大小,但在绘制时图像会伸缩以适应它的框架尺寸:如果 CSS 的尺寸与初始画布的比例不一致,它会出现扭曲。
所以,建议直接使用 height 和 width 来设置宽高。
var canvas = document.getElementById('my-canvas')if (canvas.getContext) {// 用来获得渲染2d上下文和它的绘画功能var ctx = canvas.getContext('2d')ctx.fillStyle = 'rgb(200,0,0)'ctx.fillRect(10, 10, 55, 50)ctx.fillStyle = 'rgba(0, 0, 200, 0.5)'ctx.fillRect(30, 30, 55, 50)}
2.使用canvas来绘制图形
栅格
在我们开始画图之前,我们需要了解一下画布栅格(canvas grid)以及坐标空间。上一页中的 HTML 模板中有个宽 150px, 高 150px 的 canvas 元素。如上图所示,canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。栅格的起点为左上角(坐标为(0,0))。所有元素的位置都相对于原点定位。所以图中蓝色方形左上角的坐标为距离左边(X 轴)x 像素,距离上边(Y 轴)y 像素(坐标为(x,y))。在课程的最后我们会平移原点到不同的坐标上,旋转网格以及缩放。现在我们还是使用原来的设置。
简单来讲,canvas画布中的是栅格和坐标空间,都是以右上角为坐标(0,0)做原点,向右延伸为X主轴,向下延伸为y主轴。在下文,那些canvas api要传入的x、y坐标都是基于(0,0)相对来讲的。
绘制矩形
不同于 SVG,<canvas> 只支持两种形式的图形绘制:矩形和路径(由一系列点连成的线段)。所有其他类型的图形都是通过一条或者多条路径组合而成的。不过,我们拥有众多路径生成的方法让复杂图形的绘制成为了可能。
// 绘制一个填充的矩形ctx.fillRect(x, y, width, height)// 绘制一个矩形的边框(描边矩形)ctx.strokeRect(x, y, width, height)// 清除指定矩形区域,让清除部分完全透明。ctx.clearRect(x, y, width, height)
上面提供的方法之中每一个都包含了相同的参数。x 与 y 指定了在 canvas 画布上所绘制的矩形的左上角(相对于原点)的坐标。width 和 height 设置矩形的尺寸。
例子: 用上文canvas
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {var ctx = canvas.getContext('2d')ctx.fillRect(25, 25, 100, 100) ctx.clearRect(45, 45, 60, 60)ctx.strokeRect(50, 50, 50, 50)}}
绘制路径
图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。使用路径绘制图形需要一些额外的步骤。
- 首先,你需要创建路径起始点。
- 然后你使用画图命令去画出路径。
- 之后你把路径封闭。
- 一旦路径生成,你就能通过描边或填充路径区域来渲染图形。
1. moveTo(x, y): 绘制一条从当前位置到指定 x 以及 y 位置的直线。
2. lineTo(x, y):绘制一条从当前位置到指定 x 以及 y 位置的直线。
以画下面这个三角形为例。
function draw() {if (canvas.getContext) {// 用来获得渲染2d上下文和它的绘画功能var ctx = canvas.getContext('2d')ctx.beginPath() // 1.创建路径起始点(清空之前路径列表)ctx.moveTo(75, 50) // moveTo(X,Y)设置起点ctx.lineTo(100, 75) // 绘制一条从当前位置到指定 x 以及 y 位置的直线。ctx.lineTo(100, 25)ctx.closePath()ctx.stroke() // 线条来绘制图形轮廓(路径绘制起作用,这一步几乎是必须的)}}
我们再看下面这个:
function draw() {if (canvas.getContext) {// 用来获得渲染2d上下文和它的绘画功能var ctx = canvas.getContext('2d')ctx.beginPath() // 1.创建路径起始点ctx.moveTo(0, 0)ctx.lineTo(40, 0)ctx.lineTo(0, 40)ctx.fill()// 绘制下面三角形ctx.moveTo(10, 40)ctx.lineTo(50, 40)ctx.lineTo(50, 0)ctx.fill()}}
绘制2个三角形,通过moveTo()方法开启了画笔移动的起点。
画圆弧
arc(x, y, radius, startAngle, endAngle, anticlockwise):
x,y
为绘制圆弧所在圆上的圆心坐标。radius
为半径。startAngle
以及endAngle
参数用弧度定义了开始以及结束的弧度。这些都是以 x 轴为基准。参数anticlockwise
为一个布尔值。为 true 时,是逆时针方向,否则顺时针方向。
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {// 用来获得渲染2d上下文和它的绘画功能var ctx = canvas.getContext('2d')ctx.beginPath() // 1.创建路径起始点ctx.arc(30, 30, 30, 0, Math.PI * 2, true)// X、Y、半径、起始弧度、结束弧度、顺逆时针ctx.stroke()}}
画贝塞尔曲线
嗯.对于canvas贝塞尔曲线,网上有开源的工具,当然看也有自己写的,至于作者没这个实力。
Canvas贝塞尔曲线绘制工具 (karlew.com)
quadraticCurveTo(cp1x, cp1y, x, y):
绘制二次贝塞尔曲线,cp1x,cp1y 为一个控制点,x,y 为结束点。
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y):
绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。
右边的图能够很好的描述两者的关系,二次贝塞尔曲线有一个开始点(蓝色)、一个结束点(蓝色)以及一个控制点(红色),而三次贝塞尔曲线有两个控制点。
下面就用这个贝塞尔曲线画个爱心:
PS : canvas宽高改成500
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {// 用来获得渲染2d上下文和它的绘画功能var ctx = canvas.getContext('2d')ctx.moveTo(186, 176)ctx.quadraticCurveTo(82, 69, 191, 106)ctx.moveTo(186, 176)ctx.quadraticCurveTo(300, 69, 190, 106)ctx.fill()}}
Path2D
正如我们在前面例子中看到的,你可以使用一系列的路径和绘画命令来把对象“画”在画布上。为了简化代码和提高性能,Path2D对象已可以在较新版本的浏览器中使用,用来缓存或记录绘画命令,这样你将能快速地回顾路径。
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {var ctx = canvas.getContext('2d')let path2D = new Path2D() // 想path2D缓存了一个矩形path2D.rect(10, 10, 50, 50)ctx.fill(path2D)}}
3.使用样式和颜色
色彩(Colors)
到目前为止,我们只看到过绘制内容的方法。如果我们想要给图形上色,有两个重要的属性可以做到:fillStyle
和 strokeStyle
。
fillStyle = color
设置图形的填充颜色。
strokeStyle = color
设置图形轮廓的颜色。
color
可以是表示 CSS 颜色值的字符串,渐变对象或者图案对象。
fillStyle示例
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {let ctx = canvas.getContext('2d')for (var i = 0; i < 6; i++) {for (var j = 0; j < 6; j++) {ctx.fillStyle = // 遍历设置样式颜色'rgb(' +Math.floor(255 - 50 * i) +',' +Math.floor(255 - 50 * j) +',0)'ctx.fillRect(j * 25, i * 25, 25, 25) // 遍历设置填充矩形}}}}
strokeStyle示例
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {let ctx = canvas.getContext('2d')for (var i = 0; i < 6; i++) {for (var j = 0; j < 6; j++) {ctx.strokeStyle = // 遍历设置样式颜色'rgb(' +Math.floor(255 - 50 * i) +',' +Math.floor(255 - 50 * j) +',0)'ctx.beginPath()ctx.arc(12 + i * 25, 12 + j * 25, 10, 0, Math.PI * 2, true)ctx.stroke()// 矩形填充不需要,但是路径绘图需要}}}}
透明度 Transparency
canvas中设置透明度有2种:
1.通过设置
globalAlpha
属性或者使用一个半透明颜色作为轮廓或填充的样式。值为( 0 - 1.0)2.就是通过填充或者描边颜色改变透明度。如: rgb(31, 69, 65, 0.1)
globalAlpha
示例
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {let ctx = canvas.getContext('2d')// 画背景ctx.fillStyle = '#FD0'ctx.fillRect(0, 0, 75, 75)ctx.fillStyle = 'rgb(55, 24, 207)'ctx.fillRect(75, 75, 75, 75)ctx.fillStyle = 'rgba(106, 245,23)'ctx.fillRect(75, 0, 75, 75)ctx.fillStyle = 'rgb(197, 63, 39)'ctx.fillRect(0, 75, 75, 75)// 画圈ctx.globalAlpha = 0.2 // 设置一下背景全局透明度为0.2ctx.fillStyle = 'white'for (let index = 0; index < 6; index++) {ctx.beginPath()ctx.arc(75, 75, index * 20, 0, Math.PI * 2, true)ctx.fill()}}}
rgba() 示例
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {let ctx = canvas.getContext('2d')for (let i = 0; i < 4; i++) {// 画背景ctx.fillStyle ='rgb(' +Math.floor(255 - 42.5 * i) +',' +Math.floor(255 - 42.5 * i) +',0)'ctx.fillRect(i * 40, 0, 40, 100)// 画里面的透明矩形ctx.fillStyle = 'rgb(234, 234, 234,0.5)' // 颜色,设置为透明度 0.5ctx.fillRect(5 + i * 40, 10, 30, 80)}}}
线型 Line styles
lineWidth = value
设置线条宽度。
lineCap = type
设置线条末端样式。
lineJoin = type
设定线条与线条间接合处的样式。
miterLimit = value
限制当两条线相交时交接处最大长度;所谓交接处长度(斜接长度)是指线条交接处内角顶点到外角顶点的长度。
getLineDash()
返回一个包含当前虚线样式,长度为非负偶数的数组。
setLineDash(segments)
设置当前虚线样式。
lineDashOffset = value
设置虚线样式的起始偏移量。
lineWidth
属性的例子
线宽是指给定路径的中心到两边的粗细。换句话说就是在路径的两边各绘制线宽的一半。
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {let ctx = canvas.getContext('2d')for (let i = 0; i < 4; i++) {ctx.lineWidth = 1 + i * 2ctx.beginPath()ctx.moveTo(5 + i * 14, 5)ctx.lineTo(5 + i * 14, 140)ctx.stroke()}}}
如果线宽设置为的数字 ,在线条中间对称分布字后,对称分布的坐标不不满足整数坐标,余外的半点坐标,会半渲染。
lineCap示例
function draw() {var canvas = document.getElementById('my-canvas')if (canvas.getContext) {let ctx = canvas.getContext('2d')var lineCap = ['butt', 'round', 'square']// 画辅助线ctx.strokeStyle = '#09f'ctx.beginPath()ctx.moveTo(10, 10)ctx.lineTo(140, 10)ctx.moveTo(10, 140)ctx.lineTo(140, 140)ctx.stroke()// 画线条ctx.strokeStyle = 'black'for (var i = 0; i < lineCap.length; i++) {ctx.beginPath()ctx.lineWidth = 15ctx.lineCap = lineCap[i] // 设置线末尾样式ctx.moveTo(25 + i * 50, 10)ctx.lineTo(25 + i * 50, 140)ctx.stroke()}}}
lineJoin
属性的例子
lineJoin
的属性值决定了图形中两线段连接处所显示的样子。它可以是这三种之一:round
, bevel
和 miter
。默认是 miter
。
这里我同样用三条折线来做例子,分别设置不同的 lineJoin
值。最上面一条是 round
的效果,边角处被磨圆了,圆的半径等于线宽。中间和最下面一条分别是 bevel 和 miter 的效果。当值是 miter
的时候,线段会在连接处外侧延伸直至交于一点,延伸效果受到下面将要介绍的 miterLimit
属性的制约。
function draw() {var ctx = document.getElementById('my-canvas').getContext('2d')var lineJoin = ['round', 'bevel', 'miter']ctx.lineWidth = 10for (var i = 0; i < lineJoin.length; i++) {ctx.lineJoin = lineJoin[i]ctx.beginPath()ctx.moveTo(-5, 5 + i * 40)ctx.lineTo(35, 45 + i * 40)ctx.lineTo(75, 5 + i * 40)ctx.lineTo(115, 45 + i * 40)ctx.lineTo(155, 5 + i * 40)ctx.stroke()}}
使用虚线
用 setLineDash
方法和 lineDashOffset
属性来制定虚线样式。
setLineDash
方法接受一个数组,来指定线段与间隙的交替;lineDashOffset
属性设置起始偏移量。
下面绘制蚂蚁线
let canvas = document.getElementById('my-canvas')var ctx = canvas.getContext('2d')let offset = 0function draw() {ctx.clearRect(0, 0, canvas.width, canvas.height) // 每次需要重新绘制,清除原先的的样式ctx.setLineDash([4, 2]) // 设置线段大小,和线段间隙ctx.lineDashOffset = offset // 设置起始偏移量ctx.strokeRect(10, 10, 100, 100)}setInterval(() => {if (offset > 16) {offset = 0}offset += 1draw()}, 20)
渐变 Gradients
我们可以用线性或者径向的渐变来填充或描边。
createLinearGradient(x1, y1, x2, y2)
createLinearGradient 方法接受 4 个参数,表示渐变的起点 (x1,y1) 与终点 (x2,y2)。createRadialGradient(x1, y1, r1, x2, y2, r2)
createRadialGradient 方法接受 6 个参数,前三个定义一个以 (x1,y1) 为原点,半径为 r1 的圆,后三个参数则定义另一个以 (x2,y2) 为原点,半径为 r2 的圆。
createLinearGradient
的例子
function draw() {// 定义渐变颜色var lineargradient = ctx.createLinearGradient(150, 0, 150, 150)// 渐变起始坐标 -> 渐变终点坐标lineargradient.addColorStop(0, 'white')lineargradient.addColorStop(0.5, 'black')lineargradient.addColorStop(1, 'white')// 填充矩形ctx.fillStyle = lineargradientctx.fillRect(10, 10, 130, 130)}
在这两个渐变坐标之间,填充渐变颜色。
createLinearGradient 的例子
function draw() {// 定义径向渐变颜色var lineargradient = ctx.createRadialGradient(70, 70, 20, 75, 80, 80)lineargradient.addColorStop(0, 'white')lineargradient.addColorStop(0.3, 'black')lineargradient.addColorStop(0.5, 'black')lineargradient.addColorStop(0.5, 'white')// 填充矩形ctx.fillStyle = lineargradientctx.fillRect(10, 10, 130, 130)}
我目前只能看出,径向渐变定义的颜色,他是从内圆 - >外圆来填充渐变颜色,
图案样式 Patterns
createPattern(image, type)
该方法接受两个参数。Image 可以是一个
Image
对象的引用,或者另一个 canvas 对象。Type
必须是下面的字符串值之一:repeat
,repeat-x
,repeat-y
和no-repeat
。
function draw() {var img = new Image() // 用来读取图像img.src = './img/1.png'img.onload = function () {// load确保设置图案之前图像已经装填完毕let ptrn = ctx.createPattern(img, 'no-repeat')ctx.fillStyle = ptrn // 将图案填充到样式中ctx.fillRect(0, 0, img.width, img.height) // 然后将矩形画出来}}
阴影 Shadows
shadowOffsetX = float
shadowOffsetX
和shadowOffsetY
用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为0
。shadowOffsetY = float
shadowOffsetX 和
shadowOffsetY
用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为0
。shadowBlur = float
shadowBlur 用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响,默认为
0
。shadowColor = color
shadowColor 是标准的 CSS 颜色值,用于设定阴影颜色效果,默认是全透明的黑色。
function draw() {// 阴影设置ctx.shadowOffsetX = 10ctx.shadowOffsetY = 10ctx.shadowBlur = 2ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'ctx.fillStyle = 'black'ctx.fillRect(20, 20, 100, 100)}
Canvas 填充规则
当我们用到
fill
(或者 clip和isPointinPath )你可以选择一个填充规则,该填充规则根据某处在路径的外面或者里面来决定该处是否被填充,这对于自己与自己路径相交或者路径被嵌套的时候是有用的。
function draw() {// 填充规则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') // 不填充// ctx.fill() // 默认填充}
3.绘制文本
绘制文本
fillText(text, x, y [, maxWidth])
在指定的 (x,y) 位置填充指定的文本,绘制的最大宽度是可选的。
strokeText(text, x, y [, maxWidth])
在指定的 (x,y) 位置绘制文本边框,绘制的最大宽度是可选的。
一个填充文本示例
function draw() {ctx.font = '48px serif'ctx.fillText('qhx', 40, 40)}
一个边框文本示例
function draw() {ctx.font = '48px serif'ctx.strokeText('qhx', 40, 40)}
有样式的文本
font = value
当前我们用来绘制文本的样式。这个字符串使用和 CSS font 属性相同的语法。默认的字体是
10px sans-serif
。textAlign = value
文本对齐选项。可选的值包括:
start
,end
,left
,right
orcenter
. 默认值是start
。textBaseline = value
基线对齐选项。可选的值包括:
top
,hanging
,middle
,alphabetic
,ideographic
,bottom
。默认值是alphabetic
。direction = value
文本方向。可能的值包括:
ltr
,rtl
,inherit
。默认值是inherit
。
预测量文本宽度
measureText()
将返回一个 TextMetrics对象的宽度、所在像素,这些体现文本特性的属性。
4.绘制图像
canvas 更有意思的一项特性就是图像操作能力。可以用于动态的图像合成或者作为图形的背景,以及游戏界面(Sprites)等等。浏览器支持的任意格式的外部图片都可以使用,比如 PNG、GIF 或者 JPEG。你甚至可以将同一个页面中其他 canvas 元素生成的图片作为图片源。
引入图像到 canvas 里需要以下两步基本操作:
绘图基本操作:
- 获得一个指向HTMLImageElement的对象或者另一个 canvas 元素的引用作为源,也可以通过提供一个 URL 的方式来使用图片(参见例子)
- 使用
drawImage()
函数将图片绘制到画布上
获得需要绘制的图片
HTMLImageElement
这些图片是由
Image()
函数构造出来的,或者任何的 <img> 元素HTMLVideoElement
用一个 HTML 的 <video>元素作为你的图片源,可以从视频中抓取当前帧作为一个图像
HTMLCanvasElement
可以使用另一个 <canvas> 元素作为你的图片源。
ImageBitmap
这是一个高性能的位图,可以低延迟地绘制,它可以从上述的所有源以及其他几种源中生成。
这些源统一由 CanvasImageSource类型来引用。
我们可以通过下列方法的一种来获得与 canvas 相同页面内的图片的引用:
- document.images集合
- document.getElementsByTagName()方法
- 如果你知道你想使用的指定图片的 ID,你可以用document.getElementById()获得这个图片
使用其他域名下的图片
在 HTMLImageElement上使用crossOrigin (en-US)属性,你可以请求加载其他域名上的图片。如果图片的服务器允许跨域访问这个图片,那么你可以使用这个图片而不污染 canvas,否则,使用这个图片将会污染 canvas。
使用其他 canvas 元素
和引用页面内的图片类似地,用
document.getElementsByTagName
或document.getElementById
方法来获取其他 canvas 元素。但你引入的应该是已经准备好的 canvas。一个常用的应用就是将第二个 canvas 作为另一个大的 canvas 的缩略图。
由零开始创建图像
或者我们可以用脚本创建一个新的 HTMLImageElement 对象。要实现这个方法,我们可以使用很方便的
Image()
构造函数。var img = new Image(); // 创建一个<img>元素 img.src = "myImage.png"; // 设置图片源地址
当脚本执行后,图片开始装载。
若调用
drawImage
时,图片没装载完,那什么都不会发生(在一些旧的浏览器中可能会抛出异常)。因此你应该用 load 事件来保证不会在加载完毕之前使用这个图片:var img = new Image(); // 创建 img 元素 img.onload = function () {// 执行 drawImage 语句 }; img.src = "myImage.png"; // 设置图片源地址
如果你只用到一张图片的话,这已经够了。但一旦需要不止一张图片,那就需要更加复杂的处理方法,但图片预加载策略超出本教程的范围。
通过 data: url 方式嵌入图像
我们还可以通过 data:url 方式来引用图像。Data urls 允许用一串 Base64 编码的字符串的方式来定义一个图片。
img.src ="data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==";
其优点就是图片内容即时可用,无须再到服务器兜一圈。(还有一个优点是,可以将 CSS,JavaScript,HTML 和 图片全部封装在一起,迁移起来十分方便。)缺点就是图像没法缓存,图片大的话内嵌的 url 数据会相当的长:
使用视频帧
你还可以使用<video> 中的视频帧(即便视频是不可见的)。例如,如果你有一个 ID 为“myvideo”的<video> 元素,你可以这样做:
function getMyVideo() {var canvas = document.getElementById("canvas");if (canvas.getContext) {var ctx = canvas.getContext("2d");return document.getElementById("myvideo");} }
它将为这个视频返回HTMLVideoElement对象,正如我们前面提到的,它可以作为我们的 Canvas 图片源。
绘制图片
直接看文档,比较纤细:
CanvasRenderingContext2D.drawImage() - Web API 接口参考 | MDN (mozilla.org)
缩放
ctx.drawImage(img, x, y, width, height)
// 图像源、图像X坐标、图像Y坐标、画的图像宽度、画的图像高度
// 这个缩放的意思就是,不是原先的图片宽高,设置canvas显示的图片宽高低于原图片宽高。
切片
// 切片,还是利用这个api,不过传入的参数不同,可以将原图像源的图片截取(切片),然后指定宽高缩放显示
<html><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>canvas</title><style>body {background: 0 -100px repeat-x #4f191a;margin: 10px;}img {display: none;}table {margin: 0 auto;}td {padding: 15px;}</style></head><body onload="draw();"><table><tr><td><img src="./img/gallery_1.jpg" /></td><td><img src="./img/gallery_2.jpg" /></td></tr></table><img id="frame" src="./img/canvas_picture_frame.png" width="132" height="150" /></body><script>function draw() {for (i = 0; i < document.images.length; i++) {// 拿到所有图像列表,不包括iframeif (document.images[i].getAttribute('id') != 'frame') {// 创建canvas宽高 == 相框的宽高canvas = document.createElement('CANVAS')canvas.width = 132canvas.height = 150// 将图片插入到指定父图片之前document.images[i].parentNode.insertBefore(canvas, document.images[i])// 还去2D上下文ctx = canvas.getContext('2d')// 画图像ctx.drawImage(document.images[i], 15, 15) // 画图片,canvas坐标// 这种本身能店家到一起ctx.drawImage(document.getElementById('frame'), 0, 0) // 画}}}</script>
</html>
绘制图像的缩放行为
如同前文所述,过度缩放图像可能会导致图像模糊或像素化。你可以通过使用绘图环境的imageSmoothingEnabled属性来控制是否在缩放图像时使用平滑算法。默认值为
true
,即启用平滑缩放。你也可以像这样禁用此功能:
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
5.变形 Transformations
在本教程前面的部分中,我们已经了解了 Canvas 网格和坐标空间。到目前为止,我们只是根据我们的需要使用默认的网格,改变整个画布的大小。变形是一种更强大的方法,可以将原点移动到另一点、对网格进行旋转和缩放。
状态的保存和恢复
save()
保存画布 (canvas) 的所有状态
restore()
save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。
// PS: canvas对状态的保存,用栈的形式存储。
function draw() {ctx.fillRect(0, 0, 150, 150) // 使用默认设置绘制一个矩形ctx.save() // 1.保存状态1ctx.fillStyle = '#09F' // 在原有配置基础上对颜色做改变ctx.fillRect(15, 15, 120, 120) // 使用新的设置绘制一个矩形ctx.save() // 2.保存状态2ctx.fillStyle = '#FFF' // 再次改变颜色配置ctx.globalAlpha = 0.5ctx.fillRect(30, 30, 90, 90) // 使用新的配置绘制一个矩形ctx.restore() // 3.恢复状态2ctx.fillRect(45, 45, 60, 60) // 使用上一次的配置绘制一个矩形ctx.restore() // 4.恢复状态1ctx.fillRect(60, 60, 30, 30) // 使用加载的配置绘制一个矩形}
移动 Translating
translate(x, y)
用来移动 canvas 和它的原点到一个不同的位置。移动是相对于原点当前位置偏移。
translate
方法接受两个参数。*x *是左右偏移量,y 是上下偏移量
在做变形之前先保存状态是一个良好的习惯。大多数情况下,调用 restore 方法比手动恢复原先的状态要简单得多。又,如果你是在一个循环中做位移但没有保存和恢复 canvas 的状态,很可能到最后会发现怎么有些东西不见了,那是因为它很可能已经超出 canvas 范围以外了。
translate的例子
function draw() {for (var i = 0; i < 3; i++) {for (var j = 0; j < 3; j++) {ctx.save() // 保存默认状态ctx.fillStyle = 'rgb(' + 51 * i + ', ' + (255 - 51 * i) + ', 255)'ctx.translate(10 + j * 50, 10 + i * 50) // 我们这次移动了原点ctx.fillRect(0, 0, 25, 25) // 画矩形是从原点开始的ctx.restore() // 恢复默认状态}}}
旋转 Rotating
rotate(angle)
用于以原点为中心旋转 canvas。
这个方法只接受一个参数:旋转的角度 (angle),它是顺时针方向的,以弧度为单位的值。
旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到
translate
方法。
rotate的例子
function draw() {ctx.fillStyle = '#0095DD'ctx.fillRect(30, 30, 100, 100)// 逆时针旋转原点 -25度后画矩形ctx.rotate((Math.PI / 180) * -25)ctx.fillStyle = '#4D4E53'ctx.fillRect(30, 30, 100, 100)}
缩放 scaling
scale(x, y)
scale
方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子,如果比 1 小,会缩小图形,如果比 1 大会放大图形。默认值为 1,为实际大小。
6.合成和剪辑
globalCompositeOperation
globalCompositeOperation = type
这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识 12 种遮盖方式的字符串。
裁切路径
裁切路径和普通的 canvas 图形差不多,不同的是它的作用是遮罩,用来隐藏不需要的部分。如右图所示。红边五角星就是裁切路径,所有在路径以外的部分都不会在 canvas 上绘制出来。
clip()
将当前正在构建的路径转换为当前的裁剪路径。
我们使用
clip()
方法来创建一个新的裁切路径。默认情况下,canvas 有一个与它自身一样大的裁切路径(也就是没有裁切效果)。
function draw() {ctx.fillRect(0, 0, 150, 150) // 画了一次矩形ctx.translate(75, 75) // 移动原点// 创建一个圆形裁剪路径ctx.beginPath()ctx.arc(0, 0, 60, 0, Math.PI * 2, true)ctx.clip()// 画背景,在裁剪路径内画var lingrad = ctx.createLinearGradient(0, -75, 0, 75)lingrad.addColorStop(0, '#232256')lingrad.addColorStop(1, '#143778')ctx.fillStyle = lingradctx.fillRect(-75, -75, 150, 150)// 画星星,在裁剪路径内画for (var j = 1; j < 50; j++) {ctx.save()ctx.fillStyle = '#fff'ctx.translate(75 - Math.floor(Math.random() * 150),75 - Math.floor(Math.random() * 150))drawStar(ctx, Math.floor(Math.random() * 4) + 2)ctx.restore()}}function drawStar(ctx, r) {ctx.save()ctx.beginPath()ctx.moveTo(r, 0)for (var i = 0; i < 9; i++) {ctx.rotate(Math.PI / 5)if (i % 2 == 0) {ctx.lineTo((r / 0.525731) * 0.200811, 0)} else {ctx.lineTo(r, 0)}}ctx.closePath()ctx.fill()ctx.restore()}
7. 基本的动画
动画的基本步骤
- 清空 canvas 除非接下来要画的内容会完全充满 canvas(例如背景图),否则你需要清空所有。最简单的做法就是用
clearRect
方法。 - 保存 canvas 状态 如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。
- 绘制动画图形(animated shapes) 这一步才是重绘动画帧。
- 恢复 canvas 状态 如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。
简单点讲,清空之前动画帧(也不是一定的,一些动画帧是连续的然后组成一片段,但是类似于那种每次画一次动画,都需要重新设置动画状态,那么需要先保存当前初始状态,每次绘画完之后,清空并且恢复初始状态,并重绘下一帧。
操控动画
动画是一种连续性或周期性的事务。显然我们要保证的绘图方法,要在程序的一直状态中,被调用被运行。显然,我们只能通过定时器或者其他定时性操作来实现这个效果。
首先,可以用window.setInterval(), window.setTimeout(),和window.requestAnimationFrame()来设定定期执行一个指定函数。
setInterval(function, delay) (en-US)
当设定好间隔时间后,function 会定期执行。
setTimeout(function, delay) (en-US)
在设定好的时间之后执行函数
requestAnimationFrame(callback)
告诉浏览器你希望执行一个动画,并在重绘之前,请求浏览器执行一个特定的函数来更新动画。这个方法提供了更加平缓并更加有效率的方式来执行动画,当系统准备好了重绘条件的时候,才调用绘制动画帧。一般每秒钟回调函数执行 60 次,也有可能会被降低。
如果你并不需要与用户互动,你可以使用 setInterval() 方法,它就可以定期执行指定代码。如果我们需要做一个游戏,我们可以使用键盘或者鼠标事件配合上 setTimeout() 方法来实现。通过设置事件监听,我们可以捕捉用户的交互,并执行相应的动作。