基于 Canvas 的可缩放拖动网格示例(Vue3以及TypeScript )
1. 基本知识
基本知识讲解:
-
Canvas API:
一种用于在网页上绘制图形的 HTML 元素,使用 JavaScript 的 Canvas API 来进行绘制
使用getContext('2d')
方法获取 2D 绘图上下文,允许开发者绘制矩形、文本、图像等 -
缩放和拖动:
通过监听鼠标滚轮事件 (wheel) 可以实现缩放功能,使用 scale 来控制当前缩放比例
鼠标按下、移动和松开事件实现了拖动功能,允许用户在 Canvas 上自由移动视图 -
绘制网格:
在 Canvas 上绘制 6 行 26 列的网格,每个区块可以独立显示和命名
通过动态计算坐标和应用样式,可以实现灵活的绘制效果 -
响应式设计:
通过overflow: hidden
和position: 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)!