写这篇的起因是上周帮团队新人 review 代码,他卡在"往 fabric.js 画布里加一个圆"这道坎上整整半天。其实核心就一个new fabric.Circle({...})的事情,但牵扯出来的问题一串串:为什么圆没出现?为什么位置偏了?为什么改了半径画面没反应?为什么放大之后圆的"真实大小"对不上?这些坑不踩一遍很难说清楚。
fabric.js 是前端做 Canvas 图形编辑最常用的库之一,底层封装了原生 Canvas 2D API,向上提供了一套完整的对象模型。你可以把圆、矩形、文字、图片都当作独立对象来管理,拖拽、缩放、旋转、事件监听、序列化导出全是现成的。这篇就围绕"在 fabric.js 中添加圆形"这个入口,把环境搭建、属性拆解、坐标体系、交互事件、排查技巧一条线讲完,内容和实际踩坑顺序一致,看完基本能直接上手。顺便说一句,同样是搜"圆",有人找的是视觉算法里的找圆(Halcon、RANSAC 圆拟合),有人找的是画布上的绘图圆——今天只讲后者,但最后我会把两者的数据模型对照一下,做可视化标注的同学应该用得上。
1. 先搭环境:把 fabric.js 请进项目
1.1 为什么选 fabric.js,而不是用原生 Canvas 画圆
原生 Canvas 画圆其实只要三行代码:arc()、fill()、stroke()。但问题在于,你用原生 API 画完这个圆之后,它在程序里"不存在"——它只是一堆像素,没有对象,没有属性,也没有身份。下次你想改它的半径、想拖动它、想知道用户有没有点中它,全都得自己从头实现。命中测试、重绘调度、拖拽状态管理、缩放矩阵计算,这些够写几千行。
fabric.js 的思路完全不同。它把"圆"变成一个真正的对象:有自己的radius、left、top、fill、stroke属性,有自己的事件,还能被选中、拖动、缩放。你操作的是对象,而不是像素。用生活类比就是:原生 Canvas 像黑板,你画上去的圆擦掉就没了、也抠不出来;fabric.js 像一块贴满磁贴的白板,每个圆都是一块能单独拿起来、挪走、换颜色的磁贴。
所以如果你的需求只是"一次性画个静态圆",原生 Canvas 完全够用;但只要涉及交互、编辑、数据回读,fabric.js 能帮你省掉至少一半工作量。
1.2 CDN 与 npm 两种引入方式
快速验证用 CDN,工程化项目用 npm,两条路都给你。
CDN 方式,新建一个 HTML 文件,在 body 里引入 fabric.js 的 script 标签。我用的是 5.3.0 稳定版,如果你习惯读最新文档,注意区分 6.x 的差异(后面会讲):
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>fabric.js 添加圆形</title> </head> <body> <canvas id="c" width="600" height="400"></canvas> <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js"></script> <script> // 你的代码写在这里 const canvas = new fabric.Canvas('c'); </script> </body> </html>npm 方式则是常规工程化流程:
npm install fabric然后在模块里按需导入:
import { Canvas, Circle } from 'fabric'; const canvas = new Canvas('c'); const circle = new Circle({ radius: 50, left: 100, top: 100, fill: '#4a90d9' }); canvas.add(circle);顺带说一句版本的事。fabric.js 5.x 里,改完对象属性后调canvas.renderAll();到了 6.x,官方推荐用canvas.requestRenderAll(),它做了一个渲染请求合并的优化,避免高频操作时反复刷帧。我下面的示例主要以 5.3.0 的习惯写,但涉及渲染刷新的地方,我会特意标注 6.x 的写法,避免你照抄时踩版本坑。
1.3 初始化画布的最小骨架
new fabric.Canvas('c')这一行,做的事情比看起来多得多。它会接管id="c"这个原生<canvas>元素,在它之上构建一个交互层:监听鼠标事件、管理选中状态、维护画布上的所有对象列表、处理屏幕坐标到画布坐标的换算。
有两个细节容易在入门阶段坑人,提前说:
第一,canvas 的width和height属性决定的是绘图缓冲区大小,而 CSS 里设置的width/height决定的是显示尺寸。如果两者不一致,fabric 会尽量做换算,但最好一开始就保持一致,省得坐标和视觉对不上。第二,在高分屏(devicePixelRatio > 1)下,canvas 默认会做视网膜缩放,fabric 的enableRetinaScaling选项默认打开。这本来是好事,但如果你发现画出来的圆边缘发虚或者坐标偏了,先检查这个开关,后面排查章节会细说。
还有一个选择:如果你只是想在页面里展示图形、不需要任何用户交互,可以用new fabric.StaticCanvas('c')。它省掉了一大堆事件监听和状态管理开销,性能更好,滚动和移动端表现也更稳。
2. 添加第一个圆:最小可用代码与逐行解读
2.1 一段能直接跑的完整代码
把这一段贴到 1.2 节的 HTML 里,你就能看到一个可以拖动、缩放、选中的圆:
const canvas = new fabric.Canvas('c'); const circle = new fabric.Circle({ radius: 60, left: 100, top: 80, fill: '#4a90d9', stroke: '#1c3f7a', strokeWidth: 2 }); canvas.add(circle);逐行解释一下:
radius: 60是圆的半径,单位像素。注意它不是直径,新手经常在这里除以 2 或者乘 2 搞反。left: 100和top: 80是圆在画布上的位置坐标。但它俩默认指向的是"圆的外接矩形的左上角",不是圆心!这是 fabric.js 新手最容易懵的地方,后面 3.2 节单独展开。fill是填充色,stroke是描边色,strokeWidth是描边宽度。canvas.add(circle)把圆挂到画布上。在 5.x 里,add之后会自动触发一次渲染,所以你立刻能看到它;如果是在 6.x 里,有些情况下你需要补一个canvas.requestRenderAll(),别省略。
运行之后,你会看到圆出现在画布左上角附近。点一下它会出现一圈控制框和八个控制点,拖控制点可以缩放,按住圆可以拖动位置——这些都是 fabric 白送的,不用你写一行交互代码。
2.2 圆形对象的"视觉尺寸"与"控制盒"
fabric.js 里每个对象都围绕一个"控制盒"(bounding box)来做布局和交互。对圆来说,控制盒就是包住它的那个正方形,边长等于直径。
这个控制盒在交互上有存在感:选中圆时显示的边框、四个角的控制点,全都是围绕这个控制盒来的。于是引出两个容易混淆的概念:
- 几何尺寸:由
radius决定,是圆本身的半径。 - 可视尺寸:由
radius加上strokeWidth的一半构成,因为描边是沿着圆周居中绘制的——一半在圆外,一半在圆内。
所以如果你想画一个半径 60、描边 4 的圆,它实际占据的视觉空间大约是半径 62。做像素级对齐或者导出图片做测量时,这个差值会带来肉眼可见的偏差。具体项目中你先想清楚:你的坐标对齐基准是"圆的外轮廓"还是"填充区域的边缘"。
2.3 三个导致"圆不见了"的典型原因
新手画不出来圆,九成是下面三个原因,你逐一排查就行:
画布尺寸为 0 或显示区域被遮挡。如果<canvas>没有正确设置width/height,或者外层容器高度塌陷,画布实际是零尺寸,圆自然看不到。打开浏览器 DevTools 检查一下 canvas 元素的尺寸更直观。
填充色和背景色相同。fabric.Circle 的默认填充色是黑色。如果你的页面或画布背景也是深色,圆就"隐身"了。试试改成显眼的浅色,比如fill: '#ff6347'。别笑,这个坑我在正式项目里见过不止一次。
坐标跑出画布边界。如果left或top设成了负值,或者远大于画布宽高,圆会落在可见区域之外。初学者经常以为left: 0, top: 0是画布中心,其实是左上角。想让圆出现在画布正中间,最稳妥的做法是:
const circle = new fabric.Circle({ radius: 50, fill: '#ff6347' }); canvas.add(circle); circle.center(); // 移到画布中心center()是 fabric.Object 提供的方法,等价于把对象中心对齐到画布中心。在入门阶段少算一次坐标,就少踩一次坑。
3. 圆的属性体系与几何原理拆解
3.1 视觉属性:填充、描边、透明度与阴影
fabric.Circle 的视觉属性非常直观,基本都是字面意思。我把常用的一组列出来,你可以直接抄:
const circle = new fabric.Circle({ radius: 40, fill: '#ff8c42', stroke: '#333333', strokeWidth: 3, opacity: 0.85, shadow: { color: 'rgba(0, 0, 0, 0.3)', blur: 10, offsetX: 3, offsetY: 3 } });这里有几个值得说的点:
fill设为null或'transparent'可以得到只有描边的空心圆;opacity是整体不透明度,作用在填充和描边上;shadow不是字符串而是对象,颜色、模糊半径、X/Y 偏移分开设置;visible: false可以隐藏圆,但它仍在画布对象列表里,后面可以再显示出来;selectable: false让圆不能被选中,适合做纯展示元素。
另外,如果一个圆同时有填充和描边,fabric 默认先画填充、再画描边。描边宽度对视觉尺寸的影响前面说过,这里不再重复。还有一个从 4.x 开始支持的属性strokeUniform: true,它控制缩放圆时描边是否跟着一起变粗。默认情况下,你把圆拉大两倍,描边也粗两倍,视觉效果挺突兀;设成true后描边保持原始宽度,视觉上更接近"圆变大了,但线条粗细不变",做图纸标注类应用时很实用。
3.2 originX/originY 的坐标系魔法
这一节是整个入门阶段最值钱的内容,我建议你反复读两遍。
fabric.js 的所有对象都有两个定位属性originX和originY,默认值分别是'left'和'top'。它们决定的是:left和top这两个坐标值,到底指的是对象的哪个点。
默认情况下,left: 100, top: 80表示"对象外接矩形的左上角在画布坐标 (100, 80) 处"。对圆来说,就是外接正方形的左上角,不是圆心。如果你想让left和top直接表示圆心,就得把定位原点移到圆心:
const circle = new fabric.Circle({ radius: 50, left: 300, top: 200, originX: 'center', originY: 'center', fill: '#4a90d9' });这时候(300, 200)就是圆心坐标,后续不管圆怎么移动,getCenterPoint()拿到的中心都和left/top一致。
这个设计初看绕,用多了其实很符合直觉:originX/originY就是"这个对象身上的哪个点被坐标钉住了"。'left'/'top'钉住左上角,'center'/'center'钉住中心。你还可以组合出originX: 'left', originY: 'center'这种"左边线中点"的定位模式。
实际项目里,如果你要画的是"圆心在某个具体位置"的圆(比如把圆心放在图表数据点上),强烈建议统一用originX: 'center', originY: 'center'。否则你得在心里默默换算"圆心 = left + radius",一旦圆被缩放,换算关系还会变,极其容易出错。
3.3 startAngle/endAngle:从圆到扇形
fabric.Circle 本质上是用一条圆弧路径画出来的。startAngle和endAngle决定了这段弧从哪里开始、到哪里结束。默认是 0 到2 * Math.PI,刚好一个整圆。注意单位是弧度制,不是角度制。
把角度改小,圆就变成了扇形:
const sector = new fabric.Circle({ radius: 80, left: 250, top: 200, originX: 'center', originY: 'center', startAngle: 0, endAngle: Math.PI / 2, // 90度 fill: '#ffcc00', stroke: '#333333', strokeWidth: 1 }); canvas.add(sector);运行后你看到的是一片"黄色的四分之一扇形",因为 fabric 会把起点和终点连起来闭合路径再填充。如果你希望只要弧线、不要扇形面,把fill设成null,只留描边即可。这个特性可以用来做仪表盘、饼图、进度环、雷达图刻度,用起来思路非常直接。
注意:fabric 的角度是弧度制,写 90 度一定要写成
Math.PI / 2,千万别直接写90,否则画出来的是一个几乎占满大半圆的奇怪图形。这是我亲眼见过很多次的问题。
3.4 radius 与 scaleX/scaleY:别把缩放当改半径
fabric.js 里,所有对象共享一套通用变换属性:scaleX和scaleY。你用鼠标拖拽控制点缩放圆,改的就是这两个属性,而不是radius。
这里藏着一个实际项目里最容易出 bug 的坑:拖拽缩放后,圆的radius属性不变,但视觉半径变了。举个例子:
const circle = new fabric.Circle({ radius: 50, left: 100, top: 100, fill: '#4a90d9' }); canvas.add(circle); circle.set({ scaleX: 2, scaleY: 2 });屏幕上这个圆看着半径是 100,但你读circle.radius得到的仍然是 50,读circle.scaleX才是 2。真正的视觉半径是两者相乘:radius * scaleX。
那什么时候该改radius,什么时候该改scaleX?我的经验是:缩放对应"临时显示"需求,改 radius 对应"永久数据"需求。比如拖拽控制点临时拉大看一眼,用 scale;图纸里圆的几何尺寸真的变了,就改 radius,同时把 scale 重置回 1。如果数据要和后端对接,甚至要考虑把 scale 吸收进 radius 再上报,避免下游还要做乘法。
再补一个和缩放强相关的细节:默认情况下缩放会把描边一起缩放,就是前面提过的strokeUniform。还有一个数据读取场景:拖拽结束后,如果你想记录圆的真实圆心和真实半径,别直接读radius,用下面这套组合:
circle.on('modified', function () { const center = this.getCenterPoint(); const visualRadius = this.radius * this.scaleX; console.log('圆心:', center.x, center.y); console.log('视觉半径:', visualRadius); });这段代码后面还会用在"和视觉算法数据对接"的场景里。
4. 让圆真正“活”起来:交互、事件与行为控制
4.1 开箱即用的交互:选中、拖动、缩放
fabric.js 最让人省心的,是交互能力默认就开着。你往画布上add一个圆,用户就能点选它、拖动它、用八个控制点缩放它、用旋转柄转动它。这一切的前提是你用的是fabric.Canvas而不是fabric.StaticCanvas,并且圆的selectable没有被手动关掉。
如果你想调整交互表现,常用的开关有这些:
circle.set({ selectable: true, // 是否可选中 evented: true, // 是否响应鼠标事件 hasControls: true, // 是否显示缩放/旋转控制点 hasBorders: true, // 是否显示选中边框 hoverCursor: 'pointer',// 鼠标悬停时的光标 moveCursor: 'move' // 拖动时的光标 });一个比较实用的经验:如果页面里有好几个圆叠在一起,fabric 默认按"后添加的在上层"来叠放,点选时会选中最上面的那个。你可以在鼠标按下事件里拿到当前命中的对象,再做进一步处理;如果某个圆只是装饰、不想被误选,直接设selectable: false就好。
4.2 事件监听:mouse:down、object:modified 与坐标获取
fabric.js 的事件体系分两层:画布级事件和对象级事件。
画布级事件用canvas.on()监听,比如:
canvas.on('mouse:down', function (options) { const target = options.target; if (target && target.type === 'circle') { console.log('点中了圆', target); } }); canvas.on('selection:created', function (options) { console.log('新选中', options.selected); }); canvas.on('object:added', function (options) { console.log('新对象入画布', options.target); });对象级事件用circle.on()监听,更精准:
circle.on('moving', function () { // 拖动过程中频繁触发,注意别在里面做重活 }); circle.on('scaling', function () { // 缩放过程中触发 }); circle.on('modified', function () { // 拖动/缩放/旋转结束后触发一次 const center = this.getCenterPoint(); console.log('最终圆心:', center.x, center.y); const visualRadius = this.radius * this.scaleX; console.log('最终视觉半径:', visualRadius); });getCenterPoint()返回的是对象中心在画布坐标系里的坐标,非常常用。注意它和left/top的关系取决于originX/originY,所以别自己用公式推算中心,直接调这个方法最稳。
如果需要鼠标位置和对象坐标互转,画布提供了canvas.getPointer(e),它能把鼠标事件的屏幕坐标换算成画布逻辑坐标。这在做"点击空白处创建一个圆"这类功能时是标配写法:
canvas.on('mouse:down', function (options) { if (!options.target) { const pointer = canvas.getPointer(options.e); const newCircle = new fabric.Circle({ radius: 30, left: pointer.x, top: pointer.y, originX: 'center', originY: 'center', fill: '#7ed321' }); canvas.add(newCircle); } });这里我把originX/originY都设成了'center',所以圆的圆心正好落在鼠标点击的位置,不用额外加半个半径的偏移,代码干净很多。
4.3 行为控制:锁定与高级命中
有时候你不希望用户乱动已经摆好的圆。fabric.js 提供一组锁定属性,全部设成true就相当于把圆"钉死"在画布上:
circle.set({ lockMovementX: true, // 禁止水平移动 lockMovementY: true, // 禁止垂直移动 lockScalingX: true, // 禁止水平缩放 lockScalingY: true, // 禁止垂直缩放 lockRotation: true // 禁止旋转 });这组属性在"只允许用户查看、不允许修改"的展示场景里非常好用,甚至可以不设置selectable: false,让用户能选中看到信息,但动不了。
再说一个命中检测的进阶点。默认情况下,fabric 用对象的包围盒来做命中测试,也就是说你点击圆的外接正方形范围内(包括四个角附近的空白区域),都算点中了圆。对圆这种图形来说,四个角其实是空的,但默认行为仍然会命中的。如果你希望"只有真的点在圆内才算命中",有两个方案:
一是开启像素级命中检测perPixelTargetFind: true。它会在命中时逐像素判断透明度,精度高,但性能开销也大,圆多的时候拖动会明显变卡。
二是自己做一个"圆心距离判断":在mouse:down事件里拿到点击坐标,计算它和圆心的距离,小于视觉半径才认为是命中。这个方案性能好、逻辑透明,适合要求精确热区的场景。
canvas.on('mouse:down', function (options) { const pointer = canvas.getPointer(options.e); const dx = pointer.x - circle.left; const dy = pointer.y - circle.top; const distance = Math.sqrt(dx * dx + dy * dy); const visualRadius = circle.radius * circle.scaleX; if (distance <= visualRadius) { console.log('点在圆内'); } });这里我直接用circle.left来当圆心,前提是前面把originX/originY设成了'center'。如果你没设,记得改用circle.getCenterPoint()。
5. 从“会画”到“画对”:常见问题排查实录
5.1 圆不显示或位置偏移的速查表
把我在项目里碰到的高频问题整理成一张表,遇到问题直接对照:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 圆完全不显示 | canvas 尺寸为 0,或被 CSS 隐藏 | 检查 canvas 的 width/height 与 CSS 尺寸 |
| 圆不显示且页面全黑 | 圆默认填充黑色,背景也是深色 | 显式设置填充色 |
| 圆位置和预期不符 | 没搞清 originX/originY 的作用 | 设置originX: 'center', originY: 'center'以圆心定位 |
| 圆被切片或一半在外面 | left/top 越界,或超出画布裁剪区域 | 打印 left/top 与画布宽高做对比 |
| 圆显示但无法选中 | selectable: false或用了 StaticCanvas | 打开 selectable,改用 fabric.Canvas |
| 圆模糊 | 高分屏 retina 缩放未正确启用 | 检查 enableRetinaScaling 与 devicePixelRatio |
这里面最隐蔽的是第一条和第二条。canvas 元素如果被父容器挤压到高度为 0,它在页面上占位正常但内容完全不可见;圆默认填充黑色这件事,前面说过,在深色主题页面里特别容易踩。
5.2 改了属性但画面没变化
这个现象几乎人人都遇到:circle.radius = 80,然后盯着屏幕等,什么都没发生。
原因很简单:fabric.js 不会自动监听对象属性变化。你直接给属性赋值,对象内部的数据确实变了,但画布不知道要重绘。正确流程一定是两步:改属性,再让画布重绘。
circle.set('radius', 80); // 第一步:改属性 canvas.requestRenderAll(); // 第二步:请求重绘(6.x 推荐写法)在 5.x 里你也可以用canvas.renderAll(),它是同步立即重绘;requestRenderAll()则合并渲染请求,在下一帧统一执行,性能更好。我的习惯是统一用requestRenderAll(),两个版本都兼容,也不会在高频场景里卡渲染。
另外注意,在 5.x 中canvas.add()之后会自动触发一次渲染,所以新加圆能看到;但"修改已有对象"必须手动触发重绘。很多人把这两件事弄混,以为是版本 bug,其实只是渲染时机不同。
5.3 高清屏下圆发虚、坐标偏
现在笔记本和手机屏幕普遍是高分屏,devicePixelRatio 可能是 2 甚至 3。Canvas 的绘图缓冲区如果不按物理像素设置,就会出现字迹和图形发虚。
fabric.Canvas 默认开启了enableRetinaScaling,多数情况下你不需要做额外处理。但如果你发现圆边缘特别糊,或者手动改过 canvas 的 CSS 尺寸,可以把这个开关显式打开,并注意在初始化前确保 canvas 的 width/height 属性设置正确:
const canvas = new fabric.Canvas('c', { enableRetinaScaling: true });还有一个容易被忽略的细节:不要给<canvas>元素直接加 CSS 边框或内边距。canvas 的坐标计算是基于元素左上角来的,一旦加了边框,事件点击坐标和绘图坐标会发生固定偏移,表现为"鼠标点不到圆上""选中总是差几个像素"。正确做法是给 canvas 套一个没有任何 padding 的外层 div,边框画在 div 上:
<div style="border: 1px solid #ccc; display: inline-block;"> <canvas id="c" width="600" height="400"></canvas> </div>这个坑官方文档没有重点提,但实测非常常见,写在这里提醒你省一轮调试。
5.4 圆太多导致卡顿
当你往画布里塞了成百上千个圆,即使只是纯展示,也可能明显掉帧。原因很简单:每次重绘都要把所有对象重新绘制一遍,事件层还要维护所有对象的包围盒。
三个优化手段,按性价比排序:
第一,静态展示用 StaticCanvas。不需要交互的场景直接换掉,事件层省掉一大块开销,性能翻倍。
第二,关闭对象缓存里不需要的部分。fabric 默认开启objectCaching,把每个对象的图形缓存到离屏 canvas 上,拖拽时不用重新计算路径。对圆这种简单图形,缓存开着的收益很明显,一般不用关;但如果你发现内存占用暴涨,可以针对部分对象设objectCaching: false试试。
第三,按需渲染。如果圆的位置不变化,不要频繁调用renderAll。把canvas.renderOnAddRemove设为false,批量添加完所有圆之后,统一requestRenderAll()一次,只刷一帧。
另外,如果你的圆是按规律排布的大量数据点(比如散点图),还可以考虑用一个fabric.Group把它们打包成一组对象,整体移动时只处理一个对象的变换,性能也好很多。
5.5 数据模型与外部的对接:从视觉半径到找圆数据
最后聊一个很多前端不太注意、但做视觉可视化项目时非常关键的衔接点:fabric 圆的几何数据,怎么和外部算法数据对上。
在机器视觉领域,找圆是一个经典任务。Halcon 里常见的是基于边缘梯度的找圆算子,输出圆心坐标和半径;RANSAC 圆拟合则是从一堆散点里拟合出一个最佳圆,同样输出圆心和半径。这些算法给到的核心数据模型都是三元组:(centerX, centerY, radius)。而前端要做的是把这个圆可视化到页面上,或者反过来把用户画的圆回传给算法做测量。
用 fabric.js 做这个映射非常直接。算法数据转成画布对象:
// 假设算法输出 x=320, y=240, r=45 const circle = new fabric.Circle({ radius: 45, left: 320, top: 240, originX: 'center', originY: 'center', fill: 'rgba(74, 144, 217, 0.3)', // 半透明方便看到下面的图 stroke: '#4a90d9', strokeWidth: 2 }); canvas.add(circle);用户画的圆转成算法数据:
circle.on('modified', function () { const center = this.getCenterPoint(); const result = { x: center.x, y: center.y, radius: this.radius * this.scaleX // 视觉真实半径 }; console.log(result); });为什么强调用radius * scaleX而不是直接用radius?因为你用鼠标拖拽缩放之后,radius还是初始值,但视觉半径变了。如果不乘scaleX,回传给算法测量的半径就是错的,整个标定流程就白做了。这也是前面 3.4 节反复强调的原因——几何数据从视觉对象里干净地抽出来,是这类项目的刚需。
6. 进阶玩法与我的实操经验
6.1 用圆做进度环与仪表盘
startAngle/endAngle配合定时器,可以做一个圆环进度效果。核心逻辑就是不停改变endAngle,然后触发重绘:
const progress = new fabric.Circle({ radius: 70, left: 150, top: 150, originX: 'center', originY: 'center', startAngle: 0, endAngle: 0, // 初始是 0 fill: null, stroke: '#4a90d9', strokeWidth: 8, strokeUniform: true }); canvas.add(progress); let p = 0; setInterval(() => { p = (p + 0.05) % 1; progress.set('endAngle', p * Math.PI * 2); canvas.requestRenderAll(); }, 50);注意我把fill设成了null,只留粗描边,这样就是一个进度环而不是实心扇形。strokeUniform: true保证了描边粗细不随任何变换波动。如果想让角度从顶部开始而不是从右侧顺时针开始,把startAngle设成-Math.PI / 2即可。
6.2 点击圆触发业务逻辑
把圆变成可点击的"按钮",是交互类项目最常见的诉求。做法很简单:给圆设selectable: false(避免选中框干扰视觉),然后监听画布mouse:down,判断options.target === circle:
const circle = new fabric.Circle({ radius: 40, left: 120, top: 120, fill: '#4a90d9', selectable: false, // 不显示选中框 hoverCursor: 'pointer' // 悬停显示手指形状 }); canvas.add(circle); canvas.on('mouse:down', function (options) { if (options.target === circle) { console.log('圆的业务逻辑触发'); } });这种写法比给圆单独加事件更可控,因为你可以同时判断点击到了谁、以及点击的空白区域要不要处理。加上前面 4.3 节的"圆心距离"命中判断,可以做非常精确的圆形热区。
6.3 三个我一直保留的实操习惯
第一个习惯是统一圆心定位。只要语义上允许,我一律用originX: 'center', originY: 'center'来建圆。这样left/top、getCenterPoint()、以及日志里所有坐标都保持同一套含义,团队协作时不会出现"你这个 left 是圆心还是左上角"的争论。
第二个习惯是统一封装一个取圆几何信息的方法。不管项目大小,我常在公共方法里加这样一个函数:
function getCircleMeta(circle) { const center = circle.getCenterPoint(); return { x: Math.round(center.x), y: Math.round(center.y), radius: Math.round(circle.radius * circle.scaleX) }; }只要圆的几何信息要被外部消费——不管是上报后端、做测量、还是做坐标对齐——都走这个函数,不要在业务代码里散落一堆radius和scaleX的乘积。回到前面提到的找圆数据对接,也只需要在这里加一层单位转换或者坐标偏移就行。
第三个习惯是改完属性马上写渲染调用。我在代码评审时见过太多"赋值之后忘了requestRenderAll"导致的 bug。我自己养成的是"set 后面跟渲染"的一行式写法:
circle.set('radius', 80) && canvas.requestRenderAll();或者干脆不依赖这种小聪明,规规矩矩写两行,但一定要写第二行。这一点不管你是 5.x 还是 6.x,都是通用的。
最后再分享一个我的个人体会:很多人画圆只是为了"界面好看",但在真正的工程项目里,圆几乎总是要参与计算的——要么是标注坐标,要么是测量尺寸,要么是碰撞热区。所以从入门第一天就养成"把圆心和真实半径独立抽出来"的习惯,后面能省下大量排查时间。希望这篇从添加圆形出发的文章,能帮你把 fabric.js 这条路的坑都提前避掉。