Skip to content

fabric

fabric是一个基于canvas2d 绘图工具库,如果你打算使用canvas进行2d绘图,推荐使用fabric

官方文档比较难看懂,推荐看此作者的文章:Fabric.js 从入门到____

进阶:更推荐看此仓库源码vue-fabric-editor

基础设置

源代码
vue
<template>
  <fabric-canvas @init="onFabricCanvasInit"></fabric-canvas>
  <div class="actions">
    <button type="button" @click="addRect">矩形</button>
    <button type="button" @click="useConstomContols">设置自定义控件</button>
    <button type="button" @click="resetContols">恢复控件</button>
  </div>
  <div class="actions">
    <button type="button" @click="lockObject">锁定元素</button>
    <button type="button" @click="unlockObject">解锁元素</button>
  </div>
</template>
<script setup>
import bgTransparent from '../../../images/bg-transparent.png';
import FabricCanvas from '../canvas.vue';
import FabricZoom from '../handles/zoom';
import FabricSelection from '../handles/selection';
import FabricTranslate from '../handles/translate';
import {
  customDefaultContorls,
  resetDetaultContols,
} from '../controls/default';
import useCornerRotateControls from '../controls/rotate';
import useDelControl from '../controls/del';
import {
  useLockControl,
  doLockActivedObjectControl,
  doUnlockActivedObjectControl,
} from '../controls/lock';

let fabricCanvas = null;
let fabric = null;

async function onFabricCanvasInit(canvas, fabricModule) {
  fabricCanvas = canvas;
  fabric = fabricModule;
  // 设置背景
  canvas.setBackgroundColor(
    {
      source: bgTransparent,
      repeat: 'repeat',
    },
    canvas.renderAll.bind(canvas)
  );

  // 设置缩放
  const zoomInstance = new FabricZoom(canvas, { byPoint: true });
  zoomInstance.onChange((zoom) => {
    // console.log(zoom)
  });

  // 设置拖拽移动
  const translateInstance = new FabricTranslate(canvas);
  translateInstance.onChange((vpt) => {
    // console.log(vpt)
  });

  // 选中元素回调
  const selectionInstance = new FabricSelection(canvas);
  selectionInstance.onSelect((objects) => {
    // console.log(objects)
  });
  selectionInstance.onClear(() => {
    // console.log('clear')
  });
}

// 添加一个矩形
function addRect() {
  // 创建一个长方形
  const rect = new fabric.Rect({
    top: 100, // 距离容器顶部 100px
    left: 100, // 距离容器左侧 100px
    width: 30, // 矩形宽度 30px
    height: 30, // 矩形高度 30px
    fill: 'blue', // 填充
  });

  fabricCanvas.add(rect); // 将矩形添加到 canvas 画布里
}

let removeRotateControlEventCache = null;
// 使用自定义控件
function useConstomContols() {
  // 覆盖默认控件
  customDefaultContorls();
  // 新自定义旋转控件
  const { removeRotateControlEvent } = useCornerRotateControls(fabricCanvas);
  removeRotateControlEventCache = removeRotateControlEvent;
  // 新增删除控件
  useDelControl(fabricCanvas, (object) => {
    // console.log(object);
  });
  // 新增锁定控件
  useLockControl(() => {
    unlockObject();
  });
  fabricCanvas.requestRenderAll();
}
// 恢复默认控件
function resetContols() {
  resetDetaultContols();
  removeRotateControlEventCache && removeRotateControlEventCache();
  fabricCanvas.requestRenderAll();
}

function lockObject() {
  const activeObject = fabricCanvas.getActiveObject();
  doLockActivedObjectControl(activeObject);
  fabricCanvas.requestRenderAll();
}
function unlockObject() {
  const activeObject = fabricCanvas.getActiveObject();
  doUnlockActivedObjectControl(activeObject);
  fabricCanvas.requestRenderAll();
}
</script>
<style lang="less" scoped>
.actions {
  margin-top: 10px;
  > button {
    padding: 0 10px;
    border: 1px solid #eee;
    margin-right: 10px;
  }
}
</style>
js
export default class FabricZoom {
  constructor(canvas, options = { byPoint: true }) {
    this.events = [];
    this.hotkeys = options.hotkeys || ['Ctrl'];
    const byPoint = options.byPoint;
    // 缩放,监听鼠标滚轮事件
    canvas.on('mouse:wheel', (opt) => {
      if (this.hotkeys.includes('Ctrl') && !opt.e.ctrlKey) {
        return false;
      }
      opt.e.stopPropagation();
      opt.e.preventDefault();
      const delta = opt.e.deltaY; // 滚轮向上滚一下是 -100,向下滚一下是 100
      let zoom = canvas.getZoom(); // 获取画布当前缩放值

      // 控制缩放范围在 0.01~20 的区间内
      zoom *= 0.999 ** delta;
      if (zoom > 20) zoom = 20;
      if (zoom < 0.01) zoom = 0.01;

      // 设置画布缩放比例
      // 关键点!!!
      // 参数1:将画布的所放点设置成鼠标当前位置
      // 参数2:传入缩放值
      if (byPoint) {
        canvas.zoomToPoint(
          {
            x: opt.e.offsetX, // 鼠标x轴坐标
            y: opt.e.offsetY, // 鼠标y轴坐标
          },
          zoom // 最后要缩放的值
        );
      } else {
        canvas.setZoom(zoom);
      }
      this.events.forEach((cb) => {
        cb(zoom);
      });
    });
  }
  onChange(cb) {
    this.events.push(cb);
  }
}
js
import hotkeys from 'hotkeys-js';
export default class FabricTranslate {
  constructor(canvas, options = {}) {
    this.events = [];
    this.dragMode = false;
    this.canvas = canvas;
    this.selection = canvas.selection;
    this.hotkeys = options.hotkeys || ['Space'];
    this.checkObjectMovable = options.checkObjectMovable;
    this.initHotKey();
    this.initDring();
  }

