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; } } }
效果图: