fabric 类组合容器
想实现一个类似组合的容器,不过比组合使用起来方便一些,直接拖拽放入容器,可以整体移动 ,内部元素又可拖动在容器内移动
fabric 目前没有这种功能,只能手写
增加容器size 判断,更新关联,重叠层级判断等
缺:嵌套,反序列化维持关联等
代码留念:
import { fabric } from 'fabric'; import { KeyCode } from "./key-code"; /** * 组容器 试验 * 自由组内拖动或拖出 * 元素拖入进组,更新层级关系 * 组容器改变大小,重新处理关联,要注意组的放大比例转换为实际宽高 * 层叠,元素拖拽进组时,关联唯一组 * 组容器 为激活对象时(鼠标点击触发),设置透明度,方便观察内部元素。取消激活时(取消之前触发),还原为不透明,方便观察层级 * * 未实现: * 嵌套的元素归属等 * * 后续待处理 * 包括序列化,反序列化数据关系维护 */ export class SimulationGroupBox { // 组与元素关联数组 groupRelation = []; TYPE_GROUP = 'group'; // 是否 group 修改宽高 isGroupModified = false; // 是否组移动 isGroupMoving = false; // 记录 mouse:down 事件,标记 group 起始位置 groupStartPos = {}; constructor(canvas) { // 重置长宽: canvas.setWidth(1000); canvas.setHeight(800); // 绘制图形 // let groupBox = new fabric.Polyline(SimulationGroupBox.getPoints(), SimulationGroupBox.defaultLineBox()); // 添加 rect this.addRect(canvas); // 绘制组容器 this.addGroupBox(canvas, this.groupRelation); // 绑定画布监听 this.bindCanvasEvents(canvas); // 绑定键盘 this.bindKeyBoard(canvas); } /** * 绑定键盘事件 * @param canvas */ bindKeyBoard(canvas) { $(document).on('keydown', (e) => { const key = e.originalEvent.keyCode; let objs = null; switch (key) { case KeyCode.Q: // 打印显示 console.log(canvas.getObjects()); break; case KeyCode.R: // 添加 rect this.addRect(canvas); break; case KeyCode.G: // 添加 组容器 this.addGroupBox(canvas, this.groupRelation); break; case KeyCode.B: // 更新 组容器 样式属性 objs = canvas.getActiveObjects(); if (objs.length === 1 && objs[0].id.match(new RegExp(this.TYPE_GROUP))) { let group = objs[0]; let r = Math.round(Math.random() * 20); let sw = Math.round(Math.random() * 10); group.set({ rx: r, ry: r, stroke: SimulationGroupBox.getColor(), strokeWidth: sw, }); canvas.renderAll(); } break; case KeyCode.T: // 上一层 objs = canvas.getActiveObjects(); if (objs.length === 1) { objs[0].bringForward(); // img.sendBackwards(); // 向下跳一层 // img.sendToBack(); // 向下跳底层 // img.bringForward(); // 向上跳一层 // img.bringToFront(); // 向上跳顶层 } break; case KeyCode.Y: // 下一层 objs = canvas.getActiveObjects(); if (objs.length === 1) { objs[0].sendBackwards(); } break; } }); } /** * 添加 测试矩形 * @param canvas */ addRect(canvas) { let rect = new fabric.Rect(SimulationGroupBox.defaultRect()); canvas.add(rect); } /** * 添加 组容器 * @param canvas * @param groupRelation */ addGroupBox(canvas, groupRelation) { let groupBox = new fabric.Rect(SimulationGroupBox.defaultGroup()); // 创建组后,维护关系数组 groupRelation.push({ groupId: groupBox.id, group: groupBox, members: [], }); canvas.add(groupBox); } /** * 画布绑定事件 * @param canvas */ bindCanvasEvents(canvas) { // 标记非组元素移动 let isMoving = false; canvas.on('before:selection:cleared', (e) => { if (e.target && e.target.id && e.target.id.match(new RegExp(this.TYPE_GROUP))) { // 还原组容器透明度,方便观察层级 e.target.set('opacity', 1); } }); canvas.on('mouse:down', (e) => { if (e.target && e.target.id && e.target.id.match(new RegExp(this.TYPE_GROUP))) { // 设置组容器透明度,方便显示内部元素 e.target.set('opacity', 0.5); } }); canvas.on('mouse:move', (e) => { if (e.target && e.target.id && e.target.id.match(new RegExp(this.TYPE_GROUP))) { // 标记 group 和关联元素的起始位置 this.groupStartPos = this.getMouseStartPos(e.target, this.groupRelation); } }); // canvas.on('mouse:move', (e) => { // // 可重复绑定多次事件 // console.log(1); // }); canvas.on('object:moving', (e) => { if (e.target && e.target.id && e.target.id.match(new RegExp(this.TYPE_GROUP))) { // 标记 group 移动 !this.isGroupMoving && (this.isGroupMoving = true); this.moveGroupBox(e.target, this.groupStartPos, this.groupRelation); } else if (e.target) { // 标记非 组容器 元素移动 isMoving = true; } }); canvas.on('object:scaling', (e) => { if (e.target && e.target.id && e.target.id.match(new RegExp(this.TYPE_GROUP))) { // 标记 group 宽高改变 !this.isGroupModified && (this.isGroupModified = true); } }); canvas.on('object:modified', (e) => { if ( this.isGroupModified && e.target && e.target.id && e.target.id.match(new RegExp(this.TYPE_GROUP))) { // 更新组信息,将缩放比例更新为实际宽高,后续更新关联元素时,必须使用实际位置信息判断! e.target.set({ width: e.target.width * e.target.scaleX, height: e.target.height * e.target.scaleY, scaleX: 1, scaleY: 1, }); // 更新与元素关联 this.updateGroupRelation(canvas, e.target, this.groupRelation); } }); canvas.on('mouse:up', (e) => { if (e.target && e.target.id && e.target.id.match(new RegExp(this.TYPE_GROUP))) { canvas.renderAll(); // 重置相关标记 this.isGroupModified && (this.isGroupModified = false); if (this.isGroupMoving) { this.isGroupMoving = false; // 清空组容器选中 canvas.discardActiveObject(); canvas.renderAll(); // 更新与元素关联 this.updateGroupRelation(canvas, e.target, this.groupRelation); } this.groupStartPos = {}; } if (isMoving && e.target) { // 移动其他元素,判断是否落在 组容器 中 this.createRelationWithGroup(e.target, this.groupRelation, canvas); // 重置标记 isMoving = false; } }); } /** * 获取鼠标在 组容器 down 时,组容器及内部元素,起始位置 * @param target * @param groupRelation * @returns {{top: *, left: *, members: []}} */ getMouseStartPos(target, groupRelation) { let pos = { top: target.top, left: target.left, members: [], }; // 获取组对应元素,设置移动位置 let group = groupRelation.filter((item) => { if (item.groupId === target.id) { return item; } })[0]; group.members.forEach((m) => { pos.members.push({ top: m.top, left: m.left, }); }); return pos; } /** * 移动元素后,判断元素位置,创建与 组容器 关联,注意 容器组层级,及 关联唯一 容器组 * @param ele * @param groupRelation * @param canvas */ createRelationWithGroup(ele, groupRelation, canvas) { // 保存可创建关联的组 let canRelationIds = []; // 判断元素是否在 group 内部 groupRelation.forEach((item) => { let g = item.group; // 是否存在对应关联 let sub; let i = item.members.filter((m, index) => { if (m.id === ele.id) { sub = index; return m; } }); // 移除关联性 if (i.length) { // 取消关联 item.members.splice(sub, 1); } // 暂时不考虑组的嵌套、重叠问题,判断有误,要限制区间 if (g.top <= ele.top && (ele.top + ele.height <= g.top + g.height) && g.left < ele.left && (ele.left + ele.width <= g.left + g.width)) { // 保存可关联组 canRelationIds.push(item.groupId); } }); if (canRelationIds.length) { let item = this.getUpperGroup(canvas, groupRelation, canRelationIds); if (item) { item.members.push(ele); // 如果当前元素层级在组容器下,则更新层级到组之上 this.updateLevel(canvas, item.group, ele); } } } /** * 重叠情况下,获取更高层级的 组容器 * @param canvas * @param groupRelation * @param canRelationIds * @returns {null|*} */ getUpperGroup(canvas, groupRelation, canRelationIds) { let list = this.getIdIndexList(canvas); let max = -1; let res = ''; canRelationIds.forEach((id) => { if (list[id] > max) { max = list[id]; res = id; } }); let item = groupRelation.filter((item) => { if (item.groupId === res) { return item; } }); if (item.length) { return item[0]; } return null; } /** * 更新层级关系,一对一更新 * @param canvas * @param group * @param target * @param list [id: index],可传入 */ updateLevel(canvas, group, target, list = []) { list.length === 0 && (list = this.getIdIndexList(canvas)); let gIndex = list[group.id]; let tIndex = list[target.id]; if (gIndex > tIndex) { let num = gIndex - tIndex; while(num > 0) { // 上移 target.bringForward(); num--; } } } /** * 获取键值对 [id: index] * @param canvas */ getIdIndexList(canvas) { let list = {}; canvas.getObjects().forEach((item, index) => { list[item.id] = index; }); return list; } /** * 编辑 组容器 宽高后,更新元素关联 * @param canvas * @param target 组容器 * @param groupRelation */ updateGroupRelation(canvas, target, groupRelation) { let group = groupRelation.filter((item) => { if (target.id === item.groupId) { return item; } })[0]; group.members = []; // 获取 id => index 键值对 let list = this.getIdIndexList(canvas); // 要排除已编组成员 let exclude = this.getExcludeIds(groupRelation, target.id); canvas.getObjects().forEach((item) => { // 非自己,非其他 组容器 成员 if (item.id !== target.id && !exclude.includes(item.id) && target.top <= item.top && (item.top + item.height <= target.top + target.height) && target.left < item.left && (item.left + item.width <= target.left + target.width)) { // 如果未关联,则关联 group.members.push(item); // 如果当前元素层级在组容器下,则更新层级到组之上 this.updateLevel(canvas, target, item, list); } }); } /** * * 根据关联,获取其他组成员信息 * @param groupRelation * @param targetId * @returns [ids] */ getExcludeIds(groupRelation, targetId) { let list = []; groupRelation.forEach((item) => { if (item.groupId !== targetId) { let ids = item.members.map((m) => { return m.id; }); list = [...ids]; } }); return list; } /** * 移动 组容器 ,维护组内元素统一移动 * @param target * @param groupStartPos * @param groupRelation */ moveGroupBox(target, groupStartPos, groupRelation) { // 获取差值 let moveLeft = target.left - groupStartPos.left; let moveTop = target.top - groupStartPos.top; // 获取组对应元素,设置移动位置 let group = groupRelation.filter((item) => { if (item.groupId === target.id) { return item; } })[0]; let members = groupStartPos.members; group.members.forEach((m, sub) => { let left = members[sub]['left'] + moveLeft; let top = members[sub]['top'] + moveTop; m.set({ left: left, top: top, }); // 如果您希望更新事件触发区域,则还需要调用setCoords。 m.setCoords(); }); } /** * rect 模拟组 样式 * @returns {{top: number, left: number, rx: number, ry: number, width: number, id: string, fill: string, stroke: string, height: number}} */ static defaultGroup() { return { id: SimulationGroupBox.getId('group'), fill: '#eee', stroke: '#000', rx: 10, ry: 10, top: 100, left: 200, width: 300, height: 300, // opacity: 0.6, 移动时设置透明度 }; } /** * 简单矩形 */ static defaultRect() { return { id: SimulationGroupBox.getId('rect'), fill: SimulationGroupBox.getColor(), left: 50, top: 50, width: 100, height: 100, }; } /** * 获取 id = type_time * @param type * @returns {string} */ static getId(type) { return type + '_' + new Date().getTime(); } /** * 获取随机颜色 * @returns {string} */ static getColor() { let arr = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' ]; let a = Math.round(Math.random() * 15); let b = Math.round(Math.random() * 15); let c = Math.round(Math.random() * 15); let d = Math.round(Math.random() * 15); let e = Math.round(Math.random() * 15); let f = Math.round(Math.random() * 15); return '#' + arr[a] + arr[b] + arr[c] + arr[d] + arr[e] + arr[f]; } /** * 组边框样式 * @returns {{eleType: string, fill: string, stroke: string}} */ static defaultLineBox() { return { eleType: 'group', fill: '#eee', // 透明 stroke: '#000', } } /** * 容器组边框 * @returns {[{x: number, y: number},{x: number, y: number},{x: number, y: number},{x: number, y: number},{x: number, y: number}]} */ static getPoints() { // 保存这下所有点的起点和终点坐标 return [ { x: 300, y: 100 }, { x: 600, y: 100 }, { x: 600, y: 400 }, { x: 300, y: 400 }, { x: 300, y: 100 }, ]; } }