Click to add waypoints; the dot follows them in order while steering around circular obstacles with actual collision (cannot pass through).


Code for Above

<div style="max-width:1500px;margin:0 auto;">
<iframe
  title="POC — Waypoints + Obstacle Avoidance (Collision)"
  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 — Waypoints + Obstacle Avoidance (Collision)</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: add waypoint • Right click: clear • Dot avoids obstacles + collides</div>

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

c.addEventListener("contextmenu", e => e.preventDefault());

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

// ===== Player =====
const p = { x: W/2, y: H/2, vx: 0, vy: 0 };
const PR = 10;

// ===== Waypoints =====
const waypoints = [];
let currentIdx = 0;

// ===== Obstacles (circles) =====
function rand(a,b){ return a + Math.random()*(b-a); }
const obstacles = Array.from({length: 9}, () => ({
  x: rand(200, W-200),
  y: rand(200, H-200),
  r: rand(55, 120)
}));

// Keep obstacles away from initial player
for (const o of obstacles){
  const dx = o.x - p.x, dy = o.y - p.y;
  if (Math.hypot(dx,dy) < o.r + 140){
    o.x = rand(200, W-200);
    o.y = rand(200, H-200);
  }
}

// ===== Controls =====
c.addEventListener("click", (e) => {
  const m = pos(e);
  waypoints.push({ x: m.x, y: m.y });
  // if this is the first waypoint, start immediately
  if (waypoints.length === 1) currentIdx = 0;
});

c.addEventListener("mousedown", (e) => {
  if (e.button === 2) { // right click
    waypoints.length = 0;
    currentIdx = 0;
  }
});

// ===== Steering Tunables =====
const MAX_SPEED = 7.2;
const MAX_FORCE = 0.55;          // steering strength
const ARRIVE_R  = 14;            // waypoint reach radius

// Avoidance
const AVOID_LOOK = 140;          // how far obstacles influence steering
const AVOID_FORCE = 1.25;        // how hard avoidance pushes
const AVOID_SOFT = 0.0001;

// Motion damping (makes it settle)
const VEL_DAMP = 0.985;

// ===== Helpers =====
function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }
function norm(dx,dy){
  const d = Math.hypot(dx,dy) || 1;
  return { x: dx/d, y: dy/d, d };
}
function limit(vx, vy, max){
  const d = Math.hypot(vx,vy);
  if (d <= max) return { x:vx, y:vy, d };
  return { x:(vx/d)*max, y:(vy/d)*max, d };
}

// Seek force toward a point (arrive-lite)
function seekForce(tx, ty){
  const dx = tx - p.x;
  const dy = ty - p.y;
  const n = norm(dx,dy);

  // arrive scaling near target
  let desiredSpeed = MAX_SPEED;
  if (n.d < 120) desiredSpeed = MAX_SPEED * (n.d / 120);

  const desVX = n.x * desiredSpeed;
  const desVY = n.y * desiredSpeed;

  // steering = desired - current
  let sx = desVX - p.vx;
  let sy = desVY - p.vy;

  const lim = limit(sx, sy, MAX_FORCE);
  return { x: lim.x, y: lim.y };
}

// Obstacle avoidance force (repulsion from nearby obstacle edges)
function avoidForce(){
  let ax = 0, ay = 0;

  for (const o of obstacles){
    const dx = p.x - o.x;
    const dy = p.y - o.y;
    const d = Math.hypot(dx,dy);

    // influence if within look distance from obstacle edge
    const edge = d - o.r;
    if (edge > AVOID_LOOK) continue;

    // stronger when closer to edge; very strong when penetrating
    const dir = norm(dx,dy);
    const t = clamp(1 - (edge / AVOID_LOOK), 0, 1);

    // weight spikes if inside obstacle
    const insideBoost = edge < 0 ? 2.5 : 1.0;

    const mag = (AVOID_FORCE * t * t * insideBoost) / (edge*edge + AVOID_SOFT);

    ax += dir.x * mag;
    ay += dir.y * mag;
  }

  // limit avoidance so it doesn’t explode
  const lim = limit(ax, ay, MAX_FORCE * 3.0);
  return { x: lim.x, y: lim.y };
}

