【fabricjs】利用fabric绘画矩形和多边形

需求在一张图片上标注矩形和多边形,支持回显;
fabric版本:4.6.0;
Fabric.js 是一个功能强大且操作简单的 Javascript HTML5 canvas 工具库。
官方文档
参考链接

可以标注多边形和矩形

组件代码drawer.vue
createUuid 是为了让每一个图形有自己的id;方便用于获取用户点击的那个图形等操作;
defaultRectStyle、defaultPolygonStyle是自己定义的一些默认的样式,比如颜色,边框大小等;

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

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

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

const defaultPolygonStyle: OthersConfigModel = {
  stroke: 'rgb(255, 30, 0)',
  strokeWidth: 1,
  fill: '',
  opacity: 0.8,
  cornerColor: 'rgb(255, 30, 0)',
  cornerSize: 4,
  cornerStyle: 'circle',
  selectionLineWidth: 0
};

const defaultCircleStyle: OthersConfigModel = {
  fill: '#FFE700'
  // selectionLineWidth: 0
};

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 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;
  standard_item_id: number;
  visible: boolean; // 设置元素不可见
}

export type OthersConfigModel = Partial<OthersModel>

@Component()
export default class Drawer extends Vue {
  @Prop({
    type: Number,
    required: false,
    default: 780
  })
  private width!: number;

  @Prop({
    type: Number,
    required: false,
    default: 580
  })
  private height!: number;

  @Prop({ type: Boolean, default: true })
  private editable!: boolean;

  // @PropSync('shapes', { type: Array })
  // private bindShapes!: Array<Shape>;

  @Prop({
    type: Object,
    required: false,
    default: () => {}
  })
  private rectStyle!: {};

  @Prop({
    type: Object,
    required: false,
    default: () => {}
  })
  private polygonStyle!: {};

  private canvas: any = null;
  private drawType = '';
  private drawingObject: any = null;
  private drawingShape: any[] = [];

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

  // 改变时候触发
  private async onChange () {
    // 防止用户将不想要的也传出去
    this.$emit('onChange', this.canvas.getObjects().filter(x => x.uuid));
  }

  // 将点位比例缩小 从后台传来渲染
  public narrowPolygon (narrowPolygon: MousePosition[], others?: OthersConfigModel) {
    const polygon = this.drawPolygon(narrowPolygon, others);
    polygon.centerPoints = polygon.getCenterPoint(); // 获取中心点
    this.canvas.add(polygon);
  }

  public narrowRect (points: number[], others?: OthersConfigModel) {
    this.canvas.add(this.drawRect(points, others));
  }

  public narrowCircle (narrowCircle: MousePosition, others?: OthersConfigModel) {
    this.canvas.add(this.drawCircle(narrowCircle, others));
  }

  // 展示组 矩形+文字
  public showRectGroup (points: number[], text: string, others?: OthersConfigModel) {
    const rect = this.drawRect(points, others);
    const content = this.showContent(rect, text);
    const group = this.getGroup(rect, content);
    this.canvas.add(group);
  }

  // 展示组 多边形+文字
  public showPolygonGroup (narrowPolygon: MousePosition[], text: string, others?: OthersConfigModel) {
    const polygon = this.drawPolygon(narrowPolygon, others);
    polygon.centerPoints = polygon.getCenterPoint(); // 获取中心点
    const content = this.showContent(polygon, text);
    const group = this.getGroup(polygon, content);
    this.canvas.add(group);
  }

  // 删除选中
  public remove (object: any) {
    this.canvas.remove(object);
  }

  // 删除选中
  public removeItemByIndex (itemIndex: number) {
    (this.canvasObjects ?? this.canvas.getObjects()).forEach((item, index) => {
      if (!item.uuid) {
        itemIndex += 1;
        return;
      }
      if (index === itemIndex) {
        this.remove(item);
      }
    });
    const spliceObject = this.canvasObjects?.splice(itemIndex, 1);
  }

  public removeAll () {
    this.canvas.getObjects().forEach(item => {
      this.canvas.remove(item);
    });
  }

  // 设置为当前活跃
  public async setActiveObjectByIndex (itemIndex: number) {
    (this.canvasObjects ?? this.canvas.getObjects()).forEach((item, index) => {
      if (!item.uuid) {
        itemIndex += 1;
        return;
      }
      if (index === itemIndex) {
        this.canvas.setActiveObject(item);
      }
    });
  }

