实现签字功能

封装组件,实现签字画板功能

<template>
  <div class="home">
    <div class="btnwrap">
      <div
        @click="showStrokeColorPicker = !showStrokeColorPicker"
        class="btn-color"
        ref="strokeColor"
      >
        <Sketch-picker
          v-if="showStrokeColorPicker"
          class="color-picker"
          :value="strokeColor"
          @input="updateStrokeColor"
        />
      </div>
      <span class="color-label" :style="{ color: strokeColor }">线条颜色</span>

      <!-- <div
        @click="showFillColorPicker = !showFillColorPicker"
        class="btn-color"
        ref="fillColor"
      >
        <Sketch-picker
          v-if="showFillColorPicker"
          class="color-picker"
          :value="fillColor"
          @input="updateFillColor"
        />
      </div>
      <span class="color-label" :style="{ color: strokeColor }">填充颜色</span> -->

      <div
        v-show="true"
        @click="showBgColorPicker = !showBgColorPicker"
        class="btn-color"
        ref="bgColor"
      >
        <Sketch-picker
          v-if="showBgColorPicker"
          class="color-picker"
          :value="bgColor"
          @input="updateBgColor"
        />
      </div>
      <span class="color-label" :style="{ color: strokeColor }">画布颜色</span>

      <div class="brushWidth">
        <label :style="{ color: strokeColor }">线条大小:{{ lineSize }}</label>
        <input type="range" name="vol" min="1" max="100" v-model="lineSize" />
      </div>
      <!-- <div class="brushWidth">
        <label :style="{ color: strokeColor }">文字大小:{{ fontSize }}</label>
        <input type="range" name="vol" min="18" max="50" v-model="fontSize" />
      </div> -->
      <div class="btnList">
        <div
          @click="tapToolBtn('brush')"
          :class="{ active: selectTool === 'brush' }"
          class="btn-tool"
        >
          <i class="iconfont icon-noun__cc"></i>
        </div>
        <!-- <div
          @click="tapToolBtn('line')"
          :class="{ active: selectTool === 'line' }"
          class="btn-tool"
        >
          <i class="iconfont icon-jurassic_line"></i>
        </div> -->
        <!-- <div
          @click="tapToolBtn('rect')"
          :class="{ active: selectTool === 'rect' }"
          class="btn-tool"
        >
          <i class="iconfont icon-juxing"></i>
        </div>
        <div
          @click="tapToolBtn('circle')"
          :class="{ active: selectTool === 'circle' }"
          class="btn-tool"
        >
          <i class="iconfont icon-yuanxingweixuanzhong"></i>
        </div> -->
        <!-- <div
          @click="tapToolBtn('text')"
          :class="{ active: selectTool === 'text' }"
          class="btn-tool"
        >
          <i class="iconfont icon-xingzhuang-wenzi"></i>
        </div> -->
        <div
          @click="tapToolBtn('eraser')"
          :class="{ active: selectTool === 'eraser' }"
          class="btn-tool"
        >
          <i class="iconfont icon-xiangpi"></i>
        </div>
        <!-- <div
          @click="tapToolBtn('move')"
          :class="{ active: selectTool === 'move' }"
          class="btn-tool"
        >
          <i class="iconfont icon-24gl-move"></i>
        </div> -->
        <!-- <div
          @click="tapToolBtn('select')"
          :class="{ active: selectTool === 'select' }"
          class="btn-tool"
        >
          <i class="iconfont icon-xuanzhong"></i>
        </div> -->
        <!-- <div @click="tapScaleBtn(-1)" class="btn-tool">
          <i class="iconfont icon-suoxiao"></i>
        </div>
        <div @click="tapScaleBtn(1)" class="btn-tool">
          <i class="iconfont icon-fangda"></i>
        </div> -->
        <!-- <div @click="tapHistoryBtn(-1)" class="btn-tool">
          <i class="iconfont icon-fanhuishangyibu-"></i>
        </div>
        <div @click="tapHistoryBtn(1)" class="btn-tool">
          <i class="iconfont icon-fanhuixiayibu-"></i>
        </div> -->
        <div @click="tapClearBtn()" class="btn-tool">
          <i class="iconfont icon-qingkong"></i>
        </div>
        <div @click="tapSaveBtn()" class="btn-tool">
          <i class="iconfont icon-baocun_o"></i>
        </div>
        <!-- <div @click="tapDownBtn()" class="btn-tool">
          <i class="iconfont icon-xiazai"></i>
        </div> height:260px;-->
      </div>
    </div>
    <canvas class="canvas" ref="canvas" :style="{ height: height }"></canvas>
  </div>
</template>

<script>
import { fabric } from "fabric";
import "./eraser_brush.mixin.js";
import { Sketch } from "vue-color";
// import { mapGetters } from "vuex";

