精华内容
下载资源
问答
  • Vue 简单的图片区域裁切组件
  • vue-headful允许从任何Vue.js视图设置文档的标题和几个元标记。 vue-headful是Headful的一个小包装, 是用于使用JavaScript设置元标记的通用库。 安装 npm i vue-headful 用法 注册组件: import Vue from 'vue' ; ...
  • vue下利用canvas实现在线图片标注

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

          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>

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

    在开发过程中遇到的问题:

    1. 文字输入功能在用户输入文字后,如果不再点击别的地方直接点击别的功能按钮的话,最后输入的文字将不会再画布上生成,通过监控输入框的blur事件来在画布上生成文字,避免这个问题。
    2. 文字输入时字体的大小会影响生成文字的位置,这里发现文字的大小和位置有一个偏移量:
      let offset = 0;
      if (_this.fontSize >= 28) {
        offset = (_this.fontSize / 2) - 3
      } else {
        offset = (_this.fontSize / 2) - 2
      }

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

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

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

    有别的问题以后再补充。

    参考:https://www.jianshu.com/p/7172c11c6002

     

     

    展开全文
  • vue图片标注矩形框

    2021-04-16 09:46:57
    <template> <div id="test" style="user-select: none;"> <button @click="fangda">放大</button> <button @click="suoxiao">缩小</button> ...button v-s
    <template>
      <div id="test" style="user-select: none;">
        <button @click="fangda">放大</button>
        <button @click="suoxiao">缩小</button>
        <button v-show="isTrue" @click="gai">添加</button>
        <button v-show="!isTrue" @click="gai">移动</button>
    
        <div class="content">
          <div
            :style="{
              transform: 'scale(' + num + ')',
              position: 'relative',
              width: '100%',
              height: '100%',
            }"
            @mousedown="moveMouse"
            @click="getOffect"
          >
            <div
              v-for="(item, index) in boxArry"
              :key="index"
              :ref="'biaozhu' + index"
              :class="'biaozhu' + index == 'biaozhu' + b_i ? 'biaozhu b_border' : 'biaozhu'"
              :style="{
                width: item.width + 'px',
                height: item.height + 'px',
                position: 'absolute',
                left: item.left + 'px',
                top: item.top + 'px',
                background: 'rgba(43,100,206,0.3)',
                border: 'none',
              }"
              @mousedown.stop="move"
              @click="handelClick(index)"
            >
              <div v-if="b_i == index" class="r_b" @mousedown="mouseMove11"></div>
            </div>
            <div
              :style="{
                height: biaozhuHeight + 'px',
                width: biaozhuWidth + 'px',
                top: biaozhuTop + 'px',
                left: biaozhuLeft + 'px',
                position: 'absolute',
                background: 'rgba(43,100,206,0.3)',
              }"
            ></div>
            <img
              style="width: 100%; height: 100%; pointer-events: none;"
              src="https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3479844176,2436830052&fm=26&gp=0.jpg"
              alt=""
              @mousedown="isTrue ? null : move"
            />
          </div>
        </div>
      </div>
    </template>
    <script>
      export default {
        directives: {
          drag: function(el) {
            let dragBox = el //获取当前元素
            dragBox.onmousedown = (e) => {
              //算出鼠标相对元素的位置
    
              let disX = e.clientX - dragBox.offsetLeft
              let disY = e.clientY - dragBox.offsetTop
              console.log(disX, disY)
              document.onmousemove = (e) => {
                //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
                let left = e.clientX - disX
                let top = e.clientY - disY
                //移动当前元素
                dragBox.style.left = left + 'px'
                dragBox.style.top = top + 'px'
                console.log(left, top, '111111111')
              }
              document.onmouseup = (e) => {
                //鼠标弹起来的时候不再移动
                document.onmousemove = null
                //预防鼠标弹起来后还会循环(即预防鼠标放上去的时候还会移动)
                document.onmouseup = null
              }
            }
          },
        },
        data() {
          return {
            num: 1,
            boxArry: [],
            isTrue: false,
            width: '',
            height: '',
            left: '',
            top: '',
            b_i: '',
            biaozhuHeight: 0,
            biaozhuWidth: 0,
            biaozhuTop: 0,
            biaozhuLeft: 0,
          }
        },
    
        methods: {
          getVuex() {
            console.log(this.$store.state.treeData)
          },
          commitVuex() {
            this.$store.commit('changeTreeData', { a: 1, b: 2 })
          },
          mouseOver2(e) {
            document.onmousedown = (e) => {
              let odiv = e.target //获取目标元素
    
              //算出鼠标相对元素的位置
              let disX = e.clientX - odiv.offsetLeft
              let disY = e.clientY - odiv.offsetTop
              let left = e.clientX - disX
              let top = e.clientY - disY
    
              //绑定元素位置到positionX和positionY上面
              this.positionX = top
              this.positionY = left
              console.log(this.boxArry, this.dragsIndex)
              //移动当前元素
              this.boxArry[this.b_i].width = left
              this.boxArry[this.b_i].height = top
            }
          },
          drags(e) {
            console.log(e)
          },
          mouseMove11(e) {
            // console.log(e,index)
            let odiv = e.target //获取目标元素
    
            //算出鼠标相对元素的位置
            let disX = e.clientX - odiv.offsetLeft
            let disY = e.clientY - odiv.offsetTop
            // debugger;
            e.target.onmousemove = (e) => {
              //鼠标按下并移动的事件
              //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
              // console.log('aaaaaaaaaaaaa',e)
              let left = e.clientX - disX
              let top = e.clientY - disY
    
              //绑定元素位置到positionX和positionY上面
              this.positionX = top
              this.positionY = left
              // console.log(this.boxArry,this.dragsIndex)
              //移动当前元素
              this.boxArry[this.b_i].width = left
              this.boxArry[this.b_i].height = top
              e.target.onmouseup = (e) => {
                e.target.onmousemove = null
    
                // document.onmouseup = null;
              }
            }
          },
          gai() {
            this.isTrue = !this.isTrue
          },
          getOffect(e) {
            console.log(e)
            document.onmouseup = null
            // this.left=e.offsetX
            //     this.top=e.offsetY
          },
          moveMouse(e) {
            let odiv = e.target //获取目标元素
    
            //算出鼠标相对元素的位置
            let disX = e.clientX - odiv.offsetLeft
            let disY = e.clientY - odiv.offsetTop
            console.log(disX, disY)
            if (this.isTrue) {
              // 拖动
              document.onmousemove = (e) => {
                //鼠标按下并移动的事件
                //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
                let left = e.clientX - disX
                let top = e.clientY - disY
    
                //绑定元素位置到positionX和positionY上面
                this.positionX = top
                this.positionY = left
    
                //移动当前元素
                odiv.style.left = left + 'px'
                odiv.style.top = top + 'px'
              }
              document.onmouseup = (e) => {
                document.onmousemove = null
                document.onmouseup = null
              }
            } else {
              // 添加div
    
              console.log(e)
              document.onmousemove = (e) => {
                //鼠标按下并移动的事件
                //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
    
                let left = disX - odiv.getBoundingClientRect().x
                let top = disY - odiv.getBoundingClientRect().y
    
                console.log(e.target.offsetLeft)
                this.width = (e.clientX - disX) / this.num
                this.height = (e.clientY - disY) / this.num
                this.biaozhuWidth = this.width
                this.biaozhuHeight = this.height
                this.biaozhuLeft = left
                this.biaozhuTop = top
                document.onmouseup = (e) => {
                  let left = disX - odiv.getBoundingClientRect().x
                  let top = disY - odiv.getBoundingClientRect().y
                  this.width = (e.clientX - disX) / this.num
                  this.height = (e.clientY - disY) / this.num
    
                  console.log(e.target.getBoundingClientRect(), disX, disY)
                  if (this.width > 5 && this.height > 5) {
                    this.boxArry.push({
                      width: this.width,
                      height: this.height,
                      left: left,
                      top: top,
                    })
                  }
    
                  this.biaozhuWidth = 0
                  this.biaozhuHeight = 0
                  this.biaozhuLeft = 0
                  this.biaozhuTop = 0
                  document.onmousemove = null
                  document.onmouseup = null
                }
              }
            }
          },
          move(e) {
            let odiv = e.target //获取目标元素
    
            //算出鼠标相对元素的位置
            let disX = e.clientX - odiv.offsetLeft
            let disY = e.clientY - odiv.offsetTop
            document.onmousemove = (e) => {
              //鼠标按下并移动的事件
              //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
              let left = e.clientX - disX
              let top = e.clientY - disY
    
              //绑定元素位置到positionX和positionY上面
              this.positionX = top
              this.positionY = left
    
              //移动当前元素
              odiv.style.left = left + 'px'
              odiv.style.top = top + 'px'
            }
            document.onmouseup = (e) => {
              document.onmousemove = null
              document.onmouseup = null
            }
          },
    
          fangda() {
            this.num += 0.1
          },
          suoxiao() {
            this.num -= 0.1
          },
          tianjia() {
            this.boxArry.push({
              width: 100,
              height: 100,
              left: 20,
              top: 20,
            })
          },
          handelClick(i) {
            this.b_i = i
            console.log(i)
          },
          dragstart(event, data) {
            // console.log('drag')
            // event.dataTransfer.setData('item', data)
          },
          dragend(event) {
            // event.dataTransfer.clearData()
          },
        },
      }
    </script>
    <style lang="less">
      #test {
        /deep/.el-dialog__body {
          padding: 10px 20px !important;
        }
        .content {
          width: 800px;
          height: 500px;
          background: rgb(0, 255, 64);
          margin: 0 auto;
          overflow: hidden;
          position: relative;
    
          .drag_box {
            width: 150px;
            height: 90px;
            border: 1px solid #666;
            background-color: #ccc;
            /* 使用定位,脱离文档流 */
            position: relative;
            top: 100px;
            left: 100px;
            /* 鼠标移入变成拖拽手势 */
            cursor: move;
            z-index: 3000;
          }
          .b_border {
            border: 1px solid red !important;
          }
          .biaozhu {
            z-index: 9999999;
          }
          .r_b {
            position: absolute;
            right: 0;
            bottom: 0;
            width: 20px;
            height: 20px;
            background: red;
          }
          .r_b:hover {
            cursor: se-resize;
          }
        }
      }
    </style>
    
    
    展开全文
  • vue 使用canvas 进行图片标注

    千次阅读 2020-12-28 14:02:27
    <template> <div class="draw"> <div class="drawTop" ref="drawTop" v-if="lineStep == lineNum"> <div> <el-button type @click="resetAll">清空<...撤销&l..
    
    <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 lang="scss" scope>
      * {
        box-sizing: border-box;
      }
     
    
      .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>

     

    展开全文
  • vue下用canvas实现图片标注工具,允许图片放大、缩小,允许拖拽图片 效果图片 概述:       之前在js版本上实现过canvas标注工具,最近又拎出来,重新用vue来实现该标注功能,希望能...
    vue下用canvas实现图片标注工具,允许图片放大、缩小,允许拖拽图片

    效果图片
    在这里插入图片描述

    概述:

          之前在js版本上实现过canvas标注工具,最近又拎出来,重新用vue来实现该标注功能,希望能给刚入门的小白一个指引,让小白们能少走点弯路


    实现过程中遇见的问题:

    • canvas放置在html界面上
    • 图片如何加载到canvas中
    • 如何实现图片放大缩小
    • 如何实现图片拖拽
    • 如何实现图片框选功能

    注意点:代码中需要提前引入element-ui 的内容
    以下是实现代码(vue版):
    <template>
      <div class="divbody">
        <el-row class="toolsClass">
          <el-button @click="ifIsEditClick">
            {{ params.editFlag ? "可编辑" : "不可编辑" }}
          </el-button>
          <el-button @click="cleanCanvas">清除标记</el-button>
          <el-button @click="moveImageClick">{{
            params.moveImageFlag ? "可移动" : "不可移动"
          }}</el-button>
        </el-row>
    
        <!-- canvas 区域 -->
        <div class="imgContainer" ref="imgContainer">
          <canvas
            :ref="'refmyCanvas'"
            class="canvasClass"
            :width="divWidth"
            :height="divHeight"
            @mousedown="canvasMouseDown"
            @mouseup="canvasMouseUp"
            @mousemove="canvasMouseMove"
            @mousewheel="canvasMousewheel"
          >
          </canvas>
          <img
            :id="'image'"
            :src="imageSrc"
            :ref="'refimage'"
            class="imgClass"
            @load="uploadImgLoad"
            v-show="false"
          />
    
          <!-- 右侧列表区域 -->
          <div class="houseClass">
            <div
              v-for="(item, index) in nowDictlist"
              :key="item.id"
              class="fatherClass"
            >
              <div class="itemClass">
                <div>
                  <span @click="aeroItemClick">框选区域:</span>
                </div>
    
                <div>
                  <span>颜色:</span>
                  <span
                    :style="
                      'margin-left:8px;border:1px solid black;background-color:' +
                        item.color
                    "
                    >&ensp;&ensp;</span
                  >
                </div>
    
                <div>
                  <el-button
                    type="danger"
                    size="mini"
                    @click="removeHouseClick(index)"
                    >删除</el-button
                  >
                  <el-button type="primary" size="mini" @click="updateHuInfo()"
                    >提交</el-button
                  >
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    // 这里的路径之前写错了,目前已做修改
    export default {
      data() {
        return {
          imgPath: process.env.VUE_APP_BASE_API,
          maxMinStep: 20,
          msg: "图片标注拖拽测试",
          divWidth: 0,
          divHeight: 0,
          imageSrc: "http://localhost/mainPage.jpg",
          // canvas的配置部分
          c: "",
          cxt: "",
          canvasImg: "",
          //------------------------------
          imgScale: 1, // canvas 放大缩小倍数
    
          marginX: 0, // x轴偏移量, 图片专用
          marginY: 0, // x轴偏移量, 图片专用
          //------------------------------
          imgWidth: 0, // img框的宽度
          imgHeight: 0, // img框的高度
          targetMarkIndex: -1, // 目标标注index
          params: {
            currentX: 0,
            currentY: 0,
            flag: false, // 用来判断在canvas上是否有鼠标down的事件,
            editFlag: false,
            editIndex: -1,
            moveImageFlag: false,
            addHouseFlag: false,
            currentHouseIndex: -1
          },
          colorValue: undefined, // 所选中的颜色值
          //-------------------------
          // 当前正在标注的数据内容,因为只有一条,所以只定义一个
          nowDict: {
            x1: undefined,
            y1: undefined,
            x2: undefined,
            y2: undefined,
            //--------------------------
            // 拖拽需要用的标志位
            movex1: undefined,
            movey1: undefined,
            movex2: undefined,
            movey2: undefined,
            //--------------------------
            left: undefined, // x轴侧偏移量,相对于canvas上的值
            top: undefined, // y轴偏移量,相对于canvas上的值
    
            imgScale: 1, // 标注的时候放大缩小倍数
            isclick: false, // 区域是否被点击了
            // 原始图片上的位置
            originX1: undefined,
            originX2: undefined,
            originY1: undefined,
            originY2: undefined,
    
            color: undefined // 上颜色用的的
          },
          ConstKx: undefined,
          ConstKy: undefined,
          nowDictlist: [],
          detailInfor: {},
          detailVisible: false
          //------------------------------
        };
      },
      created() {},
      beforeMount() {
        this.canvasOnDraw(this.imgWidth, this.imgHeight);
      },
      mounted() {
        // 这里是进行初始化canvas的操作 myCanvas
        const self = this;
        try {
          self.c = self.$refs.refmyCanvas;
          self.canvasImg = self.$refs.refimage;
          self.cxt = self.c.getContext("2d");
    
          self.divWidth = self.$refs.imgContainer.offsetWidth;
          self.divHeight = self.$refs.imgContainer.offsetHeight;
        } catch (err) {
          console.log(err, "====");
        }
        // 渲染已经标注的矩形区域
        this.canvasOnDraw(this.imgWidth, this.imgHeight);
      },
      beforeUpdate() {
        // 渲染已经标注的矩形区域
        this.canvasOnDraw(this.imgWidth, this.imgHeight);
      },
      updated() {
        // 渲染已经标注的矩形区域
        this.canvasOnDraw(this.imgWidth, this.imgHeight);
      },
      methods: {
        removeHouseClick(index) {
          this.nowDictlist = [];
          this.nowDict = {};
          // 重新渲染界面上的元素
          this.canvasOnDraw(this.imgWidth, this.imgHeight);
        },
        aeroItemClick() {
          this.nowDict.isClick = true;
          // 执行渲染操作
          this.canvasOnDraw(this.imgWidth, this.imgHeight);
        },
        // 是否允许图片拖动
        moveImageClick() {
          this.params.moveImageFlag = !this.params.moveImageFlag;
          if (this.params.moveImageFlag) {
            this.params.editFlag = false;
            this.params.addHouseFlag = false;
          }
        },
        // 清除canvas中的内容
        cleanCanvas() {
          this.nowDictlist = [];
          // 重置标志内容
          this.nowDict = {
            x1: undefined,
            y1: undefined,
            x2: undefined,
            y2: undefined,
            //--------------------------
            // 拖拽需要用的标志位
            movex1: undefined,
            movey1: undefined,
            movex2: undefined,
            movey2: undefined,
            //--------------------------
            left: undefined, // x轴侧偏移量,相对于canvas上的值
            top: undefined, // y轴偏移量,相对于canvas上的值
    
            imgScale: 1, // 标注的时候放大缩小倍数
            isclick: false, // 区域是否被点击了
            // 原始图片上的位置
            originX1: undefined,
            originX2: undefined,
            originY1: undefined,
            originY2: undefined,
    
            color: undefined // 上颜色用的的
          };
          // 重置canvas偏移量
          this.marginX = 0;
          this.marginY = 0;
          // 重置放大缩小倍数
          this.imgScale = 1;
    
          // 重置其他按钮状态
          this.params.editFlag = false;
          this.params.moveImageFlag = false;
          this.params.addHouseFlag = false;
          this.canvasOnDraw(this.imgWidth, this.imgHeight);
        },
        // 是否允许进行标注
        ifIsEditClick() {
          // 判断一下,只允许标注一条数据,不能乱来
          if (this.targetMarkIndex >= 0) {
            this.params.editFlag = false;
            return;
          } else {
            this.params.editFlag = !this.params.editFlag;
            if (this.params.editFlag) {
              this.params.moveImageFlag = false;
            }
          }
        },
        // 由于使用体验不太好,这段代码暂时不启用
        canvasMousewheel(e) {
          var data = Math.max(-1, Math.min(1, e.wheelDelta || -e.detail));
          if (data != -1) {
            this.wheelMax();
          } else {
            this.wheelMin();
          }
        },
        // 鼠标滚轮函数
        wheelMax() {
          this.imgScale += 0.01 * this.maxMinStep;
          this.canvasOnDraw(this.imgWidth, this.imgHeight);
        },
        wheelMin() {
          this.imgScale -= 0.02 * this.maxMinStep;
          if (this.imgScale <= 1) {
            // 改进后的 非常好的体验
            this.imgScale = 1;
            this.marginX = 0;
            this.marginY = 0;
          }
          this.canvasOnDraw(this.imgWidth, this.imgHeight);
        },
        // 根据canvas值,获取原始图片上的坐标值
        getOriginWHValue(nowX, nowY, nowMoveX, nowMoveY, fuckingScale) {
          // 判断一下偏移的方向,x方向负数,代表应该加,正值代表,应该减掉
          if (nowMoveX <= 0) {
            // 应该做加号运算
            nowMoveX = Math.abs(nowMoveX);
            // 坐标值变成1倍的情况处理
            nowX = (nowX + nowMoveX) / fuckingScale;
          } else {
            // 应该做减法运算
            nowX = (nowX - nowMoveX) / fuckingScale;
          }
          // 判断一下偏移的方向,y方向,负数,应该加;正数,应该减法
          if (nowMoveY <= 0) {
            // 应该做加法运算,先取绝对值
            nowMoveY = Math.abs(nowMoveY);
            // 坐标值变成1倍的情况处理
            nowY = (nowY + nowMoveY) / fuckingScale;
          } else {
            // 这里应该做减法运算,然后处理成1倍情况
            nowY = (nowY - nowMoveY) / fuckingScale;
          }
    
          // 获取现有canvas加载的图片大小,1倍情况 [tmpW, tmpH]
          var resPointList = this.changeOldPointToNewPoint(
            this.imgWidth,
            this.imgHeight,
            this.divWidth,
            this.divHeight
          );
          // 现有图片宽度,canvas上的
          var tmpW = resPointList[0];
          var tmpH = resPointList[1];
          // canvas x * kx = 原图 x
          var kx = this.imgWidth / tmpW;
          // canvas y * ky = 原图 y
          var ky = this.imgHeight / tmpH;
          // 全局Canvas操作,后需要用,
          this.ConstCanvasW = tmpW;
          this.ConstCanvasH = tmpH;
    
          this.ConstKx = kx;
          this.ConstKy = ky;
          return [nowX * kx, nowY * ky];
        },
        // 鼠标down事件
        canvasMouseDown(e) {
          // 代表鼠标有down的动作
          this.params.flag = true;
          if (!e) {
            e = window.event;
            // 防止IE文字选中
            this.$refs.refmyCanvas.onselectstart = function() {
              return false;
            };
          }
    
          // 这里先判断一下,看是否是在有效数据,并且初始化参数, 允许编辑
          if (this.params.flag === true && this.params.editFlag === true) {
            this.nowDict.imgScale = this.imgScale;
            // 记录canvas上的xy值
            this.nowDict.x1 = e.offsetX;
            this.nowDict.y1 = e.offsetY;
            this.nowDict.x2 = e.offsetX;
            this.nowDict.y2 = e.offsetY;
    
            // 把颜色值取下来
            this.nowDict.color = "#ff000057";
    
            // 计算出原始图片上的x y 值
            var result1 = this.getOriginWHValue(
              this.nowDict.x1,
              this.nowDict.y1,
              // 偏移值
              this.marginX,
              this.marginY,
              this.imgScale
            );
            var result2 = this.getOriginWHValue(
              this.nowDict.x2,
              this.nowDict.y2,
              // 偏移值
              this.marginX,
              this.marginY,
              this.imgScale
            );
            // 把原图坐标写回字典中
            this.nowDict.originX1 = result1[0];
            this.nowDict.originY1 = result1[1];
            this.nowDict.originX2 = result2[0];
            this.nowDict.originY2 = result2[1];
    
            this.nowDictlist = [];
            this.nowDictlist.push(this.nowDict);
            // // 执行渲染操作
            this.canvasOnDraw(this.imgWidth, this.imgHeight);
          } else {
            // 允许拖拽图片
            if (this.params.moveImageFlag === true && this.params.flag === true) {
              // 首次拖拽,记住位置
              this.nowDict.movex1 = e.offsetX;
              this.nowDict.movey1 = e.offsetY;
              this.nowDict.movex2 = e.offsetX;
              this.nowDict.movey2 = e.offsetY;
              // 执行渲染操作
              this.canvasOnDraw(this.imgWidth, this.imgHeight);
            } else {
              // 这里是被点击的情况,额外进行处理,也是牛皮了
              var tmp_x = e.offsetX;
              var tmp_y = e.offsetY;
              // 这里其实也得判断一下,当前自己勾选的矩形有没有被点击
              var xx1 =
                (this.nowDict.originX1 / this.ConstKx) * this.imgScale +
                this.marginX; // 这里应该加上一个偏移值
              var yy1 =
                (this.nowDict.originY1 / this.ConstKy) * this.imgScale +
                this.marginY; // 这里应该加上一个偏移值
    
              var xx2 =
                (this.nowDict.originX2 / this.ConstKx) * this.imgScale +
                this.marginX; // 这里应该加上一个偏移值
              var yy2 =
                (this.nowDict.originY2 / this.ConstKy) * this.imgScale +
                this.marginY; // 这里应该加上一个偏移值
    
              // 调整两个点位,找出左上角顶点
              var FinalPointListNow = this.findWhichIsFirstPoint(
                xx1,
                yy1,
                xx2,
                yy2
              );
              xx1 = FinalPointListNow[0];
              yy1 = FinalPointListNow[1];
              xx2 = FinalPointListNow[2];
              yy2 = FinalPointListNow[3];
              // 说明点位在矩形之内
              if (xx1 <= tmp_x && tmp_x <= xx2) {
                if (yy1 <= tmp_y && tmp_y <= yy2) {
                  this.nowDict.isClick = true;
                  // 这里需要判断一下huid是否为空,若不为空,咋调用一下父元素的方法
                  if (this.nowDict.huId) {
                    // huid存在,调用父元素方法
                    this.fatherDetailMethod(this.nowDict.huId);
                  }
                } else {
                  this.nowDict.isClick = false;
                }
              } else {
                this.nowDict.isClick = false;
              }
              // 执行渲染操作
              this.canvasOnDraw(this.imgWidth, this.imgHeight);
            }
          }
        },
        // 鼠标移动
        canvasMouseMove(e) {
          if (e === null) {
            e = window.event;
          }
          // 这里是正在标注的情况
          if (this.params.flag === true && this.params.editFlag === true) {
            this.nowDict.x2 = e.offsetX;
            this.nowDict.y2 = e.offsetY;
    
            var result2 = this.getOriginWHValue(
              this.nowDict.x2,
              this.nowDict.y2,
              // 偏移值
              this.marginX,
              this.marginY,
              this.imgScale
            );
            // 把原图坐标写回字典中
            this.nowDict.originX2 = result2[0];
            this.nowDict.originY2 = result2[1];
            // 执行渲染操作
            this.canvasOnDraw(this.imgWidth, this.imgHeight);
          } else {
            // 这里是 允许拖拽图片
            if (this.params.moveImageFlag && this.params.flag) {
              this.marginX = e.offsetX - this.nowDict.movex1 + this.marginX;
              this.marginY = e.offsetY - this.nowDict.movey1 + this.marginY;
              this.nowDict.movex1 = e.offsetX;
              this.nowDict.movey1 = e.offsetY;
    
              // 这里其实得做个优化,不能超出边缘
              if (this.marginY < 0) {
                if (Math.abs(this.marginY) >= this.c.height * this.imgScale - 300) {
                  this.marginY = (this.c.height * this.imgScale - 300) * -1;
                }
              } else {
                // 说明是大于0 的
                if (this.marginY >= this.c.height - 300) {
                  this.marginY = this.c.height - 300;
                }
              }
    
              if (this.marginX < 0) {
                if (Math.abs(this.marginX) >= this.c.width * this.imgScale - 300) {
                  this.marginX = (this.c.width * this.imgScale - 300) * -1;
                }
              } else {
                if (this.marginX >= this.c.width - 300) {
                  this.marginX = this.c.width - 300;
                }
              }
              // 执行渲染操作
              this.canvasOnDraw(this.imgWidth, this.imgHeight);
            }
          }
        },
        // 鼠标抬起
        canvasMouseUp(e) {
          // 当正在编辑的标志位为true时,需要传回数据
          if (this.params.editFlag === true) {
            // 把数据传回父组件,保存用的
            this.$emit("mapPointJson", JSON.stringify(this.nowDict));
          }
          this.params.flag = false;
          this.params.editFlag = false;
          // 停止区域标注
          this.params.addHouseFlag = false;
          // 重新渲染界面
          this.canvasOnDraw(this.imgWidth, this.imgHeight);
        },
        // 加载图片用的
        uploadImgLoad(e) {
          try {
            this.imgWidth = e.path[0].naturalWidth;
            this.imgHeight = e.path[0].naturalHeight;
            this.canvasOnDraw(this.imgWidth, this.imgHeight);
          } catch (err) {
            console.log(err, " img==s");
          }
        },
        // 输入两个坐标值,判断哪个坐标值离左上角最近,其中特殊情况需要进行坐标查找工作
        findWhichIsFirstPoint(x1, y1, x2, y2) {
          // 首先判断x轴的距离谁更近
          if (x1 <= x2) {
            // 说明x1 比较小,接下来判断y谁更近
            if (y1 <= y2) {
              // 说明第一个坐标离得更近,直接顺序return就好
              return [x1, y1, x2, y2];
            } else {
              // 这里遇见一个奇葩问题,需要进行顶角变换
              return [x1, y2, x2, y1];
            }
          } else {
            // 这里是x1 大于 x2 的情况
            if (y2 <= y1) {
              return [x2, y2, x1, y1];
            } else {
              // y2 大于 y1 的情况, 这里需要做顶角变换工作
              return [x2, y1, x1, y2];
            }
          }
        },
        // can vas绘图部分
        canvasOnDraw(imgW = this.imgWidth, imgH = this.imgHeight) {
          const imgWidth = imgW;
          const imgHeight = imgH;
          try {
            this.divWidth = this.$refs.imgContainer.offsetWidth;
            this.divHeight = this.$refs.imgContainer.offsetHeight;
          } catch (err) {
            return;
          }
          // 清除canvas内容
          this.cxt.clearRect(0, 0, imgWidth, imgHeight);
          // 当前的图片和现有的canvas容器之前的一个关系,是否有必要,我们后续做讨论
          var resPointList = this.changeOldPointToNewPoint(
            imgWidth,
            imgHeight,
            this.divWidth,
            this.divHeight
          );
          // 这里在加载图片之类的
          this.cxt.drawImage(
            this.canvasImg,
    
            0,
            0,
            imgWidth,
            imgHeight,
    
            this.marginX, // canvas 上的 x 偏移量
            this.marginY, // canvas 上的 y坐标位置 偏移量。
    
            resPointList[0] * this.imgScale, // width, img图像放大后的宽度
            resPointList[1] * this.imgScale // height, img图像放大后的宽度
          );
          //-------------------------------------
          // 这里在画矩形框 , 坐标值变成1倍情况,然后变成现有倍数,注意,处理过程中,需要减掉偏移量
          var x1 =
            (this.nowDict.originX1 / this.ConstKx) * this.imgScale + this.marginX; // 这里应该加上一个偏移值
          var y1 =
            (this.nowDict.originY1 / this.ConstKy) * this.imgScale + this.marginY; // 这里应该加上一个偏移值
    
          var x2 =
            (this.nowDict.originX2 / this.ConstKx) * this.imgScale + this.marginX; // 这里应该加上一个偏移值
          var y2 =
            (this.nowDict.originY2 / this.ConstKy) * this.imgScale + this.marginY; // 这里应该加上一个偏移值
    
          // 2个顶点转换函数
          const FinalPointList = this.findWhichIsFirstPoint(x1, y1, x2, y2);
          // 重新配置两个顶点数据 x1变成左上角顶点,x2 变成右下角顶点
          x1 = FinalPointList[0];
          y1 = FinalPointList[1];
          x2 = FinalPointList[2];
          y2 = FinalPointList[3];
    
          var wid = x2 - x1;
          var hei = y2 - y1;
          // 绘制矩形
          this.cxt.strokeRect(x1, y1, x2 - x1, y2 - y1);
          this.cxt.font = "16px Arial";
    
          // 被点击的情况
          if (this.nowDict.isClick) {
            // 这里是在处理高亮的地方
            this.cxt.fillStyle = "rgba(255, 0, 0, 0.1)";
            // 线条颜色
            this.canvasDrowBorder("#000000", x1, y1, wid, hei);
            // 内容填充,高亮内容
            this.canvasDrowInnerColor("rgba(200, 0, 0, 0.3)", x1, y1, wid, hei);
          } else {
            // 没有被点击的情况
            this.cxt.fillStyle = this.nowDict.color
              ? this.nowDict.color
              : "#ffffff57";
            // 线条颜色
            this.canvasDrowBorder("#000000", x1, y1, wid, hei);
            // 内容填充颜色
            this.canvasDrowInnerColor(this.nowDict.color, x1, y1, wid, hei);
          }
          this.cxt.fillText("标题->", x1, parseInt(y1) - 6);
        },
        // canvas框选区域的内容颜色
        canvasDrowInnerColor(color, x, y, w, h) {
          this.cxt.fillStyle = color;
          this.cxt.fillRect(x, y, w, h);
        },
        // canvas框选区域的边框颜色
        canvasDrowBorder(color, x, y, w, h) {
          this.cxt.strokeStyle = color;
          this.cxt.strokeRect(x, y, w, h);
        },
        // 尺寸变换函数
        changeOldPointToNewPoint(imgw, imgH, canvasW, canvasH) {
          // 这里有个要求,先以宽度为准,然后再一步步调整高度
          var tmpW = canvasW;
          var tmpH = (tmpW * imgH) / imgw;
          // 如果转换之后的高度正好小于框的高度,则直接进行显示
          if (tmpH <= canvasH) {
            // 尺寸完美匹配
            return [tmpW, tmpH];
          } else {
            // 高度超出框了,需要重新调整高度部分
            tmpW = canvasW;
            tmpH = (tmpW * imgH) / imgw;
            var count = 1;
            var raise = 0.05;
            while (tmpH > canvasH || tmpW > canvasW) {
              tmpW = tmpW * (1 - raise * count);
              tmpH = (tmpW * imgH) / imgw;
            }
            return [tmpW, tmpH];
          }
        }
      }
    };
    </script>
    <style lang="scss" scoped>
    .divbody {
      width: 100%;
      height: 100%;
    }
    
    .imgContainer {
      position: relative;
      /* width: 100vw; */
      width: 90vw;
      height: 88vh;
    }
    
    .canvasClass {
      position: absolute;
      width: auto;
      height: auto;
      max-width: 100%;
      max-height: 100%;
      border: 1px solid black;
      background-color: black;
    }
    
    .imgClass {
      width: auto;
      height: auto;
      max-width: 100%;
      max-height: 100%;
    }
    
    .toolsClass {
      position: absolute;
      z-index: 999;
      padding: 8px 8px;
      background-color: #ffffffc1;
    }
    
    .houseClass {
      position: absolute;
      float: right;
      width: 200px;
      height: 100px;
      right: 0px;
      top: 200px;
      z-index: 999;
      background-color: white;
      border: 1px solid;
      overflow: auto;
    }
    
    .fatherClass {
      height: 10%;
      width: calc(100% - 16px);
      margin: 8px;
    }
    
    .itemClass {
      cursor: pointer;
      width: calc(100% - 0px);
    }
    
    .detail {
      width: 100%;
      // display: flex;
      flex-wrap: wrap;
      max-height: 650px;
      padding: 0 20px;
      .info-title {
        line-height: 30px;
        background-color: #f5f7fa;
        padding: 5px 10px;
        margin: 10px 0;
        font-weight: 700;
        span {
          display: inline-block;
          &::before {
            display: inline-block;
            content: "|";
            line-height: 18px;
            border-radius: 5px;
            width: 5px;
            background-color: #2161fb;
          }
        }
      }
      .detail_item_infor {
        // width:33%;
        display: flex;
        flex-wrap: nowrap;
        align-items: flex-start;
        padding: 8px 0;
        .title_l {
          text-align: left;
          color: #333;
          align-items: center;
          font-weight: 700;
          margin-right: 10px;
        }
        .infor {
          flex: 1;
          align-items: center;
        }
      }
      .line {
        border-bottom: 1px dashed #d9d9d9;
        margin-bottom: 10px;
        height: 2px;
        width: 100%;
      }
      .active {
        background: #f8f8f8;
      }
    }
    </style>
    
    
    展开全文
  • VUE实现前台图片 标注(添加矩形框)、放大、缩小、拖拽 需求:实现图片上自定义多个矩形选框,选框可移动缩放拖动,图片可以放大缩小、拖拽 ,选框内填充标注文字。 标注文字部位基本都会,不多赘述 看下成品 代码 ...
  • vue-input-tag, Vue.js 2.0输入标记组件 vue-input-tagVue.js 2.0输入标记组件在反应tagsinput中激发。 安装 NPMnpm install vue-input-tag --saveregiste
  • 图像标注需求打点式实现思路矩形框实现思路右键弹出删除和添加备注菜单删除选中内容更新备注 项目中碰到一个需要对图像标注的功能,查了好几个插件,但是感觉用起来有点颇为复杂而且和自己需要的功能不完全一致,...
  • 最近和深度学习方面搞个东西,需要前端对图片进行标注,呃,这个我上网也没找到什么好方法,只能自己通过js来实现,不过现在刚初步做出一点效果,样式没留意,挺丑的,嘻嘻。 功能: 点击开始标注按钮后才可以对...
  • PhotoCommenter是一款简单易用的图片标注软件。内置丰富的标注库、图形库。可以方便快捷地对图片添加标注。 详细信息可查看:http://www.laaan.cn/?page_id=1106
  • vue+div.canvas图像标注功能实现

    千次阅读 热门讨论 2019-09-15 22:24:53
    import Vue from 'vue' import 'vueui-widgets/dist/index.css' import VueUI from 'vueui-widgets' import App from './App.vue' import router from './router' import store from './store' Vue.use(...
  • 重置画布和撤销上一步 // 重置 resetMap () { // 标注的信息都放在这个数组中 this.posArray = [] history = [history[0]] var mycanvas = document.getElementById('mycanvas') var ctx = mycanvas.getContext('2d'...
  • 使用指南 操作说明 设置Vmarker的渲染以及操作 加载图片 通过控件的imgUrl属性,可以加载url或base64的图片。...ui-marker ref="aiPanel-editor" class="ai-observer" ...而当图片加载完成后,vmarker会发送一个vmark
  • vue+svg.js 实现图像标记vue+svg.js 实现图像标记安装svg.js及相关插件在vue项目中引入创建画布画图/做标记清空画布清空resize删除某一子节点全局快捷键的监听 vue+svg.js 实现图像标记 本文讲的是如何使用svg.js...
  • html5-canvas-web-图片涂鸦
  • 一、网址以及demo 官方文档:http://ailabel.com.cn/public/ailabel/api/index.html#1 demo地址1: ...二、我的实例代码(vue环境下) 三、如有不懂的添加官方qq群向大佬请教(378301400) <tem
  • 本人也是第一次使用这个组件,因为项目需要使用标注图片功能,我就在网上看到了这个组件。然后研究了一下希望能给大家提供一些帮助,废话不多说,直接上代码。 1.下载组件 npm i ailabel 2.下载完成后vue页面标签...
  • 基于element框架,添加videojs-markers打点标记插件的demo
  • query + coorp.js 实现图片标注插件,实现对图片标注,获取标注图片的区域坐标及大小。query + coorp.js 实现图片标注插件,实现对图片标注,获取标注图片的区域坐标及大小。
  • vue实现在图片中画矩形框(JS图片画方块定位坐标),并得到对角坐标,并拿到对应坐标制作图片热区并设置map中area的样式(图片热区map area的用法)
  • 参考链接:https://blog.csdn.net/m0_46627730/article/details/106583908?utm_term=vue%E5%9B%BE%E7%89%87%E7%82%B9%E5%87%BB%E4%BD%8D%E7%BD%AE&utm_medium=distribute.pc_aggpage_search_result.none-task-...
  • vue修改html图标,tilte左边的图标(以图片格式作为图标)
  • 图片储存在后台中,根据图片的地址,在vue页面中,查看图片,并根据坐标标注指定区域。 由于浏览器的机制,使用 [removed].href 下载图片时,并不会保存到本地,会在浏览器打开。 2 实现原理 2.1 绘制画布 查看图片...
  • vue图片叠加画矩形

    2021-04-19 10:25:07
    vue图片叠加画矩形 效果图: 容器16:9 容器9:19 重难点在css布局上面,写不好布局,就会有很多问题。 比如:容器是16:9, 图片就需要宽度占满100%;如果容器9:16,那么高度就需要占满100%。 需要做的还有获取图片...
  • vue中百度地图标注

    2021-11-26 16:51:32
    npm install vue-baidu-map --save // main.js中引用,注册全局组件 import BaiduMap from 'vue-baidu-map' Vue.use(BaiduMap, { ak: '3mn83YpfI9VdPSZg1DfuK7lg0UqRljwt' }) //直接调用组件使用即可 <...
  • 需求:实现图片上自定义多个矩形选框,选框可移动缩放拖动,图片可以放大缩小、拖拽 ,选框内填充标注文字。 框内填充文字基本都会,不多赘述,后期可添加选框移除功能(思路右击选框弹出选项删除,获取当前点击选框...
  • vue-fabric purestart vue-fabric. Installation npm install vue-fabric --save 依赖 下载.和 .到本地 static/js/文件下 本地项目 index.html 引入 [removed][removed] [removed][removed] Import 在main.js中 ...
  • vue div标记点 https://www.jb51.net/article/198377.htm

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,269
精华内容 907
关键字:

vue图片标注

vue 订阅