  public setVisible (visible: boolean) {
    // 元素不可见  //  隐藏点
    (this.canvasObjects ?? this.canvas.getObjects()).forEach(item => {
      item.visible = visible;
      item.setControlsVisibility({
        mt: visible,
        mb: visible,
        ml: visible,
        mr: visible,
        bl: visible,
        br: visible,
        tl: visible,
        tr: visible
      });
    });
    this.canvas.renderAll();
  }

  public resetObjects () {
    this.canvasObjects = null;
  }

  private init () {
    this.canvas = new fabric.Canvas(this.$refs.canvas, {
      // isDrawingMode: this.editable,
    });
    this.canvas.on('mouse:down', this.mouseDown);
    this.canvas.on('mouse:move', this.mouseMove);
    this.canvas.on('mouse:up', this.mouseUp);
    this.canvas.on('mouse:dblclick', this.onPolygonDrawEnd);
    this.canvas.on('mouse:over', this.mouseOver);
    this.canvas.on('mouse:out', this.mouseOut);
    this.canvas.on('selection:updated', e => {
      this.edit(e.target);
    });
    this.canvas.on('selection:created', e => {
      this.edit(e.target);
    });
    this.canvas.on('object:modified', this.onChange);
    this.canvas.on('object:moving', this.mouseMoving); // 阻止对象移动到画布外面
  }

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

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

  private drawLine (position: MousePosition) {
    return new fabric.Line([position.x, position.y, position.x, position.y], {
      type: DrawType.Line,
      objectCaching: false,
      hasBorders: false,
      selectable: false,
      ...defaultPolygonStyle
    });
  }

  // 画多边形
  private drawPolygon (points: Array<MousePosition>, others?: OthersConfigModel) {
    return new fabric.Polygon(points, {
      type: DrawType.Polygon,
      uuid: createUuid(), // todo
      objectCaching: false,
      hasBorders: false,
      transparentCorners: false,
      selectionColor: 'rgba(0,0,0,0)',
      ...defaultPolygonStyle,
      ...this.polygonStyle,
      ...others
    });
  }

