Spawns incoming “threats” (projectiles) from off-screen aimed at the player. When a threat is outside the view, the HUD shows a directional warning wedge at the screen edge pointing toward the incoming vector. The wedge’s intensity and size scale with time-to-impact, so you can react without reading numbers. When threats enter the screen, the wedge fades out and you see the actual projectile.


Code for Above

<div style="max-width:1500px;margin:0 auto;">
  <iframe
    title="Threat Vector Warning POC"
    scrolling="no"
    style="display:block;margin:0 auto;border:0;width:1500px;height:1500px;overflow:hidden;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.55);background:#000;"
    sandbox="allow-scripts allow-same-origin"
    srcdoc='<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Threat Vector Warning POC</title>
<style>
  :root { color-scheme: dark; }
  html, body { margin:0; padding:0; width:100%; height:100%; overflow:hidden; background:#000; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; }
  #app { width:1500px; height:1500px; margin:0 auto; position:relative; background:#000; }
  canvas { display:block; width:1500px; height:1500px; outline:none; }

  .top { position:absolute; left:14px; top:14px; right:14px; display:flex; flex-direction:column; gap:6px; pointer-events:none; z-index:2; }
  .title { display:inline-flex; width:max-content; padding:8px 12px; border-radius:12px; background:rgba(0,0,0,.55); border:1px solid rgba(255,255,255,.10); box-shadow:0 10px 30px rgba(0,0,0,.35); font-weight:650; letter-spacing:.02em; }
  .desc { max-width:1120px; padding:8px 12px; border-radius:12px; background:rgba(0,0,0,.45); border:1px solid rgba(255,255,255,.08); color:rgba(255,255,255,.78); font-size:12px; line-height:1.35; }

  .hud { position:absolute; left:0; right:0; bottom:0; height:122px; padding:10px 14px;
    background:linear-gradient(to top, rgba(0,0,0,.88), rgba(0,0,0,.35));
    border-top:1px solid rgba(255,255,255,.08);
    display:flex; gap:14px; align-items:flex-start; justify-content:space-between; pointer-events:none;
    z-index:2;
  }
  .hud .left, .hud .right { display:flex; flex-direction:column; gap:6px; }
  .hud .row { display:flex; gap:10px; align-items:baseline; flex-wrap:wrap; }
  .k { opacity:.7; font-size:12px; letter-spacing:.02em; }
  .v { font-size:14px; }
  .hint { opacity:.55; font-size:12px; max-width:980px; }
  .pill { display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border:1px solid rgba(255,255,255,.10); border-radius:999px; background:rgba(255,255,255,.03); }

  /* Overlay: ensure it always accepts clicks and sits on top */
  .overlay {
    position:absolute; inset:0;
    display:flex; align-items:center; justify-content:center;
    background:rgba(0,0,0,.35);
    backdrop-filter: blur(2px);
    z-index:10;
    pointer-events:auto;
  }
  .panel {
    width:min(900px, calc(100% - 40px));
    padding:18px; border-radius:14px;
    background:rgba(0,0,0,.60);
    border:1px solid rgba(255,255,255,.12);
    box-shadow:0 18px 60px rgba(0,0,0,.45);
  }
  .panel .h1 { font-size:16px; font-weight:700; margin:0 0 8px 0; }
  .panel .p  { font-size:12px; line-height:1.4; opacity:.8; margin:0 0 12px 0; }
  .panel .btn {
    display:inline-flex; align-items:center; justify-content:center;
    padding:10px 14px; border-radius:12px;
    border:1px solid rgba(255,255,255,.22);
    background:rgba(255,255,255,.10);
    color:rgba(255,255,255,.92);
    font-weight:800;
    cursor:pointer;
    user-select:none;
  }
  .panel .p2 { font-size:12px; line-height:1.4; opacity:.65; margin:10px 0 0 0; }

  /* Debug banner: shows whether click handlers fired */
  .dbg {
    position:absolute; left:14px; right:14px; top:92px;
    padding:10px 12px; border-radius:12px;
    background:rgba(0,0,0,.75);
    border:1px solid rgba(255,255,255,.16);
    color:rgba(255,255,255,.9);
    font-size:12px; line-height:1.35;
    z-index:11;
    display:none;
    white-space:pre-wrap;
    pointer-events:none;
  }
</style>
</head>
<body>
  <div id="app">
    <canvas id="c" width="1500" height="1500" tabindex="0"></canvas>

    <div class="top">
      <div class="title">Threat Vector Warning POC</div>
      <div class="desc">
        Click Start (or press Enter) to focus the iframe. This build fixes the “stuck overlay” failure mode and includes a debug banner if clicks aren’t firing.
      </div>
    </div>

    <div class="dbg" id="dbg"></div>

    <div class="overlay" id="overlay">
      <div class="panel">
        <div class="h1">Click to Start</div>
        <p class="p">Press <b>Enter</b> or click <b>START</b>. If you still can’t start, the debug banner will show whether the click handler fired.</p>
        <div class="btn" id="startBtn" role="button" tabindex="0">START</div>
        <p class="p2">Controls: WASD move · Shift sprint · T spawn threat · C clear · R reset</p>
      </div>
    </div>

    <div class="hud">
      <div class="left">
        <div class="row">
          <span class="pill"><span class="k">Controls</span><span class="v">WASD move · Shift sprint · T spawn threat · C clear · R reset</span></span>
        </div>
        <div class="hint" id="hint">Status: waiting for start.</div>
      </div>
      <div class="right">
        <div class="row"><span class="k">Threats</span><span class="v" id="th">0</span></div>
        <div class="row"><span class="k">Closest TTI</span><span class="v" id="tti">—</span></div>
      </div>
    </div>
  </div>

<script>
(() => {
  const W = 1500, H = 1500;

  const canvas = document.getElementById("c");
  const ctx = canvas.getContext("2d");

  const uiTh = document.getElementById("th");
  const uiTti = document.getElementById("tti");
  const hintEl = document.getElementById("hint");

  const overlay = document.getElementById("overlay");
  const startBtn = document.getElementById("startBtn");
  const dbg = document.getElementById("dbg");

  const keys = new Set();
  let started = false;

  function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); }
  function rand(a,b){ return a + Math.random()*(b-a); }

  function debug(msg){
    dbg.style.display = "block";
    dbg.textContent = msg;
    // auto-hide after a bit
    clearTimeout(debug._t);
    debug._t = setTimeout(()=>{ dbg.style.display = "none"; }, 2500);
  }

  function focusCanvas(){
    try { canvas.focus({ preventScroll:true }); } catch { canvas.focus(); }
  }

  function start(){
    started = true;
    overlay.style.display = "none";
    hintEl.textContent = "Status: running. Press T to spawn threats.";
    focusCanvas();
    // ensure we actually have threats to see immediately
    if (threats.length === 0) { spawnThreat(); spawnThreat(); spawnThreat(); }
  }

  // Force reliable click handling:
  // - listen on capture phase
  // - stop propagation so nothing swallows it
  function hookStart(el){
    el.addEventListener("pointerdown", (e) => {
      e.preventDefault();
      e.stopPropagation();
      debug("pointerdown: START fired");
      start();
    }, { capture:true });

    el.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();
      debug("click: START fired");
      start();
    }, { capture:true });
  }

  hookStart(startBtn);
  hookStart(overlay);

  // Keyboard fallback: Enter / Space starts even if mouse is weird
  addEventListener("keydown", (e) => {
    if ((e.code === "Enter" || e.code === "Space") && !started){
      e.preventDefault();
      debug("keyboard start: " + e.code);
      start();
      return;
    }

    const block = ["KeyW","KeyA","KeyS","KeyD","ShiftLeft","ShiftRight","KeyT","KeyC","KeyR"];
    if (block.includes(e.code)) e.preventDefault();
    keys.add(e.code);

    if (!started && block.includes(e.code)) start();

    if (e.code === "KeyT") spawnThreat();
    if (e.code === "KeyC") threats.length = 0;
    if (e.code === "KeyR") reset();
  }, { passive:false });

  addEventListener("keyup", (e) => keys.delete(e.code));

  // Background stars
  const stars = [];
  for (let i=0; i<520; i++){
    const big = Math.random() < 0.06;
    stars.push({
      x: Math.random()*W, y: Math.random()*H,
      r: big ? (2.0 + Math.random()*1.6) : (0.8 + Math.random()*0.9),
      a: big ? (0.30 + Math.random()*0.40) : (0.10 + Math.random()*0.35)
    });
  }
  function drawStars(){
    ctx.save();
    ctx.fillStyle = "#fff";
    for (const s of stars){
      ctx.globalAlpha = s.a;
      ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI*2); ctx.fill();
    }
    ctx.restore();
  }

  // Player
  const player = { x: W/2, y: H/2, r: 10 };

  // Threats
  const threats = [];
  const OFF = 260;
  const THREAT_SPEED = 320;
  const HIT_R = 14;

  function reset(){
    player.x = W/2; player.y = H/2;
    threats.length = 0;
    // keep overlay off if already running; otherwise leave it
    if (started){
      spawnThreat(); spawnThreat(); spawnThreat();
    }
  }

  function spawnThreat(){
    const edge = Math.floor(Math.random()*4);
    let x,y;
    if (edge===0){ x = -OFF; y = rand(0, H); }
    if (edge===1){ x = W+OFF; y = rand(0, H); }
    if (edge===2){ x = rand(0, W); y = -OFF; }
    if (edge===3){ x = rand(0, W); y = H+OFF; }

    const dx = player.x - x;
    const dy = player.y - y;
    let ang = Math.atan2(dy, dx);
    ang += (Math.random()*2 - 1) * (8 * Math.PI/180);

    const vx = Math.cos(ang) * THREAT_SPEED;
    const vy = Math.sin(ang) * THREAT_SPEED;

    threats.push({ x, y, vx, vy });
  }

  function drawPlayer(){
    ctx.save();
    ctx.globalAlpha = 0.95;
    ctx.fillStyle = "rgba(255,255,255,0.92)";
    ctx.beginPath(); ctx.arc(player.x, player.y, player.r, 0, Math.PI*2); ctx.fill();
    ctx.globalAlpha = 0.35;
    ctx.strokeStyle = "rgba(255,255,255,0.55)";
    ctx.lineWidth = 2;
    ctx.beginPath(); ctx.arc(player.x, player.y, player.r+10, 0, Math.PI*2); ctx.stroke();
    ctx.restore();
  }

  function drawThreat(t){
    ctx.save();
    ctx.globalAlpha = 0.95;
    ctx.fillStyle = "rgba(255,255,255,0.92)";
    ctx.beginPath(); ctx.arc(t.x, t.y, 5, 0, Math.PI*2); ctx.fill();
    ctx.globalAlpha = 0.35;
    ctx.strokeStyle = "rgba(255,255,255,0.55)";
    ctx.lineWidth = 2;
    ctx.beginPath(); ctx.arc(t.x, t.y, 12, 0, Math.PI*2); ctx.stroke();
    ctx.restore();
  }

  // Inbound-only, scaling wedge
  function drawWarningWedge(t){
    // off-screen only
    const on = (t.x >= 0 && t.x <= W && t.y >= 0 && t.y <= H);
    if (on) return;

    const toPx = player.x - t.x;
    const toPy = player.y - t.y;
    const approaching = (toPx * t.vx + toPy * t.vy) > 0;
    if (!approaching) return;

    const dist = Math.hypot(toPx, toPy);
    const sp = Math.hypot(t.vx, t.vy) || 1;
    let tti = dist / sp;

    // fade out when very close to boundary
    const edgeDx = (t.x < 0) ? -t.x : (t.x > W ? t.x - W : 0);
    const edgeDy = (t.y < 0) ? -t.y : (t.y > H ? t.y - H : 0);
    const edgeDist = Math.hypot(edgeDx, edgeDy);
    const boundaryAlphaMul = clamp(edgeDist / 160, 0, 1);

    const ang = Math.atan2(t.y - player.y, t.x - player.x);
    const ux = Math.cos(ang), uy = Math.sin(ang);

    // intersection with screen
    const candidates = [];
    if (ux !== 0){
      let k = (0 - player.x) / ux; let y = player.y + uy*k;
      if (k>0 && y>=0 && y<=H) candidates.push({x:0,y,k});
      k = (W - player.x) / ux; y = player.y + uy*k;
      if (k>0 && y>=0 && y<=H) candidates.push({x:W,y,k});
    }
    if (uy !== 0){
      let k = (0 - player.y) / uy; let x = player.x + ux*k;
      if (k>0 && x>=0 && x<=W) candidates.push({x,y:0,k});
      k = (H - player.y) / uy; x = player.x + ux*k;
      if (k>0 && x>=0 && x<=W) candidates.push({x,y:H,k});
    }
    if (!candidates.length) return;
    candidates.sort((a,b)=>a.k-b.k);
    const hit = candidates[0];

    // scaling
    tti = clamp(tti, 0.25, 5.0);
    const danger = 1 - (tti - 0.25) / (5.0 - 0.25);
    const d2 = danger*danger;
    const d4 = d2*d2;

    const alpha = clamp((0.18 + d4*0.90) * boundaryAlphaMul, 0, 0.98);
    if (alpha <= 0.01) return;

    const width  = 18 + d4 * 90;
    const length = 44 + d4 * 220;

    const inward = ang + Math.PI;
    const inset = 20;

    const baseX = clamp(hit.x + Math.cos(inward)*inset, 0+inset, W-inset);
    const baseY = clamp(hit.y + Math.sin(inward)*inset, 0+inset, H-inset);

    const tipX = baseX + Math.cos(inward) * length;
    const tipY = baseY + Math.sin(inward) * length;

    const px = -Math.sin(inward), py = Math.cos(inward);

    const leftX  = baseX + px * width;
    const leftY  = baseY + py * width;
    const rightX = baseX - px * width;
    const rightY = baseY - py * width;

    ctx.save();
    ctx.globalAlpha = alpha * 0.55;
    ctx.fillStyle = "rgba(255,255,255,0.35)";
    ctx.beginPath(); ctx.moveTo(tipX, tipY); ctx.lineTo(leftX, leftY); ctx.lineTo(rightX, rightY); ctx.closePath(); ctx.fill();
    ctx.restore();

    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.fillStyle = "rgba(255,255,255,0.82)";
    ctx.beginPath(); ctx.moveTo(tipX, tipY); ctx.lineTo(leftX, leftY); ctx.lineTo(rightX, rightY); ctx.closePath(); ctx.fill();
    ctx.restore();
  }

  function shouldCull(t){
    return (t.x < -OFF*1.8 || t.x > W+OFF*1.8 || t.y < -OFF*1.8 || t.y > H+OFF*1.8);
  }

  // Loop
  let last = performance.now();
  function frame(now){
    const dt = Math.min(0.033, (now - last) / 1000);
    last = now;

    if (started){
      const sprint = (keys.has("ShiftLeft")||keys.has("ShiftRight")) ? 1.75 : 1.0;
      const spd = 260 * sprint;

      let mx=0,my=0;
      if (keys.has("KeyW")) my -= 1;
      if (keys.has("KeyS")) my += 1;
      if (keys.has("KeyA")) mx -= 1;
      if (keys.has("KeyD")) mx += 1;
      const mlen = Math.hypot(mx,my) || 1;
      mx/=mlen; my/=mlen;

      player.x = clamp(player.x + mx*spd*dt, 0, W);
      player.y = clamp(player.y + my*spd*dt, 0, H);

      for (let i=threats.length-1; i>=0; i--){
        const t = threats[i];
        t.x += t.vx * dt;
        t.y += t.vy * dt;

        const dx = t.x - player.x;
        const dy = t.y - player.y;
        if (Math.hypot(dx,dy) <= HIT_R){
          threats.splice(i,1);
          continue;
        }
        if (shouldCull(t)) threats.splice(i,1);
      }

      if (threats.length < 2 && Math.random() < 0.02) spawnThreat();
    }

    let closest = Infinity;
    for (const t of threats){
      const toPx = player.x - t.x;
      const toPy = player.y - t.y;
      const approaching = (toPx * t.vx + toPy * t.vy) > 0;
      if (!approaching) continue;
      const dist = Math.hypot(toPx, toPy);
      const sp = Math.hypot(t.vx, t.vy) || 1;
      closest = Math.min(closest, dist / sp);
    }

    ctx.clearRect(0,0,W,H);
    drawStars();

    for (const t of threats) drawWarningWedge(t);
    for (const t of threats){
      if (t.x >= 0 && t.x <= W && t.y >= 0 && t.y <= H) drawThreat(t);
    }
    drawPlayer();

    uiTh.textContent = String(threats.length);
    uiTti.textContent = (closest === Infinity ? "—" : closest.toFixed(2) + "s");

    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);

})();
</script>
</body>
</html>'>
  </iframe>
</div>