遇到一个需求是,给fabric的图片加一个相框,而且支持拖拽变换。查了下没查到,自己想了个蠢方法:

  1. 图片对象在canvas里可以拿到位置,在aCroods里有tl,tr,bl,br代表四角坐标,根据这个可以知道四个边框作为四个Rect的长宽,这个先直接框住图片。
  2. 但是在四边会有四个正方形的空白,相框都是要由斜角组成的,所以这里要给四个角画8个三角形,并根据图片坐标定位。
  3. 所有这些都需要在同一个选择集里面,否则无法一起跟随变形,用group组成一个新的整体对象,然后将group加回canvas
/**
 * 将边框添加到当前活动对象
 */
addFrameToActive(url) {
  url = loadFromRemote(url);
  var us = [
    "https://img.alicdn.com/imgextra/i1/O1CN01g0WBpQ1rpwhu9wD8U_!!6000000005681-2-tps-104-104.png",
    "https://img.alicdn.com/imgextra/i4/O1CN01innORS1WrDsaz9ElQ_!!6000000002841-2-tps-105-105.png",
    "https://img.alicdn.com/imgextra/i3/O1CN01MxntTp1WGaC8FV5kR_!!6000000002761-2-tps-130-130.png",
  ]
  url = us[Math.floor(Math.random() * 3)];

  let object = this.getActiveObject()
  if (!object) {
    console.log('no active object detected.'); return;
  }
  fabric.util.loadImage(url, (img) => {
    if (img.width !== img.height) {
      console.error(`this frame\'s texture is not square, please connect with administrator. url info: [${url}]`);
      return;
    }
    let frameParts, originObj = object;
    if (object.isFrame) {
      [object, ...frameParts] = object.getObjects();
    }

    // width of a frame
    let w = 20;

    // t b l r : triangle
    /**
     * x1,y1 ------- x2,y2
     *   |             |
     *   |             |
     *   |             |
     * x3,y3 ------- x4,y4
     */
    let {
      tl: { x: x1, y: y1 },
      tr: { x: x2, y: y2 },
      bl: { x: x3, y: y3 },
      br: { x: x4, y: y4 }
    } = object.aCoords
    let bar = [
      // M L L top, left
      [x1, y1, x1 - w, y1 - w, x1, y1 - w, x1 - w, y1 - w], // t
      [x2, y2, x2 + w, y2 - w, x2, y2 - w, x2, y2 - w],     // t
      [x3, y3, x3 - w, y3 + w, x3, y3 + w, x3 - w, y3],     // b
      [x4, y4, x4 + w, y4 + w, x4, y4 + w, x4, y4],         // b
      [x1, y1, x1 - w, y1 - w, x1 - w, y1, x1 - w, y1 - w], // l
      [x3, y3, x3 - w, y3 + w, x3 - w, y3, x3 - w, y3],     // l
      [x2, y2, x2 + w, y2 - w, x2 + w, y2, x2, y2 - w],     // r
      [x4, y4, x4 + w, y4 + w, x4 + w, y4, x4, y4],         // r
    ]

    // t b l r : rect
    let foo = [
      {
        width: object.aCoords.tr.x - object.aCoords.tl.x,
        height: w,
        top: object.aCoords.tl.y - w,
        left: object.aCoords.tl.x,
        rotate: 180,
        repeat: 'x'
      },
      {
        width: object.aCoords.br.x - object.aCoords.bl.x,
        height: w,
        top: object.aCoords.bl.y,
        left: object.aCoords.bl.x,
        rotate: 0,
        repeat: 'x'
      },
      {
        width: w,
        height: object.aCoords.bl.y - object.aCoords.tl.y,
        top: object.aCoords.tl.y,
        left: object.aCoords.tl.x - w,
        rotate: 90,
        repeat: 'y'
      },
      {
        width: w,
        height: object.aCoords.br.y - object.aCoords.tr.y,
        top: object.aCoords.tr.y,
        left: object.aCoords.tr.x,
        rotate: 270,
        repeat: 'y'
      },
    ].map((i, idx) => ({
      ...i,
      triangles: bar.slice(2*idx, 2*idx+2),
    }));

    const all = foo
      .map(i => {
        const o = new fabric.Image(img, {});
        o.scaleToHeight(10, true) // 縮小成 height = 50
        o.rotate(i.rotate);

        const patternSourceCanvas = new fabric.StaticCanvas();
        patternSourceCanvas.add(o);
        patternSourceCanvas.renderAll();

        const pattern = new fabric.Pattern({
          source: function () {
            patternSourceCanvas.setDimensions({
              width: o.getScaledWidth(),
              height: o.getScaledHeight(),
            });
            patternSourceCanvas.renderAll();
            // html element
            return patternSourceCanvas.getElement();
          },
          repeat: `repeat-${i.repeat}`,
        });

        const r = new fabric.Rect({
          width: i.width,
          height: i.height,
          top: i.top,
          left: i.left,
          fill: pattern,
        });

        const ts = i.triangles.map(tri => {
          const p = new fabric.Path(`M ${tri[0]} ${tri[1]} L ${tri[2]} ${tri[3]} L ${tri[4]} ${tri[5]} Z`)
          p.set({
            left: tri[6],
            top: tri[7],
            fill: pattern,
          });
          return p;
        })

        return [ r, ...ts ];
      })
        .reduce((acc, curr) => curr.concat(acc), []);

    if (originObj.isFrame) {
      originObj.remove(...frameParts);
      all.forEach(i => originObj.addWithUpdate(i));
      // originObj.render();
      this.canvas.setActiveObject(originObj);
      this.canvas.renderAll();
    } else {
      const g = new fabric.Group([
        object,
        ...all, // t, b, l, r, and triangles
      ]);

      g.isFrame = true;

      this.canvas.add(g).setActiveObject(g, { add: true });
    }

    this.updateState();
  })

}

另外几个需要考虑的点是:

  1. 相框添加后如何切换相框,要判断是否是group后者group是否是加了相框的,如果添加相框同时活动对象已经有相框,那执行的是替换而不是再加一层;
  2. 去掉相框,这个比较简单,拆分group后保留主体
  3. 相框本身不能成比例缩放,这个还没加入,因为就目前的用例来讲,相框被拖拽的很浮夸的情况不太可能发生;而解决的方案应该是监听拖拽结束事件,当拖拽结束去掉原相框,再重新计算后添加新相框,当然也可以在拖拽实时计算相框本身的改动,只是会无谓的消耗性能。