  // 画四边形
  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,
      ...defaultRectStyle,
      ...this.rectStyle,
      ...others
    });
    // eslint-disable-next-line spellcheck/spell-checker
    rect.setControlsVisibility({ mtr: false }); // 隐藏旋转点
    return rect;
  }

  private drawCircle (position: MousePosition, others?: OthersConfigModel) {
    const circle = new fabric.Circle({
      radius: 4,
      uuid: createUuid(),
      top: position.y,
      left: position.x,
      ...defaultCircleStyle,
      ...others
    });
    // eslint-disable-next-line spellcheck/spell-checker
    circle.setControlsVisibility({ mtr: false }); // 隐藏旋转点
    return circle;
  }

  private showContent (shape: any, text: string) {
    return new fabric.Text(text, {
      left: shape.left + 5,
      top: shape.top + 10,
      fill: 'white',
      fontSize: 14
    });
  }

  private getGroup (ellipse: any, text: string) {
    return new fabric.Group([ellipse, text], {
      uuid: createUuid(),
      objectCaching: false,
      transparentCorners: false,
      selectionColor: 'rgba(0,0,0,0)',
      hasControls: false,
      hasBorders: false,
      lockMovementX: true,
      lockMovementY: true,
      lockScalingX: true,
      lockScalingY: true
    });
  }

  // 鼠标按下
  private mouseDown (e) {
    // 如果type不为line,则认为正在编辑图形,鼠标点击事件不触发画新图形
    if (!this.drawType) {
      this.operateAttribute.map(item => {
        if (this.selectObject() && this.selectObject()[item]) {
          this.selectObject()[item] = !this.rectStyle ? this.selectObject()[item] : this.rectStyle[item];
        }
      });
      return;
    }
    // 防止在画1的里面画2的时候 影响1
    if (this.selectObject() && !this.drawingObject) {
      this.operateAttribute.map(item => {
        this.selectObject()[item] = true;
      });
    }
    this[`mouseDown${this.drawType}`](this.transformMouse(e));
  }

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

  private mouseDownPolygon (position: MousePosition) {
    const line = this.drawLine(position);
    this.drawingShape.push(line);
    this.drawingObject = line;
    this.canvas.add(line);
  }

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

  private mouseMovePolygon (position: MousePosition) {
    if (this.drawingObject) {
      this.drawingObject.set({
        x2: position.x,
        y2: position.y
      });
      this.canvas.remove(this.drawingObject);
      this.canvas.add(this.drawingObject);
    }
  }

  private mouseMoveRect (position: MousePosition) {
    if (this.drawingObject) {
      this.drawingObject.set({
        width: position.x - this.drawingObject.get('left'),
        height: position.y - this.drawingObject.get('top')
      });
      this.canvas.renderAll();
    }
  }

  private mouseUpRect (position: MousePosition) {
    // 第一次抬起的时候是有对象的  第二次没有才对  思路:第一个点的鼠标松开的时候重新给draw一下
    if (this.drawingObject) {
      // 让其可以拖拽 需要确保它有uuid
      if (this.selectObject() && this.selectObject().uuid) {
        this.operateAttribute.map(item => {
          this.selectObject()[item] = false;
        });
      }
      this.canvas.remove(this.drawingObject);
      const rect = this.drawRect([this.drawingObject.left, this.drawingObject.top, 0, 0]);
      this.drawingObject = rect;
      this.canvas.add(rect);
      // 设置一下width height 否则松开不动就会有问题
      this.drawingObject.set({
        width: position.x - this.drawingObject.get('left'),
        height: position.y - this.drawingObject.get('top')
      });
      this.canvas.renderAll();
    }
  }

  private mouseUp (e) {
    if (this[`mouseUp${this.drawType}`] && e.pointer) {
      this[`mouseUp${this.drawType}`](this.transformMouse(e));
    }
  }

  private mouseMoving (e) {
    const padding = -1.5; // 内容距离画布的空白宽度,主动设置
    const obj = e.target;
    if (obj.currentHeight > obj.canvas.height - padding * 2 || obj.currentWidth > obj.canvas.width - padding * 2) {
      return;
    }
    obj.setCoords();
    const objBoundingBox = obj.getBoundingRect();
    if (objBoundingBox.top < padding || objBoundingBox.left < padding) {
      obj.top = Math.max(obj.top, obj.top - objBoundingBox.top + padding);
      obj.left = Math.max(obj.left, obj.left - objBoundingBox.left + padding);
    }
    if (
      objBoundingBox.top + objBoundingBox.height > obj.canvas.height - padding ||
      objBoundingBox.left + objBoundingBox.width > obj.canvas.width - padding
    ) {
      obj.top = Math.min(obj.top, obj.canvas.height - objBoundingBox.height + obj.top - objBoundingBox.top - padding);
      obj.left = Math.min(obj.left, obj.canvas.width - objBoundingBox.width + obj.left - objBoundingBox.left - padding);
    }
  }

  private mouseOver (e: Event) {
    if (!e.target) {
      return;
    }
    this.$emit('handleMouseOver', e.target);
  }

  private mouseOut (e: Event) {
    if (!e.target) {
      return;
    }
    this.$emit('handleMouseOut', e.target);
  }

  private async onPolygonDrawEnd () {
    if (this.drawingObject) {
      const points = this.drawingShape.map(line => {
        return {
          x: line.get('x1'),
          y: line.get('y1')
        };
      });
      this.drawingShape.forEach(item => {
        this.canvas.remove(item);
      });
      const polygon = this.drawPolygon(points.splice(0, points.length - 1));
      polygon.centerPoints = polygon.getCenterPoint();
      this.drawEnd(polygon);
      this.canvas.add(polygon);
    } else {
      this.$emit('copyShape', this.selectObject());
    }
  }

  private async drawEnd (object) {
    this.drawingObject = null;
    this.drawingShape = [];
    if (this.canvasObjects) {
      this.canvasObjects.push(object); // 置于顶层/底层之后
    }
    this.edit(object);
    // 设置为当前活跃
    this.canvas.setActiveObject(object);
    await this.$nextTick();
    this.onChange();
    this.$emit('change');
  }

  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;
    }
    const ids = (this.canvasObjects ?? this.canvas.getObjects()).map(item => item.uuid).filter(x => x);
    this.canvas.requestRenderAll();
    this.$emit('editIndex', object.clickIndex ?? ids.indexOf(object.uuid));
  }

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

  public selectObject () {
    return this.canvas.getActiveObject();
  }

  public sendToBack () {
    if (!this.canvasObjects) {
      this.canvasObjects = this.canvas.getObjects();
    }
    this.canvas.sendToBack(this.selectObject());
  }

  public setTop () {
    if (!this.canvasObjects) {
      this.canvasObjects = this.canvas.getObjects();
    }
    this.canvas.bringToFront(this.selectObject());
  }

  public async onLoadStereo () {
    this.updateCanvasStyle(this.width, this.height);
  }

  public async updateCanvasStyle (width: number, height: number) {
    // 改变canvas 的宽高
    this.canvas.setWidth(width);
    this.canvas.setHeight(height);
  }

  private beforeDestroy () {
    this.canvas.removeListeners();
    this.removeAll();
  }

  private mounted () {
    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);
  .canvans,
  .container {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
  }
}
</style>
<style lang="scss">
.canvas-container {
  z-index: 2;
  width: 100% !important; //  设置这几个百分比用来解决窗口尺寸变化 框也变化
  height: 100% !important;
}
.lower-canvas,.upper-canvas  {
  width: 100% !important;
  height: 100% !important;
}
</style>

