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

 

posted @ 2022-04-28 16:31  名字不好起啊  阅读(200)  评论(4编辑  收藏  举报