- 首先在选中图形的时候需要用鼠标右击来弹出选择框,实现第一个编辑节点功能
- 在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, //是否可以操作图形
- 鼠标右击的时候打开右键菜单
//鼠标右击
rightMenu(e) {
if (this.type === 'move' && this.activeData.length > 0 && this.rightMenuType === '') {
this.$refs.RightMenu.showModal(e.offsetX, e.offsetY);
} else {
return;
}
},
- 点击选择选项接收值
//右键菜单返回
backRightMenu(e) {
this.rightMenuType = e;
this.isRightMenu = true;
//编辑图形
switch (e) {
case 'editPoint':
this.redrawMap();
break;
}
}
- redrawMap重绘过程中判断如果在编辑图形的状态,就选中图形并且图形顶点高亮
if(this.activeData.length > 0 && this.isRightMenu) {
//编辑图形
switch (this.rightMenuType) {
case 'editPoint':
drawMap.drawRectangle(this.activeData, 'editPoint');
break;
}
}
- 效果如下:
- 接下来实现吸附顶点,首先鼠标移动过程中判断是否吸附顶点,吸附状态下拖动点位可以更改图形点位坐标
// 开启吸附功能 记录是否处于吸附状态
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
}
},
- 效果如下:
- 接下来实现在鼠标按下过程中如果触碰了图形的边线,就给点击边线的位置插入一个点位,形成多边形
//鼠标按下判断选中边线按下插入节点
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
组件页请参考顶部!!!