5.1.1 鼠标事件
在Canvas中,鼠标事件分为以下三种:
1)鼠标按下:mousedown
2)鼠标松开:mouseup
3)鼠标移动:mousemove
在Canvas中,mousedown、mouseup和mousemove这三种事件常用于实现拖拽功能。
监听鼠标位置:
每个鼠标事件都有两个用于确定鼠标当前位置的属性:pageX和pageY(存在兼容性,可以使用clientX和clientY属性替代)。
下面创建一个tools对象,把所有方法都放在这个对象中:
代码语言:javascript复制'use strict';
const tools = {};
//监听鼠标位置
tools.listenMousePosition = function(el, func){
let mouse = {x: 0, y: 0};
el.addEventListener('mousemove', (event) => {
let x,y;
let e = event || window.event;
if(e.pageX || e.pageY){
x = e.pageX;
y = e.pageY;
}else {
x = e.clientX document.body.scrollLeft || document.documentElement.scrollLeft;
y = e.clientY document.body.scrollTop || document.documentElement.scrollTop;
}
x -= el.offsetLeft;
y -= el.offsetTop;
mouse.x = x;
mouse.y = y;
func(mouse);
});
};
export default tools;
5.1.2 键盘事件
在Canvas中,常用的键盘事件有两种:
1)键盘按下:keydown
2)键盘松开:keyup
Canvas元素本身不支持键盘事件,一般情况下使用window对象来实现Canvas中键盘事件的监听。
监听键盘按键:
代码语言:javascript复制//监听键盘按键
tools.listenKeyboardKey = function(func){
window.addEventListener('keydown', (e) => {
let key = '';
switch(e.keyCode){
case 38:
case 87:
key = 'up';
break;
case 39:
case 68:
key = 'right';
break;
case 40:
case 83:
key = 'down';
break;
case 37:
case 65:
key = 'left';
break;
default:
key = '';
}
func(key);
});
}
5.1.3 循环事件
在Canvas中,一般使用requestAnimationFrame()方法来实现循环,从而达到动画效果。
requestAnimationFrame()的作用跟setInterval()是一样的,但是两者有一定区别。setInterval()需要手动设置间隔时间才会生效,requestAnimationFrame()不需要手动设置间隔时间,会自动根据浏览器绘制的帧率进行调整。
语法:
代码语言:javascript复制(function frame(){
window.requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
...
})();
示例:移动的小球
代码语言:javascript复制//下面代码放置前面文章 my-canvas.vue 组件的 methods 对象里
//移动饿小球
moveBall(){
let x = 0, y = 0;
let self = this;
(function frame(){
requestAnimationFrame(frame);
self.clearCanvas();
self.cxtObj.arc(x , y , 20, 0, 360 * Math.PI / 180);
self.cxtObj.fillStyle = '#2fd6f1';
self.cxtObj.fill();
})();
},
一般情况下,由于一些变量往往都需要在动画循环中改变,所以变量的初始化都是在动画循环之外。
从这个例子中也初步知道了Canvas动画的原理是:使用requestAnimationFrame()方法不断地清除Canvas,然后重绘图形。
5.2 物理动画
物理动画,简单来说,就是模拟现实世界的一种动画效果。在物理动画中,物体会遵循牛顿运动定律,如射击游戏中打出去的炮弹会随着重力而降落。
5.2.1 三角函数
常见的三角函数有三种:
语法:
在Canvas中,凡是涉及角度都是用“弧度”表示,在实际开发中,推荐写法为:
代码语言:javascript复制度数 * Math.PI / 180
在三角函数中,我们可以使用反正切函数Math.atan()来求出两个直角边对应的夹角的度数,但是可能会出现有一个度数对应两个夹角的情况:
在Canvas中,我们可以使用反正切函数 Math.atan2() 来求出两个直角边对应的夹角的度数,并且能够准确判断该度数对应的是哪一个夹角。
语法:
代码语言:javascript复制//x:表示邻边,y:表示对边,x、y都要区分正负
Math.atan2(y, x);
示例:追随鼠标旋转
首先需要一个可供旋转的对象,先定义一个箭头类,专门用于绘制一个箭头:
代码语言:javascript复制//定义一个箭头类
export function Arrow(x, y, color, angle){
//箭头中心x坐标,默认为0
this.x = x || 0;
//箭头中心y坐标,默认为0
this.y = y || 0;
//颜色,默认值为#f09
this.color = color || '#f09';
//旋转角度,默认值为0
this.angle = angle || 0;
}
Arrow.prototype = {
draw(cxt, arrowData, type){
cxt.save();
cxt.translate(this.x, this.y);
cxt.rotate(this.angle);
cxt.beginPath();
let moveToObj = arrowData.moveToObj;
let lineToArr = arrowData.lineToArr;
cxt.moveTo(moveToObj.x, moveToObj.y);
lineToArr.forEach(val => cxt.lineTo(val.x, val.y));
cxt.closePath();
switch(type){
case 'stroke':
cxt.strokeStyle = this.color;
cxt.stroke();
break;
case 'fill':
cxt.fillStyle = this.color;
cxt.fill();
break;
}
cxt.restore();
},
}
然后在my-canvas.vue组件中应用:
代码语言:javascript复制//my-canvas.vue
<template>
<div class="container">
<div class="opt-btn">
<el-button type="primary" size="mini" v-for="(item, index) in optBtnData" :key="index" @click="item.clickBtnFunc">{{item.text}}</el-button>
</div>
<canvas ref="myCanvas" class="my-canvas" width="300" height="300"></canvas>
</div>
</template>
<script>
import tools from '@/api/tools'
import {Arrow} from '@/api/tools'
export default {
data(){
return {
cnvObj: null,//canvas对象
cxtObj: null,//上下文环境对象
optBtnData: [// 操作按钮数据
{text: '绘制箭头', clickBtnFunc: () => {this.drawArrows(this.arrowsData, this.cxtObj, this.cnvObj)}},
//...
],
arrowsData: {//绘制箭头数据
moveToObj: {x: -20, y: -10},
lineToArr: [
{x: 0, y: -10}, {x: 0, y: -20}, {x: 20, y: 0},
{x: 0, y: 20}, {x: 0, y: 10}, {x: -20, y: 10}
]
},
//...
}
},
methods: {
//绘制箭头
drawArrows(arrowsData, cxt, cnv){
let arrow = new Arrow(cnv.width / 2, cnv.height / 2);
arrow.draw(cxt, arrowsData, 'fill');
tools.listenMousePosition(cnv, (mouse) => {
(function drawFrame(){
requestAnimationFrame(drawFrame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
let dx = mouse.x - cnv.width / 2;
let dy = mouse.y - cnv.height / 2;
arrow.angle = Math.atan2(dy, dx);
arrow.draw(cxt, arrowsData, 'fill');
})()
});
},
//...
}
}
</script>
示例效果:
实现原理:在动画循环中,每次鼠标移动的时候,计算鼠标当前位置与箭头中心的夹角,然后把这个夹角作为箭头旋转的角度,重绘箭头。
三角函数的应用:
1)两点间的距离
在Canvas中,假设有两点(x1, y1)和(x2, y2),求两点之间距离:
代码语言:javascript复制dx = x2 - x1;
dy = y2 - y1;
distance = Math.sqrt(dx * dx dy * dy);
2)圆周运动
在Canvas中,圆周运动共有两种形式,即正圆运动和椭圆运动。
① 正圆运动
语法:
代码语言:javascript复制//centerX、centerY:表示圆心坐标
//angle:表示一个弧度制的角度
//radius:表示圆的半径
x = centerX Math.cos(angle) * radius;
y = centerY Math.sin(angle) * radius;
示例:小球绕着圆环旋转
首先需要一个可供旋转的对象,先定义一个球类,专门用于绘制一个球:
代码语言:javascript复制//定义一个球类
export function Ball(x, y, radius, color){
//小球中心的x坐标,默认值为0
this.x = x || 0;
//小球中心的y坐标,默认值为0
this.y = y || 0;
//小球半径,默认值为12
this.radius = radius || 12;
//小球颜色,默认值为‘#69f’
this.color = color || '#f09';
this.scaleX = 1;
this.scaleY = 1;
}
Ball.prototype = {
draw(cxt, type){
cxt.save();
cxt.scale(this.scaleX, this.scaleY);
cxt.beginPath();
cxt.arc(this.x, this.y, this.radius, 0, 360 * Math.PI / 180);
cxt.closePath();
switch(type){
case 'stroke':
cxt.strokeStyle = this.color;
cxt.stroke();
break;
case 'fill':
cxt.fillStyle = this.color;
cxt.fill();
break;
}
cxt.restore();
}
}
然后在my-canvas.vue组件中应用:
代码语言:javascript复制//my-canvas.vue
//...
<script>
//...
import {Arrow, Ball} from '@/api/tools'
export default {
data(){
return {
//...
optBtnData: [// 操作按钮数据
//...
{text: '移动小球', clickBtnFunc: () => {this.moveBall(this.cxtObj, this.cnvObj)}},
],
//...
}
},
methods: {
//...
//旋转的小球
moveBall(cxt, cnv){
let centerX = cnv.width / 2;
let centerY = cnv.height / 2;
let radius = 50, angle = 0;
let ball = new Ball();
let circle = new Ball(centerX, centerY, radius);
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
cxt.beginPath();
circle.draw(cxt, 'stroke');
cxt.closePath();
cxt.stroke();
ball.x = centerX Math.cos(angle) * radius;
ball.y = centerY Math.sin(angle) * radius;
ball.draw(cxt, 'fill');
angle = 0.05;
})();
}
}
}
</script>
示例效果:
② 椭圆运动
语法:
代码语言:javascript复制//centerX、centerY:表示圆心坐标
//angle:表示一个弧度制的角度
//radiusX:表示椭圆的x轴半径
//radiusY:表示椭圆的y轴半径
x = cneterX Math.cos(angle) * radiusX;
y = centerY Math.sin(angle) * radiusY;
3)波形运动
在Canvas中,根据sin函数作用对象的不同,常见的波形运动有:
① 作用于x轴坐标
当正弦函数sin作用于物体中心的x轴坐标时,物体会进行左右摇摆,类似于水草在水流中左右摇摆。
语法:
代码语言:javascript复制//(centerX, centerY):表示物体中心坐标
//angle:表示角度(弧度制)
//range:表示振幅
//speed:表示角度改变的大小
x = centerX Math.sin(angle) * range;
angle = speed;
示例:左右移动的小球
代码语言:javascript复制//my-canvas.vue
//...
methods: {
//...
//左右移动的小球
moveBall(cxt, cnv){
let centerX = cnv.width / 2;
let centerY = cnv.height / 2;
let ball = new Ball(centerX, centerY);
let angle = 0, range = 80;
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
ball.x = centerX Math.sin(angle) * range;
ball.draw(cxt, 'fill');
angle = 0.05;
})()
}
}
由于sin函数的值是在[-1, 1]之间,在实际开发中我们需要一个较大值的振幅,使得摆动的幅度看起来更加明显一些。
② 作用于y轴坐标
当正弦函数sin作用于物体中心的y轴坐标时,物体运动的轨迹刚好就是sin函数的波形。
语法:
代码语言:javascript复制y = centerY Math.sin(angle) * rangle;
angle = speed;
示例:波形运动
代码语言:javascript复制//my-canvas.vue
//...
methods: {
//...
//波形运动
moveBall(cxt, cnv){
let centerY = cnv.height / 2;
let ball = new Ball(0, centerY);
let angle = 0, range = 40;
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
ball.x = 1;
ball.y = centerY Math.sin(angle) * range;
ball.draw(cxt, 'fill');
angle = 0.05;
})()
}
}
5.2.2 匀速运动
语法:
代码语言:javascript复制//object.x:表示物体x轴坐标
//object.y:表示物体y轴坐标
//vx:表示x轴方向的速度大小
//vy:表示y轴方向的速度大小
object.x = vx;
object.y = vy;
在匀速运动中,速度有正反方向之分的,正数表示正方向,负数表示反方向。
速度的合成和分解:
语法:
代码语言:javascript复制//object.x:表示物体x轴坐标
//object.y:表示物体y轴坐标
//vx:表示x轴方向的速度大小
//vy:表示y轴方向的速度大小
//speed: 表示任意方向的速度大小
//angle:表示该速度的方向与x轴正方向的弧度制角度
vx = speed * Math.cos(angle);
vy = speed * Math.sin(angle);
object.x = vx;
object.y = vy;
对于非x轴方向或非y轴方向上的匀速运动,都采用“分解速度”的方式来实现。
示例:箭头跟随鼠标移动
代码语言:javascript复制//my-canvas.vue
//...
methods: {
//...
// 移动的箭头
moveArrows(arrowsData, cxt, cnv){
let arrow = new Arrow(cnv.width / 2, cnv.height / 2);
arrow.draw(cxt, arrowsData, 'fill');
let speed = 0.01, angle = 0;
tools.listenMousePosition(cnv, (mouse) => {
(function drawFrame(){
requestAnimationFrame(drawFrame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
let dx = mouse.x - cnv.width / 2;
let dy = mouse.y - cnv.height / 2;
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.draw(cxt, arrowsData, 'fill');
})()
});
},
}
示例效果:
5.2.3 加速运动
语法:
代码语言:javascript复制//object.x:表示物体x轴坐标
//object.y:表示物体y轴坐标
//vx:表示x轴方向的速度大小
//vy:表示y轴方向的速度大小
//ax:表示x轴方向加速度
//ay:表示y轴方向加速度
vx = ax;
vy = ay;
object.x = vx;
object.y = vy;
加速度的合成和分解:
语法:
代码语言:javascript复制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;
示例:加速运动的小球
代码语言:javascript复制//my-canvas.vue
//...
methods: {
//...
//加速运动的小球
moveBall(cxt, cnv){
let ball = new Ball();
let a = 0.1, vx = 0, vy = 0;
let ax = a * Math.cos(30 * Math.PI / 180);
let ay = a * Math.sin(30 * Math.PI / 180);
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
ball.x = vx;
ball.y = vy;
ball.draw(cxt, 'fill');
vx = ax;
vy = ay;
})()
}
}
5.2.4 重力
语法:
代码语言:javascript复制vy = gravity;
object.y = vy;
示例:小球从空中自由降落到地面,然后反弹,循环往复,直到它最终速度为0而停止在地面上
代码语言:javascript复制//my-canvas.vue
//...
methods: {
//...
//自由降落的小球
moveBall(cxt, cnv){
let ball = new Ball(cnv.width / 2, 0);
let vy = 0, gravity = 0.2, bounce = -0.8;
(function frame(){
requestAnimationFrame(frame);
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.draw(cxt, 'fill');
vy = gravity;
})()
}
}
一般情况下,小球碰到地面都会反弹,由于反弹会有速度损耗,并且小球y轴速度方向会变为反方向,因此需要乘以一个反弹系数bounce,一般取值为-1.0 ~ 0之间的任意数。
Canvas动画循环中注意两点:
1)对于需要不断改变的变量,一般在动画循环之前先定义;
2)对于需要不断改变的变量,一般在动画循环中图形绘制之后才递增或递减。
在实际开发的过程中,任何复杂的效果,都可以采用类似“分而治之”的方法来思考,再复杂的Canvas物理动画,我们从x轴和y轴两个方向来考虑,实现的思路就非常清晰了。
5.2.5 摩擦力
语法:
代码语言:javascript复制vx *= friction;
vy *= friction;
object.x = vx;
object.y = vy;
示例:小球沿任意方向移动
代码语言:javascript复制//my-canvas.vue
//...
methods: {
//...
//小球沿任意方向移动
moveBall(cxt, cnv){
let ball = new Ball();
let speed = 8, friction = 0.95;
let vx = speed * Math.cos(30 * Math.PI / 180);
let vy = speed * Math.sin(30 * Math.PI / 180);
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
ball.x = vx;
ball.y = vy;
ball.draw(cxt, 'fill');
vx *= friction;
vy *= friction;
})()
}
}
当物体沿任意方向运动时,如果我们加入摩擦力因素,那么每次都是先把该方向的速度分解为x轴和y轴两个方向的分速度,然后再用分速度乘以摩擦系数。