[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>
```
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs