Click to set a destination; a ghost line previews the intended path while the dot moves toward it


Code for Above

<div style="max-width:1500px;margin:0 auto;">
<iframe
  title="POC — Path Preview (Ghost Line)"
  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 — Path Preview (Ghost Line)</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"></canvas>
<div class="hint">Click: set destination • Dot moves • Line previews intent</div>

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

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 p = { x: W/2, y: H/2, vx: 0, vy: 0 };
let goal = { x: W/2, y: H/2 };
let hasGoal = false;

const SPEED = 7.0;   // pixels per frame
const ARRIVE = 6;    // stop radius

c.addEventListener("click", (e) => {
  const m = pos(e);
  goal.x = m.x; goal.y = m.y;
  hasGoal = true;
});

function update(){
  if (!hasGoal) { p.vx = p.vy = 0; return; }

  const dx = goal.x - p.x;
  const dy = goal.y - p.y;
  const d = Math.hypot(dx, dy);

  if (d <= ARRIVE){
    p.vx = p.vy = 0;
    hasGoal = false;
    return;
  }

  p.vx = (dx / d) * SPEED;
  p.vy = (dy / d) * SPEED;

  p.x += p.vx;
  p.y += p.vy;
}

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

  // ghost path + goal marker
  if (hasGoal){
    x.strokeStyle = "rgba(255,255,255,0.25)";
    x.lineWidth = 3;
    x.beginPath();
    x.moveTo(p.x, p.y);
    x.lineTo(goal.x, goal.y);
    x.stroke();

    x.strokeStyle = "rgba(255,255,255,0.45)";
    x.lineWidth = 2;
    x.beginPath();
    x.arc(goal.x, goal.y, 14, 0, Math.PI*2);
    x.stroke();
  }

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

  // HUD
  const sp = Math.hypot(p.vx, p.vy);
  x.fillStyle = "rgba(255,255,255,0.6)";
  x.font = "14px system-ui";
  x.fillText(hasGoal ? `moving • speed ${sp.toFixed(2)}` : "click to set goal", 12, 24);
}

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