canvas学习
MDN官方
教程链接
MDN官方
https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial?utm_source=chatgpt.com
好的实例
需要时才创建一个
canvas,动画结束就删除,如果一直续上动画就一直不消失。🎉效果!
介绍
canvas是一个可以使用脚本 (通常为JavaScript) 来绘制图形的 HTML 元素。例如,它可以用于绘制图表、制作图片构图或者制作简单的动画。右边的图片展示了一些 <canvas> 的实现示例,在这个教程后面我们将看到。
本篇教程从一些基础开始,描述了如何使用<canvas>元素来绘制 2D 图形。教程中提供的例子,会让你明白可以用 canvas 做什么,也会提供一些代码片段来帮助你开始构建自己的内容。
使用 <canvas> 元素不是非常难,但你需要一些基本的HTML和JavaScript知识。Canvas 的默认大小为 300 像素 ×150 像素(宽 × 高,像素的单位是 px)。但是,可以使用 HTML 的高度和宽度属性来自定义 Canvas 的尺寸。为了在 Canvas 上绘制图形,我们使用一个 JavaScript 上下文对象,它能动态创建图像(creates graphics on the fly)。
本教程包含
基本用法
你会了解到如何去设置一个 canvas 2D 上下文以及如何在你的浏览器上创建第一个例子。
<canvas id="tutorial" width="150" height="150"></canvas>canvas看起来和 img元素很相像,唯一的不同就是它并没有 src 和 alt 属性。实际上,<canvas> 标签只有两个属性**——** width和height。这些都是可选的,并且同样利用 DOM properties 来设置。当没有设置宽度和高度的时候,canvas 会初始化宽度为 300 像素和高度为 150 像素。该元素可以使用CSS来定义大小,但在绘制时图像会伸缩以适应它的框架尺寸:如果 CSS 的尺寸与初始画布的比例不一致,它会出现扭曲。
备注: 如果你绘制出来的图像是扭曲的,尝试用 width 和 height 属性为
<canvas>明确规定宽高,而不是使用 CSS。
id属性是为了我们的脚本中很容易的找到它。
兼容性
<canvas>元素与img标签的不同之处在于,就像video,audio,或者 picture元素一样,很容易定义一些替代内容。由于某些较老的浏览器(尤其是 IE9 之前的 IE 浏览器)或者文本浏览器不支持 HTML 元素”canvas”,在这些浏览器上你应该总是能展示替代内容。
这非常简单:我们只是在<canvas>标签中提供了替换内容。不支持<canvas>的浏览器将会忽略容器并在其中渲染后备内容。而支持<canvas>的浏览器将会忽略在容器中包含的内容,并且只是正常渲染 canvas。
举个例子,我们可以提供对 canvas 内容的文字描述或者是提供动态生成内容相对应的静态图片,如下所示:
<canvas id="stockGraph" width="150" height="150">
current stock price: $3.15 +0.15
</canvas>
<canvas id="clock" width="150" height="150">
<img src="images/clock.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 图像而言,如本教程,你可以使用 CanvasRenderingContext2D。
var canvas = document.getElementById("tutorial");
var ctx = canvas.getContext("2d");一旦有了元素对象,你可以通过使用它的 getContext() 方法来访问绘画上下文。
是否支持
var canvas = document.getElementById("tutorial");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
// drawing code here
} else {
// canvas-unsupported code here
}模板示例
接下来我会在下面这个模板上使用canvas,环境vue3+tailwind
<template>
<div class="main p-[30px] text-center">
<canvas ref="canvas" class=" border-red-300 border-2 m-auto h-[500px] w-[500px]"></canvas>
</div>
</template>
<script setup>
import { ref,onMounted } from 'vue'
const canvas = ref(null)
onMounted(()=>{
const ctx = canvas.value.getContext('2d')
})
</script>
一开始,让我们来看个简单的例子,我们绘制了两个有趣的长方形,其中的一个有着 alpha 透明度。我们将在接下来的例子里深入探索一下这是如何工作的。
onMounted(() => {
const ctx = canvas.value.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);
})看起来是这样的

绘制图形
你将学会如何绘制矩形,三角形,直线,圆弧和曲线,变得熟悉这些基本的形状。绘制物体到 Canvas 前,需掌握路径,我们看看到底怎么做。
canvas grid

在我们开始画图之前,我们需要了解一下画布栅格(canvas grid)以及坐标空间。如右图所示,canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。栅格的起点为左上角(坐标为(0,0))。所有元素的位置都相对于原点定位。所以图中蓝色方形左上角的坐标为距离左边(X 轴)x 像素,距离上边(Y 轴)y 像素(坐标为(x,y))。在课程的最后我们会平移原点到不同的坐标上,旋转网格以及缩放。现在我们还是使用原来的设置。
绘制矩形
不同于 SVG,canvas 只支持两种形式的图形绘制:矩形和路径(由一系列点连成的线段)。所有其他类型的图形都是通过一条或者多条路径组合而成的。不过,我们拥有众多路径生成的方法让复杂图形的绘制成为了可能。
首先,我们回到矩形的绘制中。canvas 提供了三种方法绘制矩形:
-
绘制一个填充的矩形
-
strokeRect(x, y, width, height)绘制一个矩形的边框
-
clearRect(x, y, width, height)清除指定矩形区域,让清除部分完全透明。
上面提供的方法之中每一个都包含了相同的参数。x 与 y 指定了在 canvas 画布上所绘制的矩形的左上角(相对于原点)的坐标。width 和 height 设置矩形的尺寸。
下面的 draw() 函数是前一页中取得的,现在就来使用上面的三个函数。
ctx.fillRect(25, 25, 100, 100);
ctx.clearRect(45, 45, 60, 60);
ctx.strokeRect(50, 50, 50, 50);
fillRect()函数绘制了一个边长为 100px 的黑色正方形。clearRect()函数从正方形的中心开始擦除了一个 60*60px 的正方形,接着strokeRect()在清除区域内生成一个 50px * 50px 的正方形边框。
接下来我们能够看到 clearRect() 的两个可选方法,然后我们会知道如何改变渲染图形的填充颜色及描边颜色。
绘制路径
图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。使用路径绘制图形需要一些额外的步骤。
- 首先,你需要创建路径起始点。
- 然后你使用画图命令去画出路径。
- 之后你把路径封闭。
- 一旦路径生成,你就能通过描边或填充路径区域来渲染图形。
以下是所要用到的函数:
-
新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
-
闭合路径之后图形绘制命令又重新指向到上下文中。
-
通过线条来绘制图形轮廓。
-
通过填充路径的内容区域生成实心的图形。
生成路径的第一步叫做 beginPath()。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形、等等)构成图形。而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形。
备注: 当前路径为空,即调用 beginPath() 之后,或者 canvas 刚建的时候,第一条路径构造命令通常被视为是 moveTo(),无论实际上是什么。出于这个原因,你几乎总是要在设置路径之后专门指定你的起始位置。
第二步就是调用函数指定绘制路径,本文稍后我们就能看到了。
第三,就是闭合路径 closePath(),不是必需的。这个方法会通过绘制一条从当前点到开始点的直线来闭合图形。如果图形是已经闭合了的,即当前点为开始点,该函数什么也不做。
备注: 当你调用
fill()函数时,所有没有闭合的形状都会自动闭合,所以你不需要调用closePath()函数。但是调用stroke()时不会自动闭合。
例如,绘制三角形的代码如下:
function draw() {
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(75, 50);
ctx.lineTo(100, 75);
ctx.lineTo(100, 25);
ctx.fill();
}
}长这样:

moveTo
一个非常有用的函数,而这个函数实际上并不能画出任何东西,也是上面所描述的路径列表的一部分,这个函数就是moveTo()。或者你可以想象一下在纸上作业,一支钢笔或者铅笔的笔尖从一个点到另一个点的移动过程。
-
将笔触移动到指定的坐标 x 以及 y 上。
当 canvas 初始化或者beginPath()调用后,你通常会使用moveTo()函数设置起点。我们也能够使用moveTo()绘制一些不连续的路径。看一下下面的笑脸例子。我将用到moveTo()方法(红线处)的地方标记了。
你可以尝试一下,使用下边的代码片。只需要将其复制到之前的draw()函数即可。
function draw() {
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(75, 75, 50, 0, Math.PI * 2, true); // 绘制
ctx.moveTo(110, 75);
ctx.arc(75, 75, 35, 0, Math.PI, false); // 口 (顺时针)
ctx.moveTo(65, 65);
ctx.arc(60, 65, 5, 0, Math.PI * 2, true); // 左眼
ctx.moveTo(95, 65);
ctx.arc(90, 65, 5, 0, Math.PI * 2, true); // 右眼
ctx.stroke();
}
}结果看起来是这样的:

