[TOC]
在 Canvas 中,鼠标事件可以用来实现以下三种常见的用户交互效果:
1. 捕获物体
2. 拖曳物体
3. 抛掷物体
## 捕获物体
想要拖曳一个物体或者抛掷一个物体,首先要知道怎么捕获一个物体。对于 DOM 元素的捕获,可以直接用 document.getElementById() 来实现,但是在 Canvas 中对于物体的捕获就没那简单了。
在 Canvas 中,对于物体的捕获,可以分为以下四种情况来考虑:
(1)矩形的捕获
(2)圆的捕获
(3)多边形的捕获
(4)不规则图形的捕获
多边形和不规则图形的捕获较为复杂,这里只介绍矩形和圆形的捕获。
### 矩形的捕获
![](https://img.kancloud.cn/0a/7d/0a7df45671a6433fc6060416a92e42b6_448x347.png =300x)
如图,我们可以通过获取鼠标点击时的坐标来判断是否捕获了矩形。如果鼠标点击坐标落在矩形上,则说明捕获了这个矩形。伪代码如下:
```js
if (mouse.x > rect.x &&
mouse.x < rect.x + rect.width &&
mouse.y > rect.y &&
mouse.y < rect.y + rect.height) {
// ...
}
```
### 圆的捕获
对于圆的捕获,可以通过判定鼠标与圆心之间的距离来判断是否捕获了圆。如果距离小于半径,说明鼠标落在了圆上。
![](https://img.kancloud.cn/b8/a7/b8a75131a9117c5a7712f2706acbeb89_434x346.png =300x)
伪代码:
```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>
```
![](https://img.kancloud.cn/43/dc/43dc1743ed1f6077d580d7790366127b_641x405.gif =300x)
这个例子有一个不自然的 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>
```
![](https://img.kancloud.cn/d7/ce/d7ce61dfe1281ba8b6fda54952318532_641x405.gif =300x)
下面我们加入之前学到的边界检测、重力、反弹等效果来实现一个较为复杂的动画效果。
先来看看效果:
![](https://img.kancloud.cn/bd/4b/bd4b88133ec0eb5d0ea7c8d350bf9219_641x405.gif =300x)
```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>
```