☰
Canvas画矩形三招:fillRect、strokeRect、rect用法与避坑指南
2026/10/7 10:53:54 网站建设 项目流程

前端萌新们,准备正式上手Canvas的时候,画矩形多半是绕不开的第一课。很多教程会把fillRect、strokeRect、rect这三个API简单列一遍就完事,看着都会,落笔就懵。作为Canvas里最基础也最高频的三个方法,画矩形这三招不仅撑起你后面的90%图形开发,还藏着不少新手必踩的坑:坐标反直觉、高清屏发虚、边框线被切掉一半。这篇内容我打算把三招的用法、参数背后的逻辑、以及我自己在真实项目里踩过的坑一次讲透,让你看完就能动手写,并且知道为什么这么写。适合刚学完HTML/CSS/JS基础、准备啃Canvas的萌新,也适合想在面试前快速梳理Canvas基础点的同学。

1. 开写之前,先搞懂Canvas坐标系“反直觉”的脾气

画矩形之前,坐标系这一关必须过。因为Canvas走的是屏幕坐标系的规则:原点在左上角,x轴向右为正,y轴向下为正。这和我们在数学课上学到的笛卡尔坐标系刚好相反,y轴方向整个颠倒。很多新手第一次写出来的图形位置和想象差了十万八千里,几乎都是栽在这儿。

打个比方,你在Excel表格里填格子,是从左上角的A1开始,行号往下递增,列号往右递增,Canvas的坐标系就是这套逻辑。理解成“在一张坐标纸上定位格子”会非常顺,越往右下角,坐标值越大;越往左上角,坐标值越小。负数坐标也不是不能画,只是画在画布可视区域外面,不会显示出来而已。

还有一个必须区分的点:canvas标签上的width和height是绘图区域的“像素分辨率”,并不等同于写CSS时定义的宽高。比如:

<canvas id="demo" width="300" height="150"></canvas>

这张画布如果再用CSS设置成width: 600px; height: 300px;,浏览器会把300x150的绘图结果拉伸成600x300来展示,图像就会明显变虚。默认情况下,不写width和height的时候,canvas的绘图区域就是300x150,这个默认值让不少人困惑过。

另外要留意的是,Canvas把width或height属性重新赋值,会立刻清空整个画布,并且让所有绘图状态(比如fillStyle、strokeStyle、lineWidth)恢复到初始值。这个行为在后面做动态重绘的时候会反复用到,先把“设置尺寸会清屏”这个脾气记住,后面遇到“为什么我改了画布大小内容全没了”这种问题时,你就不会慌。

搞懂坐标系和尺寸机制,咱们再回来看画矩形的主菜。矩形在Canvas里之所以重要,是因为绝大多数图形都能拆成矩形的组合来做,比如进度条、柱状图、头像裁剪框、地图框选,底层都是这三个方法在轮流干活。接下来三招逐一上桌。

2. Canvas画矩形的三招:fillRect、strokeRect、rect

画一个矩形,Canvas就给了三个方法,很多萌新会疑惑:为什么不能一个方法走天下?其实这三个API的设计分工很清晰:两个是“便捷绘制”,一个是“路径构建”。弄明白它们之间的区别,你的Canvas才算真正入了门。

2.1 第一招:fillRect,一步画出实心矩形

fillRect(x, y, width, height),四个参数分别是矩形的左上角x坐标、左上角y坐标、宽度、高度。它的作用非常直接:立即在画布上填充一个实心矩形,不需要额外调用fill()。

const canvas = document.getElementById('demo'); const ctx = canvas.getContext('2d'); ctx.fillStyle = '#4f8cff'; ctx.fillRect(20, 20, 160, 90);

这段代码会在距离左上角(20, 20)的位置,画出一个宽160、高90的蓝色实心矩形。这里注意两点:

第一,fillStyle要写在fillRect前面。它是画笔的状态,决定了接下来所有填充操作的颜色。如果你把颜色设置写在填充之后,这一次绘制的颜色不会变,但下一次就会用新颜色。

第二,fillRect是一个“原子操作”,它不会进入当前的路径系统。也就是说,即使你之前用beginPath开始了一段路径,fillRect也不会加入这段路径,它是独立的。

这一点特别重要,因为有些萌新会误以为fillRect等价于“rect + fill”,然后试图通过它来影响已有路径。其实fillRect是快照式的“画了就完”,而路径相关的rect才是“先记下来再统一处理”。理解了这个,就不会在后面的组合图形中踩坑。

