游戏、数据可视化、动画开发必备!一篇搞定Canvas核心知识点,附避坑指南与可直接运行的实战代码
前言
Canvas是HTML5提供的「画布」元素,能通过JS绘制2D/3D图形,广泛应用于游戏开发、数据可视化、海报生成、动画效果等场景。本文从「入门避坑」到「核心API」再到「实战案例」,手把手带你掌握Canvas开发,代码可直接复制运行~
一、Canvas入门必看:5个核心避坑点
新手最容易踩的坑,提前避开能节省80%的调试时间!
1. 同步思想:Canvas渲染极快,无延迟
Canvas的渲染是同步执行的,代码执行完立即渲染,不会出现「代码覆盖后延迟渲染」的问题。
开发原则:按「设置样式→绘制图形→清除画布→重新绘制」的同步流程写代码。
2. 获取上下文前,先做兼容判断
const canvas = document.getElementById('myCanvas');
// ⚠️ 必加:判断浏览器是否支持Canvas
if (!canvas.getContext) {
alert('您的浏览器不支持Canvas,请升级!');
} else {
const ctx = canvas.getContext('2d'); // 获取2D上下文
// 后续绘制代码...
}
3. 画布宽高:用HTML属性,别用CSS!
坑点:Canvas默认宽高300*150px,若用CSS定义宽高,会缩放画布内的图像,导致图形模糊/变形。
正确做法:用HTML的width/height属性定义画布尺寸:
<!-- ✅ 正确:HTML属性定义宽高 -->
<canvas id="myCanvas" width="600" height="400"></canvas>
<!-- ❌ 错误:CSS定义会缩放图像 -->
<canvas id="myCanvas" style="width:600px;height:400px;"></canvas>
4. 绘制矩形的坑:边框宽度的小数问题
Canvas的边框宽度是「在偏移量上下分别渲染一半」,若边框宽度为奇数(如1px),可能出现小数边框,浏览器会自动向上取整,导致边框模糊。
解决:尽量用偶数边框宽度,或调整偏移量(如x/y加0.5px):
const ctx = canvas.getContext('2d');
ctx.lineWidth = 2; // ✅ 偶数边框,清晰
ctx.strokeRect(10, 10, 100, 50);
// 若必须用1px边框,调整偏移量
ctx.lineWidth = 1;
ctx.strokeRect(10.5, 70.5, 100, 50); // ✅ 加0.5px,避免小数
5. 没有选择器:Canvas内的图形无法用CSS控制
Canvas是「位图」,绘制的图形只是像素点,无法用CSS选择器选中,也无法直接修改样式。若要修改图形,需「清除画布→重新绘制」。
二、画布核心API(基础操作)
| API | 作用 | 代码示例 |
|---|---|---|
canvas.getContext('2d') | 获取2D绘制上下文 | const ctx = canvas.getContext('2d'); |
canvas.width / canvas.height | 获取/设置画布宽高(HTML属性形式) | canvas.width = 600; canvas.height = 400; |
canvas.toDataURL() | 将画布转为图片地址(base64格式) | const imgUrl = canvas.toDataURL('image/png'); |
三、上下文核心API(绘制主力)
上下文ctx是Canvas的「画笔」,所有绘制操作都通过它完成,按功能分类整理如下:
1. 矩形绘制(Canvas唯一支持直接渲染的图形)
| API | 作用 | 代码示例 |
|---|---|---|
fillRect(x, y, w, h) | 填充矩形 | ctx.fillRect(10, 10, 100, 50); |
strokeRect(x, y, w, h) | 带边框的矩形 | ctx.strokeRect(10, 70, 100, 50); |
clearRect(x, y, w, h) | 清除矩形区域(常用作清空画布) | ctx.clearRect(0, 0, canvas.width, canvas.height); |
2. 样式设置
| API | 作用 | 代码示例 |
|---|---|---|
fillStyle | 填充颜色/渐变/图案 | ctx.fillStyle = '#165DFF'; |
strokeStyle | 线条颜色 | ctx.strokeStyle = '#f44336'; |
lineWidth | 线条宽度 | ctx.lineWidth = 2; |
lineCap | 线条两端样式(butt/round/square) | ctx.lineCap = 'round'; |
lineJoin | 线条连接处样式(miter/round/bevel) | ctx.lineJoin = 'round'; |
进阶:渐变与图案填充
// 1. 线性渐变
const linearGradient = ctx.createLinearGradient(0, 0, 200, 0); // 起点(x1,y1)、终点(x2,y2)
linearGradient.addColorStop(0, '#165DFF'); // 起始颜色
linearGradient.addColorStop(1, '#722ED1'); // 结束颜色
ctx.fillStyle = linearGradient;
ctx.fillRect(10, 10, 200, 50);
// 2. 径向渐变
const radialGradient = ctx.createRadialGradient(100, 100, 10, 100, 100, 50); // 内圆(x1,y1,r1)、外圆(x2,y2,r2)
radialGradient.addColorStop(0, '#fff');
radialGradient.addColorStop(1, '#165DFF');
ctx.fillStyle = radialGradient;
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fill();
// 3. 图案填充(图片加载完成后操作)
const img = new Image();
img.src = 'pattern.jpg';
img.onload = () => {
const pattern = ctx.createPattern(img, 'repeat'); // repeat/repeat-x/repeat-y/no-repeat
ctx.fillStyle = pattern;
ctx.fillRect(10, 10, 200, 200);
};
3. 路径绘制(核心!绘制复杂图形)
路径是Canvas的「灵魂」,通过「起点→连线→闭合→填充/描边」的流程绘制图形:
| API | 作用 | 代码示例 |
|---|---|---|
beginPath() | 清除路径容器(开始新路径,必加!) | ctx.beginPath(); |
closePath() | 闭合路径(自动连接起点和终点) | ctx.closePath(); |
moveTo(x, y) | 移动画笔到起点 | ctx.moveTo(10, 10); |
lineTo(x, y) | 绘制直线到终点 | ctx.lineTo(100, 10); |
rect(x, y, w, h) | 绘制矩形路径(需配合fill/stroke) | ctx.rect(10, 10, 100, 50); |
arc(x, y, r, startDeg, endDeg, dir) | 绘制圆弧/圆形(dir:true逆时针/false顺时针) | ctx.arc(100, 100, 50, 0, Math.PI * 2); |
arcTo(x1, y1, x2, y2, r) | 绘制圆角路径(结合moveTo) | ctx.moveTo(10, 10); ctx.arcTo(100, 10, 100, 50, 10); |
quadraticCurveTo(x1, y1, x2, y2) | 二次贝塞尔曲线(1个控制点) | ctx.moveTo(10, 10); ctx.quadraticCurveTo(50, 100, 100, 10); |
bezierCurveTo(x1, y1, x2, y2, x3, y3) | 三次贝塞尔曲线(2个控制点) | ctx.moveTo(10, 10); ctx.bezierCurveTo(30, 100, 70, 0, 100, 10); |
fill() | 填充路径 | ctx.fill(); |
stroke() | 描边路径 | ctx.stroke(); |
示例:绘制圆形
ctx.beginPath(); // 开始新路径
ctx.arc(100, 100, 50, 0, Math.PI * 2); // 圆心(x,y)、半径r、起始角度、结束角度(弧度制)
ctx.fillStyle = '#165DFF';
ctx.fill(); // 填充
ctx.strokeStyle = '#f44336';
ctx.lineWidth = 2;
ctx.stroke(); // 描边
4. 状态管理(save/restore,样式隔离神器)
save():将当前样式(fillStyle/strokeStyle/lineWidth等)、变换(translate/rotate/scale等)压入「样式栈」;
restore():将样式栈顶的状态弹出,恢复到之前的状态。
作用:避免样式污染,实现「局部样式修改」。
ctx.fillStyle = '#165DFF';
ctx.fillRect(10, 10, 50, 50); // 蓝色矩形
ctx.save(); // 保存当前状态(蓝色)
ctx.fillStyle = '#f44336';
ctx.fillRect(70, 10, 50, 50); // 红色矩形
ctx.restore(); // 恢复到之前的状态(蓝色)
ctx.fillRect(130, 10, 50, 50); // 蓝色矩形(不受红色影响)
5. 坐标变换(translate/rotate/scale)
| API | 作用 | 代码示例 |
|---|---|---|
translate(x, y) | 平移原点(默认原点在左上角) | ctx.translate(100, 100); |
rotate(弧度) | 旋转坐标轴(顺时针为正,弧度=角度*Math.PI/180) | ctx.rotate(Math.PI / 4); |
scale(xFactor, yFactor) | 缩放画布(>1放大,<1缩小) | ctx.scale(1.5, 1.5); |
注意:变换是「累积」的,建议配合save()/restore()使用:
ctx.save();
ctx.translate(100, 100); // 平移原点到(100,100)
ctx.rotate(Math.PI / 4); // 旋转45度
ctx.fillRect(0, 0, 50, 50); // 绘制旋转后的矩形
ctx.restore(); // 恢复原点和旋转
6. 图片绘制(drawImage,注意图片加载完成!)
const img = new Image();
img.src = 'image.jpg';
// ⚠️ 必加:图片加载完成后再绘制
img.onload = () => {
// 用法1:drawImage(img, x, y)
ctx.drawImage(img, 10, 10);
// 用法2:drawImage(img, x, y, w, h)(缩放绘制)
ctx.drawImage(img, 10, 100, 100, 80);
// 用法3:drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)(裁剪+缩放绘制)
// sx/sy/sw/sh:原图裁剪区域;dx/dy/dw/dh:画布绘制区域
ctx.drawImage(img, 50, 50, 100, 80, 10, 200, 100, 80);
};
7. 文字绘制
| API | 作用 | 代码示例 |
|---|---|---|
fillText(text, x, y, maxWidth) | 填充文字 | ctx.fillText('Hello Canvas', 10, 50); |
strokeText(text, x, y, maxWidth) | 描边文字 | ctx.strokeText('Hello Canvas', 10, 100); |
font | 文字样式(同CSS font属性) | ctx.font = 'bold 24px Arial'; |
textAlign | 水平对齐(left/center/right) | ctx.textAlign = 'center'; |
textBaseline | 垂直对齐(top/middle/bottom/alphabetic) | ctx.textBaseline = 'middle'; |
measureText(text) | 获取文字宽度(返回对象) | const width = ctx.measureText('Hello Canvas').width; |
示例:带阴影的文字
ctx.font = 'bold 32px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 阴影设置(shadowColor必需!)
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 4;
ctx.fillStyle = '#165DFF';
ctx.fillText('Hello Canvas', canvas.width / 2, canvas.height / 2);
8. 像素操作(getImageData/putImageData,图像处理核心)
| API | 作用 | 代码示例 |
|---|---|---|
getImageData(x, y, w, h) | 获取像素数据(返回ImageData对象) | const imgData = ctx.getImageData(0, 0, 100, 100); |
putImageData(imgData, x, y) | 绘制像素数据到画布 | ctx.putImageData(imgData, 10, 10); |
createImageData(w, h) | 创建空像素数据(默认透明) | const newImgData = ctx.createImageData(100, 100); |
ImageData对象:
width/height:选中区域的宽高;data:数组,包含所有像素的RGBA信息(每4个值对应1个像素:R(0-255)、G(0-255)、B(0-255)、A(0-255))。
示例:图片灰度处理
const img = new Image();
img.src = 'image.jpg';
img.onload = () => {
ctx.drawImage(img, 0, 0);
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imgData.data;
// 遍历每个像素,转为灰度
for (let i = 0; i < data.length; i += 4) {
const gray = (data[i] + data[i + 1] + data[i + 2]) / 3; // 灰度公式
data[i] = gray; // R
data[i + 1] = gray; // G
data[i + 2] = gray; // B
// A保持不变
}
ctx.putImageData(imgData, 0, 0); // 绘制处理后的像素
};
9. 全局设置
| API | 作用 | 代码示例 |
|---|---|---|
globalAlpha | 全局透明度(0-1) | ctx.globalAlpha = 0.5; |
globalCompositeOperation | 图形合成方式(源图像=新绘制的,目标图像=已有的) | ctx.globalCompositeOperation = 'source-over'; |
合成方式常用值:
| 值 | 作用 |
|---|---|
source-over(默认) | 源图像在目标图像上方 |
source-in | 只保留源图像与目标图像的重叠部分(源图像部分) |
source-out | 只保留源图像超过目标图像的部分 |
destination-over | 目标图像在源图像上方 |
destination-in | 只保留源图像与目标图像的重叠部分(目标图像部分) |
destination-out | 只保留目标图像超过源图像的部分 |
10. 路径检测
| API | 作用 | 代码示例 |
|---|---|---|
isPointInPath(x, y) | 判断点(x,y)是否在当前路径内 | const isIn = ctx.isPointInPath(100, 100); |
四、实战案例:动手写个Canvas小项目
案例1:简单的小球动画(结合requestAnimationFrame)
<canvas id="myCanvas" width="600" height="400"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
let x = 50; // 小球x坐标
let y = 200; // 小球y坐标
let vx = 2; // x方向速度
let radius = 20; // 小球半径
// 动画函数
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空画布
// 绘制小球
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = '#165DFF';
ctx.fill();
// 更新位置
x += vx;
// 边界检测(碰到画布边缘反弹)
if (x + radius > canvas.width || x - radius < 0) {
vx = -vx;
}
requestAnimationFrame(animate); // 循环调用动画函数
}
animate(); // 启动动画
</script>
案例2:绘制圆角矩形(结合arcTo)
<canvas id="myCanvas" width="600" height="400"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
function drawRoundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y); // 起点
ctx.lineTo(x + w - r, y); // 上边
ctx.arcTo(x + w, y, x + w, y + r, r); // 右上角圆角
ctx.lineTo(x + w, y + h - r); // 右边
ctx.arcTo(x + w, y + h, x + w - r, y + h, r); // 右下角圆角
ctx.lineTo(x + r, y + h); // 下边
ctx.arcTo(x, y + h, x, y + h - r, r); // 左下角圆角
ctx.lineTo(x, y + r); // 左边
ctx.arcTo(x, y, x + r, y, r); // 左上角圆角
ctx.closePath();
ctx.fillStyle = '#165DFF';
ctx.fill();
}
// 调用:绘制圆角矩形
drawRoundRect(ctx, 10, 10, 200, 100, 10);
</script>
五、总结
- 核心避坑:画布宽高用HTML属性、获取上下文先判断、同步思想、边框宽度避免小数、没有选择器;
- 核心API:上下文
ctx是画笔,掌握路径绘制、样式设置、状态管理、图片绘制、像素操作是关键; - 实战建议:从简单图形(矩形、圆形)入手,再尝试动画和图像处理,配合
requestAnimationFrame实现流畅动画。
你用Canvas做过哪些有趣的项目?评论区分享一下,抽3人送《Canvas核心API速查表》~