> 文章列表 > 基于canvas的平移缩放进行面的绘制

基于canvas的平移缩放进行面的绘制

基于canvas的平移缩放进行面的绘制

做项目中有基于高德地图进行点、线、面的绘制,使用的是高德地图封装的SDK,实现难度不大。其实点、线、面的绘制原理都是在canvas上绘制的,于是想尝试不基于高德地图,直接在canvas上进行面的绘制,并支持适应canvas画布的放大缩小与拖动。所绘制的面的类型有多边形、矩形、圆形。

# 示例demo
http://121.4.85.237:7781

1.定义canvas元素

首先是先创建一个canvas元素,让canvas元素的宽高与父容器的宽高一样,并支持响应式。还有给canvas元素添加一些监听事件。

// HTML部分
<div class="canvas-container w-full h-full relative"><canvasid="canvas":style="{ width: cssWidth + 'px', height: cssHeight + 'px' }"></canvas>
</div>// JS部分
mounted() {this.canvas = this.firstResize();this.canvas.addEventListener("mousedown", this.addPoint);this.canvas.addEventListener("mouseup", this.upPoint);this.canvas.addEventListener("mousemove", this.mousePoint);this.canvas.addEventListener("contextmenu", this.endPoint);this.canvas.addEventListener("mousewheel", this.wheelCanvas);if (this.canvas.getContext) {this.ctx = this.canvas.getContext("2d");}window.addEventListener("resize", () => {this.commomResize();});
},firstResize() {const canvas = document.getElementById("canvas");const container = document.getElementsByClassName("canvas-container")[0];canvas.width = container.offsetWidth;canvas.height = container.offsetHeight;this.cssWidth = container.offsetWidth;this.cssHeight = container.offsetHeight;return canvas;
},
// canvas大小响应式
commomResize() {this.firstResize();this.$nextTick(() => {// ...处理其他逻辑});
},

2.canvas画布的平移

这个canvas元素画布是支持画布平移和缩放的,所以需要先处理画布平移与缩放逻辑,再进行面的绘制。

在点击画布并拖拽鼠标后,此时如果没有开启面的绘制,就是要进行画布的平移操作。

在画布平移操作过程中,需要不断重绘画布上所有的面。

canvasDraw: {isDrawCanvas: false,point: [],origin: { x: 0, y: 0 }, // canvas的原点,默认为(0, 0)offset: { x: 0, y: 0 }, // canvas画布的偏移量originScale: 1, // canvas画布当前缩放比,默认为1preScale: 1,    // canvas画布上一次的缩放比
},

核心代码如下所示:

windowToCanvas(x, y) {const box = this.canvas.getBoundingClientRect();return {x: x - box.left,y: y - box.top,};
},
// canvas的mousedown事件
addPoint(e) {if (this.isDraw) {// 面的绘制} else {// 画布的平移const res = this.windowToCanvas(e.clientX, e.clientY);this.canvasDraw.isDrawCanvas = true;this.canvasDraw.point = res;}
},
// canvas的mousemove事件
mousePoint(e) {if (this.isDraw && this.point.length) {// ...} else if (this.canvasDraw.isDrawCanvas) {// 开启canvas画布的平移this.translateCnvas(e);}
},
// 移动canvas画布
translateCnvas(e) {const res = this.windowToCanvas(e.clientX, e.clientY);this.canvasDraw.offset.x =this.canvasDraw.origin.x + res.x - this.canvasDraw.point.x;this.canvasDraw.offset.y =this.canvasDraw.origin.y + res.y - this.canvasDraw.point.y;this.drawReset(); // 画布上所有面的重绘
},

在canvas画布有平移和缩放操作时,清除画布不能再使用clearRect()方法,此方法无法控制所需清除画布的大小。

此情况下可以重设canvas尺寸,就会清空并重置canvas内置的translate、scale等。

canvas属性重置后需要重新赋值。

// 重绘
drawReset() {this.clearCanvas(); // 清空画布this.ctx.translate(this.canvasDraw.offset.x, this.canvasDraw.offset.y);this.ctx.scale(this.canvasDraw.originScale, this.canvasDraw.originScale);this.drawOtherArea(); // 画布上绘制所有面
},
// 重设canvas尺寸会清空并重置canvas内置的scale等
clearCanvas() {this.canvas.width = this.cssWidth;
},

3.canvas画布的缩放

画布缩放思路同画布平移类似,这里自己设置的每次放大都是放大1.25倍,每次缩小都是缩小0.8倍。

// canvas的mousewheel事件
wheelCanvas(e) {e.preventDefault();const res = this.windowToCanvas(e.clientX, e.clientY);this.canvasDraw.preScale = this.canvasDraw.originScale;if (e.wheelDelta > 0) {// 放大this.canvasDraw.originScale = this.canvasDraw.originScale * 1.25;} else {// 缩小this.canvasDraw.originScale = this.canvasDraw.originScale * 0.8;}this.canvasDraw.offset.x =res.x -((res.x - this.canvasDraw.offset.x) * this.canvasDraw.originScale) /this.canvasDraw.preScale;this.canvasDraw.offset.y =res.y -((res.y - this.canvasDraw.offset.y) * this.canvasDraw.originScale) /this.canvasDraw.preScale;this.drawReset();this.canvasDraw.origin.x = this.canvasDraw.offset.x;this.canvasDraw.origin.y = this.canvasDraw.offset.y;
},

4.绘制面

绘制面时要考虑此时的canvas画布是否已经平移缩放过。多边形面的类型是polygon,矩形面的类型是rectangle,圆形面的类型是circle

当canvas画布已经平移或缩放过再进行面的绘制时,需要考虑到此时画布的偏移量与缩放大小。

windowToTransformCanvas(x, y) {const box = this.canvas.getBoundingClientRect();return {x:(x - box.left - this.canvasDraw.origin.x) /this.canvasDraw.originScale,y:(y - box.top - this.canvasDraw.origin.y) /this.canvasDraw.originScale,};
},
// canvas的mousedown事件
addPoint(e) {if (this.isDraw) {const res = this.windowToTransformCanvas(e.clientX, e.clientY);this.point.push({x: res.x,y: res.y,});}
},
// canvas的mousemove事件
mousePoint(e) {if (this.isDraw && this.point.length) {const res = this.windowToTransformCanvas(e.clientX, e.clientY);if (["rectangle"].includes(this.drawType)) {const w = Math.abs(this.point[0].x - res.x);const h = Math.abs(this.point[0].y - res.y);const left = this.point[0].x > res.x ? res.x : this.point[0].x;const top = this.point[0].y > res.y ? res.y : this.point[0].y;this.drawRectangle(left, top, w, h);} else if (["circle"].includes(this.drawType)) {const w = Math.abs(this.point[0].x - res.x);const h = Math.abs(this.point[0].y - res.y);const r = Math.sqrt(w * w + h * h);this.drawCircle(this.point[0], r);} else {this.drawPolygon(this.point.concat({x: res.x,y: res.y,}));}}
},

绘制矩形

// 绘制矩形
drawRectangle(left, top, w, h) {this.drawOtherArea();this.ctx.beginPath();this.ctx.strokeStyle = "red";this.ctx.fillStyle = "rgba(161, 195, 255, 0.5)";this.ctx.fillRect(left, top, w, h);this.ctx.strokeRect(left, top, w, h);
},

绘制圆形

// 绘制圆形
drawCircle(point, r) {this.drawOtherArea();this.ctx.beginPath();this.ctx.strokeStyle = "red";this.ctx.fillStyle = "rgba(161, 195, 255, 0.5)";this.ctx.arc(point.x, point.y, r, 0, 2 * Math.PI);this.ctx.fill();this.ctx.stroke();
},

绘制多边形

// 绘制多边形
drawPolygon(list) {this.drawOtherArea();this.ctx.beginPath();this.ctx.strokeStyle = "red";this.ctx.fillStyle = "rgba(161, 195, 255, 0.5)";this.ctx.moveTo(list[0].x, list[0].y);list.forEach((v) => {this.ctx.lineTo(v.x, v.y);});this.ctx.fill();this.ctx.closePath();this.ctx.stroke();
},

绘制之前绘制好的所有区域

drawOtherArea(params) {this.arrList.forEach((item, index) => {this.ctx.beginPath();this.ctx.strokeStyle = "red";this.ctx.fillStyle = "rgba(161, 195, 255, 0.5)";if (["circle"].includes(item.type)) {this.ctx.arc(item.data[0].x, item.data[0].y, item.r, 0, 2 * Math.PI);this.ctx.fill();this.ctx.stroke();} else if (["rectangle"].includes(item.type)) {this.ctx.fillRect(item.left, item.top, item.w, item.h);this.ctx.strokeRect(item.left, item.top, item.w, item.h);} else {this.ctx.moveTo(item.data[0].x, item.data[0].y);item.data.forEach((v) => {this.ctx.lineTo(v.x, v.y);});this.ctx.fill();this.ctx.closePath();this.ctx.stroke();}});
},

5.面的重心计算

有时需要在面的重心处添加一些文字提示,如面的名称、面的大小等。

矩形和圆形的重心点很容易计算,下面主要描述一下多边形的重心点计算。

# 多边形重心计算步骤
1. 求每个剖分出来的三角形的重心。重心计算公式:x = (x1+x2+x3)/3 ; y = (y1+y2+y3)/3
2. 求每个剖分出来的三角形的面积。三角形面积:area =(p0.x * p1.y +p1.x * p2.y +p2.x * p0.y -p0.x * p2.y -p1.x * p0.y -p2.x*p1.y)/2
3. 求多边形的面积。
4. 多边形重心横坐标 = 多边形剖分的每一个三角形重心的横坐标 * 该三角形的面积之和 / 多边形总面积多边形重心纵坐标 = 多边形剖分的每一个三角形重心的纵坐标 * 该三角形的面积之和 / 多边形总面积// 多边形的重心点(几何中心点)
getPolygonAreaCenter(points) {let sum_x = 0;let sum_y = 0;let sum_area = 0;let p1 = points[1];for (let i = 2; i < points.length; i++) {const p2 = points[i];const area = this.Area(points[0], p1, p2);sum_area += area;sum_x += (points[0].x + p1.x + p2.x) * area;sum_y += (points[0].y + p1.y + p2.y) * area;p1 = p2;}return {x: sum_x / sum_area / 3,y: sum_y / sum_area / 3,};
},
// 计算面积,这里是拆成一个个三角形进行的计算
Area(p0, p1, p2) {let area =(p0.x * p1.y +p1.x * p2.y +p2.x * p0.y -p0.x * p2.y -p1.x * p0.y -p2.x * p1.y) /2;return area;
},

6.总结

实现起来有几点需要注意:

1.画布的平移缩放逻辑处理,这里要精准计算出偏移量,逻辑是有一点复杂的。

2.画布的清除,这里使用clearRect()方法是清除不干净画布的。

3.在画布平移缩放后的面的绘制,这里要注意数据的处理逻辑,处理不对所绘制的面会与鼠标点击的位置产生偏移。