--- title: "Fabric.js 简单介绍和使用" date: 2025-03-15T22:51:22Z lastmod: 2025-03-15T22:55:55Z tags: [Web 前端,JavaScript,Canvas,Fabric.js] --- # Fabric.js 简单介绍和使用 ## 参考资料 - [Fabric.js JSDoc](http://fabricjs.com/docs/) - [Fabric.js Demos - Event inspector](http://fabricjs.com/events) - [Fabric.js 从入门到________](https://juejin.cn/post/7026941253845516324) - [Fabric.js 实用指南](https://blackstarxing.github.io/2019-08-17-fabric-doc.html) ## 简介 Fabric.js 是一个功能强大的 Canvas 库,它在原生 Canvas 之上提供了交互式对象模型、多种易用的 API 和 SVG 解析器等,通过简洁的 API 就可以在画布上进行丰富的操作,并且支持多种的事件方法。 注:本文中使用的 Fabric.js 版本为 v5。 ## 常用的对象、属性和方法 ### 常用对象 |名称|描述| | ------| ------------------------------| |​`ActiveSelection`|选区| |​`Group`|分组| |​`Image`|图像| |​`Line`|线段| |​`Rect`|矩形| |​`Text`|文本(不可编辑,不换行)| |​`IText`|可编辑文本(可编辑,不换行)| |​`Textbox`|文本框(可编辑,自动换行)| ### 常用属性 |属性|描述| | ------| ---------------------------------------------| |​`type`|对象类型| |​`data`|用于添加自定义的数据| |​`originX`|对象转换的水平原点(left / center / right)| |​`originY`|对象转换的垂直原点(top / center / bottom)| |​`left`|水平坐标| |​`top`|垂直坐标| |​`width`|宽度| |​`height`|高度| |​`angle`|旋转角度| |​`scaleX`|水平方向缩放倍数| |​`scaleY`|垂直方向缩放倍数| |​`stroke`|线段颜色| |​`strokeWidth`|线段宽度| |​`fill`|填充颜色| |​`fontFamily`|文本字体名称| |​`fontSize`|文本字体大小| |​`opacity`|对象的不透明度| |​`borderColor`|选区边框颜色| |​`borderDashArray`|选区边框虚线样式| |​`borderOpacityWhenMoving`|选区拖拽时的边框透明度| |​`cornerColor`|选区 “操作点” 填充颜色| |​`cornerSize`|选区 “操作点” 大小| |​`cornerStrokeColor`|选区 “操作点” 描边颜色| |​`cornerStyle`|选区 “操作点” 样式(circle / rect)| |​`transparentCorners`|选区 “操作点” 中心是否透明(只有描边)| |​`hasControls`|是否有 “操作点”| |​`lockRotation`|是否禁止旋转对象| |​`lockMovementX`|是否禁止水平移动对象| |​`lockMovementY`|是否禁止垂直移动对象| |​`lockScaleX`|是否禁止水平缩放对象| |​`lockScaleY`|是否禁止垂直缩放对象| |​`selectable`|是否可以被选中| |​`hoverCursor`|光标在对象上方时的样式(子对象优先)| |​`moveCursor`|光标在可移动对象上方时的样式(子对象优先)| ### 常用方法 #### 画布(Canvas) |方法|描述| | ------| ------------------------------------| |​`add(...object)`|添加对象到画布| |​`insertAt(object, index, nonSplicing)`|添加对象到画布指定层级| |​`moveTo(object, index)`|更改对象所在的层级| |​`remove(...object)`|移除画布上的对象| |​`discardActiveObject()`|取消选中对象| |​`getActiveObject()`|获取选中的对象| |​`setActiveObject()`|更改选中的对象| |​`getPointer()`|传递事件对象,获取相对于画布的坐标| |​`fire()`|触发画布事件| |​`forEachObject()`|遍历画布上的对象| |​`getZoom()`|获取画布缩放比例| |​`setZoom()`|设置画布缩放比例| |​`on()`|监听画布事件| |​`requestRenderAll()`|刷新画布| |​`set()`|设置画布属性| #### 对象(Object) |方法|描述| | ------| ------------------------------| |​`bringToFront()`|改变对象层级,置顶| |​`bringForward()`|改变对象层级,上移| |​`sendBackwards()`|改变对象层级,下移| |​`sendToBack()`|改变对象层级,置底| |​`intersectsWithObject()`|检测两个对象是否相交(重叠)| |​`isContainedWithinObject()`|检测对象是在另外一个对象内| |​`on()`|监听对象事件| |​`set()`|设置对象属性| |​`toDataURL()`|转换为 Base64| ## 创建对象 ### 画布 ```javascript let canvas = new fabric.Canvas({ // 画布宽度 width: 800, // 画布高度 height: 480, // 背景颜色 backgroundColor: '#FFFFFF', // 激活对象时不将对象置顶 preserveObjectStacking: true, // 选区框样式 selectionBorderColor: '#4CAF50', selectionColor: '#4CAF5040', selectionFullyContained: true, selectionLineWidth: 1, // 关闭按比例缩放 uniformScaling: false, // 其他配置选项 ... }); ``` ### 矩形 ```javascript // 创建对象 let el = new fabric.Rect({ left: 100, // 相对于画布左侧的距离 top: 100, // 相对于画布顶部的距离 width: 200, // 对象宽度 100px height: 100, // 对象高度 100px fill: '#FFFFFF', // 填充颜色 stroke: '#1E88E5', // 边框颜色 strokeWidth: 2, // 边框粗细 }); // 添加对象 canvas.add(el); ``` ### 线段 ```javascript // 创建对象 let el = new fabric.Line([ 100, 100, // 起始点坐标 200, 100, // 结束点坐标 ], { stroke: '#1E88E5', // 线段颜色 strokeWidth: 2, // 线段粗细 }); // 添加对象 canvas.add(el); ``` ```javascript // 创建对象 let el = new fabric.Line([], { stroke: '#1E88E5', strokeWidth: 2, }); // 设置位置和长度 el.set({ left: 100, top: 100, width: 100, // 注:因为是线段,高度为 0 height: 0, }); // 添加对象 canvas.add(el); ``` ### 图片 ```javascript let img = new Image(); // 监听图片加载,加载完成后再添加对象 img.onload = function() { // 创建对象 let el = new fabric.Image(img, { top: 100, left: 100, width: 200, height: 150, }); // 添加对象 canvas.add(el); }; // 设置 crossorigin 属性,开启 CORS 校验 // 若不设置,无法将画布或画布上的对象导出为图片。 // 值设置为 anonymous,表示对此元素的 CORS 请求将不设置凭据标志。 img.setAttribute('crossorigin', 'anonymous'); // 设置图片地址 img.setAttribute('src', 'http://fabricjs.com/assets/69.svg'); ``` ### 文本框 ```javascript // 创建对象 let el = new fabric.Textbox('文本内容', { top: 100, left: 100, // 设置宽度后,文本内容超出宽度后会自动换行。 // 注意:文本只会在空格处自动换行。 // 高度为自适应,不需要设置。 width: 200, }); // 添加对象 canvas.add(el); ``` ## 场景和实现方法 ### 画布内容撤销、恢复 主要通过 Fabric.js 画布对象的 `toObject()`​ 和 `loadFromJSON()` 方法实现。 为了提高性能,默认情况下 `toObject()`​ 只会导出对象上**常用**的属性,其他属性(例如 `data`​、`selectable`​)不会被导出,需要手动在其 `propertiesToInclude`​ 参数中指定(例如 `canvas.toObject(['data', 'selectable'])`)。 ### 画布内容导出为图片 将画布(显示区域)或对象导出为图片: ```javascript toDataURL(options) ``` 参数:options(可选) |属性名|数据类型|默认值|简介| | ---------------------| ----------| --------| -----------------------------------------| |format|String|png|导出图片的格式,可选值:jpeg、png| |quality|Number|1|图片质量(0 \~ 1),仅 jpeg 格式可用| |multiplier|Number|1|缩放比例| |left|Number||裁剪左偏移量(1.2.14+)| |top|Number||裁剪上偏移量(1.2.14+)| |width|Number||裁剪宽度(1.2.14+)| |height|Number||裁剪高度(1.2.14+)| |enableRetinaScaling|Boolean||为克隆图像启用 Retina 缩放(2.0.0+)| 用法示例: ```javascript let dataURL = canvas.toDataURL({ format: 'jpeg', quality: 0.8, }); ``` ```javascript let dataURL = rect.toDataURL({ format: 'png', }); ``` 注意: 若导出的对象中包含使用 URL(非 DataURL)加载的图片对象,需要使用 `crossOrigin` 属性解决资源跨域问题,否则会出现以下错误提示: ```plaintext The canvas has been tainted by cross-origin data. ``` 其中一种解决方式是先使用 `new Image()`​ 设置 `crossorigin`​ 属性,加载图片,再创建 `fabric.Image` 对象(参考 “创建对象 - 图片”)。 ### 画布自适应父 DOM 元素宽高 #### 方案一:监听浏览器窗口大小变化事件 ```javascript // 父元素(DOM) let el = vm.$el; // 画布对象 let canvas = vm.canvasInstance; // 防抖定时器 let debounce = null; // 事件处理函数 const handler = function () { clearTimeout(debounce); debounce = setTimeout(() => { if (canvas) { // 设置宽高 canvas.setWidth(el.clientWidth); canvas.setHeight(el.clientHeight); // 刷新画布(正常情况下设置宽高后会自动刷新) // canvas.requestRenderAll(); } else { console.error('处理失败,画布对象不存在!'); } }, 500); }; // 添加事件监听 window.addEventListener('resize', handler); // 移除事件监听 window.removeEventListener('resize', handler); clearTimeout(debounce); ``` #### 方案二:使用 ResizeObserver 监听 DOM 元素大小更改 参考资料:[ResizeObserver - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver) 方案优点:在窗口大小不变化的情况下也能实现自适应 ```javascript // 父元素(DOM) let el = vm.$el; // 画布对象 let canvas = vm.canvasInstance; // 防抖定时器 let debounce = null; /** * @desc 观察器回调函数 * @type {ResizeObserverCallback} */ const handler = function (entries) { clearTimeout(debounce); debounce = setTimeout(() => { const entry = entries[0]; if (canvas && entry) { const { width, height } = entry.contentRect; // 设置宽高 canvas.setWidth(width); canvas.setHeight(height); // 刷新画布(正常情况下设置宽高后会自动刷新) // canvas.requestRenderAll(); } else { console.error('处理失败,Canvas 或 ResizeObserverEntry 不存在!'); } }, 500); }; /** * @desc 观察器配置选项 * @type {ResizeObserverOptions} */ const options = { // 设置观察器以哪种盒子模型来观察变化 // 可选值:content-box(默认)、border-box、device-pixel-content-box box: 'border-box', }; // 创建观察器,并传递回调函数 const observer = new ResizeObserver(handler); // 传递配置选项 observer.observe(el, options); // 停止观察器 observer.disconnect(); clearTimeout(debounce); ``` ### 解决画布缩放后,对象显示模糊 把对象的 `objectCaching`​ 属性设置为 `false`,关闭缓存即可。 例如: ```javascript const rect = new fabric.Rect({ left: 100, top: 100, width: 200, height: 200, fill: '#FFFFFF', stroke: '#66CCFF', strokeWidth: 2, objectCaching: false, }); canvas.add(rect); ``` 注意:不建议大量使用,以免影响性能。 ### 实现对象溢出隐藏(裁剪)效果 主要用到了 `clipPath` 功能,设置对象的裁剪路径。 ```javascript // 创建用于作为“容器”的对象 const container = new fabric.Rect({ left: 0, top: 0, width: 800, height: 480, fill: '#FFFFFF', stroke: '#000000', strokeWidth: 2, // 将该属性设置为 true 后,裁剪的区域会动态更新 absolutePositioned: true, }); // 创建子对象 const item = new fabric.Rect({ left: 100, top: 100, width: 200, height: 100, fill: '#FFFFFF', stroke: '#66CCFF', strokeWidth: 2, // 将 container 对象设为该对象的 clipPath clipPath: container, }); ``` **关于** **​`absolutePositioned`​**​ **属性:** 仅当对象作为 `clipPath`​ 使用时才有意义。如果为 `true`​,`clipPath`​ 的位置将会相对于画布,且不受对象变换影响。([JSDoc: Class: Object#absolutePositioned](http://fabricjs.com/docs/fabric.Object.html#absolutePositioned)) **注意:** 后续通过 `.set()`​ 更新对象的 `clipPath`​ 属性时,建议同时将其 `dirty`​ 属性设置为 `true`​,以便清除缓存,防止裁剪区域没有更新。([JSDoc: Class: Object#dirty](http://fabricjs.com/docs/fabric.Object.html#dirty)) ```javascript item.set({ clipPath: null, dirty: true }); ``` ### 鼠标滚轮缩放画布 主要用到了画布的鼠标事件(`mouse:wheel`​)以及调用画布的 `zoomToPoint()` 方法改变视图缩放。 ```javascript // 监听事件:鼠标滚轮 canvas.on('mouse:wheel', function (ev) { const evt = ev.e; const cursorX = evt.offsetX; const cursorY = evt.offsetY; // 若按下 Ctrl 键,则为缩放 if (evt.ctrlKey) { evt.preventDefault(); } else { return; } // 若按下 Alt 键,则为重置缩放 if (evt.altKey) { canvas.zoomToPoint({ x: cursorX, y: cursorY, }, 1); return; } // 正数:向下滚动 // 负数:向上滚动 const delta = evt.deltaY; // 画布当前缩放值 let zoom = canvas.getZoom(); let min = 0.1; let max = 10; zoom += zoom * (delta > 0 ? -0.2 : 0.2); // 限制缩放范围 (zoom < min) && (zoom = min); (zoom > max) && (zoom = max); // 设置画布缩放比例 // 参数1:将画布的缩放点设置为鼠标当前位置 // 参数2:传入缩放值 canvas.zoomToPoint({ x: cursorX, y: cursorY, }, zoom); }); ``` ### 鼠标拖拽移动画布 主要用到了画布的鼠标事件(`mouse:down`​、`mouse:move`​ 和 `mouse:up`​)以及调用画布的 `setViewportTransform()` 方法改变视图偏移量。 注意:在拖拽前,若鼠标选中了对象,需要将对象的 `lockMovementX`​ 和 `lockMovementY`​ 属性设置为 `true`,锁定对象的移动,否则拖拽后对象的位置会变化。 ```javascript // 监听事件:鼠标按下 canvas.on('mouse:down', function ({ e: evt, target }) { // 仅在按下了 Ctrl 键时为拖拽画布 if (!evt.ctrlKey) { return; } if (target) { if (target.lockMovementX || target.lockMovementY) { // 记录原始锁定状态 target.isLock = true; } else { // 记录原始锁定状态 target.isLock = false; // 锁定对象的移动 target.set({ lockMovementX: true, lockMovementY: true, }); } } canvas.set({ // 关闭选区框 selection: false, // 自定义属性,标记拖拽状态 isDragCanvas: true, // 自定义属性,记录起始位置 startPosX: evt.clientX, startPosY: evt.clientY, startVpt: [...canvas.viewportTransform], }); }); // 监听事件:鼠标移动 canvas.on('mouse:move', function (ev) { // 检测是否为拖拽画布模式 if (canvas.isDragCanvas) { const evt = ev.e; const vptOld = canvas.startVpt; const vptNew = [...vptOld]; // 计算新的偏移量 vptNew[4] += (evt.clientX - canvas.startPosX); vptNew[5] += (evt.clientY - canvas.startPosY); // 更新视图偏移量 canvas.setViewportTransform(vptNew); } }); // 监听事件:鼠标松开 canvas.on('mouse:up', function ({ target }) { // 解锁对象的移动(如果可以) if (target && !target.isLock) { target.set({ lockMovementX: false, lockMovementY: false, }); } // 更新画布属性 canvas.set({ // 开启选区框 selection: true, // 自定义属性,标记拖拽状态 isDragCanvas: false, }); }); ``` ### 鼠标事件中获取分组(Group)内的目标对象 默认情况下,对于分组,`ev.target` 只能获取到整个分组,并不能具体到其中的对象。 在创建分组时,将 `subTargetCheck`​ 属性设置为 `true`​,即可通过 `ev.subTargets` 获取触发事件时的子对象。 对于支持该功能的事件(`mouse:down`​、`mouse:up`​、`mouse:move`​ 等),该属性为 `fabric.Object` 数组; 对于不支持该功能的事件,该属性为 `undefined`。 例如: ```javascript const group = new fabric.Group([obj1, obj2], { subTargetCheck: true, ... }); canvas.add(group); canvas.on('mouse:down', function (ev) { console.log(ev.subTargets); }); ``` ### 文本框强制自动换行 Fabeic.js 的文本框对象(Textbox)默认支持文本自动换行,但只能在**空格**处换行,对于类似中文句子这种不带空格的字符串并不会换行,且文本框的宽度会自动被撑开。 在 Fabric.js 2.6.0 版本中,新增了一个 `splitByGrapheme`​ 属性,将其设置为 `true`​ 即可启用在任意字符之间自动换行(参考:[JSDoc: Global](http://fabricjs.com/docs/global.html#splitByGrapheme)),例如: ```javascript const textbox = new fabric.Textbox('文本框', { left: 100, top: 100, width: 200, splitByGrapheme: true, }); canvas.add(textbox); ``` ### 选中并聚焦对象(将对象置于视图的中心) ```javascript // 获取对象的中心点坐标 // 坐标值相对于画布左上角起始点 // 坐标值不受缩放比例影响 const { x: elCenterX, y: elCenterY, } = el.getCenterPoint(); // 获取画布当前的缩放比例 const zoom = canvas.getZoom(); // 计算新的视图坐标 // 中心点坐标值需要与画布缩放比例相乘,与视图统一 // 画布坐标的起始点在左上角,坐标值需要减去画布宽高的一半 const point = { x: elCenterX * zoom - canvas.width / 2, y: elCenterY * zoom - canvas.height / 2, }; // 移动视图 canvas.absolutePan(point); // 选中对象 canvas.setActiveObject(el); ``` ### 选中多个对象,创建选区 主要用到了 Fabric.js 的 `ActiveSelection` 对象,手动创建选区: ```javascript // 画布对象 const canvas = this.canvasInstance; // 选区中包含的对象 const items = [a, b, c, ...]; // 新建一个选区对象 const sel = new fabric.ActiveSelection(items, { // 选区所属的画布 // 必须包含该参数,否则会出现奇怪的问题(不会报错) canvas, // 其他配置选项,例如: // borderColor, // borderDashArray, // opacity, // hasControls, ... }); // 使选区生效 canvas.setActiveObject(sel); ``` ‍