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>