首页 前端知识 fabricjs图形拓展

fabricjs图形拓展

2024-06-14 23:06:40 前端知识 前端哥 873 826 我要收藏
fabricjs图形拓展
1. 起因

在使用fabricjs的时候发现有几个问题,首先线段的操作只能伸缩、旋转,而没法像任意图表那样按住端点可以拖动一端;其次多边形也只能进行缩放、旋转,同样不能变更外框的形状。其实仔细分析之后可以发现上述两个问题都是一个问题 —— 多边形的端点拖拽问题(直线可以看作“二边形”)。于是我就开始尝试解决这个问题。

2. 方法1

首先想到的方法就是把多个图形组合起来视为一个可拖拽的多边形,也就是下面的写法:

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

而后对这个拼接出来的图形做特殊处理:

// 拖动以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();
}

这个实现是我从以前的历史代码里找到的,copy了一下之后发现首先写起来很麻烦,其次这个拼出来的图形没法适用fabric的一些图形操作的方法,于是做了下面第二种实现。

3. 实现2

直接拼凑图形没法使用fabric原生方法,于是我想到了继承fabric某一类,那样不就可以用原生方法操作它了吗,而具体思路还是把多个元素拼起来得到一个图形,于是我想到了Group类。下面是实现(实现的Polyline,若要实现Polygon只需要在最底层再叠一个fabric.Polygon即可):

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()方法,而且在后续功能补充是可能还要多次拓展set方法;其次是在canvas中有合并/拆分组操作时,这个多边形会被识别成一个Group,导致行为逻辑错误。因此我又做了第三种拓展。

3. 实现3

这个拓展方法是我在fabricjs官网的demo里找到的,是用object.controls自定义元素的控制器实现的,我把它封装成了一个类:

// 计算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;
      },
      {}
    );
  }
}

这个方法有一个小问题,就是如果在创建polygon,将其克隆元素添加到canvas上的同时将其选中(设置为activeObject),那么controls就会出错,因此在这种情况下可能需要特殊处理;

4. 总结

上面三种方法里,第三种方法毫无疑问是适配最好的方法(这个故事教育我们要认真看文档),前两种方式都各自有一些问题,不过也算是自己思考的结果了,于是就这样记录一下自己的思路。

转载请注明出处或者链接地址:https://www.qianduange.cn//article/12155.html
标签
评论
发布的文章

fastjson升级为fastjson2

2024-06-20 09:06:18

protobuf对象与JSON相互转换

2024-06-20 09:06:14

NVM 及 PNPM 安装

2024-06-20 09:06:01

npm有哪些插件包??

2024-06-20 09:06:01

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!