实现效果:
在图片中拖拽出矩形区域并获取对角坐标
绘制矩形:
html部分代码
<!-- 绘制矩形区域 -->
<div id="content">
<div id="imgContent">
<img id="bigImg" src="../assets/logo.png" width="200" height="200" />
<canvas id="canvas" ref="markCanvasRef"></canvas>
</div>
<el-button type="primary" style="margin: 20px" @click="saveData">保存数据</el-button>
</div>
js部分代码
// 标记内容数组
const markList = ref([]);
//画布初始化
const initCanvas = () => {
// 使用setTimeout延迟执行,确保在DOM渲染完成后再执行
setTimeout(() => {
// 使用nextTick确保在Vue渲染周期的下一个tick执行
nextTick(() => {
// 初始化canvas宽高
let cav = markCanvasRef.value;
cav.width = document.getElementById("imgContent").offsetWidth;
cav.height = document.getElementById("imgContent").offsetHeight;
console.log(cav);
let ctx = cav.getContext("2d");
ctx.strokeStyle = "blue"; // 设置矩形边框颜色
cav.style.cursor = "crosshair"; // 设置鼠标悬停样式为十字光标
// 获取画框数据集合
let list = markList.value;
// 若list长度不为0, 则显示已标记框
if (list.length !== 0) {
list.forEach(function (value, index, array) {
// 遍历绘制所有标记框
ctx.rect(value.x, value.y, value.w, value.h);
ctx.stroke();
});
}
console.log("cav ,list ", cav, list);
// 调用封装的绘制方法
draw(cav, list);
});
}, 1000); // 1秒延迟,确保DOM渲染完成后再执行初始化
};
// 保存数据函数
const saveData = () => {
console.log("矩形数据", markList.value);
};
在js中引入Draw.js文件
/**
* 画布中绘制矩形
* 参数: cav-画布对象 list-矩形数组 i-选中矩形下标
**/
/* 操作执行方法分发 */
export function draw(cav, list, i) {
// 画布初始化
let ctx = cav.getContext('2d');
// 设置矩形边框颜色和线宽
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
console.log('画布初始化', cav, list, i);
// 初始化鼠标坐标
let sX = 0; // 鼠标X坐标
let sY = 0; // 鼠标Y坐标
/*
*鼠标移动进行第一层判断, 区分情况: 无矩形, 已有矩形无选中, 已有选中矩形
*/
cav.onmousemove = function (em) {
sX = em.offsetX;
sY = em.offsetY;
let iem = undefined; // 鼠标移动时临时存储当前鼠标所在矩形的下标
if (list.length === 0) { // **** 无矩形 ****
// 绘制新矩形
console.log('无矩形,触发newDraw,绘制新矩形');
newDraw(cav, ctx, list);
} else if (i === undefined) { // **** 已有矩形无选中 ****
// 判断鼠标位置
console.log('已有矩形无选中');
// 判断鼠标位置并绘制
list.forEach(function (value, index, array) {
// 这个条件判断鼠标是否位于一个宽高均为正的矩形框的右下方。如果是,则设置 iem 为当前矩形框的索引,并调用 judgeDraw() 方法进行后续操作。
if (value.w > 0 && value.h > 0 && sX > value.x && sX < value.x + value.w && sY > value.y && sY < value.y + value.h) {
// 鼠标在右下方向生成的矩形中
iem = index;
judgeDraw(cav, ctx, list, iem);
}
if (value.w < 0 && value.h > 0 && sX < value.x && sX > value.x + value.w && sY > value.y && sY < value.y + value.h) {
// 鼠标在左下方向生成的矩形中
iem = index;
judgeDraw(cav, ctx, list, iem);
}
if (value.w > 0 && value.h < 0 && sX > value.x && sX < value.x + value.w && sY < value.y && sY > value.y + value.h) {
// 鼠标在右上方向生成的矩形中
iem = index;
judgeDraw(cav, ctx, list, iem);
}
if (value.w < 0 && value.h < 0 && sX < value.x && sX > value.x + value.w && sY < value.y && sY > value.y + value.h) {
// 鼠标在左上方向生成的矩形中
iem = index;
judgeDraw(cav, ctx, list, iem);
}
// 如果以上所有条件都不满足,则意味着鼠标不在任何一个矩形框内,此时调用 newDraw() 方法。
if (iem === undefined) {
// 鼠标不在矩形中
newDraw(cav, ctx, list);
}
})
} else { // **** 已有选中矩形 ****
console.log('已有选中矩形');
// 判断鼠标位置
for (let index = 0; index < list.length; index++) {
let value = list[index];
// 判断鼠标位置并绘制
// 如果鼠标在矩形的四个角上,表示可能需要改变矩形的大小或位置。因此,通过判断鼠标位置,确定鼠标所在的角,并根据角的位置信息调整矩形。
// 如果鼠标在矩形的边上,表示可能需要调整矩形的大小或者移动整个矩形。根据鼠标位置判断是水平边还是垂直边,并确定是左上、右上、左下还是右下角,以便进行相应的操作。
// 如果鼠标在矩形的内部,表示可能需要移动整个矩形。这时,根据鼠标位置确定是哪个矩形,以便移动对应的矩形。
// 如果以上所有条件都不满足,则表示鼠标不在任何矩形中,可能需要绘制新的矩形或者执行其他操作。
if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + 5 && sY > value.y - 5) {
// *** 鼠标在起点角 ***
if (index === i) {
changeDraw(cav, ctx, list, i, 1);
break;
}
} else if (sX < value.x + value.w + 5 && sX > value.x + value.w - 5 && sY < value.y + 5 && sY > value.y - 5) {
// *** 鼠标在起点横向角 ***
if (index === i) {
changeDraw(cav, ctx, list, i, 2);
break;
}
} else if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + value.h + 5 && sY > value.y + value.h - 5) {
// *** 鼠标在起点纵向角 ***
if (index === i) {
changeDraw(cav, ctx, list, i, 3);
break;
}
} else if (sX < value.x + value.w + 5 && sX > value.x + value.w - 5 && sY < value.y + value.h + 5 && sY > value.y + value.h - 5) {
// *** 鼠标在终点角 ***
if (index === i) {
changeDraw(cav, ctx, list, i, 4);
break;
}
} else if (value.w > 0 && value.h > 0 && sX > value.x && sX < value.x + value.w && sY > value.y && sY < value.y + value.h) {
// *** 鼠标在右下方向生成的矩形中 ***
iem = index
judgeDraw(cav, ctx, list, index);
break;
} else if (value.w < 0 && value.h > 0 && sX < value.x && sX > value.x + value.w && sY > value.y && sY < value.y + value.h) {
// *** 鼠标在左下方向生成的矩形中 ***
iem = index
judgeDraw(cav, ctx, list, index);
break;
} else if (value.w > 0 && value.h < 0 && sX > value.x && sX < value.x + value.w && sY < value.y && sY > value.y + value.h) {
// *** 鼠标在右上方向生成的矩形中 ***
iem = index
judgeDraw(cav, ctx, list, index);
break;
} else if (value.w < 0 && value.h < 0 && sX < value.x && sX > value.x + value.w && sY < value.y && sY > value.y + value.h) {
// *** 鼠标在左上方向生成的矩形中 ***
iem = index
judgeDraw(cav, ctx, list, index);
break;
} else {
if (iem === undefined) {
// *** 鼠标不在矩形中 ***
newDraw(cav, ctx, list);
}
}
}
}
/* 鼠标移出画布区域时保存选中矩形下标(如有) */
cav.onmouseout = function (eo) {
if (i !== undefined) {
// 初始化
draw(cav, list, i);
}
};
}
// console.log(cav, list, i);
return list;
}
/* 编辑矩形四个角 */
function changeDraw(cav, ctx, list, i, site) {
// 将鼠标样式改变为指针形状
cav.style.cursor = 'pointer'
console.log('编辑矩形四个角');
// site: 操作矩形角的位置, 1-起点 2-起点横向 3-起点纵向 4-终点
let mark = list[i];
/* 按下鼠标左键 */
cav.onmousedown = function (ed) {
console.log('按下鼠标左键');
// 保存鼠标落下位置的X, Y坐标, firefox中鼠标移动后ed.offsetX ed.offsetY会变成 0, 需要使用临时参数存储起来
let sX = ed.offsetX; // 起点X坐标
let sY = ed.offsetY; // 起点Y坐标
/* 移动鼠标 */
cav.onmousemove = function (em) {
// 计算绘制数据
let iframe = {}
// 判断矩形变化的类型,site 可能的取值是 1、2、3、4,分别对应矩形的不同位置和方向。根据 site 的取值,计算矩形的新位置和大小,并将其保存在名为 iframe 的对象中。
// 1 左上角;2 右上角;3 左下角;4 右下角。
switch (site) {
case 1:
iframe = {
x: em.offsetX,
y: em.offsetY,
w: mark.w - (em.offsetX - sX),
h: mark.h - (em.offsetY - sY)
}
break;
case 2:
iframe = {
x: mark.x,
y: mark.y + (em.offsetY - sY),
w: mark.w + (em.offsetX - sX),
h: mark.h - (em.offsetY - sY)
}
break;
case 3:
iframe = {
x: mark.x + (em.offsetX - sX),
y: mark.y,
w: mark.w - (em.offsetX - sX),
h: mark.h + (em.offsetY - sY)
}
break;
case 4:
iframe = {
x: mark.x,
y: mark.y,
w: mark.w + (em.offsetX - sX),
h: mark.h + (em.offsetY - sY)
}
break;
}
list.splice(i, 1, iframe);// 替换列表中的选定矩形数据
// 重新绘制
reDraw(cav, ctx, list, i);
}
/* 鼠标离开矩形区 */
cav.onmouseout = function (eo) {
console.log('鼠标离开矩形区');
// 重新绘制
reDraw(cav, ctx, list);
// 初始化
draw(cav, list)
};
/* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */
delDraw(cav, ctx, list, i);
}
}
/* 绘制新矩形 */
function newDraw(cav, ctx, list) {
// 设置鼠标样式为十字架
cav.style.cursor = 'crosshair'
// 初始化变量
let start = false; // 画框状态, false时不执行画框操作
let sX = 0; // 起点X坐标
let sY = 0; // 起点Y坐标
/* 按下鼠标左键 */
cav.onmousedown = function (ed) {
/* 使用变量 */
start = true;
sX = ed.offsetX;
sY = ed.offsetY;
/* 重置按键监听, 防止选中取消后仍可删除 */
delDraw(cav, ctx, list, null)
/* 鼠标移动 */
cav.onmousemove = function (em) {
if (start) {
// 重新绘制
reDraw(cav, ctx, list);
// 设置边框为虚线
// 这个方法开始一条路径,或者重置当前的路径。在绘制新形状之前,你通常会调用这个方法来确保当前路径是新的,而不是继续绘制之前的路径。
ctx.beginPath();
// 这个方法设置当前线条样式为虚线。在这里,它将线条设置为了一种样式,这个样式是一个数组,数组中的第一个数字表示实线的长度,
// 第二个数字表示空白的长度。所以 [8, 4] 表示每隔 8 个像素绘制一段实线,然后跳过 4 个像素绘制下一段实线,以此类推。
ctx.setLineDash([8, 4]);
// 这个方法用于创建一个新的矩形路径。它接受四个参数:起始点的 x 坐标和 y 坐标,以及矩形的宽度和高度。
ctx.rect(sX, sY, em.offsetX - sX, em.offsetY - sY);
// 这个方法绘制当前路径。在这里,它会按照之前设置的线条样式绘制矩形的轮廓。
ctx.stroke();
}
}
/* 鼠标抬起 */
cav.onmouseup = function (eu) {
// 如果绘制状态为 true,并且鼠标水平或垂直方向上移动的距离都大于 10 像素,那么才执行绘制矩形的操作。这样做是为了避免用户误操作导致绘制过小的矩形。
if (start && Math.abs(eu.offsetX - sX) > 10 && Math.abs(eu.offsetY - sY) > 10) {
// 改变矩形数组
let frame = {
x: sX, y: sY, w: eu.offsetX - sX, h: eu.offsetY - sY
};
list.push(frame);
// 重新绘制
reDraw(cav, ctx, list);
// 改变画框状态
start = false
// 初始化
draw(cav, list)
} else {
// 重新绘制
reDraw(cav, ctx, list);
// 改变画框状态
start = false
// 初始化
draw(cav, list)
}
};
/* 鼠标离开矩形区 */
cav.onmouseout = function (eo) {
if (start && Math.abs(eo.offsetX - sX) > 10 && Math.abs(eo.offsetY - sY) > 10) {
// 改变矩形数组
let frame = {
x: sX, y: sY, w: eo.offsetX - sX, h: eo.offsetY - sY
};
list.push(frame);
// 重新绘制
reDraw(cav, ctx, list);
// 改变画框状态
start = false;
// 初始化
draw(cav, list)
} else {
// 重新绘制
reDraw(cav, ctx, list);
// 改变画框状态
start = false
// 初始化
draw(cav, list)
}
};
}
}
/* 选中矩形, 重绘矩形, 并分发后续事件 */
function judgeDraw(cav, ctx, list, iem) {
console.log('选中矩形, 重绘矩形, 并分发后续事件');
cav.style.cursor = 'default';
// 初始化变量
let sX = 0; // 起点X坐标
let sY = 0; // 起点Y坐标
/* 按下鼠标左键 */
cav.onmousedown = function (ed) {
sX = ed.offsetX;
sY = ed.offsetY;
// 更改选中状态, 重绘矩形
reDraw(cav, ctx, list, iem);
/* 当仅点击选中矩形便抬起鼠标后, 重新初始化画布 */
cav.onmouseup = function () {
// 重绘矩形
reDraw(cav, ctx, list, iem);
// 初始化
draw(cav, list, iem);
};
/* 按住拖动鼠标, 移动选中矩形*/
moveDraw(cav, ctx, list, iem, sX, sY);
/* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */
delDraw(cav, ctx, list, iem);
}
}
/* 移动矩形 */
function moveDraw(cav, ctx, list, i, sX, sY) {
console.log('移动矩形');
// 获取选中的矩形
let mark = list[i]
// 监听鼠标移动事件
cav.onmousemove = function (em) {
// 计算移动后矩形的新位置和大小
let iframe = {
x: mark.x + (em.offsetX - sX),
y: mark.y + (em.offsetY - sY),
w: mark.w,
h: mark.h
}
// 更新矩形数组中选中矩形的数据
list.splice(i, 1, iframe);
/* 监听键盘, 使矩形在移动后仍可删除, 在点击未移动过的矩形时仍有一次监听 */
delDraw(cav, ctx, list, i);
// 重新绘制
reDraw(cav, ctx, list, i);
}
// 监听鼠标抬起事件
cav.onmouseup = function () {
// 重绘矩形
reDraw(cav, ctx, list, i);
// 初始化画布,使画布可以继续接收其他事件
draw(cav, list, i);
};
}
/* 删除矩形 */
function delDraw(cav, ctx, list, i) {
/* 按键事件 */
console.log('删除矩形');
if (i === null) {
// i为null时阻止按键监听事件冒泡
document.onkeydown = function (k) {
return false;
}
} else {
// 监听按键事件
document.onkeydown = function (k) {
let key = k.keyCode || k.which;
if ((key == 46 || key == 8) && i !== null) {
if (list.length >= 1) {
// 删除数组元素
list.splice(i, 1);
// 重绘矩形
reDraw(cav, ctx, list);
} else {
/* 矩形数组长度为0, 已将矩形框全部删除 */
ctx.clearRect(0, 0, cav.width, cav.height);
}
// 重置监听状态, 防止删除完毕后, 按键监听不消失
delDraw(cav, ctx, list, null)
// 重绘矩形
reDraw(cav, ctx, list);
// 初始化
draw(cav, list);
}
}
}
}
/* 重绘所有矩形 */
function reDraw(cav, ctx, list, i) {
console.log('重绘所有矩形');
ctx.setLineDash([8, 0]); // 设置边框为实线
ctx.clearRect(0, 0, cav.width, cav.height);
// 绘制未选中部分
list.forEach(function (value, index, array) {
if (i === undefined || index != i) {
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.rect(value.x, value.y, value.w, value.h);
ctx.stroke();
}
});
// 绘制已选中部分
list.forEach(function (value, index, array) {
if (index === i) {
/* 绘制方框 */
// 开始一个新的绘图路径。
ctx.beginPath();
// 设置绘制路径的边框颜色为红色。
ctx.strokeStyle = 'red';
// 绘制一个矩形路径,起始点为 (value.x, value.y),宽度为 value.w,高度为 value.h。
ctx.rect(value.x, value.y, value.w, value.h);
// 设置矩形的填充色为带有透明度的灰色。
ctx.fillStyle = 'RGBA(102,102,102,0.2)'
// 填充矩形的内部区域,以实现选中效果。
ctx.fillRect(value.x, value.y, value.w, value.h);
// 绘制矩形路径的边框。
ctx.stroke();
// 绘制四个角的圆圈
ctx.beginPath();
ctx.strokeStyle = 'red';
// 绘制一个圆弧路径,圆心为 (value.x, value.y),半径为 4,起始角度为 0,终止角度为 2π(360度),即绘制一个完整的圆。
ctx.arc(value.x, value.y, 4, 0, Math.PI * 2)
ctx.fillStyle = "red";
ctx.fill();// 画起点实心圆
ctx.stroke();
ctx.beginPath();
// 绘制另一个圆弧路径,圆心为 (value.x, value.y + value.h),参数同上。
ctx.arc(value.x, value.y + value.h, 4, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();// 画起点纵向实心圆
ctx.stroke();
ctx.beginPath();
// 绘制另一个圆弧路径,圆心为 (value.x + value.w, value.y + value.h),参数同上。
ctx.arc(value.x + value.w, value.y + value.h, 4, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();// 画起点横向实心圆
ctx.stroke();
ctx.beginPath();
// 绘制另一个圆弧路径,圆心为 (value.x + value.w, value.y),参数同上。
ctx.arc(value.x + value.w, value.y, 4, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();// 画终点实心圆
ctx.stroke();
}
})
}
图片标注:
html部分代码
<!-- 标注区域 -->
<div id="myBiaoZhuDiv" class="myBiaoZhu">
<img id="myBiaoZhu" alt="" src="../assets/logo.png" width="200" height="200" />
<el-button @click="startBiaoZhu">开始标注</el-button>
<el-button @click="endBiaoZhu">标注完成</el-button>
</div>
js部分代码
const canBiaoZhu = ref(false); // 是否可以标注
const banMa = ref([]); // 标记点数组
const pointColor = ref("red"); // 标记点颜色
const pointSize = ref("10"); // 标记点大小
// 标记内容数组
const markList = ref([
// { x: 151, y: 384, w: 320, h: 86}
]);
// 开始标注函数
function startBiaoZhu() {
console.log(
'document.getElementById("bigImg").',
document.getElementById("myBiaoZhu")
);
canBiaoZhu.value = true;
}
// 完成标注函数
function endBiaoZhu() {
// canBiaoZhu.value = false;
var markerDivs = document.getElementsByClassName("marker");
console.log("markerDivs", markerDivs);
while (markerDivs.length > 0) {
markerDivs[0].parentNode.removeChild(markerDivs[0]);
}
}
// 创建标记点函数
function createMarker(points) {
points.forEach((point) => {
const div = document.createElement("div");
div.className = "marker";
div.id = "marker" + banMa.value.length;
const y =
point.y +
document.getElementById("myBiaoZhu").offsetTop -
pointSize.value / 2;
const x =
point.x +
document.getElementById("myBiaoZhu").offsetLeft -
pointSize.value / 2;
div.style.width = pointSize.value + "px";
div.style.height = pointSize.value + "px";
div.style.backgroundColor = pointColor.value;
div.style.left = x + "px";
div.style.top = y + "px";
div.style.position = "absolute";
div.style.borderRadius = "50%";
div.style.zIndex = "999";
div.style.top = y + "px";
div.oncontextmenu = (e) => {
const id = e.target.id;
document.getElementById("myBiaoZhuDiv").removeChild(div);
banMa.value = banMa.value.filter(
(item) => item.id != id.slice(6, id.length)
);
if (e && e.preventDefault) {
e.preventDefault();
} else {
window.event.returnValue = false;
}
return false;
};
document.getElementById("myBiaoZhuDiv").appendChild(div);
});
}
// Vue 生命周期钩子函数,组件挂载后执行
onMounted(() => {
// 初始化标记点
const initpoints = [
{
x: 117,
y: 134,
},
{
x: 55,
y: 142,
},
{
x: 75,
y: 100,
},
];
createMarker(initpoints);
// 阻止默认右键菜单事件
document.getElementById("myBiaoZhu").oncontextmenu = (e) => {
if (e && e.preventDefault) {
//阻止默认浏览器动作(W3C)
e.preventDefault();
} else {
//IE中阻止函数器默认动作的方式
window.event.returnValue = false;
}
return false;
}; //阻止冒泡行为和默认右键菜单事件
// 鼠标点击事件监听,用于标注
document.getElementById("myBiaoZhu").onmousedown = (e) => {
e = e || window.event;
if (e.button !== 2) {
//判断是否右击,e.button==2,为按下右键
if (canBiaoZhu.value) {
//判断是否可以进行标注
var x = e.offsetX || e.layerX;
var y = e.offsetY || e.layerY;
console.log("获取基于图片的x,y", x, y);
console.log("获取e", e);
var myImg = document.querySelector("#myBiaoZhu");
var currWidth = myImg.clientWidth;
var currHeight = myImg.clientHeight;
var ProportionWidthInImg = x / currWidth;
var ProportionHeightInImg = y / currHeight;
console.log("图片比例高度:" + ProportionHeightInImg);
console.log("图片比例宽度:" + ProportionWidthInImg);
banMa.value.push({
id: banMa.value.length + 1,
x,
y,
});
initpoints.push({
x: x,
y: y,
});
createMarker(initpoints);
}
}
};
});