如果你想看到连续的线,你可以移除调用的 moveTo()。
备注: 需要学习更多关于 arc() 函数的内容,请看下面的圆弧
线
绘制直线,需要用到的方法lineTo()。
绘制一条从当前位置到指定 x 以及 y 位置的直线。
该方法有两个参数:x 以及 y,代表坐标系中直线结束的点。开始点和之前的绘制路径有关,之前路径的结束点就是接下来的开始点,以此类推。开始点也可以通过moveTo()函数改变。
// 填充三角形
ctx.beginPath();
ctx.moveTo(25, 25);
ctx.lineTo(105, 25);
ctx.lineTo(25, 105);
ctx.fill();
// 描边三角形
ctx.beginPath();
ctx.moveTo(125, 125);
ctx.lineTo(125, 45);
ctx.lineTo(45, 125);
ctx.closePath();
ctx.stroke();
圆弧
绘制圆弧或者圆,我们使用arc()方法。当然可以使用arcTo(),不过这个的实现并不是那么的可靠,所以我们这里不作介绍。
画一个以(x,y)为圆心的以 radius 为半径的圆弧(圆),从 startAngle 开始到 endAngle 结束,按照 anticlockwise 给定的方向(默认为顺时针)来生成。
根据给定的控制点和半径画一段圆弧,再以直线连接两个控制点。
这里详细介绍一下 arc 方法,该方法有六个参数:x,y为绘制圆弧所在圆上的圆心坐标。radius为半径。startAngle以及endAngle参数用弧度定义了开始以及结束的弧度。这些都是以 x 轴为基准。参数anticlockwise为一个布尔值。为 true 时,是逆时针方向,否则顺时针方向。
备注: arc() 函数中表示角的单位是弧度,不是角度。角度与弧度的 js 表达式:
弧度=(Math.PI/180)*角度。

选定的两个角度就像两个点,就以0、0.5PI为例子
顺时针(默认):

逆时针:

贝塞尔曲线
下一个十分有用的路径类型就是贝塞尔曲线。二次及三次贝塞尔曲线都十分有用,一般用来绘制复杂有规律的图形。
-
quadraticCurveTo(cp1x, cp1y, x, y)绘制二次贝塞尔曲线,
cp1x,cp1y为一个控制点,x,y为结束点。(开始点就是笔触点moveTo()) -
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)绘制三次贝塞尔曲线,
cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。
右边的图能够很好的描述两者的关系,二次贝塞尔曲线有一个开始点(蓝色)、一个结束点(蓝色)以及一个控制点(红色),而三次贝塞尔曲线有两个控制点。

参数 x、y 在这两个方法中都是结束点坐标。cp1x,cp1y为坐标中的第一个控制点,cp2x,cp2y为坐标中的第二个控制点。
使用二次以及三次贝塞尔曲线是有一定的难度的,因为不同于像 Adobe Illustrators 这样的矢量软件,我们所绘制的曲线没有给我们提供直接的视觉反馈。这让绘制复杂的图形变得十分困难。在下面的例子中,我们会绘制一些简单有规律的图形,如果你有时间以及更多的耐心,很多复杂的图形你也可以绘制出来。
下面的这些例子没有多少困难。这两个例子中我们会连续绘制贝塞尔曲线,最后形成复杂的图形。
二次贝塞尔曲线
绘制一个圆角矩形
onMounted(() => {
const ctx = canvas.value.getContext('2d')
ctx.beginPath();
ctx.moveTo(100,110);
ctx.quadraticCurveTo(100,100,110,100)
ctx.lineTo(150,100)
ctx.quadraticCurveTo(160,100,160,110)
ctx.lineTo(160,120)
ctx.quadraticCurveTo(160,130,150,130)
ctx.lineTo(110,130)
ctx.quadraticCurveTo(100,130,100,120)
ctx.lineTo(100,110)
ctx.stroke()
})
三次贝塞尔曲线
这个例子使用贝塞尔曲线绘制心形。
function draw() {
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
//三次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(75, 40);
ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
ctx.fill();
}
}
矩形
直接在画布上绘制矩形的三个额外方法,正如我们开始所见的绘制矩形,同样,也有 rect() 方法,将一个矩形路径增加到当前路径上。
绘制一个左上角坐标为(x,y),宽高为 width 以及 height 的矩形。前面是fillRect、strokeRect、cleanRect
当该方法执行的时候,moveTo() 方法自动设置坐标参数(0,0)。也就是说,当前笔触自动重置回默认坐标。
ctx.beginPath();
ctx.moveTo(100,110);
ctx.rect(100,100,50,50);
ctx.fill()
Path2D 对象
正如我们在前面例子中看到的,你可以使用一系列的路径和绘画命令来把对象“画”在画布上。为了简化代码和提高性能,Path2D对象已可以在较新版本的浏览器中使用,用来缓存或记录绘画命令,这样你将能快速地回顾路径。
怎样产生一个 Path2D 对象呢?
-
Path2D()会返回一个新初始化的 Path2D 对象(可能将某一个路径作为变量——创建一个它的副本,或者将一个包含 SVG path 数据的字符串作为变量)。
new Path2D(); // 空的 Path 对象
new Path2D(path); // 克隆 Path 对象
new Path2D(d); // 从 SVG 建立 Path 对象所有的路径方法比如moveTo, rect, arc或quadraticCurveTo等,如我们前面见过的,都可以在 Path2D 中使用。
Path2D API 添加了 addPath作为将path结合起来的方法。当你想要从几个元素中来创建对象时,这将会很实用。比如:
Path2D.addPath(path [, transform\])
添加了一条路径到当前路径(可能添加了一个变换矩阵)。
Path2D 示例
在这个例子中,我们创造了一个矩形和一个圆。它们都被存为 Path2D 对象,后面再派上用场。随着新的 Path2D API 产生,几种方法也相应地被更新来使用 Path2D 对象而不是当前路径。在这里,带路径参数的stroke和fill可以把对象画在画布上。
function draw() {
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
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);
}
}使用 SVG paths
新的 Path2D API 有另一个强大的特点,就是使用 SVG path data 来初始化 canvas 上的路径。这将使你获取路径时可以以 SVG 或 canvas 的方式来重用它们。
这条路径将先移动到点 (M10 10) 然后再水平移动 80 个单位(h 80),然后下移 80 个单位 (v 80),接着左移 80 个单位 (h -80),再回到起点处 (z)。你可以在Path2D constructor 查看这个例子
var p = new Path2D("M10 10 h 80 v 80 h -80 Z");结果是一个矩形:

可以利用这个绘制vue的LOGO

onMounted(() => {
const ctx = canvas.value.getContext('2d')
var outer = new Path2D("M78.8,10L64,35.4L49.2,10H0l64,110l64-110C128,10,78.8,10,78.8,10z");
var inner = new Path2D("M78.8,10L64,35.4L49.2,10H25.6L64,76l38.4-66H78.8z");
ctx.fillStyle = 'rgb(66,184,131)'
ctx.fill(outer)
ctx.fillStyle = 'rgb(53,73,94)'
ctx.fill(inner)
})应用样式和色彩
在绘制图形的章节里,我只用到默认的线条和填充样式。而在这一章里,我们将会探讨 canvas 全部的可选项,来绘制出更加吸引人的内容。
色彩
到目前为止,我们只看到过绘制内容的方法。如果我们想要给图形上色,有两个重要的属性可以做到:fillStyle 和 strokeStyle。
-
设置图形的填充颜色。
-
设置图形轮廓的颜色。
color 可以是表示 CSS 颜色值的字符串,渐变对象或者图案对象。我们迟些再回头探讨渐变和图案对象。默认情况下,线条和填充颜色都是黑色(CSS 颜色值 #000000)。
备注: 一旦你设置了 strokeStyle 或者 fillStyle 的值,那么这个新值就会成为新绘制的图形的默认值。如果你要给每个图形上不同的颜色,你需要重新设置 fillStyle 或 strokeStyle 的值。
// 这些 fillStyle 的值均为“橙色”
//rgb使用的函数间隔是,不是空格!
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)";
透明度
除了可以绘制实色图形,我们还可以用 canvas 来绘制半透明的图形。通过设置 globalAlpha 属性或者使用一个半透明颜色作为轮廓或填充的样式。
-
globalAlpha = transparencyValue这个属性影响到 canvas 里所有图形的透明度,有效的值范围是 0.0(完全透明)到 1.0(完全不透明),默认是 1.0。
globalAlpha 属性在需要绘制大量拥有相同透明度的图形时候相当高效。不过,我认为下面的方法可操作性更强一点。
因为 strokeStyle 和 fillStyle 属性接受符合 CSS 3 规范的颜色值,那我们可以用下面的写法来设置具有透明度的颜色。
// 指定透明颜色,用于描边和填充样式
//这里写的间隔就是空格!
ctx.strokeStyle = "rgb(255 0 0 / 50%)";
ctx.fillStyle = "rgb(255 0 0 / 50%)";rgba() 方法与 rgb() 方法类似,就多了一个用于设置色彩透明度的参数。它的有效范围是从 0.0(完全透明)到 1.0(完全不透明)。
//这样也是生效的!
ctx.fillStyle = 'rgba(66,184,131,0.5)'设置一半透明度

