绘制与凸包算法实现:使用Vue.js和HTML5 Canvas

前言

在本文中,我们将探讨如何使用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到游戏开发,再到教育和艺术创作,此技术的应用范围广泛,充满无限潜力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值