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&nbsp;轴:<input id="circX" type="text" style="width: 50px;margin-bottom: 10px;" value="100"/></p>
                <p style="text-indent: 2em;">Y&nbsp;轴:<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&nbsp;轴:<input id="pickX" type="text" style="width: 50px;margin-bottom: 10px;" value="0"/></p>
                <p style="text-indent: 2em;">Y&nbsp;轴:<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;
        }
    }
}

效果图:

posted @ 2021-03-10 16:57  TenFly  阅读(178)  评论(0编辑  收藏  举报