简介:面向前端开发者的 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. 进阶技巧:多选状态、快捷键操作与动画面板
这份源码包里还藏了几个值得拿出去用的细节技巧,能直接提升编辑器质感。
第一个是键盘快捷键。删除键删对象、Ctrl+D 复制对象,这两个交互是编辑器标配。实现思路是监听全局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 项目有帮助。
本文还有配套的精品资源,点击获取