vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > canvas实现在线图片标注

vue下如何利用canvas实现在线图片标注

作者:嘀嗒嗒咚呲哒哒

这篇文章主要介绍了vue下如何利用canvas实现在线图片标注,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

web端实现在线图片标注在此做下记录,功能类似微信截图时的标注,包含画线、框、箭头和文字输入,思路是利用canvas画布,先把要标注的图片使用drawImage方法画在画布上,然后定义画线、框、箭头和文字输入的方法调用

组件代码如下

<template>
  <div class="draw">
    <div class="drawTop" ref="drawTop" v-if="lineStep == lineNum">
      <div>
        <el-button type @click="resetAll">清空</el-button>
        <el-button type @click="repeal">撤销</el-button>
        <el-button type @click="canvasRedo">恢复</el-button>
        <el-button type @click="downLoad">下载</el-button>
      </div>
      <div style="width:22%">
        选择绘制类型:
        <el-radio-group v-model="type" size="medium">
          <el-radio-button
            v-for="(item,index) in typeOption"
            :key="index"
            :label="item.value"
            @click.native="radioClick(item.value)"
          >{{item.label}}
          </el-radio-button>
        </el-radio-group>
      </div>
      <div style="width:15%">
        边框粗细:
        <el-slider v-model="lineWidth" :min="0" :max="10" :step="1" style="width:70%"></el-slider>
      </div>
      <div>
        线条颜色:
        <el-color-picker v-model="strokeStyle"></el-color-picker>
      </div>
      <div>
        文字颜色:
        <el-color-picker v-model="fontColor"></el-color-picker>
      </div>
      <div style="width:15%">
        文字大小:
        <el-slider v-model="fontSize" :min="14" :max="36" :step="2" style="width:70%"></el-slider>
      </div>
    </div>
    <div style="height: 100%;width: 100%;position:relative;">
      <div class="content"></div>
      <input v-show="isShow" type="text" @blur="txtBlue" ref="txt" id="txt"
             style="z-index: 9999;position: absolute;border: 0;background:none;outline: none;"/>
    </div>
  </div>
