Files
test-repo/docs/.vitepress/theme/components/appendix/canvas-intro/EventHandlingDemo.vue
T

724 lines
15 KiB
Vue
Raw Normal View History

<!--
EventHandlingDemo.vue
Canvas 事件处理演示组件
用途
展示 Canvas 中的鼠标键盘事件处理包括点击拖拽悬停等交互
交互功能
- 鼠标点击在点击位置创建对象
- 拖拽拖动对象移动
- 悬停高亮显示鼠标下的对象
- 键盘控制使用键盘控制对象
-->
<template>
<div class="event-demo">
<div class="control-panel">
<div class="mode-selector">
<label>Interaction Mode / 交互模式</label>
<div class="button-group">
<button
v-for="mode in modes"
:key="mode.value"
:class="{ active: currentMode === mode.value }"
@click="currentMode = mode.value"
>
{{ mode.label }}
</button>
</div>
</div>
<div class="instructions">
<h4>Instructions / 操作说明</h4>
<ul>
<li v-if="currentMode === 'click'">
<strong>Click Mode</strong>点击画布创建圆形按住 Shift
可创建不同颜色
</li>
<li v-if="currentMode === 'drag'">
<strong>Drag Mode</strong>拖拽圆形移动位置拖拽时会改变颜色
</li>
<li v-if="currentMode === 'hover'">
<strong>Hover Mode</strong>鼠标悬停在圆形上会高亮显示并显示坐标
</li>
<li v-if="currentMode === 'keyboard'">
<strong>Keyboard Mode</strong>使用方向键移动选中的圆形Delete
键删除
</li>
</ul>
</div>
<div class="event-log">
<h4>Event Log / 事件日志</h4>
<div class="log-container">
<div
v-for="(log, index) in eventLogs"
:key="index"
class="log-entry"
:class="log.type"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
<button
class="clear-btn"
@click="clearAll"
>
<span class="icon">🗑</span>
Clear All / 清除全部
</button>
</div>
<div class="canvas-container">
<canvas
ref="canvasRef"
width="600"
height="400"
tabindex="0"
@click="handleClick"
@mousemove="handleMouseMove"
@mousedown="handleMouseDown"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
@keydown="handleKeyDown"
/>
</div>
<div class="code-display">
<h4>Event Handling Code / 事件处理代码</h4>
<pre><code>{{ currentCode }}</code></pre>
</div>
<div class="explanation">
<h4>Event Handling Tips / 事件处理要点</h4>
<ul>
<li>
<strong>坐标转换</strong>
使用 getBoundingClientRect() 获取 Canvas 在页面中的位置计算相对坐标
</li>
<li>
<strong>碰撞检测</strong>
对于圆形计算鼠标位置到圆心的距离对于矩形判断点是否在范围内
</li>
<li>
<strong>事件委托</strong>
Canvas 只有一个元素需要手动判断事件发生在哪个对象上
</li>
<li>
<strong>性能优化</strong>
使用 requestAnimationFrame 优化重绘避免频繁操作
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const canvasRef = ref(null)
const currentMode = ref('click')
const circles = ref([])
const selectedCircle = ref(null)
const hoveredCircle = ref(null)
const isDragging = ref(false)
const eventLogs = ref([])
const modes = [
{ value: 'click', label: 'Click / 点击' },
{ value: 'drag', label: 'Drag / 拖拽' },
{ value: 'hover', label: 'Hover / 悬停' },
{ value: 'keyboard', label: 'Keyboard / 键盘' }
]
const colors = [
'#e74c3c',
'#3498db',
'#2ecc71',
'#f39c12',
'#9b59b6',
'#1abc9c'
]
const currentCode = computed(() => {
const templates = {
click: `// 点击创建圆形
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const circle = {
x: x,
y: y,
radius: 30,
color: '#3498db'
}
circles.push(circle)
draw()
})`,
drag: `// 拖拽移动圆形
let isDragging = false
let selectedCircle = null
canvas.addEventListener('mousedown', (e) => {
const { x, y } = getMousePos(e)
// 检测点击了哪个圆形
circles.forEach(circle => {
const dist = Math.sqrt((x - circle.x) ** 2 + (y - circle.y) ** 2)
if (dist < circle.radius) {
isDragging = true
selectedCircle = circle
}
})
})
canvas.addEventListener('mousemove', (e) => {
if (isDragging && selectedCircle) {
const { x, y } = getMousePos(e)
selectedCircle.x = x
selectedCircle.y = y
draw()
}
})
canvas.addEventListener('mouseup', () => {
isDragging = false
selectedCircle = null
})`,
hover: `// 悬停高亮
canvas.addEventListener('mousemove', (e) => {
const { x, y } = getMousePos(e)
let hovered = null
// 检测悬停
circles.forEach(circle => {
const dist = Math.sqrt((x - circle.x) ** 2 + (y - circle.y) ** 2)
if (dist < circle.radius) {
hovered = circle
}
})
if (hovered) {
canvas.style.cursor = 'pointer'
// 绘制高亮效果
ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 3
ctx.beginPath()
ctx.arc(hovered.x, hovered.y, hovered.radius + 5, 0, Math.PI * 2)
ctx.stroke()
} else {
canvas.style.cursor = 'default'
}
draw()
})`,
keyboard: `// 键盘控制
canvas.tabIndex = 0 // 使 canvas 可以获取焦点
canvas.focus()
canvas.addEventListener('keydown', (e) => {
const step = 10
switch(e.key) {
case 'ArrowUp':
selectedCircle.y -= step
break
case 'ArrowDown':
selectedCircle.y += step
break
case 'ArrowLeft':
selectedCircle.x -= step
break
case 'ArrowRight':
selectedCircle.x += step
break
case 'Delete':
circles = circles.filter(c => c !== selectedCircle)
selectedCircle = null
break
}
draw()
})`
}
return templates[currentMode.value]
})
const addLog = (message, type = 'info') => {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
eventLogs.value.unshift({ time, message, type })
if (eventLogs.value.length > 10) {
eventLogs.value.pop()
}
}
const getMousePos = (e) => {
const canvas = canvasRef.value
const rect = canvas.getBoundingClientRect()
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
const draw = () => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 绘制背景
ctx.fillStyle = '#fafafa'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 绘制所有圆形
circles.value.forEach((circle) => {
// 填充
ctx.fillStyle = circle.color
ctx.beginPath()
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2)
ctx.fill()
// 描边
ctx.strokeStyle = '#2c3e50'
ctx.lineWidth = 2
ctx.stroke()
// 高光
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'
ctx.beginPath()
ctx.arc(
circle.x - circle.radius * 0.3,
circle.y - circle.radius * 0.3,
circle.radius * 0.4,
0,
Math.PI * 2
)
ctx.fill()
// 选中状态
if (circle === selectedCircle.value) {
ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 3
ctx.beginPath()
ctx.arc(circle.x, circle.y, circle.radius + 5, 0, Math.PI * 2)
ctx.stroke()
}
// 悬停状态
if (circle === hoveredCircle.value && currentMode.value === 'hover') {
ctx.fillStyle = 'rgba(231, 76, 60, 0.2)'
ctx.beginPath()
ctx.arc(circle.x, circle.y, circle.radius + 10, 0, Math.PI * 2)
ctx.fill()
// 显示坐标
ctx.fillStyle = '#2c3e50'
ctx.font = '12px Arial'
ctx.fillText(
`(${Math.round(circle.x)}, ${Math.round(circle.y)})`,
circle.x + circle.radius + 10,
circle.y
)
}
})
}
const handleClick = (e) => {
if (currentMode.value !== 'click') return
const { x, y } = getMousePos(e)
const color = e.shiftKey
? colors[Math.floor(Math.random() * colors.length)]
: '#3498db'
circles.value.push({
x,
y,
radius: 30,
color
})
addLog(`Created circle at (${Math.round(x)}, ${Math.round(y)})`, 'success')
draw()
}
const handleMouseMove = (e) => {
const { x, y } = getMousePos(e)
if (
currentMode.value === 'drag' &&
isDragging.value &&
selectedCircle.value
) {
selectedCircle.value.x = x
selectedCircle.value.y = y
draw()
return
}
if (currentMode.value === 'hover') {
let found = null
circles.value.forEach((circle) => {
const dist = Math.sqrt((x - circle.x) ** 2 + (y - circle.y) ** 2)
if (dist < circle.radius) {
found = circle
}
})
if (found !== hoveredCircle.value) {
hoveredCircle.value = found
if (found) {
addLog(
`Hovering circle at (${Math.round(found.x)}, ${Math.round(found.y)})`,
'info'
)
}
}
draw()
}
}
const handleMouseDown = (e) => {
if (currentMode.value !== 'drag') return
const { x, y } = getMousePos(e)
circles.value.forEach((circle) => {
const dist = Math.sqrt((x - circle.x) ** 2 + (y - circle.y) ** 2)
if (dist < circle.radius) {
isDragging.value = true
selectedCircle.value = circle
addLog(
`Started dragging circle at (${Math.round(x)}, ${Math.round(y)})`,
'info'
)
}
})
}
const handleMouseUp = () => {
if (isDragging.value) {
addLog(
`Dropped circle at (${Math.round(selectedCircle.value.x)}, ${Math.round(selectedCircle.value.y)})`,
'success'
)
}
isDragging.value = false
selectedCircle.value = null
}
const handleMouseLeave = () => {
isDragging.value = false
selectedCircle.value = null
hoveredCircle.value = null
draw()
}
const handleKeyDown = (e) => {
if (currentMode.value !== 'keyboard') return
if (!selectedCircle.value && circles.value.length > 0) {
selectedCircle.value = circles.value[0]
}
if (!selectedCircle.value) return
const step = 10
let moved = false
switch (e.key) {
case 'ArrowUp':
selectedCircle.value.y -= step
moved = true
break
case 'ArrowDown':
selectedCircle.value.y += step
moved = true
break
case 'ArrowLeft':
selectedCircle.value.x -= step
moved = true
break
case 'ArrowRight':
selectedCircle.value.x += step
moved = true
break
case 'Delete':
case 'Backspace':
circles.value = circles.value.filter((c) => c !== selectedCircle.value)
addLog('Deleted circle', 'warning')
selectedCircle.value = circles.value[0] || null
moved = true
break
}
if (moved) {
e.preventDefault()
draw()
}
}
const clearAll = () => {
circles.value = []
selectedCircle.value = null
hoveredCircle.value = null
addLog('Cleared all circles', 'warning')
draw()
}
onMounted(() => {
// 初始化几个圆形
circles.value = [
{ x: 150, y: 200, radius: 30, color: '#e74c3c' },
{ x: 300, y: 200, radius: 30, color: '#3498db' },
{ x: 450, y: 200, radius: 30, color: '#2ecc71' }
]
draw()
})
</script>
<style scoped>
.event-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 1.5rem;
}
.mode-selector {
margin-bottom: 15px;
}
.mode-selector label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #2c3e50;
}
.button-group {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.button-group button {
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
}
.button-group button:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.button-group button.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.instructions {
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.instructions h4 {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.instructions ul {
margin: 0;
padding-left: 1.25rem;
}
.instructions li {
margin-bottom: 0.375rem;
color: var(--vp-c-text-2);
font-size: 0.813rem;
line-height: 1.5;
}
.event-log {
margin-bottom: 15px;
}
.event-log h4 {
margin: 0 0 8px 0;
color: #2c3e50;
font-size: 14px;
}
.log-container {
max-height: 150px;
background: white;
border-radius: 6px;
padding: 10px;
}
.log-entry {
display: flex;
gap: 10px;
padding: 6px 8px;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: #95a5a6;
font-family: 'Courier New', monospace;
flex-shrink: 0;
}
.log-message {
color: #2c3e50;
}
.log-entry.info .log-message {
color: #3498db;
}
.log-entry.success .log-message {
color: #2ecc71;
}
.log-entry.warning .log-message {
color: #f39c12;
}
.clear-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
background: #e74c3c;
color: white;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.clear-btn:hover {
background: #c0392b;
transform: translateY(-1px);
}
.canvas-container {
display: flex;
justify-content: center;
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 3px solid var(--vp-c-divider);
border-radius: 6px;
cursor: crosshair;
outline: none;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
canvas:focus {
border-color: var(--vp-c-brand);
}
.code-display {
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
margin: 0;
}
.code-display code {
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
</style>