Click to set a moving target; press Space to fire a homing missile that turns with limited steering and hits on contact.


Code for Above

<div style="max-width:1500px;margin:0 auto;">
<iframe
  title="POC — Homing Missile (Steering)"
  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 — Homing Missile (Steering)</title>
<style>
  html, body { margin:0; width:100%; height:100%; overflow:hidden; background:#000; }
  canvas { display:block; outline:none; cursor:crosshair; }
  .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: move target • Space: fire missile • Missile homes + hits</div>

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

function focusCanvas(){ try { c.focus(); } catch(_){} }
focusCanvas();
c.addEventListener("pointerdown", focusCanvas);

// keys inside iframe (reliable)
const k = Object.create(null);
document.addEventListener("keydown", (e) => {
  if (e.code === "Space") { e.preventDefault(); if (!k.__space){ k.__space = true; fire(); } }
}, {passive:false});
document.addEventListener("keyup", (e) => {
  if (e.code === "Space") { e.preventDefault(); k.__space = false; }
}, {passive:false});

function pos(e){
  const r = c.getBoundingClientRect();
  return {
    x: (e.clientX - r.left) * (c.width / r.width),
    y: (e.clientY - r.top)  * (c.height / r.height)
  };
}
c.addEventListener("click", (e) => {
  const p = pos(e);
  target.x = p.x; target.y = p.y;
});

// target (moves in a small loop so homing is visible)
const target = { x: W*0.75, y: H*0.35, t: 0 };

// launcher origin
const origin = { x: W*0.25, y: H*0.65 };

// missiles
const missiles = [];

// tunables
const MISSILE_SPEED = 10.0;
const TURN_RATE = 0.10;   // radians/frame max turn
const HIT_R = 16;

function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }
function angNorm(a){
  while (a > Math.PI) a -= Math.PI*2;
  while (a < -Math.PI) a += Math.PI*2;
  return a;
}

function fire(){
  // initial heading toward current target
  const dx = target.x - origin.x;
  const dy = target.y - origin.y;
  const ang = Math.atan2(dy, dx);
  missiles.push({ x: origin.x, y: origin.y, ang, alive: true, trail: [] });
}

function update(){
  // move target in a loop around its click point
  target.t += 0.02;
  const ox = target.x, oy = target.y;
  // small orbit around its own point (visual motion)
  const tx = ox + Math.cos(target.t) * 90;
  const ty = oy + Math.sin(target.t*1.3) * 60;

  // update missiles
  for (const m of missiles){
    if (!m.alive) continue;

    // desired angle to target
    const dx = tx - m.x;
    const dy = ty - m.y;
    const desired = Math.atan2(dy, dx);

    // turn toward desired with limit
    let delta = angNorm(desired - m.ang);
    delta = clamp(delta, -TURN_RATE, TURN_RATE);
    m.ang += delta;

    // move forward
    m.x += Math.cos(m.ang) * MISSILE_SPEED;
    m.y += Math.sin(m.ang) * MISSILE_SPEED;

    // trail (keep last N points)
    m.trail.push({ x:m.x, y:m.y });
    if (m.trail.length > 22) m.trail.shift();

    // hit test
    const dist = Math.hypot(tx - m.x, ty - m.y);
    if (dist <= HIT_R){
      m.alive = false;
      m.explodeT = 1;
    }

    // bounds kill
    if (m.x < -50 || m.x > W+50 || m.y < -50 || m.y > H+50){
      m.alive = false;
    }
  }

  // decay explosion markers
  for (const m of missiles){
    if (m.explodeT){
      m.explodeT *= 0.86;
      if (m.explodeT < 0.05) m.explodeT = 0;
    }
  }

  // store target draw position for this frame
  target.drawX = tx;
  target.drawY = ty;
}

function draw(){
  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(); }

  // launcher
  x.fillStyle = "rgba(255,255,255,0.75)";
  x.beginPath(); x.arc(origin.x, origin.y, 7, 0, Math.PI*2); x.fill();
  x.strokeStyle = "rgba(255,255,255,0.18)";
  x.lineWidth = 2;
  x.beginPath(); x.arc(origin.x, origin.y, 20, 0, Math.PI*2); x.stroke();

  // target
  x.strokeStyle = "rgba(255,255,255,0.55)";
  x.lineWidth = 2;
  x.beginPath(); x.arc(target.drawX, target.drawY, 14, 0, Math.PI*2); x.stroke();
  x.fillStyle = "rgba(255,255,255,0.35)";
  x.beginPath(); x.arc(target.drawX, target.drawY, 4, 0, Math.PI*2); x.fill();

  // missiles
  for (const m of missiles){
    // trail
    if (m.trail.length > 1){
      x.strokeStyle = "rgba(255,255,255,0.20)";
      x.lineWidth = 3;
      x.beginPath();
      x.moveTo(m.trail[0].x, m.trail[0].y);
      for (let i=1;i<m.trail.length;i++) x.lineTo(m.trail[i].x, m.trail[i].y);
      x.stroke();
    }

    if (m.alive){
      // missile body
      x.fillStyle = "#fff";
      x.beginPath(); x.arc(m.x, m.y, 6, 0, Math.PI*2); x.fill();
      // nose tick
      x.strokeStyle = "rgba(0,0,0,0.6)";
      x.lineWidth = 2;
      x.beginPath();
      x.moveTo(m.x, m.y);
      x.lineTo(m.x + Math.cos(m.ang)*14, m.y + Math.sin(m.ang)*14);
      x.stroke();
    }

    if (m.explodeT){
      x.strokeStyle = `rgba(255,255,255,${m.explodeT})`;
      x.lineWidth = 3;
      x.beginPath();
      x.arc(m.x, m.y, 40*(1-m.explodeT)+10, 0, Math.PI*2);
      x.stroke();
    }
  }

  // HUD
  x.fillStyle = "rgba(255,255,255,0.6)";
  x.font = "14px system-ui";
  x.fillText(`missiles=${missiles.length}  turnRate=${TURN_RATE.toFixed(2)}  speed=${MISSILE_SPEED.toFixed(1)}`, 12, 24);

  requestAnimationFrame(loop);
}

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