← Mike Higgins Home
Custom Code Experiment •

Asteroids/Balls

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Asteroids - Balls</title>
    <link rel="icon" href="data:," />
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background: #000033;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

        #nav {
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            background-color: rgba(30, 30, 40, 0.85);
            color: white;
            display: flex;
            justify-content: center;
            padding: 8px 15px;
            z-index: 10;
            border-radius: 50px;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
            backdrop-filter: blur(5px);
            border: 1px solid rgba(255, 255, 255, 0.1);
        }

        #nav button {
            background: linear-gradient(145deg, #4a4a6d, #3a3a5a);
            color: white;
            border: none;
            margin: 0 6px;
            padding: 10px 20px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            border-radius: 30px;
            transition: all 0.3s ease;
            box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
            min-width: 50px;
        }

        #nav button:hover {
            background: linear-gradient(145deg, #5a5a8d, #4a4a7a);
            transform: translateY(-2px);
            box-shadow: 0 5px 12px rgba(0, 0, 0, 0.3);
        }

        #nav button.active {
            background: linear-gradient(145deg, #6a6aad, #5a5a9a);
            box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2), 0 3px 8px rgba(0, 0, 0, 0.2);
        }

        canvas {
            display: block;
            background: #000022;
        }
    </style>
</head>
<body>
    <div id="nav">
        <button id="btn1" class="active" onclick="startAnimation1()">1</button>
        <button id="btn2" onclick="startAnimation2()">2</button>
    </div>
    <canvas id="p"></canvas>

    <script>
        const c = document.getElementById("p"),
            x = c.getContext("2d"),
            m = Math;
        let animationFrame;
        let currentAnimation = 1;

        function resizeCanvas() {
            c.width = innerWidth * 2;
            c.height = innerHeight * 2;
            c.style.width = innerWidth + "px";
            c.style.height = innerHeight + "px";
            x.scale(2, 2);
        }

        function clearCanvas() {
            cancelAnimationFrame(animationFrame);
            x.clearRect(0, 0, c.width, c.height);
        }

        function updateButtonStates() {
            document.getElementById('btn1').classList.toggle('active', currentAnimation === 1);
            document.getElementById('btn2').classList.toggle('active', currentAnimation === 2);
        }

        function animation1() {
            const f = [],
                i = [],
                stars = [];
            let t = 0;

            class S {
                constructor() {
                    this.s = 20;
                    this.x = m.random() * (c.width / 2 - 100) + 50;
                    this.y = m.random() * (c.height / 2 - 100) + 50;
                    this.a = m.random() * m.PI * 2;
                    this.c = `hsl(${m.random() * 360},100%,50%)`;
                    this.m = 2 + m.random() * 1;
                    this.ac = 0.1;
                    this.t = 0.06;
                    this.av = 0.05;
                    this.e = 50;
                    this.n = 20 + m.random() * 10;
                    this.z = 0.5;
                    this.b = 0.05;
                    this.vx = m.cos(this.a) * this.m;
                    this.vy = m.sin(this.a) * this.m;
                    this.p = [];
                    for (let j = 0; j < this.s; j++) {
                        this.p.push({
                            x: this.x - this.vx * j * 0.25,
                            y: this.y - this.vy * j * 0.25,
                        });
                    }
                    this.g = null;
                }

                u(asteroids, spaceships) {
                    const energyDrain = this.b * m.pow(this.z, 2);
                    this.n -= energyDrain;
                    if (this.n < 0) this.n = 0;
                    this.z = 0.25 + (this.n / 100) * 0.75;

                    let target = null,
                        maxInfluence = -Infinity;

                    for (let asteroid of asteroids) {
                        const dx = asteroid.x - this.x;
                        const dy = asteroid.y - this.y;
                        const distance = m.hypot(dx, dy);
                        const influence = asteroid.n / distance;
                        if (influence > maxInfluence) {
                            maxInfluence = influence;
                            target = asteroid;
                        }
                    }

                    let targetAngle = this.a;

                    if (target) {
                        this.g = target;
                        targetAngle = m.atan2(target.y - this.y, target.x - this.x);
                    }

                    const boundaryAvoidanceAngle = this.ga();
                    if (boundaryAvoidanceAngle !== null) {
                        targetAngle = targetAngle * 0.2 + boundaryAvoidanceAngle * 0.8;
                    }

                    const alignmentAngle = this.aa(spaceships);
                    if (alignmentAngle !== null) {
                        targetAngle = targetAngle * 0.8 + alignmentAngle * 0.2;
                    }

                    let angularDifference = targetAngle - this.a;
                    angularDifference = ((angularDifference + m.PI) % (m.PI * 2)) - m.PI;
                    this.a += angularDifference * this.t;

                    const targetVx = m.cos(this.a) * this.m * this.z;
                    const targetVy = m.sin(this.a) * this.m * this.z;
                    this.vx += (targetVx - this.vx) * this.ac;
                    this.vy += (targetVy - this.vy) * this.ac;

                    this.x += this.vx;
                    this.y += this.vy;

                    if (
                        this.x < 0 ||
                        this.x > c.width / 2 ||
                        this.y < 0 ||
                        this.y > c.height / 2
                    ) {
                        this.n -= 10;
                        if (this.n < 0) this.n = 0;
                        this.x = m.max(0, m.min(this.x, c.width / 2));
                        this.y = m.max(0, m.min(this.y, c.height / 2));
                    }

                    for (let j = 0; j < this.s; j++) {
                        if (j === 0) {
                            this.p[j] = { x: this.x, y: this.y };
                        } else {
                            const previousPoint = this.p[j - 1];
                            const currentPoint = this.p[j];
                            const dx = previousPoint.x - currentPoint.x;
                            const dy = previousPoint.y - currentPoint.y;
                            const distance = m.hypot(dx, dy);
                            const trailDistance = 10 * this.z;
                            if (distance > trailDistance) {
                                currentPoint.x += dx * 0.2;
                                currentPoint.y += dy * 0.2;
                            }
                        }
                    }

                    if (this.g) {
                        const dx = this.g.x - this.x;
                        const dy = this.g.y - this.y;
                        const distance = m.hypot(dx, dy);
                        if (distance < 15 * this.z) {
                            this.n += this.g.n;
                            if (this.n > 200) this.n = 200;
                            return this.g;
                        }
                    }

                    return null;
                }

                ga() {
                    let dx = 0,
                        dy = 0;
                    if (this.x < this.e) dx = 1;
                    else if (this.x > c.width / 2 - this.e) dx = -1;
                    if (this.y < this.e) dy = 1;
                    else if (this.y > c.height / 2 - this.e) dy = -1;
                    return dx !== 0 || dy !== 0 ? m.atan2(dy, dx) : null;
                }

                aa(spaceships) {
                    let dx = 0,
                        dy = 0;
                    for (let ship of spaceships) {
                        if (ship === this) continue;
                        const deltaX = this.x - ship.x;
                        const deltaY = this.y - ship.y;
                        const distance = m.hypot(deltaX, deltaY);
                        if (distance < 50) {
                            dx += deltaX / distance;
                            dy += deltaY / distance;
                        }
                    }
                    return dx !== 0 || dy !== 0 ? m.atan2(dy, dx) : null;
                }

                d() {
                    x.save();
                    x.translate(this.x, this.y);
                    x.rotate(this.a);

                    x.beginPath();
                    x.moveTo(30 * this.z, 0);
                    x.lineTo(-30 * this.z, 15 * this.z);
                    x.lineTo(-30 * this.z, -15 * this.z);
                    x.closePath();
                    x.fillStyle = this.c;
                    x.fill();
                    x.restore();
                }
            }

            class A {
                constructor() {
                    this.x = (m.random() * c.width) / 2;
                    this.y = (m.random() * c.height) / 2;
                    this.z = 3 + m.random() * 5;
                    this.vx = (m.random() - 0.5) * 2;
                    this.vy = (m.random() - 0.5) * 2;
                    this.c = `rgb(${100 + m.random() * 155},${100 + m.random() * 155},${
                        100 + m.random() * 155
                    })`;
                    this.n = this.z * 10;
                }

                u() {
                    this.x += this.vx;
                    this.y += this.vy;

                    if (this.x < 0 || this.x > c.width / 2) this.vx *= -1;
                    if (this.y < 0 || this.y > c.height / 2) this.vy *= -1;
                }

                d() {
                    x.save();
                    x.translate(this.x, this.y);
                    x.beginPath();
                    x.arc(0, 0, this.z, 0, 2 * m.PI);
                    x.fillStyle = this.c;
                    x.fill();
                    x.restore();
                }
            }

            class Star {
                constructor() {
                    this.x = (m.random() * c.width) / 2;
                    this.y = (m.random() * c.height) / 2;
                    this.size = 0.5 + m.random() * 2;
                    this.color = `rgba(255, 255, 255, 0.7)`;
                }

                update() {
                    this.y += 0.5;
                    if (this.y > c.height / 2) {
                        this.y = 0;
                        this.x = (m.random() * c.width) / 2;
                    }
                }

                draw() {
                    x.fillStyle = this.color;
                    x.beginPath();
                    x.arc(this.x, this.y, this.size, 0, m.PI * 2);
                    x.fill();
                }
            }

            function init() {
                resizeCanvas();
                for (let j = 0; j < 20; j++) f.push(new S());
                for (let j = 0; j < 50; j++) i.push(new A());
                for (let j = 0; j < 200; j++) stars.push(new Star());
            }

            function a() {
                if (currentAnimation !== 1) return;

                x.clearRect(0, 0, c.width, c.height);

                stars.forEach((star) => {
                    star.update();
                    star.draw();
                });

                f.forEach((ship) => {
                    let g = ship.u(i, f);
                    if (g) {
                        let j = i.indexOf(g);
                        if (j > -1) i.splice(j, 1);
                    }
                    ship.d();
                });

                i.forEach((asteroid) => {
                    asteroid.u();
                    asteroid.d();
                });

                animationFrame = requestAnimationFrame(a);
            }

            init();
            a();
        }

        function animation2() {
            class Vector2D {
                constructor(x, y) {
                    this.x = x;
                    this.y = y;
                }

                add(vector) {
                    return new Vector2D(this.x + vector.x, this.y + vector.y);
                }

                multiply(scalar) {
                    return new Vector2D(this.x * scalar, this.y * scalar);
                }
            }

            class Entity {
                constructor(position, velocity, radius, color) {
                    this.position = position;
                    this.velocity = velocity;
                    this.radius = radius;
                    this.color = color;
                }

                update(deltaTime) {
                    this.position = this.position.add(this.velocity.multiply(deltaTime));
                }

                draw(ctx) {
                    ctx.beginPath();
                    ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
                    ctx.fillStyle = this.color;
                    ctx.fill();
                    ctx.closePath();
                }

                checkBoundaryCollision(width, height) {
                    if (this.position.x - this.radius <= 0 || this.position.x + this.radius >= width) {
                        this.velocity.x *= -1;
                    }
                    if (this.position.y - this.radius <= 0 || this.position.y + this.radius >= height) {
                        this.velocity.y *= -1;
                    }
                }
            }

            class ParticleSystem {
                constructor() {
                    this.entities = [];
                }

                addEntity(entity) {
                    this.entities.push(entity);
                }

                update(deltaTime, width, height) {
                    for (let i = 0; i < this.entities.length; i++) {
                        const entity = this.entities[i];
                        entity.update(deltaTime);
                        entity.checkBoundaryCollision(width, height);

                        for (let j = i + 1; j < this.entities.length; j++) {
                            this.resolveCollision(entity, this.entities[j]);
                        }
                    }
                }

                resolveCollision(entity1, entity2) {
                    const dx = entity2.position.x - entity1.position.x;
                    const dy = entity2.position.y - entity1.position.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);

                    if (distance < entity1.radius + entity2.radius) {
                        const nx = dx / distance;
                        const ny = dy / distance;

                        const relativeVelocityX = entity2.velocity.x - entity1.velocity.x;
                        const relativeVelocityY = entity2.velocity.y - entity1.velocity.y;

                        const velocityAlongNormal =
                            relativeVelocityX * nx + relativeVelocityY * ny;

                        if (velocityAlongNormal > 0) return;

                        const impulse = -2 * velocityAlongNormal;
                        const massSum =
                            entity1.radius * entity1.radius +
                            entity2.radius * entity2.radius;

                        entity1.velocity.x -=
                            (impulse * entity2.radius * entity2.radius) / massSum * nx;
                        entity1.velocity.y -=
                            (impulse * entity2.radius * entity2.radius) / massSum * ny;
                        entity2.velocity.x +=
                            (impulse * entity1.radius * entity1.radius) / massSum * nx;
                        entity2.velocity.y +=
                            (impulse * entity1.radius * entity1.radius) / massSum * ny;
                    }
                }

                draw(ctx) {
                    this.entities.forEach((entity) => entity.draw(ctx));
                }
            }

            const particleSystem = new ParticleSystem();
            const ctx = c.getContext("2d");
            resizeCanvas();

            for (let i = 0; i < 15; i++) {
                const position = new Vector2D(
                    Math.random() * c.width/2,
                    Math.random() * c.height/2
                );
                const velocity = new Vector2D(
                    (Math.random() - 0.5) * 400,
                    (Math.random() - 0.5) * 400
                );
                const radius = 10 + Math.random() * 20;
                const color = `hsl(${Math.random() * 360}, 70%, 50%)`;
                particleSystem.addEntity(
                    new Entity(position, velocity, radius, color)
                );
            }

            function animate(currentTime) {
                if (currentAnimation !== 2) return;

                ctx.clearRect(0, 0, c.width, c.height);
                particleSystem.update(0.016, c.width/2, c.height/2);
                particleSystem.draw(ctx);
                animationFrame = requestAnimationFrame(animate);
            }

            animate(0);
        }

        function startAnimation1() {
            clearCanvas();
            currentAnimation = 1;
            updateButtonStates();
            animation1();
        }

        function startAnimation2() {
            clearCanvas();
            currentAnimation = 2;
            updateButtonStates();
            animation2();
        }

        window.addEventListener('resize', function() {
            resizeCanvas();
            if (currentAnimation === 1) {
                startAnimation1();
            } else {
                startAnimation2();
            }
        });

        startAnimation1();
    </script>
</body>
</html>