组件代码ImageDrawer.vue

dropImage 可以拖拽图片
使用组件的时候需要传入shapes

<template>
  <div @mousedown.prevent="dropImage(arguments[0])">
    <drawer
      ref="drawer"
      v-bind="$attrs"
      :width="width"
      :height="height"
      v-on="$listeners"
      @onChange="onChange"
      @contextmenu.prevent.native="openMenu($event)"
    >
      <basic-img
        class="img"
        :style="`width: ${width}px; height: ${height}px;${maxWidth ? `maxWidth:${maxWidth}px` : ''}`"
        v-bind="$attrs"
        @load="imageLoaded"
      />
      <ul
        v-show="visible"
        :style="contextmenuStyle"
        class="contextmenu"
      >
        <li @click="setBack">
          置于底层
        </li>
        <li @click="setTop">
          置于顶层
        </li>
      </ul>
    </drawer>
  </div>
</template>

<script lang='ts'>
import Vue from 'vue';
import { Component, Ref, Prop, PropSync, Watch } from 'vue-property-decorator';
import Drawer, { Shape, DrawType, MousePosition, RectModel, OthersConfigModel } from './Drawer.vue';
import BasicImg from '@/components/img/BasicImg.vue';

export type ShapeModel = Shape

@Component({
  components: {
    Drawer,
    BasicImg
  }
})
export default class ImageDrawer extends Vue {
  @Ref()
  private readonly drawer !: Drawer

  @Prop({
    type: [String, Number],
    required: false,
    default: 780
  })
  private width!: string | number; // innerHeight

  @Prop({
    type: [String, Number],
    required: false,
    default: 0
  })
  private maxWidth!: string | number; // innerHeight

  @Prop({
    type: [String, Number],
    required: false,
    default: 580
  })
  private height!: string | number;

  @Prop({
    type: Boolean,
    required: false,
    default: false
  })
  private canDropImage!: boolean;

  @PropSync('shapes', { type: Array })
  private bindShapes!: Array<Shape>;

  @Prop({
    type: Array,
    required: false,
    default: () => []
  })
  private othersShapes!: Array<Shape>;

  private imgInfo: any={}; // 图片信息
  private xRate=0;
  private yRate=0;

  private contextmenuStyle: any={}
  private visible=false;

  private odiv: any=null;
  private editAble=false;

  private onChange (object: any) {
    this.bindShapes = object.map(item => {
      let points: any = [];
      if (item.type === DrawType.Polygon && item.centerPoints) {
        const pointsX = item.getCenterPoint().x - item.centerPoints.x;
        const pointsY = item.getCenterPoint().y - item.centerPoints.y;
        points = item.points.map(item => {
          return {
            x: item.x + pointsX,
            y: item.y + pointsY
          };
        });
      }
      return {
        type: item.type,
        // 放大坐标
        points: item.type === DrawType.Polygon ? this.enlargePolygon(points) : this.enlargeRect(item),
        styles: {
          ...item
        }
      };
    });
  }

  // 将点位比例放大  渲染计算之后给后台
  private enlargePolygon (points: MousePosition[]) {
    return points.map(item => {
      return [item.x / this.xRate, item.y / this.yRate];
    });
  }