</template>
<script>
  export default {
    name: "callout",
    props: {
      imgPath: undefined,
 
    },
    data() {
      return {
        isShow: false,
        canvas: "",
        ctx: "",
        ctxX: 0,
        ctxY: 0,
        lineWidth: 1,
        type: "L",
        typeOption: [
          {label: "线", value: "L"},
          {label: "矩形", value: "R"},
          {label: "箭头", value: "A"},
          {label: "文字", value: "T"},
        ],
        canvasHistory: [],
        step: 0,
        loading: false,
        fillStyle: "#CB0707",
        strokeStyle: "#CB0707",
        lineNum: 2,
        linePeak: [],
        lineStep: 2,
        ellipseR: 0.5,
        dialogVisible: false,
        isUnfold: true,
        fontSize: 24,
        fontColor: "#CB0707",
        fontFamily: '微软雅黑',
        img: new Image(),
      };
    },
    mounted() {
      let _this = this;
      let image = new Image();
      image.setAttribute('crossOrigin', 'anonymous');
      image.src = this.imgPath;
      image.onload = function () {//图片加载完,再draw 和 toDataURL
        if (image.complete) {
          _this.img = image
          let content = document.getElementsByClassName("content")[0];
          _this.canvas = document.createElement("canvas");
          _this.canvas.height = _this.img.height
          _this.canvas.width = _this.img.width
          _this.ctx = _this.canvas.getContext("2d");
          _this.ctx.globalAlpha = 1;
          _this.ctx.drawImage(_this.img, 0, 0)
          _this.canvasHistory.push(_this.canvas.toDataURL());
          _this.ctx.globalCompositeOperation = _this.type;
          content.appendChild(_this.canvas);
          _this.bindEventLisner();
        }
      }
    },
    methods: {
      radioClick(item) {
        if (item != "T") {
          this.txtBlue()
          this.resetTxt()
        }
      },
      // 下载画布
      downLoad() {
        let _this = this;
        let url = _this.canvas.toDataURL("image/png");
        let fileName = "canvas.png";
        if ("download" in document.createElement("a")) {
          // 非IE下载
          const elink = document.createElement("a");
          elink.download = fileName;
          elink.style.display = "none";
          elink.href = url;
          document.body.appendChild(elink);
          elink.click();
          document.body.removeChild(elink);
        } else {
          // IE10+下载
          navigator.msSaveBlob(url, fileName);
        }
      },
      // 清空画布及历史记录
      resetAll() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.canvasHistory = [];
        this.ctx.drawImage(this.img, 0, 0);
        this.canvasHistory.push(this.canvas.toDataURL());
        this.step = 0;
        this.resetTxt();
      },
      // 清空当前画布
      reset() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.ctx.drawImage(this.img, 0, 0);
        this.resetTxt();
      },
      // 撤销方法
      repeal() {
        let _this = this;
        if (this.isShow) {
          _this.resetTxt();
          _this._repeal();
        } else {
          _this._repeal();
        }
 
      },
      _repeal() {
        if (this.step >= 1) {
          this.step = this.step - 1;
          let canvasPic = new Image();
          canvasPic.src = this.canvasHistory[this.step];
          canvasPic.addEventListener("load", () => {
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            this.ctx.drawImage(canvasPic, 0, 0);
            this.loading = true;
          });
        } else {
          this.$message.warning("不能再继续撤销了");
        }
      },
      // 恢复方法
      canvasRedo() {
        if (this.step < this.canvasHistory.length - 1) {
          if (this.step == 0) {
            this.step = 1;
          } else {
            this.step++;
          }
          let canvasPic = new Image();
          canvasPic.src = this.canvasHistory[this.step];
          canvasPic.addEventListener("load", () => {
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            this.ctx.drawImage(canvasPic, 0, 0);
          });
        } else {
          this.$message.warning("已经是最新的记录了");
        }
      },
      // 绘制历史数组中的最后一个
      rebroadcast() {
        let canvasPic = new Image();
        canvasPic.src = this.canvasHistory[this.step];
        canvasPic.addEventListener("load", () => {
          this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
          this.ctx.drawImage(canvasPic, 0, 0);
          this.loading = true;
        });
      },
      // 绑定事件,判断分支
      bindEventLisner() {
        let _this = this;
        let r1, r2; // 绘制圆形,矩形需要
        this.canvas.onmousedown = function (e) {
          console.log("onmousedown");
          if (_this.type == "L") {
            _this.createL(e, "begin");
          } else if (_this.type == "R") {
            r1 = e.layerX;
            r2 = e.layerY;
            _this.createR(e, "begin", r1, r2);
          } else if (_this.type == "A") {
            _this.drawArrow(e, "begin")
          } else if (_this.type == "T") {
            _this.createT(e, "begin")
          }
        };
        this.canvas.onmouseup = function (e) {
          console.log("onmouseup");
          if (_this.type == "L") {
            _this.createL(e, "end");
          } else if (_this.type == "R") {
            _this.createR(e, "end", r1, r2);
            r1 = null;
            r2 = null;
          } else if (_this.type == "A") {
            _this.drawArrow(e, "end")
          } else if (_this.type == "T") {
            _this.createT(e, "end")
          }
        };
 
      },
      // 绘制线条
      createL(e, status) {
        let _this = this;
        if (status == "begin") {
          _this.ctx.beginPath();
          _this.ctx.moveTo(e.layerX, e.layerY);
          _this.canvas.onmousemove = function (e) {
            console.log("onmousemove");
            _this.ctx.lineTo(e.layerX, e.layerY);
            _this.ctx.strokeStyle = _this.strokeStyle;
            _this.ctx.lineWidth = _this.lineWidth;
            _this.ctx.stroke();
          };
        } else if (status == "end") {
          _this.ctx.closePath();
          _this.step = _this.step + 1;
          if (_this.step < _this.canvasHistory.length - 1) {
            _this.canvasHistory.length = _this.step; // 截断数组
          }
          _this.canvasHistory.push(_this.canvas.toDataURL());
          _this.canvas.onmousemove = null;
        }
      },
      // 绘制矩形
      createR(e, status, r1, r2) {
        let _this = this;
        let r;
        if (status == "begin") {
          console.log("onmousemove");
          _this.canvas.onmousemove = function (e) {
            _this.reset();
            let rx = e.layerX - r1;
            let ry = e.layerY - r2;
 
            //保留之前绘画的图形
            if (_this.step !== 0) {
              let canvasPic = new Image();
              canvasPic.src = _this.canvasHistory[_this.step];
              _this.ctx.drawImage(canvasPic, 0, 0);
            }
 
            _this.ctx.beginPath();
            _this.ctx.strokeRect(r1, r2, rx, ry);
            _this.ctx.strokeStyle = _this.strokeStyle;
            _this.ctx.lineWidth = _this.lineWidth;
            _this.ctx.closePath();
            _this.ctx.stroke();
          };
        } else if (status == "end") {
          _this.rebroadcast();
          let interval = setInterval(() => {
            if (_this.loading) {
              clearInterval(interval);
              _this.loading = false;
            } else {
              return;
            }
            let rx = e.layerX - r1;
            let ry = e.layerY - r2;
            _this.ctx.beginPath();
            _this.ctx.rect(r1, r2, rx, ry);
            _this.ctx.strokeStyle = _this.strokeStyle;
            _this.ctx.lineWidth = _this.lineWidth;
            _this.ctx.closePath();
            _this.ctx.stroke();
            _this.step = _this.step + 1;
            if (_this.step < _this.canvasHistory.length - 1) {
              _this.canvasHistory.length = _this.step; // 截断数组
            }
            _this.canvasHistory.push(_this.canvas.toDataURL());
            _this.canvas.onmousemove = null;
          }, 1);
        }
      },
 
      //绘制箭头
      drawArrow(e, status) {
        let _this = this;
        if (status == "begin") {
          //获取起始位置
          _this.arrowFromX = e.layerX;
          _this.arrowFromY = e.layerY;
          _this.ctx.beginPath();
          _this.ctx.moveTo(e.layerX, e.layerY);
        } else if (status == "end") {
          //计算箭头及画线
          let toX = e.layerX;
          let toY = e.layerY;
          let theta = 30;
          let headlen = 10;
          let _this = this;
          let fromX = this.arrowFromX;
          let fromY = this.arrowFromY;
          // 计算各角度和对应的P2,P3坐标
          let angle = Math.atan2(fromY - toY, fromX - toX) * 180 / Math.PI,
          angle1 = (angle + theta) * Math.PI / 180,
          angle2 = (angle - theta) * Math.PI / 180,
          topX = headlen * Math.cos(angle1),
          topY = headlen * Math.sin(angle1),
          botX = headlen * Math.cos(angle2),
          botY = headlen * Math.sin(angle2);
          let arrowX = fromX - topX,
          arrowY = fromY - topY;
          _this.ctx.moveTo(arrowX, arrowY);
          _this.ctx.moveTo(fromX, fromY);
          _this.ctx.lineTo(toX, toY);
          arrowX = toX + topX;
          arrowY = toY + topY;
          _this.ctx.moveTo(arrowX, arrowY);
          _this.ctx.lineTo(toX, toY);
          arrowX = toX + botX;
          arrowY = toY + botY;
          _this.ctx.lineTo(arrowX, arrowY);
          _this.ctx.strokeStyle = _this.strokeStyle;
          _this.ctx.lineWidth = _this.lineWidth;
          _this.ctx.stroke();
 
          _this.ctx.closePath();
          _this.step = _this.step + 1;
          if (_this.step < _this.canvasHistory.length - 1) {
            _this.canvasHistory.length = _this.step; // 截断数组
          }
          _this.canvasHistory.push(_this.canvas.toDataURL());
          _this.canvas.onmousemove = null;
        }
      },
 
      //文字输入
      createT(e, status) {
        let _this = this;
        if (status == "begin") {
 
        } else if (status == "end") {
          let offset = 0;
          if (_this.fontSize >= 28) {
            offset = (_this.fontSize / 2) - 3
          } else {
            offset = (_this.fontSize / 2) - 2
          }
 
          _this.ctxX = e.layerX + 2;
          _this.ctxY = e.layerY + offset;
 
          let index = this.getPointOnCanvas(e);
          _this.$refs.txt.style.left = index.x + 'px';
          _this.$refs.txt.style.top = index.y - (_this.fontSize / 2) + 'px';
          _this.$refs.txt.value = '';
          _this.$refs.txt.style.height = _this.fontSize + "px";
          _this.$refs.txt.style.width = _this.canvas.width - e.layerX - 1 + "px",
            _this.$refs.txt.style.fontSize = _this.fontSize + "px";
          _this.$refs.txt.style.fontFamily = _this.fontFamily;
          _this.$refs.txt.style.color = _this.fontColor;
          _this.$refs.txt.style.maxlength = Math.floor((_this.canvas.width - e.layerX) / _this.fontSize);
          _this.isShow = true;
          setTimeout(() => {
            _this.$refs.txt.focus();
          })
        }
      },
      //文字输入框失去光标时在画布上生成文字
      txtBlue() {
        let _this = this;
        let txt = _this.$refs.txt.value;
        if (txt) {
          _this.ctx.font = _this.$refs.txt.style.fontSize + ' ' + _this.$refs.txt.style.fontFamily;
          _this.ctx.fillStyle = _this.$refs.txt.style.color;
          _this.ctx.fillText(txt, _this.ctxX, _this.ctxY);
          _this.step = _this.step + 1;
          if (_this.step < _this.canvasHistory.length - 1) {
            _this.canvasHistory.length = _this.step; // 截断数组
          }
          _this.canvasHistory.push(_this.canvas.toDataURL());
          _this.canvas.onmousemove = null;
        }
      },
      //计算文字框定位位置
      getPointOnCanvas(e) {
        let cs = this.canvas;
        let content = document.getElementsByClassName("content")[0];
        return {
          x: e.layerX + (content.clientWidth - cs.width) / 2,
          y: e.layerY
        };
      },
      //清空文字
      resetTxt() {
        let _this = this;
        _this.$refs.txt.value = '';
        _this.isShow = false;
      }
    }
  };
