canvas根据坐标点位画图形-canvas拖拽编辑单个图形形状

  1. 首先在选中图形的时候需要用鼠标右击来弹出选择框,实现第一个编辑节点功能
    在这里插入图片描述
  • 在components文件夹下新建右键菜单
    在这里插入图片描述
  • RightMenu文件:
<template>
	<div v-show="show" class="right-menu" :style="'top:'+this.y+'px;left:'+this.x+'px'">
		<div @click="handelMenu('editPoint')">编辑节点</div>
		<div @click="handelMenu('stretch')">拉伸</div>
		<div @click="handelMenu('rotate')">旋转</div>
		<div @click="handelMenu('copy')">复制</div>
		<div @click="handelMenu('paste')">粘贴</div>
		<div @click="handelMenu('delete')">删除</div>
	</div>
</template>

<script>
	export default {
		data() {
			return {
				show: false,
				x: 0,
				y: 0,
			}
		},
		
		methods: {
			showModal(x,y) {
				this.x = x;
				this.y = y;
				this.show = true;
			},
			handelMenu(e) {
				this.hideMenu();
				this.$emit('backRightMenu',e);
			},
			hideMenu() {
				this.show = false;
			}
		}
	}
</script>

<style scoped>
	.right-menu {
		width: 100px;
		position: relative;
		background: #fff;
		min-height: 50px;
	}
	.right-menu>div {
		height: 30px;
		line-height: 30px;
		border-bottom: 1px solid rgb(228, 235, 249);
		padding: 0 10px;
		font-size: 13px;
		cursor: pointer;
	}
</style>
  • 在页面中使用:
<right-menu ref="RightMenu" @backRightMenu="backRightMenu"></right-menu>

import RightMenu from '@/components/RightMenu/index';

components: {
	RightMenu
},
  • 在data中定义所需要的变量
rightMenuType: '', //可操作图形状态
isRightMenu: false, //是否可以操作图形
  1. 鼠标右击的时候打开右键菜单
//鼠标右击
rightMenu(e) {
	if (this.type === 'move' && this.activeData.length > 0 && this.rightMenuType === '') {
		this.$refs.RightMenu.showModal(e.offsetX, e.offsetY);
	} else {
		return;
	}
},
  1. 点击选择选项接收值
//右键菜单返回
backRightMenu(e) {
	this.rightMenuType = e;
	this.isRightMenu = true;
	//编辑图形
	switch (e) {
		case 'editPoint':
			this.redrawMap();
			break;
	}
}
  1. redrawMap重绘过程中判断如果在编辑图形的状态,就选中图形并且图形顶点高亮
if(this.activeData.length > 0 && this.isRightMenu) {
	//编辑图形
	switch (this.rightMenuType) {
		case 'editPoint':
			drawMap.drawRectangle(this.activeData, 'editPoint');
			break;
	}
}
  • 效果如下:
    在这里插入图片描述
  1. 接下来实现吸附顶点,首先鼠标移动过程中判断是否吸附顶点,吸附状态下拖动点位可以更改图形点位坐标
// 开启吸附功能 记录是否处于吸附状态
if (this.activeData.length > 0 && ['editPoint'].includes(this.rightMenuType)) {
	this.activeData.map((item, idx) => {
		let result = mathUtils.attractPixel([x, y], item)
		
		if(result.isAttract && this.isMouseClick) {
			if(idx === 0 || idx === this.activeData.length - 1) {
				this.$set(this.activeData,this.activeData.length - 1,[x,y]);
			}
			this.$set(this.activeData,idx,[x,y]);
		}
	})
}
  • 涉及的算法:
