canvas根据坐标点位画图形-canvas拖拽编辑单个图形形状
- 首先在选中图形的时候需要用鼠标右击来弹出选择框,实现第一个编辑节点功能
- 在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 : point1const isAttract = len <= pixelAreareturn {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 = ireturn 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 * adsorptionDistancereturn {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.offsetXlet 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.offsetXlet y = e.offsetYif (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.offsetXlet 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 : 50if (wheelDelta > 0) {if (this.nowScale >= 1600) {this.nowScale = 1600return}this.nowScale = parseInt(this.nowScale + 1 * this.nowScale / interval)} else {if (this.nowScale <= 25) {this.nowScale = 25return}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 - moveXconst y = item[1] * scale - moveYnewGeo.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 : point1const isAttract = len <= pixelAreareturn {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 = ireturn 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 * adsorptionDistancereturn {x: px,y: py};}},
}export default mathUtils;
- drawMap.js
let ctxDom, mapCtx; //初始化必要参数const drawMap = {//初始化地图initMap({id,w,h} = obj) {ctxDom = idid.width = wid.height = hmapCtx = 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 = strokeStylemapCtx.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
组件页请参考顶部!!!