最近工作中遇到一个需要,须要在一个图片上,手动框选一些区域,而后生成绝对于图片区域的坐标。

于是想到之前用到过的一个裁剪图片的插件vue-cropper,一样是框选图片区域,于是学习了一下它的源码,并在此基础上本人实现了一个框选图片区域生成坐标的组件。

实现性能:

  1. 鼠标按下拖拽生成框选,不限度个数
  2. 可调整框选大小
  3. 可挪动框选地位
  4. 可缩放
  5. 可独自管制每个框选是否能够编辑

先来看看成果:


在实现性能之前咱们要晓得一些属性的区别:

clientXclientY 示意鼠标绝对于以后窗口的坐标,所以咱们每次创立框选/扭转框选大小/挪动框选之前,都要先记录一下过后的clientX,clientY,而后在挪动鼠标时用当初的clientX,clientY和之前记录的去比照,计算出鼠标挪动的间隔,以此来扭转框选的宽度和地位,上面代码中的fwfh就是用来记录这个的。

offsetXoffsetY 是鼠标绝对于绑定事件的那个元素,在这个组件中,绑定事件的是最外层的div元素,所以用这两个值,在创立框的时候来确定框选左上角的地位。

创立框选的步骤:

  1. 按下鼠标,触发mousedown事件,通过offsetX、offsetY来确定框选左上角在组件内的地位并记录,绑定mousemove,mouseup事件。
  2. 挪动鼠标调整框选大小,触发mousemove事件,计算出新框选的左上角坐标和宽高。
  3. 鼠标抬起,触发mouseup事件,移除刚刚绑定的mousemove,mouseup事件。

每个框选有4个属性 cropXcropYcropWcropH 来记录框选的左上角坐标以及宽高,并有四个对应的old属性来记录旧坐标,记录旧坐标的目标是为了计算新坐标,栗子:

咱们将框选拖大,如上图,那么新框选的左上角没变
cropW(新框选长度) = oldCropW + fw
cropH(新框选高度) = oldCrop + fh

再举个栗子,咱们拖动框选右下角,把他拖到上方

由上图所知:

  • cropW = fw - oldCropW
  • cropH = fh - oldCropH
  • cropX = oldCropX - cropW
  • cropY = oldCropY - cropH

扭转框大小地位的计算逻辑大略就是这样,当然在计算坐标时还须要判断一下,不能让框选超出了范畴。

代码如下:

