小球彈跳

html

<body>
    <head>
        <link rel="stylesheet" type="text/css" href="./index.css">  
    </head>
    <body>
        <main>
            <canvas id="gameboard"></canvas>
        </main>

        <script type="text/javascript" src="./index.js"></script>
    </body>
</body>

css

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  font-family: sans-serif;
}
main {
  width: 100vw;
  height: 100vh;
  background: hsl(0deg, 0%, 10%);
}

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;
// 因為重力加速度的單位是 m/s/s缚去,而畫布以像素為單位,所以重力加速度應(yīng)保持一定的比例,
// 這里簡單的使用了 100 倍的重力加速度截驮。
const gravity = 980; // 重力加速度常量


// 小球的類
class Circle {
    /**
     * @param context 對象
     * @param x 坐標(biāo)
     * @param y 坐標(biāo)
     * @param r 半徑
     * @param vx x速度
     * @param vy y速度
     * @param mass 質(zhì)量
     * @param cor 恢復(fù)系數(shù)
     *
     */
    constructor(context, x, y, r, vx, vy, mass = 1, cor = 1) {
        this.context = context;
        this.colliding = false;

        this.x = x;
        this.y = y;
        this.r = r;
        this.vx = vx;
        this.vy = vy;
        this.mass = mass;
        this.cor = cor;
    }

    // 繪制小球
    draw() {
        this.context.fillStyle = this.colliding ? "hsl(300, 100%, 70%)" : "hsl(170, 100%, 50%)";
        this.context.beginPath();
        this.context.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
        this.context.fill();
    }

    /**
    * 更新畫布
    * @param {number} seconds
    */
    update(seconds) {
        this.vy += gravity * seconds; // 重力加速度
        this.x += this.vx * seconds;
        this.y += this.vy * seconds;
    }

    // 小球是否碰撞
    isCircleCollided(other) {
        let squareDistance = (this.x - other.x) * (this.x - other.x) + (this.y - other.y) * (this.y - other.y);
        let squareRadius = (this.r + other.r) * (this.r + other.r);
        return squareDistance <= squareRadius;
    }

    // 碰撞后將小球的狀態(tài)改為碰撞
    checkCollideWith(other) {
        if (this.isCircleCollided(other)) {
            this.colliding = true;
            other.colliding = true;

            this.changeVelocityAndDirection(other); // 當(dāng)碰撞的時候去設(shè)置速度向量
        }
    }

    // 檢測小球碰撞后的速度
    changeVelocityAndDirection(other) {
        // 創(chuàng)建兩小球的速度向量
        let velocity1 = new Vector(this.vx, this.vy);
        let velocity2 = new Vector(other.vx, other.vy);

        // 獲取兩個圓心坐標(biāo)的差
        let vNorm = new Vector(this.x - other.x, this.y - other.y);

        // 獲取連心線方向的單位向量和切線方向上的單位向量
        let unitVNorm = vNorm.normalize();
        let unitVTan = new Vector(-unitVNorm.y, unitVNorm.x);

        // 用點乘計算小球速度在這兩個方向上的投影
        let v1n = velocity1.dot(unitVNorm);
        let v1t = velocity1.dot(unitVTan);
        let v2n = velocity2.dot(unitVNorm);
        let v2t = velocity2.dot(unitVTan);

        // 恢復(fù)系數(shù),取最小值
        let cor = Math.min(this.cor, other.cor);

        // 碰撞后的速度公式所需要的變量值了狮崩,直接用代碼把公式套用進去
        // v1' = (v1(m1-m2) = 2m2v2) / (m1 + m2)
        // v2' = (v2(m2-m1) = 2m1v1) / (m1 + m2)
        // let v1nAfter = (v1n * (this.mass - other.mass) + 2 * other.mass * v2n) / (this.mass + other.mass);
        // let v2nAfter = (v2n * (other.mass - this.mass) + 2 * this.mass * v1n) / (this.mass + other.mass);
        let v1nAfter = (this.mass * v1n + other.mass * v2n + cor * other.mass * (v2n - v1n)) / (this.mass + other.mass);
        let v2nAfter = (this.mass * v1n + other.mass * v2n + cor * this.mass * (v1n - v2n)) / (this.mass + other.mass);

        // 如果 v1nAfter 小于 v2nAfter其监,那么第 1 個小球和第 2 個小球會越來越遠,此時不用處理碰撞
        if (v1nAfter < v2nAfter) {
            return;
        }

        // 給碰撞后的速度加上方向则果,計算在連心線方向和切線方向上的速度,只需要讓速度標(biāo)量跟連心線單位向量和切線單位向量相乘
        let v1VectorNorm = unitVNorm.multiply(v1nAfter);
        let v1VectorTan = unitVTan.multiply(v1t);
        let v2VectorNorm = unitVNorm.multiply(v2nAfter);
        let v2VectorTan = unitVTan.multiply(v2t);

        // 最后把連心線上的速度向量和切線方向的速度向量進行加法操作漩氨,就能獲得碰撞后小球的速度向量
        let velocity1After = v1VectorNorm.add(v1VectorTan);
        let velocity2After = v2VectorNorm.add(v2VectorTan);

        this.vx = velocity1After.x;
        this.vy = velocity1After.y;
        other.vx = velocity2After.x;
        other.vy = velocity2After.y;

    }
}

