Canvas径向菜单单个菜单项Hover效果实现求助
解决Canvas径向菜单单个扇形hover高亮问题
问题出在你没有把Canvas上的扇形区域和鼠标位置做精准匹配,导致hover时整个Canvas触发变化。核心思路是记录每个扇形的角度边界,计算鼠标当前落在哪个扇形范围内,只重绘该扇形的高亮样式。
具体实现步骤
1. 存储扇形角度数据
在组件data中新增两个变量,用来保存每个扇形的角度范围和当前hover的索引:
data() { return { // ... 原有数据 sectorData: [], // 存储每个扇形的起始/结束角度 hoveredIndex: null // 当前hover的扇形索引 }; }
2. 初始化时记录扇形角度
修改createCanvasBackground方法,将每个扇形的实际角度范围存入sectorData:
createCanvasBackground() { this.sectorData = []; // 每次创建前清空 const canvas = this.$refs.canvas; const ctx = canvas.getContext('2d'); const canvasWidth = 830; const canvasHeight = 830; canvas.width = canvasWidth; canvas.height = canvasHeight; const angleStep = (2 * Math.PI) / this.navItems.length; for (let i = 0; i < this.navItems.length; i++) { const startAngle = i * angleStep - Math.PI / 2; const endAngle = (i + 1) * angleStep - Math.PI / 2; const adjustedRadius = this.radius + this.spacing; const spacingRadians = (this.spacing / (2 * Math.PI * adjustedRadius)) * Math.PI; const sectorEndAngle = endAngle - spacingRadians; // 保存当前扇形的角度范围 this.sectorData.push({ startAngle, endAngle: sectorEndAngle }); const radius = Math.min(canvasWidth / 2, canvasHeight / 2); ctx.beginPath(); ctx.moveTo(canvasWidth / 2, canvasHeight / 2); ctx.arc(canvasWidth / 2, canvasHeight / 2, radius, startAngle, sectorEndAngle); ctx.fillStyle = this.backgroundColor; ctx.fill(); } }
3. 监听鼠标事件并判断hover扇形
添加鼠标移动和离开的事件监听,计算鼠标位置对应的扇形:
mounted() { this.positionNavItems(); this.createCanvasBackground(); // 绑定鼠标事件 const canvas = this.$refs.canvas; canvas.addEventListener('mousemove', this.handleCanvasMouseMove); canvas.addEventListener('mouseleave', this.handleCanvasMouseLeave); }, beforeUnmount() { // 组件销毁时移除事件监听,避免内存泄漏 const canvas = this.$refs.canvas; canvas.removeEventListener('mousemove', this.handleCanvasMouseMove); canvas.removeEventListener('mouseleave', this.handleCanvasMouseLeave); }, methods: { // ... 原有方法 handleCanvasMouseMove(e) { const canvas = this.$refs.canvas; const rect = canvas.getBoundingClientRect(); // 计算鼠标相对于Canvas中心的坐标 const centerX = canvas.width / 2; const centerY = canvas.height / 2; const mouseX = e.clientX - rect.left - centerX; const mouseY = e.clientY - rect.top - centerY; // 计算Canvas坐标系下的角度(0朝右,顺时针递增) let angle = Math.atan2(mouseY, mouseX); // 调整角度至和扇形绘制一致的基准(从顶部开始) if (angle < -Math.PI / 2) { angle += 2 * Math.PI; } this.hoveredIndex = null; // 遍历所有扇形,判断鼠标角度落在哪个范围内 for (let i = 0; i < this.sectorData.length; i++) { const { startAngle, endAngle } = this.sectorData[i]; // 处理跨0度的扇形(比如最后一个扇形从接近2π到0附近) if (startAngle > endAngle) { if (angle >= startAngle || angle <= endAngle) { this.hoveredIndex = i; break; } } else { if (angle >= startAngle && angle <= endAngle) { this.hoveredIndex = i; break; } } } // 重绘Canvas this.redrawCanvas(); }, handleCanvasMouseLeave() { this.hoveredIndex = null; this.redrawCanvas(); },
4. 重绘Canvas实现高亮
新增redrawCanvas方法,根据当前hover索引重绘对应扇形的颜色:
redrawCanvas() { const canvas = this.$refs.canvas; const ctx = canvas.getContext('2d'); const canvasWidth = canvas.width; const canvasHeight = canvas.height; // 清空画布 ctx.clearRect(0, 0, canvasWidth, canvasHeight); const radius = Math.min(canvasWidth / 2, canvasHeight / 2); this.sectorData.forEach((sector, index) => { const { startAngle, endAngle } = sector; ctx.beginPath(); ctx.moveTo(canvasWidth / 2, canvasHeight / 2); ctx.arc(canvasWidth / 2, canvasHeight / 2, radius, startAngle, endAngle); // hover的扇形用高亮色,其他用默认背景色 ctx.fillStyle = this.hoveredIndex === index ? '#3a3a3a' : this.backgroundColor; ctx.fill(); }); }
5. 可选:同步高亮对应菜单项
如果需要让Canvas扇形和对应的<li>菜单项同步高亮,可以修改<li>的class绑定:
<li v-for="(navItem, index) in navItems" :key="index" :class="['button-' + index, { 'hovered': hoveredIndex === index }]" ref="navItems">
然后添加CSS样式:
.button-inner.hovered { /* 菜单项高亮样式,比如改变文字/图标颜色 */ color: #fff; }
关键逻辑说明
- 通过
Math.atan2计算鼠标相对于Canvas中心的角度,转换成和扇形绘制一致的坐标系基准 - 遍历
sectorData匹配角度范围,精准定位当前hover的扇形 - 每次hover变化时重绘Canvas,只修改目标扇形的填充色,避免整个画布不必要的重绘
内容的提问来源于stack exchange,提问作者Roman
相关产品推荐
相关产品推荐

