边界检测,指的是检测一个物体所处“运动环境的范围”,简单来说,就是给运动物体限定一个范围,从而实现某些动画效果。
在Canvas动画中,我们可以为物体设置一个运动范围,这个运动范围可以是整个画布,也可以是画布的一部分,大多数情况下,都会把物体运动范围设置为整个画布。
1.1 边界限制
边界限制,指的是通过边界检测的办法来限制物体的运动范围,使得其无法超出这个运动范围,而只限在范围里面运动。
语法:
代码语言:javascript复制if(ball.x < ball.radius){
//小球“碰到”左边界时
}else if(ball.x > cnv.width - ball.radius){
//小球“碰到”右边界时
}
if(ball.y < ball.radius){
//小球“碰到”上边界时
}else if(ball.y > cnv.height - ball.radius){
//小球“碰到”下边界时
}
1.2 边界环绕
边界环绕,指的是当物体从一个边界消失后,它就会从对立的边界重新出现,从而形成一种环绕效果。
语法:
代码语言:javascript复制if(ball.x < -ball.radius){
//小球“完全超出”左边界时
}else if(ball.x > cnv.width ball.radius){
//小球“完全超出”右边界时
}
if(ball.y < -ball.radius){
//小球“完全超出”上边界时
}else if(ball.y > cnv.height ball.radius){
//小球“完全超出”下边界时
}
1.3 边界生成
边界生成,指的是物体完全超出边界之后,会在最开始的位置重新生成。这种技巧可用于创建喷泉以及各种粒子效果。
边界生成可以源源不断地为Canvas提供运动物体,而不用担心Canvas上的物体过多以至于影响浏览器的性能速度,因为物体的数量是固定不变的。
语法:
代码语言:javascript复制if(ball.x < -ball.radius ||
ball.x > cnv.width ball.radius ||
ball.y < -ball.radius ||
ball.y > cnv.height ball.radius
){
...
}
示例:不断下落的小球
代码语言:javascript复制//tools.js
//随机生成一个十六进制颜色值的字符串
tools.getRandomColor = function(){
let color = '#';
for(let i = 0; i < 6; i ){
let n = Math.floor(Math.random() * 16);
color = '0123456789abcdef'[n];
}
return color;
}
代码语言:javascript复制//my-canvas.vue
//...
import tools from '@/api/tools'
import {Arrow, Ball} from '@/api/tools'
<script>
//...
export default {
data(){
return {
//...
optBtnData: [// 操作按钮数据
//...
{text: '边界生成', clickBtnFunc: () => {this.createManyBalls(this.cxtObj, this.cnvObj)}},
],
//...
}
},
methods: {
//...
//不断下落的小球
createManyBalls(cxt, cnv){
let balls = [];
let n = 50;
let gravity = 0.15;
for(let i = 0; i < n; i ){
let ball = new Ball(cnv.width / 2, 0, 5, tools.getRandomColor());
//ball.vx和ball.vy取值为:-3~3之间的任意数
ball.vx = (Math.random() * 2 -1) * 3;
ball.vy = (Math.random() * 2 -1) * 3;
balls.push(ball);
}
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
balls.forEach(ball => {
if(ball.x < -ball.radius ||
ball.x > cnv.width ball.radius ||
ball.y < -ball.radius ||
ball.y > cnv.height ball.radius){
ball.x = cnv.width / 2;
ball.y = 0;
ball.vx = (Math.random() * 2 -1) * 3;
ball.vy = (Math.random() * 2 -1) * 3;
}
ball.draw(cxt, 'fill');
ball.x = ball.vx;
ball.y = ball.vy;
ball.vy = gravity;
})
})();
},
}
}
</script>
我们可以通过调整随机数的范围,控制物体的运动方向和范围,从而实现各种有趣效果。
1.4 边界反弹
边界反弹,指的是物体触碰到边界之后就会反弹回来,就像现实世界中小球碰到墙壁反弹一样。
在物体碰到边界后,我们需要做两件事,即保持它的位置不变和改变它的速度力量。也就是说,如果物体碰到左边界或右边界的时候,就对vx取反,而vy不变;如果物体碰到上边界或下边界的时候,就对vy取反,vx不变。
语法:
代码语言:javascript复制//碰到左边界
if(ball.x < ball.radius){
ball.x = ball.radius;
vx = -vx;
//碰到右边界
}else if(ball.x > cnv.width - ball.radius){
ball.x = cnv.width - ball.radius;
vx = -vx;
}
//碰到上边界
if(ball.y < ball.radius){
ball.y = ball.radius;
vy = -vy;
//碰到下边界
}else if(ball.y > cnv.height- ball.radius){
ball.y = cnv.height- ball.radius;
vy = -vy;
}
示例:多球反弹
代码语言:javascript复制//my-canvas.vue
//...
import tools from '@/api/tools'
import {Arrow, Ball} from '@/api/tools'
<script>
//...
export default {
data(){
return {
//...
optBtnData: [// 操作按钮数据
//...
{text: '多球反弹', clickBtnFunc: () => {this.ballsRebound(this.cxtObj, this.cnvObj)}},
],
//...
}
},
methods: {
//...
//多球反弹
ballsRebound(cxt, cnv){
let balls = [];
let n = 10;
for(let i = 0; i < n; i ){
let ball = new Ball(cnv.width / 2, cnv.height / 2, 8, tools.getRandomColor());
//ball.vx和ball.vy取值为:-3~3之间的任意数
ball.vx = (Math.random() * 2 -1) * 3;
ball.vy = (Math.random() * 2 -1) * 3;
balls.push(ball);
}
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
balls.forEach(ball => {
ball.x = ball.vx;
ball.y = ball.vy;
if(ball.x < ball.radius){
ball.x = ball.radius;
ball.vx = -ball.vx;
}else if(ball.x > cnv.width - ball.radius){
ball.x = cnv.width - ball.radius;
ball.vx = -ball.vx;
}
if(ball.y < ball.radius){
ball.y = ball.radius;
ball.vy = -ball.vy;
}else if(ball.y > cnv.height - ball.radius){
ball.y = cnv.height - ball.radius;
ball.vy = -ball.vy;
}
ball.draw(cxt, 'fill');
})
})();
},
}
}
</script>
对于多物体运动,一般情况下采用以下三个步骤进行处理:
1)定义一个数组来存放多个物体;
2)使用for循环生成单个物体,然后添加到数组中;
3)在动画循环中,使用forEach()方法遍历数组,从而处理单个物体。
上面示例效果:
2. 碰撞检测
在边界检测中,我们检测的是“物体与边界”之间是否发生碰撞;而在碰撞检测中,检测的则是“物体与物体”之间是否发生碰撞。
碰撞检测常用的两种方法:外接矩形判定法和外接圆判定法。
2.1 外接矩形判定法
外接矩形判定法,指的是如果检测物体是一个矩形或者近似矩形,就可以把这个物体抽象成一个矩形,然后用判断两个矩形是否碰撞的方法进行检测。
对于外接矩形判定法,一般需要两个步骤,即找出物体的外接矩形然后对外接矩形进行碰撞检测。
判断两个矩形是否发生碰撞,只需要判断两个矩形左上角的坐标所处的范围,如果两个矩形左上角的坐标满足一定条件,则两个矩形就发生了碰撞。
语法:
代码语言:javascript复制tools.checkRect = function(rectA, rectB){
return !(rectA.x rectA.width < rectB.x ||
rectB.x rectB.width < rectA.x ||
rectA.y rectA.height < rectB.y ||
rectB.y rectB.height < rectA.y
)
}
如果上面四个条件都不满足的话,checkRect()方法返回的值是true,表示两个矩形已经发生了碰撞。
示例:简易俄罗斯方块
代码语言:javascript复制//tools.js
//判断两个矩形是否发生碰撞
tools.checkRect = function(rectA, rectB){
return !(rectA.x rectA.width < rectB.x ||
rectB.x rectB.width < rectA.x ||
rectA.y rectA.height < rectB.y ||
rectB.y rectB.height < rectA.y)
}
代码语言:javascript复制//my-canvas.vue
//...
methods: {
//创建一个块
createBox(cnv){
let x = Math.random() * cnv.width;
let y = 0;
let width = Math.random() * 40 10;
let height = Math.random() * 40 10;
let color = tools.getRandomColor();
let box = new Box(x, y, width, height, color);
return box;
},
//俄罗斯方块
drawTetris(cxt, cnv){
let vy = 20;
let self = this;
let boxes = [];
let activeBox = this.createBox(cnv);
boxes.push(activeBox);
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
activeBox.y = vy;
if(activeBox.y > cnv.height - activeBox.height){
activeBox.y = cnv.height - activeBox.height;
activeBox = self.createBox(cnv);
boxes.push(activeBox);
}
let isDraw = true;
boxes.forEach(box => {
if(box.y <= 0){
isDraw = false;
}
if(activeBox !== box && tools.checkRect(activeBox, box)){
activeBox.y = box.y - activeBox.height;
activeBox = self.createBox(cnv);
boxes.push(activeBox);
}
if(isDraw){
box.draw(cxt, 'fill');
}
})
})()
},
}
2.2 外接圆判定法
外接圆判定法,指的是如果检测物体是一个圆或者近似圆,我们可以把这个物体抽象成一个圆,然后用判断两个圆是否碰撞的方法进行检测。
对于外接圆判定法,一般也需要两个步骤,即找出物体的外接圆然后对外接圆进行碰撞检测。
判断两个圆是否发生碰撞,只需要判断两个圆心之间的距离。如果两个圆心之间的距离大于或等于两个圆的半径之和,则两个圆没有发生碰撞;如果两个圆心之间的距离小于两个圆的半径之和,则两个圆发生了碰撞。
语法:
代码语言:javascript复制tools.checkCircle = function(circleB, circleA){
let dx = circleB.x - circleA.x;
let dy = circleB.y - circleA.y;
let distance = Math.sqrt(dx * dx dy * dy);
return distance < circleA.radius circleB.radius ? true : false;
}
示例:两个球碰撞
代码语言:javascript复制//tools.js
//判断两个圆形是否发生碰撞
tools.checkCircle = function(circleB, circleA){
let dx = circleB.x - circleA.x;
let dy = circleB.y - circleA.y;
let distance = Math.sqrt(dx * dx dy * dy);
return distance < circleA.radius circleB.radius ? true : false;
}
代码语言:javascript复制//my-canvas.vue
//...
methods: {
//两个小球碰撞检测
twoBallsCrash(cxt, cnv){
let ballA = new Ball(12, cnv.height / 2, 12, '#f69');
let ballB = new Ball(cnv.width - 12, cnv.height / 2, 12, '#6cf');
let vx = 6;
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
ballA.x = vx;
ballB.x = -vx;
if(tools.checkCircle(ballB, ballA) || ballA.x < ballA.radius || ballB.x > cnv.width - ballA.radius){
vx = -vx;
}
ballA.draw(cxt, 'fill');
ballB.draw(cxt, 'fill');
})()
},
}
外接矩形判定法和外接圆判定法都可能存在误差,但这两个方法可以减少大量的计算量,实现起来比较简单。对于两个物体的碰撞检测,哪种方式的误差小,就选哪个。
上面示例效果:
2.3 多物体碰撞
如果有n个物体,根据排列组合可以知道,此时共有n*(n-1)/2种碰撞情况。
语法:
代码语言:javascript复制balls.forEach((ballA, i) => {
for(let j = i 1; j < balls.length; j ){
let ballB = balls[j];
if(tools.checkCircle(ballA, ballB)){
//...
}
}
})
示例:多球碰撞
代码语言:javascript复制//my-canvas.vue
//...
methods: {
//多球碰撞
ballsCrash(cxt, cnv){
let self = this;
let n = 10;
let balls = [];
for(let i = 0; i < n; i ){
let ball = new Ball();
ball.x = Math.random() * cnv.width;
ball.y = Math.random() * cnv.height;
ball.radius = 10;
ball.color = tools.getRandomColor();
ball.vx = Math.random() * 6 - 3;
ball.vy = Math.random() * 6 - 3;
balls.push(ball);
}
(function frame(){
requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);
//碰撞检测
balls.forEach((ballA, i) => {
for(let j = i 1; j < balls.length; j ){
let ballB = balls[j];
if(tools.checkCircle(ballB, ballA)){
ballA.vx = -ballA.vx;
ballA.vy = -ballA.vy;
ballB.vx = -ballB.vx;
ballB.vy = -ballB.vy;
}
}
});
balls.forEach(ball => {
//边界检测
if(ball.x < ball.radius){
ball.x = ball.radius;
ball.vx = -ball.vx;
}else if(ball.x > cnv.width - ball.radius){
ball.x = cnv.width - ball.radius;
ball.vx = -ball.vx;
}
if(ball.y < ball.radius){
ball.y = ball.radius;
ball.vy = -ball.vy;
}else if(ball.y > cnv.height - ball.radius){
ball.y = cnv.height - ball.radius;
ball.vy = -ball.vy;
}
//绘制小球
ball.draw(cxt, 'fill');
ball.x = ball.vx;
ball.y = ball.vy;
});
})()
},
}
示例效果:
http://mpvideo.qpic.cn/0bf2kaaaoaaacuaajbonbzpfaugda5iaabya.f10003.mp4?dis_k=e2238467b0a8d4882cdaf9fb2bee3864&dis_t=1649318399&vid=wxv_1357306148571185153&format_id=10003&support_redirect=0&mmversion=false