<template>  <div      class="cropper-container"      @mousedown.prevent="startMove"  >    <img        :src="url"        :style="{          'width': currentWidth + 'px',          'height': currentHeight + 'px'        }"        alt="背景图片"    >    <div        v-for="(item,index) in list"        :key="item.id"        class="crop-box"        :style="{        'width': item.cropW + 'px',        'height': item.cropH + 'px',        'transform': 'translate3d('+ item.cropX + 'px,' + item.cropY + 'px,' + '0)'      }"    >      <span          class="cropper-face"          @mousedown.prevent="cropMove($event,item)"          @contextmenu.prevent="deleteCrop(index)"      />      <span v-show="item.canEdit">        <span            v-for="line in lineList"            :key="line"            :class="[`line-${line}`, 'crop-line']"            @mousedown.prevent="changeCropSize($event, item, line)"        />        <span            v-for="point in pointList"            :key="point.index"            :class="[`point${point.index}`, 'crop-point']"            @mousedown.prevent="changeCropSize($event, item, point.position)"        />      </span>    </div>  </div></template><script>export default {  name: "cropper",  model: {    prop: 'list'  },  props: {    url: {      type: String,      required: true    },    disabled: {      type: Boolean,      default: false    },    list: {      type: Array,      default: function () {        return [];      }    },    scale: {      type: Number,      default: 60    },    padding: { // 边框平安间隔      type: Number,      default: 1    }  },  data() {    return {      // 框的8个操作点list,左上 | 上 | 右上 | 右 | 右下 | 下 | 左下 | 左      pointList: [        {index: 1, position: ['left', 'top']},        {index: 2, position: [null, 'top']},        {index: 3, position: ['right', 'top']},        {index: 4, position: ['right', null]},        {index: 5, position: ['right', 'bottom']},        {index: 6, position: [null, 'bottom']},        {index: 7, position: ['left', 'bottom']},        {index: 8, position: ['left', null]},      ],      lineList: ['left', 'top', 'right', 'bottom'], // 框的4条线      trueWidth: 0, // 图片理论宽度      trueHeight: 0, // 图片理论高度      currentWidth: 0, // 图片以后宽度      currentHeight: 0, // 图片以后高度      tempCrop: {},      changePosition: [], // 更改边的地位 ['left', 'top] 改上左两条边      cropClientX: 0,      cropClientY: 0,    }  },  methods: {    initImage(item) {      let image = new Image();      image.onload = () => {        // 图片以后宽高        this.currentWidth = image.width * this.scale / 100;        this.currentHeight = image.height * this.scale / 100;        // 图片实在宽高        this.trueWidth = image.width;        this.trueHeight = image.height;        this.$emit('img', {          width: image.width,          height: image.height        })      };      image.src = item;    },    startMove(e) {      if (!this.disabled) {        let item = {          id: this.guid(),          cropW: 0,          cropH: 0,          cropX: e.offsetX,          cropY: e.offsetY,          // 保留老坐标          oldCropW: 0,          oldCropH: 0,          oldCropX: e.offsetX,          oldCropY: e.offsetY,          canEdit: true // 是否能够编辑        };        this.tempCrop = item;        this.list.push(item);        this.cropClientX = e.clientX;        this.cropClientY = e.clientY;        // 绑定截图事件        window.addEventListener("mousemove", this.createCrop);        window.addEventListener("mouseup", this.endCrop);      }    },    // 创立剪裁框    createCrop(e) {      let nowX = e.clientX, nowY = e.clientY, item = this.tempCrop;      let fw = nowX - this.cropClientX, fh = nowY - this.cropClientY;      if (fw >= 0) {        item.cropW = Math.min(this.currentWidth - item.oldCropX - 2 * this.padding, fw);        item.cropX = item.oldCropX;      } else {        item.cropW = Math.min(item.oldCropX - 2 * this.padding, Math.abs(fw));        item.cropX = Math.max(item.oldCropX + fw, this.padding);      }      if (fh >= 0) {        item.cropH = Math.min(this.currentHeight - item.oldCropY - 2 * this.padding, fh);        item.cropY = item.oldCropY;      } else {        item.cropH = Math.min(item.oldCropY - 2 * this.padding, Math.abs(fh));        item.cropY = Math.max(item.oldCropY + fh, this.padding);      }      if (item.cropW > 10 && item.cropH > 10) {        item.temp = false;      }    },    // 创立实现    endCrop() {      window.removeEventListener("mousemove", this.createCrop);      window.removeEventListener("mouseup", this.endCrop);      this.tempCrop = {};    },    // 截图挪动    cropMove(e, item) {      if (!this.disabled && item.canEdit) {        this.cropClientX = e.clientX;        this.cropClientY = e.clientY;        item.oldCropW = item.cropW;        item.oldCropH = item.cropH;        item.oldCropX = item.cropX;        item.oldCropY = item.cropY;        this.tempCrop = item;        window.addEventListener("mousemove", this.moveCrop);        window.addEventListener("mouseup", this.leaveCrop);      }    },    // 截图挪动中    moveCrop(e) {      e.preventDefault();      let nowX = e.clientX, nowY = e.clientY, item = this.tempCrop;      let fw = nowX - this.cropClientX, fh = nowY - this.cropClientY;      item.cropX = Math.min(Math.max(item.oldCropX + fw, this.padding), this.currentWidth - item.cropW - 2 * this.padding);      item.cropY = Math.min(Math.max(item.oldCropY + fh, this.padding), this.currentHeight - item.cropH - 2 * this.padding);    },    // 截图挪动完结    leaveCrop() {      window.removeEventListener("mousemove", this.moveCrop);      window.removeEventListener("mouseup", this.leaveCrop);    },    // 删除框选    deleteCrop(index) {      if (!this.disabled) {        this.list.splice(index, 1);      }    },    // 扭转截图框大小    changeCropSize(e, item, position) {      if (!this.disabled && item.canEdit) {        window.addEventListener("mousemove", this.changeCropNow);        window.addEventListener("mouseup", this.changeCropEnd);        this.cropClientX = e.clientX;        this.cropClientY = e.clientY;        item.oldCropW = item.cropW;        item.oldCropH = item.cropH;        item.oldCropX = item.cropX;        item.oldCropY = item.cropY;        this.changePosition = position;        this.tempCrop = item;      }    },    // 正在扭转大小    changeCropNow(e) {      e.preventDefault();      let nowX = e.clientX, nowY = e.clientY, item = this.tempCrop, position = this.changePosition;      let fw = nowX - this.cropClientX, fh = nowY - this.cropClientY;      if (position.indexOf('left') > -1) { // 拖动的边中蕴含右边        if (item.oldCropW - fw >= 0) {          item.cropW = Math.min(item.oldCropW - fw, item.oldCropX + item.oldCropW);          item.cropX = Math.max(this.padding, item.oldCropX + fw);        } else {          item.cropW = Math.min(fw - item.oldCropW, this.currentWidth - item.oldCropX - item.oldCropW - 2 * this.padding);          item.cropX = item.oldCropX + item.oldCropW;        }      } else if (position.indexOf('right') > -1) { // 拖动的边中蕴含左边        if (item.oldCropW + fw >= 0) {          item.cropW = Math.min(this.currentWidth - item.cropX - 2 * this.padding, item.oldCropW + fw);          item.cropX = item.oldCropX;        } else {          item.cropW = Math.min(Math.abs(fw + item.oldCropW), item.oldCropX - 2 * this.padding);          item.cropX = Math.max(this.padding, item.oldCropX - item.cropW);        }      }      if (position.indexOf('top') > -1) { // 拖动的边中蕴含上边        if (item.oldCropH - fh > 0) { // 上方          item.cropH = Math.min(item.oldCropH - fh, item.oldCropH + item.oldCropY - 2 * this.padding);          item.cropY = Math.max(this.padding, item.oldCropY + fh);        } else { // 下方          item.cropH = Math.min(fh - item.oldCropH, this.currentHeight - item.oldCropY - item.oldCropH);          item.cropY = item.oldCropY + item.oldCropH;        }      } else if (position.indexOf('bottom') > -1) { // 拖动的边中蕴含下边        if (item.oldCropH + fh > 0) { // 下方          item.cropH = Math.min(this.currentHeight - item.cropY - 2 * this.padding, item.oldCropH + fh);          item.cropY = item.oldCropY;        } else { // 上方          item.cropH = Math.min(Math.abs(fh + item.oldCropH), item.oldCropY - 2 * this.padding);          item.cropY = Math.max(this.padding, item.oldCropY + item.oldCropH + fh);        }      }    },    // 完结扭转大小    changeCropEnd() {      window.removeEventListener("mousemove", this.changeCropNow);      window.removeEventListener("mouseup", this.changeCropEnd);    },    guid() {      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {        let r = Math.random() * 16 | 0,            v = c === 'x' ? r : (r & 0x3 | 0x8);        return v.toString(16);      });    },    changeCropScale(val, oldVal) {      let scale = val / oldVal;      this.list.forEach(item => {        item.cropX = item.cropX * scale;        item.cropY = item.cropY * scale;        item.cropW = item.cropW * scale;        item.cropH = item.cropH * scale;      });    },  },  watch: {    url: {      handler(val) {        if (val) {          this.initImage(val)        }      },      immediate: true    },    scale(val, oldVal) {      if (this.url) {        this.currentWidth = this.trueWidth * val / 100;        this.currentHeight = this.trueHeight * val / 100;        this.changeCropScale(val, oldVal);      }    }  }}</script><style scoped lang="scss">.cropper-container {  position: relative;  width: 100%;  height: 100%;  box-sizing: border-box;  user-select: none;  -webkit-user-select: none;  -moz-user-select: none;  -ms-user-select: none;  cursor: crosshair;}.crop-box,.cropper-face {  position: absolute;  top: 0;  right: 0;  bottom: 0;  left: 0;  user-select: none;  box-sizing: border-box;}.crop-box {  border: 1px solid #39f;}.cropper-face {  top: 0;  left: 0;  cursor: move;}.crop-line {  position: absolute;  display: block;  width: 100%;  height: 100%;}.line-top {  top: -3px;  left: 0;  height: 5px;  cursor: row-resize;}.line-left {  top: 0;  left: -3px;  width: 5px;  cursor: col-resize;}.line-bottom {  bottom: -3px;  left: 0;  height: 5px;  cursor: row-resize;}.line-right {  top: 0;  right: -3px;  width: 5px;  cursor: col-resize;}.crop-point {  position: absolute;  width: 7px;  height: 7px;  opacity: .75;  background-color: #39f;  border-radius: 100%;}.point1 {  top: -4px;  left: -4px;  cursor: nwse-resize;}.point2 {  top: -4px;  left: 50%;  transform: translateX(-50%);  cursor: row-resize;}.point3 {  top: -4px;  right: -4px;  cursor: nesw-resize;}.point4 {  top: 50%;  right: -4px;  transform: translateY(-50%);  cursor: col-resize;}.point5 {  bottom: -4px;  right: -4px;  cursor: nwse-resize;}.point6 {  bottom: -4px;  left: 50%;  transform: translateX(-50%);  cursor: row-resize;}.point7 {  bottom: -4px;  left: -4px;  cursor: nesw-resize;}.point8 {  top: 50%;  left: -4px;  transform: translateY(-50%);  cursor: col-resize;}</style>

结尾

我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~