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

embedded/2024/12/22 19:48:51/

文章目录

  • 1. 基本知识
  • 2. Vue3
  • 3. 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 高度 --><!-- 添加边框样式 --><canvasref="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"><canvasref="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>

http://www.ppmy.cn/embedded/120685.html

相关文章

基于单片机语音智能导盲仪仿真设计

文章目录 前言资料获取设计介绍设计程序具体实现截图设计获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师&#xff0c;一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设计 主要对象是咱们…

WIFI中的两个概念:TWT OFDMA

在wifi路由器中&#xff0c;会有这两个选项&#xff1a;TWT OFDMA可以选择关闭和打开。这是什么呢&#xff1f; 原来这两个&#xff0c;一个是节能的&#xff0c;一个是提高线路质量的。 在WiFi技术中&#xff0c;TWT&#xff08;Target Wake Time&#xff0c;目标唤醒时间&a…

【步联科技身份证】 身份证读取与解析———未来之窗行业应用跨平台架构

一、身份证解析代码 C# function 身份证数据解析_湖南步联科技(wzxx) {var result {};result[xm] wzxx.substr(0, 15);result[xbdm] wzxx.substr(15, 1);result[mzdm] wzxx.substr(16, 2);result[csrq] wzxx.substr(18, 8);result[dzmc] wzxx.substr(26, 35);result[gms…

鸡兔同笼,但是线性代数

灵感来自&#xff1a;bilibili&#xff0c;巨佬&#xff01; 我们有 14 14 14 个头&#xff0c; 32 32 32 只脚&#xff0c;所有鸡和兔都没有变异&#xff0c;头和脚都完整&#xff0c;没有数错。还有什么 Bug 吗 小学奥数 假设全是鸡&#xff0c;则有 14 2 28 14 \time…

【自然语言处理】补充:文本分类及朴素贝叶斯分类器

【自然语言处理】补充:文本分类及朴素贝叶斯分类器 文章目录 【自然语言处理】补充:文本分类及朴素贝叶斯分类器1. 文本分类2. 朴素贝叶斯3. 朴素贝叶斯理论4. 文本分类评价1. 文本分类 文本分类/Text Classification/Text Categorization 给定分类体系,将一篇文本分到其中一…

网通产品硬件设计工程师:汽车蓝牙收发器用网络隔离变压器有哪些选择呢?

Hqst盈盛&#xff08;华强盛&#xff09;电子导读&#xff1a;今天分享的是网通设备有关工程师产品设计时可供选择的两款汽车蓝牙收发器用网络隔离变压器... 下面我们就一起来看看网通设备有关工程师产品设计时可供选择的两款汽车蓝牙收发器用网络隔离变压器&#xff0c;让您的…

【深度学习】(6)--图像数据增强

文章目录 图像数据增强一、作用二、增强方法三、代码体现四、增强体现 总结 图像数据增强 数据增强&#xff08;Data Augmentation&#xff09;&#xff0c;也称为数据增广&#xff0c;是一种在机器学习和深度学习中常用的技术&#xff0c;它通过对现有数据进行各种变换和处理…

MySQL 生产环境性能优化

在 MySQL 生产环境中进行性能优化可以从以下几个方面入手&#xff1a; 一、硬件层面 选择高性能服务器&#xff1a; 配备足够的内存&#xff0c;以减少磁盘 I/O。MySQL 可以将经常访问的数据缓存到内存中&#xff0c;提高查询速度。一般来说&#xff0c;对于高负载的生产环境&…