自学内容网 自学内容网

基于 Canvas 的可缩放拖动网格示例(Vue3以及TypeScript )

1. 基本知识

基本知识讲解:

  • Canvas API
    一种用于在网页上绘制图形的 HTML 元素,使用 JavaScript 的 Canvas API 来进行绘制
    使用 getContext('2d') 方法获取 2D 绘图上下文,允许开发者绘制矩形、文本、图像等

  • 缩放和拖动
    通过监听鼠标滚轮事件 (wheel) 可以实现缩放功能,使用 scale 来控制当前缩放比例
    鼠标按下、移动和松开事件实现了拖动功能,允许用户在 Canvas 上自由移动视图

  • 绘制网格
    在 Canvas 上绘制 6 行 26 列的网格,每个区块可以独立显示和命名
    通过动态计算坐标和应用样式,可以实现灵活的绘制效果

  • 响应式设计
    通过 overflow: hiddenposition: relative 样式设置,确保 Canvas 超出部分不会显示,提供良好的用户体验
    鼠标位置计算和缩放中心的设置使得用户操作更加直观和便捷

主要展示如何结合 Vue 3 和 Canvas API 创建一个灵活的用户界面组件,可以用于显示数据、图形或其他可视化内容

用户可以通过鼠标操作与 Canvas 进行交互,提升了整体的使用体验

2. Vue3

以下为完整示例

<template>
  <!-- 鼠标滚轮事件,调用缩放方法 -->
  <!-- 鼠标按下事件,开始拖动 -->
  <!-- 鼠标移动事件,更新拖动位置 -->
  <!-- 鼠标松开事件,结束拖动 -->
  <!-- 设置样式以隐藏溢出部分 -->
  <div
    @wheel.prevent="onWheel"  
    @mousedown="onMouseDown"   
    @mousemove="onMouseMove"   
    @mouseup="onMouseUp"       
    style="overflow: hidden; position: relative;" 
  >
  <!-- 获取 canvas 元素的引用 -->
  <!-- 设置 canvas 宽度 -->
  <!-- 设置 canvas 高度 -->
  <!-- 添加边框样式 -->
    <canvas
      ref="canvas"              
      :width="canvasWidth"     
      :height="canvasHeight"    
      style="border: 1px solid #000;" 
    ></canvas>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';

// 定义每个区块的接口
interface Block {
  block: string; // 区块名称
  x: number;     // 区块的 X 坐标
  y: number;     // 区块的 Y 坐标
  width: number; // 区块的宽度
  height: number; // 区块的高度
}

// 创建响应式引用
const canvas = ref<HTMLCanvasElement | null>(null); // canvas 引用
const ctx = ref<CanvasRenderingContext2D | null>(null); // canvas 的上下文
const scale = ref(1); // 当前缩放比例
const translatePos = ref({ x: 0, y: 0 }); // 当前偏移位置
const isDragging = ref(false); // 是否正在拖动
const startDragPos = ref({ x: 0, y: 0 }); // 开始拖动时的鼠标位置
const canvasWidth = ref(800); // canvas 初始宽度
const canvasHeight = ref(600); // canvas 初始高度
const options = {
  blockWidth: 30, // 区块宽度
  blockHeight: 30, // 区块高度
  padding: 10, // 区块间距
  startX: 50, // 起始 X 坐标
  startY: 50, // 起始 Y 坐标
};

// 绘制网格
const drawGrid = () => {
  if (!ctx.value) return; // 如果上下文不存在则返回
  
  ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value); // 清空画布
  ctx.value.save(); // 保存当前绘图上下文
  ctx.value.translate(translatePos.value.x, translatePos.value.y); // 应用偏移
  ctx.value.scale(scale.value, scale.value); // 应用缩放

  // 绘制 6 行 26 列的区块
  for (let row = 0; row < 6; row++) {
    for (let col = 0; col < 26; col++) {
      const x = options.startX + col * (options.blockWidth + options.padding); // 计算 X 坐标
      const y = options.startY + row * (options.blockHeight + options.padding); // 计算 Y 坐标
      const block: Block = {
        block: `${row + 1}-${col + 1}`, // 区块名称
        x,
        y,
        width: options.blockWidth,
        height: options.blockHeight,
      };

      drawBlock(block); // 绘制区块
      drawBlockNameText(block); // 绘制区块名称文本
    }
  }

  ctx.value.restore(); // 恢复绘图上下文到之前的状态
};

