做项目中有基于高德地图进行点、线、面的绘制,使用的是高德地图封装的SDK,实现难度不大。其实点、线、面的绘制原理都是在canvas上绘制的,于是想尝试不基于高德地图,直接在canvas上进行面的绘制,并支持适应canvas画布的放大缩小与拖动。所绘制的面的类型有多边形、矩形、圆形。
# 示例demo
http://121.4.85.237:7781
首先是先创建一个canvas元素,让canvas元素的宽高与父容器的宽高一样,并支持响应式。还有给canvas元素添加一些监听事件。
// HTML部分
<div class="canvas-container w-full h-full relative">
<canvas
id="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(() => {
// ...处理其他逻辑
});
},
这个canvas元素画布是支持画布平移和缩放的,所以需要先处理画布平移与缩放逻辑,再进行面的绘制。
在点击画布并拖拽鼠标后,此时如果没有开启面的绘制,就是要进行画布的平移操作。
在画布平移操作过程中,需要不断重绘画布上所有的面。
canvasDraw: {
isDrawCanvas: false,
point: [],
origin: { x: 0, y: 0 }, // canvas的原点,默认为(0, 0)
offset: { x: 0, y: 0 }, // canvas画布的偏移量
originScale: 1, // canvas画布当前缩放比,默认为1
preScale: 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;
},
画布缩放思路同画布平移类似,这里自己设置的每次放大都是放大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;
},
绘制面时要考虑此时的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();
}
});
},
有时需要在面的重心处添加一些文字提示,如面的名称、面的大小等。
矩形和圆形的重心点很容易计算,下面主要描述一下多边形的重心点计算。
# 多边形重心计算步骤
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;
},
实现起来有几点需要注意:
1.画布的平移缩放逻辑处理,这里要精准计算出偏移量,逻辑是有一点复杂的。
2.画布的清除,这里使用clearRect()
方法是清除不干净画布的。
3.在画布平移缩放后的面的绘制,这里要注意数据的处理逻辑,处理不对所绘制的面会与鼠标点击的位置产生偏移。