[TOC]
在 Canvas 中,鼠標事件可以用來實現以下三種常見的用戶交互效果:
1. 捕獲物體
2. 拖曳物體
3. 拋擲物體
## 捕獲物體
想要拖曳一個物體或者拋擲一個物體,首先要知道怎么捕獲一個物體。對于 DOM 元素的捕獲,可以直接用 document.getElementById() 來實現,但是在 Canvas 中對于物體的捕獲就沒那簡單了。
在 Canvas 中,對于物體的捕獲,可以分為以下四種情況來考慮:
(1)矩形的捕獲
(2)圓的捕獲
(3)多邊形的捕獲
(4)不規則圖形的捕獲
多邊形和不規則圖形的捕獲較為復雜,這里只介紹矩形和圓形的捕獲。
### 矩形的捕獲

如圖,我們可以通過獲取鼠標點擊時的坐標來判斷是否捕獲了矩形。如果鼠標點擊坐標落在矩形上,則說明捕獲了這個矩形。偽代碼如下:
```js
if (mouse.x > rect.x &&
mouse.x < rect.x + rect.width &&
mouse.y > rect.y &&
mouse.y < rect.y + rect.height) {
// ...
}
```
### 圓的捕獲
對于圓的捕獲,可以通過判定鼠標與圓心之間的距離來判斷是否捕獲了圓。如果距離小于半徑,說明鼠標落在了圓上。