  private enlargeRect (item: RectModel) {
    item.width = item.width * (item.scaleX || 1);
    item.height = item.height * (item.scaleY || 1);
    // 解决从各个方向画的问题
    const convertItem: RectModel = this.getConvertPoints(item);
    const { left, top, width, height } = convertItem;
    // 计算完成之后需要把宽度设回去 不然会影响页面上的效果
    item.width = item.width / (item.scaleX || 1);
    item.height = item.height / (item.scaleY || 1);
    return [
      [
        left / this.xRate,
        top / this.yRate
      ],
      [
        (left + width) / this.xRate,
        (top + height) / this.yRate
      ]
    ];
  }

  private getConvertPoints (item: RectModel) {
    if (item.width < 0 && item.height < 0) {
      // 右下角画
      item.left = item.left + item.width;
      item.top = item.top + item.height;
      item.width = -item.width;
      item.height = -item.height;
    } else if (item.width < 0) {
      // 右上角画
      item.left = item.left + item.width;
      item.width = -item.width;
    } else if (item.height < 0) {
      // 左下角画
      item.top = item.top + item.height;
      item.height = -item.height;
    }
    return item;
  }

  private async imageLoaded (e) {
    // 获取图片原始宽高 // 实际宽高
    this.imgInfo = e.target;
    await this.$nextTick();
    this.drawer && this.drawer.updateCanvasStyle(this.imgInfo.width, this.imgInfo.height);
    this.$emit('load', e);
    // 最终换算比例
    this.xRate = e.target.width / e.target.naturalWidth;
    this.yRate = e.target.height / e.target.naturalHeight;
    // 得到imgInfo
    await this.initShapes();
  }

  public async updateCanvasStyle (width: number, height: number, xRate?: number, yRate?: number) {
    await this.removeAll(); // 防止有些时候执行多次
    this.drawer && this.drawer.updateCanvasStyle(width, height);
    this.xRate = xRate || this.xRate;
    this.yRate = yRate || this.yRate;
    await this.initShapes();
  }

  public async initShapes (selectFirst = true) {
    await this.removeAll();
    // if(this.bindShapes && !this.bindShapes.length && this.othersShapes && !this.othersShapes.length){
    //   return
    // }
    // 不同的type 缩小的比例不一样  缩小比例
    await this.othersShapes.map(item => {
      if (item.type === DrawType.Rect) {
        this.narrowRect(item.points as number[], item.others as OthersConfigModel);
      } else if (item.type === DrawType.Polygon) {
        this.narrowPolygon(item.points as number[], item.others as OthersConfigModel);
      }
    });
    await this.bindShapes.map(item => {
      if (item.type === DrawType.Rect) {
        this.narrowRect(item.points as number[], item.others as OthersConfigModel, item.content as string);
      } else if (item.type === DrawType.Polygon) {
        this.narrowPolygon(item.points as number[], item.others as OthersConfigModel, item.content as string);
      }
    });
    this.drawer && this.drawer.resetObjects(); // 重新绘制的时候就reset
    if (selectFirst) {
      // init选择为第一个
      this.setActiveObjectByIndex(0);
    }
  }

  // 将点位比例缩小 从后台传来渲染 content 3d点云处使用
  private narrowPolygon (points: number[], others?: OthersConfigModel, content?: string) {
    const pointsList = points.map(item => {
      return {
        x: item[0] * this.xRate,
        y: item[1] * this.yRate
      };
    });
    if (content) {
      this.drawer.showPolygonGroup(pointsList, content, others);
    } else {
      this.drawer && this.drawer.narrowPolygon(pointsList, others);
    }
  }

  private narrowRect (points: number[], others?: OthersConfigModel, content?: string) {
    const pointsList = [
      points[0][0] * this.xRate,
      points[0][1] * this.yRate,
      (points[1][0] - points[0][0]) * this.xRate,
      (points[1][1] - points[0][1]) * this.yRate
    ];
    if (content) {
      this.drawer.showRectGroup(pointsList, content, others);
    } else {
      this.drawer && this.drawer.narrowRect(pointsList, others);
    }
  }

  public narrowCircle (points: number[], others?: OthersConfigModel) {
    const point = {
      x: points[0] * this.xRate,
      y: points[1] * this.yRate
    };
    this.drawer.narrowCircle(point, others);
  }

  public removeAll () {
    this.drawer.removeAll();
  }

  public setDrawType (drawType: string) {
    this.editAble = drawType !== '';
    this.drawer.setDrawType(drawType);
  }

  public removeItemByIndex (index: number) {
    this.drawer.removeItemByIndex(index);
  }

