fabricjs graphics extension

fabricjs graphics extension

1. Origin

When using fabricjs , I found several problems. First, the operation of the line segment can only be stretched and rotated, and it is impossible to drag one end by pressing and holding the endpoint like any chart; secondly, the polygon can only be zoomed and rotated, and the outer shape cannot be changed. The shape of the box. In fact, after careful analysis, it can be found that the above two problems are the same problem - the problem of dragging the endpoints of polygons (a straight line can be regarded as a "two-sided shape"). So I started trying to solve this problem.

2. Method 1

The first method that comes to mind is to combine multiple graphics as a draggable polygon, which is the following way of writing:

let points: Ellipse[] = []; // 这里用Ellipse是因为Point无法设置大小
let lines: Line[] = [];
function drawMulti(posArr: number[][]) {
    
    
  points = [];
  lines = [];
  if (posArr.length > 0) {
    
    
    for (const index in posArr) {
    
    
      // 取当前点和下一个点做连线
      const pos = posArr[index];
      const posNext = posArr[(+index + 1) % posArr.length];
      const point = new fabric.Ellipse({
    
    
        left: pos[0],
        top: pos[1],
        rx: 5,
        ry: 5,
        originX: "center",
        originY: "center",
        selectable: true,
        opacity: 0.6,
        strokeWidth: 1,
        stroke: "#00aeff",
        fill: "#78cdd1",
        hasControls: false,
        hasRotatingPoint: false,
        data: {
    
    
          objType: `point-${
      
      index}`,
        },
      });
      points.push(point);
      if (posNext) {
    
    
        const line = new fabric.Line([...pos, ...posNext], {
    
    
          left: Math.min(pos[0], posNext[0]) - 0.5,
          top: Math.min(pos[1], posNext[1]) - 0.5,
          selectable: false,
          strokeWidth: 1,
          stroke: "#00aeff",
          opacity: 0.6,
        });
        lines.push(line);
      }
    }
    const point0 = points[0];
    // point0中保存对其余各个点各条线的信息引用,以便后续修改
    for (const index in points) {
    
    
      if (+index !== 0) point0.data[`point${
      
      index}`] = points[index];
      point0.data[`line${
      
      index}`] = lines[index];
    }
    // 各线段保存对point0的引用
    for (const index in points) {
    
    
      if (+index !== 0) points[index].data.point0 = point0;
      lines[index].data = {
    
     point0 };
    }
    // 添加图形
    for (const point of points) canvas.add(point);
    for (const line of lines) {
    
    
      canvas.add(line);
      line.sendToBack();
    }
  }
  canvas.renderAll();
}

Then do special processing on the spliced ​​graphics:

// 拖动以point标记的端点后,特殊处理
canvas.on("object:modified", (opt: IEvent) => {
    
    
  if (opt.target) {
    
    
    const data = opt.target.data;
    if (/point/.test(data.objType)) {
    
    
      redrawLine();
    }
  }
});
canvas.on("object:moving", (opt: IEvent) => {
    
    
  if (opt.target) {
    
    
    const data = opt.target.data;
    if (/point/.test(data.objType)) {
    
    
      redrawLine();
    }
  }
});

function redrawLine() {
    
    
  for (const index in lines) {
    
    
    const point = points[index];
    const pointNext = points[(+index + 1) % points.length];
    // 重新设置线段位置
    lines[index].set({
    
    
      x1: point.left,
      y1: point.top,
      x2: pointNext.left,
      y2: pointNext.top,
      left: Math.min(point.left as number, pointNext.left as number) - 0.5,
      top: Math.min(point.top as number, pointNext.top as number) - 0.5,
    });
    lines[index].sendToBack();
  }
  canvas.renderAll();
}

I found this implementation from the previous historical code. After copying it, I found that it was very troublesome to write. Secondly, the spelled graphics could not be applied to some graphics operation methods of fabric, so I made the second implementation below.

3. Implementation 2

It is impossible to use the native method of fabric to directly piece together graphics, so I thought of inheriting a certain class of fabric, so that I can use the native method to operate it, and the specific idea is to put together multiple elements to get a graphic, so I thought of Group class. The following is the implementation (implemented Polyline, if you want to implement Polygon, you only need to stack a fabric.Polygon on the bottom layer):

