
做过设备监控大屏项目的都知道画布上动态标点位最烦的不是绘图本身而是后续每个点要能被选中、拖拽、改变状态。之前我用原生Canvas写光维护几十个坐标点就硬写了两百多行后来换成fabric.js添加一个圆只需要new fabric.Circle()再加canvas.add()交互能力还自动带上了。这也是我如今遇到涉及图形编辑的前端需求几乎无脑选择fabric.js的原因。这篇入门文章只讲一件事用fabric.js添加圆形。但它会牵扯出一批最基础也必须搞懂的知识点——环境初始化、坐标系、对象属性、渲染时机。我按从零搭建到踩坑复盘的过程来写新手可以直接照着操作已经接触过fabric的同行也能看看这几个坑是不是也和你的经历重合了。1. 搭建最小可运行的绘图环境1.1 页面结构、CDN引入与Canvas初始化fabric.js的引入方式有三种CDN、npm、直接下载本地js文件。做学习验证项目用CDN最省事改动最少。这篇文章以目前很稳定的fabric.js 5.x版本为例下面这段HTML可以直接复制!DOCTYPE html html langzh-CN head meta charsetUTF-8 titlefabric.js 添加圆形/title script srchttps://unpkg.com/fabric5.3.0/dist/index.min.js/script /head body canvas idc width800 height600/canvas /body /html关键点有两个script标签把fabric库挂到window全局对象上之后你会在代码里看到一个全局的fabric命名空间canvas标签设置id、width、height这个canvas就是接下来的绘图载体。初始化代码写在另一个script标签里const canvas new fabric.Canvas(c);这一步非常关键。这里创建的不是浏览器原生canvas元素而是一个fabric.Canvas实例它会把设计原型包裹起来内部同时维护底层渲染层和上层交互层。之后你要add的所有圆形对象都由这个canvas实例来管理。原生Canvas里需要手动写的beginPath、arc、fill、stroke这些绘制指令到这里全部不需要了库内部替你完成。还要注意一点canvas标签的width和height属性必须写因为fabric.js计算对象坐标时依赖这两个属性而不是CSS样式。如果两者不一致你画的圆很容易出现“鼠标点在这里圆却出现在别处”的怪象这个在第5章会展开细说。1.2 验证环境创建一个最简圆形初始化完成马上往画布上丢一个圆const circle new fabric.Circle({ radius: 60, left: 100, top: 100, fill: #e74c3c }); canvas.add(circle);浏览器打开后你会在画布左上区域看到一个红色圆。到这里“用fabric.js添加圆形”的核心动作已经完成了。和原生Canvas相比这个写法的最大优势是对象化。fabric.Circle这个类把你和绘图细节之间的耦合封死了你传入几何参数和样式参数它生成一个可以独立存在的图形对象。这个对象之后可以被选中、拖拽、缩放、旋转。你只要在画布上点一下圆默认就会出现选中框和8个控制点这是fabric给的能力不是额外配置出来的。2. 理解fabric.Circle核心参数与坐标系2.1 radius是基础width/height由它推导而来fabric.Circle在构造参数里最容易被忽略的是“外部包围盒尺寸”和“半径”的关系。看这段代码const circle new fabric.Circle({ radius: 60 }); console.log(circle.width); // 120 console.log(circle.height); // 120 console.log(circle.radius); // 60对象创建出来之后它的width和height并不是你指定的而是自动更新为半径的两倍。因为fabric.js里所有物体的定位、外框选中、碰撞计算都以一个轴对齐的矩形包围盒为基础。圆形要融入这套体系就必须有一个隐含的包围盒最小外接正方形边长等于直径。新手如果不理解这一层后面经常在“调整圆的大小”上踩坑。有人下意识去改widthcircle.set(width, 200);这个写法通常达不到预期。虽然属性被改了但fabric.Circle内部的几何计算以radius为准渲染尺寸和选中框尺寸可能不一致甚至刷新之后又回到原值。正确改圆的大小是circle.set(radius, 100); circle.setCoords(); canvas.requestRenderAll();setCoords()刷新对象的外部包围盒坐标requestRenderAll()请求画布重新渲染。改完尺寸之后这两个方法配合使用才能让交互框和视觉内容同步。这一点在第5章还会有更细的案例分析。2.2 left、top与圆心位置的真实关系fabric.js坐标系和原生Canvas一致原点在画布左上角x轴向右y轴向下。这个不难理解。问题出在left和top这两个属性到底代表圆的哪个位置。默认情况下一个fabric.Circle对象的left和top表示“包围盒左上角”的坐标而不是圆心坐标。人的直觉天然认为“画圆就应该给圆心坐标”但fabric.js面向的是所有图形对象——矩形、图片、路径类对象都默认用left/top表示左上角圆形既然被纳入这套统一体系默认从左到右的规则也一视同仁。举一个非常具体的例子我希望圆心在(300, 200)。照直觉写const circle new fabric.Circle({ radius: 50, left: 300, top: 200, fill: #3498db });结果圆心真的在(300, 200)吗不是。因为left300、top200表示的是外接正方形左上角坐标圆心实际位置是(30050, 20050) (350, 250)。整个圆比预期位置向右下方偏移了一个半径。新手画出来总觉得“位置不对”但说不清哪里不对根因就在这里。解决办法有两个。第一主动换算写成left: desiredX - radiustop: desiredY - radius。第二设置originX和originY为center让left/top直接表示圆心位置const circle new fabric.Circle({ radius: 50, left: 300, top: 200, originX: center, originY: center, fill: #3498db });第二种写法更符合直觉对圆形定位尤其友好。但要注意一旦统一修改了originX/originY的默认值后续所有关于该对象的计算都围绕中心点展开。比如之后circle.set(left, 500)表示希望圆心到500而不是左上角到500。同一份代码里的对象尽量保持一致避免混用否则排查起来很头痛。我做图形编辑器时习惯把所有对象统一设置originX/originY为center。这样批量操作和坐标换算的思路统一按图形中心对齐时也少了很多绕弯。2.3 面积、周长从数学公式到前端工具函数如果你写过“根据用户输入的半径求圆的面积”这种题目下面这段代码逻辑你肯定很熟function getCircleArea(circle) { const r circle.get(radius); return Math.PI * r * r; } function getCirclePerimeter(circle) { const r circle.get(radius); return 2 * Math.PI * r; }fabric.Circle并没有预制的area或perimeter属性需要自己算。这个函数在小工具场景里很实用比如统计监控画面里有多少个圆点再按圆面积加权某个业务指标或者在图形编辑器里选中圆时在属性面板实时显示“面积、周长”。有一点要注意这里get(radius)拿到的是原始半径不含scale缩放。如果你对圆做了scaleX或scaleY操作视觉半径和属性半径会不一样比例变换之后的换算放到第4章说。顺便说一句如果你研究过工业视觉里的Halcon找圆、RANSAC圆拟合你会发现那类场景是通过边缘点估计圆心和半径而fabric.js是反过来的它先指定圆心和半径再生成视觉对象完全不需要拟合过程。二者用途不同但“圆心半径”这个几何模型都是稳固的基本盘。3. 给圆上色填充、描边、阴影与透明度3.1 fill、stroke、strokeWidth与strokeUniform圆形对象的外观属性由fill、stroke、strokeWidth控制写法和矩形对象完全一致const circle new fabric.Circle({ radius: 70, left: 50, top: 50, fill: #ff6b6b, stroke: #333333, strokeWidth: 4 });fill可以写颜色字符串也可以写rgba()表示半透明填充。stroke同理。如果把strokeWidth提到20你很快就会察觉fabric的描边默认是“居中线宽”一半在圆内一半在圆外。这在某些设计稿里会产生偏差需要心里有数。比较容易被忽略的是strokeUniform属性。它控制的是对象被缩放时描边宽度要不要跟着变。举例const circle new fabric.Circle({ radius: 70, left: 50, top: 50, fill: #ffeaa7, stroke: #2d3436, strokeWidth: 4, strokeUniform: true });当用户在画布上把这个圆放大两倍时设置strokeUniform: true能保证描边依然是4px视觉上很统一不设置时描边会跟着放大变成8px。如果把图形放在无限缩放的电子白板场景里后者很容易让大圆上的描边粗得吓人。3.2 径向渐变填充让平面圆变成立体球纯色圆比较单调真实项目里经常需要给圆加渐变用来模拟球体、按钮、地图标记高光。fabric.Circle的fill可以接收一个fabric.Gradient对象。径向渐变类型的写法如下const circle new fabric.Circle({ radius: 80, left: 80, top: 80, fill: new fabric.Gradient({ type: radial, gradientUnits: pixels, x1: 30, y1: 30, r1: 10, x2: 30, y2: 30, r2: 80, colorStops: [ { offset: 0, color: #ffffff }, { offset: 0.3, color: #ff9a9e }, { offset: 1, color: #f6416c } ] }) });把渐变中心放在圆内偏左上的位置高光点落在左上阴影区落在右下视觉上就像一颗立体球。这个手法在仪表盘、地图标记、聊天气泡里都能套用。如果你用过SVG会觉得这个结构很像radialGradient。确实fabric.js内部很多API设计都借鉴了SVG的模型——gradientUnits、colorStops、offset这些名词几乎一一对应。理解这一层学习成本会降低不少。3.3 阴影、透明度与对象克隆给圆加阴影可以在对象配置里直接写shadowconst circle new fabric.Circle({ radius: 45, left: 120, top: 60, fill: #fdcb6e, shadow: { color: rgba(0, 0, 0, 0.3), blur: 12, offsetX: 5, offsetY: 5 } });也可以先new fabric.Shadow()再赋值给circle.shadow日常用对象字面量就够了。整体透明度用opacity属性circle.set(opacity, 0.6);这个透明度作用于整个对象阴影也会一起变淡。如果只想让填充半透明、阴影保留很显眼那更可能需要在fill颜色里直接写rgba而不是用opacity。还有个实用技巧同一个圆要复制多个直接用const clone circle.clone(); canvas.add(clone);clone方法默认会复制可见外观、阴影、渐变但不复制事件监听器。需要复制事件的话得用回调处理这里不展开但知道clone不会顺带把事件一起带过来可以省掉很多排查时间。4. 圆形的定位、缩放与旋转规则4.1 把圆精确放到画布正中央最常见的需求之一是把圆居中。手写left/top时要考虑半径和原点另一种办法是让fabric来算canvas.add(circle); canvas.centerObject(circle); circle.setCoords();canvas.centerObject会根据画布尺寸自动计算left和top让对象居中同时保留对象原本的缩放比例。执行完之后记得调用circle.setCoords()目的是更新对象的包围盒和交互控制点坐标。如果你少了这一步后续拖动、框选时可能会出现“逻辑坐标和鼠标位置对不上”的怪毛病。如果希望创建时不展示“从左边移入”的动画更直接的写法是circle.set({ left: canvas.getWidth() / 2, top: canvas.getHeight() / 2 }); canvas.add(circle);但这种方式就回到了第2章那个原点问题如果设置了originX: center、originY: center这段代码确实把圆心放到正中央如果没设置左上角落在了中心圆心反而不在中心。4.2 旋转为什么圆看起来“没反应”fabric.js里任何对象的旋转都用angle属性控制circle.set(angle, 45);需求是“旋转一个圆”结果你会发现纯色圆旋转45度后看起来跟没转一样——因为圆在旋转几何里是个完全对称的图形。要让旋转效果可见必须给它加一个“非对称参照物”比如渐变、填充图案或者一根用来定位的“指针”线条。代码演示用渐变圆旋转const circle new fabric.Circle({ radius: 60, left: 100, top: 100, angle: 30, fill: new fabric.Gradient({ type: linear, coords: { x1: 0, y1: 0, x2: 0, y2: 1 }, colorStops: [ { offset: 0, color: #74b9ff }, { offset: 1, color: #0984e3 } ] }) });设置angle: 30之后渐变方向会跟着转这时候你能直观感受到“圆真的旋转了”。旋转中心默认是对象自身中心点。想硬改旋转中心点需要动对象的originX/originY或者用矩阵变换——对圆形来说基本没必要了解就行。4.3 scale缩放与radius的换算关系有些人不看文档就直接写circle.set(scaleX, 2); circle.set(scaleY, 2);这样确实放大了两倍但这种“放大”走的是变形矩阵对象自身的radius并没有变。反直觉的点在这里你get到的radius是原始半径屏幕上肉眼看到的半径却是原始半径乘以scaleX后的结果。所以凡是在属性面板、对话框里展示圆的半径用这个函数更准确function getVisualRadius(circle) { return circle.get(radius) * circle.get(scaleX); }前提是scaleX和scaleY相等。如果只改了scaleX圆在水平方向被拉长视觉上已经变成椭圆了再叫它圆的半径就不严谨。要让圆维持圆形并放大最稳妥的做法是直接改radiuscircle.set({ scaleX: 1, scaleY: 1, radius: 100 });为什么要把scale和radius放一起讲因为很多人在做动画时喜欢依赖scale做交互时又依赖radius两边数据不同步最终测出来的几何信息全是错的。我的建议是如果对象最终必须是一个标准圆尽量让radius承担尺寸标识scaleX/scaleY只负责临时动画。4.4 交互属性可选中、锁定方向fabric自带的交互控件对图形对象非常友好。直接对圆设置几个开关circle.set({ selectable: true, hasControls: true, hasBorders: true, lockRotation: true, lockMovementX: false, lockMovementY: false });selectable为false时圆不能被点击选中hasControls为false时选中后不显示缩放、旋转控制点hasBorders为false时不显示外接矩形边框lockRotation为true时锁定旋转lockMovementX、lockMovementY可以限制对象只在某个轴方向移动。实际做“地图标记点”时我通常会同时加上lockRotation和lockScalingFlip防止用户把标记点翻转成奇怪的角度。不加这个属性的后果是用户拖控制点拉拽时可能会把图形反转视觉上很怪异。5. 添加圆形时的踩坑复盘与性能优化5.1 canvas元素宽高与CSS尺寸不一致导致坐标错位有一回我做响应式容器里的画布只给canvas元素写了style宽高没设置它的width和height属性。结果fabric创建的画布宽高沿用了默认值300x150而CSS又把整个canvas放大到容器宽度于是绘制坐标全面错乱。设置圆的left为100屏幕上的实际水平位置却差出一大截拖拽时鼠标和对象总是歪的。排查下来根因很明确fabric.js内部计算坐标依赖canvas的width和height属性CSS尺寸主要决定显示尺寸。两套尺寸不一致时浏览器会把小画布拉伸到显示宽度所有坐标被等比放大但fabric拿到的事件坐标仍按原像素分辨率换算自然就错位了。修复方法canvas.setDimensions({ width: 800, height: 600 });或者直接在HTML里写死canvas的width和height属性。如果页面需要自适应可以监听窗口变化后调用canvas.setDimensions再重新加载缩放逻辑。这个坑不只发生在圆形上矩形、图片、文字都会中招但因为圆形本身有一种“中心偏移”的错觉新手更容易把两件事混着抱怨。5.2 left/top和originX/originY的混用陷阱今天画圆用了originX: center明天在同一个画布上画矩形却忘记设置这就是很多项目的真实状态一批对象以左上角为锚点另一批以中心为锚点。最终对齐操作时大家设置的left和top意义完全不同相互之间怎么也对不齐。我的建议是在新项目一开始就约定好统一标准所有图形对象都设置originX: center、originY: center或者干脆全部保持默认不碰origin系。两种方式没有绝对优劣但绝不能在同一块画布里混用。如果你发现一个圆用left/top定位时总是偏一个半径先去查它的锚点到底是圆心还是左上角不要急着调数值。5.3 修改radius后没有刷新包围盒在我遇到的坑里最隐蔽的是动态更新圆的大小。比如在定时器里根据实时数据刷新半径function updateCircle(circle, newRadius) { circle.set(radius, newRadius); // 如果少了下面这两行选中框还会停留在旧尺寸 circle.setCoords(); canvas.requestRenderAll(); }如果不调用setCoords画布上点击圆时选中边框仍停留在旧尺寸拖动控制点缩放时可能出现对象突然跳变的效果。这不是fabric的Bug而是缩放与包围盒缓存没有同步。我的经验是业务代码里封装一个通用的“修改半径”方法固定走set、setCoords、requestRenderAll三步。这一步固定之后基于改半径的动画、自动缩放、批量更新都稳了很多。5.4 批量添加圆形时的渲染优化业务中经常要一次性添加几十个标记圆点。最直接的循环写法for (let i 0; i 30; i) { const dot new fabric.Circle({ radius: 10, left: Math.random() * 700, top: Math.random() * 500, fill: #00b894 }); canvas.add(dot); }这样写能跑但fabric每次add一个对象都可能触发渲染相关逻辑循环次数多时整体帧率会明显掉。更好的写法是一次性传入多个对象const dots []; for (let i 0; i 30; i) { dots.push(new fabric.Circle({ radius: 10, left: Math.random() * 700, top: Math.random() * 500, fill: #00b894 })); } canvas.add(...dots);一次性add会让对象创建和画布更新在更紧凑的流程里完成。如果更新的对象很多还可以临时关闭自动渲染canvas.renderOnAddRemove false; // 批量 add/remove canvas.add(...dots); canvas.requestRenderAll();把renderOnAddRemove设为false之后增删对象不会自动触发渲染最后统一requestRenderAll一次。这个优化在加载几百个点位时特别明显。我在实际项目里依靠这些习惯避免了大量返工所有圆形对象统一采用center原点业务侧只记录圆心和半径界面侧不用再关心包围盒偏移。如果你也是从这个“圆”开始入门fabric.js我建议下一步先别急着学很多图形找个案例把fabric.Circle和事件拖动、动画结合起来玩一遍。等把半径修改、拖拽、缩放这一整套流程走熟了fabric.js才算真正入了门。