Canvas
Canvas 是 HTML5 提供的一个用于绘制图形的元素。通过 JavaScript,可以在 canvas 上绘制图表、制作图片构图或者制作简单的动画。
注
基本用法
<canvas>
标签:用于创建画布,常用属性有 width 和 height。
绘制图形
矩形
- fillRect(x, y, width, height):绘制填充矩形
- strokeRect(x, y, width, height):绘制矩形边框
- clearRect(x, y, width, height):清除指定区域
路径
- beginPath():开始新路径
- moveTo(x, y):移动笔触
- lineTo(x, y):画直线
- arc(x, y, r, startAngle, endAngle, anticlockwise):画圆/弧
- closePath():闭合路径
- stroke():描边
- fill():填充
- Path2D
- new Path2D():创建路径对象,可缓存路径命令
- ctx.stroke(path) / ctx.fill(path):对 Path2D 对象描边/填充
样式和色彩
- fillStyle / strokeStyle:设置填充/描边颜色
- globalAlpha:全局透明度
- 线型相关:lineWidth、lineCap、lineJoin、setLineDash()、lineDashOffset
- 渐变:createLinearGradient()、createRadialGradient()、addColorStop()
- 图案:createPattern(image, type)
- 阴影:shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor
文本
- fillText(text, x, y, maxWidth):填充文本
- strokeText(text, x, y, maxWidth):描边文本
- 样式:font、textAlign、textBaseline、direction
- 测量:measureText(text)
图像
- drawImage(image, x, y):绘制图片
- drawImage(image, x, y, width, height):缩放绘制
- drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight):切片绘制
变形(Transformations)
- save() / restore():保存/恢复状态
- translate(x, y):平移
- rotate(angle):旋转(弧度)
- scale(x, y):缩放
组合与裁切
- globalCompositeOperation:设置全局混合模式
- clip():裁切路径
基本用法
canvas 元素
<canvas>
元素只有两个属性:width
和height
,当没有设置宽度和高度的时候,canvas 会初始化宽度为 300 像素和高度为 150 像素。<canvas>
标签必须有结束标签</canvas>
。开始标签和结束标签之间的内容为替换内容,如果浏览器不支持<canvas>
标签,则这些内容会被显示。
<canvas id="myCanvas" width="400" height="300"> 你的浏览器不支持 canvas 标签。 </canvas>
渲染上下文
<canvas>
元素创造了一个固定大小的画布,它公开了一个或多个渲染上下文,其可以用来绘制和处理要展示的内容。
const canvas = document.getElementById("myCanvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
// 绘制代码
} else {
// 不支持 canvas 的代码
}
示例
<canvas id="canvas" width="150" height="150"></canvas>
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const 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);
}
}
draw();
使用 canvas 绘制图形
栅格
通常来说网格中的一个单元相当于 canvas 元素中的一像素。栅格的起点为左上角(坐标为(0,0))。所有元素的位置都相对于原点定位。所以图中蓝色方形左上角的坐标为距离左边(X 轴)x 像素,距离上边(Y 轴)y 像素(坐标为(x,y))。