// 计算两点距离
dealLength(start, end) {
	var a = end.x - start.x;
	var b = end.y - start.y;
	return Math.sqrt(a * a + b * b);
},
// 鼠标点击位置和目标点相距<=吸附半径则吸附到目标点
attractPixel(point1, pointTarget, pixelArea = adsorptionDistance) {
	const len = this.dealLength({
		x: point1[0],
		y: point1[1]
	}, {
		x: pointTarget[0],
		y: pointTarget[1]
	})
	const finalPoint = len <= pixelArea ? pointTarget : point1
	const isAttract = len <= pixelArea
	return {
		finalPoint,
		isAttract
	}
},
  • 效果如下:
    在这里插入图片描述
  1. 接下来实现在鼠标按下过程中如果触碰了图形的边线,就给点击边线的位置插入一个点位,形成多边形
//鼠标按下判断选中边线按下插入节点
if (this.activeData.length > 0 && ['editPoint'].includes(this.rightMenuType)) {
	const pointData = mathUtils.attractOnCheckLine({x,y}, this.activeData);
	if(pointData && pointData.overIdx >= 0) {
		this.activeData.splice(pointData.overIdx + 1, 0, [x, y])
	}
}
  • 效果如下:
    在这里插入图片描述
  • 涉及算法:
// 计算当前点到所有线段的垂点,小于5px,则吸附
attractOnCheckLine(point, coordinates) {
	for (var i = 0; i < coordinates.length; i++) {
		if (this.checkPoint(coordinates[i], point)) {
			return {
				x: coordinates[i][0],
				y: coordinates[i][1],
				idx: i
			};
		}
	}
	for (var i = 0; i < coordinates.length - 1; i++) {
		var pt = this.pointToSegDist(point.x, point.y, coordinates[i][0], coordinates[i][1],
			coordinates[i + 1][0], coordinates[i + 1][1], Math.pow(adsorptionDistance, 2));
		if (pt) {
			pt.overIdx = i
			return pt;
		}
	}
	return null;
},
checkPoint(target, point) {
	if (point.x >= target[0] - adsorptionDistance &&
		point.x <= target[0] + adsorptionDistance &&
		point.y >= target[1] - adsorptionDistance &&
		point.y <= target[1] + adsorptionDistance) {
		return true;
	} else {
		return false;
	}
},
pointToSegDist(x, y, x1, y1, x2, y2, dist) {
	var cross = (x2 - x1) * (x - x1) + (y2 - y1) * (y - y1);
	if (cross <= 0) return null;
	var d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
	if (cross >= d2) return null;
	var r = cross / d2;
	var px = x1 + (x2 - x1) * r;
	var py = y1 + (y2 - y1) * r;
	var dis = (x - px) * (x - px) + (py - y) * (py - y);
	if (dis <= dist) { // adsorptionDistance * adsorptionDistance
		return {
			x: px,
			y: py
		};
	}
},
  • 接下来插入的点位也可以进行拖拽了,大功告成!!!
    在这里插入图片描述

本节其他文件附下,复制可用:

  • 首页
<template>
	<div id="app">
		<div class="nav-top">
			<div :class="{'nav-sel':type==='move'}" @click="setType('move')">选择</div>
			<div :class="{'nav-sel':type==='rectangle'}" @click="setType('rectangle')">矩形</div>
			<div :class="{'nav-sel':type==='circle'}" @click="setType('circle')">圆形</div>
		</div>
		<div class="draw-box" ref="drawBox">
			<canvas class="canvas-style" ref="canvasMap" @click="mapClick" @mousedown="mapMousedown"
				@mousemove="mapMousemove" @mouseup="mapMouseUp" @dblclick="mapDbclick"
				@mousewheel.prevent="mapMouseWheel" @contextmenu.prevent="rightMenu"></canvas>
		</div>
		<right-menu ref="RightMenu" @backRightMenu="backRightMenu"></right-menu>
	</div>
</template>

