前言
在本文中,我们将探讨如何使用Vue.js框架结合HTML5 Canvas来实现一个简单的绘图应用。该应用将允许用户在画布上绘制点,并且能够计算这些点的凸包(convex hull),以及基于凸包生成一个缓冲区。这不仅可以作为一个有趣的绘图工具,还可以作为学习计算机图形学和几何算法的实践项目。
核心功能实现
画布初始化与事件绑定:使用Vue的mounted
钩子进行初始化,并为画布绑定点击事件,用于捕捉用户输入。
凸包算法实现:采用Andrew's monotone chain算法计算给定点集的凸包,形成最小凸多边形。
缓冲区绘制:根据凸包边界和预设宽度,计算并绘制缓冲区,形成禁止驶入的视觉提示。
绘制控制:通过按钮控制绘制的开启与暂停,提高应用的交互性。
实现步骤
1. 创建Vue组件
首先,我们需要创建一个Vue组件,它将包含我们的画布和控制按钮。
<template>
<!-- 定义一个包含按钮和画布的容器 -->
<div class="content">
<el-button type="primary" icon="EditPen" color="#3AA965" plain class="buttonClass" @click="pauseDrawing">{{ isPaused ? '暂停绘制' : '开启绘制' }}</el-button>
<canvas ref="canvas" width="800" height="600" @click="addPoint"></canvas>
</div>
</template>
2. 定义数据和方法
在Vue组件的<script>
部分,我们定义了组件的数据和方法。
export default {
data() {
return {
points: [], // 存储点位
boundaryWidth: 50, // 边界宽度,可以根据需要调整
isPaused: false, // 控制是否暂停绘制
};
},
mounted() {
// 当组件被挂载后,开始绘制画布
this.draw();
},
methods: {
// 切换绘制状态的函数
pauseDrawing() {
this.isPaused = !this.isPaused;
},
// 绘制画布的方法
draw() {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext('2d');
// 清空画布,准备重新绘制
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 加载并绘制背景图片
const backgroundImage = new Image();
backgroundImage.src = 'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg'; // 替换为你的图片路径
backgroundImage.onload = () => {
ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
if (this.points.length > 0) {
// ...开始绘制点位、闭合区域、禁止驶入区域
}
};
},
addPoint(event) {
// ...当用户点击画布时,获取点击位置相对于画布左上角的坐标然后重新绘制画布
},
calculateConvexHull(points) {
// ...计算凸包的方法,Andrew's monotone chain algorithm 计算凸包
},
cross(o, a, b) {
// ...计算向量叉乘的方法,用于凸包算法
},
calculateNormal(p1, p2) {
// ...计算两个点之间的法线向量的方法
}
}
}
3. 样式定义
最后,我们定义了一些基本的CSS样式来美化我们的组件。
<style scoped>
.content{
display: flex;
flex-direction: column;
align-items: flex-start;
width: 800px;
}
.buttonClass{
margin: 10px 0;
margin-left: auto;
}
canvas {
border: 1px solid #000;
}
</style>
4.完整代码展示
<template>
<!-- 定义一个包含按钮和画布的容器 -->
<div class="content">
<el-button type="primary" icon="EditPen" color="#3AA965" plain class="buttonClass" @click="pauseDrawing">{{ isPaused ? '暂停绘制' : '开启绘制' }}</el-button>
<canvas ref="canvas" width="800" height="600" @click="addPoint"></canvas>
</div>
</template>
<script>
export default {
data() {
return {
points: [], // 存储点位
boundaryWidth: 50, // 边界宽度,可以根据需要调整
isPaused: false, // 控制是否暂停绘制
};
},
mounted() {
// 当组件被挂载后,开始绘制画布
this.draw();
},
methods: {
// 切换绘制状态的函数
pauseDrawing() {
this.isPaused = !this.isPaused;
},
// 绘制画布的方法,包括清空画布、绘制背景、绘制点位、凸包和缓冲区
draw() {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext('2d');
// 清空画布,准备重新绘制
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 加载并绘制背景图片
const backgroundImage = new Image();
backgroundImage.src = 'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg'; // 替换为你的图片路径
backgroundImage.onload = () => {
ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
if (this.points.length > 0) {
// 计算凸包
const convexHull = this.calculateConvexHull(this.points);
// 计算缓冲区路径
const bufferedPath = this.calculateBufferedPath(convexHull, this.boundaryWidth);
// 1.绘制点位
ctx.fillStyle = 'red';
this.points.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
ctx.fill();
});
// 2.绘制闭合区域
ctx.fillStyle = 'rgba(0, 0, 255, 0.3)';
ctx.beginPath();
convexHull.forEach((point, index) => {
if (index === 0) {
ctx.moveTo(point.x, point.y);
} else {
ctx.lineTo(point.x, point.y);
}
});
ctx.closePath();
ctx.fill();
// 3..绘制禁止驶入区域
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.beginPath();
bufferedPath.forEach((point, index) => {
if (index === 0) {
ctx.moveTo(point.x, point.y);
} else {
ctx.lineTo(point.x, point.y);
}
});
ctx.closePath();
ctx.fill();
}
};
},
// 当用户点击画布时,调用此方法添加点位
addPoint(event) {
if (!this.isPaused) return // 如果当前处于暂停状态,则不添加点位
// 计算点击位置相对于画布左上角的坐标
const rect = this.$refs.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.points.push({ x, y });
// 重新绘制
this.draw();
},
// 计算凸包的方法,使用Andrew's monotone chain算法
calculateConvexHull(points) {
// Andrew's monotone chain algorithm 计算凸包
const sortPoints = points.slice().sort((a, b) => a.x - b.x || a.y - b.y);
const lower = [];
for (const point of sortPoints) {
while (lower.length >= 2 &&
this.cross(lower[lower.length - 2], lower[lower.length - 1], point) <= 0) {
lower.pop();
}
lower.push(point);
}
const upper = [];
for (const point of sortPoints.slice().reverse()) {
while (upper.length >= 2 &&
this.cross(upper[upper.length - 2], upper[upper.length - 1], point) <= 0) {
upper.pop();
}
upper.push(point);
}
upper.pop();
lower.pop();
return lower.concat(upper);
},
// 计算向量叉乘的方法,用于凸包算法
cross(o, a, b) {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
},
// 计算缓冲区路径的方法
calculateBufferedPath(points, bufferWidth) {
const offsetPoints = [];
for (let i = 0; i < points.length; i++) {
const prev = points[i === 0 ? points.length - 1 : i - 1];
const current = points[i];
const next = points[(i + 1) % points.length];
// 计算法线向量
const normal1 = this.calculateNormal(prev, current);
const normal2 = this.calculateNormal(current, next);
// 法线向量的平均值
const averageNormal = {
x: (normal1.x + normal2.x) / 2,
y: (normal1.y + normal2.y) / 2
};
// 归一化平均法线向量
const length = Math.sqrt(averageNormal.x * averageNormal.x + averageNormal.y * averageNormal.y);
const normalizedNormal = {
x: averageNormal.x / length,
y: averageNormal.y / length
};
// 计算偏移点
offsetPoints.push({
x: current.x - normalizedNormal.x * bufferWidth,
y: current.y - normalizedNormal.y * bufferWidth
});
}
return offsetPoints;
},
// 计算两个点之间的法线向量的方法
calculateNormal(p1, p2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const length = Math.sqrt(dx * dx + dy * dy);
return {
x: -dy / length,
y: dx / length
};
}
}
};
</script>
<style scoped>
.content{
display: flex;
flex-direction: column;
align-items: flex-start;
width: 800px;
}
.buttonClass{
margin: 10px 0;
margin-left: auto;
}
canvas {
border: 1px solid #000;
}
</style>
结论
通过本文的步骤,我们实现了一个简单的绘图应用,它不仅能够让用户自由地在画布上绘制点,还能够展示这些点的凸包和基于凸包的缓冲区。这个项目是学习Vue.js和Canvas API的好方法,同时也能够加深对几何算法的理解。交互式绘图应用的构建不仅展示了Vue.js和Canvas的强大功能,也为不同领域的应用提供了新的可能性。从GIS到游戏开发,再到教育和艺术创作,此技术的应用范围广泛,充满无限潜力。