canvas 画区域,功能支持画多区域,删除选中拖拽 已封装
前面做了个简单的,这次有时间用es6 的class 封装了一下 本人菜鸡,觉着好用的话点个赞,觉着不好用的勿喷 谢谢!!!
init.js
//canvas video 画区域
class CanvasServer {
constructor(opts) {
this.option = opts
// 画布
this.can = null;
this.canvasSaves = null;
// 画布上下文
this.ctxSave = null;
this.ctx = null;
// 临时存放绘制的点
this.pointArr = []
//拷贝存放绘制的点
this.cloneData = []
//处理后后端需要的点
this.arrData = []
//画区域所需要的点
this.newData = []
this.startDrawArea = false //true 开始画区域 false 不能操作
// 重合距离
this.coincidentDistance = 10,
//判断鼠标是否移动到起始点处,-1为否,1为是
this.oIndex = -1,
//默认线条颜色
this.defaultLineColor = "rgba(64, 158, 255, 1)"
//默认填充颜色
this.defaultFllStyle = "rgba(161,195,255,0.4)"
//默认选中颜色
this.defaultSelectStyle = "rgba(255,0,0,0.3)"
//线条颜色
this.lineColor = ''
//填充颜色
this.fillStyle = '',
//选中颜色
this.selectStyle = ''
//选中区域的下标
this.moveIndex = null
//判断鼠标下落方式,
this.endMethod = false
this.clickMove = false //是否移动
this.completedDraw = true //是否完成画框
this.MovePointArr = [] //移动后的数据
this.copyArrData = []
this.currentArrData = []
// 圆点的定义
this.arcInfo = {
radius: 8,
curPointColor: '#ff0000',
pointColor: 'blue'
}
// 初始化canvas
this._init(opts);
}
//初始
_init(opts) {
this.can = opts.canvas;
this.can.width = opts.width;
this.can.height = opts.height;
this.canvasSaves = opts.canvasSave
this.canvasSaves.width = opts.width
this.canvasSaves.height = opts.height
this.lineColor = opts?.lineColor || this.defaultLineColor
this.fillStyle = opts?.fillStyle || this.defaultFllStyle
this.selectStyle = opts?.selectStyle || this.defaultSelectStyle
this.ctx = this.can.getContext(`2d`);
this.ctx.strokeStyle = this.lineColor; //线条颜色
this.ctx.fillStyle = this.fillStyle; //填充颜色
this.ctxSave = this.canvasSaves.getContext(`2d`);
this.ctxSave.strokeStyle = this.lineColor; //线条颜色
this.ctxSave.fillStyle = this.fillStyle; //填充颜色
// 初始化鼠标事件
this._initCanvasEvent();
}
//添加事件
_initCanvasEvent() {
this.canvasSaves.addEventListener('mousemove', this._handleCanvasSaveMove);
this.canvasSaves.addEventListener('click', this._handleCanvasSaveClick);
this.canvasSaves.addEventListener('mousedown', this._handleMouseDown);
this.canvasSaves.addEventListener('mouseup', this._handleMouseUp);
}
_handleMouseDown = (e) => {
if (e.button == 2) return
if (this.completedDraw && this.newData.some(i => i.checked)) {
this.pointX = e.offsetX
this.pointY = e.offsetY
this.clickMove = true
} else {
this.completedDraw = false
}
// console.log('长按鼠标', e)
}
_handleMouseUp = (e, endMethod) => {
if (e?.button == 2) return
if (this.clickMove && this.completedDraw && this.currentArrData.length > 0) {
this.clickMove = false;
this.newData = JSON.parse(JSON.stringify(this.MovePointArr))
this.arrData = JSON.parse(JSON.stringify(this.currentArrData))
this.option.getData(this.arrData)
this.endMethod = endMethod || false
console.log('松下鼠标', this.arrData)
}
// console.log('松下鼠标', this.arrData)
}
//删除按钮操作
deleteCanvas(delNone) {
return delNone
}
//获取操作完成的数据
getData(arrData) {
return arrData
}
//区域回显
echoCanvas = (pointColorArr) => {
let arrData = pointColorArr.map(i => {
return i.map(j => {
let obj = {
x: j.x, //(Number(j.x) / this.scaling).toFixed(2),
y: j.y //(Number(j.y) / this.scaling).toFixed(2)
}
return obj
})
})
this.cloneData = JSON.parse(JSON.stringify(arrData))
let arr = arrData.map(item => {
let obj = {
ponitArr: item,
checked: false,
color: this.fillStyle
}
return obj
})
arr.forEach(item => {
this.moveChecked(item.ponitArr, item.color)
})
}
getGcd = (a, b) => {
let n1, n2;
if (a > b) {
n1 = a;
n2 = b;
} else {
n1 = b;
n2 = a;
}
let remainder = n1 % n2;
if (remainder === 0) {
return n2;
} else {
return this.getGcd(n2, remainder)
}
}
//绘制文字
canvasText(text, point) {
this.ctxSave.font = "20px Consolas"; //字体样式的属性
this.ctxSave.textAlign = "center"; //设置文本对齐方式
this.ctxSave.textBaseline = "middle"; //文本基线
let textWidth = this.ctxSave.measureText(text).width;
var canvasWidth = this.can.width;
this.ctxSave.fillStyle = "red"; //字体颜色
this.ctxSave.fillText(text, +point.x + 100, +point.y + 100); //绘制文字
this.ctxSave.arc(point.x, point.y, 3, 0, Math.PI * 2); //基准点
}
/**
* @description 射线法判断点是否在多边形内部
* @param {Object} point 待判断的点,格式:{ x: X坐标, y: Y坐标 }
* @param {Array} polygon 多边形顶点,数组成员的格式同 point
* @return {Boolean} true false
*/
// 判断点是否在多边形内
pointInPolygon(targetPoint, targetPoints) {
var leftPointCount = 0;
var rightPointCount = 0;//左右点的个数
var _points = [];
//第一步:取出所有的点,并计算交点坐标
for (var i = 0, _length = targetPoints.length - 1; i < _length; i++) {
var p1 = targetPoints[i], p2 = targetPoints[i + 1];//取出当前点和当前点的下一个点
var point = this._calcCrossoverPoint(targetPoint, p1, p2);
//如果交点有效,则保存
if (point) {
_points.push(point);
}
}
// 第二步:计算给定的坐标点,左右两边的交点个数,奇数在范围内,偶数则不在
for (var j = 0, length = _points.length; j < length; j++) {
var x = _points[j];
if (x === targetPoint.x) {
return false;//在线上,直接返回不在范围内
} else {
(targetPoint.x !== x && targetPoint.x > x) ? leftPointCount++ : rightPointCount++;
}
}
//判断交点个数
return (leftPointCount % 2 !== 0 && rightPointCount % 2 !== 0);
}
/**
* 算交点坐标,坐标点在扫描行上或者上方时,交点无效
* @param targetPoint
* @param startPoint
* @param endPoint
* @returns {*}
*/
_calcCrossoverPoint(targetPoint, startPoint, endPoint) {
var crossoverPointX = startPoint.x - ((startPoint.y - targetPoint.y) * (startPoint.x - endPoint.x) / (startPoint.y - endPoint.y));
//判断交点坐标是否有效,即交点在startPoint,endPoint构成的线段范围内
if ((startPoint.y < targetPoint.y && endPoint.y >= targetPoint.y) || (endPoint.y < targetPoint.y && startPoint.y >= targetPoint.y)) {
if ((crossoverPointX >= startPoint.x && crossoverPointX <= endPoint.x) || (crossoverPointX <= startPoint.x && crossoverPointX >= endPoint.x)) {
return crossoverPointX;
} else {
return false;
}
} else {
return false;
}
}
// 通过多边形的各点位获得重心
centerPoint(pointArr) {
let X = 0,
Y = 0;
for (let i = 0; i < pointArr.length; i++) {
X = X + +pointArr[i].x;
Y = Y + +pointArr[i].y;
}
X = X / pointArr.length;
Y = Y / pointArr.length;
return { x: X, y: Y };
}
//鼠标左点击
_handleCanvasSaveClick = (e) => {
//判断是左点击并且处于画框状态
if (!this.startDrawArea && e.button == 2) return
//判断是选中状态并且没有正在画框
if (this.newData.some(i => i.checked) && this.completedDraw) {
return
}
//解决拖拽区域后到达边界后区域停止,鼠标继续前进造成click 点击事件触发画区域
if (this.endMethod) {
let event = {
button: 0,
offsetX: e.offsetX,
offsetY: e.offsetY
}
this._handleMouseUp(event, true)
return
}
if (e.offsetX) {
this.pointX = e.offsetX;
this.pointY = e.offsetY;
var piX, piY;
if (this.oIndex > 0 && this.pointArr.length > 0) {
piX = this.pointArr[0].x;
piY = this.pointArr[0].y;
//画点
this.makearc(
this.ctx,
piX,
piY,
this.GetRandomNum(2, 2),
0,
180,
this.lineColor
);
// console.log('piX2', piX,piY)
this.pointArr.push({ x: piX, y: piY });
this.canvasSave(this.pointArr); //保存点线同步到另一个canvas
this.saveCanvas(); //生成画布
} else {
piX = this.pointX;
piY = this.pointY;
this.makearc(
this.ctx,
piX,
piY,
this.GetRandomNum(2, 2),
0,
180,
this.lineColor
);
// console.log('piX', piX,piY)
this.pointArr.push({ x: piX, y: piY });
this.canvasSave(this.pointArr); //保存点线同步到另一个canvas
}
}
}
//鼠标移入
_handleCanvasSaveMove = (e) => {
//先判断是操作状态 ,再判断是不是画区域状态
if (this.startDrawArea && this.completedDraw) {
if (this.clickMove) {
// console.log('进来了', '')
this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
this.ctx.clearRect(0, 0, this.can.width, this.can.height);
this.MovePointArr = [...this.newData]
this.copyArrData = JSON.parse(JSON.stringify(this.arrData))
this.MovePointArr.forEach(i => {
i.checked = false,
i.color = this.fillStyle
})
let copyArrData = this.copyArrData[this.moveIndex] || []
let flag = true;
// 多边形不可出画板
for (let i = 0; i < copyArrData.length; i++) {
if (+copyArrData[i].x + e.offsetX - this.pointX >= this.can.width ||
+copyArrData[i].x + e.offsetX - this.pointX <= 0 ||
+copyArrData[i].y + e.offsetY - this.pointY >= this.can.height ||
+copyArrData[i].y + e.offsetY - this.pointY <= 0) {
flag = false;
let event = {
button: 0,
offsetX: e.offsetX,
offsetY: e.offsetY
}
this._handleMouseUp(event, true)
}
}
if (flag == false) return;
// 表示点击之后移动
let MovePointArr = copyArrData.map(ele => {
return {
x: (+ele.x + e.offsetX - this.pointX).toFixed(2),
y: (+ele.y + e.offsetY - this.pointY).toFixed(2),
};
})
let pointArrs = {
checked: true,
ponitArr: MovePointArr,
color: this.selectStyle
}
this.moveIndex = JSON.parse(JSON.stringify(this.moveIndex))
this.MovePointArr.splice(this.moveIndex, 1, pointArrs)
this.copyArrData.splice(this.moveIndex, 1, MovePointArr)
this.currentArrData = JSON.parse(JSON.stringify(this.copyArrData))
this.MovePointArr.forEach(item => {
this.moveChecked(item.ponitArr, item.color)
})
// console.log('MovePointArr',this.MovePointArr)
// this.newData = [...this.MovePointArr]
} else {
this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
this.ctx.clearRect(0, 0, this.can.width, this.can.height);
this.moveCheckedRegion(e)
}
if (this.moveIndex != null) return
}
if (e.offsetX) {
console.log('触发', '')
this.pointX = e.offsetX;
this.pointY = e.offsetY;
var piX, piY;
/*清空画布*/
this.ctx.clearRect(0, 0, this.can.width, this.can.height);
/*鼠标下跟随的圆点*/
this.makearc(
this.ctx,
this.pointX,
this.pointY,
this.GetRandomNum(4, 4),
0,
180,
this.lineColor
);
if (this.pointArr.length > 0) {
if (
this.pointX > this.pointArr[0].x - this.coincidentDistance &&
this.pointX < this.pointArr[0].x + this.coincidentDistance &&
this.pointY > this.pointArr[0].y - this.coincidentDistance &&
this.pointY < this.pointArr[0].y + this.coincidentDistance
) {
if (this.pointArr.length > 1) {
piX = this.pointArr[0].x;
piY = this.pointArr[0].y;
// console.log(piX, piY);
this.ctx.clearRect(0, 0, this.can.width, this.can.height);
this.makearc(
this.ctx,
piX,
piY,
this.GetRandomNum(4, 4),
0,
180,
this.lineColor
);
this.oIndex = 1;
}
} else {
piX = this.pointX;
piY = this.pointY;
this.oIndex = -1;
}
/*开始绘制*/
this.ctx.beginPath();
this.ctx.moveTo(this.pointArr[0].x, this.pointArr[0].y);
if (this.pointArr.length > 1) {
for (var i = 1; i < this.pointArr.length; i++) {
this.ctx.lineTo(this.pointArr[i].x, this.pointArr[i].y);
}
}
this.ctx.lineTo(piX, piY);
this.ctx.fillStyle = this.fillStyle; //填充颜色
this.ctx.fill(); //填充
this.ctx.stroke(); //绘制
}
}
}
moveCheckedRegion(e) {
//处理回显数据
if (!this.startDrawArea) return
let arr = []
if (this.cloneData.length > 0 && this.arrData.length == 0) {
arr = this.cloneData.map(i => {
return i
})
}
this.cloneData = []
this.arrData = [...this.arrData, ...arr]
//鼠标滑动数据处理
if (this.arrData.length > 0) {
let checkedNum = []
this.newData = this.arrData.map((i, index) => {
// console.log('当前坐标',e.offsetX,e.offsetY)
console.log('区域选中', this.pointInPolygon({ x: e.offsetX, y: e.offsetY, }, i))
// console.log('选中的当前区域',i)
// console.log('全部',this.arrData)
let obj = {
ponitArr: i,
checked: this.pointInPolygon({ x: e.offsetX, y: e.offsetY, }, i),
color: this.pointInPolygon({ x: e.offsetX, y: e.offsetY, }, i) ? this.selectStyle : this.fillStyle
}
return obj
})
//清除画布
this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
// let checkedNum = []
//选中添加颜色
this.newData.forEach((item, index) => {
//当前鼠标下选中的判断
if (item.checked) {
checkedNum.push(item)
this.moveIndex = index
} else {
this.moveChecked(item.ponitArr, item.color)
}
})
// console.log('moveIndex',this.moveIndex)
// console.log('checkedNum',checkedNum)
//叠加时,选中数组最后的一个 层级最高
if (checkedNum.length > 0) {
if (checkedNum.length > 1) {
checkedNum.forEach((i, j) => {
if (checkedNum.length - 1 == j) {
this.moveChecked(checkedNum[checkedNum.length - 1].ponitArr, checkedNum[checkedNum.length - 1].color)
} else {
this.moveChecked(i.ponitArr, this.fillStyle)
}
})
} else {
this.moveChecked(checkedNum[0].ponitArr, checkedNum[0].color)
}
} else {
//选中后鼠标离开操作
this.moveIndex = null
this.option.deleteCanvas('none')
}
}
}
//删除区域
delCanvas() {
this.newData.splice(this.moveIndex, 1)
this.arrData.splice(this.moveIndex, 1)
//清除画布
this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
this.newData.forEach(i => {
this.moveChecked(i.ponitArr, i?.color || this.fillStyle)
})
this.option.getData(this.arrData)
}
//绘制区域
moveChecked = (pointArr, color) => {
if (pointArr.length > 0) {
/*开始绘制*/
this.ctxSave.beginPath();
this.ctxSave.moveTo(pointArr[0].x, pointArr[0].y);
if (pointArr.length > 1) {
for (var i = 1; i < pointArr.length; i++) {
this.ctxSave.lineTo(pointArr[i].x, pointArr[i].y);
}
}
this.ctxSave.lineTo(pointArr[0].x, pointArr[0].y);
this.ctxSave.fillStyle = color; //填充颜色
this.ctxSave.fill(); //填充
this.ctxSave.stroke(); //绘制
}
}
// 存储已生成的点线
canvasSave = (pointArr) => {
this.ctxSave.clearRect(0, 0, this.ctxSave.width, this.ctxSave.height);
this.ctxSave.beginPath();
if (pointArr.length > 1) {
this.ctxSave.moveTo(pointArr[0].x, pointArr[0].y);
for (var i = 1; i < pointArr.length; i++) {
this.ctxSave.lineTo(pointArr[i].x, pointArr[i].y);
this.ctxSave.fillStyle = this.fillStyle; //填充颜色
//ctxSave.fill();
this.ctxSave.stroke(); //绘制
}
this.ctxSave.closePath();
}
}
/*生成画布 结束绘画*/
saveCanvas() {
this.ctx.clearRect(0, 0, this.can.width, this.can.height);
this.ctxSave.closePath(); //结束路径状态,结束当前路径,如果是一个未封闭的图形,会自动将首尾相连封闭起来
this.ctxSave.fill(); //填充
this.ctxSave.stroke(); //绘制
this.arrData.push(this.pointArr)
this.pointArr = []
this.completedDraw = true
this.option.getData(this.arrData)
}
/*验证canvas画布是否为空函数*/
isCanvasBlank(canvas) {
var blank = document.createElement("canvas"); //创建一个空canvas对象
blank.width = canvas.width;
blank.height = canvas.height;
return canvas.toDataURL() == blank.toDataURL(); //为空 返回true
}
/*canvas生成圆点*/
GetRandomNum(Min, Max) {
var Range = Max - Min;
var Rand = Math.random();
return Min + Math.round(Rand * Range);
}
makearc = (ctx, x, y, r, s, e, color) => {
ctx.clearRect(0, 0, this.can.width, this.can.height); //清空画布
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(x, y, r, s, e);
ctx.fill();
}
resetCanvas() {
this.ctx.clearRect(0, 0, this.can.width, this.can.height);
this.ctxSave.clearRect(0, 0, this.canSave.width, this.canSave.height);
this.pointArr = [];
}
reset() {
if (this.ctx || this.ctxSave) {
this.ctx.clearRect(0, 0, this.can.width, this.can.height);
this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
this.pointArr = [];
this.arrData = []
this.newData = []
this.cloneData = []
this.clickMove = false //是否移动
this.endMethod = false
this.completedDraw = true //是否完成画框
this.MovePointArr = [] //移动后的数据
this.copyArrData = []
this.currentArrData = []
this.startDraw = false
this.startDrawArea = false
}
}
};
export default CanvasServer
index.vue
因为我这个是在video 上操作画区域,所有要视频成功加载后获取video 的真实宽高做的比例计算,请根据自己需求来修改。
//初始化
async initCanvas(pointColorArr) {
if (!this.videoUrl) return;
this.videoShow = true
var video = document.getElementById(this.videoId)
this.ctxSave = document.getElementById('canvas')
this.canSave = document.getElementById('canvasSave')
this.del = document.getElementById('del')
video.oncanplay = () => {
let videoWidth = video.videoWidth
let videoHeight = video.videoHeight
let heights = video.clientHeight //video 的高度
let Gcd = this.getGcd(videoWidth, videoHeight) //获取最大公约数
let width = videoWidth / Gcd //宽高的比例
let height = videoHeight / Gcd //宽高的比例
let itemHeight = heights / height //1份占的值
this.scaling = (videoHeight / heights).toFixed(2) //外面高与真实高的比
let widths = itemHeight * width //得到的最后真实video宽度
this.CanvasServer = new CanvasServer({
canvas: this.ctxSave,
canvasSave: this.canSave,
width: widths.toFixed(2),
height: heights.toFixed(2),
deleteCanvas: (val) => {
if (this.del) {
this.del.style.display = val
}
},
getData: (newArr) => {
this.arrData = newArr
this.$emit('finish',newArr)
}
})
let arr = pointColorArr.map(i => {
return i.map(j => {
let obj = {
x: (Number(j.x) / this.scaling).toFixed(2),
y: (Number(j.y) / this.scaling).toFixed(2)
}
return obj
})
})
this.CanvasServer.startDraw = true
this.CanvasServer.echoCanvas(arr || [])
this.startDraw = this.CanvasServer.startDraw
this.startDrawArea = this.CanvasServer.startDrawArea
}
video.onerror=()=>{
this.CanvasServer.reset()
}
},
getGcd(a, b) {
let n1, n2;
if (a > b) {
n1 = a;
n2 = b;
} else {
n1 = b;
n2 = a;
}
let remainder = n1 % n2;
if (remainder === 0) {
return n2;
} else {
return this.getGcd(n2, remainder)
}
},