小球弹跳

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);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容