A moving target and a projectile speed; the reticle shows the predicted intercept point (lead) so aiming feels “smart.”


Code for Above

<div style="max-width:1500px;margin:0 auto;">
<iframe
  title="POC — Lead Reticle + Fire (Always Visible)"
  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 — Lead Reticle + Fire (Always Visible)</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 canvas once • Mouse aim • Space fire • Reticle always drawn</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);

// keyboard
const keys = Object.create(null);
document.addEventListener("keydown", (e) => {
  if (e.code === "Space") { e.preventDefault(); if (!keys.__space){ keys.__space = true; fire(); } }
}, {passive:false});
document.addEventListener("keyup", (e) => {
  if (e.code === "Space") { e.preventDefault(); keys.__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)
  };
}

const shooter = { x: W*0.25, y: H*0.70 };
let mx = W*0.75, my = H*0.35;
c.addEventListener("mousemove", (e) => { const p = pos(e); mx=p.x; my=p.y; });

// moving target
const tgt = { x: W*0.72, y: H*0.32, vx: 3.2, vy: -2.1, r: 12 };

// projectile
const BULLET_SPEED = 16.0; // bumped so intercept almost always exists
const bullets = [];
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;
}

// intercept solution
function intercept(){
  const rx = tgt.x - shooter.x;
  const ry = tgt.y - shooter.y;
  const vx = tgt.vx;
  const vy = tgt.vy;
  const s = BULLET_SPEED;

  const a = (vx*vx + vy*vy) - s*s;
  const b = 2*(rx*vx + ry*vy);
  const c2 = (rx*rx + ry*ry);

  let t = null;

  if (Math.abs(a) < 1e-6){
    if (Math.abs(b) < 1e-6) return null;
    const tt = -c2 / b;
    if (tt > 0) t = tt;
  } else {
    const disc = b*b - 4*a*c2;
    if (disc < 0) return null;
    const sd = Math.sqrt(disc);
    const t1 = (-b - sd) / (2*a);
    const t2 = (-b + sd) / (2*a);
    const cand = [t1, t2].filter(v => v > 0).sort((m,n)=>m-n);
    if (cand.length) t = cand[0];
  }
  if (t === null || !isFinite(t)) return null;

  return { x: tgt.x + tgt.vx*t, y: tgt.y + tgt.vy*t, t };
}

let lastLead = null;

function fire(){
  const aim = lastLead ? {x:lastLead.x, y:lastLead.y} : {x:mx, y:my};
  let dx = aim.x - shooter.x;
  let dy = aim.y - shooter.y;
  const d = Math.hypot(dx,dy) || 1;
  dx /= d; dy /= d;

  bullets.push({
    x: shooter.x,
    y: shooter.y,
    vx: dx * BULLET_SPEED,
    vy: dy * BULLET_SPEED,
    life: 1
  });
}

function update(){
  // move target
  tgt.x += tgt.vx;
  tgt.y += tgt.vy;
  if (tgt.x < tgt.r || tgt.x > W - tgt.r) tgt.vx *= -1;
  if (tgt.y < tgt.r || tgt.y > H - tgt.r) tgt.vy *= -1;
  tgt.x = clamp(tgt.x, tgt.r, W - tgt.r);
  tgt.y = clamp(tgt.y, tgt.r, H - tgt.r);

  lastLead = intercept();

  // bullets
  for (const b of bullets){
    b.x += b.vx;
    b.y += b.vy;
    b.life *= 0.992;

    const d = Math.hypot(b.x - tgt.x, b.y - tgt.y);
    if (d <= HIT_R){
      b.life = 0;
      tgt.vx *= -1; tgt.vy *= -1;
    }

    if (b.x < -50 || b.x > W+50 || b.y < -50 || b.y > H+50) b.life = 0;
  }
  for (let i=bullets.length-1;i>=0;i--){
    if (bullets[i].life < 0.05) bullets.splice(i,1);
  }
}

function drawReticle(px, py, strong){
  x.strokeStyle = strong ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,0.35)";
  x.lineWidth = 2;
  x.beginPath(); x.arc(px, py, 14, 0, Math.PI*2); x.stroke();

  x.beginPath();
  x.moveTo(px-18, py); x.lineTo(px-6, py);
  x.moveTo(px+6, py); x.lineTo(px+18, py);
  x.moveTo(px, py-18); x.lineTo(px, py-6);
  x.moveTo(px, py+6); x.lineTo(px, py+18);
  x.stroke();
}

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

  // shooter
  x.fillStyle = "rgba(255,255,255,0.85)";
  x.beginPath(); x.arc(shooter.x, shooter.y, 7, 0, Math.PI*2); x.fill();
  x.strokeStyle = "rgba(255,255,255,0.18)";
  x.lineWidth = 2;
  x.beginPath(); x.arc(shooter.x, shooter.y, 22, 0, Math.PI*2); x.stroke();

  // target
  x.strokeStyle = "rgba(255,255,255,0.65)";
  x.lineWidth = 2;
  x.beginPath(); x.arc(tgt.x, tgt.y, tgt.r, 0, Math.PI*2); x.stroke();

  // choose reticle point (lead if possible, else mouse)
  const rx = lastLead ? lastLead.x : mx;
  const ry = lastLead ? lastLead.y : my;

  // line FROM TARGET (your request)
  x.strokeStyle = "rgba(255,255,255,0.20)";
  x.lineWidth = 2;
  x.beginPath();
  x.moveTo(tgt.x, tgt.y);
  x.lineTo(rx, ry);
  x.stroke();

  // reticle ALWAYS visible
  drawReticle(rx, ry, !!lastLead);

  // time label if lead exists
  if (lastLead){
    x.fillStyle = "rgba(255,255,255,0.55)";
    x.font = "12px system-ui";
    x.fillText(`t=${lastLead.t.toFixed(2)}s`, rx + 18, ry - 18);
  }

  // bullets
  x.fillStyle = "rgba(255,255,255,0.85)";
  for (const b of bullets){
    x.globalAlpha = b.life;
    x.beginPath(); x.arc(b.x, b.y, 3.5, 0, Math.PI*2); x.fill();
  }
  x.globalAlpha = 1;

  // HUD
  x.fillStyle = "rgba(255,255,255,0.6)";
  x.font = "14px system-ui";
  x.fillText(`bulletSpeed=${BULLET_SPEED.toFixed(1)}  targetV=(${tgt.vx.toFixed(1)},${tgt.vy.toFixed(1)})  bullets=${bullets.length}  Space=fire`, 12, 24);
  x.fillText(lastLead ? "LEAD reticle" : "Fallback reticle (no lead solution)", 12, 44);

  requestAnimationFrame(loop);
}

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