我们在上一次讲到用数理观点来观察反射
行为的诸多细节,而这篇文则是要讲解斜向抛射
。
不过因为斜向抛射
的概念其实不是挺复杂,所以为了不要浪费篇幅,我也会先把一些程序的实作面先在这一篇做一个简单的引导~
斜向抛射
这个名词,我想应该对很多人来说都有很深的印象。(想当年刚成为理组小菜鸡的我在物理课上到这一段的时候真的有三观被刷新的感觉XD)
斜向抛射
顾名思义就是朝某个角度丢出一个球的意思,而丢出去的球会在过程中持续地受到重力的影响,导致它会有每秒向下1g
的加速度。
我们之前有介绍过向量
可以分裂成垂直向量
和水平向量
,而如果我们观察被斜向抛射
出去的球,他的速度其实也可以被拆成垂直
和水平
的部分。在水平
的部分,因为水平方向上没有受力(我们假设环境中没有风力或摩擦力),所以水平
方向的速度会维持不变,但是垂直
方向就不同了,因为垂直
方向会受到重力影响,而假设球一开始是往上丢的,那麽球在上升的过程中,上升的速度就会越变越慢,直到达到最高点,接着就会开始往下加速,最後在落地前一刻,他会达到跟刚被丢出去一样的垂直
速度(但是方向是相反的)。
若用数学来描述垂直
和水平
的速度向量,则可以这样表示:
// θ 是抛射出去的仰角,t是经过时间
horizontalVelocity = v*cos(θ) // 水平速度
horizontalVelocity = v*sin(θ) - gt // 垂直速度
好啦~ 斜向抛射
就是这麽简单,没什麽特别的。接下来我们会直接带一个简单的弹跳球范例来作为热身用~
这边我们先来看一个没有倾斜面的范例
其实这个例子在MDN上面也有类似的版本。
延伸阅读:MDN弹跳球
但是MDN
上的版本其实简化了很多东西,例如没有做碰撞的Reposition
,除此之外加速度的模拟也没有透过侦测经过时间
来给予速度加乘,而是变成每一帧加一点点速度。
MDN
之所以可以不用作Reposition
和透过侦测经过时间来给予速度加乘
,是因为他的速度设定的很慢,所以就算真的产生帧间误差
,也不会出现很诡异的状况。
通常如果物体运动的速度很快,但是又没有做Reposition
,反弹动画依据程序的写法差异会有两种异常现象:
而加速度的部分,他有给一个0.99
的浮点数
作为摩擦系数
,这让球不会在碰撞多次之後产生异常状况。
那麽这边我们就来提出自己的版本~
const DEFAULT = {
radius: 40,
color: 'red',
speedX: 1000,
speedY: 30,
accelerationX: 0,
accelerationY: 980,
frictionX: 1,
frictionY: 0.999,
}
class BasicRefelection {
constructor(ctx, config) {
this.ctx = ctx;
this.time = 0;
this.cvs = ctx.canvas;
this.config = config;
this.init();
}
init() {
this.initBall();
this.time = performance.now();
this.animateBall();
let $this = this;
// 绑定visibilitychange事件
window.addEventListener('visibilitychange', () => {
if (document.visibilityState !== "visible") {
$this.frameIsPaused = true;
}
else{
$this.frameIsPaused = false;
$this.time = performance.now();
}
});
}
initBall() {
let $this = this;
this.ball = {
color: $this.config.color,
radius: $this.config.radius,
location: {
x: $this.cvs.width / 2,
y: $this.cvs.height / 2,
},
speed: {
x: $this.config.speedX,
y: $this.config.speedY
},
acceleration: {
x: $this.config.accelerationX,
y: $this.config.accelerationY
},
friction: {
x: $this.config.frictionX,
y: $this.config.frictionY
}
}
}
drawBall() {
this.drawCircle(this.ball.location.x, this.ball.location.y, this.ball.radius * 2, this.ball.color);
}
animateBall() {
let $this = this;
// 当画面没有被暂停(页签停在这页)
if(!$this.frameIsPaused){
$this.ctx.clearRect(0, 0, $this.cvs.width, $this.cvs.height);
// 画球
$this.drawBall();
// 更新位置
$this.refreshLocation();
// 更新速度
$this.refreshSpeed();
// 检查碰撞行为,确定是否倒转向量
$this.checkBoundary();
// 更新纪录时间
$this.time = performance.now();
// 用RAF递回$this.animateBall()
requestAnimationFrame($this.animateBall.bind($this));
}
// 当画面被暂停,就单纯的递回$this.animateBall()
else{
$this.animateBall();
}
}
refreshSpeed() {
let dt = (performance.now() - this.time) / 1000;
this.ball.speed.x = this.ball.speed.x * this.ball.friction.x + this.ball.acceleration.x * dt;
this.ball.speed.y = this.ball.speed.y * this.ball.friction.y + this.ball.acceleration.y * dt;
}
refreshLocation() {
let dt = (performance.now() - this.time) / 1000;
this.ball.location.x += this.ball.speed.x * dt;
this.ball.location.y += this.ball.speed.y * dt;
}
checkBoundary() {
let ball = this.ball;
let canvas = this.cvs;
// 当球正在底端
if (ball.location.y + ball.radius > canvas.height) {
// 且速度为正值(朝下)
if (ball.speed.y > 0) {
ball.speed.y = -ball.speed.y;
}
}
// 当球正在顶端
else if (ball.location.y - ball.radius < 0) {
// 且速度为负值(朝上)
if (ball.speed.y < 0) {
ball.speed.y = -ball.speed.y;
}
}
// 当球正在右端
if (ball.location.x + ball.radius > canvas.width) {
if (ball.speed.x > 0) {
ball.speed.x = -ball.speed.x;
}
}
// 当球正在左端
else if (ball.location.x - ball.radius < 0) {
if (ball.speed.x < 0) {
ball.speed.x = -ball.speed.x;
}
}
}
drawCircle(x, y, width, color, alpha) {
let ctx = this.ctx;
ctx.save()
ctx.fillStyle = color;
ctx.globalAlpha = alpha;
ctx.beginPath();
ctx.arc(x, y, width / 2, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
(()=>{
let ctx = document.querySelector('canvas').getContext('2d');
let instance = new BasicRefelection(ctx,DEFAULT)
})()
codepen连结:https://codepen.io/mizok_contest/pen/ZEymVZQ?editors=1010
这边说明一下程序的流程面:
class
在创建实例之後会进入入口方法init
init
会先创建球
物件的refernece
,物件内会根据输入的参数预设一个初始速度向量,和加速度与颜色/球的大小之类的数值, 而球的初始位置定在Canvas正中央。animateBall
开始进行动画球
物件中纪录的当前位置
、大小
、颜色
等画出球
球
物件中纪录的位置,更新的方法为球
的旧位置
+速度
*帧间时差(dt)
球
物件中纪录的瞬时速度,更新的方法为球
的旧速度
+加速度
*帧间时差(dt)
碰撞
到墙壁,若确定会碰撞
,则逆转
对应的速度分量,这边我们调整了优化了碰撞反弹的判断,在判断反弹时补上一个防呆判定,让球不至於会卡进去墙壁里面出不来,这边值得一提的是,由於我们在前面有讲过,canvas
专案适合使用requestAniamtionFrame(简称RAF)来做动画帧渲染的looping。
但是实际上RAF本身有个特点,就是他只会在页面"visible"时触发。
在这种情况下,因为我们在速度计算上是采用帧间时差
制来计算(而不是By Frame),如果我们让页面进入hidden
状态(例如切换页签
/缩小视窗)然後再切换回visible
状态,球的速度就会因为hidden
的时候都没有去刷新this.time
(用来记录当下时间的property)而产生暴走
的现象。
这边就可以利用window的预设事件visibilitychange
来阻止暴走
发生,藉由侦测document.visibilityState
来判断是否在切换回visible
时刷新this.time
。
以上就是弹跳球
在没有倾斜面
的动画程序范例~
下一篇文我们将会介绍有倾斜面
的状况下,程序的写法,敬请期待 :D
>>: # Day 18 Physical Memory Model (三)
提完了那麽多有关 APCS 的事,这次想要分析考 APCS 能够有怎样的好处。 权威性: APCS ...
我的目的 学习图像辨识,顺便拯救专题,再顺便参加铁人赛,一鱼三吃,真香。 图像辨识的原理 简单说就是...
Azure Defender提供目的导向的使用者介面,可管理和调查 Microsoft 365 服务...
这次的问题是上一篇文的延伸 上一篇是使用者输入参数後,再将指定栏位的值改为1 那这次是输入参数後检查...
昨天结束在Facebook登入之後,今天就接续昨天的内容,以木棉花的粉丝专页为例,来讲怎麽爬下来贴...