//通过globalAlpha设置
ctx.globalAlpha = 0.5
ctx.fillStyle = 'rgb(66,184,131)'
ctx.fill(outer)
ctx.fillStyle = 'rgb(53,73,94)'
ctx.fill(inner)
//通过rgba设置
ctx.fillStyle = 'rgba(66,184,131,0.5)'
ctx.fill(outer)
ctx.fillStyle = 'rgba(53,73,94,0.5)'
ctx.fill(inner)线的样式
可以通过一系列属性来设置线的样式。
设置线条宽度。
设置线条末端样式。
设定线条与线条间接合处的样式。
限制当两条线相交时交接处最大长度;所谓交接处长度(斜接长度)是指线条交接处内角顶点到外角顶点的长度。
返回一个包含当前虚线样式,长度为非负偶数的数组。
设置当前虚线样式。
设置虚线样式的起始偏移量。
lineWidth 属性的示例
这个属性设置当前绘线的粗细。属性值必须为正数。默认值是 1.0。
线宽是指给定路径的中心到两边的粗细。换句话说就是在路径的两边各绘制线宽的一半。因为画布的坐标并不和像素直接对应,当需要获得精确的水平或垂直线的时候要特别注意。
在下面的例子中,用递增的宽度绘制了 10 条直线。最左边的线宽 1.0 单位。并且,最左边的以及所有宽度为奇数的线并不能精确呈现,这就是因为路径的定位问题。

lineCap 属性的示例
属性 lineCap 的值决定了线段端点显示的样子。它可以为下面的三种的其中之一:butt、round 和 square。默认是 butt。
在这个例子里面,我绘制了三条直线,分别赋予不同的 lineCap 值。还有两条辅助线,为了可以看得更清楚它们之间的区别,三条线的起点终点都落在辅助线上。
最左边的线用了默认的 butt 。可以注意到它是与辅助线齐平的。中间的是 round 的效果,端点处加上了半径为一半线宽的半圆。右边的是 square 的效果,端点处加上了等宽且高度为一半线宽的方块。

lineJoin 属性的示例
lineJoin 的属性值决定了图形中两线段连接处所显示的样子。它可以是这三种之一:round、bevel 和 miter。默认是 miter。
这里我同样用三条折线来做例子,分别设置不同的 lineJoin 值。
最上面一条是 round 的效果,边角处被磨圆了,圆的半径等于线宽。
中间和最下面一条分别是 bevel 和 miter 的效果。当值是 miter 的时候,线段会在连接处外侧延伸直至交于一点,延伸效果受到下面将要介绍的 miterLimit 属性的制约。

miterLimit 属性的演示示例
就如上一个例子所见的应用 miter 的效果,线段的外侧边缘会被延伸交汇于一点上。线段之间夹角比较大时,交点不会太远,但随着夹角变小,交点距离会呈指数级增大。
miterLimit 属性就是用来设定外延交点与连接点的最大距离,如果交点距离大于此值,连接效果会变成了 bevel。注意,最大斜接长度(即交点距离)是当前坐标系测量线宽与此miterLimit属性值(默认为 10.0)的乘积,所以miterLimit可以单独设置,不受显示比例改变或任何仿射变换的影响:它只影响线条边缘的有效绘制形状。
关键就是:
斜接限定值默认为 10.0,这将会去除所有小于大约 11 度的斜接。
使用虚线
用 setLineDash 方法和 lineDashOffset 属性来制定虚线样式。setLineDash 方法接受一个数组,来指定线段与间隙的交替;lineDashOffset 属性设置起始偏移量。
在这个例子中,我们要创建一个蚂蚁线的效果。它往往应用在计算机图形程序选区工具动效中。它可以帮助用户通过动画的边界来区分图像背景选区边框。
onMounted(() => {
const ctx = canvas.value.getContext('2d')
let offset = 0;
function draw() {
ctx.clearRect(0, 0, 500, 500);
//4是实线 2是空格
ctx.setLineDash([4, 2]);
ctx.lineDashOffset = -offset;
ctx.strokeRect(10, 10, 100, 100);
}
function march() {
offset++;
if (offset > 5) {
offset = 0;
}
draw();
setTimeout(march, 20);
}
march()
})渐变
就好像一般的绘图软件一样,我们可以用线性或者径向的渐变来填充或描边。我们用下面的方法新建一个 canvasGradient 对象,并且赋给图形的 fillStyle 或 strokeStyle 属性。
-
createLinearGradient(x1, y1, x2, y2)createLinearGradient 方法接受 4 个参数,表示渐变的起点 (x1,y1) 与终点 (x2,y2)。
createRadialGradient 方法接受 6 个参数,前三个定义一个以 (x1,y1) 为原点,半径为 r1 的圆,后三个参数则定义另一个以 (x2,y2) 为原点,半径为 r2 的圆。
这两个圆共同决定了渐变的范围和形状:
- 渐变是从第一个圆向第二个圆的区域过渡的。
- 通常
r1比较小,r2比较大,这样就能做出“由内向外扩散”的光晕效果。- 如果两个圆心位置不同,渐变会偏向某个方向,不再是完全同心的光圈。
const lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
const radialgradient = ctx.createRadialGradient(75, 75, 0, 75, 75, 100);创建出 canvasGradient 对象后,我们就可以用 addColorStop 方法给它上色了。
-
gradient.addColorStop(position, color)addColorStop 方法接受 2 个参数,
position参数必须是一个 0.0 与 1.0 之间的数值,表示渐变中颜色所在的相对位置。例如,0.5 表示颜色会出现在正中间。color参数必须是一个有效的 CSS 颜色值(如 #FFF、rgba(0,0,0,1),等等)。
你可以根据需要添加任意多个色标(color stops)。下面是最简单的线性黑白渐变的例子。
onMounted(() => {
const ctx = canvas.value.getContext('2d')
ctx.beginPath()
var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
// 创建渐变
lineargradient.addColorStop(0, "white");
lineargradient.addColorStop(1, "black");
// 设置渐变
ctx.fillStyle = lineargradient;
ctx.strokeStyle = lineargradient;
// 画图形
ctx.fillRect(10, 10, 130, 130);
ctx.strokeRect(50, 50, 50, 50);
})
径向的渐变用例:
onMounted(() => {
const ctx = canvas.value.getContext('2d')
// 创建渐变
var radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30);
radgrad.addColorStop(0, "#A7D30C");
radgrad.addColorStop(0.9, "#019F62");
radgrad.addColorStop(1, "rgba(1,159,98,0)");//等价于 transparent 可替换
// 画图形
ctx.fillStyle = radgrad;
ctx.fillRect(0, 0, 150, 150);
ctx.strokeRect(0, 0, 150, 150);
})
(经典的径向渐变是只有一个中心点,简单地由中心点向外围的圆形扩张)
径向渐变效果的最后一个色标都是透明色。如果想要两色标直接的过渡柔和一些,只要两个颜色值一致就可以了。代码里面看不出来,是因为我用了两种不同的颜色表示方法,但其实是相同的,
#019F62 = rgba(1,159,98,1)。
扩散(如果不设置最后为透明):
var radgrad = ctx.createRadialGradient(45, 45, 10, 45, 45, 30);
radgrad.addColorStop(0, "#A7D30C");
radgrad.addColorStop(0.9, "#019F62");
radgrad.addColorStop(1, "red");
// 画图形
ctx.fillStyle = radgrad;
ctx.fillRect(0, 0, 150, 150);
ctx.strokeRect(0, 0, 150, 150);
图案样式
上一节的一个例子里面,我用了循环来实现图案的效果。其实,有一个更加简单的方法:createPattern。
-
该方法接受两个参数。Image 可以是一个
Image对象的引用,或者另一个canvas对象。Type必须是下面的字符串值之一:repeat,repeat-x,repeat-y和no-repeat。
备注: 用 canvas 对象作为 Image 参数在 Firefox 1.5 (Gecko 1.8) 中是无效的。
图案的应用跟渐变很类似的,创建出一个图案之后,赋给 fillStyle 或 strokeStyle 属性即可。
const img = new Image();
img.src = "some-image.png";
const pattern = ctx.createPattern(img, "repeat");备注: 与
drawImage有点不同,你需要确认image对象已经装载完毕,否则图案可能效果不对的。
示例
我创建一个图案然后赋给了 fillStyle 属性。唯一要注意的是,使用 Image 对象的 onload 处理器来确保设置图案之前图像已经装载完毕。
// 创建新 image 对象,用作图案
const img = new Image();
img.src = "canvas_create_pattern.png";
img.onload = () => {
// 创建图案
const pattern = ctx.createPattern(img, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 150, 150);
};
阴影
-
shadowOffsetX和shadowOffsetY用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为0。 -
shadowOffsetX 和
shadowOffsetY用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为0。 -
shadowBlur 用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响,默认为
0。 -
shadowColor 是标准的 CSS 颜色值,用于设定阴影颜色效果,默认是全透明的黑色。
示例
这个例子绘制了带阴影效果的文字。
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 2;
ctx.shadowColor = "rgb(0 0 0 / 50%)";
ctx.font = "20px Times New Roman";
ctx.fillStyle = "Black";
ctx.fillText("Sample String", 5, 30);
}我们可以通过下一章来了解文字属性和 fillText 方法相关的内容。