2.2 第二招:strokeRect,只描边、不填充

strokeRect(x, y, width, height)的参数和fillRect完全一样,区别在于它只画矩形的边框,内部是透明的。

ctx.strokeStyle = '#f56565'; ctx.lineWidth = 2; ctx.strokeRect(60, 40, 120, 80);

这里用strokeStyle设置边框颜色,用lineWidth设置线条粗细,然后strokeRect就会沿着矩形边缘描一圈边框。

新手最容易忽略的点:strokeRect的边框是以你指定的坐标线为“中心线”向内外两侧各扩张一半线宽来绘制的。这意味着当lineWidth = 2时,左边框的一半厚度会画在矩形坐标线的外侧,另一半画在内侧。如果坐标刚好落在整数像素点上,还会出现边框被“削”掉一半或者颜色变淡的问题,这个细节我放到第三章的避坑指南里详细讲。

strokeRect和fillRect一样,只画一个矩形就结束,不进路径系统。所以你没法用strokeRect在同一个路径里连出多个矩形,再统一提交渲染——批量图形操作需要用第三招。

2.3 第三招:rect,进入路径系统的正主

rect(x, y, width, height)和前两招逻辑完全不同。它做的事情只是“把当前这个矩形追加到当前路径中”,不会直接绘制在画布上。真正让它显现出来,要靠后续的fill()或stroke()函数。

ctx.beginPath(); ctx.rect(30, 30, 100, 80); ctx.fillStyle = '#68d391'; ctx.fill(); ctx.beginPath(); ctx.rect(160, 30, 100, 80); ctx.strokeStyle = '#ed8936'; ctx.lineWidth = 3; ctx.stroke();

为什么要把rect单独拎出来当成一招?因为它让你能玩出前两招做不到的组合效果:同一个路径里加入多个矩形,一次性填充或者描边;配合clip()做裁剪区域;和moveTo、lineTo一起拼复杂形状;利用“非零环绕规则”做带空洞的图形。

这里的核心要点是beginPath()的使用。每当你准备开始定义一个新路径时,最好先调用beginPath()把之前的路径清掉,否则新旧路径会叠加在一起,渲染时出现意外的连线或插入内容。beginPath不会清理fillStyle、strokeStyle这些画笔状态,它只清空路径集合,两者要分清。

三招的适用场景也可以浓缩成一句话:想快速出结果、不需要路径参与,用fillRect或strokeRect;想构造复杂图形、组合同一批量渲染,或者需要裁剪、填充规则,就用rect。画矩形看着简单,但理解“立即绘制”和“路径积累”的区别,才不会被后面更复杂的Canvas代码绕晕。

3. 避坑指南:模糊、边框残缺、状态污染,一个都别放过

光会调用API只能算是“能画”,离“画得对”“画得细腻”还差着好几个排坑的过程。下面这几个问题,是我在实际项目里真实遇到过的,几乎每个都会在前端萌新的开发路上出现,单独拎出来讲清楚。

3.1 高清屏模糊:为什么画出来的矩形总是发虚

在非Retina屏幕上,直接设置canvas.width和CSS宽高相等,画出来的内容看着还行。但在高分屏(devicePixelRatio大于1)的设备上,你会发现画出的矩形边缘模糊,像是被打了一层轻微的马赛克。

原因在于逻辑像素和物理像素的分辨率不对等。CSS里的width: 300px是逻辑尺寸,而canvas.width = 300决定的是位图画布大小。高DPI屏幕上,浏览器为了把一个300逻辑像素的内容撑满物理300x3的屏幕,只能做插值放大,自然就虚了。

标准解法是“按设备像素比缩放画布”,也就是用两倍或三倍的缓冲区画图,再通过CSS把画布显示尺寸压回逻辑大小:

const canvas = document.getElementById('bar'); const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const cssWidth = 320; const cssHeight = 40; canvas.style.width = cssWidth + 'px'; canvas.style.height = cssHeight + 'px'; canvas.width = cssWidth * dpr; canvas.height = cssHeight * dpr; ctx.scale(dpr, dpr);

这段代码里,ctx.scale(dpr, dpr)很关键。它让后续所有绘图坐标仍然按CSS的逻辑像素来书写,不需要你手动去乘dpr。比如你在逻辑坐标(20, 20)画矩形,底层实际绘制的物理坐标是(60, 60),画出来的内容在高分屏上才够锐利。

