Canvas从入门到实战:核心API、避坑指南与实战案例

0 阅读9分钟

游戏、数据可视化、动画开发必备!一篇搞定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/squarectx.lineCap = 'round';
lineJoin线条连接处样式(miter/round/bevelctx.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/rightctx.textAlign = 'center';
textBaseline垂直对齐(top/middle/bottom/alphabeticctx.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>

chrome-capture-2026-03-12.gif

案例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>

ScreenShot_2026-03-12_102924_760.png

五、总结

  1. 核心避坑:画布宽高用HTML属性、获取上下文先判断、同步思想、边框宽度避免小数、没有选择器;
  2. 核心API:上下文ctx是画笔,掌握路径绘制、样式设置、状态管理、图片绘制、像素操作是关键;
  3. 实战建议:从简单图形(矩形、圆形)入手,再尝试动画和图像处理,配合requestAnimationFrame实现流畅动画。

你用Canvas做过哪些有趣的项目?评论区分享一下,抽3人送《Canvas核心API速查表》~