  initHotKey() {
    const hotkeyEvent = (eventName, e) => {
      e.preventDefault();
      const matchKey = this.hotkeys.find((item) => item === e.code);
      if (matchKey && e.type === 'keydown') {
        if (!this.dragMode) {
          this.startDring();
        }
      }
      if (matchKey && e.type === 'keyup') {
        this.endDring();
      }
    };
    this.hotkeys.forEach((keyName) => {
      hotkeys(keyName, { keyup: true }, (e) => {
        hotkeyEvent(keyName, e);
      });
    });
  }

  initDring() {
    const canvas = this.canvas;
    // 平移
    canvas.on('mouse:down', (opt) => {
      const checkObjectMovable = this.checkObjectMovable;
      // 鼠标按下时触发
      const evt = opt.e;
      const targetObject = opt.target;
      const onDragState =
        (!opt.transform || opt.transform.action === 'drag') && evt.button === 0;
      // 传入checkObjectMovable且以空白处开始拖拽时,允许拖拽画布
      const onDragWhitespace =
        checkObjectMovable &&
        (!targetObject || !checkObjectMovable(targetObject));
      if (this.dragMode || (onDragState && onDragWhitespace)) {
        canvas.setCursor('grab');
        canvas.isDragging = true; // isDragging 是自定义的,开启移动状态
        canvas.lastPosX = evt.clientX; // lastPosX 是自定义的
        canvas.lastPosY = evt.clientY; // lastPosY 是自定义的
        // 禁止选中
        canvas.selection = false;
        canvas.getObjects().forEach((obj) => {
          obj.beforeCanvasMoveSelectable = obj.selectable;
          obj.selectable = false;
        });
        canvas.requestRenderAll();
      }
    });

    canvas.on('mouse:move', (opt) => {
      // 鼠标移动时触发
      if (canvas.isDragging) {
        const evt = opt.e;
        const vpt = canvas.viewportTransform; // 聚焦视图的转换
        vpt[4] += evt.clientX - canvas.lastPosX;
        vpt[5] += evt.clientY - canvas.lastPosY;
        canvas.requestRenderAll(); // 重新渲染
        canvas.lastPosX = evt.clientX;
        canvas.lastPosY = evt.clientY;
        this.events.forEach((cb) => {
          cb(vpt);
        });
      }
    });

    canvas.on('mouse:up', (opt) => {
      // 鼠标松开时触发
      canvas.setViewportTransform(canvas.viewportTransform); // 设置此画布实例的视口转换
      canvas.isDragging = false; //
      canvas.setCursor('auto');
      // 取消禁止选中
      canvas.selection = this.selection;
      canvas.getObjects().forEach((obj) => {
        obj.selectable =
          typeof obj.beforeCanvasMoveSelectable !== 'undefined'
            ? obj.beforeCanvasMoveSelectable
            : obj.selectable;
        delete obj.beforeCanvasMoveSelectable;
      });
      canvas.requestRenderAll();
    });
  }

  startDring() {
    this.dragMode = true;
    this.canvas.defaultCursor = 'grab';
    this.canvas.renderAll();
  }

  endDring() {
    this.dragMode = false;
    this.canvas.defaultCursor = 'default';
    this.canvas.isDragging = false;
    this.canvas.renderAll();
  }

  onChange(cb) {
    this.events.push(cb);
  }
}
js
export default class FabricSelection {
  constructor(canvas) {
    this.selectEvents = [];
    this.clearEvents = [];
    canvas.on('selection:created', (e) => onObjectsSelect(e));
    canvas.on('selection:updated', (e) => onObjectsSelect(e));
    canvas.on('selection:cleared', (e) => onObjectsSelect(e));

    const onObjectsSelect = (e) => {
      const actives = canvas.getActiveObjects();
      if (actives && actives.length > 0) {
        this.selectEvents.forEach((cb) => {
          cb(actives);
        });
      } else {
        this.clearEvents.forEach((cb) => {
          cb();
        });
      }
    };
  }
  onSelect(cb) {
    this.selectEvents.push(cb);
  }
  onClear(cb) {
    this.clearEvents.push(cb);
  }
}
js
import { fabric } from 'fabric';
import verticalImg from '../../../images/controls/middlecontrol.svg';
import horizontalImg from '../../../images/controls/middlecontrolhoz.svg';
import edgeImg from '../../../images/controls/edgecontrol.svg';
import rotateImg from '../../../images/controls/rotateicon.svg';

// 缓存默认控件
const defaultControls = { ...fabric.Object.prototype.controls };
// 缓存默认对象设置
const defaultObjectConfig = {
  transparentCorners: fabric.Object.prototype.transparentCorners,
  borderColor: fabric.Object.prototype.borderColor,
  cornerColor: fabric.Object.prototype.cornerColor,
  borderScaleFactor: fabric.Object.prototype.borderScaleFactor,
  cornerStyle: fabric.Object.prototype.cornerStyle,
  cornerStrokeColor: fabric.Object.prototype.cornerStrokeColor,
  borderOpacityWhenMoving: fabric.Object.prototype.borderOpacityWhenMoving,
};
// 缓存默认组设置
const defaultGroupConfig = {
  lockMovementX: fabric.Group.prototype.lockMovementX,
  lockMovementY: fabric.Group.prototype.lockMovementY,
  lockRotation: fabric.Group.prototype.lockRotation,
  lockScalingX: fabric.Group.prototype.lockScalingX,
  lockScalingY: fabric.Group.prototype.lockScalingY,
  selectable: fabric.Group.prototype.selectable,
  hasControls: fabric.Group.prototype.hasControls,
};