绘制矩形
不同于 SVG,<canvas>
只支持两种形式的图形绘制:矩形和路径(由一系列点连成的线段)。所有其他类型的图形都是通过一条或者多条路径组合而成的。
// fillRect(x, y, width, height) // 填充矩形
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 100, 50);
// strokeRect(x, y, width, height) // 描边矩形
ctx.strokeStyle = "blue";
ctx.strokeRect(10, 70, 100, 50);
// clearRect(x, y, width, height) // 清除指定区域
ctx.clearRect(20, 20, 30, 30);
以上的三个函数绘制之后会马上显现在 canvas 上,即时生效。
<canvas id="canvas"></canvas>
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
ctx.fillRect(25, 25, 100, 100);
ctx.clearRect(45, 45, 60, 60);
ctx.strokeRect(50, 50, 50, 50);
}
}
draw();
绘制路径
图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。使用路径绘制图形需要一些额外的步骤。
- 首先,你需要创建路径起始点。
- 然后你使用画图命令去画出路径。
- 之后你把路径封闭。
- 一旦路径生成,你就能通过描边或填充路径区域来渲染图形。
以下是所要用到的函数:
beginPath()
新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。closePath()
闭合路径之后图形绘制命令又重新指向到上下文中。(不是必需的。这个方法会通过绘制一条从当前点到开始点的直线来闭合图形。如果图形是已经闭合了的,即当前点为开始点,该函数什么也不做)stroke()
通过线条来绘制图形轮廓。fill()
通过填充路径的内容区域生成实心的图形。
ctx.beginPath(); // 新建路径
ctx.moveTo(150, 50); // 移动笔触到指定位置
ctx.lineTo(200, 100); // 从当前位置绘制一条线到指定位置
ctx.lineTo(100, 100); // 从当前位置绘制一条线到指定位置
ctx.closePath(); // 闭合路径
ctx.stroke(); // 描边
ctx.fill(); // 填充
移动笔触 moveTo(x, y)
将笔触移动到指定位置,不进行绘制。
线:lineTo(x, y)
绘制一条从当前位置到指定 x 以及 y 位置的直线。
圆弧:arc(x, y, radius, startAngle, endAngle, anticlockwise)
画一个以(x,y)为圆心的以 radius 为半径的圆弧(圆),从 startAngle 开始到 endAngle 结束,按照 anticlockwise 给定的方向(默认为顺时针)来生成。
二次贝塞尔曲线:quadraticCurveTo(cp1x, cp1y, x, y)
绘制二次贝塞尔曲线,cp1x,cp1y 为一个控制点,x,y 为结束点。
三次贝塞尔曲线:bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
绘制三次贝塞尔曲线,cp1x,cp1y 为控制点一,cp2x,cp2y 为控制点二,x,y 为结束点。
矩形:rect(x, y, width, height)
绘制一个左上角坐标为(x,y),宽高为 width 以及 height 的矩形。
闭合路径 closePath()
绘制一条从当前点到开始点的直线来闭合图形。如果图形是已经闭合了的,即当前点为开始点,该函数什么也不做。
描边 stroke()
通过线条来绘制图形轮廓。
Path2D 对象
Path2D 对象是路径列表,用来缓存或记录绘画命令,这样你将能快速地回顾路径。
new Path2D(); // 空的 Path 对象
new Path2D(path); // 克隆 Path 对象
new Path2D(d); // 从 SVG 建立 Path 对象
Path2D.addPath(path [, transform]) // 添加路径
示例
<canvas id="canvas"></canvas>
function draw() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
const rectangle = new Path2D(); // 创建矩形路径
rectangle.rect(10, 10, 50, 50);
const circle = new Path2D(); // 创建圆形路径
circle.moveTo(125, 35);
circle.arc(100, 35, 25, 0, 2 * Math.PI);
ctx.stroke(rectangle); // 描边矩形
ctx.fill(circle); // 填充圆形
}
}
draw();
样式和色彩
色彩
fillStyle
设置图形的填充颜色。默认值为黑色。strokeStyle
设置图形轮廓的颜色。默认值为黑色。
<canvas id="canvas" width="500"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
ctx.fillStyle = `rgb(${Math.floor(255 - 42.5 * i)} ${Math.floor(255 - 42.5 * j)} 0)`;
ctx.fillRect(j * 25, i * 25, 25, 25);
}
}
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
ctx.strokeStyle = `rgb(0 ${Math.floor(255 - 42.5 * i)} ${Math.floor(255 - 42.5 * j)})`;
ctx.beginPath();
ctx.arc(200 + j * 25, 12.5 + i * 25, 10, 0, 2 * Math.PI, true);
ctx.stroke();
}
}
}
draw();
透明度
可以通过颜色值的 alpha 通道来设置透明度,也可以通过 globalAlpha
属性来设置透明度。
globalAlpha
设置透明度。globalAlpha
属性是一个介于 0.0 和 1.0 之间的值(包含 0.0 和 1.0)。globalAlpha
属性是全局的,影响所有绘制的颜色。
<canvas id="canvas"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 设置透明度值
ctx.globalAlpha = 0.2;
// 画半透明圆
for (let i = 0; i < 7; i++) {
ctx.beginPath();
ctx.arc(75, 75, 10 + 10 * i, 0, Math.PI * 2, true);
ctx.fill();
}
}
draw();
线型
lineWidth
设置线条宽度。lineCap
设置线条末端样式。(butt, round, square)lineJoin
设置线条连接处样式。(round, bevel, miter)miterLimit
设置最大斜接长度。getLineDash()
获取线条样式。setLineDash(segments)
设置线条样式。lineDashOffset
设置线条样式的偏移量。
注
线宽的默认值为 1,由于画布的坐标并不和像素直接对应,当需要获得精确的水平或垂直线的时候要特别注意。
想要获得精确的线条,必须对线条是如何描绘出来的有所理解。见下图,用网格来代表 canvas 的坐标格,每一格对应屏幕上一个像素点。在第一个图中,填充了 (2,1) 至 (5,5) 的矩形,整个区域的边界刚好落在像素边缘上,这样就可以得到的矩形有着清晰的边缘。
如果你想要绘制一条从 (3,1) 到 (3,5),宽度是 1.0 的线条,你会得到像第二幅图一样的结果。实际填充区域(深蓝色部分)仅仅延伸至路径两旁各一半像素。而这半个像素又会以近似的方式进行渲染,这意味着那些像素只是部分着色,结果就是以实际笔触颜色一半色调的颜色来填充整个区域(浅蓝和深蓝的部分)。这就是上例中为何宽度为 1.0 的线并不准确的原因。
要解决这个问题,你必须对路径施以更加精确的控制。已知粗 1.0 的线条会在路径两边各延伸半像素,那么像第三幅图那样绘制从 (3.5,1) 到 (3.5,5) 的线条,其边缘正好落在像素边界,填充出来就是准确的宽为 1.0 的线条。

