🎮ArcadeLab

Triangles and Balls by XAWPHAD

by PixelRider35
234 lines6.1 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Triangles and Balls by XAWPHAD</title>
<style>
  html, body { margin:0; padding:0; background:black; overflow:hidden; }
  canvas { display:block; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
canvas.width = innerWidth;
canvas.height = innerHeight;

window.addEventListener("resize",()=>{
  canvas.width = innerWidth;
  canvas.height = innerHeight;
});

class Triangle {
  constructor(x, y, color) {
    this.x = x; this.y = y;
    this.size = 60;
    this.angle = Math.random() * Math.PI * 2;
    this.speed = 0.01 + Math.random() * 0.01;
    this.direction = Math.random() < 0.5 ? 1 : -1;
    this.color = color;
  }
  update(){ this.angle += this.speed * this.direction; }
  draw(ctx){
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.rotate(this.angle);
    ctx.beginPath();
    ctx.moveTo(0, -this.size/2);
    ctx.lineTo(this.size/2, this.size/2);
    ctx.lineTo(-this.size/2, this.size/2);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.restore();
  }
  containsPoint(px, py){
    const dx = px - this.x, dy = py - this.y;
    return Math.abs(dx) < this.size/2 && Math.abs(dy) < this.size/2;
  }
  getEdges(){
    const s = this.size/2;
    const pts = [
      {x:0, y:-s},
      {x:s, y:s},
      {x:-s, y:s}
    ];
    const c = Math.cos(this.angle), si = Math.sin(this.angle);
    return pts.map(p=>({
      x:this.x + p.x*c - p.y*si,
      y:this.y + p.x*si + p.y*c
    }));
  }
}

class Ball {
  constructor(x,y){
    this.x = x; this.y = y;
    this.r = 10 + Math.random()*10;
    const speed = 1 + Math.random()*3;
    const angle = Math.random() * Math.PI * 2;
    this.vx = Math.cos(angle) * speed;
    this.vy = Math.sin(angle) * speed;
    this.color = `hsl(${Math.random()*360},100%,50%)`;
    this.bounceCount = 0;
    this.cooldown = 0; // cooldown frames to prevent double-counts
  }
  update(){
    this.x += this.vx;
    this.y += this.vy;
    if (this.cooldown > 0) this.cooldown--;
  }
  draw(ctx){
    ctx.beginPath();
    ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
    ctx.fillStyle = this.color;
    ctx.fill();
  }
  touchesWall(){
    let bounced = false;
    if(this.x-this.r<0){this.x=this.r;this.vx*=-1;bounced=true;}
    if(this.x+this.r>canvas.width){this.x=canvas.width-this.r;this.vx*=-1;bounced=true;}
    if(this.y-this.r<0){this.y=this.r;this.vy*=-1;bounced=true;}
    if(this.y+this.r>canvas.height){this.y=canvas.height-this.r;this.vy*=-1;bounced=true;}
    if(bounced && this.cooldown === 0){ 
      this.bounceCount++; 
      this.cooldown = 5; // prevents multiple counts per frame
    }
  }
}

function reflect(ball, nx, ny){
  const dot = ball.vx*nx + ball.vy*ny;
  ball.vx -= 2*dot*nx;
  ball.vy -= 2*dot*ny;
  if(ball.cooldown === 0){
    ball.bounceCount++;
    ball.cooldown = 5;
  }
}

function pointLineDistance(px,py,x1,y1,x2,y2){
  const A=px-x1,B=py-y1,C=x2-x1,D=y2-y1;
  const dot=A*C+B*D;
  const len=C*C+D*D;
  let t=dot/len;
  t=Math.max(0,Math.min(1,t));
  const nx=x1+t*C, ny=y1+t*D;
  const dx=px-nx, dy=py-ny;
  return {dist:Math.hypot(dx,dy), dx, dy};
}

function triangleBallCollision(tri, ball){
  const pts=tri.getEdges();
  for(let i=0;i<3;i++){
    const p1=pts[i],p2=pts[(i+1)%3];
    const {dist,dx,dy}=pointLineDistance(ball.x,ball.y,p1.x,p1.y,p2.x,p2.y);
    if(dist<ball.r){
      const overlap = ball.r - dist;
      const nlen = Math.hypot(dx,dy);
      if(nlen>0){
        const ux=dx/nlen, uy=dy/nlen;
        ball.x += ux*overlap;
        ball.y += uy*overlap;
        reflect(ball, ux, uy);
      }
    }
  }
}

function resolveBallCollision(b1, b2){
  const dx = b2.x - b1.x;
  const dy = b2.y - b1.y;
  const dist = Math.hypot(dx, dy);
  if(dist === 0) return;
  const overlap = (b1.r + b2.r) - dist;
  if(overlap > 0){
    const nx = dx / dist;
    const ny = dy / dist;
    const move = overlap / 2;
    b1.x -= nx * move;
    b1.y -= ny * move;
    b2.x += nx * move;
    b2.y += ny * move;

    const p1 = b1.vx * nx + b1.vy * ny;
    const p2 = b2.vx * nx + b2.vy * ny;

    const diff = p1 - p2;
    b1.vx -= diff * nx;
    b1.vy -= diff * ny;
    b2.vx += diff * nx;
    b2.vy += diff * ny;

    if (b1.cooldown === 0) { b1.bounceCount++; b1.cooldown = 5; }
    if (b2.cooldown === 0) { b2.bounceCount++; b2.cooldown = 5; }
  }
}

const colors = ["red","green","blue"];
const triangles = [];
const balls = [];
const MAX_BALLS = 100;

canvas.addEventListener("click",(e)=>{
  const mx = e.clientX, my = e.clientY;

  for(const t of triangles){
    if(t.containsPoint(mx,my)){ t.direction*=-1; return; }
  }

  if(triangles.length<3){
    const color = colors[triangles.length];
    const newT = new Triangle(mx,my,color);
    if(newT.x-newT.size/2<0||newT.x+newT.size/2>canvas.width||
       newT.y-newT.size/2<0||newT.y+newT.size/2>canvas.height) return;
    for(const t of triangles){
      const dx=t.x-newT.x,dy=t.y-newT.y;
      if(Math.hypot(dx,dy)<t.size) return;
    }
    triangles.push(newT);
    return;
  }

  if(balls.length>=MAX_BALLS) return;
  for(const t of triangles){
    if(t.containsPoint(mx,my)) return;
  }
  balls.push(new Ball(mx,my));
});

function animate(){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.fillStyle="black";
  ctx.fillRect(0,0,canvas.width,canvas.height);

  for(const t of triangles){ t.update(); t.draw(ctx); }

  for(const b of balls){ 
    b.update(); 
    b.touchesWall(); 
    for(const t of triangles){ triangleBallCollision(t,b); }
  }

  for(let i=0;i<balls.length;i++){
    for(let j=i+1;j<balls.length;j++){
      resolveBallCollision(balls[i], balls[j]);
    }
  }

  // remove only after exactly 10 true bounces
  for(let i=balls.length-1;i>=0;i--){
    if(balls[i].bounceCount >= 15) balls.splice(i,1);
  }

  for(const b of balls){ b.draw(ctx); }

  requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>

Game Source: Triangles and Balls by XAWPHAD

Creator: PixelRider35

Libraries: none

Complexity: complex (234 lines, 6.1 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: triangles-and-balls-by-xawphad-pixelrider35" to link back to the original. Then publish at arcadelab.ai/publish.