ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 键盘事件 在 Canvas 中,常用的键盘事件有两种 (1)键盘按下:keydown (2)键盘松开:keyup 键盘事件中一般都是根据按键的 keyCode 来判断用户按下的是键盘中的哪个键,常见的按键及其对应的 keyCode 如下表: | 按键 | keyCode | | --- | --- | | W | 87 | | S | 83 | | A | 65 | | D | 68 | | ↑ | 38 | | ↓ | 40 | | ← | 37 | | → | 39 | 我们先写一个 tool.js 做一些封装 ```js // 获取键盘控制方向 window.tools.getKey = function () { var key = {} window.addEventListener('keydown', function (e) { if (e.keyCode === 38 || e.keyCode === 87) { key.direction = 'up' } else if (e.keyCode === 39 || e.keyCode === 68) { key.direction = 'right' } else if (e.keyCode === 40 || e.keyCode === 83) { key.direction = 'down' } else if (e.keyCode === 37 || e.keyCode === 65) { key.direction = 'left' } else { key.direction = '' } }, false) return key } ``` getKey() 方法返回一个对象 key,这个对象有一个 direction 属性,表示用户控制物体移动的方向。使用时只需要判断 direction 属性值是什么即可。 ```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="./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') // 初始化一个圆形 drawBall(cnv.width / 2, cnv.height / 2) // 初始化变量 let x = 100 let y = 75 // 获取按键方向 let key = tools.getKey() // 添加鼠标按下事件 window.addEventListener('keydown', function (e) { // 清除整个 Canvas,以重绘图形 cxt.clearRect(0, 0, cnv.width, cnv.height) // 根据 key.direction 的值,判断小球移动方向 switch (key.direction) { case 'up': y -= 2 drawBall(x, y) break case 'down': y += 2 drawBall(x, y) break case 'left': x -= 2 drawBall(x, y) break case 'right': x += 2 drawBall(x, y) break default: // 如果不加 default 按下其他按键小球就会消失了! drawBall(x, y) } }, false) // 定义绘制小球的函数 function drawBall (x, y) { cxt.beginPath() cxt.arc(x, y, 20, 0, 360 * Math.PI / 180, true) cxt.closePath() cxt.fillStyle = '#6699FF' cxt.fill() } } </script> </body> </html> ``` 这里先引入了 tool.js 文件,以便使用 getKey() 方法来获取用户控制小球的方向,然后使用 window.addEventListener() 来监听键盘事件,根据 key.direction 的值来判断小球移动的方向以实现控制小球的移动。 ## requestAnimationFrame() 的使用 Canvas 中一般都使用 requestAnimationFrame() 来实现循环,从而达到动画效果,常见的语法如下,详细解释见 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame) ```js (function frame () { window.requestAnimationFrame(frame) context.clearRect(0, 0, canvas.width, canvas.height) // ... })() ``` 这里定义了一个自执行函数 frame(),然后在函数内部使用 window.requestAnimationFrame() 不断调用 frame()。对于 Canvas 动画效果,每次必须清空画布然后重绘才行,所以需要使用 clearRect() 方法清空画布。 requestAnimationFrame() 方法的兼容代码如下: ```js window.requestAnimationFrame = ( window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || function (callback) { return window.setTimeout(callback, 1000 / 60) } ) ``` ```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> </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') // 初始化圆的 x 轴坐标为 0 let x = 0; // 动画循环 (function frame () { window.requestAnimationFrame(frame) cxt.clearRect(0, 0, cnv.width, cnv.height) // 绘制圆 cxt.beginPath() cxt.arc(x, 70, 20, 0, 360 * Math.PI / 180, true) cxt.closePath() cxt.fillStyle = 'skyblue' cxt.fill() // 变量递增 x += 2 })() } </script> </body> </html> ``` ## 物理动画 物理动画,简单来说,就是模拟现实世界的一种动画效果。在物理动画中,物体会遵循牛顿运动定律。 ### 三角函数 ![](https://img.kancloud.cn/43/59/43595a3cd0ecefa813481c74d8a66280_529x283.png =400x) 对应 JavaScript 中的函数如下: - sin(θ):Math.sin(θ * Math.PI / 180) - cos(θ):Math.cos(θ * Math.PI / 180) - tan(θ):Math.tan(θ * Math.PI / 180) - arcsin(x / R):Math.asin(x / R) * (180 / Math.PI) - arccos(x / R): Math.acos(x / R) * (180 / Math.PI) - arctan(x / R): Math.atan(x / R) * (180 / Math.PI) 在 Canvas 中,凡是涉及角度都是用 “弧度制” 表示,例如 180° 写成 Math.PI,360° 写成 Math.PI * 2,所以角度都推荐下面这种写法 ```js 度数 * Math.PI / 180 ``` <span style="font-size: 15px; font-weight: 600; color: #409EFF;">Math.atan() 与 Math.atan2()</span> 使用 Math.atan() 函数可能会出现有一个度数对应两个夹角的情况,如图(注意 canvas 中的坐标系): ![](https://img.kancloud.cn/2a/88/2a88e7f930b9226df8d07860d9a7e018_952x607.png) 对于上图中的四个内角,将有以下正切值: tan(A) = tan(C) = -0.5 tan(B) = tan(D) = 0.5 为了解决这个问题,可以使用反正切函数 Math.atan2() 来求出两条边之间夹角的度数,并且能够准确判断度数对应哪一个夹角。 ![](https://img.kancloud.cn/47/7d/477d6bd16536cad48f463beaa976de64_452x191.png =300x) `Math.atan2(y, x)`接收两个参数,y 表示对边的变长,x 表示邻边的边长 Math.atan(1 / 2) 和 Math.atan((-1) / (-2)) 的结果是一样的,但是对于 Math.atan2() 函数而言,其结果是不同的。 ```js console.log(Math.atan2(1, 2), Math.atan2(-1, -2)) // 0.4636476090008061 -2.677945044588987 console.log(`Math.atan2(1, 2) 对应的角度为: ${Math.atan2(1, 2) * 180 / Math.PI} Math.atan2(-1, -2) 对应的角度为: ${Math.atan2(-1, -2) * 180 / Math.PI} `) // Math.atan2(1, 2) 对应的角度为: 26.56505117707799 // Math.atan2(-1, -2) 对应的角度为: -153.43494882292202 ``` ![](https://img.kancloud.cn/2a/88/2a88e7f930b9226df8d07860d9a7e018_952x607.png =400x) 可以看到 Math.atan2(1, 2) 对应的是角 B,而 Math.atan2(-1, -2) 对应的是角 D,-153。43° 这个角度是从 x 轴正方向开始以逆时针方向计算的,这样就把两个角区分开来了。 ![](https://img.kancloud.cn/3b/4c/3b4cc5c88e3f73e7757324aeb61bfd48_600x209.png =300x) 下面的示例是 Math.atan2() 的一个经典效果:追随鼠标旋转 首先写一个箭头类 arrow.js 用于绘制箭头 ```js Arrow.prototype = { stroke: function (cxt) { cxt.save() cxt.translate(this.x, this.y) cxt.rotate(this.angle) cxt.strokeStyle = this.color cxt.beginPath() cxt.moveTo(-20, -10) cxt.lineTo(0, -10) cxt.lineTo(0, -20) cxt.lineTo(20, 0) cxt.lineTo(0, 20) cxt.lineTo(0, 10) cxt.lineTo(-20, 10) cxt.closePath() cxt.stroke() cxt.restore() }, fill: function (cxt) { cxt.save() cxt.translate(this.x, this.y) cxt.rotate(this.angle) cxt.fillStyle = this.color cxt.beginPath() cxt.moveTo(-20, -10) cxt.lineTo(0, -10) cxt.lineTo(0, -20) cxt.lineTo(20, 0) cxt.lineTo(0, 20) cxt.lineTo(0, 10) cxt.lineTo(-20, 10) cxt.closePath() cxt.fill() cxt.restore() } } ``` ```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="./arrow.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 arrow = new Arrow(cnv.width / 2, cnv.height / 2) // 获取鼠标坐标 let mouse = tools.getMouse(cnv); // 这里记得加分号,封装见 tools.js (function drawFrame () { window.requestAnimationFrame(drawFrame, cnv) cxt.clearRect(0, 0, cnv.width, cnv.height) let dx = mouse.x - cnv.width / 2 let dy = mouse.y - cnv.height / 2 // 使用 Math.atan2() 方法计算出鼠标与建投中心的夹角 arrow.angle = Math.atan2(dy, dx) arrow.fill(cxt) })() } </script> </body> </html> ``` ![](https://img.kancloud.cn/50/d0/50d003464ea2d321d869c4084c9f30c2_641x405.gif =300x) 效果:当鼠标在画布上移动时,箭头会跟着鼠标移动的方向进行旋转。其原理很简单,在动画循环过程中,每次鼠标移动的时候,都会计算鼠标当前位置与箭头中心的夹角,然后把这个夹角作为箭头旋转的角度,重绘箭头即可。 ![](https://img.kancloud.cn/fa/8e/fa8e848b17a26ff022bb585a883ddc8f_396x323.png =200x) <span style="font-size: 15px; font-weight: 600; color: #409EFF;">圆周运动</span> Canvas 中的圆周运动一般有两种,即正圆运动和椭圆运动。 ![](https://img.kancloud.cn/51/13/5113d51659793de9362a4d666a4e54b3_752x548.png) 正圆运动就是利用上图中的数学公式,下面做一个小球做正圆运动的例子: 首先建立一个 ball.js 文件用于存放小球类: ```js function Ball (x, y, radius, color) { this.x = x || 0 this.y = y || 0 this.radius = radius || 12 this.color = color || '#6699FF' this.scaleX = 1 this.scaleY = 1 } Ball.prototype = { // 绘制 "描边" 小球 stroke: function (cxt) { cxt.save() cxt.scale(this.scaleX, this.scaleY) cxt.strokeStyle = this.color cxt.beginPath() cxt.arc(this.x, this.y, this.radius, 0, 360 * Math.PI / 180, false) cxt.closePath() cxt.stroke() cxt.restore() }, // 绘制 "填充" 小球 fill: function (cxt) { cxt.save() cxt.translate(this.x, this.y) cxt.rotate(this.rotation) cxt.scale(this.scaleX, this.scaleY) cxt.fillStyle = this.color cxt.beginPath() cxt.arc(0, 0, this.radius, 0, 360 * Math.PI / 180, false) cxt.closePath() cxt.fill() cxt.restore() } } ``` 把圆的坐标公式套进去就能完成动画了。 ```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') // 实例化一个小球,中心坐标为 (100, 25),半径、颜色都取默认值 let ball = new Ball(100, 25) let centerX = cnv.width / 2 let centerY = cnv.height / 2 let radius = 50 let angle = 0; (function frame () { window.requestAnimationFrame(frame) cxt.clearRect(0, 0, cnv.width, cnv.height) // 绘制圆形 cxt.beginPath() cxt.arc(centerX, centerY, 50, 0, 360 * Math.PI / 180, false) cxt.closePath() cxt.stroke() // 计算小球坐标 ball.x = centerX + Math.cos(angle) * radius ball.y = centerY + Math.sin(angle) * radius ball.fill(cxt) // 角度递增 angle += 0.05 })() } </script> </body> </html> ``` ![](https://img.kancloud.cn/15/21/15210546e03269527a07e185643f276a_641x405.gif =300x) <span style="font-size: 15px; font-weight: 600; color: #409EFF;">椭圆运动</span> ![](https://img.kancloud.cn/19/d7/19d7e429531964f088352dcc43698844_934x656.png) 总之就是把上面的公式套进去即可,代码略作修改: ```js ball.x = centerX + Math.cos(angle) * radiusX ball.y = centerY + Math.sin(angle) * radiusY ``` ![](https://img.kancloud.cn/57/ed/57edd6f8807bdcd5e10761b4ec41d650_641x405.gif =300x) <span style="font-size: 15px; font-weight: 600; color: #409EFF;">波形运动</span> 正弦函数 sin 和余弦函数 cos 都有属于它们自身的波形,由于它俩是相似的,这里仅以 sin 函数为例介绍。 在 Canvas 中,根据 sin 函数作用对象的不同,常见的波形运动可以分为三种 (1)作用于 x 轴坐标 (2)作用于 y 轴坐标 (3)作用于缩放属性(scaleX 或 scaleY) 1、作用于 x 轴坐标 当正弦函数作用于物体中心的 x 轴坐标时,物体会进行左右摇摆,类似于水草在水流中左右摇摆 语法: ```js x = centerX + Math.sin(angle) * range angel += speed ``` 其中,(centerX,centerY)表示物体中心坐标,angle 表示角度(弧度制),range 表示振幅,speed 表示角度改变的大小。 ```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) let angle = 0 let range = 80; (function frame () { window.requestAnimationFrame(frame) cxt.clearRect(0, 0, cnv.width, cnv.height) // 计算小球坐标 ball.x = cnv.width / 2 + Math.sin(angle) * range ball.fill(cxt) // 角度递增 angle += 0.05 })() } </script> </body> </html> ``` ![](https://img.kancloud.cn/49/a1/49a19834e2eee972c06a9f3e416e665f_641x405.gif =300x) 当正弦函数 sin 只作用于物体的 x 轴坐标时,就可以实现类似水草摆动的平滑运动效果。如果想使摆动的幅度看起来更明显一些,可以乘以一个较大的值(振幅)。 2、作用于 y 轴坐标 当正弦函数 sin 作用于物体中心的 y 轴坐标时,物体运动的轨迹刚好就是 sin 函数的波形 语法: ```js y = centerY + Math.sin(angle) * rangel angle += speed ``` ```js ball.x += 1 ball.y = cnv.height / 2 + Math.sin(angle) * range ball.fill(cxt) ``` ![](https://img.kancloud.cn/b8/cf/b8cf78b24c1012e96c789d9ae5542dbc_641x405.gif =300x) 3、作用于缩放属性(scaleX 或 scaleY) 当正弦函数 sin 作用于物体的缩放属性时,物体会不断地放大然后缩小,从而产生一种脉冲动画的效果。 语法: ```js scaleX = 1 + Math.sin(angle) * range scaleY = 1 + Math.sin(angle) * range angle += speed ``` ```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>sin 函数作用于缩放属性</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, 25) let range = 0.5 let angle = 0; (function frame () { window.requestAnimationFrame(frame) cxt.clearRect(0, 0, cnv.width, cnv.height) ball.scaleX = 1 + Math.sin(angle) * range ball.scaleY = 1 + Math.sin(angle) * range ball.fill(cxt) // 角度递增 angle += 0.05 })() } </script> </body> </html> ``` ![](https://img.kancloud.cn/71/a2/71a2676386a619514eb5b589a9af4ecd_641x405.gif =300x) ### 匀速运动 匀速运动是一种加速度为 0 的运动,比较简单,语法如下: ```js object.x += vx object.y += vy ``` 其中,object.x 表示物体 x 轴坐标,object.y 表示物体 y 轴坐标。vx 表示 x 轴方向的速度大小,vy 表示 y 轴方向的速度大小。 如果我们想在任意方向上做匀速运动该怎么做呢?这就需要用到速度的合成与分解。 ![](https://img.kancloud.cn/ce/a6/cea6cda4aa3d44ce0b8e83085510e9b8_747x417.png =400x) 语法: ```js vx = speed * Math.cos(angle * Math.PI / 180) vy = speed * Math.sin(angle * Math.PI / 180) object.x += vx object.y += vy ``` 下面是一个箭头追随鼠标匀速移动的例子: ```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="./arrow.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 arrow = new Arrow(cnv.width / 2, cnv.height / 2) // 获取鼠标坐标 let mouse = tools.getMouse(cnv) let speed = 1.5 let angle = 0; (function drawFrame () { window.requestAnimationFrame(drawFrame, cnv) cxt.clearRect(0, 0, cnv.width, cnv.height) let dx = mouse.x - cnv.width / 2 let dy = mouse.y - cnv.height / 2 // 使用 Math.atan2() 方法计算出鼠标与箭头中心的夹角 angle = Math.atan2(dy, dx) let vx = Math.cos(angle) * speed let vy = Math.sin(angle) * speed arrow.x += vx arrow.y += vy arrow.angle = angle arrow.fill(cxt) })() } </script> </body> </html> ``` ![](https://img.kancloud.cn/42/36/4236777e2b01f12f8305d9931cc599ce_641x405.gif =300x) ### 加速运动 匀速运动的速度大小是一直保持不变的,而加速运动的速度大小是会随着时间变化而变化的。语法: ```js vx += ax // ax 表示 x 轴方向加速度 vy += ay object.x += vx object.y += vy ``` 同样的,想做任意方向上的加速度就需要用到加速度的合成与分解。 ![](https://img.kancloud.cn/ea/f5/eaf50397b16a8531ec428705ea3c2729_620x313.png =300x) 语法: ```js ax = a * Math.cos(angle * Math.PI / 180) ay = a * Math.sin(angle * Math.PI / 180) vx += ax vy += ay object.x += vx object.y += vy ``` ### 重力 语法: ```js vy += gravity object.y += vy ``` 对于重力引起的运动,可以看成是沿着 y 轴正方向的加速运动。 利用重力的一个常见的效果就是自由落体反弹的效果,例子如下: ```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, 0) // y 轴初始速度为 0,重力加速度为 0.2,反弹系数为 -0.8 let vy = 0 const gravity = 0.2 const bounce = -0.8; (function drawFrame () { window.requestAnimationFrame(drawFrame) cxt.clearRect(0, 0, cnv.width, cnv.height) ball.y += vy // 边界检测 if (ball.y > cnv.height - ball.radius) { ball.y = cnv.height - ball.radius // 速度反向并且减小 vy = vy * bounce } ball.fill(cxt) // 变量递增,注意放在图形绘制之后 vy += gravity })() } </script> </body> </html> ``` ![](https://img.kancloud.cn/bb/07/bb07d38db1c1f3563bedbab24b9ad420_641x405.gif =300x) 小球碰到地面一般都会反弹,由于反弹会有速度损耗,并且小球 y 轴速度方向会变为反方向,因此需要乘以一个反弹系数 bounce,其取值一般为 -1.0 ~ 0 之间的任意数。 ### 摩擦力 摩擦力指的是阻碍物体相对运动的力,其方向与物体运动的方向相反。摩擦力只会改变速度的大小而不会改变它的方向,即摩擦力只能将物体的速度降为 0,但它无法让物体掉头往相反的方向移动。 语法: ```js vx *= friction // 摩擦系数 vy *= friction object.x += vx object.y += vy ``` 需要注意的是,当物体沿任意方向运动时,如果加入摩擦力因素,那么每次都应该先把该方向的速度分解为 x 轴和 y 轴两个方向的分速度,然后再用分速度乘以摩擦系数,而不是分解摩擦力。