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 },
];
}
}

浙公网安备 33010602011771号