Canvas 填充规则
当我们用到 fill(或者 clip和isPointinPath )你可以选择一个填充规则,该填充规则根据某处在路径的外面或者里面来决定该处是否被填充,这对于自己与自己路径相交或者路径被嵌套的时候是有用的。
两个可能的值:
非零环绕规则, 默认值。
这个例子,我们用 evenodd 规则。
ctx.beginPath();
ctx.arc(150, 150, 60, 0, Math.PI * 2, true);
ctx.arc(150, 150, 45, 0, Math.PI * 2, true);
ctx.arc(150, 150, 30, 0, Math.PI * 2, true);
ctx.arc(150, 150, 15, 0, Math.PI * 2, true);
ctx.fill("evenodd");
使用默认值得到:

绘制文本
我们现在来看一下如何在画布(canvas)中绘制文本。
绘制文本
画布渲染上下文提供了两种渲染文本的方法:
-
fillText(text, x, y [, maxWidth\])在指定的 (x,y) 位置填充指定的文本。绘制的最大宽度是可选的。
-
strokeText(text, x, y [, maxWidth\])在指定的 (x,y) 位置绘制文本边框。绘制的最大宽度是可选的。
fillText 示例
文本用当前的填充样式(fillStyle)被填充:
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
ctx.font = "48px serif";
ctx.fillText("你好世界", 10, 50);
}
设置最大宽度就会:

strokeText 示例
文本用当前的边框样式(strokeStyle)被填充:
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
ctx.font = "48px serif";
ctx.strokeText("你好世界", 10, 50);
}
为文本添加样式
在上面的示例用我们已经使用了 font 属性来使文本比默认尺寸大一些。还有更多的属性可以让你改变画布显示文本的方式:
-
当前我们用来绘制文本的文本样式。这个字符串使用和 CSS
font属性相同的语法。默认的字体是10px sans-serif。 -
文本对齐选项。可选的值包括:
start、end、left、right或center。默认值是start。 -
基线对齐选项。可选的值包括:
top、hanging、middle、alphabetic、ideographic、bottom。默认值是alphabetic。 -
文本方向。可能的值包括:
ltr、rtl、inherit。默认值是inherit。
如果你之前使用过 CSS,那么你会很熟悉这些属性。
在
canvas的ctx.font里,字体名字如果包含空格,需要用 引号括起来。否则它会被当成多个字体。比如:
ctx.font = "50px 'Smiley Sans Oblique'";或者用双引号包起来:
ctx.font = '50px "Smiley Sans Oblique"';
下面来自 HTML 规范的图展示了 textBaseline 属性支持的不同的基线情况。

textBaseline 示例
此示例演示了不同的 textBaseline 属性值。请参见 CanvasRenderingContext2D.textBaseline 页面以了解更多信息和详细示例。
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
ctx.font = "48px serif";
ctx.textBaseline = "hanging";
ctx.strokeText("hanging", 10, 50);
ctx.textBaseline = "middle";
ctx.strokeText("middle", 250, 50);
ctx.beginPath();
ctx.moveTo(10, 50);
ctx.lineTo(300, 50);
ctx.stroke();
}
获取文本宽度
当你需要获得更多的文本细节时,下面的方法可以给你测量文本的方法。
-
返回一个包含以当前文本样式绘制指定文本时,其所具有的宽度(以像素为单位)的
TextMetrics对象。
下面的代码片段将展示如何测量文本来获得它的宽度:
onMounted(() => {
const ctx = canvas.value.getContext('2d')
ctx.font = "12px serif";
const text = ctx.measureText("foo"); // TextMetrics 对象
console.log(text.width); // 18.762222290039062;
})使用图像
canvas 更有意思的一项特性就是图像操作能力。可以用于动态的图像合成或者作为图形的背景,以及游戏界面(Sprites)等等。浏览器支持的任意格式的外部图片都可以使用,比如 PNG、GIF 或者 JPEG。你甚至可以将同一个页面中其他 canvas 元素生成的图片作为图片源。
引入图像到 canvas 里需要以下两步基本操作:
- 获得一个指向
HTMLImageElement的对象或者另一个 canvas 元素的引用作为源,也可以通过提供一个 URL 的方式来使用图片(参见例子) - 使用
drawImage()函数将图片绘制到画布上
我们来看看具体是怎么做的。
获得需要绘制的图片
canvas 的 API 可以使用下面这些类型中的一种作为图片的源:
这些图片是由 Image() 函数构造出来的,或者任何的 img 元素
用一个 HTML 的 video元素作为你的图片源,可以从视频中抓取当前帧作为一个图像
可以使用另一个canvas元素作为你的图片源。
这是一个高性能的位图,可以低延迟地绘制,它可以从上述的所有源以及其他几种源中生成。
这些源统一由 CanvasImageSource类型来引用。
有几种方式可以获取到我们需要在 canvas 上使用的图片。
使用相同页面内的图片
我们可以通过下列方法的一种来获得与 canvas 相同页面内的图片的引用:
document.images集合document.getElementsByTagName()方法- 如果你知道你想使用的指定图片的 ID,你可以用
document.getElementById()获得这个图片
使用其他域名下的图片
在 HTMLImageElement上使用crossOrigin属性,你可以请求加载其他域名上的图片。如果图片的服务器允许跨域访问这个图片,那么你可以使用这个图片而不污染 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 图片源。
绘制图片
一旦获得了源图对象,我们就可以使用 drawImage 方法将它渲染到 canvas 里。drawImage 方法有三种形态,下面是最基础的一种。
其中 image 是 image 或者 canvas 对象,x 和 y 是其在目标 canvas 里的起始坐标。
备注: SVG 图像必须在 <svg> 根指定元素的宽度和高度。
示例:一个简单的线图
下面一个例子我用一个外部图像作为一线性图的背景。用背景图我们就不需要绘制复杂的背景,省下不少代码。这里只用到一个 image 对象,于是就在它的 onload 事件响应函数中触发绘制动作。drawImage 方法将背景图放置在 canvas 的左上角 (0,0) 处。
function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
var img = new Image();
img.onload = function () {
ctx.drawImage(img, 0, 0);
ctx.beginPath();
ctx.moveTo(30, 96);
ctx.lineTo(70, 66);
ctx.lineTo(103, 76);
ctx.lineTo(170, 15);
ctx.stroke();
};
img.src = "backdrop.png";
}
缩放
drawImage 方法的又一变种是增加了两个用于控制图像在 canvas 中缩放的参数。
-
drawImage(image, x, y, width, height)这个方法多了 2 个参数:
width和height,这两个参数用来控制 当向 canvas 画入时应该缩放的大小
示例:平铺图像
在这个例子里,我会用一张图片像背景一样在 canvas 中以重复平铺开来。实现起来也很简单,只需要循环铺开经过缩放的图片即可。见下面的代码,第一层 for 循环是做行重复,第二层是做列重复的。图像大小被缩放至原来的三分之一,50x38 px。这种方法可以用来很好的达到背景图案的效果,在下面的教程中会看到。
备注: 图像可能会因为大幅度的缩放而变得起杂点或者模糊。如果你的图像里面有文字,那么最好还是不要进行缩放,因为那样处理之后很可能图像里的文字就会变得无法辨认了。
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
const img = new Image();
img.onload = () => {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 3; j++) {
ctx.drawImage(img, j * 50, i * 38, 50, 38);
}
}
};
img.src = "https://mdn.github.io/shared-assets/images/examples/rhino.jpg";
}
draw();
切片
drawImage 方法的第三个也是最后一个变种有 8 个新参数,用于控制做切片显示的。
-
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)第一个参数和其他的是相同的,都是一个图像或者另一个 canvas 的引用。其他 8 个参数最好是参照右边的图解,前 4 个是定义图像源的切片位置和大小,后 4 个则是定义切片的目标显示位置和大小。

