← 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>