// 覆盖默认中间横杠
function intervalControl() {
  const verticalImgIcon = document.createElement('img');
  verticalImgIcon.src = verticalImg;

  const horizontalImgIcon = document.createElement('img');
  horizontalImgIcon.src = horizontalImg;

  function renderIcon(ctx, left, top, styleOverride, fabricObject) {
    const wsize = 20;
    const hsize = 20;
    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
    ctx.drawImage(verticalImgIcon, -wsize / 2, -hsize / 2, wsize, hsize);
    ctx.restore();
  }

  function renderIconHoz(ctx, left, top, styleOverride, fabricObject) {
    const wsize = 20;
    const hsize = 20;
    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
    ctx.drawImage(horizontalImgIcon, -wsize / 2, -hsize / 2, wsize, hsize);
    ctx.restore();
  }
  // 中间横杠:左
  fabric.Object.prototype.controls.ml = new fabric.Control({
    visible: true,
    x: -0.5,
    y: 0,
    offsetX: -1,
    cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
    actionHandler: fabric.controlsUtils.scalingXOrSkewingY,
    getActionName: fabric.controlsUtils.scaleOrSkewActionName,
    render: renderIcon,
  });
  // 中间横杠:右
  fabric.Object.prototype.controls.mr = new fabric.Control({
    visible: true,
    x: 0.5,
    y: 0,
    offsetX: 1,
    cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
    actionHandler: fabric.controlsUtils.scalingXOrSkewingY,
    getActionName: fabric.controlsUtils.scaleOrSkewActionName,
    render: renderIcon,
  });
  // 中间横杠:底
  fabric.Object.prototype.controls.mb = new fabric.Control({
    visible: true,
    x: 0,
    y: 0.5,
    offsetY: 1,
    cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
    actionHandler: fabric.controlsUtils.scalingYOrSkewingX,
    getActionName: fabric.controlsUtils.scaleOrSkewActionName,
    render: renderIconHoz,
  });
  // 中间横杠:顶
  fabric.Object.prototype.controls.mt = new fabric.Control({
    visible: true,
    x: 0,
    y: -0.5,
    offsetY: -1,
    cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
    actionHandler: fabric.controlsUtils.scalingYOrSkewingX,
    getActionName: fabric.controlsUtils.scaleOrSkewActionName,
    render: renderIconHoz,
  });
}

// 覆盖顶点
function peakControl() {
  const img = document.createElement('img');
  img.src = edgeImg;

  function renderIconEdge(ctx, left, top, styleOverride, fabricObject) {
    const wsize = 20;
    const hsize = 20;
    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
    ctx.drawImage(img, -wsize / 2, -hsize / 2, wsize, hsize);
    ctx.restore();
  }
  // 四角图标:上左
  fabric.Object.prototype.controls.tl = new fabric.Control({
    x: -0.5,
    y: -0.5,
    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,
    actionHandler: fabric.controlsUtils.scalingEqually,
    render: renderIconEdge,
  });
  // 四角图标:下左
  fabric.Object.prototype.controls.bl = new fabric.Control({
    x: -0.5,
    y: 0.5,
    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,
    actionHandler: fabric.controlsUtils.scalingEqually,
    render: renderIconEdge,
  });
  // 四角图标:上右
  fabric.Object.prototype.controls.tr = new fabric.Control({
    x: 0.5,
    y: -0.5,
    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,
    actionHandler: fabric.controlsUtils.scalingEqually,
    render: renderIconEdge,
  });
  // 四角图标:下右
  fabric.Object.prototype.controls.br = new fabric.Control({
    x: 0.5,
    y: 0.5,
    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,
    actionHandler: fabric.controlsUtils.scalingEqually,
    render: renderIconEdge,
  });
}

// 覆盖旋转点
function rotationControl() {
  const img = document.createElement('img');
  img.src = rotateImg;
  function renderIconRotate(ctx, left, top, styleOverride, fabricObject) {
    const wsize = 40;
    const hsize = 40;
    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
    ctx.drawImage(img, -wsize / 2, -hsize / 2, wsize, hsize);
    ctx.restore();
  }
  // 旋转图标
  fabric.Object.prototype.controls.mtr = new fabric.Control({
    x: 0,
    y: 0.5,
    cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,
    actionHandler: fabric.controlsUtils.rotationWithSnapping,
    offsetY: 30,
    withConnecton: false,
    actionName: 'rotate',
    render: renderIconRotate,
  });
}

// 自定义控件
export function customDefaultContorls(canvas) {
  // 顶点图标
  peakControl(canvas);
  // 中间横杠图标
  intervalControl(canvas);
  // 旋转图标
  rotationControl(canvas);
  // 选中样式
  fabric.Object.prototype.set({
    transparentCorners: false,
    borderColor: '#5E7FFF',
    cornerColor: '#FFF',
    borderScaleFactor: 1.5,
    cornerStyle: 'circle',
    cornerStrokeColor: '#0E98FC',
    borderOpacityWhenMoving: 0.7,
  });
  // 组样式
  fabric.Group.prototype.set({
    lockMovementX: false,
    lockMovementY: false,
    lockRotation: false,
    lockScalingX: false,
    lockScalingY: false,
    selectable: true,
    hasControls: true,
  });
}

// 恢复默认控件
export function resetDetaultContols() {
  fabric.Object.prototype.controls = {
    ...defaultControls,
  };
  fabric.Object.prototype.set(defaultObjectConfig);
  fabric.Group.prototype.set(defaultGroupConfig);
}
js
import { fabric } from 'fabric';

// 定义旋转光标样式,根据转动角度设定光标旋转
function rotateIcon(angle) {
  return `url("data:image/svg+xml,%3Csvg height='18' width='18' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'%3E%3Cg fill='none' transform='rotate(${angle} 16 16)'%3E%3Cpath d='M22.4484 0L32 9.57891L22.4484 19.1478V13.1032C17.6121 13.8563 13.7935 17.6618 13.0479 22.4914H19.2141L9.60201 32.01L0 22.4813H6.54912C7.36524 14.1073 14.0453 7.44023 22.4484 6.61688V0Z' fill='white'/%3E%3Cpath d='M24.0605 3.89587L29.7229 9.57896L24.0605 15.252V11.3562C17.0479 11.4365 11.3753 17.0895 11.3048 24.0879H15.3048L9.60201 29.7308L3.90932 24.0879H8.0806C8.14106 15.3223 15.2645 8.22345 24.0605 8.14313V3.89587Z' fill='black'/%3E%3C/g%3E%3C/svg%3E ") 12 12,crosshair`;
}

