<
>

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

2022-04-28 12:59:06 来源:易采站长站 作者:

目录
组件代码如下在开发过程中遇到的问题

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.toDatx.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.fontSizercIugcYzdj / 2) - 2}

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

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

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

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

暂时禁止评论

微信扫一扫

易采站长站微信账号