浏览器实现大画布操作

首先这个画布是超出浏览器画布的限制的最大范围的;
需求:在一个大画布上标注 画矩形;还是使用的fablicjs库;可以查看我的另一个文章 详细介绍了使用
fablicjs画矩形和多边形,这篇主要是讲述我完成大画布功能的过程;

准备工作

首先我需要知道各大浏览器对canvas的限制

那么重点来了,考虑怎么完成大画布呢?我采用的方法是分割画布

正式开始

  1. 怎么分割
    因为我的画布是纵向的,所以我只考虑高度分割,宽度撑满容器的宽度即可;
    我们先假设画布90000,每个画布高1000;也就是说我们分割9个画布,先完成canvas的渲染;并且给每一个canvas绑定事件
// 在HTML中创建一个包含分割画布的容器元素,例如:
<div id="canvas-container" />

const canvasWidth = 1000; // 画布宽度
const canvasHeight = 90000; // 画布高度
const blockSize = 1000; // 小块宽度和高度

  // 计算需要分割的小块数
  const numBlocks = Math.ceil(canvasHeight / blockSize);

  // 循环创建和显示小块画布
  private mounted () {
    this.init();
  }

  private init () {
    // 先清空之前的canvas 和数组
    const canvasContainer = document.getElementById('canvas-container') as any;
    canvasContainer.innerHTML = '';
    this.canvases = [];
    // 循环创建和显示小块画布
    for (let i = 0; i < this.numBlocks; i++) {
      const canvasElement = document.createElement('canvas');
      canvasElement.id = `canvas-${i}`;
      canvasElement.width = this.width;
      // 假设不是整数的话 看最后还剩下多少高度
      canvasElement.height = i < this.numBlocks - 1 ? this.blockSize : this.height - this.blockSize * (this.numBlocks - 1);
      const canvasContainer = document.getElementById('canvas-container') as any;

      // 添加每个部分的画布对象到画布容器中
      canvasContainer.appendChild(canvasElement);

      const canvas = new fabric.Canvas(`canvas-${i}`, {
        width: this.width,
        height: i < this.numBlocks - 1 ? this.blockSize : this.height - this.blockSize * (this.numBlocks - 1)
      });
	  // 给canvas绑定事件
      canvas.on('mouse:down', (e) => { this.mouseDown(e, i); });
      canvas.on('mouse:move', (e) => { this.mouseMove(e, i); });
      canvas.on('mouse:up', this.mouseUp);
      canvas.on('selection:updated', e => {
        this.edit(e.target);
      });
      canvas.on('selection:created', e => {
        this.edit(e.target);
      });
      canvas.on('object:modified', this.onChange);
      canvas.on('object:scaling', e => {
        this.onScaling(e.target, i);
      });
      canvas.on('object:moving', e => {
        this.mouseMoving(e.target, i);
      });
      // canvases存放所有的画布
      this.canvases.push(canvas);
    }
  }