export function useCornerRotateControls(canvas) {
  // 添加旋转控制响应区域
  fabric.Object.prototype.controls.mtr = new fabric.Control({
    x: -0.5,
    y: -0.5,
    offsetY: -10,
    offsetX: -10,
    rotate: 20,
    actionName: 'rotate',
    actionHandler: fabric.controlsUtils.rotationWithSnapping,
    render: () => {},
  });
  // ↖左上
  fabric.Object.prototype.controls.mtr2 = new fabric.Control({
    x: 0.5,
    y: -0.5,
    offsetY: -10,
    offsetX: 10,
    rotate: 20,
    actionName: 'rotate',
    actionHandler: fabric.controlsUtils.rotationWithSnapping,
    render: () => {},
  }); // ↗右上
  fabric.Object.prototype.controls.mtr3 = new fabric.Control({
    x: 0.5,
    y: 0.5,
    offsetY: 10,
    offsetX: 10,
    rotate: 20,
    actionName: 'rotate',
    actionHandler: fabric.controlsUtils.rotationWithSnapping,
    render: () => {},
  }); // ↘右下
  fabric.Object.prototype.controls.mtr4 = new fabric.Control({
    x: -0.5,
    y: 0.5,
    offsetY: 10,
    offsetX: -10,
    rotate: 20,
    actionName: 'rotate',
    actionHandler: fabric.controlsUtils.rotationWithSnapping,
    render: () => {},
  }); // ↙左下

  const onCanvasRender = () => {
    if (canvas.getActiveObject()) {
      fabric.Object.prototype.controls.mtr.cursorStyle = rotateIcon(
        Number(canvas.getActiveObject().angle.toFixed(2))
      );
      fabric.Object.prototype.controls.mtr2.cursorStyle = rotateIcon(
        Number(canvas.getActiveObject().angle.toFixed(2)) + 90
      );
      fabric.Object.prototype.controls.mtr3.cursorStyle = rotateIcon(
        Number(canvas.getActiveObject().angle.toFixed(2)) + 180
      );
      fabric.Object.prototype.controls.mtr4.cursorStyle = rotateIcon(
        Number(canvas.getActiveObject().angle.toFixed(2)) + 270
      );
    }
  };
  const onObjectRotating = (event) => {
    const body = canvas.lowerCanvasEl.nextSibling;
    switch (event.transform.corner) {
      case 'mtr':
        body.style.cursor = rotateIcon(
          Number(canvas.getActiveObject().angle.toFixed(2))
        );
        break;
      case 'mtr2':
        body.style.cursor = rotateIcon(
          Number(canvas.getActiveObject().angle.toFixed(2)) + 90
        );
        break;
      case 'mtr3':
        body.style.cursor = rotateIcon(
          Number(canvas.getActiveObject().angle.toFixed(2)) + 180
        );
        break;
      case 'mtr4':
        body.style.cursor = rotateIcon(
          Number(canvas.getActiveObject().angle.toFixed(2)) + 270
        );
        break;
      default:
        break;
    } // 设置四角旋转光标
  };

  // 渲染时,执行
  canvas.on('after:render', onCanvasRender);

  // 旋转时,实时更新旋转控制图标
  canvas.on('object:rotating', onObjectRotating);

  function removeRotateControlEvent() {
    canvas.off('after:render', onCanvasRender);
    canvas.off('object:rotating', onObjectRotating);
  }

  return {
    removeRotateControlEvent,
  };
}

export default useCornerRotateControls;
js
import { fabric } from 'fabric';
import deleteIcon from '../../../images/controls/canvasObjectDeleteIcon.svg';
// 删除按钮
export function useDelControl(canvas, onObjectRemove) {
  const delImg = document.createElement('img');
  delImg.src = deleteIcon;

  function renderDelIcon(ctx, left, top, styleOverride, fabricObject) {
    const size = this.cornerSize;
    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
    ctx.drawImage(delImg, -size / 2, -size / 2, size, size);
    ctx.restore();
  }

  // 删除选中元素
  function deleteObject() {
    const activeObject = canvas.getActiveObjects();
    if (activeObject) {
      activeObject.map((item) => canvas.remove(item));
      canvas.requestRenderAll();
      canvas.discardActiveObject();
    }
    onObjectRemove && onObjectRemove(activeObject);
  }

  // 删除图标
  fabric.Object.prototype.controls.del = new fabric.Control({
    x: 0.5,
    y: -0.5,
    offsetY: -16,
    offsetX: 16,
    cursorStyle: 'pointer',
    mouseUpHandler: deleteObject,
    render: renderDelIcon,
    cornerSize: 24,
  });
}

export default useDelControl;
js
import { fabric } from 'fabric';
import lockCanvasImg from '../../../images/controls/lockCanvasObject.svg';

const LOCK_CONTROL_KEY = 'lock';

/**
 * 锁定元素
 */
export function doLockActivedObjectControl(activeObject) {
  if (!activeObject) {
    return false;
  }
  const oldControlVisibleCache = {};
  const oldBorderColor = activeObject.borderColor;
  for (let key in activeObject.controls) {
    oldControlVisibleCache[key] = activeObject.controls[key].visible;
    if (key === LOCK_CONTROL_KEY) {
      activeObject.setControlVisible(LOCK_CONTROL_KEY, true);
    } else {
      activeObject.setControlVisible(key, false);
    }
  }
  activeObject.oldControlVisibleCache = oldControlVisibleCache;
  activeObject.oldBorderColor = oldBorderColor;
  activeObject.borderColor = '#FF3232';
}

/**
 * 解锁元素
 */
export function doUnlockActivedObjectControl(activeObject) {
  if (!activeObject) {
    return false;
  }
  const oldControlVisibleCache = activeObject.oldControlVisibleCache || {};

  for (let key in activeObject.controls) {
    if (key === LOCK_CONTROL_KEY) {
      activeObject.setControlVisible(LOCK_CONTROL_KEY, false);
    } else {
      activeObject.setControlVisible(key, oldControlVisibleCache[key] || true);
    }
  }
  activeObject.borderColor = activeObject.oldBorderColor || '#FF3232';
}

export function useLockControl(unlockCallback) {
  const img = document.createElement('img');
  img.src = lockCanvasImg;
  function renderIconLock(ctx, left, top, styleOverride, fabricObject) {
    const wsize = 24;
    const hsize = 24;
    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
    ctx.drawImage(img, -wsize / 2, -hsize, wsize, hsize);
    ctx.restore();
  }
  // 解锁元素
  function unlockActivedObject() {
    unlockCallback && unlockCallback();
  }
  // 锁定图标,只有被锁定时才显示,点击后解锁
  fabric.Object.prototype.controls[LOCK_CONTROL_KEY] = new fabric.Control({
    x: 0.5,
    y: 0.5,
    offsetY: 16,
    touchSizeX: 36,
    touchSizeY: 36,
    visible: false,
    sizeX: 36,
    sizeY: 36,
    mouseUpHandler: unlockActivedObject,
    cursorStyle: 'pointer',
    actionName: 'ulock',
    render: renderIconLock,
  });
}

