Canvas 交互与事件
本文档介绍如何为 Canvas 添加用户交互功能,包括鼠标事件、触摸事件、图形选择与拖拽等内容。
鼠标事件处理
Canvas 本身不支持为绘制的图形添加事件监听器,需要通过监听 Canvas 元素的事件并计算坐标来实现交互。
常用鼠标事件
| 事件 | 说明 |
|---|---|
click | 单击 |
dblclick | 双击 |
mousedown | 鼠标按下 |
mouseup | 鼠标释放 |
mousemove | 鼠标移动 |
mouseenter | 鼠标进入 |
mouseleave | 鼠标离开 |
获取鼠标在 Canvas 中的坐标
javascript
const canvas = document.getElementById('myCanvas')
function getMousePos(canvas, e) {
const rect = canvas.getBoundingClientRect()
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
canvas.addEventListener('click', (e) => {
const pos = getMousePos(canvas, e)
console.log('点击位置:', pos.x, pos.y)
})鼠标绘制
javascript
let isDrawing = false
let lastX = 0
let lastY = 0
canvas.addEventListener('mousedown', (e) => {
isDrawing = true
const pos = getMousePos(canvas, e)
lastX = pos.x
lastY = pos.y
})
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return
const pos = getMousePos(canvas, e)
ctx.beginPath()
ctx.moveTo(lastX, lastY)
ctx.lineTo(pos.x, pos.y)
ctx.strokeStyle = '#333'
ctx.lineWidth = 2
ctx.stroke()
lastX = pos.x
lastY = pos.y
})
canvas.addEventListener('mouseup', () => {
isDrawing = false
})
canvas.addEventListener('mouseleave', () => {
isDrawing = false
})鼠标样式
javascript
canvas.addEventListener('mousemove', (e) => {
const pos = getMousePos(canvas, e)
if (isInsideShape(pos.x, pos.y)) {
canvas.style.cursor = 'pointer'
} else {
canvas.style.cursor = 'default'
}
})触摸事件处理
触摸事件与鼠标事件类似,但需要处理多点触控。
常用触摸事件
| 事件 | 说明 |
|---|---|
touchstart | 触摸开始 |
touchmove | 触摸移动 |
touchend | 触摸结束 |
touchcancel | 触摸取消 |
获取触摸坐标
javascript
function getTouchPos(canvas, touch) {
const rect = canvas.getBoundingClientRect()
return {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top
}
}
canvas.addEventListener('touchstart', (e) => {
e.preventDefault()
const touch = e.touches[0]
const pos = getTouchPos(canvas, touch)
console.log('触摸位置:', pos.x, pos.y)
})触摸绘制
javascript
let isDrawing = false
let lastX = 0
let lastY = 0
canvas.addEventListener('touchstart', (e) => {
e.preventDefault()
isDrawing = true
const pos = getTouchPos(canvas, e.touches[0])
lastX = pos.x
lastY = pos.y
})
canvas.addEventListener('touchmove', (e) => {
e.preventDefault()
if (!isDrawing) return
const pos = getTouchPos(canvas, e.touches[0])
ctx.beginPath()
ctx.moveTo(lastX, lastY)
ctx.lineTo(pos.x, pos.y)
ctx.strokeStyle = '#333'
ctx.lineWidth = 2
ctx.stroke()
lastX = pos.x
lastY = pos.y
})
canvas.addEventListener('touchend', () => {
isDrawing = false
})阻止默认行为
javascript
// 阻止触摸时的默认行为(如滚动)
canvas.addEventListener('touchstart', (e) => e.preventDefault(), { passive: false })
canvas.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false })图形选择
由于 Canvas 绘制的图形不是 DOM 元素,需要手动实现图形选择逻辑。
点击检测
矩形检测
javascript
function isPointInRect(px, py, rect) {
return px >= rect.x &&
px <= rect.x + rect.width &&
py >= rect.y &&
py <= rect.y + rect.height
}
canvas.addEventListener('click', (e) => {
const pos = getMousePos(canvas, e)
if (isPointInRect(pos.x, pos.y, { x: 50, y: 50, width: 100, height: 80 })) {
console.log('点击了矩形')
}
})圆形检测
javascript
function isPointInCircle(px, py, cx, cy, radius) {
const dx = px - cx
const dy = py - cy
return dx * dx + dy * dy <= radius * radius
}
canvas.addEventListener('click', (e) => {
const pos = getMousePos(canvas, e)
if (isPointInCircle(pos.x, pos.y, 200, 150, 50)) {
console.log('点击了圆形')
}
})使用 isPointInPath
javascript
canvas.addEventListener('click', (e) => {
const pos = getMousePos(canvas, e)
// 重新构建路径
ctx.beginPath()
ctx.arc(200, 150, 50, 0, Math.PI * 2)
if (ctx.isPointInPath(pos.x, pos.y)) {
console.log('点击了圆形')
}
})图形管理
javascript
class ShapeManager {
constructor() {
this.shapes = []
}
add(shape) {
this.shapes.push(shape)
}
findAt(x, y) {
// 从后往前查找(后绘制的在上层)
for (let i = this.shapes.length - 1; i >= 0; i--) {
if (this.shapes[i].contains(x, y)) {
return this.shapes[i]
}
}
return null
}
draw(ctx) {
this.shapes.forEach(shape => shape.draw(ctx))
}
}
// 图形类
class Rectangle {
constructor(x, y, width, height, color) {
this.x = x
this.y = y
this.width = width
this.height = height
this.color = color
}
contains(px, py) {
return px >= this.x && px <= this.x + this.width &&
py >= this.y && py <= this.y + this.height
}
draw(ctx) {
ctx.fillStyle = this.color
ctx.fillRect(this.x, this.y, this.width, this.height)
}
}
// 使用
const manager = new ShapeManager()
manager.add(new Rectangle(50, 50, 100, 80, '#3498db'))
manager.add(new Rectangle(200, 100, 80, 60, '#e74c3c'))
canvas.addEventListener('click', (e) => {
const pos = getMousePos(canvas, e)
const shape = manager.findAt(pos.x, pos.y)
if (shape) {
console.log('点击了图形')
}
})拖拽功能
基本拖拽
javascript
let selectedShape = null
let offsetX = 0
let offsetY = 0
canvas.addEventListener('mousedown', (e) => {
const pos = getMousePos(canvas, e)
selectedShape = manager.findAt(pos.x, pos.y)
if (selectedShape) {
offsetX = pos.x - selectedShape.x
offsetY = pos.y - selectedShape.y
}
})
canvas.addEventListener('mousemove', (e) => {
if (!selectedShape) return
const pos = getMousePos(canvas, e)
selectedShape.x = pos.x - offsetX
selectedShape.y = pos.y - offsetY
// 重绘
ctx.clearRect(0, 0, canvas.width, canvas.height)
manager.draw(ctx)
})
canvas.addEventListener('mouseup', () => {
selectedShape = null
})完整拖拽类
javascript
class DraggableShape {
constructor(x, y, width, height, color) {
this.x = x
this.y = y
this.width = width
this.height = height
this.color = color
this.isDragging = false
}
contains(px, py) {
return px >= this.x && px <= this.x + this.width &&
py >= this.y && py <= this.y + this.height
}
startDrag(px, py) {
this.isDragging = true
this.offsetX = px - this.x
this.offsetY = py - this.y
}
drag(px, py) {
if (!this.isDragging) return
this.x = px - this.offsetX
this.y = py - this.offsetY
}
endDrag() {
this.isDragging = false
}
draw(ctx) {
ctx.fillStyle = this.color
ctx.fillRect(this.x, this.y, this.width, this.height)
// 拖拽时添加边框
if (this.isDragging) {
ctx.strokeStyle = '#333'
ctx.lineWidth = 2
ctx.strokeRect(this.x, this.y, this.width, this.height)
}
}
}交互式应用
绘图应用
javascript
class DrawingApp {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.isDrawing = false
this.lastX = 0
this.lastY = 0
this.color = '#333'
this.lineWidth = 2
this.init()
}
init() {
this.canvas.addEventListener('mousedown', this.startDraw.bind(this))
this.canvas.addEventListener('mousemove', this.draw.bind(this))
this.canvas.addEventListener('mouseup', this.endDraw.bind(this))
this.canvas.addEventListener('mouseleave', this.endDraw.bind(this))
// 触摸支持
this.canvas.addEventListener('touchstart', this.startDrawTouch.bind(this))
this.canvas.addEventListener('touchmove', this.drawTouch.bind(this))
this.canvas.addEventListener('touchend', this.endDraw.bind(this))
}
getPos(e) {
const rect = this.canvas.getBoundingClientRect()
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
startDraw(e) {
this.isDrawing = true
const pos = this.getPos(e)
this.lastX = pos.x
this.lastY = pos.y
}
startDrawTouch(e) {
e.preventDefault()
this.isDrawing = true
const touch = e.touches[0]
const rect = this.canvas.getBoundingClientRect()
this.lastX = touch.clientX - rect.left
this.lastY = touch.clientY - rect.top
}
draw(e) {
if (!this.isDrawing) return
const pos = this.getPos(e)
this.ctx.beginPath()
this.ctx.moveTo(this.lastX, this.lastY)
this.ctx.lineTo(pos.x, pos.y)
this.ctx.strokeStyle = this.color
this.ctx.lineWidth = this.lineWidth
this.ctx.lineCap = 'round'
this.ctx.stroke()
this.lastX = pos.x
this.lastY = pos.y
}
drawTouch(e) {
e.preventDefault()
if (!this.isDrawing) return
const touch = e.touches[0]
const rect = this.canvas.getBoundingClientRect()
const x = touch.clientX - rect.left
const y = touch.clientY - rect.top
this.ctx.beginPath()
this.ctx.moveTo(this.lastX, this.lastY)
this.ctx.lineTo(x, y)
this.ctx.strokeStyle = this.color
this.ctx.lineWidth = this.lineWidth
this.ctx.lineCap = 'round'
this.ctx.stroke()
this.lastX = x
this.lastY = y
}
endDraw() {
this.isDrawing = false
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
setColor(color) {
this.color = color
}
setLineWidth(width) {
this.lineWidth = width
}
}
const app = new DrawingApp(canvas)游戏交互
javascript
class Game {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.player = { x: 200, y: 150, radius: 20 }
this.target = { x: 300, y: 100, radius: 15 }
this.score = 0
this.init()
}
init() {
this.canvas.addEventListener('click', this.handleClick.bind(this))
this.animate()
}
handleClick(e) {
const pos = this.getPos(e)
// 检测是否点击目标
const dx = pos.x - this.target.x
const dy = pos.y - this.target.y
const distance = Math.sqrt(dx * dx + dy * dy)
if (distance <= this.target.radius) {
this.score++
this.relocateTarget()
}
}
getPos(e) {
const rect = this.canvas.getBoundingClientRect()
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
relocateTarget() {
this.target.x = Math.random() * (this.canvas.width - 40) + 20
this.target.y = Math.random() * (this.canvas.height - 40) + 20
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
// 绘制玩家
this.ctx.fillStyle = '#3498db'
this.ctx.beginPath()
this.ctx.arc(this.player.x, this.player.y, this.player.radius, 0, Math.PI * 2)
this.ctx.fill()
// 绘制目标
this.ctx.fillStyle = '#e74c3c'
this.ctx.beginPath()
this.ctx.arc(this.target.x, this.target.y, this.target.radius, 0, Math.PI * 2)
this.ctx.fill()
// 绘制分数
this.ctx.fillStyle = '#333'
this.ctx.font = '20px Arial'
this.ctx.fillText(`分数: ${this.score}`, 20, 30)
}
animate() {
this.draw()
requestAnimationFrame(this.animate.bind(this))
}
}常见问题
1. 坐标计算错误
问题: 点击位置与实际位置不符。
解决方案: 确保正确计算 Canvas 的边界矩形。
javascript
function getMousePos(canvas, e) {
const rect = canvas.getBoundingClientRect()
const scaleX = canvas.width / rect.width
const scaleY = canvas.height / rect.height
return {
x: (e.clientX - rect.left) * scaleX,
y: (e.clientY - rect.top) * scaleY
}
}2. 触摸与鼠标冲突
问题: 同时监听触摸和鼠标事件导致重复触发。
解决方案: 检测设备类型,只使用一种事件。
javascript
const isTouchDevice = 'ontouchstart' in window
if (isTouchDevice) {
canvas.addEventListener('touchstart', handleStart)
canvas.addEventListener('touchmove', handleMove)
canvas.addEventListener('touchend', handleEnd)
} else {
canvas.addEventListener('mousedown', handleStart)
canvas.addEventListener('mousemove', handleMove)
canvas.addEventListener('mouseup', handleEnd)
}3. 性能问题
问题: 频繁的事件处理导致性能下降。
解决方案: 使用节流或防抖。
javascript
function throttle(func, limit) {
let inThrottle
return function(...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
canvas.addEventListener('mousemove', throttle(handleMove, 16))下一步学习
返回:Canvas 教程目录 | 上一篇:高级特性