// 畫布
class Gameboard {

    constructor() {
        this.startTime;
        this.init();
    }
    init() {
        this.circles = [
            new Circle(ctx, 30, 50, 30, -100, 390, 30, 0.7),
            new Circle(ctx, 60, 180, 20, 180, -275, 20, 0.7),
            new Circle(ctx, 120, 100, 60, 120, 262, 100, 0.3),
            new Circle(ctx, 150, 180, 10, -130, 138, 10, 0.7),
            new Circle(ctx, 190, 210, 10, 138, -280, 10, 0.7),
            new Circle(ctx, 220, 240, 10, 142, 350, 10, 0.7),
            new Circle(ctx, 100, 260, 10, 135, -460, 10, 0.7),
            new Circle(ctx, 120, 285, 10, -165, 370, 10, 0.7),
            new Circle(ctx, 140, 290, 10, 125, 230, 10, 0.7),
            new Circle(ctx, 160, 380, 10, -175, -180, 10, 0.7),
            new Circle(ctx, 180, 310, 10, 115, 440, 10, 0.7),
            new Circle(ctx, 100, 310, 10, -195, -325, 10, 0.7),
            new Circle(ctx, 60, 150, 10, -138, 420, 10, 0.7),
            new Circle(ctx, 70, 430, 45, 135, -230, 45, 0.7),
            new Circle(ctx, 250, 290, 40, -140, 335, 40, 0.7),
        ];
        window.requestAnimationFrame(this.process.bind(this));
    }

    process(now) {
        if (!this.startTime) {
            this.startTime = now;
        }
        let seconds = (now - this.startTime) / 1000;
        this.startTime = now;

        for (let i = 0; i < this.circles.length; i++) {
            this.circles[i].update(seconds);
        }

        this.checkEdgeCollision();
        this.checkCollision();

        ctx.clearRect(0, 0, width, height);

        for (let i = 0; i < this.circles.length; i++) {
            this.circles[i].draw(ctx);
        }       

        window.requestAnimationFrame(this.process.bind(this));
    }

    // 重置碰撞狀態(tài)
    checkCollision() {
        this.circles.forEach((circle) => (circle.colliding = false));
        for (let i = 0; i < this.circles.length; i++) {
            for (let j = i + 1; j < this.circles.length; j++) {
                this.circles[i].checkCollideWith(this.circles[j]);
            }
        }
    }

