A rotating vision cone reveals only what’s inside it; everything else is masked to prove stealth/vision mechanics visually.


Code for Above

<div style="max-width:1500px;margin:0 auto;">
<iframe
  title="POC — Cone of Vision (FOV)"
  scrolling="no"
  style="width:1500px;height:1500px;border:0;display:block;background:#000;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.5);"
  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>POC — Cone of Vision (FOV)</title>
<style>
  html, body { margin:0; width:100%; height:100%; overflow:hidden; background:#000; }
  canvas { display:block; outline:none; }
  .hint { position:absolute; left:12px; bottom:12px; color:#aaa; font:14px system-ui; user-select:none; }
</style>
</head>
<body>
<canvas id="c" width="1500" height="1500"></canvas>
<div class="hint">A/D rotate • W/S change range • Objects only visible in cone</div>

<script>
const c = document.getElementById("c");
const x = c.getContext("2d");
const W = 1500, H = 1500;

// Offscreen mask layer
const mask = document.createElement("canvas");
mask.width = W; mask.height = H;
const mx = mask.getContext("2d");

// Prevent page scroll on keys (WASD)
const keys = {};
addEventListener("keydown", (e) => {
  if (["w","a","s","d","W","A","S","D"].includes(e.key)) e.preventDefault();
  keys[e.key.toLowerCase()] = true;
}, { passive:false });
addEventListener("keyup", (e) => {
  if (["w","a","s","d","W","A","S","D"].includes(e.key)) e.preventDefault();
  keys[e.key.toLowerCase()] = false;
}, { passive:false });

// Viewer (agent)
const agent = { x: W/2, y: H/2, ang: -0.6 };
let range = 520;
const fov = Math.PI * 0.42; // ~75 degrees

// Objects to reveal
const objs = Array.from({length: 70}, () => ({
  x: Math.random()*W,
  y: Math.random()*H,
  r: 5 + Math.random()*10
}));

function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }

function update(){
  if (keys.a) agent.ang -= 0.035;
  if (keys.d) agent.ang += 0.035;
  if (keys.w) range = clamp(range + 6, 120, 900);
  if (keys.s) range = clamp(range - 6, 120, 900);
}

function drawScene(){
  x.clearRect(0,0,W,H);

  // grid
  x.strokeStyle = "rgba(255,255,255,0.05)";
  for (let i=0;i<=W;i+=150){ x.beginPath(); x.moveTo(i,0); x.lineTo(i,H); x.stroke(); }
  for (let j=0;j<=H;j+=150){ x.beginPath(); x.moveTo(0,j); x.lineTo(W,j); x.stroke(); }

  // objects
  x.fillStyle = "rgba(255,255,255,0.75)";
  for (const o of objs){
    x.beginPath();
    x.arc(o.x, o.y, o.r, 0, Math.PI*2);
    x.fill();
  }

  // agent
  x.fillStyle = "#fff";
  x.beginPath();
  x.arc(agent.x, agent.y, 10, 0, Math.PI*2);
  x.fill();

  // facing tick
  x.strokeStyle = "rgba(255,255,255,0.35)";
  x.lineWidth = 3;
  x.beginPath();
  x.moveTo(agent.x, agent.y);
  x.lineTo(agent.x + Math.cos(agent.ang)*24, agent.y + Math.sin(agent.ang)*24);
  x.stroke();
}

function drawMask(){
  // full blackout
  mx.clearRect(0,0,W,H);
  mx.fillStyle = "rgba(0,0,0,0.92)";
  mx.fillRect(0,0,W,H);

  // cut out cone
  mx.globalCompositeOperation = "destination-out";

  // soft edge cone using gradient along arc
  // base cone
  mx.beginPath();
  mx.moveTo(agent.x, agent.y);
  mx.arc(agent.x, agent.y, range, agent.ang - fov/2, agent.ang + fov/2);
  mx.closePath();
  mx.fillStyle = "rgba(0,0,0,1)";
  mx.fill();

  // restore
  mx.globalCompositeOperation = "source-over";

  // outline cone for readability
  x.strokeStyle = "rgba(255,255,255,0.18)";
  x.lineWidth = 2;
  x.beginPath();
  x.moveTo(agent.x, agent.y);
  x.arc(agent.x, agent.y, range, agent.ang - fov/2, agent.ang + fov/2);
  x.closePath();
  x.stroke();

  // overlay mask
  x.drawImage(mask, 0, 0);
}

function drawHUD(){
  x.fillStyle = "rgba(255,255,255,0.6)";
  x.font = "14px system-ui";
  x.fillText(`range=${range}  fov=${Math.round((fov*180/Math.PI))}deg`, 12, 24);
}

function loop(){
  update();
  drawScene();
  drawMask();
  drawHUD();
  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
'></iframe>
</div>