<script>
	import drawMap from '@/utils/drawMap.js';
	import mathUtils from '@/utils/mathUtils.js';
	import RightMenu from '@/components/RightMenu/index';
	export default {
		name: 'app',
		data() {
			return {
				type: 'rectangle', //当前可编辑图形的状态
				mouseStartPos: [], //鼠标点击的位置
				mouseMovePos: [0, 0], //鼠标移动位置与图形中心点位置的差值
				mouseClickArr: [], //当前已点击的坐标记录
				drawAllData: [], //当前所有保存的数据
				activeData: [], //当前选中的图形坐标数据
				isMouseClick: false, //是否按住鼠标左键
				nowScale: 100, //初始化滚动大小
				lastScale: 100, //最后一次滚动大小
				rightMenuType: '', //可操作图形状态
				isRightMenu: false, //是否可以操作图形
			}
		},

		components: {
			RightMenu
		},

		mounted() {
			//初始化画板
			const initData = {
				id: this.$refs.canvasMap,
				w: this.$refs.drawBox.clientWidth,
				h: this.$refs.drawBox.clientHeight
			}
			drawMap.initMap(initData);
			this.redrawMap();
		},

		methods: {
			//单击地图
			mapClick(e) {
				let x = e.offsetX
				let y = e.offsetY

				//非操作点击空白


				//点击地图加入点位
				switch (this.type) {
					case 'rectangle':
						this.mouseClickArr.push([x, y])
						if (this.mouseClickArr.length === 3) {
							this.drawRectangle(this.mouseClickArr)
							this.redrawMap()
							this.mouseClickArr = []
						}
						break;
				}
			},
			//鼠标按下
			mapMousedown(e) {
				let x = e.offsetX
				let y = e.offsetY

				if (e.button === 2) {
					// 鼠标右击
					this.redrawMap()
					return
				}

				this.mouseStartPos = [e.offsetX, e.offsetY]

				this.isMouseClick = true; //鼠标左键已按下,可以进行平移操作
				
				//鼠标按下判断选中边线按下插入节点
				if (this.activeData.length > 0 && ['editPoint'].includes(this.rightMenuType)) {
					const pointData = mathUtils.attractOnCheckLine({x,y}, this.activeData);
					if(pointData && pointData.overIdx >= 0) {
						this.activeData.splice(pointData.overIdx + 1, 0, [x, y])
					}
				}

				if (this.type === 'move' && this.isMouseClick) {
					let activePoint = []
					if (this.drawAllData.length > 0) {
						for (const [i, item] of this.drawAllData.entries()) {
							mathUtils.pointInPolygonORLine(this.mouseStartPos, item) === true ? activePoint = item : []
						}
					}
					if (this.activeData.length > 0 && !mathUtils.pointInPolygonORLine(this.mouseStartPos, this
						.activeData)) {
						this.drawAllData = this.drawAllData.concat([this.activeData])
						this.activeData = [];
					} else if (this.activeData.length === 0) {
						this.activeData = activePoint;
						this.drawAllData = this.drawAllData.filter(item => {
							return item !== this.activeData
						})
					}

					this.redrawMap();
				}
			},
			//鼠标移动
			mapMousemove(e) {
				let x = e.offsetX
				let y = e.offsetY

				// 开启吸附功能 记录是否处于吸附状态
				if (this.activeData.length > 0 && ['editPoint'].includes(this.rightMenuType)) {
					this.activeData.map((item, idx) => {
						let result = mathUtils.attractPixel([x, y], item)
						
						if(result.isAttract && this.isMouseClick) {
							if(idx === 0 || idx === this.activeData.length - 1) {
								this.$set(this.activeData,this.activeData.length - 1,[x,y]);
							}
							this.$set(this.activeData,idx,[x,y]);
						}
					})
				}

				//鼠标移动中判断当前是否状态是move,activeData当前选中是否有数据,isMouseClick当前是否可以移动 isRightMenu当前是否不能编辑图形
				if (this.type === 'move' && this.activeData.length > 0 && this.isMouseClick && !this.isRightMenu) {
					//获取图形中心位置
					const center = mathUtils.getPolygonCenter(this.activeData);
					//计算点击位置与图形中心位置的差值,如果差值大于0或小于0代表移动了
					this.mouseMovePos = [x - center[0], y - center[1]]
					//移动图形
					this.movePoint(this.mouseMovePos, this.activeData)
				}

				this.redrawMap({
					x,
					y
				})
			},
			//鼠标抬起
			mapMouseUp(e) {
				this.isMouseClick = false; //禁止移动
				this.mouseStartPos = []; //抬起后开始点击位置清空
				this.mouseMovePos = [0, 0]; //清空两次点位的差值,按下后重新计算
			},
			//鼠标双击
			mapDbclick(e) {
				console.log('鼠标双击', e);
			},
			//鼠标滚轮
			mapMouseWheel(e) {
				if (this.activeData.length === 0) return;
				const wheelDelta = e.wheelDelta //滚轮上下滚动的数值,默认为0,正数为向上滚动,负数为向下滚动
				const interval = this.nowScale <= 50 ? 25 : 50

				if (wheelDelta > 0) {
					if (this.nowScale >= 1600) {
						this.nowScale = 1600
						return
					}
					this.nowScale = parseInt(this.nowScale + 1 * this.nowScale / interval)
				} else {
					if (this.nowScale <= 25) {
						this.nowScale = 25
						return
					}
					this.nowScale = parseInt(this.nowScale - 1 * this.nowScale / interval)
				}

				this.redrawMap()
			},
			//鼠标右击
			rightMenu(e) {
				if (this.type === 'move' && this.activeData.length > 0 && this.rightMenuType === '') {
					this.$refs.RightMenu.showModal(e.offsetX, e.offsetY);
				} else {
					return;
				}
			},
			async redrawMap(point) {
				//canvas重绘
				drawMap.redrawMap();

				//保存滚动后的数据
				this.savePointData();

				//实时画鼠标点位
				point && point.x && drawMap.drawCircle({
					x: point.x,
					y: point.y,
					r: 4,
					fillStyle: '#fff'
				})

				//绘制已经保存的房间数据
				if (this.drawAllData.length > 0) {
					for (const [i, item] of this.drawAllData.entries()) {
						drawMap.drawRectangle(item);
					}
				}

				//绘制正在编辑的数据
				if (this.activeData.length > 0) {
					drawMap.drawRectangle(this.activeData, true);
				}

				//实时的画各类图形
				point && point.x && this.drawNowDrawing(point.x, point.y);

				if(this.activeData.length > 0 && this.isRightMenu) {
					//编辑图形
					switch (this.rightMenuType) {
						case 'editPoint':
							drawMap.drawRectangle(this.activeData, 'editPoint');
							break;
						case 'stretch':
					
							break;
						case 'rotate':
					
							break;
						case 'copy':
					
							break;
						case 'paste':
					
							break;
						case 'delete':
					
							break;
					}
				}
			},
			//保存数据
			savePointData() {
				if (this.activeData.length > 0) {
					const oCenter = mathUtils.getPolygonCenter(this.activeData);
					this.activeData = mathUtils.scalePoint(this.activeData, this.nowScale / this.lastScale, oCenter);
					this.lastScale = this.nowScale
				} else {
					this.nowScale = 100;
					this.lastScale = 100;
				}

			},
			//实时画图形
			drawNowDrawing(x, y) {
				switch (this.type) {
					case 'rectangle':
						if (this.mouseClickArr.length >= 1) {
							const mouseClick = this.mouseClickArr.length === 1 ? [
								[x, y],
								[x, y]
							] : [
								[x, y]
							]
							const newArr = this.mouseClickArr.concat(mouseClick)
							this.drawRectangle(newArr)
						}
						break;
				}
			},
			//画矩形
			drawRectangle(arr) {
				// 画矩形,点选三个点完成一个矩形
				const vPoint = mathUtils.calculateVerticalPoint(arr);
				// 根据第一点算的为第四点 根据第二点算的为第三点
				const point4 = mathUtils.calculatePoint(vPoint, arr[0], arr[2]);
				const point3 = mathUtils.calculatePoint(vPoint, arr[1], arr[2]);
				const rectangleData = [arr[0], arr[1], point3, point4, arr[0]];
				if (this.mouseClickArr.length === 3) {
					this.drawAllData = this.drawAllData.concat([rectangleData])
				}
				drawMap.drawRectangle(rectangleData);
			},
			//图形平移,通过差值计算点位坐标
			movePoint(movePos, data) {
				this.activeData = data.map(item => {
					return [item[0] + movePos[0], item[1] + movePos[1]]
				})
			},
			//设置可编辑类型
			setType(e) {
				this.type = e
			},
			//右键菜单返回
			backRightMenu(e) {
				this.rightMenuType = e;
				this.isRightMenu = true;
				//编辑图形
				switch (e) {
					case 'editPoint':
						this.redrawMap();
						break;
					case 'stretch':

						break;
					case 'rotate':

						break;
					case 'copy':

						break;
					case 'paste':

						break;
					case 'delete':

						break;
				}
			}
		}
	}