// 绘制单个区块
const drawBlock = (block: Block) => {
  if (!ctx.value) return; // 如果上下文不存在则返回
  ctx.value.strokeStyle = 'black'; // 设置边框颜色
  ctx.value.lineWidth = 1; // 设置边框宽度
  ctx.value.strokeRect(block.x, block.y, block.width, block.height); // 绘制矩形
};

// 绘制区块名称文本
const drawBlockNameText = (block: Block) => {
  if (!ctx.value) return; // 如果上下文不存在则返回
  ctx.value.font = '10px serif'; // 设置字体
  const textWidth = ctx.value.measureText(block.block).width; // 计算文本宽度
  ctx.value.fillText(
    block.block,
    block.x + (block.width - textWidth) / 2, // 水平居中
    block.y + block.height / 2 + 4 // 垂直居中
  );
};

// 处理鼠标滚轮事件以进行缩放
const onWheel = (event: WheelEvent) => {
  event.preventDefault(); // 阻止默认滚动行为
  const delta = event.deltaY > 0 ? 0.98 : 1.02; // 缩放因子
  const newScale = scale.value * delta; // 计算新的缩放比例
  
  const mousePos = getMousePos(event); // 获取鼠标位置
  const worldPos = {
    x: (mousePos.x - translatePos.value.x) / scale.value, // 计算世界坐标
    y: (mousePos.y - translatePos.value.y) / scale.value,
  };

  // 更新偏移位置,使得缩放以鼠标为中心
  translatePos.value.x = mousePos.x - worldPos.x * newScale;
  translatePos.value.y = mousePos.y - worldPos.y * newScale;
  scale.value = newScale; // 更新缩放比例

  drawGrid(); // 重新绘制内容
};

// 处理鼠标按下事件以开始拖动
const onMouseDown = (event: MouseEvent) => {
  isDragging.value = true; // 设置为正在拖动状态
  startDragPos.value = { x: event.clientX, y: event.clientY }; // 记录起始拖动位置
};

// 处理鼠标移动事件
const onMouseMove = (event: MouseEvent) => {
  if (isDragging.value) { // 如果正在拖动
    const dx = event.clientX - startDragPos.value.x; // 计算 X 轴位移
    const dy = event.clientY - startDragPos.value.y; // 计算 Y 轴位移

    // 更新偏移位置
    translatePos.value.x += dx;
    translatePos.value.y += dy;
    startDragPos.value = { x: event.clientX, y: event.clientY }; // 更新起始拖动位置

    drawGrid(); // 重新绘制内容
  }
};

// 处理鼠标松开事件以结束拖动
const onMouseUp = () => {
  isDragging.value = false; // 设置为不拖动状态
};

// 获取鼠标在 canvas 中的位置
const getMousePos = (event: MouseEvent) => {
  const rect = canvas.value?.getBoundingClientRect(); // 获取 canvas 的边界
  return {
    x: event.clientX - (rect?.left || 0), // 计算 X 坐标
    y: event.clientY - (rect?.top || 0), // 计算 Y 坐标
  };
};

// 在组件挂载后初始化 canvas 和绘制内容
onMounted(() => {
  const canvasElement = canvas.value; // 获取 canvas 元素
  if (canvasElement) {
    ctx.value = canvasElement.getContext('2d'); // 获取 2D 上下文
    drawGrid(); // 初始绘制网格
  }
});
</script>