export default {
  useLockControl,
  doLockActivedObjectControl,
  doUnlockActivedObjectControl,
};
vue
<template>
  <div ref="wrapEl" class="canvas-box">
    <canvas :id="canvasId"></canvas>
  </div>
</template>
<script setup>
import { useId } from 'vue';
// import { fabric } from 'fabric';
import { onMounted, ref } from 'vue';

const canvasId = `fabric-canvas-${useId()}`;

const emit = defineEmits(['init']);

const wrapEl = ref(null);
let fabricCanvas;

const initFabric = () => {
  import('fabric').then((module) => {
    // use code
    const { fabric } = module.default;
    const wrapHeight = wrapEl.value.offsetHeight;
    const wrapWidth = wrapEl.value.offsetWidth;
    fabricCanvas = new fabric.Canvas(canvasId, {
      width: wrapWidth,
      height: wrapHeight,
    });
    emit('init', fabricCanvas, fabric);
  });
};

onMounted(initFabric);
</script>
<style>
.canvas-box {
  width: 100%;
  height: 450px;
  border: 1px solid #eaeaea;
  background-color: #fafafa;
}
</style>

应用

一、常用处理函数

源代码
vue
<template>
  <fabric-canvas @init="onFabricCanvasInit"></fabric-canvas>
  <div class="actions">
    <button type="button" @click="screenshot">截图</button>
    <button type="button" @click="centerSelectedObject">居中选中元素</button>
    <button type="button" @click="zoomToFitObjects(fabricCanvas)">
      展示所有元素
    </button>
  </div>
</template>
<script setup>
import bgTransparent from '../../../images/bg-transparent.png';
import FabricCanvas from '../canvas.vue';
import FabricZoom from '../handles/zoom';
import FabricSelection from '../handles/selection';
import FabricTranslate from '../handles/translate';
import { setCenterFromObject, zoomToFitObjects } from '../handles/utils';

let fabricCanvas = null;
let fabric = null;
let selectedObject = null;

async function onFabricCanvasInit(canvas, fabricModule) {
  fabricCanvas = canvas;
  fabric = fabricModule;
  // 设置背景
  canvas.setBackgroundColor(
    {
      source: bgTransparent,
      repeat: 'repeat',
    },
    canvas.renderAll.bind(canvas)
  );

  // 设置缩放
  const zoomInstance = new FabricZoom(canvas, { byPoint: true });
  zoomInstance.onChange((zoom) => {
    // console.log(zoom)
  });

  // 设置拖拽移动
  const translateInstance = new FabricTranslate(canvas);
  translateInstance.onChange((vpt) => {
    // console.log(vpt)
  });

  // 选中元素回调
  const selectionInstance = new FabricSelection(canvas);
  selectionInstance.onSelect((objects) => {
    selectedObject = objects[0];
  });
  selectionInstance.onClear(() => {
    selectedObject = null;
    // console.log('clear')
  });

  // 创建一个长方形
  const rect = new fabric.Rect({
    top: 100, // 距离容器顶部 100px
    left: 100, // 距离容器左侧 100px
    width: 30, // 矩形宽度 30px
    height: 30, // 矩形高度 30px
    fill: 'blue', // 填充
  });

  fabricCanvas.add(rect); // 将矩形添加到 canvas 画布里

  // 创建一个长方形
  const rect2 = new fabric.Rect({
    top: 500, // 距离容器顶部 100px
    left: 500, // 距离容器左侧 100px
    width: 100, // 矩形宽度 30px
    height: 100, // 矩形高度 30px
    fill: 'purple', // 填充
  });

  fabricCanvas.add(rect2); // 将矩形添加到 canvas 画布里
}

function screenshot() {
  const originalTransform = fabricCanvas.viewportTransform;
  const imageData = fabricCanvas.toDataURL({
    format: 'png',
  });

  fabricCanvas.viewportTransform = originalTransform;

  // 下载
  const a = document.createElement('a');
  a.href = imageData;
  a.download = `fabric-screenshot-${Date.now()}`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
}

function centerSelectedObject() {
  if (!selectedObject) {
    return false;
  }
  setCenterFromObject(selectedObject, fabricCanvas);
}
</script>
<style lang="less" scoped>
.canvas-box {
  margin-top: 10px;
}
.actions {
  margin-top: 10px;
  > button {
    padding: 0 10px;
    border: 1px solid #eee;
    margin-right: 10px;
  }
}
</style>
js
// import { fabric } from 'fabric-with-erasing'

/**
 * 设置画布中心到指定对象中心点上
 * @param {Object} obj 指定的对象
 */
export function setCenterFromObject(obj, canvas) {
  const objCenter = obj.getCenterPoint();
  const viewportTransform = canvas.viewportTransform;
  if (
    canvas.width === undefined ||
    canvas.height === undefined ||
    !viewportTransform
  )
    return;
  viewportTransform[4] = canvas.width / 2 - objCenter.x * viewportTransform[0];
  viewportTransform[5] = canvas.height / 2 - objCenter.y * viewportTransform[3];
  canvas.setViewportTransform(viewportTransform);
  canvas.renderAll();
}

/**
 * @method zoomToFitObjects
 * @description 缩放到显示所有对象
 * @param {Object} canvas
 */
export function zoomToFitObjects(canvas) {
  if (canvas.getObjects().length < 1) {
    return;
  }
  const x1 = Math.min(...canvas.getObjects().map((obj) => obj.left));
  const y1 = Math.min(...canvas.getObjects().map((obj) => obj.top));
  const x2 = Math.max(
    ...canvas.getObjects().map((obj) => obj.left + obj.getScaledWidth())
  );
  const y2 = Math.max(
    ...canvas.getObjects().map((obj) => obj.top + obj.getScaledHeight())
  );
  const height = y2 - y1;
  const width = x2 - x1;
  const zoom1 = canvas.getHeight() / height;
  const zoom2 = canvas.getWidth() / width;
  const zoom = zoom1 > zoom2 ? zoom2 : zoom1;
  const viewportTransform = [
    zoom,
    0,
    0,
    zoom,
    (0 - x1) * zoom,
    (0 - y1) * zoom,
  ];
  canvas.setViewportTransform(viewportTransform);
}

