← back to Games Agentabrams
games/dvd-bounce/index.html
312 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DVD Bounce — Elevated</title>
<style>
:root{
--bg:#05060a;
--ink:#e8ecf5;
--accent:#7c5cff;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden}
body{
background:radial-gradient(120% 120% at 50% 0%, #0d1020 0%, #04050a 60%, #000 100%);
color:var(--ink);
font-family:"Segoe UI",system-ui,-apple-system,sans-serif;
-webkit-font-smoothing:antialiased;
}
#stage{position:fixed;inset:0;display:block}
/* HUD */
.hud{
position:fixed;top:16px;left:16px;z-index:10;
display:flex;flex-direction:column;gap:10px;
pointer-events:none;
}
.panel{
pointer-events:auto;
background:rgba(18,20,34,.55);
backdrop-filter:blur(12px) saturate(140%);
-webkit-backdrop-filter:blur(12px) saturate(140%);
border:1px solid rgba(255,255,255,.09);
border-radius:14px;
padding:14px 16px;
box-shadow:0 10px 40px rgba(0,0,0,.45);
}
.stats{display:flex;gap:22px;align-items:center}
.stat{display:flex;flex-direction:column;line-height:1.1}
.stat .num{font-size:26px;font-weight:800;letter-spacing:.5px;font-variant-numeric:tabular-nums}
.stat .lbl{font-size:10px;text-transform:uppercase;letter-spacing:2px;opacity:.55;margin-top:3px}
.corner .num{color:#ffd35c;text-shadow:0 0 18px rgba(255,211,92,.55)}
.controls{display:flex;gap:8px;flex-wrap:wrap}
button{
pointer-events:auto;
font:inherit;font-size:12px;font-weight:600;letter-spacing:.4px;
color:var(--ink);
background:linear-gradient(180deg,rgba(124,92,255,.35),rgba(124,92,255,.15));
border:1px solid rgba(124,92,255,.5);
border-radius:10px;
padding:9px 14px;cursor:pointer;
transition:transform .08s ease, background .15s ease, box-shadow .15s ease;
}
button:hover{background:linear-gradient(180deg,rgba(124,92,255,.55),rgba(124,92,255,.25));box-shadow:0 0 22px rgba(124,92,255,.4)}
button:active{transform:translateY(1px) scale(.98)}
button.ghost{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.14)}
button.ghost:hover{background:rgba(255,255,255,.1);box-shadow:none}
.hint{
position:fixed;bottom:14px;right:16px;z-index:10;
font-size:11px;opacity:.4;letter-spacing:.5px;pointer-events:none;
}
.flash{animation:pop .4s ease}
@keyframes pop{0%{transform:scale(1)}35%{transform:scale(1.28)}100%{transform:scale(1)}}
</style>
</head>
<body>
<canvas id="stage"></canvas>
<div class="hud">
<div class="panel stats">
<div class="stat corner">
<span class="num" id="cornerCount">0</span>
<span class="lbl">Corners</span>
</div>
<div class="stat">
<span class="num" id="wallCount">0</span>
<span class="lbl">Wall hits</span>
</div>
<div class="stat">
<span class="num" id="logoCount">1</span>
<span class="lbl">Logos</span>
</div>
</div>
<div class="panel controls">
<button id="addBtn">+ Add logo</button>
<button id="chaosBtn">Chaos ×5</button>
<button class="ghost" id="resetBtn">Reset</button>
</div>
</div>
<div class="hint">corner hits trigger the confetti · click canvas to spawn</div>
<script>
(() => {
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const cornerEl = document.getElementById('cornerCount');
const wallEl = document.getElementById('wallCount');
const logoEl = document.getElementById('logoCount');
let W = 0, H = 0, DPR = 1;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = Math.floor(W * DPR);
canvas.height = Math.floor(H * DPR);
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// ---- palette ----
const PALETTE = [
'#ff4d6d','#ffd35c','#4ade80','#38bdf8','#7c5cff',
'#f472b6','#fb923c','#22d3ee','#a3e635','#e879f9'
];
const rand = (a, b) => a + Math.random() * (b - a);
const pick = arr => arr[(Math.random() * arr.length) | 0];
const nextColor = cur => { let c; do { c = pick(PALETTE); } while (c === cur); return c; };
// ---- state ----
let logos = [];
let particles = [];
let cornerHits = 0, wallHits = 0;
// The DVD wordmark, drawn once to an offscreen canvas and tinted per-logo.
function makeLogoSprite() {
const w = 150, h = 78;
const c = document.createElement('canvas');
c.width = w; c.height = h;
const g = c.getContext('2d');
g.fillStyle = '#fff';
g.textAlign = 'center';
g.textBaseline = 'middle';
// "DVD"
g.font = 'italic 900 52px "Arial Black", Arial, sans-serif';
g.save();
g.translate(w/2, h*0.36);
g.scale(1, 0.82);
g.fillText('DVD', 0, 0);
g.restore();
// squashed ellipse under it (the classic logo oval)
g.beginPath();
g.ellipse(w/2, h*0.70, 46, 12, 0, 0, Math.PI*2);
g.fill();
// knock out the label bar
g.save();
g.globalCompositeOperation = 'destination-out';
g.fillRect(w/2 - 34, h*0.66, 68, 8);
g.restore();
// small "video" text on the bar
g.font = '900 11px Arial, sans-serif';
g.fillText('V I D E O', w/2, h*0.70);
return c;
}
const SPRITE = makeLogoSprite();
const LW = SPRITE.width, LH = SPRITE.height;
function spawnLogo(x, y) {
const speed = rand(2.2, 3.6);
const ang = rand(0, Math.PI * 2);
logos.push({
x: x != null ? x : rand(0, Math.max(0, W - LW)),
y: y != null ? y : rand(0, Math.max(0, H - LH)),
vx: Math.cos(ang) * speed * (Math.random() < .5 ? 1 : -1),
vy: Math.sin(ang) * speed * (Math.random() < .5 ? 1 : -1),
color: pick(PALETTE),
trail: []
});
updateHUD();
}
function burst(x, y, big) {
const n = big ? 90 : 22;
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const sp = rand(big ? 2 : 1, big ? 9 : 4);
particles.push({
x, y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - (big ? 1.5 : 0),
life: 1,
decay: rand(0.012, 0.03),
size: rand(2, big ? 6 : 4),
color: pick(PALETTE),
spin: rand(-0.3, 0.3),
rot: Math.random() * Math.PI
});
}
}
function flash(el){ el.classList.remove('flash'); void el.offsetWidth; el.classList.add('flash'); }
function updateHUD() {
cornerEl.textContent = cornerHits;
wallEl.textContent = wallHits;
logoEl.textContent = logos.length;
}
// ---- main loop ----
function frame() {
// fade previous frame → motion trail
ctx.fillStyle = 'rgba(4,5,10,0.20)';
ctx.fillRect(0, 0, W, H);
const maxX = W - LW, maxY = H - LH;
for (const L of logos) {
L.x += L.vx;
L.y += L.vy;
let hitX = false, hitY = false;
if (L.x <= 0) { L.x = 0; L.vx = Math.abs(L.vx); hitX = true; }
else if (L.x >= maxX) { L.x = maxX; L.vx = -Math.abs(L.vx); hitX = true; }
if (L.y <= 0) { L.y = 0; L.vy = Math.abs(L.vy); hitY = true; }
else if (L.y >= maxY) { L.y = maxY; L.vy = -Math.abs(L.vy); hitY = true; }
if (hitX || hitY) {
L.color = nextColor(L.color);
if (hitX && hitY) {
// CORNER!
cornerHits++;
burst(L.x + LW/2, L.y + LH/2, true);
flash(cornerEl);
} else {
wallHits++;
burst(hitX ? (L.x <= 0 ? 0 : W) : L.x + LW/2,
hitY ? (L.y <= 0 ? 0 : H) : L.y + LH/2, false);
}
updateHUD();
}
// trail
L.trail.push({ x: L.x, y: L.y });
if (L.trail.length > 14) L.trail.shift();
for (let i = 0; i < L.trail.length; i++) {
const t = L.trail[i];
const alpha = (i / L.trail.length) * 0.28;
drawSprite(t.x, t.y, L.color, alpha);
}
drawSprite(L.x, L.y, L.color, 1);
}
// particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.vy += 0.08; // gravity
p.vx *= 0.99;
p.x += p.vx;
p.y += p.vy;
p.rot += p.spin;
p.life -= p.decay;
if (p.life <= 0) { particles.splice(i, 1); continue; }
ctx.save();
ctx.globalAlpha = Math.max(0, p.life);
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillStyle = p.color;
ctx.shadowColor = p.color;
ctx.shadowBlur = 8;
const s = p.size;
ctx.fillRect(-s/2, -s/2, s, s * 1.6);
ctx.restore();
}
requestAnimationFrame(frame);
}
function drawSprite(x, y, color, alpha) {
ctx.save();
ctx.globalAlpha = alpha;
// tint: draw white sprite then multiply the color through
ctx.drawImage(SPRITE, x, y);
ctx.globalCompositeOperation = 'source-atop';
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.fillRect(x, y, LW, LH);
ctx.restore();
}
// ---- controls ----
document.getElementById('addBtn').addEventListener('click', () => spawnLogo());
document.getElementById('chaosBtn').addEventListener('click', () => {
for (let i = 0; i < 5; i++) spawnLogo();
});
document.getElementById('resetBtn').addEventListener('click', () => {
logos = []; particles = [];
cornerHits = 0; wallHits = 0;
spawnLogo(W/2 - LW/2, H/2 - LH/2);
});
canvas.addEventListener('pointerdown', e => spawnLogo(
Math.min(Math.max(0, e.clientX - LW/2), W - LW),
Math.min(Math.max(0, e.clientY - LH/2), H - LH)
));
// ---- go ----
spawnLogo(W/2 - LW/2, H/2 - LH/2);
updateHUD();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>