<style scoped>
/* 鼠标悬停时显示抓取手势 */
canvas {
  cursor: grab; 
}
</style>

截图如下:

在这里插入图片描述

继续升级

<template>
  <div class="canvas-container">
    <canvas
      ref="canvas"
      width="800"
      height="600"
      @wheel="onWheel"
      @mousedown="onMouseDown"
      @mousemove="onMouseMove"
      @mouseup="onMouseUp"
    ></canvas>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';

// 定义类型
interface MousePosition {
  x: number;
  y: number;
}

interface Block {
  block: string;
  x: number;
  y: number;
  width: number;
  height: number;
}

// Canvas 相关的状态
const canvas = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
const scale = ref(1); // 当前缩放比例
const translatePos = ref<MousePosition>({ x: 0, y: 0 }); // 当前平移位置
const isDragging = ref(false); // 是否正在拖动
const startDragPos = ref<MousePosition>({ x: 0, y: 0 }); // 开始拖动时的鼠标位置

// 网格相关的设置
const options = {
  startX: 50, // 起始 X 坐标
  startY: 50, // 起始 Y 坐标
  blockWidth: 50, // 区块宽度
  blockHeight: 50, // 区块高度
  padding: 0, // 区块间距(设置为0)
};

const canvasWidth = ref(800);
const canvasHeight = ref(600);

// 获取鼠标在 Canvas 上的位置
function getMousePos(event: MouseEvent): MousePosition {
  const rect = canvas.value?.getBoundingClientRect();
  return {
    x: event.clientX - (rect ? rect.left : 0),
    y: event.clientY - (rect ? rect.top : 0),
  };
}

// 滚动缩小放大
function onWheel(event: WheelEvent) {
  event.preventDefault(); // 阻止默认滚动行为
  const mousePos = getMousePos(event);
  const delta = event.deltaY > 0 ? 0.98 : 1.02; // 缩放因子(向下缩小,向上放大)
  const newScale = scale.value * delta; // 计算新的缩放比例

  // 计算新的偏移位置,以鼠标为中心进行缩放
  const worldPos = {
    x: (mousePos.x - translatePos.value.x) / scale.value,
    y: (mousePos.y - translatePos.value.y) / scale.value,
  };

  translatePos.value.x = mousePos.x - worldPos.x * newScale; // 更新平移位置
  translatePos.value.y = mousePos.y - worldPos.y * newScale;
  scale.value = newScale; // 更新缩放比例

  drawInitialContent(); // 重新绘制内容
}

// 鼠标点击
function onMouseDown(event: MouseEvent) {
  isDragging.value = true; // 设置为拖动状态
  startDragPos.value = { x: event.clientX, y: event.clientY }; // 记录开始拖动的鼠标位置
}

// 鼠标移动
function onMouseMove(event: MouseEvent) {
  if (isDragging.value) {
    const dx = event.clientX - startDragPos.value.x; // 计算鼠标移动的距离
    const dy = event.clientY - startDragPos.value.y;

    translatePos.value.x += dx; // 更新平移位置
    translatePos.value.y += dy;

    startDragPos.value = { x: event.clientX, y: event.clientY }; // 更新开始拖动的鼠标位置
    drawInitialContent(); // 重新绘制内容
  }
}

// 鼠标松开
function onMouseUp() {
  isDragging.value = false; // 取消拖动状态
}

// 绘制初始内容
function drawInitialContent() {
  if (ctx.value) {
    ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value); // 清空画布
    ctx.value.save(); // 保存当前的绘图上下文
    ctx.value.translate(translatePos.value.x, translatePos.value.y); // 应用平移
    ctx.value.scale(scale.value, scale.value); // 应用缩放

    drawGrid(); // 绘制网格

    ctx.value.restore(); // 恢复绘图上下文到之前的状态
  }
}

