在学习 JavaScript
的时候,看到一个练习事例,就想到能不能做成类似 Windows
屏幕保护气泡那种效果,经过不断思考尝试,最后做出的效果如下图:

思路
其实上面的那种效果就是模拟理想物理环境下的多个小球非对心碰撞(对心碰撞是其特殊情况),所谓理想物理情况就是没有外力作用的封闭系统,内部遵循动量守恒定律和能量守恒定律。假设小球 A 和小球 B 的质量分别为 ����
和 ����
,初始速度分别为 ����
和 ����
,碰撞后的速度分别为 ����'
和 ����'
,两个小球的碰撞瞬间的状态如下图:

其中 ������
和 ������
是两小球沿球心连线方向上的分速度, ������
和 ������
是两小球垂直球心连线方向上的分速度。碰撞后,由于两小球在垂直球心连线方向上没有力的相互作用,所以速度不变,还是 ������
和 ������
,沿球心连线方向上的分速度为 ������'
和 ������'
。运用以下物理公式:
能量守恒定律:
����•����²/2+����•����²/2=����•����'²/2+����•����'²/2
向量运算:
����²= ������²+������²
����²= ������²+������²
����'²= ������'²+������²
����'²= ������'²+������²
推导得出:
����•������²/2+����•������²/2=����•������'²/2+����•������'²/2
再联合动量守恒定律:
����•������+����•������=����•������'+����•������'
推导得出:
������'=(������•(����-����)+2•����•������)/(����+����)
������'=(������•(����-����)+2•����•������)/(����+����)
最后合成碰撞后的速度 ����'
和 ����'
就ok了!
代码实现
// 小球对象构造函数 function Ball(x, y, speedX, speedY, color, radius, density) { this.x = x; this.y = y; this.speedX = speedX; this.speedY = speedY; this.color = color; this.radius = radius; this.density = density; } // 绘制 Ball.prototype.draw = function () { ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); ctx.fill(); } // 边界碰撞检测 Ball.prototype.borderCollisionDetect = function () { if ((this.x + this.radius) >= width && this.speedX > 0) { this.speedX *= -1; } if ((this.x - this.radius) <= 0 && this.speedX < 0) { this.speedX *= -1; } if ((this.y + this.radius) >= height && this.speedY > 0) { this.speedY *= -1; } if ((this.y - this.radius) <= 0 && this.speedY < 0) { this.speedY *= -1; } }
// 创建小球对象 function createBalls() { require(['utils'], function (utils) { while (balls.length < 8) { var ball = new Ball( utils.random(0, width), // x utils.random(0, height), // y utils.random(1, 8), // speedX utils.random(1, 8), // speedY 'rgb('+utils.random(0, 255) +','+ utils.random(0, 255)+','+ utils.random(0, 255) +')', 30, // radius 1 // density ); balls.push(ball); } }); }
// 更新小球速度和位置 function update() { for (let i = 0; i < balls.length; i++) { balls[i].borderCollisionDetect(); for (let j = i + 1; j < balls.length; j++) { if (ballsCollisionDetect(balls[i], balls[j])) { collide(balls[i], balls[j]); } } // 更新位置 balls[i].x += balls[i].speedX; balls[i].y += balls[i].speedY; } }
// 碰撞检测 function ballsCollisionDetect(ball1, ball2) { // 当前距离 var dx = ball1.x - ball2.x; var dy = ball1.y - ball2.y; var distance = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); // 预测下一时刻会不会碰撞 let dx_next = ball1.x + ball1.speedX - ball2.x - ball2.speedX; let dy_next = ball1.y + ball1.speedY - ball2.y - ball2.speedY; let distance_next = Math.sqrt(Math.pow(dx_next, 2) + Math.pow(dy_next, 2)); if (distance_next < ball1.radius + ball2.radius && distance_next < distance) { return true; } return false; }
// 更新碰撞后的状态 function collide(ball1, ball2) { require(['Vector2d'], function (Vector2d) { // 初始速度向量 let speed_ball1_initial = new Vector2d(ball1.speedX, ball1.speedY); let speed_ball2_initial = new Vector2d(ball2.speedX, ball2.speedY); // 球心方向单位向量 let s = new Vector2d(ball2.x - ball1.x, ball2.y - ball1.y); s = s.normalize(); // 垂直球心方向单位向量 let t = s.rotate(Math.PI / 2); // 速度在球心向量上的分速度投影 let speed_ball1_initial_sc = speed_ball1_initial.dotProduct(s)/s.length(); let speed_ball2_initial_sc = speed_ball2_initial.dotProduct(s)/s.length(); // 速度在垂直球心向量上的分速度投影 let speed_ball1_initial_tc = speed_ball1_initial.dotProduct(t)/t.length(); let speed_ball2_initial_tc = speed_ball2_initial.dotProduct(t)/t.length(); // 碰撞后球心方向上的分速度 let speed_ball1_final_sc = (speed_ball1_initial_sc * (ball1.density * Math.pow(ball1.radius,3) - ball2.density * Math.pow(ball2.radius,3)) + 2 * (ball2.density * Math.pow(ball2.radius,3)) * speed_ball2_initial_sc) / (ball1.density * Math.pow(ball1.radius,3) + ball2.density * Math.pow(ball2.radius,3)); let speed_ball2_final_sc = (speed_ball2_initial_sc * (ball2.density * Math.pow(ball2.radius,3) - ball1.density * Math.pow(ball1.radius,3)) + 2 * (ball1.density * Math.pow(ball1.radius,3)) * speed_ball1_initial_sc) / (ball1.density * Math.pow(ball1.radius,3) + ball2.density * Math.pow(ball2.radius,3)); // 碰撞后球心方向上的分速度向量 let speed_ball1_final_s = s.scale(speed_ball1_final_sc); let speed_ball2_final_s = s.scale(speed_ball2_final_sc); // 碰撞后垂直球心方向上的分速度向量 let speed_ball1_final_t = t.scale(speed_ball1_initial_tc); let speed_ball2_final_t = t.scale(speed_ball2_initial_tc); // 结束速度向量 let speed_ball1_final = speed_ball1_final_s.add(speed_ball1_final_t); let speed_ball2_final = speed_ball2_final_s.add(speed_ball2_final_t); // 更新速度 ball1.speedX = speed_ball1_final.x; ball1.speedY = speed_ball1_final.y; ball2.speedX = speed_ball2_final.x; ball2.speedY = speed_ball2_final.y; }); }
缺陷
本代码是通过 window.requestAnimationFrame()
方法循环执行来实现动画效果的,它的回调次数是每秒60次,所以对于一些速度"过快"的小球,会在撞击边界时出现"撞出去一小部分"的情况。还有本代码只考虑了两个小球相撞的情况,没有考虑三个以上小球同时相撞的场景。
结语
本代码是学习 JavaScript
时的实战演练,能加深对这门语言的理解和掌握。完整代码详见 GitHub地址 。
参考链接
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。