1.1 绘制矩形

2 阅读11分钟

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()清理矩形透明擦除无(不依赖任何样式)清空画布、擦除局部内容、动画刷新

四、路径绘制矩形

你的总结非常准确!fillRectstrokeRectclearRect 是 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;
}

性能建议

  • 批量绘制:尽量减少 fillRectstrokeRect 的调用次数
  • 使用路径:绘制大量相同样式矩形时,用路径批量处理
  • 避免频繁样式切换:相同样式的矩形一起绘制
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');
}
注意事项
  1. 路径有顶点限制:浏览器对单个路径的顶点数量有限制(通常数万个),超大规模需要分批
  2. 相同样式才能批量:不同填充色/描边色的矩形必须分开批次
  3. 路径会累积:记得在每批次前调用 beginPath()
  4. 权衡复杂度:极简单的场景(几个矩形)独立调用更清晰,批量路径适合大量重复图形

五、完整演示代码(可直接运行)

你复制这段代码,打开浏览器就能看到三者的直观区别:

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

效果说明:

  1. 左边是实心蓝色矩形(fillRect)
  2. 右边是红色空心边框(strokeRect)
  3. 左边矩形中间被擦除成透明(clearRect),能看到画布背景

六、关键补充知识点

  1. 样式优先级

    • fillRect 只受 fillStyle 影响
    • strokeRect 只受 strokeStyle/lineWidth 影响
    • clearRect 不受任何样式影响,永远是透明擦除
  2. 常用场景

    • 做表格、网格:用 strokeRect
    • 做按钮、色块:用 fillRect
    • 做动画、重绘:用 clearRect(0,0,画布宽,画布高) 清空整个画布

总结

  1. fillRect:画实心矩形,靠 fillStyle 上色
  2. strokeRect:画空心边框,靠 strokeStyle 定边框样式
  3. clearRect:擦除矩形区域,无样式,直接变透明 三者参数完全一致,核心区别就是填充、描边、擦除三个功能。

七、strokeRect 线宽(lineWidth)占用规则

这是 Canvas 里最容易踩坑的点!我用最简单的方式给你讲清楚:线宽会向内外两侧同时延伸,不是只画在矩形边框内侧,也不是只画在外侧。

1.核心结论(必背)

  1. strokeRect 绘制的边框,线宽会均匀分布在矩形轮廓的内侧 + 外侧
  2. 比如设置 lineWidth = 4
    • 矩形轮廓内侧占 2px
    • 矩形轮廓外侧占 2px
  3. 最终矩形的实际占用尺寸 = 你设置的 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内侧占外侧占实际尺寸增加
1px0.5px0.5px宽高各 +1px
2px1px1px宽高各 +2px
10px5px5px宽高各 +10px

总结
  1. strokeRect 线宽 = 内侧50% + 外侧50%
  2. 实际占用尺寸 = 你设置的宽高 + 线宽
  3. 画 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>