</script>

<style>
	html,
	body {
		margin: 0;
		padding: 0;
	}

	.nav-top {
		display: flex;
		align-items: center;
	}

	.nav-top>div {
		padding: 10px;
		border: 1px solid;
		border-radius: 8px;
		margin-right: 20px;
		cursor: pointer;
	}

	.nav-top .nav-sel {
		border: 2px solid #18c1f6;
	}

	.draw-box {
		width: 100vw;
		height: calc(100vh - 64px);
		background: #F1F2F6;
		position: fixed;
		bottom: 0;
	}

	.hidden-icon {
		position: absolute;
		top: 0;
		z-index: -100;
		left: 0;
		visibility: hidden;
	}

	.del-icon {
		width: 16px;
		transform: translate(-8px, -8px);
		user-select: none;
	}
</style>
  • mathUtils.js
import * as turf from "@/utils/turf.es";

let adsorptionDistance = 6
const mathUtils = {
	// 计算两点距离
	dealLength(start, end) {
		var a = end.x - start.x;
		var b = end.y - start.y;
		return Math.sqrt(a * a + b * b);
	},
	// 计算点到线垂点的方法
	calculateVerticalPoint(arr) {
		const point = arr[2]

		var x1 = arr[0][0];
		var y1 = arr[0][1];
		var x2 = arr[1][0];
		var y2 = arr[1][1]
		if (x1 == x2 && y1 == y2) {
			return [point[0], point[1]];
		}
		var m = point[0];
		var n = point[1];
		var a = y2 - y1;
		var b = x1 - x2;
		var c = x2 * y1 - x1 * y2;
		var x3 = (b * b * m - a * b * n - a * c) / (a * a + b * b);
		var y3 = (a * a * n - a * b * m - b * c) / (a * a + b * b);
		return [Math.round(x3 * 100) / 100, Math.round(y3 * 100) / 100];
	},
	// 根据垂点计算平行点
	calculatePoint(vPoint, point, point2) {
		const x = point[0] - vPoint[0] + point2[0]
		const y = point[1] - vPoint[1] + point2[1]
		return [x, y]
	},
	// 判断点是否在多边形内部或者线上
	pointInPolygonORLine(point, polygon) {
		var pt = turf.point(point);
		var poly = turf.polygon([polygon]);
		return turf.booleanPointInPolygon(pt, poly)
	},
	// 获取多边形的中心点
	getPolygonCenter(arr) {
		var polygon = turf.polygon([arr]);
		var center = turf.centerOfMass(polygon);
		return center.geometry.coordinates
	},
	// 获取缩放后的坐标
	scalePoint(oGeo, scale, oCenter) {
		const newGeo = []
		const moveX = oCenter[0] * scale - oCenter[0]
		const moveY = oCenter[1] * scale - oCenter[1]
		for (var item of oGeo) {
			const x = item[0] * scale - moveX
			const y = item[1] * scale - moveY
			newGeo.push([x, y])
		}
		return newGeo
	},
	// 鼠标点击位置和目标点相距<=吸附半径则吸附到目标点
	attractPixel(point1, pointTarget, pixelArea = adsorptionDistance) {
		const len = this.dealLength({
			x: point1[0],
			y: point1[1]
		}, {
			x: pointTarget[0],
			y: pointTarget[1]
		})
		const finalPoint = len <= pixelArea ? pointTarget : point1
		const isAttract = len <= pixelArea
		return {
			finalPoint,
			isAttract
		}
	},
	// 计算当前点到所有线段的垂点,小于5px,则吸附
	attractOnCheckLine(point, coordinates) {
		for (var i = 0; i < coordinates.length; i++) {
			if (this.checkPoint(coordinates[i], point)) {
				return {
					x: coordinates[i][0],
					y: coordinates[i][1],
					idx: i
				};
			}
		}
		for (var i = 0; i < coordinates.length - 1; i++) {
			var pt = this.pointToSegDist(point.x, point.y, coordinates[i][0], coordinates[i][1],
				coordinates[i + 1][0], coordinates[i + 1][1], Math.pow(adsorptionDistance, 2));
			if (pt) {
				pt.overIdx = i
				return pt;
			}
		}
		return null;
	},
	checkPoint(target, point) {
		if (point.x >= target[0] - adsorptionDistance &&
			point.x <= target[0] + adsorptionDistance &&
			point.y >= target[1] - adsorptionDistance &&
			point.y <= target[1] + adsorptionDistance) {
			return true;
		} else {
			return false;
		}
	},
	pointToSegDist(x, y, x1, y1, x2, y2, dist) {
		var cross = (x2 - x1) * (x - x1) + (y2 - y1) * (y - y1);
		if (cross <= 0) return null;
		var d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
		if (cross >= d2) return null;
		var r = cross / d2;
		var px = x1 + (x2 - x1) * r;
		var py = y1 + (y2 - y1) * r;
		var dis = (x - px) * (x - px) + (py - y) * (py - y);
		if (dis <= dist) { // adsorptionDistance * adsorptionDistance
			return {
				x: px,
				y: py
			};
		}
	},
}