// Collision resolution: push player out of obstacles (hard collision)
function resolveCollisions(){
  for (const o of obstacles){
    const dx = p.x - o.x;
    const dy = p.y - o.y;
    const d = Math.hypot(dx,dy) || 0.0001;
    const minD = o.r + PR;

    if (d < minD){
      const nx = dx / d;
      const ny = dy / d;
      const push = (minD - d);

      // push position out
      p.x += nx * push;
      p.y += ny * push;

      // remove inward velocity component (slide)
      const vn = p.vx*nx + p.vy*ny;
      if (vn < 0){
        p.vx -= vn * nx;
        p.vy -= vn * ny;
      }
    }
  }
}

// ===== Update =====
function update(){
  // No waypoints => settle
  if (waypoints.length === 0){
    p.vx *= 0.95;
    p.vy *= 0.95;
  } else {
    // current waypoint
    const wp = waypoints[currentIdx];
    const dist = Math.hypot(wp.x - p.x, wp.y - p.y);

    // advance if reached
    if (dist <= ARRIVE_R){
      currentIdx++;
      if (currentIdx >= waypoints.length){
        // done: stop and clear route (keep last point visible? no, clear for POC clarity)
        waypoints.length = 0;
        currentIdx = 0;
      }
    }

    // compute steering (seek + avoid)
    if (waypoints.length > 0){
      const w = waypoints[currentIdx];
      const seek = seekForce(w.x, w.y);
      const avoid = avoidForce();

      // combine
      p.vx += seek.x + avoid.x;
      p.vy += seek.y + avoid.y;

      // speed limit
      const lim = limit(p.vx, p.vy, MAX_SPEED);
      p.vx = lim.x; p.vy = lim.y;

      // mild damping
      p.vx *= VEL_DAMP;
      p.vy *= VEL_DAMP;
    }
  }

  // integrate
  p.x += p.vx;
  p.y += p.vy;

  // world bounds + bounce a bit
  if (p.x < PR){ p.x = PR; p.vx *= -0.25; }
  if (p.x > W-PR){ p.x = W-PR; p.vx *= -0.25; }
  if (p.y < PR){ p.y = PR; p.vy *= -0.25; }
  if (p.y > H-PR){ p.y = H-PR; p.vy *= -0.25; }

  // hard collisions
  resolveCollisions();
}

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

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

    x.strokeStyle = "rgba(255,255,255,0.18)";
    x.lineWidth = 2;
    x.beginPath(); x.arc(o.x, o.y, o.r, 0, Math.PI*2); x.stroke();
  }

  // path polyline
  if (waypoints.length > 0){
    x.strokeStyle = "rgba(255,255,255,0.20)";
    x.lineWidth = 3;
    x.beginPath();
    x.moveTo(p.x, p.y);
    for (let i=currentIdx;i<waypoints.length;i++){
      x.lineTo(waypoints[i].x, waypoints[i].y);
    }
    x.stroke();

    // waypoints markers
    for (let i=0;i<waypoints.length;i++){
      const w = waypoints[i];
      const isCur = (i === currentIdx);

      x.strokeStyle = isCur ? "rgba(255,255,255,0.65)" : "rgba(255,255,255,0.30)";
      x.lineWidth = isCur ? 3 : 2;
      x.beginPath();
      x.arc(w.x, w.y, isCur ? 16 : 12, 0, Math.PI*2);
      x.stroke();

      x.fillStyle = isCur ? "rgba(255,255,255,0.65)" : "rgba(255,255,255,0.25)";
      x.beginPath();
      x.arc(w.x, w.y, 3, 0, Math.PI*2);
      x.fill();
    }
  }

  // player
  x.fillStyle = "#fff";
  x.beginPath(); x.arc(p.x, p.y, PR, 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(`waypoints=${waypoints.length}  current=${waypoints.length?currentIdx+1:0}  speed=${sp.toFixed(2)}`, 12, 24);
}

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