小球弹跳

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,而画布以像素为单位,所以重力加速度应保持一定的比例,
// 这里简单的使用了 100 倍的重力加速度。
const gravity = 980; // 重力加速度常量


// 小球的类
class Circle {
    /**
     * @param context 对象
     * @param x 坐标
     * @param y 坐标
     * @param r 半径
     * @param vx x速度
     * @param vy y速度
     * @param mass 质量
     * @param cor 恢复系数
     *
     */
    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;
    }

    // 碰撞后将小球的状态改为碰撞
    checkCollideWith(other) {
        if (this.isCircleCollided(other)) {
            this.colliding = true;
            other.colliding = true;

            this.changeVelocityAndDirection(other); // 当碰撞的时候去设置速度向量
        }
    }

    // 检测小球碰撞后的速度
    changeVelocityAndDirection(other) {
        // 创建两小球的速度向量
        let velocity1 = new Vector(this.vx, this.vy);
        let velocity2 = new Vector(other.vx, other.vy);

        // 获取两个圆心坐标的差
        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);

        // 恢复系数,取最小值
        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;
        }

        // 给碰撞后的速度加上方向,计算在连心线方向和切线方向上的速度,只需要让速度标量跟连心线单位向量和切线单位向量相乘
        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));
    }

    // 重置碰撞状态
    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; // 设置恢复系统

        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);
    }

    /**
    * 向量与标量乘法
    * @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;
    }

    /**
    * 向量标准化(除去长度)
    * @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);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容