这是我建议所有Canvas项目都要优先处理的一步,否则后面做图表、画板,都会被“模糊”问题反复折磨。很多前端面试题里也喜欢问“canvas在高清屏下变模糊怎么解决”,背答案不如自己动手调一次。

3.2 边框线被“切”掉一半:lineWidth和坐标对齐的坑

用strokeRect画边框时,我见过最多的情况是:明明画了一个矩形,结果左边框和上边框看起来比右边框和下边框细很多,甚至会消失一半。原因就是边框线宽朝外侧和内侧各有50%,而整数坐标落在了两个像素的分界线上。

举个例子,lineWidth = 2时,你执行:

ctx.strokeRect(10, 10, 100, 80);

左边框实际是沿着 x=10 这条线向左右各扩散1像素,也就是从 x=9 到 x=11。其中x=9到x=10属于一块屏幕像素,x=10到x=11属于另一块,两块各覆盖一半颜色,视觉上就会觉得颜色淡了一圈。

把坐标加上0.5,问题就会立刻缓解:

ctx.strokeRect(10.5, 10.5, 100, 80);

边框现在精确落在整数像素内部,左边从x=9.5画到x=10.5,其中x=10正好是像素边界,整个1像素宽度的线会完整覆盖一个物理像素列。这个方法在Retina屏上同样适用,前提是你已经用dpr做了缩放,否则0.5偏移会被放大,得重新计算。

另一个经验是:如果不需要特别细的边框,把lineWidth设成奇数(1、3、5)也会让边框正好落在整数像素网格上,视觉清晰度会好一些。如果需要偶数线宽,推荐优先使用0.5偏移方案。

3.3 状态污染:颜色设置一次,后面所有图形都被“传染”

Canvas的画笔状态是“粘滞”的。fillStyle一旦设置成某个颜色,之后所有fill操作都会用这个颜色,直到你再次修改它。很多萌新画完第一个红色矩形,接着画第二个,想当然以为第二个会自动是默认颜色,结果画出来还是红色。

演示一个真实的Bug场景:

ctx.fillStyle = '#e53e3e'; ctx.fillRect(0, 0, 100, 100); // 忘了重新设置 fillStyle ctx.fillRect(120, 0, 100, 100); // 这里仍然是红色

正确做法是每次绘制前都显式声明期望的颜色,或者用ctx.save()和ctx.restore()把状态包起来:

ctx.save(); ctx.fillStyle = '#e53e3e'; ctx.fillRect(0, 0, 100, 100); ctx.restore(); ctx.save(); ctx.fillStyle = '#3182ce'; ctx.fillRect(120, 0, 100, 100); ctx.restore();

save()会保存当前所有画笔状态(颜色、线宽、变换矩阵、裁剪区域等),restore()会把状态恢复回去。在复杂绘制场景里,这个习惯能帮你避开大量“状态串线”的Bug。

其实状态污染范围不止颜色,lineWidth、shadowBlur、globalAlpha、transform全部会“粘”下去。在Canvas开发中,我的习惯是:只要在一个绘制函数里临时改了状态,结束时一定用restore()还原,否则下一个绘制函数拿到的就是被污染过的画笔。

4. 实操演练:用三招做一个可复用的动态进度条

纸上谈兵讲完,接下来用一个常见的需求把三招串起来:动态进度条。这个需求在加载页、上传页、数据报告里到处都是,虽然看起来简单,却是把fillRect、strokeRect、rect、状态管理和高清屏适配全部结合起来的经典小项目。

4.1 需求拆解与骨架设计

进度条要满足这几个点:有一个背景条,一个按百分比填充的前景条,一条清晰的边框,以及一个动态更新的数值。核心思路很简单:背景用fillRect铺底,前景用fillRect根据进度比例计算宽度,边框用strokeRect勾线。

骨架先定下来,HTML只需要一个canvas元素:

<canvas id="progress" width="320" height="40"></canvas>

注意这里我先设置了width和height,但实际项目中更稳的做法是像第三章那样,用JS动态计算尺寸并适配dpr,避免高清屏模糊。下面完整代码就是按这个思路来的。

4.2 动态进度条:完整代码与关键点解析

