外观
Fabric.js
介绍
Fabric.js 是一个基于 HTML5 Canvas 的交互式对象模型库。它在原生 Canvas 之上抽象出 图形对象,并提供画布(Canvas)、选择、分组、序列化等能力,使开发者无需手动维护 Canvas 绘制状态即可搭建编辑类应用。
Fabric.js 的 Canvas 对象模型由两类核心组成:
fabric.Canvas:交互式画布,支持选中、拖拽、缩放、旋转等操作,并会渲染对象周围的控制器与控制条。适用于编辑器主界面。fabric.StaticCanvas:静态画布,不绑定任何交互逻辑,只负责渲染,适合只读展示场景(如分享页)。
两者 API 高度一致,可以比较方便地替换使用。
图形对象与坐标变换
Fabric.js 中的每个图形都是一个 FabricObject 子类实例,例如 fabric.Rect、fabric.Circle、fabric.Triangle、fabric.Polygon。它们共享一套属性:
- 位置:
top/left表示对象左上角在画布坐标中的位置。 - 尺度:
scaleX/scaleY表示对象的缩放系数,与width/height相乘得到渲染尺寸。 - 样式:
fill(填充色)、stroke(描边)、rx/ry(圆角)等。 - 变换:
angle(旋转角)、originX/originY(变换原点)。
Fabric.js 的对象坐标是本地坐标与画布坐标的复合:对象自身维护一个本地宽高,通过 scaleX/scaleY、旋转与位置变换映射到画布平面。如果你后期打算与 Three.js 联动实现二三维视图切换的话,理解这一点帮助会很大。
画布的初始化
fabric.StaticCanvas 和 fabric.Canvas 这两个类都可以用于画布的初始化。这里我们以 fabric.Canvas 为例进行初始化:
typescript
const canvas = new fabric.Canvas("elementCanvasId", {
// 序列化时省略值为默认值的属性,使导出的 JSON 更精简
includeDefaultValues: false,
// 父级容器的宽高尺寸
width: elemWrapper.clientWidth,
height: elemWrapper.clientHeight,
// 画布背景色
backgroundColor: "#444444",
// 设置为true后可以开启绘画模式,此模式下不支持框选操作
isDrawingMode: projectType.value === 1,
// 阻止画布区域的默认右键菜单
stopContextMenu: true,
// 对象选中时仍保持原有层级顺序展示
preserveObjectStacking: true,
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对象的锁定和解锁
通常来说,你不会希望用户用鼠标不小心点击了画布上的元素就导致元素被意外挪了位置,我们可以让元素默认不可移动来避免这个问题。
typescript
Object.assign(fabric.FabricObject.prototype, fabric.FabricObject.ownDefaults);
fabric.FabricObject.ownDefaults = {
// 不显示控制器
hasControls: false,
// 在水平方向上不可移动
lockMovementX: true,
// 在竖直方向上不可移动
lockMovementY: true,
// ... 其余默认值
};1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
只有进入「移动对象」「编辑对象」等操作时,才对选中的对象临时解锁:
typescript
const activeObjects = canvas.getActiveObjects();
// 移动:解锁位置
activeObjects.forEach((obj) => {
obj.lockMovementX = false;
obj.lockMovementY = false;
});
canvas.requestRenderAll();
// 编辑:显示角部控制器
activeObjects.forEach((obj) => {
obj.hasControls = true;
obj.setControlsVisibility({
mtr: false,
mt: false,
tr: true,
mr: false,
br: true,
mb: false,
bl: true,
ml: false,
tl: true,
});
});
canvas.requestRenderAll();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
当用户取消选中某个或者某些元素后,需要让这些元素恢复锁定状态:
typescript
// 对象被取消选中
canvas.on("selection:cleared", (opt: any) => {
// 对取消选中的对象,去掉编辑控件
const deselected = opt.deselected || [];
deselected.forEach((obj: any) => {
obj.hasControls = false;
obj.lockMovementX = true;
obj.lockMovementY = true;
});
});1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
typescript
// 选中内容有改变(比如先选A,后选B)
canvas.on("selection:updated", async (opt: any) => {
// 对取消选中的对象,去掉编辑控件
const deselected = opt.deselected || [];
deselected.forEach((obj: any) => {
obj.hasControls = false;
obj.lockMovementX = true;
obj.lockMovementY = true;
});
});1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
实现对象创建过程中的实时鼠标预览
如果用户想在指定位置创建产线需要先创建一个产品生成到默认位置,然后再让其进入编辑态移动它,这样的交互体验是不可接受的。
我们需要让用户觉得他点击创建对象的按钮后并没有真的创建对象,只有一个跟随鼠标的预览效果,直到用户再次在他期望的位置上点击鼠标将对象放置在指定位置上,这个创建过程才算完成。
添加对象时,先在画布外的一个不可见坐标处创建对象,再监听 mouse:move 随鼠标位置实时移动实现预览;点击鼠标(mouse:down)后移除监听,将对象固定放置在画布:
typescript
const rect = new fabric.Rect({
width: 50,
height: 50,
top: -9999 - height / 2,
left: -9999 - width / 2,
});
canvas.add(rect);
const handlerOnMouseMove = (
opt: fabric.TPointerEventInfo<fabric.TPointerEvent>,
) => {
const pointer = opt.scenePoint;
rect.set("left", pointer.x - width / 2);
rect.set("top", pointer.y - height / 2);
rect.setCoords();
canvas.requestRenderAll();
};
canvas.on("mouse:move", handlerOnMouseMove);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
选择、分组与层级
使用 canvas.getActiveObjects() 获取当前选中的对象;通过 new fabric.ActiveSelection(objects, { canvas }) 将多个对象组成一个选中集合实现全选。层级操作通过 bringObjectToFront / sendObjectToBack 完成上移、下移。
缩放与事件
监听 mouse:wheel 并结合 fabric.Point 实现以鼠标位置为原点的缩放:
typescript
canvas.on("mouse:wheel", (opt) => {
let zoom = canvas.getZoom() * 0.999 ** opt.e.deltaY;
zoom = Math.min(20, Math.max(0.01, zoom));
canvas.zoomToPoint(new fabric.Point(opt.e.offsetX, opt.e.offsetY), zoom);
});1
2
3
4
5
2
3
4
5
编辑器在 mouse:wheel、selection:*、object:moving / rotating / added / removed / modified、mouse:down 等事件上挂载了保存与属性刷新逻辑,并通过 renderAll 或 requestRenderAll 请求重新渲染。
自定义元数据与序列化
Fabric.js 默认不会序列化对象的任意自定义字段。apps/design 通过两处配置把业务元数据 meta 持久化:
fabric.FabricObject.customProperties = ["meta"],声明meta是需要参与序列化的自定义属性。- 序列化时传入
canvas.toObject(["id", "meta"]),显式列出要导出的属性。
导出的 JSON 由后端保存;封面使用 canvas.toSVG() 生成 SVG。加载时通过 await canvas.loadFromJSON(data) 还原,随后调用 renderAll 刷新。
meta 承载了二三维共同需要的业务字段(见 Three.js(3D 可视化) 如何消费这份数据),例如 DesignObjectMetaCube 包含 id、type、name、depthAboveGround:
typescript
import * as fabric from "fabric";
fabric.FabricObject.customProperties = ["meta"];
const canvas = new fabric.Canvas("editor");
const rect = new fabric.Rect({
width: 50,
height: 50,
fill: "#ffffff",
meta: { id: 1, type: "cube", name: "立方体", depthAboveGround: 0 },
});
canvas.add(rect);
const project = canvas.toObject(["meta"]); // 包含 meta
await canvas.loadFromJSON(project); // 从 JSON 还原
const svg = canvas.toSVG(); // 生成封面 SVG1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
属性树驱动的编辑状态
右侧属性栏由一份「属性树」驱动:对象被选中后,元素实例的 getAttrTree() 生成属性树并写入 store.setAttrTreeList。用户在属性栏修改值时,attribute.setValue(value) 更新对象并触发延迟保存,随后重新获取属性树刷新右侧面板。
常见问题与实践建议
meta不会自动序列化:必须同时配置customProperties并在toObject中显式列出,否则业务字段丢失。- 保存建议做防抖:编辑器在
delaySaveCanvas中用setTimeout延迟 300ms,合并高频移动/旋转事件触发的连续保存。 - 分享页用
StaticCanvas渲染并用toSVG作为封面,避免把完整交互逻辑和原画布绑定暴露给只读端。