🎮ArcadeLab

Lines and Balls by XAWPHAD

by PixelRider35
203 lines5.9 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lines and Balls by XAWPHAD</title>
<style>
html, body { margin:0; padding:0; background:black; overflow:hidden; }
canvas { display:block; cursor:crosshair; touch-action:none; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let lines = [];
let currentLine = null;
let balls = [];

function resizeCanvas(){
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    ctx.fillStyle='black';
    ctx.fillRect(0,0,canvas.width,canvas.height);
    redrawAll();
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();

// ---- LINE DRAWING ----
function randomColor(){ const h = Math.floor(Math.random()*360); return `hsl(${h},80%,50%)`; }

function startLine(x,y){ currentLine={color:randomColor(), width:2+Math.random()*4, points:[{x,y}]}; lines.push(currentLine); }
function addPoint(x,y){
    if(!currentLine) return;
    const pts = currentLine.points;
    pts.push({x,y});
    const p0 = pts[pts.length-2], p1 = pts[pts.length-1];
    ctx.strokeStyle=currentLine.color; ctx.lineWidth=currentLine.width; ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(p0.x,p0.y); ctx.lineTo(p1.x,p1.y); ctx.stroke();
}
function endLine(){ currentLine=null; }

function redrawAll(){
    ctx.fillStyle='black'; ctx.fillRect(0,0,canvas.width,canvas.height);
    for(const line of lines){
        if(line.points.length<2) continue;
        ctx.strokeStyle=line.color; ctx.lineWidth=line.width; ctx.lineCap='round';
        ctx.beginPath(); ctx.moveTo(line.points[0].x,line.points[0].y);
        for(let i=1;i<line.points.length;i++) ctx.lineTo(line.points[i].x,line.points[i].y);
        ctx.stroke();
    }
}

// ---- BALLS ----
function spawnBall(x,y){
    const radius = 5 + Math.random()*20;
    const speed = 2 + Math.random()*3;
    const angle = Math.random()*Math.PI*2;
    const dx = Math.cos(angle)*speed;
    const dy = Math.sin(angle)*speed;
    balls.push({x,y,dx,dy,radius,color:randomColor(), hits:0});
}

function updateBalls(){
    // update positions
    for(const ball of balls){
        ball.x += ball.dx;
        ball.y += ball.dy;

        // bounce off walls
        if(ball.x-ball.radius<0){ ball.x=ball.radius; ball.dx*=-1; }
        if(ball.x+ball.radius>canvas.width){ ball.x=canvas.width-ball.radius; ball.dx*=-1; }
        if(ball.y-ball.radius<0){ ball.y=ball.radius; ball.dy*=-1; }
        if(ball.y+ball.radius>canvas.height){ ball.y=canvas.height-ball.radius; ball.dy*=-1; }

        // bounce off lines
        for(const line of lines){
            for(let i=1;i<line.points.length;i++){
                const p1=line.points[i-1], p2=line.points[i];
                bounceBallOffLine(ball,p1,p2);
            }
        }
    }

    // ball-ball collisions
    for(let i=0;i<balls.length;i++){
        for(let j=i+1;j<balls.length;j++){
            if(collideBalls(balls[i],balls[j])){
                balls[i].hits++;
                balls[j].hits++;
            }
        }
    }

    // remove balls with hits > 25
    balls = balls.filter(b => b.hits <= 25);
}

function bounceBallOffLine(ball,p1,p2){
    const lx = p2.x - p1.x;
    const ly = p2.y - p1.y;
    const len = Math.sqrt(lx*lx + ly*ly);
    if(len===0) return;
    const t = Math.max(0, Math.min(1, ((ball.x-p1.x)*lx + (ball.y-p1.y)*ly)/(len*len)));
    const closestX = p1.x + t*lx;
    const closestY = p1.y + t*ly;
    const dx = ball.x - closestX;
    const dy = ball.y - closestY;
    const dist = Math.sqrt(dx*dx + dy*dy);
    if(dist < ball.radius){
        const nx = dx/dist || 0;
        const ny = dy/dist || 0;
        const dot = ball.dx*nx + ball.dy*ny;
        ball.dx -= 2*dot*nx;
        ball.dy -= 2*dot*ny;
        ball.x = closestX + nx*ball.radius;
        ball.y = closestY + ny*ball.radius;
    }
}

// proper 2D elastic collision between equal-mass balls
// returns true if collision happened
function collideBalls(b1, b2) {
    let dx = b2.x - b1.x;
    let dy = b2.y - b1.y;
    let dist = Math.sqrt(dx*dx + dy*dy);
    if(dist === 0) dist = 0.1;
    if(dist >= b1.radius + b2.radius) return false;

    // Normal and tangent vectors
    const nx = dx / dist;
    const ny = dy / dist;
    const tx = -ny;
    const ty = nx;

    // Project velocities
    const dpTan1 = b1.dx*tx + b1.dy*ty;
    const dpTan2 = b2.dx*tx + b2.dy*ty;

    const dpNorm1 = b1.dx*nx + b1.dy*ny;
    const dpNorm2 = b2.dx*nx + b2.dy*ny;

    // Swap normal components
    const m1 = dpNorm2;
    const m2 = dpNorm1;

    // Update velocities
    b1.dx = tx*dpTan1 + nx*m1;
    b1.dy = ty*dpTan1 + ny*m1;
    b2.dx = tx*dpTan2 + nx*m2;
    b2.dy = ty*dpTan2 + ny*m2;

    // Separate overlapping balls
    const overlap = 0.5*(b1.radius + b2.radius - dist + 0.1);
    b1.x -= nx*overlap;
    b1.y -= ny*overlap;
    b2.x += nx*overlap;
    b2.y += ny*overlap;

    return true;
}

// ---- DRAWING & ANIMATION ----
function drawBalls(){
    for(const ball of balls){
        ctx.fillStyle=ball.color;
        ctx.beginPath();
        ctx.arc(ball.x,ball.y,ball.radius,0,Math.PI*2);
        ctx.fill();
    }
}

function animate(){
    redrawAll();
    updateBalls();
    drawBalls();
    requestAnimationFrame(animate);
}
animate();

// ---- EVENTS ----
canvas.addEventListener('pointerdown', e=>{
    if(e.button===0){ startLine(e.clientX,e.clientY); canvas.setPointerCapture(e.pointerId); }
});

canvas.addEventListener('pointermove', e=>{
    if(currentLine) addPoint(e.clientX,e.clientY);
});

canvas.addEventListener('pointerup', e=>{
    if(currentLine){ addPoint(e.clientX,e.clientY); endLine(); }
    canvas.releasePointerCapture(e.pointerId);
});

canvas.addEventListener('contextmenu', e=>{
    e.preventDefault();
    spawnBall(e.clientX,e.clientY);
});
</script>
</body>
</html>

Game Source: Lines and Balls by XAWPHAD

Creator: PixelRider35

Libraries: none

Complexity: complex (203 lines, 5.9 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: lines-and-balls-by-xawphad-pixelrider35" to link back to the original. Then publish at arcadelab.ai/publish.