// 注意需要在代码中引入 fabric.js 库,并确保正确加载。
`
  1. 我们接下来就可以完成跨画布画矩形了
    在这个步骤中,我们面临的情况比较多
    原本不跨画布->跨画布
    原本不跨画布->跨画布->不跨画布
    从各个方向的绘画

我的解决办法:跨的画布的图形 他们的的高度都是一致的。只是他们的top不一致罢了 同时给他们同一个uuid,用于之后给后端传数据的时候 同一个uuid的数据只需要一个即可;

  private mouseDownRect (position: MousePosition, index: number) {
    if (this.drawingObject) {
      // 点击第二个点的时候
      this.drawEnd(this.drawingObject);
      return;
    }
    // 点击第一个点的时候  记录下来点击的第几个画布
    this.activeCanvas = this.canvases[index];
    const rect = this.drawRect([position.x, position.y, 0, 0]);
    this.drawingObject = rect;
    this.activeCanvas.add(rect);
  }

  // 鼠标移动
  private mouseMove (e, index) {
    if (!this.drawType || !this.drawingObject) {
      return;
    }
    this[`mouseMove${this.drawType}`](this.transformMouse(e), index);
  }

  private mouseMoveRect (position: MousePosition, mouseMoveCanvasIndex: any) {
    if (this.drawingObject) {
      // 从第几个画布开始绘画的
      const startIndex = Number(this.activeCanvas.lowerCanvasEl.id.split('-')[1]);

      // 检查是否跨越相邻画布并绘制矩形  提到上面  因为取消渲染的时候也需要判断是从上到下还是从下到上
      // getCanvasFromCoordinates 是返回当前结束的坐标在第几个画布
      const adjacentCanvas = this.getCanvasFromCoordinates(position.x, position.y + mouseMoveCanvasIndex * this.blockSize);
      // adjacentCanvas 返回null  则不执行下面
      if (!adjacentCanvas) {
        return;
      }
      const adjacentCanvasIndex = Number(adjacentCanvas.lowerCanvasEl.id.split('-')[1]);

	  // 如果一直在跨了画布。看是从上到下 还是从下到上
      const fromTopToBottom = startIndex < adjacentCanvasIndex;
      // 最后一个画布的index不是最开始的index
      if (this.adjacentCanvasIndex > -1 && this.adjacentCanvasIndex !== startIndex) {
        // 从上往下  清除下面的框。从下往上 清楚上面的框【不然每次都会在所跨画布新增多个矩形】
        const clearBottom = startIndex < this.adjacentCanvasIndex;
        // for (let i = startIndex + 1; i < this.adjacentCanvasIndex + 1; i++) {
        for (let i = clearBottom ? (startIndex + 1) : (startIndex - 1);
          clearBottom ? (i < this.adjacentCanvasIndex + 1) : (i > this.adjacentCanvasIndex - 1);
          clearBottom ? i++ : i--) {
          const draweredItem = this.canvases[i].getObjects().filter(x => x.uuid === this.drawingObject.uuid)[0];
          this.canvases[i].remove(draweredItem);
        }
      }

      if (adjacentCanvas && adjacentCanvas !== this.activeCanvas) {
        // for (let i = startIndex + 1; i < adjacentCanvasIndex + 1; i++) {
        for (let i = fromTopToBottom ? startIndex + 1 : startIndex - 1;
          fromTopToBottom ? i < adjacentCanvasIndex + 1 : i > adjacentCanvasIndex - 1;
          fromTopToBottom ? i++ : i--) {
          // 重新绘画第一个画布的矩形  修改高度为图形的总高度
          const width = position.x - this.drawingObject.get('left');
          const height = position.y + (mouseMoveCanvasIndex - startIndex) * this.blockSize - this.drawingObject.get('top');

          this.drawingObject.set({
            width: width,
            height: height,
            adjCanvasNumber: adjacentCanvasIndex - startIndex
          });
          this.activeCanvas.renderAll();

          // 已经渲染过的
          // const draweredItem = this.canvases[i].getObjects().filter(x => x.uuid === this.drawingObject.uuid)[0];
          // this.canvases[i].remove(draweredItem);

	      // 绘制跨画布的矩形 同一个uuid 同时记录他们跨了几个画布adjCanvasNumber;
          const adjObject = this.drawRect([
            this.drawingObject.get('left'),
            -((this.blockSize * (i - startIndex)) - this.drawingObject.get('top')),
            position.x - this.drawingObject.get('left'),
            height],
          {
            uuid: this.drawingObject.uuid,
            adjCanvasNumber: adjacentCanvasIndex - startIndex
          });
          this.canvases[i].add(adjObject);
          // 最后的画布记录下来 如果用户跨画布 但是又取消跨画布 需要清楚所跨的画布的图形
          if (i === adjacentCanvasIndex) {
            this.adjacentCanvasIndex = i;
          }
        }
      } else {
      	// 没有跨画布 则正常处理
        const width = position.x - this.drawingObject.get('left');
        const height = position.y - this.drawingObject.get('top');
        this.drawingObject.set({
          width: width,
          height: height,
          adjCanvasNumber: 0
          // startCanvansIndex: startIndex
        });
        this.activeCanvas.renderAll();
      }
    }
  }

  // 获取包含指定坐标的画布
  private getCanvasFromCoordinates (x, y) {
    for (let i = 0; i < this.canvases.length; i++) {
      const canvas = this.canvases[i];
      const localPoint = canvas.getPointer({ x: x, y: y });
      const canvasElement = canvas.getElement();
      const canvasRect = canvasElement.parentElement.getBoundingClientRect();
      if (
        localPoint.x >= 0 &&
      localPoint.x <= canvasRect.width &&
      localPoint.y >= 0 &&
      localPoint.y <= canvasRect.height
      ) {
        return canvas;
      }
    }
    return null;
  }

  // 画四边形 originTop是用于缩放和移动的时候新增的字段 
  private drawRect (points: Array<number>, others?: OthersConfigModel) {
    const rect = new fabric.Rect({
      type: DrawType.Rect,
      uuid: createUuid(), // todo
      left: points[0],
      top: points[1],
      width: points[2] || 0,
      height: points[3] || 0,
      objectCaching: false,
      transparentCorners: false,
      selectionColor: 'rgba(0,0,0,0)',
      lockRotation: true,
      strokeUniform: true,
      // 移动时候需要 记录他移动了多少 来让同一个矩形uuid也移动
      originLeft: points[0],
      originTop: points[1],
      ...defaultRectStyle,
      ...this.rectStyle,
      ...others
    });
    // eslint-disable-next-line spellcheck/spell-checker
    rect.setControlsVisibility({ mtr: false }); // 隐藏旋转点
    return rect;
  }
  1. 处理缩放+移动
    原本一个画布->缩放跨画布
    原本一个画布->缩放跨画布->取消跨画布
    原本跨画布->缩放为一个画布
    跨多个画布的缩放
    不同方向的缩放

在处理缩放的时候 一开始面临了一个问题 就是假设是从上到下绘画的话 移动上面的矩形就会出现bug

  private onScaling (object, index: number) {
    // 得到所有同一个uuid的图形
    const sameUuidObject = this.getObjectByUuid(object.uuid);
    sameUuidObject.map(item => {
      item.shape.set({
        scaleX: object.scaleX,
        scaleY: object.scaleY,
        left: object.left// 万一用户拉的左侧的,
      });
      this.canvases[item.canvasIndex].renderAll();
    });
    // 跨canvas的时候 防止他缩放的是上面 带来的bug
    this.mouseMoving(object, index);
  }

移动的情况
不能移动出超出范围外;左右不能移动出去,第一个和最后一个画布不能移动到上面/下面

private mouseMoving (object, index) {
    const padding = -1.5; // 内容距离画布的空白宽度,主动设置
    // 限制左右不能移出外面
    object.setCoords();
    const objBoundingBox = object.getBoundingRect();
    if (objBoundingBox.left < padding) {
      object.left = Math.max(object.left, object.left - objBoundingBox.left + padding);
    }
    if (objBoundingBox.left + objBoundingBox.width > object.canvas.width - padding) {
      object.left = Math.min(object.left, object.canvas.width - objBoundingBox.width + object.left - objBoundingBox.left - padding);
    }

    // 如果是第一块  限制不能移到上面
    if (!index && objBoundingBox.top < 0) {
      object.top = Math.max(object.top, object.top - objBoundingBox.top);
    } else if (index === this.numBlocks - 1 && objBoundingBox.top + objBoundingBox.height > object.canvas.height) {
      // 如果是最后一块 不能移动到下面
      object.top = Math.min(object.top, object.canvas.height - objBoundingBox.height + object.top - objBoundingBox.top);
    }

    // 跨框的情况下
    if (object.adjCanvasNumber) {
      const sameUuidObject = this.getObjectByUuid(object.uuid);
      sameUuidObject.map(item => {
        item.shape.left = object.left;
        item.shape.top = item.shape.originTop - (object.originTop - object.top);
        // 防止框一闪一闪的
        item.shape.visible = true;
        this.canvases[item.canvasIndex].renderAll();
        this.canvases[item.canvasIndex].setActiveObject(item.shape);
      });
      // // 原本跨多个canvas 移动到下个画布 会消失
      // if (object.adjCanvasNumber > 1) {
      const adjCanvasNumber = Math.floor((object.top + object.height * object.scaleY) / this.blockSize);
      if (index + adjCanvasNumber > object.adjCanvasNumber + sameUuidObject[0].canvasIndex) {
        const lastRect = sameUuidObject[object.adjCanvasNumber].shape;

        // 如果在同一个画布停顿两次 第一次就会留下
        const draweredItem = this.canvases[index + adjCanvasNumber].getObjects().filter(x => x.uuid === object.uuid)[0];

        if (draweredItem) {
          return;
        }
        lastRect.set({
          originTop: -(this.blockSize - lastRect.originTop)
        });
        const adjObject = this.drawRect([
          object.left,
          -(this.blockSize - lastRect.originTop),
          object.width,
          object.height],
        {
          uuid: object.uuid,
          adjCanvasNumber: object.adjCanvasNumber
        });
        // 设置为同样的缩放比例
        adjObject.set({
          scaleX: object.scaleX,
          scaleY: object.scaleY
        });
        this.canvases[index + adjCanvasNumber].add(adjObject);
        this.canvases[index + adjCanvasNumber].setActiveObject(adjObject);
        // 移动/缩放 导致跨的画布+1了
        sameUuidObject.map(item => {
          item.shape.adjCanvasNumber = object.adjCanvasNumber + 1;
        });
      }
      // 多个画布的向上移动
      if (object.top < 0) {
        const draweredItem = this.canvases[index - 1].getObjects().filter(x => x.uuid === object.uuid)[0];

        if (draweredItem) {
          return;
        }
        const adjObject = this.drawRect([
          object.left,
          this.blockSize + object.originTop,
          object.width,
          object.height],
        {
          uuid: object.uuid,
          adjCanvasNumber: object.adjCanvasNumber
        });
        adjObject.set({
          scaleX: object.scaleX,
          scaleY: object.scaleY,
          // 不然就会出现闪一下
          visible: false
        });
        this.canvases[index - 1].add(adjObject);
      }
      return;
    }

    // 原本不跨canvas 变量adjCanvasNumber变更
    const adjCanvasNumber = Math.floor((object.top + object.height * object.scaleY) / this.blockSize);
    if (adjCanvasNumber) {
      object.adjCanvasNumber = adjCanvasNumber;
      for (let i = index + 1; i < adjCanvasNumber + index + 1; i++) {
        const adjObject = this.drawRect([
          object.left,
          -(this.blockSize - object.originTop),
          object.width,
          object.height],
        // i !== adjCanvasNumber ? this.blockSize + 1 : object.height * object.scaleY],
        {
          uuid: object.uuid,
          adjCanvasNumber: adjCanvasNumber
        });
        adjObject.set({
          scaleX: object.scaleX,
          scaleY: object.scaleY,
          visible: false
        });
        this.canvases[i].add(adjObject);
        // this.canvases[i].setActiveObject(adjObject);
      }
    }

    //  不跨canvas 向上移动  缩放或移动导致跨 其他块向上移动或缩放 成了1;第一块向上移动还是0
    // 用于之前不跨 移动或缩放导致的跨画布处理
    if (object.top < 0) {
      object.adjCanvasNumber = index ? 1 : 0;
    }
  }
  1. 删除多余图形
    鼠标抬起的时候删除多余图形
  private mouseUp (e) {
    this.setCursor();

    if (this[`mouseUp${this.drawType}`] && e.pointer) {
      return;
    }
    if (this.selectObject()) {
      // 删除不在范围内的图形 并且重置他的adjCanvasNumber
      this.deleteExceedShape();
    }
  }

  private deleteExceedShape () {
    this.canvases.map(item => {
      const draweredItem = item.getObjects().filter(x => x.uuid === this.selectObject().uuid)[0];
      if (draweredItem) {
        // 向下超出的被删除
        // 向上超出也要被删除  || draweredItem.top + draweredItem.height * draweredItem.scaleY < 0
        if (draweredItem.top > this.blockSize || draweredItem.top + draweredItem.height * draweredItem.scaleY < 0) {
          // 修改adjCanvasNumber
          const sameUuidObject = this.getObjectByUuid(draweredItem.uuid);
          sameUuidObject.map(sameUuiditem => {
            sameUuiditem.shape.adjCanvasNumber = sameUuiditem.shape.adjCanvasNumber - 1;
          });
          // 删除
          item.remove(draweredItem);
          item.renderAll();
        }
      }
    });
  }
  1. 绘画过程中 鼠标为+字
    在鼠标抬起和鼠标移动过程中设置。canvas提供方法setCursor
  private mouseMove (e, index) {
    // 画的时候使用十字
    this.setCursor();
    ....
  }
  private mouseUp (e) {
    this.setCursor();
    ....
  }


  private setCursor () {
    if (this.drawType) {
      this.canvases.map(item => {
        item.setCursor('crosshair');
      });
    }
  }
  1. 鼠标点击处存在多个矩形,处理点击哪个矩形
    (这个问题其实是之前处理 fabric的遗留问题了,这里顺便记录一下吧!)
    因为矩形也是有面积的,在fabric中他会按照渲染顺序来处理,可以理解为就是和z-index相关,也就意味着有些小的矩形如果在下面,我们鼠标点击的时候是获取不到的
    之前因为矩形不多,所以之前的处理方法是右键点击可以置于顶层或底层,方便他选中自己想要的;
    新的解决方法:按照矩形的面积来判断;
    在这个过程中 出现了一个问题 就是因为存在多个画布 所以必须保证点击的最小面积是在当前这个画布内 所以新增了一个canvasIndex字段
// 鼠标按下
  private mouseDown (e, index) {
    // 如果type不为‘’,则认为正在编辑图形,鼠标点击事件不触发画新图形
    if (!this.drawType) {
      // 设置点击的图形为活跃 从面积来判断
      // 需要多增一个canvasIndex字段 点击的其他位置 在其他画布也有可能有更小面积的图形 所以需要保证画布统一
      const allObjects: any = [];
      this.canvases.map((canvas, canvasIndex) => {
        if (canvas.getObjects().length) {
          canvas.getObjects().map(object => {
            allObjects.push({
              shape: object,
              canvasIndex: canvasIndex
            });
          });
        }
      });

      let smallestRect: any = null;

      const pointer = this.transformMouse(e);

      // 遍历画布上的对象
      allObjects.forEach(function (object) {
        const shape = object.shape;
        const shapeArea = Math.abs(shape.width) * shape.scaleX * shape.scaleY * Math.abs(shape.height);
        const smallestArea = smallestRect ? Math.abs(smallestRect.width) * smallestRect.scaleX * smallestRect.scaleY * Math.abs(smallestRect.height) : 0;
        if (shape.containsPoint(pointer) && object.canvasIndex === index && (smallestRect === null || shapeArea < smallestArea)) {
          smallestRect = shape;
        }
      });

      if (smallestRect !== null) {
        this.setActiveByUuid(smallestRect.uuid);
      }

      this.operateAttribute.map(item => {
        if (this.selectObject() && this.selectObject()[item]) {
          this.selectObject()[item] = !this.rectStyle ? this.selectObject()[item] : this.rectStyle[item];
        }
      });

      // 点击的哪个图形
      this.$emit('clickShape', this.selectObject());
      return;
    }
    // // 防止在画1的里面画2的时候 影响1
    // if (this.selectObject() && !this.drawingObject) {
    //   this.operateAttribute.map(item => {
    //     this.selectObject()[item] = true;
    //   });
    // }
    this[`mouseDown${this.drawType}`](this.transformMouse(e), index);
  }

最后附上全部代码

<template>
  <div
    class="drawer"
    :style="`width: ${width}px; height: ${height}px`"
  >
    <div id="canvas-container" />
    <div class="container">
      <slot />
    </div>
  </div>
</template>
<script lang="ts">
import Vue from 'vue';
import { Component, Prop, Watch } from 'vue-property-decorator';
import { fabric } from 'fabric';
import { createUuid } from '@/utils/uuid';

export const enum DrawType {
  Pointer = '',
  Polygon = 'polygon',
  Rect = 'rectangle',
  Line = 'Line',
}

export interface MousePosition {
  x: number;
  y: number;
}

export interface Shape {
  type: DrawType | string;
  points: Array<MousePosition | number> | number[][];
  content?: number|string;
  others?: OthersConfigModel;
}

export interface RectModel {
  left: number;
  top: number;
  width: number;
  height: number;
  scaleX?: number;
  scaleY?: number;
}

export interface GetShapeByUuidModel {
  shape: any;
  canvasIndex: number;
}

const defaultRectStyle: OthersConfigModel = {
  stroke: 'rgb(0, 232, 8)',
  strokeWidth: 1,
  fill: '',
  opacity: 0.8,
  cornerColor: 'rgb(0, 232, 8)',
  cornerSize: 4,
  selectionLineWidth: 0,
  hasBorders: false
};

export interface OthersModel{
  stroke: string;
  opacity: number;
  uuid: string | null;
  strokeWidth: number;
  fill: string;
  cornerColor: string;
  cornerStyle: string;
  cornerSize: number;
  radius: number;
  selectionLineWidth: number;
  clickIndex: number;
  hasBorders: boolean;
  hasControls: boolean; //  不显示边框点
  selectable: boolean; // 禁止选中当前元素
  lockMovementX: boolean; // 禁止元素移动
  lockMovementY: boolean;
  lockScalingX: boolean; // 禁止元素缩
  lockScalingY: boolean;
  visible: boolean; // 设置元素不可见
  topToRect: number; // 文字到矩形顶部的距离
  leftToRect: number; // 文字到矩形左边的距离
  originX: string; // 旋转x轴 设置文字时候使用
  originY: string; // 旋转y轴 设置文字时候使用
  adjCanvasNumber: number; // 这个图形跨canvas的数字
}

export type OthersConfigModel = Partial<OthersModel>

@Component
export default class ImageMarkDrawer extends Vue {
  @Prop({
    type: Object,
    required: false,
    default: () => {}
  })
  private rectStyle!: {};

  @Prop({
    type: Number,
    required: false,
    default: 780
  })
  private width!: number; // 画布宽度

  @Prop({
    type: Number,
    required: false,
    default: 580
  })
  private height!: number; // 画布高度

  private blockSize = 5000; // 小块宽度和高度
  // 计算需要分割的小块数
  private numBlocks = Math.ceil(this.height / this.blockSize);

  private canvases: any=[]; // canvas数组
  private activeCanvas: any = null;
  // private adjacentCanvas: any = null; // 跨canvas
  private adjacentCanvasIndex = -1; // 跨canvas的最后一个index

  private drawType = ''; // 绘画类型
  private drawingObject: any = null;
  private adjObject: any = null; // 跨canvas的对象
  // private drawingShape: any[] = [];

  private operateAttribute = ['lockMovementX', 'lockMovementY', 'lockScalingX', 'lockScalingY']; // 画框的时候不能放大缩小移动;
  // private canvasObjects: number[] | null = null; // 用户点击置于顶层/底层 导致顺序变化 ids变化 index也变化

  private mounted () {
    this.init();
  }

  private init () {
    // 先清空之前的canvas 和数组
    const canvasContainer = document.getElementById('canvas-container') as any;
    canvasContainer.innerHTML = '';
    this.canvases = [];
    // 循环创建和显示小块画布
    for (let i = 0; i < this.numBlocks; i++) {
      const canvasElement = document.createElement('canvas');
      canvasElement.id = `canvas-${i}`;
      canvasElement.width = this.width;
      canvasElement.height = i < this.numBlocks - 1 ? this.blockSize : this.height - this.blockSize * (this.numBlocks - 1);
      const canvasContainer = document.getElementById('canvas-container') as any;

      // 添加每个部分的画布对象到画布容器中
      canvasContainer.appendChild(canvasElement);

      const canvas = new fabric.Canvas(`canvas-${i}`, {
        width: this.width,
        height: i < this.numBlocks - 1 ? this.blockSize : this.height - this.blockSize * (this.numBlocks - 1)
      });
      canvas.on('mouse:down', (e) => { this.mouseDown(e, i); });
      canvas.on('mouse:move', (e) => { this.mouseMove(e, i); });
      canvas.on('mouse:up', this.mouseUp);
      canvas.on('selection:updated', e => {
        this.edit(e.target);
      });
      canvas.on('selection:created', e => {
        this.edit(e.target);
      });
      canvas.on('object:modified', this.onChange);
      canvas.on('object:scaling', e => {
        this.onScaling(e.target, i);
      });
      canvas.on('object:moving', e => {
        this.mouseMoving(e.target, i);
      });
      this.canvases.push(canvas);
    }
  }

  // 鼠标按下
  private mouseDown (e, index) {
    // 如果type不为line,则认为正在编辑图形,鼠标点击事件不触发画新图形
    if (!this.drawType) {
      // 设置点击的图形为活跃 从面积来判断
      // 需要多增一个canvasIndex字段 点击的其他位置 在其他画布也有可能有更小面积的图形 所以需要保证画布统一
      const allObjects: any = [];
      this.canvases.map((canvas, canvasIndex) => {
        if (canvas.getObjects().length) {
          canvas.getObjects().map(object => {
            allObjects.push({
              shape: object,
              canvasIndex: canvasIndex
            });
          });
        }
      });

      let smallestRect: any = null;

      const pointer = this.transformMouse(e);

      // 遍历画布上的对象
      allObjects.forEach(function (object) {
        const shape = object.shape;
        const shapeArea = Math.abs(shape.width) * shape.scaleX * shape.scaleY * Math.abs(shape.height);
        const smallestArea = smallestRect ? Math.abs(smallestRect.width) * smallestRect.scaleX * smallestRect.scaleY * Math.abs(smallestRect.height) : 0;
        if (shape.containsPoint(pointer) && object.canvasIndex === index && (smallestRect === null || shapeArea < smallestArea)) {
          smallestRect = shape;
        }
      });

      if (smallestRect !== null) {
        this.setActiveByUuid(smallestRect.uuid);
      }

      this.operateAttribute.map(item => {
        if (this.selectObject() && this.selectObject()[item]) {
          this.selectObject()[item] = !this.rectStyle ? this.selectObject()[item] : this.rectStyle[item];
        }
      });

      // 点击的哪个图形
      this.$emit('clickShape', this.selectObject());
      return;
    }
    // // 防止在画1的里面画2的时候 影响1
    // if (this.selectObject() && !this.drawingObject) {
    //   this.operateAttribute.map(item => {
    //     this.selectObject()[item] = true;
    //   });
    // }
    this[`mouseDown${this.drawType}`](this.transformMouse(e), index);
  }

  private mouseDownRect (position: MousePosition, index: number) {
    if (this.drawingObject) {
      this.drawEnd(this.drawingObject);
      return;
    }
    this.activeCanvas = this.canvases[index];
    const rect = this.drawRect([position.x, position.y, 0, 0]);
    this.drawingObject = rect;
    this.activeCanvas.add(rect);
  }

  // 鼠标移动
  private mouseMove (e, index) {
    // 画的时候使用十字
    this.setCursor();

    if (!this.drawType || !this.drawingObject) {
      return;
    }
    this[`mouseMove${this.drawType}`](this.transformMouse(e), index);
  }

  private mouseMoveRect (position: MousePosition, mouseMoveCanvasIndex: any) {
    if (this.drawingObject) {
      const startIndex = Number(this.activeCanvas.lowerCanvasEl.id.split('-')[1]);

      // 检查是否跨越相邻画布并绘制矩形  提到上面  因为取消渲染的时候也需要判断是从上到下还是从下到上
      const adjacentCanvas = this.getCanvasFromCoordinates(position.x, position.y + mouseMoveCanvasIndex * this.blockSize);
      if (!adjacentCanvas) {
        return;
      }
      const adjacentCanvasIndex = Number(adjacentCanvas.lowerCanvasEl.id.split('-')[1]);

      const fromTopToBottom = startIndex < adjacentCanvasIndex;
      // 最后一个画布的index不是最开始的index
      if (this.adjacentCanvasIndex > -1 && this.adjacentCanvasIndex !== startIndex) {
        // 从上往下  清除下面的框
        const clearBottom = startIndex < this.adjacentCanvasIndex;
        // for (let i = startIndex + 1; i < this.adjacentCanvasIndex + 1; i++) {
        for (let i = clearBottom ? (startIndex + 1) : (startIndex - 1);
          clearBottom ? (i < this.adjacentCanvasIndex + 1) : (i > this.adjacentCanvasIndex - 1);
          clearBottom ? i++ : i--) {
          const draweredItem = this.canvases[i].getObjects().filter(x => x.uuid === this.drawingObject.uuid)[0];
          this.canvases[i].remove(draweredItem);
        }
      }

      if (adjacentCanvas && adjacentCanvas !== this.activeCanvas) {
        // for (let i = startIndex + 1; i < adjacentCanvasIndex + 1; i++) {
        for (let i = fromTopToBottom ? startIndex + 1 : startIndex - 1;
          fromTopToBottom ? i < adjacentCanvasIndex + 1 : i > adjacentCanvasIndex - 1;
          fromTopToBottom ? i++ : i--) {
          // 重新绘画开始的矩形 具体是下面这种这样方式  还是说高度+1 将它隐藏 待定
          const width = position.x - this.drawingObject.get('left');
          const height = position.y + (mouseMoveCanvasIndex - startIndex) * this.blockSize - this.drawingObject.get('top');

          this.drawingObject.set({
            width: width,
            height: height,
            adjCanvasNumber: adjacentCanvasIndex - startIndex
          });
          this.activeCanvas.renderAll();

          // 已经渲染过的
          const draweredItem = this.canvases[i].getObjects().filter(x => x.uuid === this.drawingObject.uuid)[0];
          this.canvases[i].remove(draweredItem);

          const adjObject = this.drawRect([
            this.drawingObject.get('left'),
            -((this.blockSize * (i - startIndex)) - this.drawingObject.get('top')),
            position.x - this.drawingObject.get('left'),
            height],
          {
            uuid: this.drawingObject.uuid,
            adjCanvasNumber: adjacentCanvasIndex - startIndex
          });
          this.canvases[i].add(adjObject);
          // 最后的画布记录下来 如果用户跨画布 但是又取消跨画布 需要清楚所跨的画布的图形
          if (i === adjacentCanvasIndex) {
            this.adjacentCanvasIndex = i;
          }
        }
      } else {
        const width = position.x - this.drawingObject.get('left');
        const height = position.y - this.drawingObject.get('top');
        this.drawingObject.set({
          width: width,
          height: height,
          adjCanvasNumber: 0
          // startCanvansIndex: startIndex
        });
        this.activeCanvas.renderAll();
      }
    }
  }

  private mouseUp (e) {
    this.setCursor();

    if (this[`mouseUp${this.drawType}`] && e.pointer) {
      return;
    }
    if (this.selectObject()) {
      // 删除不在范围内的图形 并且重置他的adjCanvasNumber
      this.deleteExceedShape();
    }
  }

  private setCursor () {
    if (this.drawType) {
      this.canvases.map(item => {
        item.setCursor('crosshair');
      });
    }
  }

  private deleteExceedShape () {
    this.canvases.map(item => {
      const draweredItem = item.getObjects().filter(x => x.uuid === this.selectObject().uuid)[0];
      if (draweredItem) {
        // 向下超出的被删除
        // 向上超出也要被删除  || draweredItem.top + draweredItem.height * draweredItem.scaleY < 0
        if (draweredItem.top > this.blockSize || draweredItem.top + draweredItem.height * draweredItem.scaleY < 0) {
          // 修改adjCanvasNumber
          const sameUuidObject = this.getObjectByUuid(draweredItem.uuid);
          sameUuidObject.map(sameUuiditem => {
            sameUuiditem.shape.adjCanvasNumber = sameUuiditem.shape.adjCanvasNumber - 1;
          });
          // 删除
          item.remove(draweredItem);
          item.renderAll();
        }
      }
    });
  }

  private async drawEnd (object) {
    // 设置为当前活跃
    this.setActiveByUuid(object.uuid);
    // 如果高度< 0 则重新整一下他的高度
    if (object.height < 0) {
      const sameUuidObject = this.getObjectByUuid(object.uuid);
      sameUuidObject.map(item => {
        item.shape.set({
          top: item.shape.height + item.shape.top,
          originTop: item.shape.height + item.shape.top,
          height: Math.abs(object.height)
        });
        this.canvases[item.canvasIndex].renderAll();
      });
    }

    this.drawingObject = null;
    this.adjObject = null;
    this.adjacentCanvasIndex = -1;

    // this.adjacentCanvas = null;
    // this.drawingShape = [];

    // if (this.canvasObjects) {
    //   this.canvasObjects.push(object); // 置于顶层/底层之后
    // }
    this.edit(object);

    await this.$nextTick();
    this.onChange();
    this.$emit('drawEnd');
  }

  public setActiveByUuid (uuid: string) {
    // 设置为当前活跃
    this.canvases.map(item => {
      const draweredItem = item.getObjects().filter(x => x.uuid === uuid)[0];
      // discardActiveObject 抛弃当前处于活动状态的Object。
      draweredItem ? item.setActiveObject(draweredItem) : item.discardActiveObject();
      item.renderAll();
    });
  }

  // 改变时候触发
  private async onChange () {
    let allObjects: any = [];
    this.canvases.map((canvas, index) => {
      if (canvas.getObjects().length) {
        // 更改他的top
        const getObjects = canvas.getObjects().map(shape => {
          return {
            ...shape,
            top: shape.top + index * this.blockSize
          };
        });
        allObjects.push(...getObjects);
      }
    });

    const hash = {};
    const newData = [...allObjects];
    allObjects = newData.reduce((item, next) => {
      if (!hash[next.uuid]) {
        hash[next.uuid] = true;
        item.push(next);
      }
      return item;
    }, []);

    this.$emit('onChange', allObjects);
  }

  // points 数组 存放left、top、width、height  从后端渲染框到前端
  public narrowRect (points: number[], others?: OthersConfigModel) {
    if (!this.canvases.length) {
      return;
    }
    const belowXPoint = points[1] + points[3];
    const startCanvasIndex = Math.floor(points[1] / this.blockSize);
    const adjCanvasNumber = Math.floor(belowXPoint / this.blockSize);
    // 因为points传入的top是全局图片的 需要把它转化为某个canvans的相对top
    const drawPoint = [
      points[0],
      points[1] - startCanvasIndex * this.blockSize,
      points[2],
      points[3]
    ];

    // 跨框
    if (startCanvasIndex !== adjCanvasNumber) {
      // const uuid = createUuid();
      for (let i = startCanvasIndex; i < adjCanvasNumber + 1; i++) {
        const adjObject = this.drawRect([
          points[0],
          // i===startCanvasIndex?points[1]:i !== adjCanvasNumber?this.blockSize + 1 :
          i === startCanvasIndex ? drawPoint[1] : -((this.blockSize * (i - startCanvasIndex)) - drawPoint[1]),
          points[2],
          points[3]],
        {
          ...others,
          // uuid: uuid,
          adjCanvasNumber: adjCanvasNumber - startCanvasIndex
        });
        this.canvases[i].add(adjObject);
      }
    } else {
      // 不跨框
      this.canvases[startCanvasIndex].add(this.drawRect(drawPoint, {
        ...others,
        adjCanvasNumber: 0
      }));
    }
  }

  public narrowLine (points: number[], others?: OthersConfigModel) {
    const startCanvasIndex = Math.floor(points[1] / this.blockSize);
    const drawPoint = [
      points[0],
      points[1] - startCanvasIndex * this.blockSize,
      points[2],
      points[3] - startCanvasIndex * this.blockSize
    ];
    const adjObject = this.drawLine(drawPoint, others);
    this.canvases[startCanvasIndex].add(adjObject);
  }

  private onScaling (object, index: number) {
    // 跨了之后取消跨画布 sameUuidObject 依然还是2个 后续如果清楚另一个框的话  return的判断就需要换一下了
    const sameUuidObject = this.getObjectByUuid(object.uuid);
    sameUuidObject.map(item => {
      item.shape.set({
        scaleX: object.scaleX,
        scaleY: object.scaleY,
        left: object.left// 万一用户拉的左侧的,
      });
      this.canvases[item.canvasIndex].renderAll();
    });
    // 跨canvas的时候 防止他缩放的是上面 带来的bug
    this.mouseMoving(object, index);
  }

  private mouseMoving (object, index) {
    const padding = -1.5; // 内容距离画布的空白宽度,主动设置
    // 限制左右不能移出外面
    object.setCoords();
    const objBoundingBox = object.getBoundingRect();
    if (objBoundingBox.left < padding) {
      object.left = Math.max(object.left, object.left - objBoundingBox.left + padding);
    }
    if (objBoundingBox.left + objBoundingBox.width > object.canvas.width - padding) {
      object.left = Math.min(object.left, object.canvas.width - objBoundingBox.width + object.left - objBoundingBox.left - padding);
    }

    // 如果是第一块  限制不能移到上面
    if (!index && objBoundingBox.top < 0) {
      object.top = Math.max(object.top, object.top - objBoundingBox.top);
    } else if (index === this.numBlocks - 1 && objBoundingBox.top + objBoundingBox.height > object.canvas.height) {
      // 如果是最后一块 不能移动到下面
      object.top = Math.min(object.top, object.canvas.height - objBoundingBox.height + object.top - objBoundingBox.top);
    }

    // 跨框的情况下
    if (object.adjCanvasNumber) {
      const sameUuidObject = this.getObjectByUuid(object.uuid);
      sameUuidObject.map(item => {
        item.shape.left = object.left;
        item.shape.top = item.shape.originTop - (object.originTop - object.top);
        item.shape.visible = true;
        // 不在这里做判断 因为跨框之后 删除就会操成停顿 解决办法:除了mouseUp写移除 在就是 设置新增的那个为移动状态 ???
        this.canvases[item.canvasIndex].renderAll();
        this.canvases[item.canvasIndex].setActiveObject(item.shape);
      });
      // // 原本跨多个canvas 移动到下个画布 会消失
      // if (object.adjCanvasNumber > 1) {
      const adjCanvasNumber = Math.floor((object.top + object.height * object.scaleY) / this.blockSize);
      if (index + adjCanvasNumber > object.adjCanvasNumber + sameUuidObject[0].canvasIndex) {
        const lastRect = sameUuidObject[object.adjCanvasNumber].shape;

        // 如果在同一个画布停顿两次 第一次就会留下
        const draweredItem = this.canvases[index + adjCanvasNumber].getObjects().filter(x => x.uuid === object.uuid)[0];

        if (draweredItem) {
          return;
        }
        lastRect.set({
          originTop: -(this.blockSize - lastRect.originTop)
        });
        const adjObject = this.drawRect([
          object.left,
          -(this.blockSize - lastRect.originTop),
          object.width,
          object.height],
        {
          uuid: object.uuid,
          adjCanvasNumber: object.adjCanvasNumber
        });
        adjObject.set({
          scaleX: object.scaleX,
          scaleY: object.scaleY
        });
        this.canvases[index + adjCanvasNumber].add(adjObject);
        this.canvases[index + adjCanvasNumber].setActiveObject(adjObject);
        sameUuidObject.map(item => {
          item.shape.adjCanvasNumber = object.adjCanvasNumber + 1;
        });
      }
      // 多个画布的向上移动
      if (object.top < 0) {
        const draweredItem = this.canvases[index - 1].getObjects().filter(x => x.uuid === object.uuid)[0];

        if (draweredItem) {
          return;
        }
        const adjObject = this.drawRect([
          object.left,
          this.blockSize + object.originTop,
          object.width,
          object.height],
        {
          uuid: object.uuid,
          adjCanvasNumber: object.adjCanvasNumber
        });
        adjObject.set({
          scaleX: object.scaleX,
          scaleY: object.scaleY,
          // 不然就会出现闪一下
          visible: false
        });
        this.canvases[index - 1].add(adjObject);
      }
      return;
    }

    // 原本不跨canvas 变量adjCanvasNumber变更
    const adjCanvasNumber = Math.floor((object.top + object.height * object.scaleY) / this.blockSize);
    if (adjCanvasNumber) {
      object.adjCanvasNumber = adjCanvasNumber;
      for (let i = index + 1; i < adjCanvasNumber + index + 1; i++) {
        const adjObject = this.drawRect([
          object.left,
          -(this.blockSize - object.originTop),
          object.width,
          object.height],
        // i !== adjCanvasNumber ? this.blockSize + 1 : object.height * object.scaleY],
        {
          uuid: object.uuid,
          adjCanvasNumber: adjCanvasNumber
        });
        adjObject.set({
          scaleX: object.scaleX,
          scaleY: object.scaleY,
          visible: false
        });
        this.canvases[i].add(adjObject);
        // this.canvases[i].setActiveObject(adjObject);
      }
    }

    // 向上移动 原本不跨canvas 缩放或移动导致跨  第一块向上移动还是0
    if (object.top < 0) {
      object.adjCanvasNumber = index ? 1 : 0;
    }
  }

  // 获取包含指定坐标的画布
  private getCanvasFromCoordinates (x, y) {
    for (let i = 0; i < this.canvases.length; i++) {
      const canvas = this.canvases[i];
      const localPoint = canvas.getPointer({ x: x, y: y });
      const canvasElement = canvas.getElement();
      const canvasRect = canvasElement.parentElement.getBoundingClientRect();
      if (
        localPoint.x >= 0 &&
      localPoint.x <= canvasRect.width &&
      localPoint.y >= 0 &&
      localPoint.y <= canvasRect.height
      ) {
        return canvas;
      }
    }
    return null;
  }

  private getObjectByUuid (uuid: number) {
    const objects: GetShapeByUuidModel[] = [];
    this.canvases.map((canvas, index) => {
      const object = canvas.getObjects().filter(x => x.uuid === uuid)[0];
      object && objects.push({
        shape: object,
        canvasIndex: index
      });
    });
    return objects;
  }

  private edit (object) {
    if (object.type === DrawType.Polygon) {
      const lastControl = object.points.length - 1;
      object.controls = object.points.reduce((a, point, index) => {
        a['p' + index] = new fabric.Control({
          positionHandler: (dim, finalMatrix, fabricObject) => {
            const x = fabricObject.points[index].x - fabricObject.pathOffset.x;
            const y = fabricObject.points[index].y - fabricObject.pathOffset.y;
            return fabric.util.transformPoint(
              { x: x, y: y },
              fabric.util.multiplyTransformMatrices(
                fabricObject.canvas.viewportTransform,
                fabricObject.calcTransformMatrix()
              )
            );
          },
          actionHandler: this.anchorWrapper(index > 0 ? index - 1 : lastControl, this.actionHandler),
          actionName: 'modifyPolygon',
          pointIndex: index
        });
        return a;
      }, {});
    } else {
      object.cornerStyle = 'circle';
      object.controls = fabric.Object.prototype.controls;
    }

    // 点击的时候 已经传给后端的框 防止画框执行下面的
    //  !this.drawingObject 在已有的框中结束画 会触发emit事件  导致被选中的图形不是绘画的而是点击的
    if (typeof (object.clickIndex) !== 'undefined' && !this.drawingObject) {
      this.$emit('editIndex', object.clickIndex);
    }
    // const ids = (this.canvasObjects ?? this.activeCanvas.getObjects()).map(item => item.uuid).filter(x => x);
    // this.activeCanvas.requestRenderAll();
    // this.$emit('editIndex', object.clickIndex ?? ids.indexOf(object.uuid));
  }

  // 画四边形
  private drawRect (points: Array<number>, others?: OthersConfigModel) {
    const rect = new fabric.Rect({
      type: DrawType.Rect,
      uuid: createUuid(), // todo
      left: points[0],
      top: points[1],
      width: points[2] || 0,
      height: points[3] || 0,
      objectCaching: false,
      transparentCorners: false,
      selectionColor: 'rgba(0,0,0,0)',
      lockRotation: true,
      strokeUniform: true,
      // 移动时候需要 记录他移动了多少 来让同一个矩形uuid也移动
      originLeft: points[0],
      originTop: points[1],
      ...defaultRectStyle,
      ...this.rectStyle,
      ...others
    });
    // eslint-disable-next-line spellcheck/spell-checker
    rect.setControlsVisibility({ mtr: false }); // 隐藏旋转点
    return rect;
  }

  // 起点坐标 xy // 终点坐标 xy others目前专门设置颜色
  private drawLine (position: number[], others?: OthersConfigModel) {
    return new fabric.Line(position, {
      type: DrawType.Line,
      stroke: 'blue',
      strokeWidth: 2,
      objectCaching: false,
      hasBorders: false,
      selectable: false,
      transparentCorners: true,
      lockRotation: true,
      lockMovementX: true,
      lockMovementY: true,
      lockScalingX: true,
      lockScalingY: true,
      hasControls: false, // 隐藏控制点
      ...others
    });
  }

  public selectObject () {
    const selectedObjects = this.canvases.map(canvas => canvas.getActiveObject()).filter(x => x);
    return selectedObjects.length ? selectedObjects[0] : undefined;
  }

  public setDrawType (type) {
    // 外部调用此方法,切换画图模式
    this.drawType = type;
  }

  public removeAll () {
    this.canvases.map(item => {
      item.clear();
    });
  }

  public removeLine () {
    this.canvases.map(item => {
      const draweredItem = item.getObjects().filter(x => x.type === DrawType.Line)[0];
      if (draweredItem) {
        // 删除
        item.remove(draweredItem);
        item.renderAll();
      }
    });
  }

  public removeSelectedObject () {
    this.canvases.map(item => {
      if (this.selectObject()) {
        const draweredItem = item.getObjects().filter(x => x.uuid === this.selectObject().uuid)[0];
        if (draweredItem) {
        // 删除
          item.remove(draweredItem);
          item.renderAll();
        }
      }
    });
  }

  public setObjectVisible (uuid: string, visible: boolean) {
    // 设置为当前活跃
    this.canvases.map(item => {
      const draweredItem = item.getObjects().filter(x => x.uuid === uuid)[0];
      if (draweredItem) {
        draweredItem.visible = visible;
        draweredItem.setControlsVisibility({
          mt: visible,
          mb: visible,
          ml: visible,
          mr: visible,
          bl: visible,
          br: visible,
          tl: visible,
          tr: visible
        });
      }
      item.renderAll();
    });
  }

  private transformMouse (e): MousePosition {
    return e.pointer;
  }

  private anchorWrapper (anchorIndex, fn) {
    return function (eventData, transform, x, y) {
      const fabricObject = transform.target;
      const absolutePoint = fabric.util.transformPoint(
        {
          x: fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x,
          y: fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
        },
        fabricObject.calcTransformMatrix()
      );
      const actionPerformed = fn(eventData, transform, x, y);
      const polygonBaseSize = fabricObject._getNonTransformedDimensions();
      const newX = (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) / polygonBaseSize.x;
      const newY = (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) / polygonBaseSize.y;
      fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
      return actionPerformed;
    };
  }

  private actionHandler (eventData, transform, x, y) {
    const polygon = transform.target;
    const currentControl = polygon.controls[polygon.__corner];
    const mouseLocalPosition = polygon.toLocalPoint(new fabric.Point(x, y), 'center', 'center');
    const polygonBaseSize = polygon._getNonTransformedDimensions();
    const size = polygon._getTransformedDimensions(0, 0);
    const finalPointPosition = {
      x: (mouseLocalPosition.x * polygonBaseSize.x) / size.x + polygon.pathOffset.x,
      y: (mouseLocalPosition.y * polygonBaseSize.y) / size.y + polygon.pathOffset.y
    };
    polygon.points[currentControl.pointIndex] = finalPointPosition;
    return true;
  }

  @Watch('width')
  @Watch('height')
  private styleChaneg () {
    this.numBlocks = Math.ceil(this.height / this.blockSize);
    this.init();
  }
}
</script>

<style lang="scss" scoped>
.drawer {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  user-select: none;
  width: 100%;
  height: calc(100% - 70px);
  #canvas-container,
  .container {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
  }
  #canvas-container {
    z-index: 2;
  }
}
</style>

  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值