偽代碼:
```js
dx = mouse.x - ball.x
dy = mouse.y - ball.y
distance = Math.sqrt(dx * dx + dy * dy)
if (distance < ball.radius) {
// ...
}
```
### 捕獲靜止物體
對于捕獲物體,這里分為兩種情況來討論,即捕獲靜止物體和捕獲運動物體,下面是一個捕獲靜止的小球的例子。
首先給之前的小球類添加一個新的方法:
```js
// 檢測鼠標是否捕獲了小球
checkMouse: function (mouse) {
let dx = mouse.x - this.x
let dy = mouse.y - this.y
let distance = Math.sqrt(dx * dx + dy * dy)
if (distance < this.radius) {
return true
} else {
return false
}
}
```
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>捕獲靜止小球</title>
<script src="./ball.js"></script>
<script src="./tool.js"></script>
</head>
<body>
<canvas id="canvas" width="480" height="300" style="border: 1px solid gray; display: block; margin: 0 auto;"></canvas>
<p id="txt"></p>
<script>
window.onload = function () {
let cnv = document.getElementById('canvas')
let cxt = cnv.getContext('2d')
let txt = document.getElementById('txt')
let ball = new Ball(cnv.width / 2, cnv.height / 2, 30)
ball.fill(cxt)
let mouse = tools.getMouse(cnv)
// 添加鼠標移動事件
cnv.addEventListener('mousemove', function () {
// 判斷鼠標當前坐標是否處于小球內
if (ball.checkMouse(mouse)) {
txt.innerHTML = '鼠標移入小球'
} else {
txt.innerHTML = '鼠標移出小球'
}
}, false)
}
</script>
</body>
</html>
```
### 捕獲運動物體
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>捕獲運動小球</title>
<script src="./ball.js"></script>
<script src="./tool.js"></script>
</head>
<body>
<canvas id="canvas" width="480" height="300" style="border: 1px solid gray; display: block; margin: 0 auto;"></canvas>
<p id="txt"></p>
<script>
window.onload = function () {
let cnv = document.getElementById('canvas')
let cxt = cnv.getContext('2d')
let ball = new Ball(0, cnv.height / 2, 20)
let mouse = tools.getMouse(cnv)
let isMouseDown = false // isMouseDown 用于標識鼠標是否按下的狀態
let vx = 3
cnv.addEventListener('mousedown', function () {
// 判斷鼠標點擊坐標是否位于小球上,如果是,則 isMouseDown 為 true
if (ball.checkMouse(mouse)) {
isMouseDown = true
alert('捕獲成功')
}
}, false);
(function drawFrame () {
window.requestAnimationFrame(drawFrame)
cxt.clearRect(0, 0, cnv.width, cnv.height)
// 如果鼠標不是按下狀態,則小球繼續運動,否則就會停止
if (!isMouseDown) {
ball.x += vx
}
ball.fill(cxt)
})()
// 添加鼠標移動事件
cnv.addEventListener('mousemove', function () {
// 判斷鼠標當前坐標是否處于小球內
if (ball.checkMouse(mouse)) {
txt.innerHTML = '鼠標移入小球'
} else {
txt.innerHTML = '鼠標移出小球'
}
}, false)
}
</script>
</body>
</html>
```
在這個例子中,我們使用一個變量 isMouseDown 來標識鼠標是否為按下的狀態。然后為 Canvas 添加一個 mousedown 事件,并且在事件中對按下鼠標的坐標進行判斷。在動畫循環中,如果鼠標不是按下狀態,則小球繼續運動,否則就會停止。
## 拖曳物體
在 Canvas 中,想要拖曳一個物體,一般情況下需要以下三個步驟:
(1) 捕獲物體:在鼠標按下(mousedown)時,判斷鼠標坐標是否落在物體表面上,如果落在,則添加兩個事件:mousemove 和 mouseup
(2) 移動物體:在鼠標移動(mousemove)的過程中,更新物體坐標為鼠標坐標
(3) 松開物體:在鼠標松開(mouseup)時,移除 mouseup 事件和 mousemove 事件
偽代碼:
```js
cnv.addEventListener('mousedown', function () {
document.addEventListener('mousemove', onMouseMove, false)
document.addEventListener('mouseup', onMouseUp, false)
}, false)
```
下面是一個拖曳小球的例子:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>捕獲靜止小球</title>
<script src="./ball.js"></script>
<script src="./tool.js"></script>
</head>
<body>
<canvas id="canvas" width="480" height="300" style="border: 1px solid gray; display: block; margin: 0 auto;"></canvas>
<script>
window.onload = function () {
let cnv = document.getElementById('canvas')
let cxt = cnv.getContext('2d')
let ball = new Ball(cnv.width / 2, cnv.height / 2, 20)
ball.fill(cxt)
let mouse = tools.getMouse(cnv)
// 為 canvas 添加鼠標按下事件
cnv.addEventListener('mousedown', function () {
// 判斷鼠標點擊是否落在小球上,如果落在,就添加兩個事件: mousemove, mouseup
if (ball.checkMouse(mouse)) {
document.addEventListener('mousemove', onMouseMove, false)
document.addEventListener('mouseup', onMouseUp, false)
}
}, false)
function onMouseMove () {
// 鼠標移動時,更新小球坐標
ball.x = mouse.x
ball.y = mouse.y
}
function onMouseUp () {
// 鼠標松開時,移除鼠標松開事件:mouseup
document.removeEventListener('mouseup', onMouseUp, false)
document.removeEventListener('mousemove', onMouseMove, false)
};
(function drawFrame () {
window.requestAnimationFrame(drawFrame)
cxt.clearRect(0, 0, cnv.width, cnv.height)
ball.fill(cxt)
})()
}
</script>
</body>
</html>
```

這個例子有一個不自然的 bug,我們在點擊小球時,有時點擊的位置不一定就是小球的中心,但是無論點擊小球什么地方,在點擊之后,小球都會快速地偏移,使得鼠標位于小球的中心。要想修復這個 bug,就得在點擊的時候計算出鼠標與球心之間的坐標差值并在移動小球的過程中進行修正。
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>捕獲靜止小球</title>
<script src="./ball.js"></script>
<script src="./tool.js"></script>
</head>
<body>
<canvas id="canvas" width="480" height="300" style="border: 1px solid gray; display: block; margin: 0 auto;"></canvas>
<script>
window.onload = function () {
let cnv = document.getElementById('canvas')
let cxt = cnv.getContext('2d')
let ball = new Ball(cnv.width / 2, cnv.height / 2, 20)
ball.fill(cxt)
let mouse = tools.getMouse(cnv)
let dx = 0, dy = 0
// 為 canvas 添加鼠標按下事件
cnv.addEventListener('mousedown', function () {
// 判斷鼠標點擊是否落在小球上,如果落在,就添加兩個事件: mousemove, mouseup
if (ball.checkMouse(mouse)) {
dx = mouse.x - ball.x // dx 為鼠標與球心的水平偏移量
dy = mouse.y - ball.y
document.addEventListener('mousemove', onMouseMove, false)
document.addEventListener('mouseup', onMouseUp, false)
}
}, false)
function onMouseMove () {
// 鼠標移動時,更新小球坐標
ball.x = mouse.x - dx
ball.y = mouse.y - dy
}
function onMouseUp () {
// 鼠標松開時,移除鼠標松開事件:mouseup
document.removeEventListener('mouseup', onMouseUp, false)
document.removeEventListener('mousemove', onMouseMove, false)
};
(function drawFrame () {
window.requestAnimationFrame(drawFrame)
cxt.clearRect(0, 0, cnv.width, cnv.height)
ball.fill(cxt)
})()
}
</script>
</body>
</html>
```
## 拋擲效果
怎樣在動畫中表現出拋擲效果呢?我們可以用鼠標選中一個物體,拖曳它向某個方向移動,松開鼠標后物體會沿著拖曳的方向繼續前進。在拋擲物體時,必須在拖曳物體的過程中計算物體的速度向量,并且在釋放物體時將這個額速度向量賦給物體。
舉個例子,如果你以 10px 的速度向左拖曳小球,那么在你釋放小球時,它的速度向量應該是 vx = -10。如果你已每幀 10px 的速度向下拖曳小球,那么在你釋放小球時,它的速度向量應該為 vy = 10,依次類推。
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>拋擲小球</title>
<script src="./ball.js"></script>
<script src="./tool.js"></script>
</head>
<body>
<canvas id="canvas" width="480" height="300" style="border: 1px solid gray; display: block; margin: 0 auto;"></canvas>
<script>
window.onload = function () {
let cnv = document.getElementById('canvas')
let cxt = cnv.getContext('2d')
let ball = new Ball(cnv.width / 2, cnv.height / 2, 20)
ball.fill(cxt)
let mouse = tools.getMouse(cnv)
let isMouseDown = false
let dx = 0, dy = 0
// oldX 和 oldY 用于存儲小球舊的坐標
let oldX, oldY
// 初始速度 vx 和 vy 都為 0
let vx = 0, vy = 0
// 添加 mousedown 事件
cnv.addEventListener('mousedown', function () {
// 判斷鼠標點擊是否落在小球上
if (ball.checkMouse(mouse)) {
// 鼠標按下小球時,isMouseDown 設置為 true
isMouseDown = true
// 鼠標按下小球時,將當前鼠標位置賦值給 oldX 和 oldY
oldX = ball.x
oldY = ball.y
dx = mouse.x - ball.x
dy = mouse.y - ball.y
document.addEventListener('mousemove', onMouseMove, false)
document.addEventListener('mouseup', onMouseUp, false)
}
}, false)
function onMouseMove () {
// 鼠標移動時,更新小球坐標
ball.x = mouse.x - dx
ball.y = mouse.y - dy
}
function onMouseUp () {
isMouseDown = false
document.removeEventListener('mouseup', onMouseUp, false)
document.removeEventListener('mousemove', onMouseMove, false)
};
(function drawFrame () {
window.requestAnimationFrame(drawFrame)
cxt.clearRect(0, 0, cnv.width, cnv.height)
if (isMouseDown) {
// 如果 isMouseDown 為 true,用當前小球的位置減去上一幀的位置
vx = ball.x - oldX
vy = ball.y - oldY
// 更新 oldX 和 oldY 為當前小球位置
oldX = ball.x
oldY = ball.y
} else {
// 如果 isMouseDown 為 false,小球沿著拋擲方向運動
ball.x += vx
ball.y += vy
}
ball.fill(cxt)
})()
}
</script>
</body>
</html>
```

下面我們加入之前學到的邊界檢測、重力、反彈等效果來實現一個較為復雜的動畫效果。
先來看看效果:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>拋擲小球</title>
<script src="./ball.js"></script>
<script src="./tool.js"></script>
</head>
<body>
<canvas id="canvas" width="480" height="300" style="border: 1px solid gray; display: block; margin: 0 auto;"></canvas>
<script>
window.onload = function () {
let cnv = document.getElementById('canvas')
let cxt = cnv.getContext('2d')
let ball = new Ball(cnv.width / 2, cnv.height / 2, 20)
ball.fill(cxt)
let mouse = tools.getMouse(cnv)
let isMouseDown = false
let dx = 0, dy = 0
let oldX, oldY
let vx = 0, vy = 0 // 初始速度
const gravity = 1.5 // 重力
const bounce = -0.8 // 反彈消耗
cnv.addEventListener('mousedown', function () {
// 判斷鼠標點擊是否落在小球上
if (ball.checkMouse(mouse)) {
isMouseDown = true
oldX = ball.x
oldY = ball.y
dx = mouse.x - ball.x
dy = mouse.y - ball.y
document.addEventListener('mousemove', onMouseMove, false)
document.addEventListener('mouseup', onMouseUp, false)
}
}, false)
function onMouseMove () {
// 鼠標移動時,更新小球坐標
ball.x = mouse.x - dx
ball.y = mouse.y - dy
// 加入邊界限制
if (ball.x < ball.radius) {
ball.x = ball.radius
} else if (ball.x > cnv.width - ball.radius) {
ball.x = cnv.width - ball.radius
}
if (ball.y < ball.radius) {
ball.y = ball.radius
} else if (ball.y > cnv.height - ball.radius) {
ball.y = cnv.height - ball.radius
}
}
function onMouseUp () {
isMouseDown = false
document.removeEventListener('mouseup', onMouseUp, false)
document.removeEventListener('mousemove', onMouseMove, false)
};
(function drawFrame () {
window.requestAnimationFrame(drawFrame)
cxt.clearRect(0, 0, cnv.width, cnv.height)
if (isMouseDown) {
vx = ball.x - oldX
vy = ball.y - oldY
oldX = ball.x
oldY = ball.y
} else {
vy += gravity
ball.x += vx
ball.y += vy
// 邊界檢測
if (ball.x > cnv.width - ball.radius) {
ball.x = cnv.width - ball.radius
vx = vx * bounce
} else if (ball.x < ball.radius) {
ball.x = ball.radius
vx = vx * bounce
}
// 碰到下邊界
if (ball.y > canvas.height - ball.radius) {
ball.y = canvas.height - ball.radius
vy = vy * bounce
} else if (ball.y < ball.radius) {
ball.y = ball.radius
vy = vy * bounce
}
}
ball.fill(cxt)
})()
}
</script>
</body>
</html>
```