二、在画布中进行框选创建矩形

源代码
vue
<template>
  <fabric-canvas
    style="margin-top: 10px"
    @init="onFabricCanvasInit"
  ></fabric-canvas>
</template>
<script setup>
import FabricCanvas from '../canvas.vue';
import bgTransparent from '../../../images/bg-transparent.png';
import useCornerRotateControls from '../controls/rotate';
import useDelControl from '../controls/del';
import useConfirmControl from '../controls/confirm';
import useMouseCreateRect from '../handles/useMouseCreateRect';
import useResizeMask from '../handles/useResizeMask';
import { customDefaultContorls } from '../controls/default';

let fabricCanvas = null;
let fabric = null;

// 使用自定义控件
function useConstomContols(delCallback) {
  // 覆盖默认控件
  customDefaultContorls();
  // 新自定义旋转控件
  useCornerRotateControls(fabricCanvas);
  // 新增删除控件
  useDelControl(fabricCanvas, delCallback);
  fabricCanvas.requestRenderAll();
}

async function onFabricCanvasInit(canvas, fabricModule) {
  fabricCanvas = canvas;
  fabric = fabricModule;
  let onCreateRectTypeChange;
  // 设置背景
  canvas.setBackgroundColor(
    {
      source: bgTransparent,
      repeat: 'repeat',
    },
    canvas.renderAll.bind(canvas)
  );

  // 使用自定义控件
  useConstomContols(() => {
    // 被删除后可以再次生成
    setTimeout(() => {
      onCreateRectTypeChange && onCreateRectTypeChange('rect');
    }, 500);
  });

  // 即将使用自定义确认控件
  const { setControlToObject, removeControlFromObject } = useConfirmControl();

  // 可以使用鼠标框选生成矩形
  const { typeChange } = useMouseCreateRect(fabricCanvas, (rect) => {
    // 为元素设置遮罩层,遮罩层随元素变化
    useResizeMask(fabricCanvas, rect);
    // 添加确认控件设置
    setControlToObject(rect, {
      onCancel: () => {
        fabricCanvas.remove(rect);
        setTimeout(() => {
          // 被删除后可以再次生成
          typeChange('rect');
        }, 500);
      },
      onConfirm: () => {
        fabricCanvas.discardActiveObject();
      },
    });

    // 只能绘制一次,恢复为点选模式
    setTimeout(() => {
      // 被删除后可以再次生成
      typeChange('default');
    }, 500);
  });

  onCreateRectTypeChange = typeChange;

  fabricCanvas.renderAll();
}
</script>
js
import { fabric } from 'fabric';