渐变
createLinearGradient(x0, y0, x1, y1)
创建线性渐变。- createLinearGradient 方法接受 4 个参数,表示渐变的起点 (x1,y1) 与终点 (x2,y2)。
createRadialGradient(x0, y0, r0, x1, y1, r1)
创建径向渐变。- createRadialGradient 方法接受 6 个参数,前三个定义一个以 (x1,y1) 为原点,半径为 r1 的圆,后三个参数则定义另一个以 (x2,y2) 为原点,半径为 r2 的圆。
addColorStop(offset, color)
添加颜色断点。
<canvas id="canvas"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 创建渐变
const linGrad = ctx.createLinearGradient(0, 0, 0, 150);
linGrad.addColorStop(0, "#00ABEB");
linGrad.addColorStop(0.5, "#fff");
linGrad.addColorStop(0.5, "#26C000");
linGrad.addColorStop(1, "#fff");
const linGrad2 = ctx.createLinearGradient(0, 50, 0, 95);
linGrad2.addColorStop(0.5, "#000");
linGrad2.addColorStop(1, "rgb(0 0 0 / 0%)");
// 赋给 fillStyle 和 strokeStyle 属性
ctx.fillStyle = linGrad;
ctx.strokeStyle = linGrad2;
// 画图形
ctx.fillRect(10, 10, 130, 130);
ctx.strokeRect(50, 50, 50, 50);
}
draw();
图案样式
createPattern(image, type)
创建图案。
该方法接受两个参数。Image 可以是一个 Image 对象的引用,或者另一个 canvas 对象。Type 必须是下面的字符串值之一:repeat,repeat-x,repeat-y 和 no-repeat。
图案的应用跟渐变很类似的,创建出一个图案之后,赋给 fillStyle 或 strokeStyle 属性即可。
<canvas id="canvas"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 创建新 image 对象,用作图案
const img = new Image();
img.src = "https://picsum.photos/id/10/200";
img.onload = () => {
// 创建图案
const pattern = ctx.createPattern(img, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 150, 150);
};
}
draw();
阴影
shadowOffsetX
设置阴影的横向偏移量。shadowOffsetY
设置阴影的纵向偏移量。shadowBlur
设置阴影的模糊程度。shadowColor
设置阴影的颜色。
<canvas id="canvas" height="50"></canvas>
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);
}
draw();
绘制文本
fillText(text, x, y [, maxWidth])
在指定的 (x,y) 位置填充指定的文本。strokeText(text, x, y [, maxWidth])
在指定的 (x,y) 位置绘制文本边框。
<canvas id="canvas"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
ctx.font = "48px serif";
ctx.fillText("你好世界", 10, 50);
ctx.strokeText("你好世界", 10, 100);
}
draw();
文本样式
font
设置文本样式。(默认的字体是 10px sans-serif。)textAlign
设置文本对齐方式。(start, end, left, right, center)textBaseline
设置文本基线。(top, hanging, middle, alphabetic, ideographic, bottom)direction
设置文本方向。(ltr, rtl, inherit)
测量文本
measureText(text)
测量文本的宽度。
<canvas id="canvas" height="20"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
const text = ctx.measureText("foo"); // TextMetrics 对象
ctx.fillText("foo", 0, 10);
text.width; // 16;
}
draw();
使用图像
引入图像到 canvas 里需要以下两步基本操作:
- 获得一个指向
HTMLImageElement
的对象或者另一个 canvas 元素的引用作为源,也可以通过提供一个 URL 的方式来使用图片 - 使用
drawImage()
函数将图片绘制到画布上
获取需要绘制的图片
canvas 的 API 可以使用下面这些类型中的一种作为图片的源:
HTMLImageElement
这些图片是由 Image() 函数构造出来的,或者任何的<img>
元素HTMLVideoElement
用视频元素作为图片源,可以从视频中抓取当前帧作为一个图像HTMLCanvasElement
可以使用另一个<canvas>
元素作为你的图片源ImageBitmap
这是一个高性能的位图,可以低延迟地绘制,它可以从上述的所有源以及其他几种源中生成
绘制图片
可以使用下面三种方法之一来绘制图片:
drawImage(image, x, y)
- 其中 image 是 image 或者 canvas 对象
- x 和 y 是其在目标 canvas 里的起始坐标。
drawImage(image, x, y, width, height)
缩放- 其中 image 是 image 或者 canvas 对象
- x 和 y 是其在目标 canvas 里的起始坐标
- width 和 height 设置图像的宽高。
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
切片- 其中 image 是 image 或者 canvas 对象
- sx 和 sy 是其在源图像中的起始坐标,sWidth 和 sHeight 设置图像的宽高
- dx 和 dy 是其在目标 canvas 里的起始坐标,dWidth 和 dHeight 设置图像的宽高。
<canvas id="canvas"></canvas>
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 = "https://picsum.photos/id/10/200";
}
draw();
相框示例
<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="https://picsum.photos/id/10/150" width="150" 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("frame"), 0, 0);
// 绘制切片
ctx.drawImage(document.getElementById("source"), 33, 71, 100, 100, 25, 25, 100, 100);
}
draw();
变形 Transformations
变形是一种更强大的方法,可以将原点移动到另一点、对网格进行旋转和缩放。
状态的保存和恢复
save()
保存当前状态。restore()
恢复之前保存过的状态。
Canvas 状态存储在栈中,每当 save()
方法被调用后,当前的状态就被推送到栈中保存。restore()
方法从栈中恢复之前保存过的状态。一个绘画状态包括:
- 当前应用的变形
- 下面这些属性:
strokeStyle
、fillStyle
、globalAlpha
、lineWidth
、lineCap
、lineJoin
、miterLimit
、shadowOffsetX
、shadowOffsetY
、shadowBlur
、shadowColor
、globalCompositeOperation
、font
、textAlign
、textBaseline
、direction
、imageSmoothingEnabled
- 当前的裁切路径
<canvas id="canvas"></canvas>
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); // 使用加载的配置绘制一个矩形
}
draw();
移动
translate(x, y)
x 是左右偏移量,y 是上下偏移量
<canvas id="canvas"></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(); // 恢复状态,恢复原点位置
}
}
}
draw();
旋转
rotate(angle)
旋转,angle 是旋转角度,以弧度为单位,顺时针。旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到 translate 方法。
<canvas id="canvas"></canvas>
function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
ctx.save();
ctx.translate(100, 0);
ctx.rotate(Math.PI / 4);
ctx.fillRect(0, 0, 100, 100);
ctx.restore();
}
draw();
缩放
scale(x, y)
缩放,x 是水平缩放比例,y 是垂直缩放比例。
<canvas id="canvas"></canvas>
function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
ctx.save();
ctx.scale(0.5, 0.5);
ctx.fillRect(0, 0, 100, 100);
ctx.restore();
}
draw();
组合 Compositing
全局混合模式 globalCompositeOperation
我们不仅可以在已有图形后面再画新图形,还可以用来遮盖指定区域,清除画布中的某些部分(清除区域不仅限于矩形,像 clearRect()
方法做的那样)以及更多其他操作。
source-over
在目标图像上显示源图像。source-in
在目标图像上显示源图像,只显示目标图像和源图像重叠的部分。source-out
在目标图像上显示源图像,只显示源图像和目标图像不重叠的部分。source-atop
在目标图像上显示源图像,只显示源图像和目标图像重叠的部分。destination-over
在源图像上显示目标图像。
裁切路径
clip()
方法将当前创建的路径设置为当前的剪切路径。
<canvas id="canvas"></canvas>
function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
ctx.beginPath();
ctx.arc(75, 75, 50, 0, Math.PI * 2, true);
ctx.clip(); // 将当前正在构建的路径转换为当前的裁剪路径,此处创建了一个圆形裁剪路径
// 裁切路径创建之后所有出现在它里面的东西才会画出来。
ctx.fillRect(25, 25, 100, 100);
}
draw();
基本动画
动画的基本步骤
- 清空 canvas
- 保存 canvas 的当前状态
- 绘制动画图形(通常是一个简单图形,如矩形)
- 恢复 canvas 的状态
- 重复第 3 步和第 4 步,直到动画结束
示例
<canvas id="canvas" width="300" height="300"></canvas>
// 画太阳
function drawSun(ctx) {
ctx.save();
ctx.beginPath();
ctx.arc(150, 150, 30, 0, 2 * Math.PI, false); // 画太阳圆路径
ctx.fillStyle = "#FDB813";
ctx.shadowColor = "#FDB813";
ctx.shadowBlur = 20;
ctx.fill(); // 填充颜色
ctx.shadowBlur = 0;
ctx.restore(); // 恢复坐标状态
}
// 画地球轨道
function drawOrbit(ctx) {
ctx.save();
ctx.beginPath();
ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // 画地球轨道路径
ctx.strokeStyle = "rgb(0 153 255 / 40%)";
ctx.stroke(); // 画地球轨道
ctx.restore(); // 恢复坐标状态
}
// 画地球
function drawEarth(ctx, time) {
ctx.save();
ctx.translate(150, 150); // 移动坐标到太阳中心
ctx.rotate(((2 * Math.PI) / 60) * time.getSeconds() + ((2 * Math.PI) / 60000) * time.getMilliseconds()); // 旋转角度
ctx.translate(105, 0); // 移动坐标到地球中心
// 画地球
ctx.beginPath();
ctx.arc(0, 0, 12, 0, 2 * Math.PI, false); // 画地球圆路径
ctx.fillStyle = "#3af"; // 填充颜色
ctx.fill(); // 填充颜色
// 画地球阴影(半球)
ctx.beginPath();
ctx.arc(0, 0, 12, Math.PI / 2, (Math.PI * 3) / 2, true);
ctx.closePath();
ctx.fillStyle = "#1976d2";
ctx.fill();
ctx.restore();
}
// 画月亮
function drawMoon(ctx, time) {
ctx.save();
ctx.translate(150, 150);
ctx.rotate(((2 * Math.PI) / 60) * time.getSeconds() + ((2 * Math.PI) / 60000) * time.getMilliseconds());
ctx.translate(105, 0);
// 坐标移动到了地球的中心,开始画月亮
ctx.save();
ctx.rotate(((2 * Math.PI) / 6) * time.getSeconds() + ((2 * Math.PI) / 6000) * time.getMilliseconds());
ctx.translate(0, 28.5);
// 坐标移动到了月亮中心,开始画月亮
ctx.beginPath();
ctx.arc(0, 0, 3.5, 0, 2 * Math.PI, false);
ctx.fillStyle = "#ccc";
ctx.fill();
ctx.restore();
ctx.restore();
}
// 动画
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
ctx.clearRect(0, 0, 300, 300);
// 顺序:太阳-轨道-地球-月亮
drawSun(ctx);
drawOrbit(ctx);
const time = new Date();
drawEarth(ctx, time);
drawMoon(ctx, time);
// 动画
requestAnimationFrame(draw);
}
draw();
高级动画
<canvas id="canvas" style="border: 1px solid" width="600" height="300"></canvas>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf; // 动画帧
// 标记动画是否正在运行
let running = false;
// 小球对象,包含位置、速度、半径、颜色和绘制方法
const ball = {
x: 100, // 小球初始 x 坐标
y: 100, // 小球初始 y 坐标
vx: 5, // 小球 x 方向速度
vy: 1, // 小球 y 方向速度
radius: 25, // 小球半径
color: "blue", // 小球颜色
// 绘制小球的方法
draw: function () {
ctx.beginPath(); // 开始新路径
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true); // 画圆
ctx.closePath(); // 关闭路径
ctx.fillStyle = this.color; // 设置填充颜色
ctx.fill(); // 填充圆形
},
};
// 半透明清空画布,制造拖影效果
function clear() {
ctx.fillStyle = "rgba(255,255,255,0.3)"; // 设置半透明白色,制造拖影效果
ctx.fillRect(0, 0, canvas.width, canvas.height); // 覆盖整个画布
}
// 动画绘制函数
function draw() {
clear(); // 半透明清空画布,制造拖影效果
ball.draw(); // 绘制小球
ball.x += ball.vx; // 更新小球 x 坐标
ball.y += ball.vy; // 更新小球 y 坐标
// 碰到上下边界时反弹
if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
ball.vy = -ball.vy;
}
// 碰到左右边界时反弹
if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
ball.vx = -ball.vx;
}
// 请求下一帧动画
raf = window.requestAnimationFrame(draw);
}
// 鼠标移动时,如果动画未运行,则让小球跟随鼠标移动并绘制
canvas.addEventListener("mousemove", function (e) {
if (!running) {
clear(); // 清空画布
ball.x = e.offsetX; // 小球跟随鼠标 x
ball.y = e.offsetY; // 小球跟随鼠标 y
ball.draw(); // 绘制小球
}
});
// 鼠标点击时,启动动画
canvas.addEventListener("click", function (e) {
if (!running) {
raf = window.requestAnimationFrame(draw); // 启动动画
running = true; // 标记动画已运行
}
});
// 鼠标移出 canvas 时,停止动画
canvas.addEventListener("mouseout", function (e) {
window.cancelAnimationFrame(raf); // 停止动画
running = false; // 标记动画未运行
});
// 页面加载时先绘制一次小球(静止状态)
ball.draw();
像素操作
ImageData 对象
ImageData 对象中存储着 canvas 对象真实的像素数据,它包含以下几个只读属性:
- width:图片宽度,以像素计。
- height:图片高度,以像素计。
- data:
Uint8ClampedArray
类型的数组,包含图片的 RGBA 数据,每个像素 4 个值(红、绿、蓝、透明度)。
Uint8ClampedArray
包含 height × width × 4
字节数据,索引值从 0 到 (height× width × 4)-1
。
例如,要读取图片中位于第 50 行,第 200 列的像素的蓝色部份,你会写以下代码:
const blueComponent = imageData.data[50 * (imageData.width * 4) + 200 * 4 + 2];
获取场景像素数据
为了获得一个包含画布场景像素数据的 ImageData 对象,你可以用 getImageData()
方法:
const imageData = ctx.getImageData(left, top, width, height);
<div class="container">
<canvas id="canvas" width="300" height="227"></canvas>
<div id="hovered-color" style="width: 200px"></div>
<div id="selected-color" style="width: 200px"></div>
</div>
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "https://mdn.github.io/shared-assets/images/examples/rhino.jpg";
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
img.onload = function () {
ctx.drawImage(img, 0, 0);
img.style.display = "none";
};
const hoveredColorElement = document.getElementById("hovered-color");
const selectedColorElement = document.getElementById("selected-color");
// 获取鼠标位置的像素颜色,并设置到元素的背景色和文本内容
function pick(event, element) {
const x = event.layerX;
const y = event.layerY;
const pixel = ctx.getImageData(x, y, 1, 1);
const data = pixel.data;
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
element.style.background = rgba;
element.textContent = rgba;
return rgba;
}
// 鼠标移动时,获取鼠标位置的像素颜色,并设置到元素的背景色和文本内容
canvas.addEventListener("mousemove", function (event) {
pick(event, hoveredColorElement);
});
// 鼠标点击时,获取鼠标位置的像素颜色,并设置到元素的背景色和文本内容
canvas.addEventListener("click", function (event) {
pick(event, selectedColorElement);
});
.container {
height: 227px;
display: flex;
gap: 10px;
}
设置场景像素数据
要设置场景像素数据,你需要使用 putImageData()
方法:
ctx.putImageData(myImageData, dx, dy);
dx
和 dy
参数表示在画布上放置图像的左上角位置。
myImageData
参数是一个 ImageData 对象,它包含要放置的图像数据。
保存图片
示例
<canvas id="canvas"></canvas>
/** 获取画布 */
const canvas = document.getElementById("canvas");
/** 获取画布的上下文对象 */
const ctx = canvas.getContext("2d");
/** 初始化画布的宽高 */
function init() {
// canvas.width = window.innerWidth * window.devicePixelRatio;
// canvas.height = window.innerHeight * window.devicePixelRatio;
canvas.width = window.innerWidth;
canvas.height = 500;
}
init();
/** 获取随机数 */
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/** 点 */
class Point {
constructor() {
this.r = 6; // 半径
this.x = getRandom(this.r / 2, canvas.width - this.r / 2); // 随机 x 坐标
this.y = getRandom(this.r / 2, canvas.height - this.r / 2); // 随机 y 坐标
this.vx = getRandom(-50, 50); // x 速度
this.vy = getRandom(-50, 50); // y 速度
this.lastDrawTime = null; // 上一次绘制时间
}
/** 绘制点 */
draw() {
// 更新坐标
if (this.lastDrawTime) {
const deltaTime = (Date.now() - this.lastDrawTime) / 1000;
this.x += this.vx * deltaTime;
this.y += this.vy * deltaTime;
// 如果超出边界,则反弹
if (this.x < this.r / 2 || this.x > canvas.width - this.r / 2) {
this.vx = -this.vx;
}
if (this.y < this.r / 2 || this.y > canvas.height - this.r / 2) {
this.vy = -this.vy;
}
}
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fillStyle = "rgb(200, 200, 200)";
ctx.fill();
this.lastDrawTime = Date.now(); // 更新上一次绘制时间
}
}
/** 图 */
class Graph {
constructor(pointNum = 30, maxDistance = 200) {
this.maxDistance = maxDistance;
this.points = Array.from({ length: pointNum }, () => new Point());
}
draw() {
requestAnimationFrame(() => {
this.draw();
});
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制图
this.points.forEach((point, index) => {
point.draw();
// 绘制连线
for (let i = index + 1; i < this.points.length; i++) {
const p = this.points[i];
// 计算两点之间的距离
const d = Math.sqrt(Math.pow(point.x - p.x, 2) + Math.pow(point.y - p.y, 2));
// 超过最大距离,则不绘制连线
if (d > this.maxDistance) continue;
// 绘制连线
ctx.beginPath();
ctx.moveTo(point.x, point.y);
ctx.lineTo(p.x, p.y);
ctx.closePath();
ctx.strokeStyle = `rgba(200, 200, 200, ${1 - d / this.maxDistance})`;
ctx.stroke();
}
});
}
}
const graph = new Graph();
graph.draw();
body {
width: 100%;
height: 500px;
margin: 0;
padding: 0;
}
#canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #222;
}