export class PolylineExtended extends fabric.Group {
    
    
  private dragging = NaN;  // 正在拖拽的点的index
  private readonly lines: Line[] = [];  // 线段数组
  private readonly points: Ellipse[] = [];  // 点数组
  private readonly pointsPos: number[][] = [];  // 点位置数组
  constructor(
    pointsArr: number[][],
    lineOpts?: ILineOptions,
    pointOpts?: IEllipseOptions,
    groupOpts?: IGroupOptions
  ) {
    
    
    // 初始化
    const lines: Line[] = [];
    const points: Ellipse[] = [];
    const pointsPos: number[][] = [];
    for (const index in pointsArr) {
    
    
      const pos = pointsArr[index];
      const next = (+index + 1) % pointsArr.length;
      const posNext = pointsArr[next];
      const line = new fabric.Line([...pos, ...posNext], {
    
    
        ...lineOpts,
        data: {
    
    
          objType: `line-${
      
      index}-${
      
      next}`,
          ...(lineOpts && lineOpts.data),
        },
        hasControls: false,
        hasRotatingPoint: false,
      });
      lines.push(line);
      const point = new fabric.Ellipse({
    
    
        left: pos[0],
        top: pos[1],
        rx: 5,
        ry: 5,
        originX: "center",
        originY: "center",
        selectable: false,
        opacity: 0.6,
        strokeWidth: 1,
        stroke: "#0000ff",
        fill: "#0000ff",
        ...pointOpts,
        data: {
    
    
          objType: `point-${
      
      index}`,
          ...(pointOpts && pointOpts.data),
        },
        hasControls: false,
        hasRotatingPoint: false,
      });
      points.push(point);
      pointsPos.push(pos);
    }

    super([...lines, ...points], {
    
    
      ...groupOpts,
      hasControls: false,
      hasRotatingPoint: false,
    });
    this.lines = lines;
    this.points = points;
    this.pointsPos = pointsPos;

    // 对Group本身的监听,用于进行拖拽
    this.on("mousedown", (opt: IEvent) => {
    
    
      if (opt.pointer && this.left !== undefined && this.top !== undefined) {
    
    
        const left = opt.pointer.x - this.left;
        const top = opt.pointer.y - this.top;
        const hWidth = (this.width as number) / 2;
        const hHeight = (this.height as number) / 2;
        let flag = true;
        for (const index in this.points) {
    
    
          const point = this.points[index];
          const x = point.left ?? 0;
          const y = point.top ?? 0;
          const offsetX = x - left + hWidth; // 圆心距鼠标点击位置的X轴距离
          const offsetY = y - top + hHeight;  // 圆心距鼠标点击位置的Y轴距离
          // 根据偏移量判断是否点击在端点上
          if (offsetX * offsetX + offsetY * offsetY <= 25) {
    
    
            this.dragging = +index;
            flag = false;
            break;
          }
        }
        if (flag) this.dragging = NaN;
      }
    });
    this.on("mouseup", (opt: IEvent) => {
    
    
      this.dragging = NaN;
    });
    this.on("moving", (opt: IEvent) => {
    
    
      if (opt.pointer && this.left !== undefined && this.top !== undefined) {
    
    
        if (!isNaN(this.dragging)) {
    
    
          // 正拖拽点时
          const left = opt.pointer.x;
          const top = opt.pointer.y;
          /**
           ** 这里用remove移除元素的原因是,如果用set单纯设置每一个内部图形的大小,这个group是无法自动适应内部大小的,
		   ** 因此先移除后添加,这样group就能重新计算其自己的大小了
		   **/
          const objects = this.getObjects();
          for (const object of objects) {
    
    
            this.remove(object);
          }
          // 重新设置各point, line的位置并重新添加到group中
          for (const index in this.points) {
    
    
            const point = this.points[index];
            if (+index === this.dragging) {
    
    
              // 是正在拖拽的点,直接设置其为当前鼠标位置
              point.set({
    
    
                left,
                top,
              });
              this.pointsPos[index] = [left, top];
            } else {
    
    
          /**
           ** 此处重新设置位置的原因是,在group中和不在group中时,object.left/top的坐标系不同,
		   ** 在group中时为相对group的偏移,而不在时是相对canvas的偏移,此时point已不在group中
		   **/
              point.set({
    
    
                left: this.pointsPos[index][0],
                top: this.pointsPos[index][1],
              });
            }
            let prevIndex = +index - 1;
            let nextIndex = +index + 1;
            if (+index === 0) prevIndex = this.points.length - 1;
            if (+index === this.points.length - 1) nextIndex = 0;
            const prevPoint = this.points[prevIndex];
            const nextPoint = this.points[nextIndex];
            const line1 = this.lines[prevIndex];
            const line2 = this.lines[+index];
            // 由于上方已经重设了point的位置,此处直接使用point的位置即可
            line1.set({
    
    
              x1: prevPoint.left,
              y1: prevPoint.top,
              x2: point.left,
              y2: point.top,
            });
            line2.set({
    
    
              x1: point.left,
              y1: point.top,
              x2: nextPoint.left,
              y2: nextPoint.top,
            });
          }
          for (const line of this.lines) this.addWithUpdate(line);
          for (const point of this.points) this.addWithUpdate(point);
        } else {
    
    
          // 并未拖拽点时(对象平移),更新pointPos中的位置信息
          const centerX = this.left + (this.width as number) / 2;
          const centerY = this.top + (this.height as number) / 2;
          for (const index in this.points) {
    
    
            const point = this.points[index];
            const x = (point.left as number) + centerX;
            const y = (point.top as number) + centerY;
            this.pointsPos[index] = [x, y];
          }
        }
      }
    });
  }
  /**
   ** 重写group.set方法,object.set有两种参数,
   ** 一种是<K extends keyof this>(key: K, value: this[K] | ((value: this[K]) => this[K])),
   ** 另一种是set(options: Partial<this>),
   ** 因此要分别处理
   **/
  set(...params: any) {
    
    
    if (typeof params[0] === "string") {
    
    
      // 第一种set方法
      super.set(params[0] as keyof this, params[1]);
      if (["strokeWidth", "stroke", "opacity"].includes(params[0])) {
    
    
        for (const line of this.lines) {
    
    
          line.set(params[0] as keyof Line, params[1]);
        }
      }
      super.set(params);
    } else if (typeof params[0] === "object") {
    
    
      // 第二种set方法
      for (const line of this.lines) {
    
    
        line.set({
    
    
          strokeWidth: params[0].strokeWidth ?? line.strokeWidth,
          stroke: params[0].stroke ?? line.stroke,
          opacity: params[0].opacity ?? line.opacity,
        });
      }
      super.set(params[0]);
    }
    return this;
  }
}

