我正在参加「编程·启航计划」
背景
针对图片某区域内容进行矩形/多边形框选及标注,单张图片支持多区域标注。同时进行数据记录包含位置信息、标签信息、原始图片信息。
主要功能交互
- 框选: 鼠标在图片区域时,按住左键进行拖动时代表正在框选位置。
- 预置标签: 框选完毕后弹出预置标签的标签框,选择完毕后隐藏标签框,同时在标签区域生成标签框。
- 可删除: 框选完毕后位置左上角出现“X”按钮,此按钮代表可进行删除此标签框。
- 拖动更改: 区域标注框可进行拖动及更改大小,且位置信息同步
- 区域标识:区域右上角需要有序号标识,点击同步高亮
- 区域标签联动: 图片标签更改或者删除与框选区域联动。
- 图片放大:需要支持图片放大进行框选,区域回显
实现过程
1.分析
事件:鼠标按下@mousedown、鼠标拖动@mousemove、鼠标弹起@mouseup
原理:在图片上方增加一个div作为画布。区域框选,就是在画布上创建div,通过position: absolute;
确定元素的位置left、top、width、height。拖拽、放大缩小、删除都是对div操作
实现:
- 定义 数组detailImgOption,
- 创建 鼠标按下到鼠标弹起表示创建完成,就往数组中添加一条数据
- 修改 计算鼠标的移动位置,然后赋值给
- 删除 删除就detailImgOption中splice删除一条数据。
关键:只需要记录div的(x,y),(ex,ey)坐标,就可以计算出div的left、top、width、height
2.具体代码
分为两个部分,html部分和事件处理部分
html部分
<div class="imageLabel-img-boxs">
<!-- 图片 -->
<img ref="Img" :src="picUrl" alt="">
<!-- 区域 -->
<div id="regionImg"
ref="regionImg"
class="imageLabel-content"
@mousedown="IMGmousedown"
@mousemove="IMGmousemove"
@mouseup="IMGmouseup">
<!-- 框选区域 -->
<div v-for="(n,i) in detailImgOption" :key="i"
:id="n.id"
class="imageLabel-imgdrop"
:class="{'imageLabel-imgdrop-active':moveDropId===n.id}"
:style="{'z-index':n.zindex,left:n.x+'px',top:n.y+'px',width: Math.abs(n.ex - n.x)+'px',height:Math.abs(n.ey - n.y)+'px',}"
@mousedown.self="dropMousedown(n.id,$event)"
@mousemove.self="dropMousemove"
@mouseup="dropMouseup">
<!-- 拖拽的点 -->
<i v-for="(label,k) in 8" :key="k"
class="imageLable-i"
@mousedown="labelMousedown(n.id,$event)"
@mousemove="labelMousemove(k,$event)"
@mouseup="labelMouseup">
</i>
<!-- 标签order标识 -->
<span class="region-img-label-order region-order">{{i+1}}</span>
</div>
</div>
</div>
事件处理部分
IMGmousedown (e) {
if (this.flag_target || this.flag_label || this.flag_feature) {
return
}
if (e.buttons === 1) {
// offsetX/Y 只读属性提供该事件与目标节点的填充边缘之间鼠标指针的X/Y坐标中的偏移量
// pageX/Y 只读属性返回事件相对于整个文档的水平/垂直坐标
this.obj = {
x: (e.pageX - this.imgLeft),
y: (e.pageY - this.imgTop)
}
this.flag = true
if (this.detailImgOption.length) {
this.detailImgOption.forEach(n => {
n.zindex = 0
})
}
this.detailImgOption.push({
id: `regin-div${this.regionCount}`,
gradeValue: [],
x: this.obj.x,
y: this.obj.y,
ex: 0,
ey: 0,
zindex: 100
})
}
},
IMGmousemove (e) {
let offsetX = (e.pageX - this.imgLeft)
let offsetY = (e.pageY - this.imgTop)
if (this.flag) {
let item = this.detailImgOption.find(n => n.id === `regin-div${this.regionCount}`)
item.x = this.obj.x > offsetX ? offsetX : this.obj.x
item.y = this.obj.y > offsetY ? offsetY : this.obj.y
item.ex = this.obj.x > offsetX ? this.obj.x : offsetX
item.ey = this.obj.y > offsetY ? this.obj.y : offsetY
}
},
IMGmouseup (e) {
this.flag = false
this.flag_target = false
this.flag_label = false
},
dropMousedown (id, e) {
this.flag_target = true
this.moveDropId = id
let item = this.detailImgOption.find(n => n.id === this.moveDropId)
this.obj = {
x: e.offsetX,
y: e.offsetY,
pageX: e.pageX,
pageY: e.pageY,
itemx: item.x,
itemy: item.y,
itemex: item.ex,
itemey: item.ey
}
if (this.detailImgOption.length) {
this.detailImgOption.forEach(n => {
n.zindex = n.id === id ? 100 : 0
})
}
},
dropMousemove (e) {
if (this.flag_target && e.target.id === this.moveDropId) {
let item = this.detailImgOption.find(n => n.id === this.moveDropId)
// 边界优化2 开始
// (e.pageX - this.obj.pageX) < 0 表示向左移动,可移动的距离范围:点击下去时候元素的x坐标
if ((e.pageX - this.obj.pageX) < 0 && Math.abs(e.pageX - this.obj.pageX) > this.obj.itemx) {
return
}
// (e.pageX - this.obj.pageX) > 0 表示向右移动,可移动的距离范围:图片长度-点击下去时候元素的ex坐标
if ((e.pageX - this.obj.pageX) > 0 && Math.abs(e.pageX - this.obj.pageX) > (this.imgWidth - this.obj.itemex)) {
return
}
// (e.pageY - this.obj.pageY) < 0 表示向上移动,可移动的距离范围:点击下去时候元素的y坐标
if ((e.pageY - this.obj.pageY) < 0 && Math.abs(e.pageY - this.obj.pageY) > this.obj.itemy) {
return
}
// (e.pageY - this.obj.pageY) > 0 表示向下移动,可移动的距离范围:图片高度-点击下去时候元素的ey坐标
if ((e.pageY - this.obj.pageY) > 0 && Math.abs(e.pageY - this.obj.pageY) > (this.imgHeight - this.obj.itemey)) {
// console.log('4', 4)
return
}
// 边界优化2 结束
item.x += (e.offsetX - this.obj.x)
item.y += (e.offsetY - this.obj.y)
item.ex += (e.offsetX - this.obj.x)
item.ey += (e.offsetY - this.obj.y)
}
},
dropMouseup (e) {
this.flag_target = false
this.flag_label = false
},
removeDrop (i) {
this.detailImgOption.splice(i, 1)
},
labelMousedown (id, e) {
e.preventDefault()
this.flag_label = true
this.obj = {
pageX: (e.pageX - this.imgLeft),
pageY: (e.pageY - this.imgTop),
x: e.offsetX,
y: e.offsetY
}
this.moveDropId = id
if (this.detailImgOption.length) {
this.detailImgOption.forEach(n => {
n.zindex = n.id === id ? 100 : 0
})
}
},
labelMousemove (k, e) {
if (this.flag_label) {
let item = this.detailImgOption.find(n => n.id === this.moveDropId)
this.k = k
// 左上角
if (k === 0) {
item.x += (e.offsetX - this.obj.x)
item.y += (e.offsetY - this.obj.y)
}
// 右上角
if (k === 1) {
item.y += (e.offsetY - this.obj.y)
item.ex += (e.offsetX - this.obj.x)
}
// 右下角
if (k === 2) {
item.ex += (e.offsetX - this.obj.x)
item.ey += (e.offsetY - this.obj.y)
}
// 左下角
if (k === 3) {
item.x += (e.offsetX - this.obj.x)
item.ey += (e.offsetY - this.obj.y)
}
// 上中角
if (k === 4) {
item.y += (e.offsetY - this.obj.y)
}
// 右中角
if (k === 5) {
item.ex += (e.offsetX - this.obj.x)
}
// 下中角
if (k === 6) {
item.ey += (e.offsetY - this.obj.y)
}
// 左中角
if (k === 7) {
item.x += (e.offsetX - this.obj.x)
}
}
},
labelMouseup (e) {
this.flag_label = false
this.flag_target = false
}
过程中遇到问题
- 层级重叠覆盖问题:需要给元素设置层级z-index。
- 移动的边界问题:需要对元素进行边界判断处理
- 事件捕获、冒泡问题使用.self,如@mousedown.self="dropMousedown(n.id,$event)"