export default function useConfirmControl() {
  // 删除按钮样式
  const deleteIcon = `data:image/svg+xml,${encodeURIComponent(
    '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692954890354" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13326" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M617.92 516.096l272 272-101.824 101.824-272-272-272 272-101.856-101.824 272-272-275.008-275.04L241.056 139.2l275.04 275.04 275.04-275.04 101.824 101.824-275.04 275.04z" fill="#F85F4A" p-id="13327"></path></svg>'
  )}`;
  const deleteImg = document.createElement('img');
  deleteImg.src = deleteIcon;

  // 确定按钮样式
  const collectIcon = `data:image/svg+xml,${encodeURIComponent(
    '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692954639646" class="icon" viewBox="0 0 1092 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13291" xmlns:xlink="http://www.w3.org/1999/xlink" width="213.28125" height="200"><path d="M445.781333 756.667733L204.8 515.003733l71.953067-72.021333L470.357333 637.1328 835.242667 273.066667l71.953066 71.8848-410.8288 409.873066a33.928533 33.928533 0 0 1-3.6864 3.208534 34.133333 34.133333 0 0 1-46.967466-1.365334z" fill="#00D775" p-id="13292"></path></svg>'
  )}`;
  const collectImg = document.createElement('img');
  collectImg.src = collectIcon;

  // 渲染图标
  function renderIcon(icon, size) {
    return function renderIcon(ctx, left, top, styleOverride, fabricObject) {
      ctx.save();
      ctx.translate(left, top);
      ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
      ctx.drawImage(icon, -size / 2, -size / 2, size, size);
      ctx.restore();
    };
  }
  // 圆角矩形
  function createRadiusRect(
    ctx,
    x,
    y,
    width,
    height,
    cornerSize,
    bgColor,
    borderColor
  ) {
    ctx.beginPath();
    ctx.moveTo(x + cornerSize, y);
    ctx.arcTo(x + width, y, x + width, y + cornerSize, cornerSize);
    ctx.lineTo(x + width, y + height - cornerSize);
    ctx.arcTo(
      x + width,
      y + height,
      x + width - cornerSize,
      y + height,
      cornerSize
    );
    ctx.lineTo(x + cornerSize, y + height);
    ctx.arcTo(x, y + height, x, y + height - cornerSize, cornerSize);
    ctx.lineTo(x, y + cornerSize);
    ctx.arcTo(x, y, x + cornerSize, y, cornerSize);
    ctx.closePath();

    ctx.fillStyle = bgColor;
    // ctx.strokeStyle = borderColor
    ctx.fill();
    // ctx.stroke()
  }

  // 根据矩形状态更新按钮区域的位置
  function checkEmptyPosition(object, emptyPositionChange) {
    const checkPosition = () => {
      const bottom = object.canvas.height - object.top - object.height;
      if (bottom > object.top) {
        emptyPositionChange('bottom');
      } else {
        emptyPositionChange('top');
      }
    };
    // 更新
    const updateEvents = [
      'scaled',
      'selected',
      'modified',
      'scaling',
      'moving',
      'rotating',
      'skewing',
    ];
    updateEvents.forEach((key) => {
      object.on(key, checkPosition);
    });
    // 移除
    // const removeEvents = ['removed', 'deselected']
    // removeEvents.forEach((key) => {
    //   object.on(key, removeMask)
    // })
  }

  // 为对象设置自定义控件
  function setControlToObject(object, options) {
    const bg = new fabric.Control({
      x: 0.5,
      y: -0.5,
      offsetY: -25,
      offsetX: -31,
      cursorStyle: 'default', // 鼠标移到控件时的指针样式
      render: function (ctx, left, top, styleOverride, fabricObject) {
        ctx.save();
        const width = 62;
        const height = 23;
        ctx.translate(left, top);
        ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
        createRadiusRect(
          ctx,
          -width / 2,
          -height / 2,
          width,
          height,
          3,
          'rgba(0,0,0,0.5)',
          'rgba(0,0,0,0.5)'
        );
        ctx.restore();
      },
      cornerSize: 24,
    });

    const btnCancel = new fabric.Control({
      x: 0.5,
      y: -0.5,
      offsetY: -25,
      offsetX: -46,
      cursorStyle: 'pointer',
      mouseUpHandler: options.onCancel,
      render: renderIcon(deleteImg, 16),
      cornerSize: 24,
    });

    const btnConfirm = new fabric.Control({
      x: 0.5,
      y: -0.5,
      offsetY: -25,
      offsetX: -16,
      cursorStyle: 'pointer',
      mouseUpHandler: options.onConfirm,
      render: renderIcon(collectImg, 24),
      cornerSize: 24,
    });

    if (object.controls.deleteControl) {
      object.controls.deleteControl.visible = false;
    }
    object.set({
      controls: {
        confirmBg: bg,
        confirmBtnCancel: btnCancel,
        confirmBtnConfirm: btnConfirm,
      },
    });
    // 自动更换控件的位置
    checkEmptyPosition(object, (position) => {
      if (position === 'bottom') {
        object.controls.confirmBg.y = 0.5;
        object.controls.confirmBg.offsetY = 25;

        object.controls.confirmBtnCancel.y = 0.5;
        object.controls.confirmBtnCancel.offsetY = 25;

        object.controls.confirmBtnConfirm.y = 0.5;
        object.controls.confirmBtnConfirm.offsetY = 25;
      } else {
        object.controls.confirmBg.y = -0.5;
        object.controls.confirmBg.offsetY = -25;

        object.controls.confirmBtnCancel.y = -0.5;
        object.controls.confirmBtnCancel.offsetY = -25;

        object.controls.confirmBtnConfirm.y = -0.5;
        object.controls.confirmBtnConfirm.offsetY = -25;
      }
    });
  }

  function removeControlFromObject(object) {
    delete object.controls.confirmBg;
    delete object.controls.confirmBtnCancel;
    delete object.controls.confirmBtnConfirm;
    if (object.controls.deleteControl) {
      object.controls.deleteControl.visible = true;
    }
  }

  return {
    setControlToObject,
    removeControlFromObject,
  };
}
js
import { fabric } from 'fabric';
import useClipMask from './useClipMask';
export function getRectByPoints(startPoint, endPoint) {
  // 矩形参数计算(前面总结的4条公式)
  const top = Math.min(startPoint.y, endPoint.y);
  const left = Math.min(startPoint.x, endPoint.x);
  const width = Math.abs(startPoint.x - endPoint.x);
  const height = Math.abs(startPoint.y - endPoint.y);
  return {
    top,
    left,
    width,
    height,
  };
}

// 创建矩形
export function createRect(position) {
  // 矩形对象
  return new fabric.Rect({
    ...position,
    fill: 'transparent', // 填充色:透明
    stroke: '#3E63F1', // 边框颜色:主题色
    shadow: '1px 1px 2px rgba(255, 255, 255, 0.5)', // 阴影
    inverted: true,
    absolutePositioned: true,
  });
}
export default function (canvas, onRectInsert) {
  // 当前操作模式(默认 || 创建矩形)
  let currentType = 'rect';

  // 画布操作类型切换
  function typeChange(opt) {
    currentType = opt;
    switch (opt) {
      case 'default': // 默认框选模式
        canvas.selection = true; // 允许框选
        canvas.selectionColor = 'rgba(100, 100, 255, 0.3)'; // 选框填充色:半透明的蓝色
        canvas.selectionBorderColor = 'rgba(255, 255, 255, 0.3)'; // 选框边框颜色:半透明灰色
        canvas.skipTargetFind = false; // 允许选中
        break;
      case 'rect': // 创建矩形模式
        canvas.selectionColor = 'transparent'; // 选框填充色:透明
        canvas.selectionBorderColor = 'rgba(0, 0, 0, 0.2)'; // 选框边框颜色:透明度很低的黑色(看上去是灰色)
        canvas.skipTargetFind = true; // 禁止选中
        break;
    }
  }
  typeChange('rect');

  // 起始和结束坐标
  let startPoint = null;
  let endPoint = null;
  let pressing = false;

  canvas.on('mouse:down', canvasMouseDown);
  canvas.on('mouse:move', canvasMouseMove);
  canvas.on('mouse:up', canvasMouseUp);

  // 鼠标在画布上按下
  function canvasMouseDown(e) {
    pressing = true;
    // 鼠标左键按下时,将当前坐标 赋值给 startPoint。{x: xxx, y: xxx} 的格式
    startPoint = e.absolutePointer;
  }

  // 鼠标在画布上移动
  let mask = null;
  function canvasMouseMove(e) {
    if (pressing && currentType === 'rect') {
      const currentPoint = e.absolutePointer;
      const position = getRectByPoints(startPoint, currentPoint);
      const rect = createRect(position);
      if (!mask) {
        mask = useClipMask(canvas);
      }
      mask.updateMask(rect);
    }
  }

  // 鼠标在画布上松开
  function canvasMouseUp(e) {
    pressing = false;
    // 清除绘制时的遮罩层
    if (mask) {
      mask.removeMask();
      mask = null;
    }
    // 绘制矩形的模式下,才执行下面的代码
    if (currentType === 'rect') {
      // 松开鼠标左键时,将当前坐标 赋值给 endPoint
      endPoint = e.absolutePointer;
      const canCreate =
        endPoint.x !== startPoint.x || endPoint.y !== startPoint.y;
      if (!canCreate) {
        return false;
      }
      // 如果点击和松开鼠标,都是在同一个坐标点,不会生成矩形
      if (JSON.stringify(startPoint) !== JSON.stringify(endPoint)) {
        // 调用 创建矩形 的方法
        const position = getRectByPoints(startPoint, endPoint);
        const rect = createRect(position);
        // 返回
        onRectInsert && onRectInsert(rect);

        // 将矩形添加到画布上
        canvas.add(rect);

        // 设置聚焦
        canvas.setActiveObject(rect);
      }
      // 清空绘制数据
      startPoint = null;
      endPoint = null;
    }
  }

  return {
    typeChange,
  };
}
js
import useClipMask from './useClipMask';
export default function (canvas, object) {
  // 自定义遮罩层,用于裁剪时显示黑色半透明背景
  const { updateMask, removeMask } = useClipMask(canvas);
  // 更新
  const updateEvents = [
    'scaled',
    'selected',
    'modified',
    'scaling',
    'moving',
    'rotating',
    'skewing',
  ];
  updateEvents.forEach((key) => {
    object.on(key, () => {
      updateMask(object);
    });
  });
  // 移除
  const removeEvents = ['removed', 'deselected'];
  removeEvents.forEach((key) => {
    object.on(key, removeMask);
  });
}

