Canvas系列(16):实战-小球与斜面碰撞
上一章我们讲了小球的拖拽,《小球三部曲》还差一部,今天它来了!本章研究的是小球与斜面碰撞过程。小球与平面或者垂直的面碰撞我们早就会了,在上一章中,有一个函数checkWalls
就是检测边界并且处理碰撞,这里的边界就是水平或者垂直的面。现实生活中,大多数情况下,小球碰撞到的并不是平面或者垂直的面,而是斜面,本章就来讨论小球在斜面上运动的过程。
画一个斜面
我们这里简单的画一条线,代表着斜面,Canvas画线很简单只要使用moveTo
和lineTo
方法就可以了。当然为了代码的可维护性,我们有必要把线封装成一个类,本章的代码是在上一章的代码的基础上添加斜面的操作处理的,画线操作如下:
class Line {
constructor(context, options = {}) {
this.context = context;
this.x1 = options.x1 || 0;
this.y1 = options.y1 || 0;
this.x2 = options.x2 || 0;
this.y2 = options.y2 || 0;
this.lineWidth = options.lineWidth || 1;
this.color = options.color || '#000';
this.rotation = Math.atan2(this.y2 - this.y1, this.x2 - this.x1);
}
draw() {
this.context.save();
this.context.lineWidth = this.lineWidth;
this.context.strokeStyle = this.color;
this.context.beginPath();
this.context.moveTo(this.x1, this.y1);
this.context.lineTo(this.x2, this.y2);
this.context.closePath();
this.context.stroke();
this.context.restore();
}
}
let line = new Line(context,{
x1: 50, y1: 200,
x2: 300, y2: 260,
});
// ... 其他代码相同
function animate (){
// ... 其他代码相同
// 绘制
line.draw();
balls.forEach(draw);
}
上述代码内置了一个rotation
字段,用来表示线段与起点所在的x轴的夹角,这个角度后面将会有大用。
小球肯定会穿过斜面,此时的效果(没错就是张静态图片):
与斜面碰撞的理论基础
之前我们做过小球与小球碰撞,小球碰撞时我们用了非常厉害的一招就是旋转坐标系,把正常的坐标系,转化斜着的坐标系然后来处理,最后再把处理后的坐标系旋转回去。这里也一样,由于水平面的碰撞,我们早就会了,所以我们可以把斜面的碰撞转换为水平面的碰撞。
小球与斜面碰撞,初始时候如下图,其中速度可以分解为水平的vx和垂直的vy(图中蓝色部分)。
为了方便我们对坐标系进行旋转,转化为水平的位置,此时重新计算新的坐标系的x轴的分速度和y轴的分速度(图中黄色部分),当然还得计算小球在新坐标系中的位置。我们这里把旋转中心设置为斜面最左边的点。
对旋转后的速度做碰撞处理,并求出新的速度。
最后把坐标系旋转回去即可。
小球与斜面碰撞的代码实现
在写代码之初我们修改一下上次代码中的checkWalls
方法,把反弹损耗的速度比例用一个变量bounce
来定义,这样触碰斜面的时候损耗的速度也用这个变量来计算,如下:
let bounce = -0.95;
function checkWalls(ball){
// 边界反弹å
if (ball.x < ball.radius) {
ball.x = ball.radius;
ball.vx *= bounce;
} else if (ball.x > canvas.width - ball.radius) {
ball.x = canvas.width - ball.radius;
ball.vx *= bounce;
}
if (ball.y < ball.radius) {
ball.y = ball.radius;
ball.vy *= bounce;
} else if (ball.y > canvas.height - ball.radius) {
ball.y = canvas.height - ball.radius;
ball.vy *= bounce; // 假设能量损耗是0.05
ball.vx *= 0.99; // 摩擦力
}
}
因为我们要涉及到坐标的旋转,还记得之前小球碰撞时坐标旋转时封装的方法吗?这里选择坐标也得用到这个方法,此外由于sin
和cos
我们计算时用的多,所以也用一个变量声明一下:
function rotate (x, y, sin, cos, reverse) {
return {
x: (reverse) ? (x * cos + y * sin) : (x * cos - y * sin),
y: (reverse) ? (y * cos - x * sin) : (y * cos + x * sin)
};
}
let cos = Math.cos(line.rotation);
let sin = Math.sin(line.rotation);
接下来就是前方高能时刻——处理斜面碰撞的过程,代码如下:
function animate (){
requestAnimationFrame(animate);
context.clearRect(0, 0, canvas.width, canvas.height);
balls.forEach(ball=>{
if (selectedBall === ball) {
trackVelocity();
} else {
// 更新小球的速度
ball.update();
// 位置以(line.x1,line.y1)为坐标原点来旋转坐标
let pos = rotate(ball.x - line.x1, ball.y - line.y1, sin, cos, true);
let vel = rotate(ball.vx, ball.vy, sin, cos, true);
// 线的y坐标如果小于小球的半径 说明碰撞上了 由于小球在斜线上面所以pos.y是负数 需要加个符号变为正数在比较
if (-pos.y < ball.radius) {
// 反弹处理
vel.y *= bounce;
pos.y = -ball.radius;
// 选择回去
let velF = rotate(vel.x, vel.y, sin, cos, false);
let posF = rotate(pos.x, pos.y, sin, cos, false);
ball.vx = velF.x;
ball.vy = velF.y;
ball.x = line.x1 + posF.x;
ball.y = line.y1 + posF.y;
}
// 检测是否碰撞到边界
checkWalls(ball);
}
});
// 绘制
line.draw();
balls.forEach(draw);
}
代码和注释,相信你能看得懂,这里需要注意的是小球位置的旋转中心是斜面的最左边,所以位置坐标需要减去左边的坐标,此时的效果如下:
由上我们发现我们的代码还是有问题的,目前斜面是无限长的。
只在斜面区域内处理斜面碰撞
如图,只有当小球在粉色区域内才需要判断小球与斜面是否相交,其他情况下都不需要去判断。
为了方便操作,我们有必要给Line
这个类添加一个获取边界的方法getBounds
,如下:
class Line {
constructor(context, options = {}) {
this.context = context;
this.x1 = options.x1 || 0;
this.y1 = options.y1 || 0;
this.x2 = options.x2 || 0;
this.y2 = options.y2 || 0;
this.lineWidth = options.lineWidth || 1;
this.color = options.color || '#000';
this.rotation = Math.atan2(this.y2 - this.y1, this.x2 - this.x1);
}
draw() {
this.context.save();
this.context.lineWidth = this.lineWidth;
this.context.strokeStyle = this.color;
this.context.beginPath();
this.context.moveTo(this.x1, this.y1);
this.context.lineTo(this.x2, this.y2);
this.context.closePath();
this.context.stroke();
this.context.restore();
}
getBounds() {
let minX = Math.min(this.x1, this.x2);
let minY = Math.min(this.y1, this.y2);
let maxX = Math.max(this.x1, this.x2);
let maxY = Math.max(this.y1, this.y2);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
}
};
}
接下来就很简单了,判断一下是否在粉色的区域就可以了,为了代码更加有条理性,我们把小球与斜面碰撞的过程抽取成一个方法checkLine
,如下:
let bounds = line.getBounds();
function checkLine(ball){
// 判断小球是否在粉色区域内
if (ball.x + ball.radius > bounds.x && ball.x - ball.radius < bounds.x + bounds.width){
// 位置以(line.x1,line.y1)为坐标原点来旋转坐标
let pos = rotate(ball.x - line.x1, ball.y - line.y1, sin, cos, true);
let vel = rotate(ball.vx, ball.vy, sin, cos, true);
// 线的y坐标如果小于小球的半径 说明碰撞上了 由于小球在斜线上面所以pos.y是负数 需要加个符号变为正数在比较
if (-pos.y < ball.radius) {
// 反弹处理
vel.y *= bounce;
pos.y = -ball.radius;
// 选择回去
let velF = rotate(vel.x, vel.y, sin, cos, false);
let posF = rotate(pos.x, pos.y, sin, cos, false);
ball.vx = velF.x;
ball.vy = velF.y;
ball.x = line.x1 + posF.x;
ball.y = line.y1 + posF.y;
}
}
}
function animate (){
requestAnimationFrame(animate);
context.clearRect(0, 0, canvas.width, canvas.height);
balls.forEach(ball=>{
if (selectedBall === ball) {
trackVelocity();
} else {
// 更新小球的速度
ball.update();
// 检测是否碰撞到斜面
checkLine(ball);
// 检测是否碰撞到边界
checkWalls(ball);
}
});
// 绘制
line.draw();
balls.forEach(draw);
}
现在我们发现,小球确实是在粉色区域内去弹起,但是如果小球走了斜面的下面,那么小球也会立即弹起,所以我们需要处理一下这个问题。
小球在斜面下的处理
小球在斜面下面的时候也可能会碰撞到斜面,此时也需要反弹,由于我们已经旋转过了,直接添加逻辑就可以了,现在修改checkLine
方法,如下:
function checkLine(ball){
// 判断小球是否在粉色区域内
if (ball.x + ball.radius > bounds.x && ball.x - ball.radius < bounds.x + bounds.width){
// 位置以(line.x1,line.y1)为坐标原点来旋转坐标
let pos = rotate(ball.x - line.x1, ball.y - line.y1, sin, cos, true);
let vel = rotate(ball.vx, ball.vy, sin, cos, true);
// 当小球中心距离斜面的距离小于半径的时说明已经相碰撞了
if (Math.abs(pos.y) < ball.radius) {
// 判断小球是上面碰撞还是下面碰撞
if (pos.y - vel.y <= 0) {
pos.y = -ball.radius;
} else {
pos.y = ball.radius;
}
vel.y *= bounce;
// 选择回去
let velF = rotate(vel.x, vel.y, sin, cos, false);
let posF = rotate(pos.x, pos.y, sin, cos, false);
ball.vx = velF.x;
ball.vy = velF.y;
ball.x = line.x1 + posF.x;
ball.y = line.y1 + posF.y;
}
}
}
判断小球是上面碰撞还是下面碰撞的时候我们用到了pos.y - vel.y <= 0
来判断是上面,一般情况下只要判断pos.y <= 0
就可以说明小球在斜面的上面,毕竟旋转后的y坐标小于斜面的y坐标基本上可以说是在上面了。但是因为本次绘制的时候我们拿到的位置是已经加上y坐标上的速度了,当前帧的位置可能会让代码出现bug,就比如小球是从上往下撞到斜面(此时已经按平面处理了)的,由于本次加了一个速度,就有一定的可能让pos.y
大于0,也就是小球加了个速度可能会使位置到了斜面的下方;为了规避这种情况,我们使用没有加y轴上速度时候的y轴的值,也就是上一帧y轴的距离,即pos.y - vel.y
来判断。
我们的斜面碰撞终于写完了,当然现在先别高兴的太早了,上一章拖拽的时候小球被甩出时很可能会去一个很大的速度,这样就会有“穿墙”的可能性,为了避免这种问题的发生我们让甩出去的合速度最大为半径的大小,修改方法trackVelocity
,如下:
function trackVelocity () {
selectedBall.vx = selectedBall.x - oldX;
selectedBall.vy = selectedBall.y - oldY;
let v = Math.hypot(selectedBall.vx, selectedBall.vy);
if (v > selectedBall.radius) {
let rate = selectedBall.radius / v;
selectedBall.vx *= rate;
selectedBall.vy *= rate;
}
oldX = selectedBall.x;
oldY = selectedBall.y;
}
大功告成,完整代码请点这里。
PS:我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1sfdnjsw8b22
- 数据场景分析 线上线下商家到底谁能干过谁?
- 张钦坤:云计算、开放平台与服务商版权责任
- biztalk 2010 dev版安装小记
- 微信小程序如何获取组件实际高度
- flex4/flash builder中动态加载Module并与之交互的正确方式
- Google发布会看图的人工智能,让它来评评你的照片拍得好不好
- Git日常操作命令梳理
- Git忽略规则.gitignore梳理
- 深入浅出事件流处理NEsper(二)
- 微信推出“微信使用小助手”,中老年人也能轻松玩转微信
- Flex4中使用WCF
- storm如何分配任务和负载均衡?
- Flex4中的ModuleLoader,Alert以及TitleWindow
- Flex4中使用HDividedBox,VDividedBox
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 市面上数据库种类那么多,如何选择?
- 玩转正则!推荐一个速查、调试、验证、可视化工具
- 当一个http请求来临时,SpringMVC究竟偷偷帮你做了什么?
- Js实现文本复制
- 当一个http请求来临时,SpringMVC究竟偷偷帮你做了什么?处理器映射器与处理器篇
- anetTcpGenericConnect 详解
- 详解 MySQL 基准测试和sysbench工具
- 第六天:网络处理(anet部分)-- redis源码慢慢学,慢慢看【redis6.0.6】
- python爬王者荣耀壁纸
- 搞定三大神器之 Python 装饰器
- 当一个http请求来临时,SpringMVC究竟偷偷帮你做了什么?请求映射器篇
- rabbitpy使用purge不生效
- Springboot读取自定义属性之集合(list,数组)
- 被遗忘的 10 个Linux命令,很实用!
- Nginx配置中一个不起眼字符"/"的巨大作用,失之毫厘谬以千里