示例:相框
在这个例子里面我用到上面已经用过的犀牛图像,不过这次我要给犀牛头做个切片特写,然后合成到一个相框里面去。相框带有阴影效果,是一个以 24-bit PNG 格式保存的图像。因为 24-bit PNG 图像带有一个完整的 8-bit alpha 通道,与 GIF 和 8-bit PNG 不同,我可以将它放成背景而不必担心底色的问题。
我用一个与上面用到的不同的方法来装载图像,直接将图像插入到 HTML 里面,然后通过 CSS 隐藏(display:none)它。两个图像我都赋了 id ,方便后面使用。看下面的脚本,相当简单,首先对犀牛头做好切片(第一次drawImage)放在 canvas 上,然后再上面套个相框(第二次drawImage)。
<canvas id="canvas" width="150" height="150"></canvas>
<div style="display: none;">
<img
id="source"
src="https://mdn.github.io/shared-assets/images/examples/rhino.jpg"
width="300"
height="227" />
<img id="frame" src="canvas_picture_frame.png" width="132" height="150" />
</div>async function draw() {
// 等待所有图片的加载。
await Promise.all(
Array.from(document.images).map(
(image) =>
new Promise((resolve) => image.addEventListener("load", resolve)),
),
);
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// 绘制切片
ctx.drawImage(
document.getElementById("source"),
33,
71,
104,
124,
21,
20,
87,
104,
);
// 绘制相框
ctx.drawImage(document.getElementById("frame"), 0, 0);
}
draw();
控制图像的缩放行为
如同前文所述,过度缩放图像可能会导致图像模糊或像素化。你可以通过使用绘图环境的imageSmoothingEnabled属性来控制是否在缩放图像时使用平滑算法。默认值为true,即启用平滑缩放。你也可以像这样禁用此功能:
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;变形 Transformations
到目前为止,我们只是根据我们的需要使用默认的网格,改变整个画布的大小。变形是一种更强大的方法,可以将原点移动到另一点、对网格进行旋转和缩放。
状态的保存和恢复
在了解变形之前,我先介绍两个在你开始绘制复杂图形时必不可少的方法。
-
保存画布 (canvas) 的所有状态
-
save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。
Canvas 状态存储在栈中,每当save()方法被调用后,当前的状态就被推送到栈中保存。一个绘画状态包括:
- 当前应用的变形(即移动,旋转和缩放,见下)
- 以及下面这些属性:
strokeStyle,fillStyle,globalAlpha,lineWidth,lineCap,lineJoin,miterLimit,lineDashOffset,shadowOffsetX,shadowOffsetY,shadowBlur,shadowColor,globalCompositeOperation,font,textAlign,textBaseline,direction,imageSmoothingEnabled - 当前的裁切路径(clipping path),会在下一节介绍
你可以调用任意多次 save方法。每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。
save 和 restore 的应用示例
我们尝试用这个连续矩形的例子来描述 canvas 的状态栈是如何工作的。
第一步是用默认设置画一个大四方形,然后保存一下状态。改变填充颜色画第二个小一点的蓝色四方形,然后再保存一下状态。再次改变填充颜色绘制更小一点的半透明的白色四方形。
到目前为止所做的动作和前面章节的都很类似。不过一旦我们调用 restore,状态栈中最后的状态会弹出,并恢复所有设置。如果不是之前用 save 保存了状态,那么我们就需要手动改变设置来回到前一个状态,这个对于两三个属性的时候还是适用的,一旦多了,我们的代码将会猛涨。
当第二次调用 restore 时,已经恢复到最初的状态,因此最后是再一次绘制出一个黑色的四方形。

function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
ctx.fillRect(0, 0, 150, 150); // 使用默认设置绘制一个矩形
ctx.save(); // 保存默认状态
ctx.fillStyle = "#09F"; // 在原有配置基础上对颜色做改变
ctx.fillRect(15, 15, 120, 120); // 使用新的设置绘制一个矩形
ctx.save(); // 保存当前状态
ctx.fillStyle = "#FFF"; // 再次改变颜色配置
ctx.globalAlpha = 0.5;
ctx.fillRect(30, 30, 90, 90); // 使用新的配置绘制一个矩形
ctx.restore(); // 重新加载之前的颜色状态
ctx.fillRect(45, 45, 60, 60); // 使用上一次的配置绘制一个矩形
ctx.restore(); // 加载默认颜色配置
ctx.fillRect(60, 60, 30, 30); // 使用加载的配置绘制一个矩形
}移动
我们先介绍 translate 方法,它用来移动 canvas 和它的原点到一个不同的位置。
-
translate方法接受两个参数。*x *是左右偏移量,y 是上下偏移量,如右图所示。

在做变形之前先保存状态是一个良好的习惯。大多数情况下,调用 restore 方法比手动恢复原先的状态要简单得多。又,如果你是在一个循环中做位移但没有保存和恢复 canvas 的状态,很可能到最后会发现怎么有些东西不见了,那是因为它很可能已经超出 canvas 范围以外了。
translate 的示例
这个例子显示了一些移动 canvas 原点的好处。如果不使用 translate 方法,那么所有矩形都将被绘制在相同的位置(0,0)。translate 方法同时让我们可以任意放置这些图案,而不需要在 fillRect() 方法中手工调整坐标值,既好理解也方便使用。
我在 draw 方法中调用 fillRect() 方法 9 次,用了 2 层循环。每一次循环,先移动 canvas,画螺旋图案,然后恢复到原始状态。
function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
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方法,它用于以原点为中心旋转 canvas。
-
这个方法只接受一个参数:旋转的角度 (angle),它是顺时针方向的,以弧度为单位的值。
旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到 translate方法。
rotate 的示例
在这个例子里,我们将会使用 rotate() 方法来画圆并构成圆形图案。当然你也可以分别计算出 x 和 y 坐标(x = r*Math.cos(a); y = r*Math.sin(a))。这里无论用什么方法都无所谓的,因为我们画的是圆。计算坐标的结果只是旋转圆心位置,而不是圆本身。即使用 rotate旋转两者,那些圆看上去还是一样的,不管它们绕中心旋转有多远。
这里我们又用到了两层循环。第一层循环决定环的数量,第二层循环决定每环有多少个点。每环开始之前,我都保存一下 canvas 的状态,这样恢复起来方便。每次画圆点,我都以一定夹角来旋转 canvas,而这个夹角则是由环上的圆点数目的决定的。最里层的环有 6 个圆点,这样,每次旋转的夹角就是 360/6 = 60 度。往外每一环的圆点数目是里面一环的 2 倍,那么每次旋转的夹角随之减半。
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// left rectangles, rotate from canvas origin
ctx.save();
// blue rect
ctx.fillStyle = "#0095DD";
ctx.fillRect(30, 30, 100, 100);
ctx.rotate((Math.PI / 180) * 25);
// grey rect
ctx.fillStyle = "#4D4E53";
ctx.fillRect(30, 30, 100, 100);
ctx.restore();
// right rectangles, rotate from rectangle center
// draw blue rect
ctx.fillStyle = "#0095DD";
ctx.fillRect(150, 30, 100, 100);
ctx.translate(200, 80); // translate to rectangle center
// x = x + 0.5 * width
// y = y + 0.5 * height
ctx.rotate((Math.PI / 180) * 25); // rotate
ctx.translate(-200, -80); // translate back
// draw grey rect
ctx.fillStyle = "#4D4E53";
ctx.fillRect(150, 30, 100, 100);
}
缩放
接着是缩放。我们用它来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大。
-
scale方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子,如果比 1 小,会缩小图形,如果比 1 大会放大图形。默认值为 1,为实际大小。
画布初始情况下,是以左上角坐标为原点的第一象限。如果参数为负实数,相当于以 x 或 y 轴作为对称轴镜像反转(例如,使用translate(0,canvas.height); scale(1,-1); 以 y 轴作为对称轴镜像反转,就可得到著名的笛卡尔坐标系,左下角为原点)。
默认情况下,canvas 的 1 个单位为 1 个像素。举例说,如果我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍。
scale 的示例
这最后的例子里,我们用不同的缩放方式来画两个图形。
function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
// draw a simple rectangle, but scale it.
ctx.save();
ctx.scale(10, 3);
ctx.fillRect(1, 10, 10, 10);
ctx.restore();
// mirror horizontally
ctx.scale(-1, 1);
ctx.font = "48px serif";
ctx.fillText("MDN", -135, 120);
}
变形
最后一个方法允许对变形矩阵直接修改。
-
这个方法是将当前的变形矩阵乘上一个基于自身参数的矩阵,如下面的矩阵所示:

