js基于“合成大西瓜的”碰撞模型(一)
在玩过“合成大西瓜”后,对其中的碰撞原理产生了兴趣,想着探究一下其中的原理,首先探究一下平面碰撞原理
事先分析:物理里面的力学知识,数学计算坐标系位移。
页面:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black" /> <script src="js/jquery.min.js"></script> <title></title> <style> html,body{ height: 100%; } * { box-sizing: border-box; padding: 0; margin: 0; } main { width: 100vw; height: 100vh; } </style> </head> <body> <div id="showBox" style="position: absolute;top: 0;left: -150px;z-index: 999; width: 200px;height: 100%;"> <div id="leftTree" style="width: 150px;height: 100%;padding:10px;float: left;"> <p>屏幕宽度:<label id="screenWidth"></label></p> <p>屏幕高度:<label id="screenHeight"></label></p> <p>圆心位置:</p> <p style="text-indent: 2em;">X 轴:<input id="circX" type="text" style="width: 50px;margin-bottom: 10px;" value="100"/></p> <p style="text-indent: 2em;">Y 轴:<input id="circY" type="text" style="width: 50px;" value="100"/></p> <p>小求半径:</p> <p style="text-indent: 2em;">半径:<input id="circR" type="text" style="width: 50px;" value="60"/></p> <p>击打点:</p> <p style="text-indent: 2em;">X 轴:<input id="pickX" type="text" style="width: 50px;margin-bottom: 10px;" value="0"/></p> <p style="text-indent: 2em;">Y 轴:<input id="pickY" type="text" style="width: 50px;" value="0"/></p> <p>击打力:</p> <p style="text-indent: 2em;">动能:<input id="mf" type="text" style="width: 50px;" value="1000"/></p> <p>小求质量:</p> <p style="text-indent: 2em;">质量:<input id="mg" type="text" style="width: 50px;" value="10"/></p> <p>滚动摩擦系数:</p> <p style="text-indent: 2em;">系数:<input id="mk" type="text" style="width: 50px;" value="0.3"/></p> <p>撞墙衰减系数:</p> <p style="text-indent: 2em;">系数:<input id="me" type="text" style="width: 50px;" value="0.3"/></p> <button type="button" style="width: 100%;height: 30px;margin-top: 10px;" onclick="start(this)">开始</button> </div> <button type="button" style="width: 40px;height: 40px;margin-top: 10px; float: right;border-radius: 50%;opacity: 0.5;" onclick="showBox()">***</button> </div> <main> <canvas id="gameboard"></canvas> </main> </body> </html> <script src="js/module.js"></script>
module.js:
const canvas = document.getElementById("gameboard");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let width = canvas.width;
let height = canvas.height;
//Web坐标系是以左上角为原点,向右为x轴正方向,向下为y轴正方向
//为了符合数学习惯,将其转换为笛卡尔坐标系
ctx.save();
ctx.translate(0,height);
ctx.rotate(Math.PI);
ctx.scale(-1,1);
//绘制初始圆
ctx.fillStyle = "hsl(170, 100%, 50%)";
ctx.beginPath();
ctx.arc($("#circX").val(), $("#circY").val(), $("#circR").val(),0, 2 * Math.PI);
ctx.fill();
//滚动摩擦系数
let mk=0;
//动能衰减系数
let me=0;
//剩余动能系数
let loseProp=0;
//屏幕长宽
const screenWidth=$(window).width(),screenHeight=$(window).height();
$("#screenWidth").text(screenWidth);$("#screenHeight").text(screenHeight);
//显示/隐藏信息栏
let clickFlag=false;
function showBox(){
if(clickFlag){
$("#showBox").animate({left:'-150px'});
clickFlag=false;
}
else{
$("#showBox").animate({left:'0'});
clickFlag=true;
}
}
//执行
function start(obj){
if($(obj).text()=="开始"){
mk=parseFloat($("#mk").val());
me=parseFloat($("#me").val());
loseProp=1-me;
const game = new Circle();
$(obj).text("停止");
}
else{
window.location.reload();
}
}
//球类
class Circle{
/**
* @param {Object} start 是否初始化
* @param {Object} mf 附加初始动能
* @param {Object} mg 小球重量
* @param {Object} x 圆心x轴位置
* @param {Object} y 圆心y轴位置
* @param {Object} r 圆半径
* @param {Object} vx 往x轴移动距离
* @param {Object} vy 往y轴移动距离
* @param {Object} vh 斜边移动距离
* @param {Object} pickX 击打点x轴坐标
* @param {Object} pickY 击打点y轴坐标
* @param {Object} dirX 往x轴正方向移动
* @param {Object} dirY 往y轴正方向移动
* @param {Object} lastTime 上一帧耗时
*/
constructor() {
this.start=true;
this.mf = parseFloat($("#mf").val());
this.mg = parseFloat($("#mg").val());
this.x = parseFloat($("#circX").val());
this.y = parseFloat($("#circY").val());
this.r = parseFloat($("#circR").val());
this.vx = 0;
this.vy = 0;
this.vh = 0;
this.pickX = parseFloat($("#pickX").val());
this.pickY = parseFloat($("#pickY").val());
this.dirX = null;
this.dirY = null;
this.lastTime = null;
this.init();
}
// 初始化画布
init() {
this.circles = [
this.draw(this.x, this.y, this.r, 0, 2 * Math.PI)
];
window.requestAnimationFrame(this.Sport.bind(this));
}
draw(x,y,r,sAngle,eAngle){
ctx.fillStyle = "hsl(170, 100%, 50%)";
ctx.beginPath();
ctx.arc(x, y, r,sAngle,eAngle);
ctx.fill();
}
print(obj){
let str="";
$.each(obj,function(key,value){
str+=key+":"+value+"。";
})
console.log(str);
}
//参考Web Api https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
//window.requestAnimationFrame() 会把当前时间的毫秒数(即时间戳:00.000)传递给回调函数
Sport(timestamp){
let flag=true;
//计算上一帧耗时秒数
if (!this.lastTime) {
this.lastTime = 0;
flag=false;
}
let timeDiff=(timestamp-this.lastTime)/1000;
this.lastTime=timestamp;
//用为这里是调用方法执行,而非加载执行,所以第一帧时间会造成累计,排除掉第一帧
if(flag) this.locationReset(timeDiff)
window.requestAnimationFrame(this.Sport.bind(this));
}
locationReset(t){
let obj={"合力":this.mf};
this.print(obj);
var s=this.mf*t**2/(this.mg+this.mg*mk);
if(this.mf>this.mg+this.mg*mk){
this.calcAngle(s*1400,this.r);
this.mf-= this.mg*mk*s*10;
}
}
calcAngle(s,r){
if(this.start){
//true往x轴正方向移动,false往x轴负方向移动
this.dirX=this.x-this.pickX>=0;
//true往y轴正方向移动,false往y轴负方向移动
this.dirY=this.y-this.pickY>=0
//发力点到球心的x轴距离
let calcX=Math.abs(this.x-this.pickX);
//发力点到球心的y轴距离
let calcY=Math.abs(this.y-this.pickY);
//发力点到球心的斜边距离
let calcH=Math.sqrt(calcX**2+calcY**2);
let prop=s/calcH;
let moveX=prop*calcX;
let moveY=prop*calcY;
let moveH=prop*calcH;
this.vx=this.dirX?moveX:-moveX;
this.vy=this.dirY?moveY:-moveY;
this.vh=moveH;
this.start=false;
}
else{
//this.vh/s=this.vx/x=this.vy/y
this.vx=s*this.vx/this.vh;
this.vy=s*this.vy/this.vh;
this.vh=s;
}
this.x=this.dirX?this.x+this.vx:this.x-this.vx;
this.y=this.dirY?this.y+this.vy:this.y-this.vy;
this.reboundPath(this.dirX,this.dirY,Math.abs(this.x/this.y));
}
reboundPath(dirX,dirY,prop){
let thisScreenWidth=screenWidth-this.r;
let thisScreenHeight=screenHeight-this.r;
//圆心位置超出x轴正方向,必然发生碰撞
if(this.x>thisScreenWidth){
//debugger;
let moreThanX=this.x-thisScreenWidth;
let moreThanY=this.y>this.r?this.y-thisScreenHeight:this.r-this.y;
//先碰右墙
if(moreThanX/moreThanY>prop||this.y<thisScreenHeight){
this.x=thisScreenWidth-moreThanX*loseProp;
this.dirX=!this.dirX;
this.mf*=loseProp;
}
//先碰上下墙
else if(moreThanX/moreThanY<prop){
this.y=this.dirY?thisScreenHeight-moreThanY*loseProp:this.r+moreThanY*loseProp;
this.dirY=!this.dirY;
this.mf*=loseProp;
}
//碰右上、右下角
else{
this.x=thisScreenWidth-moreThanX*loseProp;
this.dirX=!this.dirX;
this.y=this.dirY?thisScreenHeight-moreThanY*loseProp:this.r+moreThanY*loseProp;
this.dirY=!this.dirY;
this.mf*=(1-me);
}
}
//圆心位置超出x轴负方向,必然发生碰撞
else if(this.x<this.r){
let moreThanX=this.r-this.x;
let moreThanY=this.y>this.r?this.y-thisScreenHeight:this.r-this.y;
//先碰左墙
if(moreThanX/moreThanY>prop||this.y<thisScreenHeight){
this.x=this.r+moreThanX*loseProp;
this.dirX=!this.dirX;
this.mf*=loseProp;
}
//先碰上下墙
else if(moreThanX/moreThanY<prop){
this.y=this.dirY?thisScreenHeight-moreThanY*loseProp:this.r+(this.r+this.y)*loseProp;
this.dirY=!this.dirY;
this.mf*=loseProp;
}
//碰左上、左下角
else{
this.x=this.r+moreThanX*loseProp;
this.dirX=!this.dirX;
this.y=this.dirY?thisScreenHeight-moreThanY*loseProp:this.r+(this.r+this.y)*loseProp;
this.dirY=!this.dirY;
this.mf*=loseProp;
}
}
else if(this.y>=thisScreenHeight){
let moreThanY=this.y-thisScreenHeight;
this.y=thisScreenHeight-moreThanY*loseProp;
this.dirY=!this.dirY;
this.mf*=loseProp;
}
else if(this.y<=this.r){
let moreThanY=this.r-this.y;
this.y=this.r+moreThanY*loseProp;
this.dirY=!this.dirY;
this.mf*=loseProp;
}
if(this.x>thisScreenWidth||this.x<this.r||this.y>thisScreenHeight||this.y<this.r){
return this.reboundPath(dirX,dirY,prop)
}
else{
ctx.clearRect(0,0,screenWidth,screenHeight);
this.draw(this.x, this.y, this.r, 0, 2 * Math.PI)
return;
}
}
}
效果图:


浙公网安备 33010602011771号