fabric.js 前端实战:把 Canvas 从画布变成对象模型

发布时间:2026/10/23 9:20:15
fabric.js 前端实战:把 Canvas 从画布变成对象模型 简介面向前端开发者的 Fabric.js 源码与示例压缩包适合需要在 HTML5 canvas 上快速构建图形编辑器、数据可视化或在线绘图工具的中高级前端工程师。压缩包整合 Fabric.js 核心库与配套资源共 307 个文件149 个 JavaScript 脚本承载库源码与示例逻辑86 张 PNG 和 40 个 SVG 可用于界面预览与图形测试另含 Markdown、JSON 配置、字体文件及构建辅助工具整体约 14.4MB。通过研读源码和运行示例可以理解 Fabric.js 基于对象模型的绘图方式掌握 canvas 上图形添加、选中、变换与事件处理同时体验 SVG 转 canvas 的解析流程以及 Node.js 环境下的图形渲染。目前已吸引 417 人学习适合希望提升前端图形编程能力并快速搭建交互式 canvas 项目的开发者。借助这批文件能够缩短从 API 了解到实际应用的距离为图表绘制、在线画板等场景提供可直接参考的蓝本。1. fabric.js 前端项目把 Canvas 从一块画布变成一套数据模型某次给内部工具做在线商品图编辑器第一版用原生 Canvas API 写拖拽选中做到一半就翻车了。原因很简单原生 Canvas 是一次性的画布画完就只剩像素没有哪个矩形被拖到哪儿的概念每次交互都得自己维护坐标数组、重绘整个场景。换到 fabric.js 之后同一套功能只用了原来三分之一的行数就收尾了。这份资源是一份完整的前端项目源码包核心就是围绕 fabric.js 展开的可拖拽画布应用适合正在做编辑器、白板、图片标注、海报设计类需求的前端开发者。它能帮你把对象拖拽、缩放、旋转、图层管理这些高频交互在短时间内跑通而不是反复造轮子。2. fabric.js 核心机制从像素画布到对象模型的转变2.1 原生 Canvas 的痛点状态全得自己记先看一段原生 Canvas 的常规操作画一个矩形然后让用户拖走它。// 原生 Canvas画完 信息丢失 const canvas document.getElementById(c); const ctx canvas.getContext(2d); let rects []; // 自己维护数组 function draw() { ctx.clearRect(0, 0, 400, 400); rects.forEach(r ctx.fillRect(r.x, r.y, r.w, r.h)); }这里最麻烦的是rects数组得自己维护每次拖动要算命中、更新数组、再全量重绘。如果场景里有几十个对象、又叠加了图片和文字状态管理会迅速失控。fabric.js 解决的正是这一层每个图形变成一个可感知自身位置、尺寸、旋转角度的事件对象画布负责把它们渲染出来。再往下走原生 Canvas 的命中检测也是个深坑。你得手动遍历所有形状用坐标范围判断鼠标点在了哪个对象上如果有旋转角度还要做逆变换非常烧脑。fabric.js 把这些都封装好了每个对象都有自带的边界检测、旋转矩阵计算和层级排序你只需要调用canvas.getActiveObject()就能拿到当前选中的对象。2.2 fabric.js 的对象模型图形是一等公民fabric.js 把画布抽象成两层fabric.Canvas对应页面上的canvas元素负责渲染、选中、事件派发fabric.Object的各种子类fabric.Rect、fabric.Circle、fabric.Text、fabric.Image等它们以对象形式存在于内存中修改属性后调用canvas.renderAll()即可。对象和对象的组合也有fabric.Group可以把多个子对象绑定成一个整体拖一个等于拖一组。这个模型带来的直接好处是不用手写坐标数组对象的left、top属性就是当前位置内置了命中检测点击一个对象时 fabric 已经帮你算好是否选中换图层顺序、锁定对象、隐藏对象都是属性级操作。底层机制上fabric.js 在初始化时会对canvas元素做一次包装创建一套独立的渲染上下文。每次renderAll()都会清空画布、遍历对象栈、按顺序把每个对象绘制出来。对象上任意属性变化——比如改了fill颜色、angle角度、scaleX缩放——都会触发一次局部重绘不需要你手动控制上下文状态。2.3 快速上手初始化画布与最小示例要拿到这份源码跑起来先做最基础的一步初始化一个可交互画布。import { Canvas, Rect } from fabric; // 第 1 个参数是 canvas 元素的 id const canvas new Canvas(canvasId, { width: 800, height: 600, backgroundColor: #ffffff, selection: true, // 允许框选 preserveObjectStacking: false, }); const rect new Rect({ left: 100, top: 100, width: 160, height: 80, fill: #4A90D9, rx: 4, // 圆角 hasControls: true, // 显示控制点 }); canvas.add(rect); canvas.renderAll();关键参数说明width/height逻辑尺寸真实像素受 devicePixelRatio 影响后面避坑章节会展开selection是否允许用鼠标框选多个对象hasControls控制点开关false 时对象只能移动不能缩放旋转preserveObjectStacking框选多个对象时是否保持原有层级顺序。初始化之后canvas.add()把对象挂到画布上renderAll()触发一次完整绘制。如果后续修改了对象属性也需要再调一次renderAll()让变更落到画布上。3. 实战搭建用 fabric.js 实现一个可拖拽画板3.1 项目文件结构与入口配置这份源码包解压后是一个基于 Vite 的纯前端项目结构大致如下src/ main.js # 入口文件初始化画布 fabric-setup.js # 画布配置与通用方法封装 tools.js # 工具栏添加矩形/圆形/文本/图片 export.js # 导出 JSON 与图片 index.html # 页面容器用npm install安装依赖后npm run dev即可启动。入口处唯一要留意的是new Canvas()必须在 DOM 渲染完成后执行否则拿不到 canvas 元素。实际项目中我一般会把画布初始化单独拆成一个模块方便多个工具方法共用同一个画布实例。下面是最小入口配置// fabric-setup.js import { Canvas } from fabric; export const canvas new Canvas(board, { width: 960, height: 640, backgroundColor: #fafafa, selection: true, preserveObjectStacking: true, }); canvas.on(object:modified, (e) { console.log(对象已变更, e.target.type); });3.2 工具栏动态添加不同类型的图形画板类应用的核心动作是从工具栏点一个按钮往画布上放一个对象。fabric.js 的做法是把每种图形封装成一个工厂函数。// tools.js import { Rect, Circle, Textbox, Image } from fabric; import { canvas } from ./fabric-setup; export function addRect() { const rect new Rect({ left: 50 Math.random() * 200, top: 50 Math.random() * 200, width: 120, height: 80, fill: #4A90D9, stroke: #333, strokeWidth: 2, cornerColor: #4A90D9, cornerSize: 10, }); canvas.add(rect); } export function addCircle() { const circle new Circle({ left: 100, top: 100, radius: 50, fill: #E67E22, stroke: #333, strokeWidth: 2, }); canvas.add(circle); } export function addText() { const text new Textbox(双击编辑文字, { left: 100, top: 100, fontSize: 20, fontFamily: PingFang SC, sans-serif, fill: #333, editable: true, // 双击进入编辑态 }); canvas.add(text); }逻辑说明Textbox和Text的差别值得说——Textbox支持自动换行和框宽拖动适合做文本对象Text更适合单行固定文本。如果需要用户在画布上自由输入长文本选Textbox就够了。editable: true配合canvas.on(text:editing:entered)事件可以拿到进入编辑态的时机。参数说明cornerColor控制点颜色、cornerSize控制点大小这两个参数决定用户缩放旋转时的操作手感。stroke是描边颜色strokeWidth是描边粗细加了描边后视觉上层次会更分明。3.3 事件绑定选中、拖拽、修改后的联动fabric.js 的事件名是按生命周期拆开的mouse:down、object:selected、object:modified。其中object:modified是实际开发中最常用的——对象被拖拽、缩放、旋转结束时会触发。// main.js 里的事件绑定 canvas.on(object:modified, (e) { const target e.target; if (!target) return; console.log(对象变更, target.type, { left: target.left, top: target.top, scaleX: target.scaleX, angle: target.angle, }); updatePropertyPanel(target); }); canvas.on(selection:created, (e) { const obj e.selected[0]; if (obj) showBorder(obj); }); canvas.on(selection:cleared, () { hidePropertyPanel(); });逻辑说明object:modified只会在鼠标松开、对象最终位置确定后触发一次不会在拖拽过程中连续飙升这就保证了属性面板和状态同步不会被打爆。而拖拽过程中的实时预览由 fabric 内部渲染完成不需要额外监听 mousemove。如果需要做拖拽过程中实时显示坐标监听object:moving事件即可但它触发频率极高更新 DOM 时要节流。常见做法是配合requestAnimationFrame做一帧一次更新let rafId null; canvas.on(object:moving, (e) { if (rafId) return; rafId requestAnimationFrame(() { const target e.target; document.getElementById(posX).textContent Math.round(target.left); document.getElementById(posY).textContent Math.round(target.top); rafId null; }); });事件名与适用场景对照如下表事件名触发时机推荐用途mouse:down鼠标按下清空选中、开始绘制object:selected对象被选中显示属性面板selection:cleared选中被取消隐藏属性面板object:moving拖拽过程中持续触发实时坐标显示需节流object:modified拖拽/缩放/旋转结束持久化状态、提交后端3.4 序列化与导出把画板内容保存下来编辑器类应用必须解决用户画了一半刷新后内容还在的问题。fabric.js 提供了两板斧toJSON和loadFromJSON。// export.js import { canvas } from ./fabric-setup; export function saveCanvasToLocal() { const json JSON.stringify(canvas.toJSON([customId, userData])); localStorage.setItem(canvas-state, json); } export function restoreCanvasFromLocal() { const saved localStorage.getItem(canvas-state); if (!saved) return; canvas.loadFromJSON(saved).then(() { canvas.renderAll(); }); }参数说明toJSON()的数组参数是可选的额外属性白名单默认只序列化 fabric 对象的标准属性如果项目里给对象挂了自己的customId必须加在白名单里才会被保留。loadFromJSON返回一个 Promise必须等它 resolve 之后再renderAll()否则画布上会出现空白。图片导出走canvas.toDataURL()export function exportAsPng() { const dataUrl canvas.toDataURL({ format: png, multiplier: 2, // 2 倍分辨率输出 }); const link document.createElement(a); link.download design.png; link.href dataUrl; link.click(); }逻辑说明multiplier: 2适合导出高清图但如果画布里有模糊的位图素材放大 2 倍后位图会更模糊这是素材分辨率决定的不是 fabric 能处理的。导出 JPEG 格式时quality参数控制压缩比const jpegUrl canvas.toDataURL({ format: jpeg, quality: 0.85, });4. 避坑指南fabric.js 项目里最容易翻车的 5 个地方4.1 Canvas 初始化时找不到元素现象new Canvas(board)抛错控制台显示Element with id board is not found。原因脚本放在head里或 DOM 尚未解析完就执行了初始化。解决把初始化放进DOMContentLoaded或把script标签挪到 body 底部。Vite 项目里如果是在main.js顶层直接执行确认index.html中 canvas 元素在 script 之前出现。document.addEventListener(DOMContentLoaded, () { const canvas new Canvas(board); });这个坑在新手项目里出现频率极高尤其是从 CDN 引入脚本的页面script 标签位置不对就会触发。4.2 对象拖不动但点击能选中现象对象边框和控制点都显示鼠标按住拖拽却没有反应。原因fabric.Canvas默认开启了selection但可能被canvas.skipTargetFind或canvas.selection false干扰更隐蔽的是父容器overflow: hidden或 CSS 尺寸与 canvas 实际宽高不一致拖出边界后指针事件失控。解决先检查配置里skipTargetFind是否为true再检查容器 CSS。这里最容易犯的错是给 canvas 元素写了width: 100%但new Canvas()里又传了固定width两者不一致时 fabric 内部坐标会错位。检查方法console.log(canvas.width, canvas.getWidth()); console.log(canvas.getZoom());如果 canvas 逻辑宽度和 CSS 宽度对不上优先统一尺寸设置。4.3 toJSON 导出后图片对象变成空白现象保存 JSON、刷新页面、loadFromJSON后fabric.Image对象渲染不出来。原因canvas.toJSON()默认不包含图片的原始src数据只记录对象的位置和尺寸。图片对象在序列化前需要给图片元素挂上src并标记跨域否则保存的是空引用。解决canvas.toJSON([src, crossOrigin]);同时在加载图片对象时给fabric.Image.fromURL()传入交叉源配置import { Image } from fabric; Image.fromURL(imageUrl, (img) canvas.add(img), { crossOrigin: anonymous, });如果图片来自第三方 CDN服务端必须返回允许跨域的响应头否则即便配置了crossOrigin也会被浏览器拦截。4.4 高清屏上线条模糊现象画布在 Retina 屏幕上边缘发虚控制点位置和鼠标点击有偏差。原因fabric.js 的逻辑尺寸不等于物理像素尺寸没有按window.devicePixelRatio设置画布缩放。解决初始化时按比例缩放const dpr window.devicePixelRatio || 1; const canvas new Canvas(board, { width: 800, height: 600, }); canvas.setDimensions({ width: 800 * dpr, height: 600 * dpr }, { cssOnly: true }); canvas.setZoom(dpr);这样逻辑坐标仍是 800x600但底层渲染分辨率提升了边缘线条会清晰很多。注意setZoom(dpr)之后所有对象缩放系数会同步放大如果项目里依赖scaleX做业务判断要做好换算。4.5 框选多对象后层级顺序被重置现象两个对象 A、B 有重叠A 在上层框选后整体移动A 突然跑到 B 下面。原因fabric.js 默认框选会把所有选中对象移到画布对象栈的末尾保证它们绘制在顶层如果不想改变对象间的相对层级需要开启preserveObjectStacking。解决const canvas new Canvas(board, { preserveObjectStacking: true, });开启后框选只记录选中状态不再调整对象栈顺序层级关系保持稳定。这在高频拖拽的业务场景里几乎是必开的配置项。5. 进阶技巧多选状态、快捷键操作与动画面板这份源码包里还藏了几个值得拿出去用的细节技巧能直接提升编辑器质感。第一个是键盘快捷键。删除键删对象、CtrlD 复制对象这两个交互是编辑器标配。实现思路是监听全局keydown然后检查canvas.getActiveObjects()document.addEventListener(keydown, (e) { if (e.key Backspace || e.key Delete) { canvas.getActiveObjects().forEach(obj canvas.remove(obj)); canvas.discardActiveObject(); canvas.renderAll(); } if ((e.ctrlKey || e.metaKey) e.key d) { e.preventDefault(); const active canvas.getActiveObjects(); Promise.all(active.map(obj obj.clone())).then(clones { clones.forEach(c { c.set({ left: c.left 20, top: c.top 20 }); canvas.add(c); }); }); } });obj.clone()是异步方法返回 Promise所以外面包了一层Promise.all再统一add避免异步时序导致克隆对象缺失。第二个是旋转角度实时回显。canvas.on(object:rotating)事件在旋转过程中持续触发把角度同步到属性面板比松手后再刷新体验好一截canvas.on(object:rotating, (e) { const angle Math.round(e.target.angle); document.getElementById(angleInput).value angle; });第三个技巧是自定义控制点。fabric.js 默认提供四角缩放和顶部旋转点但也可以隐藏某些控制点rect.setControlsVisibility({ mt: false, // 上边中点的缩放柄 mb: false, ml: false, mr: false, });如果设计的是只能整体缩放的长图横幅关掉水平或者垂直方向的缩放柄用户就不会把比例拉坏。四个角仍然保留缩放能力视觉和管理上都干净。当初我在这份项目上最深刻的教训就是永远不要在object:modified回调里做复杂计算。第一次接属性面板同步时我在回调里直接读了toJSON()再做 diff结果对象一多页面明显卡顿。那次之后我给所有同步逻辑都加了requestAnimationFrame节流每秒最多刷新一次面板。从那以后我每次接这种画布类需求都会强制走一遍先配preserveObjectStacking再确认devicePixelRatio缩放最后才写业务逻辑。希望这些细节对你复现这份 fabric.js 项目有帮助。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询