如果任意一个参数是
Infinity,变形矩阵也必须被标记为无限大,否则会抛出异常。这个函数的参数各自代表如下:a (m11)水平方向的缩放b(m12)竖直方向的倾斜偏移c(m21)水平方向的倾斜偏移d(m22)竖直方向的缩放e(dx)水平方向的移动f(dy)竖直方向的移动 -
setTransform(a, b, c, d, e, f)这个方法会将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用
transform方法。如果任意一个参数是无限大,那么变形矩阵也必须被标记为无限大,否则会抛出异常。从根本上来说,该方法是取消了当前变形,然后设置为指定的变形,一步完成。 -
重置当前变形为单位矩阵,它和调用以下语句是一样的:
ctx.setTransform(1, 0, 0, 1, 0, 0);
transform 和 setTransform 的示例
function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
var sin = Math.sin(Math.PI / 6);
var cos = Math.cos(Math.PI / 6);
ctx.translate(100, 100);
var c = 0;
for (var i = 0; i <= 12; i++) {
c = Math.floor((255 / 12) * i);
ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
ctx.fillRect(0, 0, 100, 10);
ctx.transform(cos, sin, -sin, cos, 0, 0);
}
ctx.setTransform(-1, 0, 0, 1, 100, 100);
ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
ctx.fillRect(0, 50, 100, 100);
}
对于
ctx.setTransform(-1, 0, 0, 1, 100, 100);对应参数:
- a = -1 → 水平缩放 -1(相当于水平翻转)
- b = 0
- c = 0
- d = 1 → 垂直缩放保持不变
- e = 100 → 水平平移 100 像素
- f = 100 → 垂直平移 100 像素
👉 这一步的效果就是:
- 把后续绘制内容沿 y 轴翻转(镜像)。
- 然后再 整体平移到 (100,100)。
渲染的时候把
(x,y)与这个矩阵相乘得到(x', y')$x′=a⋅x+c⋅y+e $
$y’ = b \cdot x + d \cdot y + f$
组合 Compositing
在之前的例子里面,我们总是将一个图形画在另一个之上,对于其他更多的情况,仅仅这样是远远不够的。比如,对合成的图形来说,绘制顺序会有限制。不过,我们可以利用 globalCompositeOperation 属性来改变这种状况。此外,clip属性允许我们隐藏不想看到的部分图形。
globalCompositeOperation
我们不仅可以在已有图形后面再画新图形,还可以用来遮盖指定区域,清除画布中的某些部分(清除区域不仅限于矩形,像clearRect()方法做的那样)以及更多其他操作。
这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识 12 种遮盖方式的字符串。
裁切路径

裁切路径和普通的 canvas 图形差不多,不同的是它的作用是遮罩,用来隐藏不需要的部分。如右图所示。红边五角星就是裁切路径,所有在路径以外的部分都不会在 canvas 上绘制出来。
如果和上面介绍的 globalCompositeOperation 属性作一比较,它可以实现与 source-in 和 source-atop差不多的效果。最重要的区别是裁切路径不会在 canvas 上绘制东西,而且它永远不受新图形的影响。这些特性使得它在特定区域里绘制图形时相当好用。
在 绘制图形 一章中,我只介绍了 stroke 和 fill 方法,这里介绍第三个方法clip。
-
将当前正在构建的路径转换为当前的裁剪路径。
我们使用 clip()方法来创建一个新的裁切路径。
默认情况下,canvas 有一个与它自身一样大的裁切路径(也就是没有裁切效果)。
clip 的示例
这个例子,我会用一个圆形的裁切路径来限制随机星星的绘制区域。
function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
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 = lingrad;
ctx.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();
}首先,我画了一个与 canvas 一样大小的黑色方形作为背景,然后移动原点至中心点。然后用 clip 方法创建一个弧形的裁切路径。裁切路径也属于 canvas 状态的一部分,可以被保存起来。如果我们在创建新裁切路径时想保留原来的裁切路径,我们需要做的就是保存一下 canvas 的状态。
裁切路径创建之后所有出现在它里面的东西才会画出来。在画线性渐变时我们就会注意到这点。然后会绘制出 50 颗随机位置分布(经过缩放)的星星,当然也只有在裁切路径里面的星星才会绘制出来。