</script>
<style scope>
  * {
    box-sizing: border-box;
  }
 
  body,
  html,
  #app {
    overflow: hidden;
  }
 
  .draw {
    height: 100%;
    min-width: 420px;
    display: flex;
    flex-direction: column;
  }
 
  .content {
    flex-grow: 1;
    height: 100%;
    width: 100%;
  }
 
  .drawTop {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    padding: 5px;
    height: 52px;
  }
 
  .drawTop > div {
    display: flex;
    align-items: center;
    padding: 5px 5px;
  }
 
  div.drawTopContrllor {
    display: none;
  }
 
  @media screen and (max-width: 1200px) {
    .drawTop {
      position: absolute;
      background-color: white;
      width: 100%;
      flex-direction: column;
      align-items: flex-start;
      height: 30px;
      overflow: hidden;
    }
 
    .drawTopContrllor {
      display: flex !important;
      height: 30px;
      width: 100%;
      justify-content: center;
      align-items: center;
      padding: 0 !important;
    }
  }
</style>

然后在页面中引入组件,传入图片链接。

在开发过程中遇到的问题

文字输入功能在用户输入文字后,如果不再点击别的地方直接点击别的功能按钮的话,最后输入的文字将不会再画布上生成,通过监控输入框的blur事件来在画布上生成文字,避免这个问题。

文字输入时字体的大小会影响生成文字的位置,这里发现文字的大小和位置有一个偏移量:

let offset = 0;
if (_this.fontSize >= 28) {
  offset = (_this.fontSize / 2) - 3
} else {
  offset = (_this.fontSize / 2) - 2
}

在画布上生成文字的时候需要加上这个偏移量,这里字体范围是14~36,别的字体大小没有校验,不一定适用这个计算方式。

绘制矩形的时候需要先清空画布,在清空之前先保存一次画布然后再清空再重新画一下画布,负责矩形框会不停的出现轨迹,并且之前画的元素会消失。

撤销的时候需要考虑文字输入,判断input得v-show是否为true,如果是true需要先清空文字,再撤销,否则画布上会一直存在一个输入框。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文