  public setActiveObjectByIndex (index: number) {
    this.drawer && this.drawer.setActiveObjectByIndex(index);
  }

  public setVisible (visible: boolean) {
    this.drawer.setVisible(visible);
  }

  // 鼠标右键
  private async openMenu (e) {
    this.contextmenuStyle = {
      left: e.offsetX + 'px',
      top: e.offsetY + 'px'
    };
    this.visible = true;
  }

  private setBack () {
    this.drawer.sendToBack();
  }

  private setTop () {
    this.drawer.setTop();
  }

  private closeMenu () {
    this.visible = false;
  }

  // 放大缩小
  public setTransform (multiples: number) {
    (this.$el as any).style.transform = `scale(${multiples})`;
  }

  // 拖拽
  public dropImage (e: any) {
    if (!e || this.editAble || !this.canDropImage) {
      return;
    }
    this.odiv = this.drawer.$el; // 获取目标元素
    // 算出鼠标相对元素的位置
    const disX = e.clientX - this.odiv.offsetLeft;
    const disY = e.clientY - this.odiv.offsetTop;
    document.onmousemove = (e) => { // 鼠标按下并移动的事件
      // 用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
      const left = e.clientX - disX;
      const top = e.clientY - disY;
      // 移动当前元素
      this.odiv.style.left = left + 'px';
      this.odiv.style.top = top + 'px';
    };
    document.onmouseup = (e) => {
      document.onmousemove = null;
      document.onmouseup = null;
      this.$emit('onmouseup', this.odiv.style.left, this.odiv.style.top);
    };
  }

  // 回到原始位置 一起拖拽
  public setDrawerPosition (left = 0, top = 0) {
    this.odiv = this.drawer.$el; // 获取目标元素
    if (this.odiv) {
      this.odiv.style.left = left;
      this.odiv.style.top = top;
    }
  }

  @Watch('visible')
  private visibleChange () {
    if (this.visible) { // 显示的时候 添加一个点击事件  用于隐藏右键的内容
      window.addEventListener('click', this.closeMenu);
    } else {
      window.removeEventListener('click', this.closeMenu);
    }
  }
}
</script>

<style lang='scss' scoped>
@import '~@/css/variable';
.contextmenu {
  min-width: 86px;
  margin: 0;
  background: #eeeded;
  opacity: 0.9;
  z-index: 3000;
  position: absolute;
  list-style-type: none;
  padding: 5px;
  border-radius: 4px;
  font-size: 12px;
  color: #505050;
  border: 1px solid $color-border;
  box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
}
.contextmenu li {
  margin: 0;
  padding: 2px 10px;
  border-radius: 2px;
  cursor: pointer;
}
.contextmenu li:hover {
  background: $color-button-disabled-hover;
  color: $color-basic;
}
::v-deep .el-image__placeholder {
  background: transparent;
}
</style>

fabric版本5.3.0
多边形选中的时候 点位显示有问题
参考官方例子

所以在多边形这里需要单独处理一下,我是在设置选中的时候 做的处理【需要说明这时候我是用的是vue3】,核心代码如下

// 根据id设置当前活跃的框
const setActiveObjectById = (id: number) => {
  (canvasObjects.value ?? canvas.value.getObjects()).forEach((item: any) => {
    if (item.shapeId === id) {
      canvas.value?.setActiveObject(item);
      if (item.type === DrawType.Polygon) {
        item.controls = item.points.reduce(
          function (
            // eslint-disable-next-line spellcheck/spell-checker
            acc: { [x: string]: fabric.Control },
            point: any,
            index: number
          ) {
            // eslint-disable-next-line spellcheck/spell-checker
            acc['p' + index] = new fabric.Control({
              positionHandler: polygonPositionHandler,
              pointIndex: index
            } as any);
            // eslint-disable-next-line spellcheck/spell-checker
            return acc;
          } as any,
          {}
        );
      }
    }
  });
};





function polygonPositionHandler(
  this: { positionHandler: (dim: any, finalMatrix: any, fabricObject: any) => fabric.Point; pointIndex: number },
  dim: any,
  finalMatrix: any,
  fabricObject: any
) {
  var x = fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x,
    y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y;
  return fabric.util.transformPoint(
    { x: x, y: y } as any,
    fabric.util.multiplyTransformMatrices(fabricObject.canvas.viewportTransform, fabricObject.calcTransformMatrix())
  );
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值