ring clicker · miss penalty
by PixelRider35348 lines12.3 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ring clicker · miss penalty</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
background: black;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Segoe UI', system-ui, sans-serif;
}
canvas {
display: block;
background: black;
width: 100vw;
height: 100vh;
object-fit: contain;
cursor: crosshair;
}
#scoreboard {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 32px;
font-weight: 700;
letter-spacing: 1px;
background: rgba(0, 0, 0, 0.65);
padding: 10px 28px;
border-radius: 40px;
border: 1px solid rgba(255, 255, 255, 0.15);
backdrop-filter: blur(4px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
z-index: 10;
pointer-events: none;
text-shadow: 0 0 12px rgba(255, 255, 255, 0.2);
}
#scoreboard span {
color: #ffd966;
}
#missIndicator {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
color: #ff6b6b;
font-size: 20px;
font-weight: 600;
opacity: 0;
transition: opacity 0.15s ease;
pointer-events: none;
z-index: 10;
text-shadow: 0 0 20px rgba(255, 0, 0, 0.4);
}
#missIndicator.show {
opacity: 1;
}
</style>
</head>
<body>
<div id="scoreboard"> <span id="scoreDisplay">0</span></div>
<div id="missIndicator"></div>
<canvas id="ringCanvas"></canvas>
<script>
(function() {
const canvas = document.getElementById('ringCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
const missIndicator = document.getElementById('missIndicator');
// ----- score -----
let score = 0;
// ----- resize canvas to fill viewport -----
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// ----- ring storage -----
let rings = [];
// ----- random helpers -----
const rand = (min, max) => Math.random() * (max - min) + min;
const randInt = (min, max) => Math.floor(rand(min, max + 1));
// ----- generate a random bright color (HSL) -----
function randomColor() {
const h = randInt(0, 360);
const s = randInt(70, 100);
const l = randInt(55, 85);
return `hsl(${h}, ${s}%, ${l}%)`;
}
// ----- collision detection (circle vs circle) -----
function circlesOverlap(x1, y1, r1, x2, y2, r2) {
const dx = x1 - x2;
const dy = y1 - y2;
const dist = Math.hypot(dx, dy);
return dist < r1 + r2;
}
// ----- check if a new ring overlaps any existing ring -----
function overlapsAny(newRing) {
for (let r of rings) {
if (circlesOverlap(
newRing.x, newRing.y, newRing.radius,
r.x, r.y, r.radius
)) {
return true;
}
}
return false;
}
// ----- generate a non-overlapping ring with random color -----
function generateNonOverlappingRing(maxAttempts = 800) {
const w = canvas.width;
const h = canvas.height;
const minR = 12;
const maxR = 100;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const radius = rand(minR, maxR);
const margin = radius + 2;
const x = rand(margin, w - margin);
const y = rand(margin, h - margin);
const candidate = { x, y, radius };
if (!overlapsAny(candidate)) {
const minLife = 300; //
const maxLife = 2000; //
const lifeMs = rand(minLife, maxLife);
return {
x, y,
radius,
color: randomColor(),
birth: performance.now(),
lifeMs: lifeMs,
deathTime: performance.now() + lifeMs,
filled: false,
wasClicked: false, // track if user clicked this ring
id: Math.random().toString(36).substring(2, 9)
};
}
}
return null;
}
// ----- spawn a new ring (if possible) -----
function spawnRing() {
const newRing = generateNonOverlappingRing();
if (newRing) {
rings.push(newRing);
}
}
// ----- remove expired rings & penalize if not clicked -----
function removeExpiredRings(now) {
const toRemove = [];
for (let i = rings.length - 1; i >= 0; i--) {
const r = rings[i];
if (r.deathTime <= now) {
// Ring expired naturally
if (!r.wasClicked && !r.filled) {
// User missed this ring ? -1 point
score -= 1;
updateScore();
showMissIndicator();
}
toRemove.push(i);
}
}
// Remove from end to start
for (let idx of toRemove) {
rings.splice(idx, 1);
}
}
// ----- draw all rings with fade and color -----
function drawRings(now) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let r of rings) {
const timeLeft = r.deathTime - now;
let alpha = 1.0;
if (timeLeft < 400 && timeLeft > 0) {
alpha = timeLeft / 400;
} else if (timeLeft <= 0) {
alpha = 0;
}
ctx.globalAlpha = alpha;
if (r.filled) {
// FILLED ring: solid color
ctx.beginPath();
ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2);
ctx.fillStyle = r.color;
ctx.fill();
ctx.strokeStyle = r.color;
ctx.lineWidth = 1.5;
ctx.stroke();
} else {
// NORMAL ring: only stroke
ctx.beginPath();
ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2);
ctx.strokeStyle = r.color;
ctx.lineWidth = 2.8;
ctx.stroke();
}
}
ctx.globalAlpha = 1.0;
}
// ----- update score display -----
function updateScore() {
scoreSpan.textContent = score;
}
// ----- show miss indicator briefly -----
let missTimeout = null;
function showMissIndicator() {
missIndicator.classList.add('show');
if (missTimeout) {
clearTimeout(missTimeout);
}
missTimeout = setTimeout(() => {
missIndicator.classList.remove('show');
}, 300);
}
//
function handleCanvasClick(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mouseX = (e.clientX - rect.left) * scaleX;
const mouseY = (e.clientY - rect.top) * scaleY;
let hitRing = false;
const now = performance.now();
// iterate rings (backwards so we can modify safely)
for (let i = rings.length - 1; i >= 0; i--) {
const r = rings[i];
const timeLeft = r.deathTime - now;
if (timeLeft <= 0) continue;
const dx = mouseX - r.x;
const dy = mouseY - r.y;
const dist = Math.hypot(dx, dy);
if (dist < r.radius) {
//
score += 5;
updateScore();
// Mark as clicked and fill it
r.wasClicked = true;
r.filled = true;
// Remove after a short delay so fill is visible
const ringId = r.id;
setTimeout(() => {
const index = rings.findIndex(rr => rr.id === ringId);
if (index !== -1) {
rings.splice(index, 1);
}
}, 150);
hitRing = true;
break; // only one ring per click
}
}
// if no ring was hit: -1 point
if (!hitRing) {
score -= 1;
updateScore();
showMissIndicator();
}
}
// ----- animation loop -----
let lastSpawnTime = 0;
let spawnInterval = rand(200, 600);
function animate(timestamp) {
const now = performance.now();
// 1) remove expired rings & penalize misses
removeExpiredRings(now);
// 2) spawn new rings at a fast pace
if (now - lastSpawnTime > spawnInterval) {
spawnInterval = rand(200, 600);
spawnRing();
lastSpawnTime = now;
}
// 3) draw everything
drawRings(now);
requestAnimationFrame(animate);
}
// ----- START WITH A BLACK SCREEN (no initial rings) -----
// rings array starts empty ? canvas stays black until first spawn
// set spawn timer
lastSpawnTime = performance.now();
// start animation
requestAnimationFrame(animate);
// ----- click listener -----
canvas.addEventListener('click', handleCanvasClick);
// ----- resize: keep rings, they'll naturally expire -----
window.addEventListener('resize', () => {
resizeCanvas();
});
// init score display
updateScore();
console.log(' Click inside a ring ? +5 points & ring fills!');
console.log('? Miss a ring (click outside or let it expire) ? -1 point');
})();
</script>
</body>
</html>Game Source: ring clicker · miss penalty
Creator: PixelRider35
Libraries: none
Complexity: complex (348 lines, 12.3 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: ring-clicker-miss-penalty-pixelrider35" to link back to the original. Then publish at arcadelab.ai/publish.