// 绘制网格
const drawGrid = () => {
  if (!ctx.value) return; // 如果上下文不存在则返回
  
  ctx.value.save(); // 保存当前绘图上下文
  ctx.value.translate(translatePos.value.x, translatePos.value.y); // 应用偏移
  ctx.value.scale(scale.value, scale.value); // 应用缩放

  // 绘制 6 行 26 列的区块
  for (let row = 0; row < 6; row++) {
    for (let col = 0; col < 26; col++) {
      const x = options.startX + col * (options.blockWidth + options.padding); // 计算 X 坐标
      const y = options.startY + row * (options.blockHeight + options.padding); // 计算 Y 坐标
      const block: Block = {
        block: `${row + 1}-${col + 1}`, // 区块名称
        x,
        y,
        width: options.blockWidth,
        height: options.blockHeight,
      };

      drawBlock(block); // 绘制区块
      drawBlockNameText(block); // 绘制区块名称文本
    }
  }

  // 绘制边缘数字
  drawEdgeNumbers();

  ctx.value.restore(); // 恢复绘图上下文到之前的状态
};

// 绘制单个区块
const drawBlock = (block: Block) => {
  if (!ctx.value) return; // 如果上下文不存在则返回
  ctx.value.strokeStyle = 'black'; // 设置边框颜色
  ctx.value.lineWidth = 1; // 设置边框宽度
  ctx.value.strokeRect(block.x, block.y, block.width, block.height); // 绘制矩形
};

// 绘制区块名称文本
const drawBlockNameText = (block: Block) => {
  if (!ctx.value) return; // 如果上下文不存在则返回
  ctx.value.font = '10px serif'; // 设置字体
  const textWidth = ctx.value.measureText(block.block).width; // 计算文本宽度
  ctx.value.fillText(
    block.block,
    block.x + (block.width - textWidth) / 2, // 水平居中
    block.y + block.height / 2 + 4 // 垂直居中
  );
};

// 绘制边缘数字
const drawEdgeNumbers = () => {
  // 左侧数字(从0到5)
  for (let row = 0; row < 6; row++) {
    ctx.value!.font = '12px serif';
    ctx.value!.fillText(
      `${row}`,
      options.startX - 20, // 左侧偏移
      options.startY + row * (options.blockHeight + options.padding) + options.blockHeight / 2 + 4 // 垂直居中
    );
  }

  // 下侧数字(奇数,从1到25)
  for (let col = 0; col < 26; col += 2) {
    ctx.value!.font = '12px serif';
    ctx.value!.fillText(
      `${col + 1}`,
      options.startX + col * (options.blockWidth + options.padding) + options.blockWidth / 2 - 8, // 水平居中
      options.startY + 6 * (options.blockHeight + options.padding) + 20 // 下侧偏移
    );
  }
};

// 在组件挂载后初始化
onMounted(() => {
  ctx.value = canvas.value?.getContext('2d'); // 获取 2D 绘图上下文
  drawInitialContent(); // 绘制初始内容
});
</script>

<style scoped>
.canvas-container {
  overflow: hidden; /* 防止超出部分显示 */
  position: relative; /* 相对定位 */
  width: 800px; /* 设置宽度 */
  height: 600px; /* 设置高度 */
}
canvas {
  border: 1px solid #ccc; /* 设置 Canvas 边框 */
}
</style>

截图如下:

在这里插入图片描述

3. TypeScript

只是展示拖拽的一些方法

<template>
  <div class="canvas-container">
    <canvas ref="canvas" width="800" height="600" @wheel="onWheel" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp"></canvas>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';

// 定义类型
interface MousePosition {
  x: number;
  y: number;
}

const canvas = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
const scale = ref(1); // 当前缩放比例
const translatePos = ref<MousePosition>({ x: 0, y: 0 }); // 当前平移位置
const isDragging = ref(false); // 是否正在拖动
const startDragPos = ref<MousePosition>({ x: 0, y: 0 }); // 开始拖动时的鼠标位置

