Canvas 矩形绘制三方法详解
Canvas 提供了填充矩形、描边矩形、清理矩形三个核心矩形绘制方法,它们的作用、视觉效果、使用场景完全不同,我用最直观的方式给你对比清楚。
一、基础语法(统一参数)
三个方法的参数完全一样:
方法名(x, y, width, height)
x:矩形左上角在画布的 X 坐标y:矩形左上角在画布的 Y 坐标width:矩形宽度(正数向右,负数向左)height:矩形高度(正数向下,负数向上)
二、三个方法的核心区别
1. fillRect(x, y, w, h) —— 填充矩形
作用:绘制一个实心、有颜色的矩形(内部被填满)。
必须搭配:ctx.fillStyle 设置填充颜色(默认黑色)。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 1. 填充矩形
ctx.fillStyle = 'blue'; // 设置填充色
ctx.fillRect(50, 50, 100, 80); // 实心蓝矩形
2. strokeRect(x, y, w, h) —— 描边矩形
作用:绘制一个只有边框、空心的矩形(内部透明)。
必须搭配:ctx.strokeStyle(描边颜色)、ctx.lineWidth(边框粗细)。
// 2. 描边矩形
ctx.strokeStyle = 'red'; // 边框颜色
ctx.lineWidth = 3; // 边框宽度
ctx.strokeRect(200, 50, 100, 80); // 红色空心边框矩形
3. clearRect(x, y, w, h) —— 清理矩形
作用:擦除画布上指定区域,变成完全透明(不是白色!)。 特点:不需要设置任何样式,直接调用即可生效。
// 3. 清理矩形
ctx.clearRect(75, 75, 50, 40);
// 擦除第一个蓝色实心矩形中间的一块区域
三、直观对比总结表
| 方法 | 功能 | 视觉效果 | 依赖样式 | 核心用途 |
|---|---|---|---|---|
fillRect() | 填充矩形 | 实心色块 | fillStyle | 绘制实心图形、背景、色块 |
strokeRect() | 描边矩形 | 空心边框 | strokeStyle/lineWidth | 绘制边框、轮廓、表格线 |
clearRect() | 清理矩形 | 透明擦除 | 无(不依赖任何样式) | 清空画布、擦除局部内容、动画刷新 |
四、路径绘制矩形
你的总结非常准确!fillRect、strokeRect 和 clearRect 是 Canvas 2D API 中最基础、最常用的三个矩形绘制方法。
在此基础上,我再补充一些相关的绘制技巧和高级方法:
矩形路径方法
// 使用路径绘制矩形(更灵活)
ctx.rect(x, y, width, height);
ctx.fill(); // 填充
ctx.stroke(); // 描边
// 示例:绘制带独立样式的矩形
ctx.beginPath();
ctx.rect(50, 50, 100, 80);
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fill();
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.stroke();
带圆角的矩形
Canvas 原生没有直接提供圆角矩形方法,但可以通过 roundRect 方法(较新浏览器支持)或手动绘制路径实现:
方法一:使用 roundRect(现代浏览器)
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 检查是否支持
if (ctx.roundRect) {
ctx.roundRect(20, 20, 150, 100, 20); // 最后一个参数是圆角半径
ctx.fill();
} else {
// 降级方案
drawRoundRect(ctx, 20, 20, 150, 100, 20);
}
// 自定义圆角矩形函数
function drawRoundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
return ctx;
}
性能建议
- 批量绘制:尽量减少
fillRect和strokeRect的调用次数 - 使用路径:绘制大量相同样式矩形时,用路径批量处理
- 避免频繁样式切换:相同样式的矩形一起绘制
1. 基础原理:路径 vs 独立调用
❌ 低效方式(多次调用)
// 绘制100个矩形,每个都单独调用fillRect
for (let i = 0; i < 100; i++) {
ctx.fillStyle = 'blue'; // 实际上只需要设置一次
ctx.fillRect(i * 10, 0, 8, 100); // 100次渲染调用
}
✅ 高效方式(路径批量绘制)
// 先用路径记录所有矩形
ctx.beginPath();
for (let i = 0; i < 100; i++) {
ctx.rect(i * 10, 0, 8, 100); // 只记录路径,不渲染
}
ctx.fillStyle = 'blue';
ctx.fill(); // 一次性渲染所有矩形
2. 实际对比示例
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 方法1:独立绘制(慢)
console.time('独立绘制');
for (let i = 0; i < 1000; i++) {
ctx.fillStyle = `hsl(${i % 360}, 100%, 50%)`;
ctx.fillRect(Math.random() * 400, Math.random() * 400, 10, 10);
}
console.timeEnd('独立绘制');
// 方法2:批量路径绘制(快)
console.time('批量路径');
ctx.beginPath();
for (let i = 0; i < 1000; i++) {
ctx.rect(Math.random() * 400, Math.random() * 400, 10, 10);
}
ctx.fill(); // 注意:所有矩形会是相同颜色
console.timeEnd('批量路径');
3. 不同颜色的批量处理
如果需要不同颜色,可以按颜色分组:
// 按颜色分组绘制
const redRects = [[10,10,50,50], [100,20,30,40]];
const blueRects = [[200,30,40,50], [300,50,60,70]];
// 一次性绘制所有红色矩形
ctx.beginPath();
redRects.forEach(rect => ctx.rect(...rect));
ctx.fillStyle = 'red';
ctx.fill();
// 一次性绘制所有蓝色矩形
ctx.beginPath();
blueRects.forEach(rect => ctx.rect(...rect));
ctx.fillStyle = 'blue';
ctx.fill();
4. 实际应用:游戏格子地图
// 绘制网格地图(100x100格子)
const gridSize = 10;
const map = Array(100).fill().map(() => Array(100).fill().map(() => Math.random() > 0.8 ? 1 : 0));
// ❌ 低效:10,000次fillRect调用
for (let y = 0; y < 100; y++) {
for (let x = 0; x < 100; x++) {
if (map[y][x] === 1) {
ctx.fillStyle = 'green';
ctx.fillRect(x * gridSize, y * gridSize, gridSize - 1, gridSize - 1);
}
}
}
// ✅ 高效:只调用1次fill
ctx.beginPath();
for (let y = 0; y < 100; y++) {
for (let x = 0; x < 100; x++) {
if (map[y][x] === 1) {
ctx.rect(x * gridSize, y * gridSize, gridSize - 1, gridSize - 1);
}
}
}
ctx.fillStyle = 'green';
ctx.fill(); // 一次性渲染所有格子
5. 描边矩形的批量处理
// 批量绘制边框矩形
ctx.beginPath();
for (let i = 0; i < 500; i++) {
ctx.rect(i * 2, 0, 1, canvas.height);
}
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.stroke(); // 一次性描边所有矩形
// 注意:同时需要填充和描边时,可以分开处理
ctx.beginPath();
// ... 添加所有矩形路径
ctx.fill(); // 先填充
ctx.stroke(); // 后描边(同一个路径会重复使用)
6. 使用 ImageData 实现极致性能
对于像素级的批量绘制,可以用 ImageData:
// 极高性能:直接操作像素数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 批量设置像素(绘制多个矩形)
for (let rect of rectangles) {
for (let y = rect.y; y < rect.y + rect.h; y++) {
for (let x = rect.x; x < rect.x + rect.w; x++) {
const index = (y * canvas.width + x) * 4;
data[index] = 255; // R
data[index + 1] = 0; // G
data[index + 2] = 0; // B
data[index + 3] = 255; // A
}
}
}
ctx.putImageData(imageData, 0, 0); // 一次性渲染
7. 性能对比测试
function testPerformance() {
const iterations = 5000;
// 测试1:独立调用
console.time('独立fillRect');
for (let i = 0; i < iterations; i++) {
ctx.fillRect(0, 0, 10, 10);
}
console.timeEnd('独立fillRect');
// 测试2:批量路径
console.time('批量rect+fill');
ctx.beginPath();
for (let i = 0; i < iterations; i++) {
ctx.rect(0, 0, 10, 10);
}
ctx.fill();
console.timeEnd('批量rect+fill');
// 测试3:ImageData(最快)
console.time('ImageData');
const imgData = ctx.getImageData(0, 0, 10, 10);
for (let i = 0; i < iterations; i++) {
// 操作像素数据
}
ctx.putImageData(imgData, 0, 0);
console.timeEnd('ImageData');
}
注意事项
- 路径有顶点限制:浏览器对单个路径的顶点数量有限制(通常数万个),超大规模需要分批
- 相同样式才能批量:不同填充色/描边色的矩形必须分开批次
- 路径会累积:记得在每批次前调用
beginPath() - 权衡复杂度:极简单的场景(几个矩形)独立调用更清晰,批量路径适合大量重复图形
五、完整演示代码(可直接运行)
你复制这段代码,打开浏览器就能看到三者的直观区别:
<canvas id="myCanvas" width="400" height="200" style="border:1px solid #000;"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
canvas.width = 800;
canvas.height = 800;
const ctx = canvas.getContext('2d');
// 1. 填充矩形:实心蓝色
ctx.fillStyle = '#ADD8E6';
ctx.fillRect(100, 100, 100, 100);
// 2. 描边矩形:红色边框
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 10;
ctx.strokeRect(300, 100, 100, 100);
// 3. 清理矩形:擦除中间区域(第一个绘制矩形的内容)
ctx.clearRect(110, 110, 80, 80);
// 设置线条宽度为1px
ctx.lineWidth = 1;
// 关键:坐标加0.5,让线条中线对齐像素中心
// 起点和终点的坐标都需要加上0.5
ctx.beginPath();
ctx.moveTo(500, 100);
ctx.lineTo(600, 100);
ctx.moveTo(500.5, 200.5); // 起点坐标加0.5
ctx.lineTo(600.5, 200.5); // 终点坐标加0.5
ctx.strokeStyle = '#000';
ctx.stroke();
// 4.路径绘制矩形
ctx.beginPath();
ctx.moveTo(300, 300);
ctx.lineTo(400, 300);
ctx.lineTo(400, 400);
ctx.lineTo(300, 400);
ctx.fillStyle = '#ADD8E6';
ctx.fill();
// 5.圆角矩形
ctx.fillStyle = '#ff0000';
ctx.roundRect(600, 600, 100, 100, 20); // 最后一个参数是圆角半径
ctx.fill();
</script>
效果说明:
- 左边是实心蓝色矩形(fillRect)
- 右边是红色空心边框(strokeRect)
- 左边矩形中间被擦除成透明(clearRect),能看到画布背景
六、关键补充知识点
-
样式优先级
fillRect只受fillStyle影响strokeRect只受strokeStyle/lineWidth影响clearRect不受任何样式影响,永远是透明擦除
-
常用场景
- 做表格、网格:用
strokeRect - 做按钮、色块:用
fillRect - 做动画、重绘:用
clearRect(0,0,画布宽,画布高)清空整个画布
- 做表格、网格:用
总结
fillRect:画实心矩形,靠fillStyle上色strokeRect:画空心边框,靠strokeStyle定边框样式clearRect:擦除矩形区域,无样式,直接变透明 三者参数完全一致,核心区别就是填充、描边、擦除三个功能。
七、strokeRect 线宽(lineWidth)占用规则
这是 Canvas 里最容易踩坑的点!我用最简单的方式给你讲清楚:线宽会向内外两侧同时延伸,不是只画在矩形边框内侧,也不是只画在外侧。
1.核心结论(必背)
strokeRect绘制的边框,线宽会均匀分布在矩形轮廓的内侧 + 外侧- 比如设置
lineWidth = 4- 矩形轮廓内侧占 2px
- 矩形轮廓外侧占 2px
- 最终矩形的实际占用尺寸 = 你设置的 w/h + lineWidth
2.直观计算示例
你画一个矩形:
ctx.lineWidth = 4; // 线宽 4px
ctx.strokeRect(10,10, 50,50); // 坐标(10,10),宽高50
实际占用空间:
- 左边框:从
10 - 2 = 8开始 - 右边框:到
10 + 50 + 2 = 62结束 - 上边框:从
10 - 2 = 8开始 - 下边框:到
10 + 50 + 2 = 62结束
✅ 实际视觉大小:54px × 54px
(比你设置的 50px 宽高多了一圈线宽)
3. 最常见的特殊情况:lineWidth = 1(细线)
ctx.lineWidth = 1;
ctx.strokeRect(10,10,50,50);
- 线宽 1px → 内侧 0.5px + 外侧 0.5px
- 因为屏幕最小单位是 1px,所以会模糊渲染
- 想画清晰的 1px 细线:坐标 +0.5
ctx.strokeRect(10.5,10.5,50,50);
4.一句话总结
strokeRect 的线宽 一半在内、一半在外,会让矩形整体变大。
| lineWidth | 内侧占 | 外侧占 | 实际尺寸增加 |
|---|---|---|---|
| 1px | 0.5px | 0.5px | 宽高各 +1px |
| 2px | 1px | 1px | 宽高各 +2px |
| 10px | 5px | 5px | 宽高各 +10px |
总结
strokeRect线宽 = 内侧50% + 外侧50%- 实际占用尺寸 = 你设置的宽高 + 线宽
- 画 1px 细线时,坐标 +0.5 才会清晰不模糊
这个问题问到了 Canvas 渲染最本质的像素原理,我用最简单、最直观的方式给你讲明白:
七、为什么 1px 线条要 +0.5 才清晰?
一句话结论: 因为 Canvas 的坐标点,落在像素格子的「缝隙」上,而不是格子中心。
1. 先看屏幕像素是怎么排列的
你可以把屏幕想象成棋盘格:
(0,0) (1,0) (2,0)
┌─────────┬─────────┬───
│ │ │
│ 像素0,0 │ 像素1,0 │
│ │ │
(0,1)├────────┼─────────┼───
│ │ │
│ 像素0,1 │ 像素1,1 │
重点:
- 坐标 (x,y) 是像素格子的角点
- 不是像素中心!
2. 画 1px 线条时发生了什么?
Canvas 画线规则: 线条以坐标为中心,向左右各延伸 0.5px
比如你画:
ctx.lineWidth = 1
ctx.strokeRect(10, 10, 50, 50)
边框线会从:
- 左:10.0 向左 0.5px → 9.5
- 右:10.0 向右 0.5px → 10.5
结果: 线条跨在两个像素中间!
- 一半在 9 号像素
- 一半在 10 号像素
浏览器只能做一件事: 👉 两个像素都半透明模糊显示 于是你看到的就是:发虚、模糊、灰线
3. 为什么 +0.5 就清晰了?
当你写:
ctx.strokeRect(10.5, 10.5, 50, 50)
线条中心在 10.5
- 向左 0.5px → 10.0
- 向右 0.5px → 11.0
刚好完美占满第 10 号像素格子!
没有跨像素,没有半透明 → 显示为清晰锐利的 1px 实线
4. 超直观总结
- 坐标是整数 → 线落在像素缝隙 → 模糊
- 坐标是 x.5 → 线落在像素正中心 → 清晰
| 写法 | 线条位置 | 效果 |
|---|---|---|
| strokeRect(10,10,w,h) | 跨两个像素 | 模糊、发灰 |
| strokeRect(10.5,10.5,w,h) | 刚好填满一个像素 | 清晰、锐利 |
5. 只有 1px 线需要这样吗?
是的!
- lineWidth = 1 → 必须 +0.5
- lineWidth ≥ 2 → 不需要 因为偶数宽度可以刚好对齐整数坐标,不会跨像素。
最终一句话记忆
Canvas 坐标在像素角,1px 线要放在像素中心,所以 +0.5。
需要我给你写一段对比代码,一眼看清模糊 vs 清晰吗?
八、案例
<canvas id="myCanvas"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
canvas.width = 800;
canvas.height = 800;
const ctx = canvas.getContext('2d');
// 1. 填充矩形:实心蓝色
ctx.fillStyle = '#ADD8E6';
ctx.fillRect(100, 100, 100, 100);
// 2. 描边矩形:红色边框
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 10;
ctx.strokeRect(300, 100, 100, 100);
// 3. 清理矩形:擦除中间区域(第一个绘制矩形的内容)
ctx.clearRect(110, 110, 80, 80);
// 设置线条宽度为1px
ctx.lineWidth = 1;
// 关键:坐标加0.5,让线条中线对齐像素中心
// 起点和终点的坐标都需要加上0.5
ctx.beginPath();
ctx.moveTo(500, 100);
ctx.lineTo(600, 100);
ctx.moveTo(500.5, 200.5); // 起点坐标加0.5
ctx.lineTo(600.5, 200.5); // 终点坐标加0.5
ctx.strokeStyle = '#000';
ctx.stroke();
// 4.路径绘制矩形
ctx.beginPath();
ctx.moveTo(300, 300);
ctx.lineTo(400, 300);
ctx.lineTo(400, 400);
ctx.lineTo(300, 400);
ctx.fillStyle = '#ADD8E6';
ctx.fill();
// 5.圆角矩形
ctx.fillStyle = '#ff0000';
ctx.roundRect(600, 600, 100, 100, 20); // 最后一个参数是圆角半径
ctx.fill();
</script>