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

Angie ·
更新时间:2024-11-10
· 1705 次阅读

目录

组件代码如下

在开发过程中遇到的问题

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持软件开发网。



VUE 图片 canvas

需要 登录 后方可回复, 如果你还没有账号请 注册新账号