基本的动画
由于我们是用 JavaScript 去操控canvas对象,这样要实现一些交互动画也是相当容易的。在本章中,我们将看看如何做一些基本的动画。
可能最大的限制就是图像一旦绘制出来,它就是一直保持那样了。如果需要移动它,我们不得不对所有东西(包括之前的)进行重绘。重绘是相当费时的,而且性能很依赖于电脑的速度。
动画的基本步骤
你可以通过以下的步骤来画出一帧:
- 清空 canvas 除非接下来要画的内容会完全充满 canvas(例如背景图),否则你需要清空所有。最简单的做法就是用
clearRect方法。 - 保存 canvas 状态 如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。
- 绘制动画图形(animated shapes) 这一步才是重绘动画帧。
- 恢复 canvas 状态 如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。
操控动画
在 canvas 上绘制内容是用 canvas 提供的或者自定义的方法,而通常,我们仅仅在脚本执行结束后才能看见结果,比如说,在 for 循环里面做完成动画是不太可能的。
因此,为了实现动画,我们需要一些可以定时执行重绘的方法。有两种方法可以实现这样的动画操控。首先可以通过 setInterval 和 setTimeout 方法来控制在设定的时间点上执行重绘。
有安排地更新画布
首先,可以用 setInterval()、setTimeout() 和 requestAnimationFrame() 来设定定期执行一个指定函数。
当设定好间隔时间后,function 会定期执行。
在设定好的时间之后执行函数
告诉浏览器你希望执行一个动画,并在重绘之前,请求浏览器执行一个特定的函数来更新动画。
如果你并不需要与用户互动,你可以使用 setInterval() 方法,它就可以定期执行指定代码。如果我们需要做一个游戏,我们可以使用键盘或者鼠标事件配合上 setTimeout() 方法来实现。通过设置事件监听,我们可以捕捉用户的交互,并执行相应的动作。
备注: 下面的例子,采用 window.requestAnimationFrame()实现动画效果。这个方法提供了更加平缓并更加有效率的方式来执行动画,当系统准备好了重绘条件的时候,才调用绘制动画帧。一般每秒钟回调函数执行 60 次,也有可能会被降低。想要了解更多关于动画循环的信息,尤其是游戏,可以在Game development zone 参考这篇文章 Anatomy of a video game。
动画时钟
<template>
<div class="main p-[30px] text-center">
<canvas ref="cavas" class=" border-red-300 border-2 m-auto" height="500px" width="500px"></canvas>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const cavas = ref(null)
onMounted(() => {
const ctx = cavas.value.getContext('2d')
function clock() {
const now = new Date();
ctx.save();
ctx.clearRect(0, 0, 150, 150);
ctx.translate(75, 75);
ctx.scale(0.4, 0.4);
ctx.rotate(-Math.PI / 2);
ctx.strokeStyle = "black";
ctx.fillStyle = "white";
ctx.lineWidth = 8;
ctx.lineCap = "round";
// 小时刻度
ctx.save();
for (let i = 0; i < 12; i++) {
ctx.beginPath();
ctx.rotate(Math.PI / 6);
ctx.moveTo(100, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.restore();
// 分钟刻度
ctx.save();
ctx.lineWidth = 5;
for (let i = 0; i < 60; i++) {
if (i % 5 !== 0) {
ctx.beginPath();
ctx.moveTo(117, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.rotate(Math.PI / 30);
}
ctx.restore();
const sec = now.getSeconds();
// 要显示扫秒式的时钟,请使用:
// const sec = now.getSeconds() + now.getMilliseconds() / 1000;
const min = now.getMinutes();
const hr = now.getHours() % 12;
ctx.fillStyle = "black";
// 显示图像描述
cavas.innerText = `当前时间:${hr}:${min}`;
// 时针
ctx.save();
ctx.rotate(
(Math.PI / 6) * hr + (Math.PI / 360) * min + (Math.PI / 21600) * sec,
);
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(-20, 0);
ctx.lineTo(80, 0);
ctx.stroke();
ctx.restore();
// 分针
ctx.save();
ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(-28, 0);
ctx.lineTo(112, 0);
ctx.stroke();
ctx.restore();
// 秒针
ctx.save();
ctx.rotate((sec * Math.PI) / 30);
ctx.strokeStyle = "#D40000";
ctx.fillStyle = "#D40000";
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(-30, 0);
ctx.lineTo(83, 0);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
ctx.stroke();
ctx.fillStyle = "rgb(0 0 0 / 0%)";
ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
ctx.fill();
ctx.restore();
ctx.beginPath();
ctx.lineWidth = 14;
ctx.strokeStyle = "#325FA2";
ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
ctx.stroke();
ctx.restore();
window.requestAnimationFrame(clock);
}
window.requestAnimationFrame(clock);
})
</script>
高级动画
在上一章,我们制作了基本动画以及逐步了解了让物件移动的方法。在这一部分,我们将会对运动有更深的了解并学会添加一些符合物理的运动以让我们的动画更加高级。
长尾效果
现在,我们使用的是 clearRect 函数帮我们清除前一帧动画。若用一个半透明的 fillRect 函数取代之,就可轻松制作长尾效果。
function clear() {
ctx.fillStyle = "rgba(255,255,255,0.3)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
//每一次都会让上个界面盖上一层半透明,这样动态的东西以前的就会越往前越淡出
像素操作
到目前为止,我们尚未深入了解 Canvas 画布真实像素的原理,事实上,你可以直接通过 ImageData 对象操纵像素数据,直接读取或将数据数组写入该对象中。稍后我们也将深入了解如何控制图像使其平滑(反锯齿)以及如何从 Canvas 画布中保存图像。
ImageData 对象
ImageData对象中存储着 canvas 对象真实的像素数据,它包含以下几个只读属性:
图片宽度,单位是像素
图片高度,单位是像素
Uint8ClampedArray 类型的一维数组,包含着 RGBA 格式的整型数据,范围在 0 至 255 之间(包括 255)。
data 属性返回一个 Uint8ClampedArray,它可以被使用作为查看初始像素数据。每个像素用 4 个 1bytes 值 (按照红,绿,蓝和透明值的顺序; 这就是”RGBA”格式) 来代表。每个颜色值部份用 0 至 255 来代表。每个部份被分配到一个在数组内连续的索引,左上角像素的红色部份在数组的索引 0 位置。像素从左到右被处理,然后往下,遍历整个数组。
Uint8ClampedArray 包含 height × width × 4 字节数据,索引值从 0 到 (height× width × 4)-1
例如,要读取图片中位于第 50 行,第 200 列的像素的蓝色部份,你会写以下代码:
const blueComponent = imageData.data[50 * (imageData.width * 4) + 200 * 4 + 2];根据行、列读取某像素点的 R/G/B/A 值的公式:
imageData.data[50 * (imageData.width * 4) + 200 * 4 + 0 / 1 / 2 / 3];你可能用会使用 Uint8ClampedArray.length 属性来读取像素数组的大小(以字节为单位):
var numBytes = imageData.data.length;创建一个 ImageData 对象
去创建一个新的,空白的 ImageData 对象,你应该会使用createImageData() 方法。有 2 个版本的 createImageData() 方法。
var myImageData = ctx.createImageData(width, height);上面代码创建了一个新的具体特定尺寸的 ImageData 对象。所有像素被预设为透明黑。
你也可以创建一个被 anotherImageData 对象指定的相同像素的 ImageData 对象。这个新的对象像素全部被预设为透明黑。这个并非复制了图片数据。
var myImageData = ctx.createImageData(anotherImageData);得到场景像素数据
为了获得一个包含画布场景像素数据的 ImageData 对象,你可以用 getImageData() 方法:
var myImageData = ctx.getImageData(left, top, width, height);这个方法会返回一个 ImageData 对象,它代表了画布区域的对象数据,此画布的四个角落分别表示为 (left, top), (left + width, top), (left, top + height), 以及 (left + width, top + height) 四个点。这些坐标点被设定为画布坐标空间元素。
备注: 任何在画布以外的元素都会被返回成一个透明黑的 ImageData 对象。
这个方法也会在文章用画布操作视频中展示。
颜色选择器
在这个例子里面,我们会使用 getImageData() 去展示鼠标光标下的颜色。为此,我们要当前鼠标的位置,记为 layerX 和 layerY,然后我们去查询 getImageData() 给我们提供的在那个位置的像数数组里面的像素数据。最后我们使用数组数据去设置背景颜色和 <div> 的文字去展示颜色值。
var img = new Image();
img.crossOrigin = "anonymous";
img.src = "./assets/rhino.jpg";
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
img.onload = function () {
ctx.drawImage(img, 0, 0);
img.style.display = "none";
};
var hoveredColor = document.getElementById("hovered-color");
var selectedColor = document.getElementById("selected-color");
function pick(event, destination) {
var x = event.layerX;
var y = event.layerY;
var pixel = ctx.getImageData(x, y, 1, 1);
var data = pixel.data;
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
destination.style.background = rgba;
destination.textContent = rgba;
return rgba;
}
canvas.addEventListener("mousemove", function (event) {
pick(event, hoveredColor);
});
canvas.addEventListener("click", function (event) {
pick(event, selectedColor);
});
在场景中写入像素数据
你可以用 putImageData() 方法去对场景进行像素数据的写入。
ctx.putImageData(myImageData, dx, dy);dx 和 dy 参数表示你希望在场景内左上角绘制的像素数据所得到的设备坐标。
例如,为了在场景内左上角绘制 myImageData 代表的图片,你可以写如下的代码:
ctx.putImageData(myImageData, 0, 0);图片灰度和反相颜色
在这个例子里,我们遍历所有像素以改变他们的数值。然后我们将被修改的像素数组通过putImageData() 放回到画布中去。invert 函数仅仅是去减掉颜色的最大色值 255。grayscale 函数仅仅是用红绿和蓝的平均值。你也可以用加权平均,例如 x = 0.299r + 0.587g + 0.114b 这个公式。更多资料请参考维基百科的Grayscale。
var img = new Image();
img.crossOrigin = "anonymous";
img.src = "./assets/rhino.jpg";
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
img.onload = function () {
ctx.drawImage(img, 0, 0);
};
var original = function () {
ctx.drawImage(img, 0, 0);
};
var invert = function () {
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // red
data[i + 1] = 255 - data[i + 1]; // green
data[i + 2] = 255 - data[i + 2]; // blue
}
ctx.putImageData(imageData, 0, 0);
};
var grayscale = function () {
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
var avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // red
data[i + 1] = avg; // green
data[i + 2] = avg; // blue
}
ctx.putImageData(imageData, 0, 0);
};
const inputs = document.querySelectorAll("[name=color]");
for (const input of inputs) {
input.addEventListener("change", function (evt) {
switch (evt.target.value) {
case "inverted":
return invert();
case "grayscale":
return grayscale();
default:
return original();
}
});
}缩放和反锯齿
在drawImage() 方法,第二个画布和imageSmoothingEnabled 属性的帮助下,我们可以放大显示我们的图片及看到详情内容。
我们得到鼠标的位置并裁剪出距左和上 5 像素,距右和下 5 像素的图片。然后我们将这幅图复制到另一个画布然后将图片调整到我们想要的大小。在缩放画布里,我们将 10×10 像素的对原画布的裁剪调整为 200×200。
zoomctx.drawImage(
canvas,
Math.abs(x - 5),
Math.abs(y - 5),
10,
10,
0,
0,
200,
200,
);因为反锯齿默认是启用的,我们可能想要关闭它以看到清楚的像素。你可以通过切换勾选框来看到 imageSmoothingEnabled 属性的效果(不同浏览器需要不同前缀)。
缩放示例
<canvas id="canvas" width="300" height="227"></canvas>
<canvas id="zoom" width="300" height="227"></canvas>
<div>
<label for="smoothbtn">
<input type="checkbox" name="smoothbtn" checked="checked" id="smoothbtn" />
Enable image smoothing
</label>
</div>var img = new Image();
img.src = "rhino.jpg";
img.onload = function () {
draw(this);
};
function draw(img) {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
img.style.display = "none";
var zoomctx = document.getElementById("zoom").getContext("2d");
var smoothbtn = document.getElementById("smoothbtn");
var toggleSmoothing = function (event) {
zoomctx.imageSmoothingEnabled = this.checked;
zoomctx.mozImageSmoothingEnabled = this.checked;
zoomctx.webkitImageSmoothingEnabled = this.checked;
zoomctx.msImageSmoothingEnabled = this.checked;
};
smoothbtn.addEventListener("change", toggleSmoothing);
var zoom = function (event) {
var x = event.layerX;
var y = event.layerY;
zoomctx.drawImage(
canvas,
Math.abs(x - 5),
Math.abs(y - 5),
10,
10,
0,
0,
200,
200,
);
};
canvas.addEventListener("mousemove", zoom);
}
保存图片
HTMLCanvasElement 提供一个 toDataURL() 方法,此方法在保存图片的时候非常有用。它返回一个包含被类型参数规定的图像表现格式的数据链接。返回的图片分辨率是 96 dpi。
-
默认设定。创建一个 PNG 图片。
-
canvas.toDataURL('image/jpeg', quality)创建一个 JPG 图片。你可以有选择地提供从 0 到 1 的品质量,1 表示最好品质,0 基本不被辨析但有比较小的文件大小。
当你从画布中生成了一个数据链接,例如,你可以将它用于任何 <image> 元素,或者将它放在一个有 download 属性的超链接里用于保存到本地。
你也可以从画布创建一个 Blob 对象。
-
canvas.toBlob(callback, type, encoderOptions)这个创建了一个在画布中的代表图片的
Blob对象。
canvas 的优化
canvas元素是众多广泛使用的网络 2D 图像渲染标准之一。它被广泛用于游戏及复杂的图像可视化中。然而,随着网站和应用将 canvas 画布推至极限,性能开始成为问题。此文目标是给使用 canvas 画布元素的优化带来建议,去保证你的网站或者应用表现卓越。
性能贴士
下面是一些改善性能的建议
在离屏 canvas 上预渲染相似的图形或重复的对象
如果发现自己在每个动画帧上重复了一些相同的绘制操作,请考虑将其分流到屏幕外的画布上。然后,你可以根据需要频繁地将屏幕外图像渲染到主画布上,而不必首先重复生成该图像的步骤。
myEntity.offscreenCanvas = document.createElement("canvas");
myEntity.offscreenCanvas.width = myEntity.width;
myEntity.offscreenCanvas.height = myEntity.height;
myEntity.offscreenContext = myEntity.offscreenCanvas.getContext("2d");
myEntity.render(myEntity.offscreenContext);避免浮点数的坐标点,用整数取而代之
当你画一个没有整数坐标点的对象时会发生子像素渲染。
ctx.drawImage(myImage, 0.3, 0.5);浏览器为了达到抗锯齿的效果会做额外的运算。为了避免这种情况,请保证在你调用drawImage()函数时,用Math.floor()函数对所有的坐标点取整。
不要在用drawImage时缩放图像
在离屏 canvas 中缓存图片的不同尺寸,而不要用drawImage()去缩放它们。
使用多层画布去画一个复杂的场景
在你的应用程序中,你可能会发现某些对象需要经常移动或更改,而其他对象则保持相对静态。在这种情况下,可能的优化是使用多个<canvas>元素对你的项目进行分层。
这样长尾可以给特定的运动物体
例如,假设你有一个游戏,其 UI 位于顶部,中间是游戏性动作,底部是静态背景。在这种情况下,你可以将游戏分成三个<canvas>层。UI 将仅在用户输入时发生变化,游戏层随每个新框架发生变化,并且背景通常保持不变。
<div id="stage">
<canvas id="ui-layer" width="480" height="320"></canvas>
<canvas id="game-layer" width="480" height="320"></canvas>
<canvas id="background-layer" width="480" height="320"></canvas>
</div>
<style>
#stage {
width: 480px;
height: 320px;
position: relative;
border: 2px solid black;
}
canvas {
position: absolute;
}
#ui-layer {
z-index: 3;
}
#game-layer {
z-index: 2;
}
#background-layer {
z-index: 1;
}
</style>用 CSS 设置大的背景图
如果像大多数游戏那样,你有一张静态的背景图,用一个静态的div元素,结合background 特性,以及将它置于画布元素之后。这么做可以避免在每一帧在画布上绘制大图。
用 CSS 变换特性缩放画布
CSS 变换使用 GPU,因此速度更快。最好的情况是不直接缩放画布,或者具有较小的画布并按比例放大,而不是较大的画布并按比例缩小。
var scaleX = window.innerWidth / canvas.width;
var scaleY = window.innerHeight / canvas.height;
var scaleToFit = Math.min(scaleX, scaleY);
var scaleToCover = Math.max(scaleX, scaleY);
stage.style.transformOrigin = "0 0"; //scale from top left
stage.style.transform = "scale(" + scaleToFit + ")";关闭透明度
如果你的游戏使用画布而且不需要透明,当使用 HTMLCanvasElement.getContext() 创建一个绘图上下文时把 alpha 选项设置为 false 。这个选项可以帮助浏览器进行内部优化。
var ctx = canvas.getContext("2d", { alpha: false });更多的贴士
- 将画布的函数调用集合到一起(例如,画一条折线,而不要画多条分开的直线)
- 避免不必要的画布状态改变
- 渲染画布中的不同点,而非整个新状态
- 尽可能避免
shadowBlur特性 - 尽可能避免text rendering
- 尝试不同的方法来清除画布 (
clearRect()vs.fillRect()vs. 调整 canvas 大小) - 有动画,请使用
Window.requestAnimationFrame()而非setInterval() - 请谨慎使用大型物理库
无敌推荐
其他的 Web API
当和 canvas 和图形进一步打交道时,这些 API 可能会排上用场。
-
用于渲染交互式 3D 图形的 API.
-
可缩放矢量图形(简称 SVG)允许你使用矢量线,矢量图形,确保无论图片大小都可以比例平滑地显示。
-
Web Audio API 提供了 Web 上的音频控制——个强大和灵活的系统,允许开发人员选择的音频源,添加效果的音频,创建音频可视化,应用空间效应(如平移)等等。
🎉效果
canvas-confetti

