需求:视频监控中我们需要只监控某个区域的范围,而且是多个区域,那么我们需要画出多个区域的集合点,如图:

Image

要求:可拖动,可改变,可删除,可保存点

直接上源码,按照自己需求更改吧。

//@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()

代码中有依赖代码,选择性阅读。。。。。