    // 檢測邊界碰撞
    checkEdgeCollision() {
        const cor = 0.8; // 設(shè)置恢復(fù)系統(tǒng)

        this.circles.forEach((circle) => {
            // 左右碰壁
            if(circle.x < circle.r) {
                circle.vx = -circle.vx * cor;
                circle.x = circle.r;
            } else if(circle.x > width - circle.r) {
                circle.vx = -circle.vx * cor;
                circle.x = width - circle.r;
            }

            // 上下碰壁
            if(circle.y < circle.r) {
                circle.vy = -circle.vy * cor;
                circle.y = circle.r;
            } else if(circle.y > height - circle.r) {
                circle.vy = -circle.vy * cor;
                circle.y = height - circle.r;
            }
        })
    }
}

new Gameboard();

class Vector {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    /**
    * 向量加法
    * @param {Vector} v
    */
    add(v) {
        return new Vector(this.x + v.x, this.y + v.y);
    }

    /**
    * 向量減法
    * @param {Vector} v
    */
    substract(v) {
        return new Vector(this.x - v.x, this.y - v.y);
    }

    /**
    * 向量與標(biāo)量乘法
    * @param {Vector} s
    */
    multiply(s) {
        return new Vector(this.x * s, this.y * s);
    }

    /**
    * 向量與向量點乘(投影)
    * @param {Vector} v
    */
    dot(v) {
        return this.x * v.x + this.y * v.y;
    }

    /**
    * 向量標(biāo)準(zhǔn)化(除去長度)
    * @param {number} distance
    */
    normalize() {
        let distance = Math.sqrt(this.x * this.x + this.y * this.y);
        return new Vector(this.x / distance, this.y / distance);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末西壮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子叫惊,更是在濱河造成了極大的恐慌款青,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,080評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霍狰,死亡現(xiàn)場離奇詭異抡草,居然都是意外死亡,警方通過查閱死者的電腦和手機蔗坯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評論 3 385
  • 文/潘曉璐 我一進店門康震,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宾濒,你說我怎么就攤上這事腿短。” “怎么了绘梦?”我有些...
    開封第一講書人閱讀 157,630評論 0 348
  • 文/不壞的土叔 我叫張陵橘忱,是天一觀的道長。 經(jīng)常有香客問我卸奉,道長钝诚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,554評論 1 284
  • 正文 為了忘掉前任榄棵,我火速辦了婚禮凝颇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘疹鳄。我一直安慰自己拧略,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,662評論 6 386
  • 文/花漫 我一把揭開白布尚辑。 她就那樣靜靜地躺著辑鲤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杠茬。 梳的紋絲不亂的頭發(fā)上月褥,一...
    開封第一講書人閱讀 49,856評論 1 290
  • 那天弛随,我揣著相機與錄音,去河邊找鬼宁赤。 笑死舀透,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的决左。 我是一名探鬼主播愕够,決...
    沈念sama閱讀 39,014評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼佛猛!你這毒婦竟也來了惑芭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,752評論 0 268
  • 序言:老撾萬榮一對情侶失蹤继找,失蹤者是張志新(化名)和其女友劉穎遂跟,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體婴渡,經(jīng)...
    沈念sama閱讀 44,212評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡幻锁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,541評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了边臼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哄尔。...
    茶點故事閱讀 38,687評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖柠并,靈堂內(nèi)的尸體忽然破棺而出岭接,到底是詐尸還是另有隱情,我是刑警寧澤堂鲤,帶...
    沈念sama閱讀 34,347評論 4 331
  • 正文 年R本政府宣布亿傅,位于F島的核電站,受9級特大地震影響瘟栖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谅阿,卻給世界環(huán)境...
    茶點故事閱讀 39,973評論 3 315
  • 文/蒙蒙 一半哟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧签餐,春花似錦寓涨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,777評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至冠摄,卻和暖如春糯崎,著一層夾襖步出監(jiān)牢的瞬間几缭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,006評論 1 266
  • 我被黑心中介騙來泰國打工沃呢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留年栓,地道東北人。 一個月前我還...
    沈念sama閱讀 46,406評論 2 360
  • 正文 我出身青樓薄霜,卻偏偏與公主長得像某抓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子惰瓜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,576評論 2 349

推薦閱讀更多精彩內(nèi)容