需求:视频监控中我们需要只监控某个区域的范围,而且是多个区域,那么我们需要画出多个区域的集合点,如图:
要求:可拖动,可改变,可删除,可保存点
直接上源码,按照自己需求更改吧。
//@auther: lion.bai
import cannel from '@/assets/img/cannel.png';
import save from '@/assets/img/save.png';
import clear from '@/assets/img/clear.png';
import hint from '@/assets/img/hint.png';
import EventBus from './EventBus.js'
import msg from '@/utils/Msg.js';
import alertMsg from '@/utils/AlertMsg.js';
import { fa, vi } from 'element-plus/es/locales.mjs';
export default class CanvasUtilsNew {
//是否初始化过
isInit = false;
//画布高宽
canvasWidth = 0
canvasHeight = 0
canvasDom = null
canvasContext = null;
points = [] // 路径点位 处于编辑的点集合
pointsList = [] // 全部路径点位
isMovePoint = false // 是否处于移动
isPaint = false // 是否处于画图
type = '' // 画图的类型
parentContainer = null
strokeStyle = '#017FFF'
fillStyle = 'rgba(0, 0, 0, 0.8 )'
dotSide = 15 // 点的大小
currentPoint = null//当前可以移动点
currentMoveType = ''//当前移动的类型
currentMovePoint = null//
isInSide = false//是否在图形内
isDraging = false//是否拖动
currentDragPoint = null//当前拖动的点
currentDragGraphIndex = -1//当前拖动图形的索引地址
//移动
mousemove(e) {
let x = e.offsetX;
let y = e.offsetY;
//是否拖动中
if (this.isDraging &&
this.currentDragPoint &&
this.currentDragGraphIndex >= 0 &&
this.pointsList.length > 0) {
this.canvasDom.style.cursor = 'move';
//移动的点
let moveX = x - this.currentDragPoint.x;
let moveY = y - this.currentDragPoint.y;
//console.log('moveX', moveX, 'moveY', moveY);
let points = this.pointsList[this.currentDragGraphIndex];
//判断是否超出边界
for (let i = 0; i < points.length; i++) {
let point = points[i];
let xTemp = point.x + moveX;
let yTemp = point.y + moveY;
if (xTemp < 0 || xTemp > this.canvasWidth || yTemp < 0 || yTemp > this.canvasHeight) {
return;
}
}
//未超出边界
for (let i = 0; i < points.length; i++) {
let point = points[i];
point.x = point.x + moveX;
point.y = point.y + moveY;
}
this.currentDragPoint = { x: x, y: y };
this.draw();
return;
}
//判断当前x,y 坐标是否在 this.points几个点组成的图形内
//不在画图中 不在移动点中
if (this.pointsList.length > 0 && !this.isPaint && !this.isMovePoint) {
this.isPointInPolygon({ x: x, y: y });
//console.log('isInSide', this.isInSide, this.currentDragGraphIndex );
if (this.isInSide) {
this.canvasDom.style.cursor = 'move';
} else {
this.canvasDom.style.cursor = 'default';
}
}
//移动图形的某个点 改变图形
if (this.isMovePoint) {
//判断currentPoint 在 this.pointsList中的位置
if (this.currentMoveType == 'rectangleAction') {
outer: for (let i = 0; i < this.pointsList.length; i++) {
let points = this.pointsList[i];
for (let j = 0; j < points.length; j++) {
let point = points[j];
if (point == this.currentPoint) {
//矩形的某个坐标点改变,相邻的2个点的 x,y也要改变
if (j == 0) {
points[1].y = y;
points[3].x = x;
} else if (j == 1) {
points[0].y = y;
points[2].x = x;
} else if (j == 2) {
points[1].x = x;
points[3].y = y;
} else if (j == 3) {
points[0].x = x;
points[2].y = y;
}
points[j] = { x: x, y: y };
this.currentPoint = points[j];
break outer;
}
}
}
} else {
this.currentPoint.x = x;
this.currentPoint.y = y;
}
this.draw();
return;
}
//是否画图中
if (this.isPaint) {
this.currentMovePoint = { x: x, y: y };
this.draw();
}
}
//抬起
mouseup(e) {
let x = e.offsetX;
let y = e.offsetY;
//移动图形的某个点 改变图形
if (this.isMovePoint) {
this.isMovePoint = false;
this.currentPoint = null;
this.currentMoveType = '';
return;
}
this.isDraging = false;
this.currentDragGraphIndex = -1;
}
//点击
mousedown(e) {
let x = e.offsetX;
let y = e.offsetY;
//console.log('mousedown:isDraging:', this.isDraging, "isMovePoint:"+this.isMovePoint, "isPaint:"+this.isPaint,"isInSide:" +this.isInSide);
//处于拖动中
if (this.isDraging) {
return;
}
//处于移动点中
if (this.isMovePoint) {
return;
}
//在图形内拖动
if (this.isInSide && !this.isPaint && !this.isMovePoint) {
//是否拖动
this.isDraging = true;
this.currentDragPoint = { x: x, y: y };
return;
}
//在没有画图中
if (!this.isPaint) {
//判断点是否在需要拖动的顶点上
outer: for (let i = 0; i < this.pointsList.length; i++) {
let points = this.pointsList[i];
for (let j = 0; j < points.length; j++) {
let point = points[j];
if (Math.abs(x - point.x) < this.dotSide / 2 && Math.abs(y - point.y) < this.dotSide / 2) {
this.currentPoint = point; //当前的点
this.isMovePoint = true;
//判断裁剪图像是否是矩形,点数等于,对边相等 才是矩形
if (points.length == 4 &&
points[0].x == points[3].x &&
points[1].x == points[2].x &&
points[0].y == points[1].y &&
points[2].y == points[3].y) {
this.currentMoveType = 'rectangleAction';
}
//break outer;
return;
}
}
}
console.log('不在点上....');
this.isPaint = true;
}
console.log('记录点', this.type, this.points.length);
//记录当前点到列表中
if (this.type == 'rectangleAction' && this.points.length > 0) { ///矩形
//只需2个点结束矩形
let rectPoints = this.calculateRectanglePoints(this.points[0], { x: x, y: y });
this.pointsList.push(rectPoints);
//重新初始化
this.initValue();
} else {
this.points.push({ x: x, y: y });
}
this.draw();
}
draw() {
//创建背景点
this.createBackground();
let ctx = this.canvasContext;
//已经完成的图像
for (let i = 0; i < this.pointsList.length; i++) {
// 绘制第一个镂空圆形
ctx.beginPath();
let points = this.pointsList[i];
this.drawPath(points, true);
this.drawDragSmallRect(points);
}
///正在画的图像
this.drawPath(this.points, false);
this.drawDragSmallRect(this.points);
}
//绘制图形
/**
*
* @param {*} points
* @param {*} isComplete 是否是已经完成的图像
*/
drawPath(points, isComplete = false) {
if (points.length <= 0) {
return;
}
let ctx = this.canvasContext;
// 绘制第一个镂空圆形
ctx.beginPath();
for (let i = 0; i < points.length; i++) {
let point = points[i];
if (i == 0) {
ctx.moveTo(point.x, point.y);
} else {
ctx.lineTo(point.x, point.y);
}
}
//多边形移动的点
if (this.currentMovePoint && !isComplete) {
//矩形,需要计算其他 对角点
if (this.type == 'rectangleAction') {
let rectPoints = this.calculateRectanglePoints(points[0], this.currentMovePoint);
for (let i = 0; i < rectPoints.length; i++) {
let point = rectPoints[i];
ctx.lineTo(point.x, point.y);
}
} else {
ctx.lineTo(this.currentMovePoint.x, this.currentMovePoint.y);
}
}
ctx.closePath();
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgba(1,127,255,0.8)'; //描边色
ctx.lineCap = 'square';
ctx.lineJoin = 'square';
ctx.setLineDash([5, 5]); // [实线长度, 间隙长度]
ctx.lineDashOffset = -0;
ctx.stroke();
ctx.globalCompositeOperation = 'destination-out';
ctx.fillStyle = "#017FFF"; // 设置填充色
ctx.fill();
ctx.save();
}
//根据对角计算矩形的4个点
calculateRectanglePoints(firstPoint, threePoint) {
let points = [];
points.push(firstPoint);
points.push({ x: threePoint.x, y: firstPoint.y });
points.push(threePoint);
points.push({ x: firstPoint.x, y: threePoint.y });
return points;
}
//判断点是否在多边形内
isPointInPolygon(point) {
let x = point.x;
let y = point.y;
let _inside = false;
let _currentDragGraphIndex = -1;
//从上层到下层判断
for (let i = this.pointsList.length - 1; i >= 0; i--) {
_inside = this._doPointInPolygon(this.pointsList[i], point);
if (_inside) {//在图形内
_currentDragGraphIndex = i; //当前拖动的图形索引
break;
}
}
this.isInSide = _inside;
this.currentDragGraphIndex = _currentDragGraphIndex;
return _inside;
}
_doPointInPolygon(polygon, point) {
let windingNumber = 0;
let lastPoint = polygon[polygon.length - 1];
for (let i = 0; i < polygon.length; i++) {
let currPoint = polygon[i];
if (currPoint.y <= point.y) {
if (lastPoint.y > point.y) {
if ((currPoint.x - point.x) * (lastPoint.y - point.y) -
(lastPoint.x - point.x) * (currPoint.y - point.y) > 0) {
windingNumber++;
}
}
} else {
if (lastPoint.y <= point.y) {
if ((currPoint.x - point.x) * (lastPoint.y - point.y) -
(lastPoint.x - point.x) * (currPoint.y - point.y) < 0) {
windingNumber--;
}
}
}
lastPoint = currPoint;
}
return windingNumber !== 0;
}
//绘制小矩形拖动框
drawDragSmallRect(points) {
let ctx = this.canvasDom.getContext('2d');
//每个顶点画个小矩形点 用于拖动
for (let i = 0; i < points.length; i++) {
let point = points[i];
ctx.beginPath();
ctx.fillStyle = this.strokeStyle; //填充色
ctx.globalCompositeOperation = 'source-over';
ctx.fillRect(point.x - this.dotSide / 2, point.y - this.dotSide / 2, this.dotSide, this.dotSide);
ctx.closePath();
ctx.fill();
ctx.save();
}
}
//创建背景
createBackground() {
let ctx = this.canvasContext;
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
//绘制背景
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'black';
//透明度设置
ctx.globalAlpha = 0.8;
ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
// console.log('createBackground');
}
//初始化值
initValue() {
this.points = [];
this.isMovePoint = false;
this.isPaint = false;
this.currentPoint = null;
this.currentMoveType = '';
this.currentMovePoint = null;
this.isDraging = false;
this.currentDragPoint = null;
this.currentDragGraphIndex = -1;
this.isInSide = false;
if (this.canvasDom) this.canvasDom.style.cursor = 'default';
}
//初始化
createCanvasLayout(parentWidth, parentHeight, type, parentContainer, points, vWidth, vHeight) {
this.type = type;
if (this.isInit) return;
this.removeCanvasLayout() //删除之前的画布,重新插入
this.parentContainer = parentContainer;
//根据视频宽高比 获取canvas的大小
let canvasSize = this.fitVideoToContainer(vWidth, vHeight, parentWidth, parentHeight);
this.canvasWidth = canvasSize.width;
this.canvasHeight = canvasSize.height;
this.pointsList = points;
this.initValue();
const div = document.createElement('div');
div.className = 'canvas-box';
div.style.position = 'absolute';
div.style.top = '0';
div.style.right = '0';
div.style.bottom = '0';
div.style.left = '0';
div.style.zIndex = '100';
//div.style.background = 'rgba(357,95,70,0.5)' //红色背景
div.style.width = parentWidth + 'px';
div.style.height = parentHeight + 'px';
//阻止事件往下传递
div.addEventListener('click', (e) => {
e.stopPropagation();
});
let positionAction = (ob) => {
ob.style.position = 'absolute';
ob.style.top = '0';
ob.style.right = '0';
ob.style.bottom = '0';
ob.style.left = '0';
ob.style.margin = 'auto';
}
//顶一个父类遮罩
const maskDiv = document.createElement('div');
maskDiv.style.width = canvasSize.width + 'px';
maskDiv.style.height = canvasSize.height + 'px';
maskDiv.style.background = 'rgba(0,0,0,0)';
maskDiv.style.outline = '99999px solid rgba(0,0,0,1)';
// maskDiv.style.border = '1px dotted rgba(255,255,255,0.5)';
maskDiv.style.zIndex = '101';
positionAction(maskDiv);
div.appendChild(maskDiv);
const canvas = document.createElement('canvas');
canvas.width = canvasSize.width;
canvas.height = canvasSize.height;
canvas.style.zIndex = '102';
canvas.style.border = '1px dotted #017FFF';
positionAction(canvas)
//canvas.style.background = 'rgba(0,0,0,0.8)';
//鼠标按下事件
canvas.addEventListener('mousedown', (e) => {
this.mousedown(e);
e.stopPropagation();
});
//鼠标抬起事件
canvas.addEventListener('mouseup', (e) => {
this.mouseup(e);
e.stopPropagation();
});
//鼠标移动事件
canvas.addEventListener('mousemove', (e) => {
this.mousemove(e);
e.stopPropagation();
});
//移除
canvas.addEventListener('mouseleave', (e) => {
this.initValue();
this.draw();
e.stopPropagation();
});
//鼠标右键事件 结束画图
canvas.addEventListener('contextmenu', (e) => {
// 防止打开菜单菜单
e.stopPropagation();
e.preventDefault();
//弹框删除
if (this.isInSide &&
!this.isPaint &&
!this.isMovePoint &&
this.isDraging &&
this.currentDragGraphIndex >= 0 &&
this.currentDragGraphIndex < this.pointsList.length) {
let _currentDragGraphIndex = this.currentDragGraphIndex;
alertMsg.confirm('确定删除该裁剪框?').then(() => {
this.pointsList.splice(_currentDragGraphIndex, 1);
this.initValue();
this.draw();
}).catch(() => {
this.initValue();
this.draw();
});
return;
}
if (!this.isPaint) return;//没有在画图中,不处理
this.isPaint = false;//到起点了 结束绘制
if (this.points.length < 3) {
this.initValue();
this.draw();
msg.error("至少需要3个点");
return;
}
this.pointsList.push(this.points);
this.initValue();
this.draw();
return false
});
div.appendChild(canvas);
this.canvasDom = canvas;
this.canvasContext = canvas.getContext('2d');
//插入提示按钮
const imgHint = document.createElement('img');
imgHint.src = hint
imgHint.style.position = 'absolute';
imgHint.style.top = '10px';
imgHint.style.left = '10px';
imgHint.style.zIndex = '103';
imgHint.style.width = '20px';
imgHint.style.height = '20px';
imgHint.style.cursor = 'pointer';
imgHint.onclick = function (e) {
alertMsg.alert('1、鼠标右键结束绘制;\n<br/>2、点击矩形点可改变点位置;\n<br/>3、移入图形内、鼠标变为可拖动图标,可改变图形位置;\n<br/>4、当鼠标移入裁剪框内,点击鼠标右键,可删除当前裁剪框。').then(() => { }).catch(() => { });
e.stopPropagation();
}
div.appendChild(imgHint);
//插入取消按钮
const imgCannel = document.createElement('img');
imgCannel.src = cannel
imgCannel.style.position = 'absolute';
imgCannel.style.top = '10px';
imgCannel.style.right = '10px';
imgCannel.style.zIndex = '103';
imgCannel.style.width = '20px';
imgCannel.style.height = '20px';
imgCannel.style.cursor = 'pointer';
imgCannel.onclick = function (e) {
alertMsg.confirm('确定退出编辑?').then(() => {
EventBus.emit('cropAction');
}).catch(() => {
console.log('取消');
});
e.stopPropagation();
}
div.appendChild(imgCannel);
//插入保存按钮
const imgSave = document.createElement('img');
imgSave.src = save
imgSave.style.position = 'absolute';
imgSave.style.top = '10px';
imgSave.style.right = '40px';
imgSave.style.zIndex = '103';
imgSave.style.width = '20px';
imgSave.style.height = '20px';
imgSave.style.cursor = 'pointer';
imgSave.onclick = (e) => {
if (this.pointsList.length <= 0) {
msg.error("请选择需要裁剪的区域");
return;
}
let jsonData = {
points: this.pointsList,
width: this.canvasWidth,
height: this.canvasHeight,
videoWidth: vWidth,
videoHeight: vHeight
}
EventBus.emit('savePoint', jsonData);
e.stopPropagation();
}
div.appendChild(imgSave);
//插入清除按钮
const imgClear = document.createElement('img');
imgClear.src = clear
imgClear.style.position = 'absolute';
imgClear.style.top = '10px';
imgClear.style.right = '70px';
imgClear.style.zIndex = '103';
imgClear.style.width = '20px';
imgClear.style.height = '20px';
imgClear.style.cursor = 'pointer';
imgClear.onclick = (e) => {
//删除上一个点,只针对当前画的图像
alertMsg.confirm('确定删除上一个裁剪框?').then(() => {
if (this.points.length > 0) {
this.initValue();
this.draw();
return;
}
if (this.pointsList.length > 0) {
this.pointsList.pop();
this.draw();
}
}).catch(() => {
console.log('取消');
});
e.stopPropagation();
}
div.appendChild(imgClear);
this.draw();
this.parentContainer.appendChild(div);
this.isInit = true;
}
removeCanvasLayout() {
if (!this.parentContainer) return;
///let canvas = this.parentContainer.getElementsByTagName('canvas');
let canvas = this.parentContainer.getElementsByClassName('canvas-box');
if (canvas && canvas.length > 0) {
var newCanvas = Array.prototype.slice.call(canvas, 0);
for (let i = 0; i < newCanvas.length; i++) {
this.parentContainer.removeChild(newCanvas[i]);
}
}
}
// 适应视频大小
/**
*
* @param {*} vWidth 视频宽度
* @param {*} vHeight 视频高度
* @param {*} cWidth 容器宽度
* @param {*} cHeight 容器高度
* @returns
*/
fitVideoToContainer(vWidth, vHeight, cWidth, cHeight) {
// 获取视频的宽高比
const videoAspectRatio = vWidth / vHeight;
// 获取容器的宽高
const containerWidth = cWidth;
const containerHeight = cHeight;
// 计算 video-box 的高度和宽度
let boxWidth, boxHeight;
if (videoAspectRatio > containerWidth / containerHeight) {
// 视频宽度超出容器
boxWidth = containerWidth;
boxHeight = containerWidth / videoAspectRatio;
} else {
// 视频高度超出容器
boxHeight = containerHeight;
boxWidth = containerHeight * videoAspectRatio;
}
return { width: boxWidth, height: boxHeight };
}
//销毁
destory() {
this.removeCanvasLayout();
this.initValue();
this.isInit = false;
}
}
调用代码:
const pointsData = ref(null)//裁剪点 这里根据视频id 赋值之前保存在数据库的点集合
const videoWidth = ref(640) //视频宽度 需要初始化赋值 分辨率
const videoHeight = ref(320) //视频高度 需要初始化赋值 分辨率
const canvasUtilsNewObj = new CanvasUtilsNew()
//获取 容器 元素的高宽
const videoElement = ref()
let parentWidth = videoElement.value.offsetWidth
let parentHeight = videoElement.value.offsetHeight
//调用
canvasUtilsNewObj.createCanvasLayout(
parentWidth,
parentHeight,
type, //矩形还是多边形
videoElement.value,//canvas 插入到的父类容器
//历史的点
pointsData.value && pointsData.value.points ? pointsData.value.points : [],
videoWidth.value,
videoHeight.value
)
//销毁
canvasUtilsNewObj.destory()
代码中有依赖代码,选择性阅读。。。。。