最近工作中遇到一个需要,须要在一个图片上,手动框选一些区域,而后生成绝对于图片区域的坐标。
于是想到之前用到过的一个裁剪图片的插件vue-cropper
,一样是框选图片区域,于是学习了一下它的源码,并在此基础上本人实现了一个框选图片区域生成坐标的组件。
实现性能:
- 鼠标按下拖拽生成框选,不限度个数
- 可调整框选大小
- 可挪动框选地位
- 可缩放
- 可独自管制每个框选是否能够编辑
先来看看成果:
在实现性能之前咱们要晓得一些属性的区别:
clientX
、clientY
示意鼠标绝对于以后窗口的坐标,所以咱们每次创立框选/扭转框选大小/挪动框选之前,都要先记录一下过后的clientX,clientY,而后在挪动鼠标时用当初的clientX,clientY和之前记录的去比照,计算出鼠标挪动的间隔,以此来扭转框选的宽度和地位,上面代码中的fw
,fh
就是用来记录这个的。
offsetX
、offsetY
是鼠标绝对于绑定事件的那个元素,在这个组件中,绑定事件的是最外层的div元素,所以用这两个值,在创立框的时候来确定框选左上角的地位。
创立框选的步骤:
- 按下鼠标,触发mousedown事件,通过offsetX、offsetY来确定框选左上角在组件内的地位并记录,绑定mousemove,mouseup事件。
- 挪动鼠标调整框选大小,触发mousemove事件,计算出新框选的左上角坐标和宽高。
- 鼠标抬起,触发mouseup事件,移除刚刚绑定的mousemove,mouseup事件。
每个框选有4个属性 cropX
、cropY
、cropW
、cropH
来记录框选的左上角坐标以及宽高,并有四个对应的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>
结尾
我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~