function createProgress(canvasId) { const canvas = document.getElementById(canvasId); const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const cssWidth = 320; const cssHeight = 40; // 适配高清屏 canvas.style.width = cssWidth + 'px'; canvas.style.height = cssHeight + 'px'; canvas.width = cssWidth * dpr; canvas.height = cssHeight * dpr; ctx.scale(dpr, dpr); let progress = 0; function draw(value) { // 清空画布 ctx.clearRect(0, 0, cssWidth, cssHeight); // 1. 背景条 ctx.fillStyle = '#edf2f7'; ctx.fillRect(0, 0, cssWidth, cssHeight); // 2. 前景条(按进度百分比计算宽度) const barWidth = cssWidth * value; ctx.fillStyle = '#3182ce'; ctx.fillRect(0, 0, barWidth, cssHeight); // 3. 边框:0.5偏移防模糊 ctx.strokeStyle = '#a0aec0'; ctx.lineWidth = 1; ctx.strokeRect(0.5, 0.5, cssWidth - 1, cssHeight - 1); // 4. 右侧文字:显示百分比 ctx.fillStyle = '#1a202c'; ctx.font = '16px sans-serif'; ctx.textAlign = 'right'; ctx.fillText(Math.round(value * 100) + '%', cssWidth - 12, 26); } function update() { progress += 0.002; if (progress >= 1) progress = 0; draw(progress); requestAnimationFrame(update); } update(); return { setProgress(v) { draw(Math.min(1, Math.max(0, v))); } }; } createProgress('progress');

这里有几个细节值得展开讲。

clearRect(0, 0, cssWidth, cssHeight)是每一帧重绘前必做的清屏操作。它把整个画布内容擦干净,防止上一次的进度条残影叠加。如果你画的区域只是屏幕的一部分,也可以只clearRect对应区域,性能更好。

前景条宽度的计算cssWidth * value就是矩形参数width的典型用法:value在0到1之间,宽度随进度线性增长。右下角的百分比文字使用的是fillText,它虽然不属于矩形三招,但和矩形填充配合得非常多。绘制文字时也用到了fillStyle,这又一次体现了状态管理:如果这里忘了设置fillStyle,文字会用之前前景条的颜色,视觉上会乱。

strokeRect(0.5, 0.5, cssWidth - 1, cssHeight - 1)这行是避坑指南的直接应用。如果写成strokeRect(0, 0, cssWidth, cssHeight),边框会从画布边缘开始向内外各扩散0.5像素,上下左右都会被裁掉一半,看起来就像“漏气”的矩形。偏移0.5并让宽度减1,边框才能正好完整落在画布内。

这个进度条其实只用了fillRect和strokeRect两招,rect招在下面这个交互扩展里才会真正发光。

4.3 交互扩展:鼠标拖拽画矩形选区

真实项目中,拖拽鼠标画一个矩形选区很常见,比如图片裁剪框、截图工具、地图框选。这个需求正好能把rect招带到实战里。

思路是这样的:鼠标按下时记录起始点,鼠标移动时不断用clearRect+fillRect重画一个半透明矩形作为视觉反馈,鼠标抬起时用strokeRect固化最终边框。

const canvas = document.getElementById('select'); const ctx = canvas.getContext('2d'); let startX = 0, startY = 0, drawing = false; canvas.addEventListener('mousedown', (e) => { const rect = canvas.getBoundingClientRect(); startX = e.clientX - rect.left; startY = e.clientY - rect.top; drawing = true; }); canvas.addEventListener('mousemove', (e) => { if (!drawing) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(49, 130, 206, 0.2)'; ctx.fillRect(startX, startY, x - startX, y - startY); }); canvas.addEventListener('mouseup', (e) => { drawing = false; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; ctx.strokeStyle = '#3182ce'; ctx.lineWidth = 2; ctx.strokeRect(startX, startY, x - startX, y - startY); });

这里有个新手很容易忽略的坐标换算问题:e.clientX是相对于浏览器视口的坐标,而canvas的绘图坐标是相对于画布左上角的。中间如果隔了margin、padding或者页面滚动,不换算直接画,矩形位置会整体偏移。用canvas.getBoundingClientRect()取canvas在页面中的实际位置,再把鼠标坐标减去它,就能得到正确的画布内坐标。

在这个例子里,fillRect用来画选区半透明预览,属于“临时反馈”;strokeRect用来最终描边,属于“固化结果”。如果想把选区变成未来可操作的对象,比如继续编辑、调整大小,那就要用rect把矩形坐标存进数据结构里,再在每一帧里统一重绘——这已经慢慢摸到Canvas应用架构的边了。

5. 常见问题与排查技巧实录

我整理了一份快速排查表,都是实际开发中反复出现的状况,建议收藏,遇到问题先来这里查。

现象很可能的原因解决方案
矩形边缘模糊不清未处理devicePixelRatio缩放设置canvas.width = CSS宽度 * dpr,并用ctx.scale(dpr, dpr)
边框某几条边特别细或者消失整数坐标落在像素分界线上坐标加0.5偏移,或者用奇数lineWidth
后续所有图形都是一个颜色fillStyle/strokeStyle状态被污染每次绘制前显式设置颜色,或用save/restore隔离
canvas区域有内容却显示为空宽高被CSS拉伸,绘图缓冲区和显示区不一致检查canvas.width和height是否被设置成0或过小
重绘后上一帧内容残留clearRect范围或调用时机不对在绘制前调用clearRect,并确认清除范围覆盖整个区域
绘制的矩形位置和预期偏差很大鼠标坐标没换算成canvas画布坐标使用getBoundingClientRect()换算坐标

除了这张表,再说几个实战调试技巧。

排查填充是否生效,可以用ctx.getImageData(x, y, 1, 1).data看某个像素点的RGBA值。这个技巧特别适合排查“我以为画到这里了,其实没画”的情况,取到的透明度可以直观反映填充情况。

如果你刚开始做复杂图形,建议在开发阶段给每个关键矩形画一个调试用的strokeRect轮廓,能快速确认矩形位置和尺寸是否符合预期。调试完成后再移除,这招在多人协作时也很好用,相当于给图形上了一层“网格辅助线”。

还有一个容易被忽略的点:canvas的默认尺寸是300x150,如果你设置了CSS尺寸但忘了设置canvas.width和canvas.height,绘图坐标系仍然是300x150的。绘制任何超出这个范围的矩形,都会在画布外被截断,表现形式就是“我画了但屏幕上什么都没有”。

6. 从矩形出发,Canvas进阶还能做什么

三招讲完,不少萌新会觉得“画矩形这么简单,没意思”。但真实的前端开发中,绝大多数Canvas需求都是从矩形起步,一步步长出更复杂的能力的。这里给你几条清晰的方向。

先说离矩形最近的:圆角矩形。现在浏览器对ctx.roundRect(x, y, width, height, radii)的支持度已经越来越好,它就是在矩形四角加圆弧半径,适合做进度条、按钮、卡片这类UI元素。如果项目需要兼容老旧内核环境,也可以用arcTo手动拼接路径,本质上依然是“路径系统”那一招的延展。

再往上是transform相关操作。前端SDK里各种复杂图形绘制,底层逻辑往往是“先画矩形、再对上下文做旋转、缩放、位移”,也就是ctx.translate、ctx.rotate、ctx.scale,以及它们汇总后的ctx.setTransform(a, b, c, d, e, f)矩阵写法。热搜词里提到的matrix()矩形变换,本质上就是用一个矩阵把坐标系统一变换,矩形本身没变,变的是“画布坐标系”。这个概念通了,Canvas的地图、游戏、直播H5里的动效都能串起来理解。

如果你对数据可视化感兴趣,每一个图表库的底层都离不开矩形绘制:柱状图是fillRect堆出来,刻度线是细长的fillRect,选中区又是fillRect + strokeRect的组合。理解三招背后的“立即绘制 + 路径组合”思路,再去看ECharts这类库的源码,你会觉得它们不再高不可攀。

动画方向同样值得投入:requestAnimationFrame配合clearRect+ 重绘,就能让静态矩形活起来。要记住动画性能的关键是“只在需要的区域重绘”,比如进度条只更新进度条区域,而不是整屏clear。这个习惯越早养成越好。

我给新手的学习建议是:别急着学各种高级API,先用三招画满一整张画布。画一个网格棋盘,画一个柱状图,画一个拖拽选区,再挑战画一个简易的俄罗斯方块。这些练习做完,你对坐标、状态、路径、重绘的理解会上一整个台阶,再去啃globalCompositeOperation、离屏Canvas、GPU加速之类的话题,都会顺畅很多。

最后说一点个人体会。我最早学Canvas的时候,觉得fillRect太简单了,草草看过就去折腾“看起来很厉害”的粒子特效,结果第一个生产项目就被边框模糊和状态污染坑了一整天。后来沉下心来,把每个基础API都亲手调到“知其所以然”才真正开窍。Canvas学习没有捷径,唯一的捷径就是多画、多改、多观察像素变化。这三招看似基础,但如果你能把它们背后的坐标系直觉、状态管理和路径机制吃透,整个Canvas的根基就稳了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询