fabric 组合group 增加边框
本身是没有边框属性支持的,所以想要边框就得手写:
大体逻辑:
1、组合时,额外增加 rect 模拟边框样式,保存最初组合时 left top width height 属性
2、拆分时,需计算元素 top left(缩放等属性,因为group 移除元素时使用了 removeWithUpdate,故省却计算)
3、更新边框属性时,需删除对应边框(通常为组合最后一个元素),再根据当前 left top 和 历史 width height 计算出边框值添加到组合中
代码:
import { fabric } from "fabric"; import { KeyCode } from "./key-code"; /** * 组合 * 使用添加 边框方法,模拟 group 的边框 * 组合时,额外增加 rect 模拟边框样式,保存最初组合时 left top width height 属性 * 拆分时,需计算元素 top left(缩放等属性,因为group 移除元素时使用了 removeWithUpdate,故省却计算) * 更新边框属性时,需删除对应边框(通常为组合最后一个元素),再根据当前 left top 和 历史 width height 计算出边框值添加到组合中 */ export class GroupBoxTest { constructor(canvas) { // 创建多个 rect 对象 let rect = new fabric.Rect(GroupBoxTest.defaultRect('red', 100)); let rect1 = new fabric.Rect(GroupBoxTest.defaultRect('yellow', 250)); let rect2 = new fabric.Rect(GroupBoxTest.defaultRect('blue', 400)); canvas.add(rect, rect1, rect2); // 绑定键盘事件 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.W: // 组合 // 获取选中元素 objs = canvas.getActiveObjects(); // 增加判断只能组合未组合图形 let hasGroup = false; objs.forEach((item) => { if (item.id.match(/group/)) { hasGroup = true; return false; } }); if (hasGroup) { return false; } // 清除活动对象,活动对象已成组,会导致位置紊乱 canvas.discardActiveObject(); // 再进行组合 let group = new fabric.Group(objs, { id: GroupBoxTest.getId('group'), lockRotation: true, // 组合元素不给旋转 }); // 给组合增加 矩形背景,用于设置 模拟组容器边框等属性 let groupInfo = { width: group.width - 1, height: group.height - 1, left: group.get('left'), top: group.get('top') }; let borderInfo = new fabric.Rect(GroupBoxTest.defaultBorder(groupInfo)); group.addWithUpdate(new fabric.Rect(borderInfo)); canvas.add(group); // 删掉之前对象 let ids = objs.map((o) => { return o.id; }); canvas.getObjects().forEach((item) => { if (ids.includes(item.id)) { canvas.remove(item); } }); canvas.setActiveObject(group).renderAll(); break; case KeyCode.E: // 拆分 objs = canvas.getActiveObjects(); if (objs.length === 1 && objs[0].id.match(/group/)) { // 组标志,同时拆分1 个组 canvas.discardActiveObject().renderAll(); let group = objs[0]; let childrenLen = group.size(); // 删除边框 group.removeWithUpdate(childrenLen - 1); childrenLen -= 1; // 处理内部元素 while (childrenLen > 0) { let child = group.item(childrenLen - 1); this.deleteGroup(child); child.setCoords(); canvas.add(child); childrenLen--; } canvas.remove(group).renderAll(); canvas.discardActiveObject(); } break; case KeyCode.R: // 更新表框属性 objs = canvas.getActiveObjects(); if (objs.length === 1 && objs[0].id.match(/group/)) { let group = objs[0]; let childrenLen = group.size(); group.removeWithUpdate(group.item(childrenLen - 1)); let colorA = ['purple', 'grey', 'green', 'lightblue', 'orange', 'red']; let r = Math.round(Math.random() * 20); let sw = Math.round(Math.random() * 10); let c = Math.round(Math.random() * 5); let borderInfo = { rx: r, ry: r, width: group.width - 1, height: group.height - 1, left: group.left, top: group.top, stroke: colorA[c], strokeWidth: sw, }; group.addWithUpdate(new fabric.Rect(GroupBoxTest.defaultBorder(borderInfo))); canvas.renderAll(); } break; } }); } /** * 删除组信息,并计算元素相对信息数据 * @param ele */ deleteGroup(ele) { ele.top = ele.top + ele.group.top + ele.group.height * 0.5; ele.left = ele.left + ele.group.left + ele.group.width * 0.5; ele.width = ele.width * ele.scaleX; ele.height = ele.height * ele.scaleY; ele.scaleX = 1; ele.scaleY = 1; delete ele.group; } /** * 获取 id = type_time * @param type * @returns {string} */ static getId(type) { return type + '_' + new Date().getTime(); } /** * 获取默认测试矩形 * @param color * @param left * @returns {{top: number, left, width: number, id: string, fill, height: number}} */ static defaultRect(color, left) { return { fill: color, height: 100, left: left, top: 100, width: 100, id: GroupBoxTest.getId('rect'), } } /** * 获取模拟边框默认样式 * @param info * @returns {{strokeWidth: number, top: number, left: number, rx: number, ry: number, width: *, fill: string, stroke: string, height: *}} */ static defaultBorder(info) { let rx = info.rx || 0; let ry = info.ry || 0; let strokeWidth = info.strokeWidth || 1; let stroke = info.stroke || '#000'; return { ry: rx, rx: ry, width: info.width + rx + strokeWidth, height: info.height + ry + strokeWidth, left: info.left - rx * 0.5 - strokeWidth, top: info.top - ry * 0.5 - strokeWidth, fill: 'transparent', stroke: stroke, strokeWidth: strokeWidth, } } }