object.set()This method still has some disadvantages. First, the method needs to be rewritten, and the set method may need to be expanded several times in the follow-up function supplement; second, when there is a merge/split group operation in the canvas, the polygon will be recognized as a Group, resulting in behavioral logic errors. So I made a third extension.

3. Achieve 3

I found this extension method in the demo of the fabricjs official website . It is implemented with the controller of the object.controls custom element. I encapsulate it into a class:

// 计算polygon新位置
function polygonPositionHandler(
  this: any,
  dim: any,
  finalMatrix: any,
  fabricObject: any
) {
    
    
  const x = fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x,
    y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y;
  return fabric.util.transformPoint(
    new fabric.Point(x, y),
    fabric.util.multiplyTransformMatrices(
      fabricObject.canvas.viewportTransform,
      fabricObject.calcTransformMatrix()
    )
  );
}
// 计算整个polygon的右下位置
function getObjectSizeWithStroke(object: any) {
    
    
  const stroke = new fabric.Point(
    object.strokeUniform ? 1 / object.scaleX : 1,
    object.strokeUniform ? 1 / object.scaleY : 1
  ).multiply(object.strokeWidth);
  return new fabric.Point(object.width + stroke.x, object.height + stroke.y);
}
// 拖拽操作处理
function actionHandler(eventData: any, transform: any, x: number, y: number) {
    
    
  const polygon = transform.target,
    currentControl = polygon.controls[polygon.__corner],
    mouseLocalPosition = polygon.toLocalPoint(
      new fabric.Point(x, y),
      "center",
      "center"
    ),
    polygonBaseSize = getObjectSizeWithStroke(polygon),
    size = polygon._getTransformedDimensions(0, 0);
  polygon.points[currentControl.pointIndex] = {
    
    
    x:
      (mouseLocalPosition.x * polygonBaseSize.x) / size.x +
      polygon.pathOffset.x,
    y:
      (mouseLocalPosition.y * polygonBaseSize.y) / size.y +
      polygon.pathOffset.y,
  };
  return true;
}
// 固定polygon的位置,避免默认的拖拽平移
function anchorWrapper(anchorIndex: number, fn: any) {
    
    
  return function (eventData: any, transform: any, x: number, y: number) {
    
    
    const fabricObject = transform.target,
      absolutePoint = fabric.util.transformPoint(
        new fabric.Point(
          fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x,
          fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
        ),
        fabricObject.calcTransformMatrix()
      ),
      actionPerformed = fn(eventData, transform, x, y),
      newDim = fabricObject._setPositionDimensions({
    
    }),
      polygonBaseSize = getObjectSizeWithStroke(fabricObject),
      newX =
        (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) /
        polygonBaseSize.x,
      newY =
        (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) /
        polygonBaseSize.y;
    fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
    return actionPerformed;
  };
}

export class PolygonExtended extends fabric.Polygon {
    
    
  constructor(points: {
    
     x: number; y: number }[], options?: IPolylineOptions) {
    
    
    super(points, options);
    const lastControl = (this.points?.length ?? 0) - 1;
    this.controls = this.points?.reduce(
      (acc: any, point: any, index: number) => {
    
    
        acc["p" + index] = new fabric.Control({
    
    
          positionHandler: polygonPositionHandler,
          actionHandler: anchorWrapper(
            index > 0 ? index - 1 : lastControl,
            actionHandler
          ),
          actionName: "modifyPolygon",
        });
        acc["p" + index].pointIndex = index;
        return acc;
      },
      {
    
    }
    );
  }
}

There is a small problem with this method, that is, if the polygon is selected (set to activeObject) while its clone element is added to the canvas, then the controls will go wrong, so special handling may be required in this case;

4. Summary

Among the above three methods, the third method is undoubtedly the best method for adaptation (this story teaches us to read the document carefully). The first two methods have their own problems, but they are also the result of my own thinking. So record your thoughts like this.

Guess you like

Origin blog.csdn.net/qq_41575208/article/details/128093037