Triangles and Balls by XAWPHAD
by PixelRider35234 lines6.1 KB
<!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.