ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Canvas画矩形三招:fillRect、strokeRect、rect用法与避坑指南

Canvas画矩形三招:fillRect、strokeRect、rect用法与避坑指南 前端萌新们准备正式上手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 iddemo width300 height150/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);左边框实际是沿着 x10 这条线向左右各扩散1像素也就是从 x9 到 x11。其中x9到x10属于一块屏幕像素x10到x11属于另一块两块各覆盖一半颜色视觉上就会觉得颜色淡了一圈。把坐标加上0.5问题就会立刻缓解ctx.strokeRect(10.5, 10.5, 100, 80);边框现在精确落在整数像素内部左边从x9.5画到x10.5其中x10正好是像素边界整个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 idprogress width320 height40/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招带到实战里。思路是这样的鼠标按下时记录起始点鼠标移动时不断用clearRectfillRect重画一个半透明矩形作为视觉反馈鼠标抬起时用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的根基就稳了。
返回列表