export default {
  components: {
    "Sketch-picker": Sketch,
  },
  computed: {
    // ...mapGetters(["hammerType"])
  },
  prop: {
    height: {
      type: String,
      required: false,
      default: "260px",
    },
  },
  data() {
    return {
      canvas: null, // fabric canvas对象
      strokeColor: "#000000", // 线框色
      showStrokeColorPicker: false, // 是否显示 线框色选择器
      fillColor: "rgba(0,0,0,0)", // 填充色
      showFillColorPicker: false, // 是否显示 填充色选择器
      bgColor: "#ffffff", // 背景色
      showBgColorPicker: false, // 是否显示 背景色选择器
      lineSize: 10, // 线条大小 (线条 and 线框)
      fontSize: 18, // 字体大小
      selectTool: "", // 当前用户选择的绘图工具 画笔:brush 直线:line 矩形:rect 圆形 circle 文本 text
      mouseFrom: {}, // 鼠标绘制起点
      mouseTo: {}, // 鼠标绘制重点
      drawingObject: null, // 保存鼠标未松开时用户绘制的临时图像
      textObject: null, // 保存用户创建的文本对象
      isDrawing: false, // 当前是否正在绘制图形(画笔,文本模式除外)
      stateArr: [], // 保存画布的操作记录
      stateIdx: 0, // 当前操作步数
      isRedoing: false, // 当前是否在执行撤销或重做操作
    };
  },
  watch: {
    // hammerType: {
    //   handler(val, oldVal) {
    //     if (val === "top") {
    //       this.initCanvas();
    //       // 初始化 画布
    //       // 默认开启画笔模式
    //       this.tapToolBtn("brush");
    //       // 初始化 画布 事件
    //       this.initCanvasEvent();
    //     }
    //   }
    // },
    // 监听线条大小变化
    lineSize() {
      this.canvas.freeDrawingBrush.width = parseInt(this.lineSize, 10);
      this.lineSize = parseInt(this.lineSize, 10);
    },
    // 监听背景色变化
    bgColor() {
      this.canvas.setBackgroundColor(this.bgColor, undefined, {
        erasable: false,
      });
      this.canvas.renderAll();
    },
  },
  mounted() {
    this.initCanvas();
    // 初始化 画布
    // 默认开启画笔模式
    this.tapToolBtn("brush");
    // 初始化 画布 事件
    this.initCanvasEvent();
  },
  methods: {
    // 监听线框色选择器 颜色选择
    updateStrokeColor(val) {
      // 保存用户选择的线框色
      this.strokeColor = val.hex;
      // 修改当前选择的颜色指示
      this.$refs.strokeColor.style.backgroundColor = this.strokeColor;
      this.tapToolBtn();
      this.tapToolBtn("brush");
    },
    // 监听填充色选择器 颜色选择
    updateFillColor(val) {
      // 保存用户选择的线框色
      // this.fillColor = val.hex;
      // 修改当前选择的颜色指示
      // this.$refs.fillColor.style.backgroundColor = this.fillColor;
    },
    // 监听背景色选择器 颜色选择
    updateBgColor(val) {
      // 保存用户选择的背景色
      this.bgColor = val.hex;
      this.$refs.bgColor.style.backgroundColor = this.bgColor;
    },
    // 初始化画布
    initCanvas() {
      this.$refs.canvas.width = this.$refs.canvas.offsetWidth;
      this.$refs.canvas.height = this.$refs.canvas.offsetHeight;
      // 初始化线框色 与 指示器
      this.$refs.strokeColor.style.backgroundColor = this.strokeColor;
      // 初始化填充色 与 指示器
      // this.$refs.fillColor.style.backgroundColor = this.fillColor;
      // 初始化背景色 与 指示器
      this.$refs.bgColor.style.backgroundColor = this.bgColor;
      // 初始化 fabric canvas对象
      if (!this.canvas) {
        this.canvas = new fabric.Canvas(this.$refs.canvas, {});
        // 设置画布背景色 (背景色需要这样设置,否则拓展的橡皮功能会报错)
        this.canvas.setBackgroundColor(this.bgColor, undefined, {
          erasable: false,
        });
        // 设置背景色不受缩放与平移的影响
        this.canvas.set("backgroundVpt", false);
        // 禁止用户进行组选择
        this.canvas.selection = false;
        this.canvas.isDrawingMode = true;
        // 设置当前鼠标停留在
        this.canvas.hoverCursor = "default";
        // 重新渲染画布
        this.canvas.renderAll();
        // 记录画布原始状态
        this.stateArr.push(JSON.stringify(this.canvas));
        this.stateIdx = 0;
      }
    },
    // 初始化画布事件
    initCanvasEvent() {
      // 操作类型集合
      const toolTypes = ["line", "rect", "circle", "text", "move"];
      // 监听鼠标按下事件
      this.canvas.on("mouse:down", (options) => {
        this.showBgColorPicker = false;
        this.showStrokeColorPicker = false;
        if (this.selectTool !== "text" && this.textObject) {
          // 如果当前存在文本对象,并且不是进行添加文字操作 则 退出编辑模式,并删除临时的文本对象
          // 将当前文本对象退出编辑模式
          this.textObject.exitEditing();
          this.textObject.set("backgroundColor", "rgba(0,0,0,0)");
          if (this.textObject.text === "") {
            this.canvas.remove(this.textObject);
          }
          this.canvas.renderAll();
          this.textObject = null;
        }
        // 判断当前是否选择了集合中的操作
        if (toolTypes.indexOf(this.selectTool) !== -1) {
          // 记录当前鼠标的起点坐标 (减去画布在 x y轴的偏移,因为画布左上角坐标不一定在浏览器的窗口左上角)
          this.mouseFrom.x = options.e.clientX - this.canvas._offset.left;
          this.mouseFrom.y = options.e.clientY - this.canvas._offset.top;
          // 判断当前选择的工具是否为文本
          if (this.selectTool === "text") {
            // 文本工具初始化
            this.initText();
          } else {
            // 设置当前正在进行绘图 或 移动操作
            this.isDrawing = true;
          }
        }
      });
      // 监听鼠标移动事件
      this.canvas.on("mouse:move", (options) => {
        // 如果当前正在进行绘图或移动相关操作
        if (this.isDrawing) {
          // 记录当前鼠标移动终点坐标 (减去画布在 x y轴的偏移,因为画布左上角坐标不一定在浏览器的窗口左上角)
          this.mouseTo.x = options.e.clientX - this.canvas._offset.left;
          this.mouseTo.y = options.e.clientY - this.canvas._offset.top;
          switch (this.selectTool) {
            case "line":
              // 当前绘制直线,初始化直线绘制
              this.initLine();
              break;
            case "rect":
              // 初始化 矩形绘制
              this.initRect();
              break;
            case "circle":
              // 初始化 绘制圆形
              this.initCircle();
              break;
            case "move":
              // 初始化画布移动
              this.initMove();
          }
        }
      });
      // 监听鼠标松开事件
      this.canvas.on("mouse:up", () => {
        // 如果当前正在进行绘图或移动相关操作
        if (this.isDrawing) {
          // 清空鼠标移动时保存的临时绘图对象
          this.drawingObject = null;
          // 重置正在绘制图形标志
          this.isDrawing = false;
          // 清空鼠标保存记录
          this.resetMove();
          // 如果当前进行的是移动操作,鼠标松开重置当前视口缩放系数
          if (this.selectTool === "move") {
            this.canvas.setViewportTransform(this.canvas.viewportTransform);
          }
        }
      });
      // 监听画布渲染完成
      this.canvas.on("after:render", () => {
        if (!this.isRedoing) {
          // 当前不是进行撤销或重做操作
          // 在绘画时会频繁触发该回调,所以间隔1s记录当前状态
          if (this.recordTimer) {
            clearTimeout(this.recordTimer);
            this.recordTimer = null;
          }
          this.recordTimer = setTimeout(() => {
            this.stateArr.push(JSON.stringify(this.canvas));
            this.stateIdx++;
          }, 100);
        } else {
          // 当前正在执行撤销或重做操作,不记录重新绘制的画布
          this.isRedoing = false;
        }
      });
    },
    // 初始化画笔工具
    initBruch() {
      // 设置绘画模式画笔类型为 铅笔类型
      this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
      // 设置画布模式为绘画模式
      this.canvas.isDrawingMode = true;
      // 设置绘画模式 画笔颜色与画笔线条大小
      this.canvas.freeDrawingBrush.color = this.strokeColor;
      this.canvas.freeDrawingBrush.width = parseInt(this.lineSize, 10);
    },
    // 初始化 绘制直线
    initLine() {
      // 根据保存的鼠标起始点坐标 创建直线对象
      const canvasObject = new fabric.Line(
        [
          this.getTransformedPosX(this.mouseFrom.x),
          this.getTransformedPosY(this.mouseFrom.y),
          this.getTransformedPosX(this.mouseTo.x),
          this.getTransformedPosY(this.mouseTo.y),
        ],
        {
          fill: this.fillColor,
          stroke: this.strokeColor,
          strokeWidth: this.lineSize,
        }
      );
      // 绘制 图形对象
      this.startDrawingObject(canvasObject);
    },
    // 初始化 绘制矩形
    initRect() {
      // 计算矩形长宽
      const left = this.getTransformedPosX(this.mouseFrom.x);
      const top = this.getTransformedPosY(this.mouseFrom.y);
      const width = this.mouseTo.x - this.mouseFrom.x;
      const height = this.mouseTo.y - this.mouseFrom.y;
      // 创建矩形 对象
      const canvasObject = new fabric.Rect({
        left: left,
        top: top,
        width: width,
        height: height,
        stroke: this.strokeColor,
        fill: this.fillColor,
        strokeWidth: this.lineSize,
      });
      // 绘制矩形
      this.startDrawingObject(canvasObject);
    },
    // 初始化绘制圆形
    initCircle() {
      const left = this.getTransformedPosX(this.mouseFrom.x);
      const top = this.getTransformedPosY(this.mouseFrom.y);
      // 计算圆形半径
      const radius =
        Math.sqrt(
          (this.getTransformedPosX(this.mouseTo.x) - left) *
            (this.getTransformedPosY(this.mouseTo.x) - left) +
            (this.getTransformedPosX(this.mouseTo.y) - top) *
              (this.getTransformedPosY(this.mouseTo.y) - top)
        ) / 2;
      // 创建 原型对象
      const canvasObject = new fabric.Circle({
        left: left,
        top: top,
        stroke: this.strokeColor,
        fill: this.fillColor,
        radius: radius,
        strokeWidth: this.lineSize,
      });
      // 绘制圆形对象
      this.startDrawingObject(canvasObject);
    },
    // 初始化文本工具
    initText() {
      if (!this.textObject) {
        // 当前不存在绘制中的文本对象
        // 创建文本对象
        this.textObject = new fabric.Textbox("", {
          left: this.getTransformedPosX(this.mouseFrom.x),
          top: this.getTransformedPosY(this.mouseFrom.y),
          fontSize: this.fontSize,
          fill: this.strokeColor,
          hasControls: false,
          editable: true,
          width: 30,
          backgroundColor: "#fff",
          selectable: false,
        });
        this.canvas.add(this.textObject);
        // 文本打开编辑模式
        this.textObject.enterEditing();
        // 文本编辑框获取焦点
        this.textObject.hiddenTextarea.focus();
      } else {
        // 将当前文本对象退出编辑模式
        this.textObject.exitEditing();
        this.textObject.set("backgroundColor", "rgba(0,0,0,0)");
        if (this.textObject.text === "") {
          this.canvas.remove(this.textObject);
        }
        this.canvas.renderAll();
        this.textObject = null;
      }
    },
    // 初始化橡皮擦功能
    initEraser() {
      this.canvas.freeDrawingBrush = new fabric.EraserBrush(this.canvas);
      this.canvas.freeDrawingBrush.width = parseInt(this.lineSize, 10);
      this.canvas.isDrawingMode = true;
    },
    // 初始化画布移动
    initMove() {
      var vpt = this.canvas.viewportTransform;
      vpt[4] += this.mouseTo.x - this.mouseFrom.x;
      vpt[5] += this.mouseTo.y - this.mouseFrom.y;
      this.canvas.requestRenderAll();
      this.mouseFrom.x = this.mouseTo.x;
      this.mouseFrom.y = this.mouseTo.y;
    },
    // 绘制图形
    startDrawingObject(canvasObject) {
      // 禁止用户选择当前正在绘制的图形
      canvasObject.selectable = false;
      // 如果当前图形已绘制,清除上一次绘制的图形
      if (this.drawingObject) {
        this.canvas.remove(this.drawingObject);
      }
      // 将绘制对象添加到 canvas中
      this.canvas.add(canvasObject);
      // 保存当前绘制的图形
      this.drawingObject = canvasObject;
    },
    // 清空鼠标移动记录 (起点 与 终点)
    resetMove() {
      this.mouseFrom = {};
      this.mouseTo = {};
    },
    // 绘图工具点击选择
    tapToolBtn(tool) {
      if (this.selectTool === tool) return;
      // 保存当前选中的绘图工具
      this.selectTool = tool;
      // 选择任何工具前进行一些重置工作
      // 禁用画笔模式
      this.canvas.isDrawingMode = false;
      this.canvas.selection = false;
      // 禁止图形选择编辑
      const drawObjects = this.canvas.getObjects();
      if (drawObjects.length > 0) {
        drawObjects.map((item) => {
          item.set("selectable", false);
        });
      }
      if (this.selectTool === "brush") {
        // 如果用户选择的是画笔工具,直接初始化,无需等待用户进行鼠标操作
        this.initBruch();
      } else if (this.selectTool === "eraser") {
        // 如果用户选择的是橡皮擦工具,直接初始化,无需等待用户进行鼠标操作
        this.initEraser();
      } else if (this.selectTool === "select") {
        this.canvas.selection = true;
        this.canvas.isDrawingMode = false;
        if (drawObjects.length > 0) {
          drawObjects.map((item) => {
            item.set("selectable", true);
          });
        }
      }
    },
    // 缩放按钮点击
    tapScaleBtn(flag) {
      // flag -1 缩小 1 放大
      let zoom = this.canvas.getZoom();
      if (flag > 0) {
        // 放大
        zoom *= 1.1;
      } else {
        // 缩小
        zoom *= 0.9;
      }
      // zoom 不能大于 20 不能小于0.01
      zoom = zoom > 20 ? 20 : zoom;
      zoom = zoom < 0.01 ? 0.01 : zoom;
      this.canvas.setZoom(zoom);
    },
    // 撤销重做按钮点击
    tapHistoryBtn(flag) {
      this.isRedoing = true;
      const stateIdx = this.stateIdx + flag;
      // 判断是否已经到了第一步操作
      if (stateIdx < 0) return;
      // 判断是否已经到了最后一步操作
      if (stateIdx >= this.stateArr.length) return;
      if (this.stateArr[stateIdx]) {
        this.canvas.loadFromJSON(this.stateArr[stateIdx]);
        if (this.canvas.getObjects().length > 0) {
          this.canvas.getObjects().forEach((item) => {
            item.set("selectable", false);
          });
        }
        this.stateIdx = stateIdx;
      }
    },
    // 监听画布重新绘制
    tapClearBtn() {
      this.$confirm("此操作将清空画布, 是否继续?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          const children = this.canvas.getObjects();
          if (children.length > 0) {
            this.canvas.remove(...children);
          }
        })
        .catch(() => {});
    },
    tapClearFun() {
      const children = this.canvas.getObjects();
      if (children.length > 0) {
        this.canvas.remove(...children);
      }
    },
    // 保存按钮点击
    tapSaveBtn() {
      this.canvas.clone((cvs) => {
        //遍历所有对对象,获取最小坐标,最大坐标
        let top = 0;
        let left = 0;
        let width = this.canvas.width;
        let height = this.canvas.height;
        var objects = cvs.getObjects();
        if (objects.length > 0) {
          var rect = objects[0].getBoundingRect();
          var minX = rect.left;
          var minY = rect.top;
          var maxX = rect.left + rect.width;
          var maxY = rect.top + rect.height;
          for (var i = 1; i < objects.length; i++) {
            rect = objects[i].getBoundingRect();
            minX = Math.min(minX, rect.left);
            minY = Math.min(minY, rect.top);
            maxX = Math.max(maxX, rect.left + rect.width);
            maxY = Math.max(maxY, rect.top + rect.height);
          }
          top = minY - 100;
          left = minX - 100;
          width = maxX - minX + 200;
          height = maxY - minY + 200;
          cvs.sendToBack(
            new fabric.Rect({
              left,
              top,
              width,
              height,
              stroke: "rgba(0,0,0,0)",
              fill: this.bgColor,
              strokeWidth: 0,
            })
          );
        }
        const dataURL = cvs.toDataURL({
          format: "png",
          multiplier: cvs.getZoom(),
          left,
          top,
          width,
          height,
        });
        // var file = this.dataURLtoFile(dataURL, "index.png");
        this.$emit("sendImg", dataURL);
        // this.tapClearBtn();
        const children = this.canvas.getObjects();
        if (children.length > 0) {
          this.canvas.remove(...children);
        }
      });
    },
    // 下载按钮点击
    tapDownBtn() {
      this.canvas.clone((cvs) => {
        //遍历所有对对象,获取最小坐标,最大坐标
        let top = 0;
        let left = 0;
        let width = this.canvas.width;
        let height = this.canvas.height;
        var objects = cvs.getObjects();
        if (objects.length > 0) {
          var rect = objects[0].getBoundingRect();
          var minX = rect.left;
          var minY = rect.top;
          var maxX = rect.left + rect.width;
          var maxY = rect.top + rect.height;
          for (var i = 1; i < objects.length; i++) {
            rect = objects[i].getBoundingRect();
            minX = Math.min(minX, rect.left);
            minY = Math.min(minY, rect.top);
            maxX = Math.max(maxX, rect.left + rect.width);
            maxY = Math.max(maxY, rect.top + rect.height);
          }
          top = minY - 100;
          left = minX - 100;
          width = maxX - minX + 200;
          height = maxY - minY + 200;
          cvs.sendToBack(
            new fabric.Rect({
              left,
              top,
              width,
              height,
              stroke: "rgba(0,0,0,0)",
              fill: this.bgColor,
              strokeWidth: 0,
            })
          );
        }
        const dataURL = cvs.toDataURL({
          format: "png",
          multiplier: cvs.getZoom(),
          left,
          top,
          width,
          height,
        });
        const link = document.createElement("a");
        link.download = "canvas.png";
        link.href = dataURL;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      });
    },
    dataURLtoFile(dataurl, filename) {
      // 将base64转换为文件
      var arr = dataurl.split(",");
      var mime = arr[0].match(/:(.*?);/)[1];
      var bstr = atob(arr[1]);
      var n = bstr.length;
      var u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new File([u8arr], filename, {
        type: mime,
      });
    },
    // 计算画布移动之后的x坐标点
    getTransformedPosX(x) {
      const zoom = Number(this.canvas.getZoom());
      return (x - this.canvas.viewportTransform[4]) / zoom;
    },
    getTransformedPosY(y) {
      const zoom = Number(this.canvas.getZoom());
      return (y - this.canvas.viewportTransform[5]) / zoom;
    },
  },
};
</script>
<style lang="scss" scoped>
.home {
  overflow: hidden;
  height: 100%;
  width: 100%;
  position: relative;
  .btnwrap {
    position: absolute;
    bottom: 80px;
    z-index: 40;
    width: 100%;
    height: 50px;
    display: flex;
    align-items: center;
    justify-content: center;
    .btnList {
      position: absolute;
      width: 100%;
      display: flex;
      justify-content: center;
      bottom: -60px;
    }
    .btn-color {
      width: 40px;
      height: 40px;
      position: relative;
      border: 1px solid #999;
      margin-left: 20px;
      .color-picker {
        position: absolute;
        left: 0;
        bottom: 40px;
        z-index: 1000;
      }
    }
    .color-label {
      padding-left: 4px;
    }
    .brushWidth {
      margin-left: 30px;
      display: flex;
      label {
        display: block;
        width: 100px;
      }
    }
    .btn-tool {
      margin: 10px 20px 0;
      padding-bottom: 10px;
      color: #a6a6a7;
      i {
        font-size: 30px;
      }
      &:hover {
        cursor: pointer;
        color: #333;
      }
      &.active {
        color: #333;
        border-bottom: 2px solid #3291ff;
      }
    }
  }
  .canvas {
    height: 100%;
    width: 100%;
    border: 1px solid #d3d3d3;
  }
}
</style>

 JS部分