三、实现文本的垂直居中

源代码
vue
<template>
  <fabric-canvas @init="onFabricCanvasInit"></fabric-canvas>
  <div class="actions">
    <button type="button" @click="verticalAlign('top')">垂直-顶部</button>
    <button type="button" @click="verticalAlign('middle')">垂直-居中</button>
    <button type="button" @click="verticalAlign('bottom')">垂直-底部</button>
    <button type="button" @click="textAlign('left')">水平-左</button>
    <button type="button" @click="textAlign('center')">水平-居中</button>
    <button type="button" @click="textAlign('right')">水平-右</button>
  </div>
</template>
<script setup>
import bgTransparent from '../../../images/bg-transparent.png';
import FabricCanvas from '../canvas.vue';

let fabricCanvas = null;
let fabric = null;

async function onFabricCanvasInit(canvas, fabricModule) {
  fabricCanvas = canvas;
  fabric = fabricModule;
  // 设置背景
  canvas.setBackgroundColor(
    {
      source: bgTransparent,
      repeat: 'repeat',
    },
    canvas.renderAll.bind(canvas)
  );

  const width = 150;
  const height = 100;

  // 文本
  const textBox = new fabric.Text('hello world', {
    fontSize: 14,
    splitByGrapheme: true,
  });
  // 容器
  const rect = new fabric.Rect({
    width: width,
    height: height,
    fill: 'transparent',
  });
  // 建组
  const group = new fabric.Group([rect, textBox], {
    top: 100,
    left: 100,
    width: width,
    height: height,
  });
  textBox.set('width', width);
  fabricCanvas.add(group);
}

const groupKeys = [
  'instance',
  'top',
  'left',
  'width',
  'height',
  'scaleX',
  'scaleY',
];
// 更新属性
function updateObjectProperty(config, object) {
  if (object) {
    Object.keys(config).forEach((key) => {
      const value = config[key];
      object.set(key, value);
    });
  }
}
function updateTextProperty(config, object) {
  // 组内的容器和文本
  const rectObject = object.item(0);
  const textObject = object.item(1);

  // 应始终具有宽高
  if (!config.width) {
    config.width = object.get('scaleX') * object.get('width');
  }
  if (!config.height) {
    config.height = object.get('scaleY') * object.get('height');
  }

  // 取组的属性进行设置
  const groupConfig = {};
  groupKeys.forEach((key) => {
    if (typeof config[key] !== 'undefined') {
      groupConfig[key] = config[key];
    }
  });

  // 取文本的属性进行设置
  const textConfig = {};
  for (const key in config) {
    if (!groupConfig[key]) {
      textConfig[key] = config[key];
    }
  }

  // 特殊处理属性:
  if (typeof config.visible !== 'undefined') {
    groupConfig.visible = config.visible;
    textConfig.visible = config.visible;
  }

  updateObjectProperty(groupConfig, rectObject);
  updateObjectProperty(textConfig, textObject);

  // 更新文本在组中的位置
  const updateTextVerticalAlign = (type) => {
    if (type === 'top') {
      updateObjectProperty(
        {
          top: -object.height / 2,
        },
        textObject
      );
    }
    if (type === 'middle') {
      updateObjectProperty(
        {
          top: -textObject.height / 2,
        },
        textObject
      );
    }
    if (type === 'bottom') {
      updateObjectProperty(
        {
          top: object.height / 2 - textObject.height,
        },
        textObject
      );
    }
  };

  // 根据组的大小,更新组内矩形、文本大小位置
  if (typeof groupConfig.width !== 'undefined') {
    updateObjectProperty(
      {
        scaleX: 1,
        width: object.width,
        left: -object.width / 2,
      },
      rectObject
    );

    updateObjectProperty(
      {
        scaleX: 1,
        width: object.width,
        left: -object.width / 2,
      },
      textObject
    );
  }

  if (typeof groupConfig.height !== 'undefined') {
    updateObjectProperty(
      {
        scaleY: 1,
        height: object.height,
        top: -object.height / 2,
      },
      rectObject
    );

    updateTextVerticalAlign(textObject.verticalAlign);
  }
}
function verticalAlign(type) {
  const activeObjects = fabricCanvas.getActiveObjects();
  const group = activeObjects[0];
  if (!group) {
    return false;
  }
  updateTextProperty(
    {
      verticalAlign: type,
    },
    group
  );
  fabricCanvas.renderAll();
}
function textAlign(type) {
  const activeObjects = fabricCanvas.getActiveObjects();
  const group = activeObjects[0];
  if (!group) {
    return false;
  }
  updateTextProperty(
    {
      textAlign: type,
    },
    group
  );
  fabricCanvas.renderAll();
}
</script>
<style lang="less" scoped>
.canvas-box {
  margin-top: 10px;
}
.actions {
  margin-top: 10px;
  > button {
    padding: 0 10px;
    border: 1px solid #eee;
    margin-right: 10px;
  }
}
</style>