文章目录
- 1. 基本知识
- 2. Vue3
- 3. 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 高度 --><!-- 添加边框样式 --><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>