当前位置:首页 > 开发教程 > js/jQuery教程 >

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

时间:2022-04-28 13:32 来源:未知 作者:独拥你 收藏

这篇文章主要介绍了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需要先清空文字,再撤销,否则画布上会一直存在一个输入框。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持源码搜藏网。


下一篇:没有了

js/jQuery教程阅读排行

最新文章