这个库叫 canvas-confetti ✅
基本信息
-
名称:
canvas-confetti -
作者:catdad(GitHub 上很活跃的前端开发者)
-
官网 / 仓库:https://github.com/catdad/canvas-confetti https://www.kirilv.com/canvas-confetti/
-
CDN 导入(免安装方式):
jsimport confetti from 'https://cdn.skypack.dev/canvas-confetti'或者
js<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"></script>
🧠 简介
canvas-confetti 是一个轻量级的 HTML5 Canvas 动效库,
主要用来在网页上生成彩纸、彩带、撒花效果(confetti animation)。
它广泛用于:
- 活动庆祝页面(如注册成功、支付成功)
- 电商抽奖页、节日活动页
- “恭喜获奖”“升级成功”等弹窗
几乎所有网页上看到的“向上撒礼花”“满屏飘彩纸”的效果, 十有八九就是用这个库做的(有时也被封装成 React/Vue 组件)。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>彩片动画演示</title>
</head>
<body>
<script type="module">
import confetti from 'https://cdn.skypack.dev/canvas-confetti';
setInterval(() => {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 }
});
}, 2000)
</script>
</body>
</html>js-confetti
这是 JS Confetti 库的官方仓库 / 官网地址:
- GitHub 仓库:https://github.com/loonywizard/js-confetti
- 官方 Demo / 网站:https://loonywizard.github.io/js-confetti/

party.js
https://github.com/yiliansource/party-js

tsParticles
用于制作背景
GitHub 仓库:https://github.com/tsparticles/tsparticles GitHub 官网/文档:https://particles.js.org/ tsParticles+1
如果你愿意,我可以提供一个简单的 demo 链接或在线 CodePen 给你。


mo.js
mo.js 是一个非常有代表性的动效库,它比 confetti.js、party.js 那类“庆祝特效”库更强大,属于 基于物理和时间线的动画引擎,可以做彩带爆炸、按钮波纹、粒子喷射、文字弹出、爆炸 LOGO 等等效果。
🌈 基本信息
- 官网: https://mojs.github.io/
- GitHub: https://github.com/mojs/mojs
演示:::https://blog.codepen.io/2017/09/22/fun-mo-js/
网页截图
原理是将页面元素在canvas绘制,然后导出图片
评论
评论加载中……