// 获取鼠标在 Canvas 上的位置
function getMousePos(event: MouseEvent): MousePosition {
  const rect = canvas.value?.getBoundingClientRect();
  return {
    x: event.clientX - (rect ? rect.left : 0),
    y: event.clientY - (rect ? rect.top : 0),
  };
}

// 滚动缩小放大
function onWheel(event: WheelEvent) {
  event.preventDefault(); // 阻止默认滚动行为
  const mousePos = getMousePos(event);
  const delta = event.deltaY > 0 ? 0.98 : 1.02; // 缩放因子(向下缩小,向上放大)
  const newScale = scale.value * delta; // 计算新的缩放比例

  // 计算新的偏移位置,以鼠标为中心进行缩放
  const worldPos = {
    x: (mousePos.x - translatePos.value.x) / scale.value,
    y: (mousePos.y - translatePos.value.y) / scale.value,
  };

  translatePos.value.x = mousePos.x - worldPos.x * newScale; // 更新平移位置
  translatePos.value.y = mousePos.y - worldPos.y * newScale;
  scale.value = newScale; // 更新缩放比例

  drawInitialContent(); // 重新绘制内容
}

// 鼠标点击
function onMouseDown(event: MouseEvent) {
  isDragging.value = true; // 设置为拖动状态
  startDragPos.value = { x: event.clientX, y: event.clientY }; // 记录开始拖动的鼠标位置
}

// 鼠标移动
function onMouseMove(event: MouseEvent) {
  if (isDragging.value) {
    const dx = event.clientX - startDragPos.value.x; // 计算鼠标移动的距离
    const dy = event.clientY - startDragPos.value.y;

    translatePos.value.x += dx; // 更新平移位置
    translatePos.value.y += dy;

    startDragPos.value = { x: event.clientX, y: event.clientY }; // 更新开始拖动的鼠标位置
    drawInitialContent(); // 重新绘制内容
  }
}

// 鼠标松开
function onMouseUp() {
  isDragging.value = false; // 取消拖动状态
}

// 绘制初始内容
function drawInitialContent() {
  if (ctx.value) {
    ctx.value.clearRect(0, 0, canvas.value!.width, canvas.value!.height); // 清空画布
    ctx.value.save(); // 保存当前的绘图上下文
    ctx.value.translate(translatePos.value.x, translatePos.value.y); // 应用平移
    ctx.value.scale(scale.value, scale.value); // 应用缩放

    // 绘制网格
    drawGrid();

    ctx.value.restore(); // 恢复绘图上下文到之前的状态
  }
}

// 绘制网格
function drawGrid() {
  const rows = 6; // 行数
  const cols = 26; // 列数
  const cellWidth = 30; // 单元格宽度
  const cellHeight = 30; // 单元格高度

  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      const x = col * cellWidth; // 计算 x 坐标
      const y = row * cellHeight; // 计算 y 坐标

      ctx.value!.strokeStyle = 'black'; // 设置边框颜色
      ctx.value!.strokeRect(x, y, cellWidth, cellHeight); // 绘制单元格边框

      // 绘制单元格编号
      ctx.value!.font = '12px Arial';
      ctx.value!.fillStyle = 'black';
      ctx.value!.fillText(`${row + 1}-${col + 1}`, x + 5, y + 20); // 在单元格中间绘制文本
    }
  }
}

// 在组件挂载后初始化
onMounted(() => {
  ctx.value = canvas.value?.getContext('2d'); // 获取 2D 绘图上下文
  drawInitialContent(); // 绘制初始内容
});
</script>

<style scoped>
.canvas-container {
  overflow: hidden; /* 防止超出部分显示 */
  position: relative; /* 相对定位 */
  width: 800px; /* 设置宽度 */
  height: 600px; /* 设置高度 */
}
canvas {
  border: 1px solid #ccc; /* 设置 Canvas 边框 */
}
</style>

原文地址:https://blog.csdn.net/weixin_47872288/article/details/142499249

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!