Lines and Balls by XAWPHAD
by PixelRider35203 lines5.9 KB
<!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.