/* eslint-disable */
(function() {
  /** ERASER_START */
  var __setBgOverlayColor = fabric.StaticCanvas.prototype.__setBgOverlayColor;
  var ___setBgOverlay = fabric.StaticCanvas.prototype.__setBgOverlay;
  var __setSVGBgOverlayColor =
    fabric.StaticCanvas.prototype._setSVGBgOverlayColor;
  fabric.util.object.extend(fabric.StaticCanvas.prototype, {
    backgroundColor: undefined,
    overlayColor: undefined,
    /**
     * Create Rect that holds the color to support erasing
     * patches {@link CommonMethods#_initGradient}
     * @private
     * @param {'bakground'|'overlay'} property
     * @param {(String|fabric.Pattern|fabric.Rect)} color Color or pattern or rect (in case of erasing)
     * @param {Function} callback Callback to invoke when color is set
     * @param {Object} options
     * @return {fabric.Canvas} instance
     * @chainable true
     */
    __setBgOverlayColor: function(property, color, callback, options) {
      if (color && color.isType && color.isType("rect")) {
        // color is already an object
        this[property] = color;
        color.set(options);
        callback && callback(this[property]);
      } else {
        var _this = this;
        var cb = function() {
          _this[property] = new fabric.Rect(
            fabric.util.object.extend(
              {
                width: _this.width,
                height: _this.height,
                fill: _this[property]
              },
              options
            )
          );
          callback && callback(_this[property]);
        };
        __setBgOverlayColor.call(this, property, color, cb);
        //  invoke cb in case of gradient
        //  see {@link CommonMethods#_initGradient}
        if (color && color.colorStops && !(color instanceof fabric.Gradient)) {
          cb();
        }
      }

      return this;
    },

    setBackgroundColor: function(backgroundColor, callback, options) {
      return this.__setBgOverlayColor(
        "backgroundColor",
        backgroundColor,
        callback,
        options
      );
    },

    setOverlayColor: function(overlayColor, callback, options) {
      return this.__setBgOverlayColor(
        "overlayColor",
        overlayColor,
        callback,
        options
      );
    },

    /**
     * patch serialization - from json
     * background/overlay properties could be objects if parsed by this mixin or could be legacy values
     * @private
     * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor)
     * @param {(Object|String)} value Value to set
     * @param {Object} loaded Set loaded property to true if property is set
     * @param {Object} callback Callback function to invoke after property is set
     */
    __setBgOverlay: function(property, value, loaded, callback) {
      var _this = this;

      if (
        (property === "backgroundColor" || property === "overlayColor") &&
        value &&
        typeof value === "object" &&
        value.type === "rect"
      ) {
        fabric.util.enlivenObjects([value], function(enlivedObject) {
          _this[property] = enlivedObject[0];
          loaded[property] = true;
          callback && callback();
        });
      } else {
        ___setBgOverlay.call(this, property, value, loaded, callback);
      }
    },

    /**
     * patch serialization - to svg
     * background/overlay properties could be objects if parsed by this mixin or could be legacy values
     * @private
     */
    _setSVGBgOverlayColor: function(markup, property, reviver) {
      var filler = this[property + "Color"];
      if (filler && filler.isType && filler.isType("rect")) {
        var excludeFromExport =
          filler.excludeFromExport ||
          (this[property] && this[property].excludeFromExport);
        if (filler && !excludeFromExport && filler.toSVG) {
          markup.push(filler.toSVG(reviver));
        }
      } else {
        __setSVGBgOverlayColor.call(this, markup, property, reviver);
      }
    },

    /**
     * @private
     * @param {CanvasRenderingContext2D} ctx Context to render on
     * @param {string} property 'background' or 'overlay'
     */
    _renderBackgroundOrOverlay: function(ctx, property) {
      var fill = this[property + "Color"],
        object = this[property + "Image"],
        v = this.viewportTransform,
        needsVpt = this[property + "Vpt"];
      if (!fill && !object) {
        return;
      }
      if (fill || object) {
        ctx.save();
        if (needsVpt) {
          ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
        }
        fill && fill.render(ctx);
        object && object.render(ctx);
        ctx.restore();
      }
    }
  });

  var _toObject = fabric.Object.prototype.toObject;
  var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup;
  fabric.util.object.extend(fabric.Object.prototype, {
    /**
     * Indicates whether this object can be erased by {@link fabric.EraserBrush}
     * @type boolean
     * @default true
     */
    erasable: true,

    /**
     *
     * @returns {fabric.Group | null}
     */
    getEraser: function() {
      return this.clipPath && this.clipPath.eraser ? this.clipPath : null;
    },

    /**
     * Returns an object representation of an instance
     * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
     * @return {Object} Object representation of an instance
     */
    toObject: function(additionalProperties) {
      return _toObject.call(this, ["erasable"].concat(additionalProperties));
    },

    /**
     * use <mask> to achieve erasing for svg
     * credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649
     * @param {Function} reviver
     * @returns {string} markup
     */
    eraserToSVG: function(options) {
      var eraser = this.getEraser();
      if (eraser) {
        var fill = eraser._objects[0].fill;
        eraser._objects[0].fill = "white";
        eraser.clipPathId = "CLIPPATH_" + fabric.Object.__uid++;
        var commons = [
          'id="' + eraser.clipPathId + '"'
          /*options.additionalTransform ? ' transform="' + options.additionalTransform + '" ' : ''*/
        ].join(" ");
        var objectMarkup = [
          "<defs>",
          "<mask " + commons + " >",
          eraser.toSVG(options.reviver),
          "</mask>",
          "</defs>"
        ];
        eraser._objects[0].fill = fill;
        return objectMarkup.join("\n");
      }
      return "";
    },

    /**
     * use <mask> to achieve erasing for svg, override <clipPath>
     * @param {string[]} objectMarkup
     * @param {Object} options
     * @returns
     */
    _createBaseSVGMarkup: function(objectMarkup, options) {
      var eraser = this.getEraser();
      if (eraser) {
        var eraserMarkup = this.eraserToSVG(options);
        this.clipPath = null;
        var markup = __createBaseSVGMarkup.call(this, objectMarkup, options);
        this.clipPath = eraser;
        return [
          eraserMarkup,
          markup.replace(">", 'mask="url(#' + eraser.clipPathId + ')" >')
        ].join("\n");
      } else {
        return __createBaseSVGMarkup.call(this, objectMarkup, options);
      }
    }
  });

  var _groupToObject = fabric.Group.prototype.toObject;
  fabric.util.object.extend(fabric.Group.prototype, {
    /**
     * Returns an object representation of an instance
     * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
     * @return {Object} Object representation of an instance
     */
    toObject: function(additionalProperties) {
      return _groupToObject.call(this, ["eraser"].concat(additionalProperties));
    }
  });

  fabric.util.object.extend(fabric.Canvas.prototype, {
    /**
     * Used by {@link #renderAll}
     * @returns boolean
     */
    isErasing: function() {
      return (
        this.isDrawingMode &&
        this.freeDrawingBrush &&
        this.freeDrawingBrush.type === "eraser" &&
        this.freeDrawingBrush._isErasing
      );
    },

    /**
     * While erasing, the brush is in charge of rendering the canvas
     * It uses both layers to achieve diserd erasing effect
     *
     * @returns fabric.Canvas
     */
    renderAll: function() {
      if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) {
        this.clearContext(this.contextTop);
        this.contextTopDirty = false;
      }
      // while erasing the brush is in charge of rendering the canvas so we return
      if (this.isErasing()) {
        this.freeDrawingBrush._render();
        return;
      }
      if (this.hasLostContext) {
        this.renderTopLayer(this.contextTop);
      }
      var canvasToDrawOn = this.contextContainer;
      this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender());
      return this;
    }
  });

  /**
   * EraserBrush class
   * Supports selective erasing meaning that only erasable objects are affected by the eraser brush.
   * In order to support selective erasing all non erasable objects are rendered on the main/bottom ctx
   * while the entire canvas is rendered on the top ctx.
   * Canvas bakground/overlay image/color are handled as well.
   * When erasing occurs, the path clips the top ctx and reveals the bottom ctx.
   * This achieves the desired effect of seeming to erase only erasable objects.
   * After erasing is done the created path is added to all intersected objects' `clipPath` property.
   *
   *
   * @class fabric.EraserBrush
   * @extends fabric.PencilBrush
   */
  fabric.EraserBrush = fabric.util.createClass(
    fabric.PencilBrush,
    /** @lends fabric.EraserBrush.prototype */
    {
      type: "eraser",

      /**
       * Indicates that the ctx is ready and rendering can begin.
       * Used to prevent a race condition caused by {@link fabric.EraserBrush#onMouseMove} firing before {@link fabric.EraserBrush#onMouseDown} has completed
       *
       * @private
       */
      _ready: false,

      /**
       * @private
       */
      _drawOverlayOnTop: false,

      /**
       * @private
       */
      _isErasing: false,

      initialize: function(canvas) {
        this.callSuper("initialize", canvas);
        this._renderBound = this._render.bind(this);
        this.render = this.render.bind(this);
      },

      /**
       * Used to hide a drawable from the rendering process
       * @param {fabric.Object} object
       */
      hideObject: function(object) {
        if (object) {
          object._originalOpacity = object.opacity;
          object.set({ opacity: 0 });
        }
      },

      /**
       * Restores hiding an object
       * {@link fabric.EraserBrush#hideObject}
       * @param {fabric.Object} object
       */
      restoreObjectVisibility: function(object) {
        if (object && object._originalOpacity) {
          object.set({ opacity: object._originalOpacity });
          object._originalOpacity = undefined;
        }
      },

      /**
       * Drawing Logic For background drawables: (`backgroundImage`, `backgroundColor`)
       * 1. if erasable = true:
       *    we need to hide the drawable on the bottom ctx so when the brush is erasing it will clip the top ctx and reveal white space underneath
       * 2. if erasable = false:
       *    we need to draw the drawable only on the bottom ctx so the brush won't affect it
       * @param {'bottom' | 'top' | 'overlay'} layer
       */
      prepareCanvasBackgroundForLayer: function(layer) {
        if (layer === "overlay") {
          return;
        }
        var canvas = this.canvas;
        var image = canvas.get("backgroundImage");
        var color = canvas.get("backgroundColor");
        var erasablesOnLayer = layer === "top";
        if (image && image.erasable === !erasablesOnLayer) {
          this.hideObject(image);
        }
        if (color && color.erasable === !erasablesOnLayer) {
          this.hideObject(color);
        }
      },

      /**
       * Drawing Logic For overlay drawables (`overlayImage`, `overlayColor`)
       * We must draw on top ctx to be on top of visible canvas
       * 1. if erasable = true:
       *    we need to draw the drawable on the top ctx as a normal object
       * 2. if erasable = false:
       *    we need to draw the drawable on top of the brush,
       *    this means we need to repaint for every stroke
       *
       * @param {'bottom' | 'top' | 'overlay'} layer
       * @returns boolean render overlay above brush
       */
      prepareCanvasOverlayForLayer: function(layer) {
        var canvas = this.canvas;
        var image = canvas.get("overlayImage");
        var color = canvas.get("overlayColor");
        if (layer === "bottom") {
          this.hideObject(image);
          this.hideObject(color);
          return false;
        }
        var erasablesOnLayer = layer === "top";
        var renderOverlayOnTop =
          (image && !image.erasable) || (color && !color.erasable);
        if (image && image.erasable === !erasablesOnLayer) {
          this.hideObject(image);
        }
        if (color && color.erasable === !erasablesOnLayer) {
          this.hideObject(color);
        }
        return renderOverlayOnTop;
      },

      /**
       * @private
       */
      restoreCanvasDrawables: function() {
        var canvas = this.canvas;
        this.restoreObjectVisibility(canvas.get("backgroundImage"));
        this.restoreObjectVisibility(canvas.get("backgroundColor"));
        this.restoreObjectVisibility(canvas.get("overlayImage"));
        this.restoreObjectVisibility(canvas.get("overlayColor"));
      },

      /**
       * @private
       * This is designed to support erasing a group with both erasable and non-erasable objects.
       * Iterates over collections to allow nested selective erasing.
       * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer}
       * to prepare the bottom layer by hiding erasable nested objects
       *
       * @param {fabric.Collection} collection
       */
      prepareCollectionTraversal: function(collection) {
        var _this = this;
        collection.forEachObject(function(obj) {
          if (obj.forEachObject) {
            _this.prepareCollectionTraversal(obj);
          } else {
            if (obj.erasable) {
              _this.hideObject(obj);
            }
          }
        });
      },

      /**
       * @private
       * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer}
       * to reverse the action of {@link fabric.EraserBrush#prepareCollectionTraversal}
       *
       * @param {fabric.Collection} collection
       */
      restoreCollectionTraversal: function(collection) {
        var _this = this;
        collection.forEachObject(function(obj) {
          if (obj.forEachObject) {
            _this.restoreCollectionTraversal(obj);
          } else {
            _this.restoreObjectVisibility(obj);
          }
        });
      },

      /**
       * @private
       * This is designed to support erasing a group with both erasable and non-erasable objects.
       *
       * @param {'bottom' | 'top' | 'overlay'} layer
       */
      prepareCanvasObjectsForLayer: function(layer) {
        if (layer !== "bottom") {
          return;
        }
        this.prepareCollectionTraversal(this.canvas);
      },

      /**
       * @private
       * @param {'bottom' | 'top' | 'overlay'} layer
       */
      restoreCanvasObjectsFromLayer: function(layer) {
        if (layer !== "bottom") {
          return;
        }
        this.restoreCollectionTraversal(this.canvas);
      },

      /**
       * @private
       * @param {'bottom' | 'top' | 'overlay'} layer
       * @returns boolean render overlay above brush
       */
      prepareCanvasForLayer: function(layer) {
        this.prepareCanvasBackgroundForLayer(layer);
        this.prepareCanvasObjectsForLayer(layer);
        return this.prepareCanvasOverlayForLayer(layer);
      },

      /**
       * @private
       * @param {'bottom' | 'top' | 'overlay'} layer
       */
      restoreCanvasFromLayer: function(layer) {
        this.restoreCanvasDrawables();
        this.restoreCanvasObjectsFromLayer(layer);
      },

      /**
       * Render all non-erasable objects on bottom layer with the exception of overlays to avoid being clipped by the brush.
       * Groups are rendered for nested selective erasing, non-erasable objects are visible while erasable objects are not.
       */
      renderBottomLayer: function() {
        var canvas = this.canvas;
        this.prepareCanvasForLayer("bottom");
        canvas.renderCanvas(
          canvas.getContext(),
          canvas.getObjects().filter(function(obj) {
            return !obj.erasable || obj.isType("group");
          })
        );
        this.restoreCanvasFromLayer("bottom");
      },

      /**
       * 1. Render all objects on top layer, erasable and non-erasable
       *    This is important for cases such as overlapping objects, the background object erasable and the foreground object not erasable.
       * 2. Render the brush
       */
      renderTopLayer: function() {
        var canvas = this.canvas;
        this._drawOverlayOnTop = this.prepareCanvasForLayer("top");
        canvas.renderCanvas(canvas.contextTop, canvas.getObjects());
        this.callSuper("_render");
        this.restoreCanvasFromLayer("top");
      },

      /**
       * Render all non-erasable overlays on top of the brush so that they won't get erased
       */
      renderOverlay: function() {
        this.prepareCanvasForLayer("overlay");
        var canvas = this.canvas;
        var ctx = canvas.contextTop;
        this._saveAndTransform(ctx);
        canvas._renderOverlay(ctx);
        ctx.restore();
        this.restoreCanvasFromLayer("overlay");
      },

      /**
       * @extends @class fabric.BaseBrush
       * @param {CanvasRenderingContext2D} ctx
       */
      _saveAndTransform: function(ctx) {
        this.callSuper("_saveAndTransform", ctx);
        ctx.globalCompositeOperation = "destination-out";
      },

      /**
       * We indicate {@link fabric.PencilBrush} to repaint itself if necessary
       * @returns
       */
      needsFullRender: function() {
        return this.callSuper("needsFullRender") || this._drawOverlayOnTop;
      },

      /**
       *
       * @param {fabric.Point} pointer
       * @param {fabric.IEvent} options
       * @returns
       */
      onMouseDown: function(pointer, options) {
        if (!this.canvas._isMainEvent(options.e)) {
          return;
        }
        this._prepareForDrawing(pointer);
        // capture coordinates immediately
        // this allows to draw dots (when movement never occurs)
        this._captureDrawingPath(pointer);

        this._isErasing = true;
        this.canvas.fire("erasing:start");
        this._ready = true;
        this._render();
      },

      /**
       * Rendering is done in 4 steps:
       * 1. Draw all non-erasable objects on bottom ctx with the exception of overlays {@link fabric.EraserBrush#renderBottomLayer}
       * 2. Draw all objects on top ctx including erasable drawables {@link fabric.EraserBrush#renderTopLayer}
       * 3. Draw eraser {@link fabric.PencilBrush#_render} at {@link fabric.EraserBrush#renderTopLayer}
       * 4. Draw non-erasable overlays {@link fabric.EraserBrush#renderOverlay}
       *
       * @param {fabric.Canvas} canvas
       */
      _render: function() {
        if (!this._ready) {
          return;
        }
        this.isRendering = 1;
        this.renderBottomLayer();
        this.renderTopLayer();
        this.renderOverlay();
        this.isRendering = 0;
      },

      /**
       * @public
       */
      render: function() {
        if (this._isErasing) {
          if (this.isRendering) {
            this.isRendering = fabric.util.requestAnimFrame(this._renderBound);
          } else {
            this._render();
          }
          return true;
        }
        return false;
      },

      /**
       * Adds path to existing clipPath of object
       *
       * @param {fabric.Object} obj
       * @param {fabric.Path} path
       */
      _addPathToObjectEraser: function(obj, path) {
        var clipObject;
        var _this = this;
        //  object is collection, i.e group
        if (obj.forEachObject) {
          obj.forEachObject(function(_obj) {
            if (_obj.erasable) {
              _this._addPathToObjectEraser(_obj, path);
            }
          });
          return;
        }
        if (!obj.getEraser()) {
          var size = obj._getNonTransformedDimensions();
          var rect = new fabric.Rect({
            width: size.x,
            height: size.y,
            clipPath: obj.clipPath,
            originX: "center",
            originY: "center"
          });
          clipObject = new fabric.Group([rect], {
            eraser: true
          });
        } else {
          clipObject = obj.clipPath;
        }

        path.clone(function(path) {
          path.globalCompositeOperation = "destination-out";
          // http://fabricjs.com/using-transformations
          var desiredTransform = fabric.util.multiplyTransformMatrices(
            fabric.util.invertTransform(obj.calcTransformMatrix()),
            path.calcTransformMatrix()
          );
          fabric.util.applyTransformToObject(path, desiredTransform);
          clipObject.addWithUpdate(path);
          obj.set({
            clipPath: clipObject,
            dirty: true
          });
        });
      },

      /**
       * Add the eraser path to canvas drawables' clip paths
       *
       * @param {fabric.Canvas} source
       * @param {fabric.Canvas} path
       * @returns {Object} canvas drawables that were erased by the path
       */
      applyEraserToCanvas: function(path) {
        var canvas = this.canvas;
        var drawables = {};
        [
          "backgroundImage",
          "backgroundColor",
          "overlayImage",
          "overlayColor"
        ].forEach(function(prop) {
          var drawable = canvas[prop];
          if (drawable && drawable.erasable) {
            this._addPathToObjectEraser(drawable, path);
            drawables[prop] = drawable;
          }
        }, this);
        return drawables;
      },

      /**
       * On mouseup after drawing the path on contextTop canvas
       * we use the points captured to create an new fabric path object
       * and add it to every intersected erasable object.
       */
      _finalizeAndAddPath: function() {
        var ctx = this.canvas.contextTop,
          canvas = this.canvas;
        ctx.closePath();
        if (this.decimate) {
          this._points = this.decimatePoints(this._points, this.decimate);
        }

        // clear
        canvas.clearContext(canvas.contextTop);
        this._isErasing = false;

        var pathData =
          this._points && this._points.length > 1
            ? this.convertPointsToSVGPath(this._points).join("")
            : "M 0 0 Q 0 0 0 0 L 0 0";
        if (pathData === "M 0 0 Q 0 0 0 0 L 0 0") {
          canvas.fire("erasing:end");
          // do not create 0 width/height paths, as they are
          // rendered inconsistently across browsers
          // Firefox 4, for example, renders a dot,
          // whereas Chrome 10 renders nothing
          canvas.requestRenderAll();
          return;
        }

        var path = this.createPath(pathData);
        canvas.fire("before:path:created", { path: path });

        // finalize erasing
        var drawables = this.applyEraserToCanvas(path);
        var _this = this;
        var targets = [];
        canvas.forEachObject(function(obj) {
          if (obj.erasable && obj.intersectsWithObject(path, true)) {
            _this._addPathToObjectEraser(obj, path);
            targets.push(obj);
          }
        });

        canvas.fire("erasing:end", {
          path: path,
          targets: targets,
          drawables: drawables
        });

        canvas.requestRenderAll();
        path.setCoords();
        this._resetShadow();

        // fire event 'path' created
        canvas.fire("path:created", { path: path });
      }
    }
  );

  /** ERASER_END */
})();

  组件得引用

  

 <drawers ref="drawers" @sendImg="imageSend"></drawers>

import drawers from "../../components/DrawingBoard/index.vue";

components: {
    drawers,
  },

imageSend(val) {  接收签字生成得图片
      this.attendPersonList[this.rowDrawersVal].signImage = val;
      this.isDrawers = false;
    },

  

posted @ 2022-05-21 15:25  沁猿春  阅读(109)  评论(0编辑  收藏  举报