export default mathUtils;
  • drawMap.js
let ctxDom, mapCtx; //初始化必要参数

const drawMap = {
	//初始化地图
	initMap({
		id,
		w,
		h
	} = obj) {
		ctxDom = id
		id.width = w
		id.height = h
		mapCtx = id.getContext("2d");
	},
	//地图重绘
	redrawMap() {
		mapCtx.clearRect(0, 0, ctxDom.width, ctxDom.height);
	},
	//画圆
	drawCircle({
		x,
		y,
		r,
		strokeStyle = '#1289ff80', //边框色
		fillStyle = '#fff0', //填充色
	} = obj) {
		mapCtx.beginPath();
		mapCtx.fillStyle = fillStyle;
		mapCtx.setLineDash([]);
		mapCtx.strokeStyle = strokeStyle
		mapCtx.arc(x, y, r, 0, 2 * Math.PI);
		mapCtx.closePath();
		mapCtx.stroke();
		mapCtx.fill();
	},
	drawRectangle(arr, isCheck) {
		mapCtx.strokeStyle = isCheck ? '#1289ff' : '#1289ff80';
		mapCtx.fillStyle = isCheck ? '#ffffff80' : '#fff0';
		mapCtx.lineWidth = 2;
		mapCtx.setLineDash([]);
		mapCtx.lineJoin = 'bevel';
		mapCtx.beginPath();
		mapCtx.moveTo(arr[0][0], arr[0][1]);
		for (let i = 1; i < arr.length; i++) {
			mapCtx.lineTo(arr[i][0], arr[i][1]);
		}
		mapCtx.stroke();
		mapCtx.fill();
		if (isCheck == 'editPoint') {
		  for (let i = 0; i < arr.length; i++) {
		    this.drawCircle({
		      x: arr[i][0],
		      y: arr[i][1],
		      r: 3,
		      strokeStyle: '#1289ff80',
		      fillStyle: '#fff'
		    })
		  }
		}
	},
}

export default drawMap

组件页请参考顶部!!!

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值