Move a player dot; the world is covered by a dark mask, and only a circular area around the player is revealed.


Code for Above

<div style="max-width:1500px;margin:0 auto;">
<iframe
  title="POC — Fog of War (Opaque Mask)"
  scrolling="no"
  style="width:1500px;height:1500px;border:0;display:block;background:#111;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 — Fog of War (Opaque Mask)</title>
<style>
  html, body { margin:0; width:100%; height:100%; overflow:hidden; background:#111; }
  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" tabindex="0"></canvas>
<div class="hint">Click canvas to focus • Arrows move • Outside circle is hidden</div>

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

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

// Prevent page scroll
const k = {};
function isArrow(key){ return key.startsWith("Arrow"); }
addEventListener("keydown", e => { if (isArrow(e.key)) e.preventDefault(); k[e.key]=true; }, {passive:false});
addEventListener("keyup",   e => { if (isArrow(e.key)) e.preventDefault(); k[e.key]=false; }, {passive:false});
c.focus(); c.addEventListener("pointerdown", () => c.focus());

// Player
const p = { x: W/2, y: H/2, vx: 0, vy: 0 };
const ACCEL = 0.55, DRAG = 0.90, MAX = 14;

// Fog settings (key change: opaque)
const REVEAL_R = 170;
const SOFT_EDGE = 20;      // smaller soft edge = less bleed
const FOG_ALPHA = 1.0;     // opaque mask

// Background objects
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 (k.ArrowUp)    p.vy -= ACCEL;
  if (k.ArrowDown)  p.vy += ACCEL;
  if (k.ArrowLeft)  p.vx -= ACCEL;
  if (k.ArrowRight) p.vx += ACCEL;

  p.vx *= DRAG; p.vy *= DRAG;
  const sp = Math.hypot(p.vx, p.vy);
  if (sp > MAX){ p.vx = (p.vx/sp)*MAX; p.vy = (p.vy/sp)*MAX; }

  p.x = clamp(p.x + p.vx, 0, W);
  p.y = clamp(p.y + p.vy, 0, H);
}

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

  // grid
  x.strokeStyle = "rgba(255,255,255,0.06)";
  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();
  }

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

function drawFog(){
  // 1) Fill entire fog layer with OPAQUE black
  fx.globalCompositeOperation = "source-over";
  fx.clearRect(0,0,W,H);
  fx.fillStyle = `rgba(0,0,0,${FOG_ALPHA})`;
  fx.fillRect(0,0,W,H);

  // 2) Cut a hole (destination-out)
  fx.globalCompositeOperation = "destination-out";
  const g = fx.createRadialGradient(
    p.x, p.y, REVEAL_R - SOFT_EDGE,
    p.x, p.y, REVEAL_R
  );
  g.addColorStop(0, "rgba(0,0,0,1)");
  g.addColorStop(1, "rgba(0,0,0,0)");

  fx.fillStyle = g;
  fx.beginPath();
  fx.arc(p.x, p.y, REVEAL_R, 0, Math.PI*2);
  fx.fill();

  // 3) Overlay fog on the visible scene (this hides everything outside hole)
  x.drawImage(fog, 0, 0);

  // 4) Outline reveal so it reads instantly
  x.strokeStyle = "rgba(255,255,255,0.35)";
  x.lineWidth = 2;
  x.beginPath();
  x.arc(p.x, p.y, REVEAL_R, 0, Math.PI*2);
  x.stroke();
}

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