共计 8892 个字符,预计需要花费 23 分钟才能阅读完成。
最近工作中遇到一个需要,须要在一个图片上,手动框选一些区域,而后生成绝对于图片区域的坐标。
于是想到之前用到过的一个裁剪图片的插件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>
结尾
我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~