Click to aim; a laser ray bounces off circular obstacles with correct reflection angles for a clear collision/physics visual.


Code for Above

<div style="max-width:1500px;margin:0 auto;">
<iframe
  title="POC — Ricochet Laser (Reflection)"
  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 — Ricochet Laser (Reflection)</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">Move mouse: aim • Click: lock aim • Laser bounces off circles</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)
  };
}

function rand(a,b){ return a + Math.random()*(b-a); }

const origin = { x: W/2, y: H/2 };
let aim = { x: W*0.75, y: H*0.35 };
let locked = false;

const obstacles = Array.from({length: 10}, () => ({
  x: rand(180, W-180),
  y: rand(180, H-180),
  r: rand(55, 120)
}));

c.addEventListener("mousemove", (e) => {
  if (locked) return;
  const p = pos(e);
  aim.x = p.x; aim.y = p.y;
});
c.addEventListener("click", () => locked = !locked);

// Ray-circle intersection: returns nearest hit t, point, normal (unit)
function rayCircleHit(ox, oy, dx, dy, cx, cy, r){
  // solve |(o + d t) - c|^2 = r^2
  const fx = ox - cx;
  const fy = oy - cy;

  const a = dx*dx + dy*dy;
  const b = 2*(fx*dx + fy*dy);
  const c2 = fx*fx + fy*fy - r*r;

  const disc = b*b - 4*a*c2;
  if (disc < 0) return null;

  const s = Math.sqrt(disc);
  const t1 = (-b - s) / (2*a);
  const t2 = (-b + s) / (2*a);

  const t = (t1 > 0.0001) ? t1 : (t2 > 0.0001 ? t2 : null);
  if (t === null) return null;

  const hx = ox + dx*t;
  const hy = oy + dy*t;

  // normal from circle center to hit
  let nx = hx - cx;
  let ny = hy - cy;
  const nd = Math.hypot(nx, ny) || 1;
  nx /= nd; ny /= nd;

  return { t, hx, hy, nx, ny };
}

function reflect(dx, dy, nx, ny){
  // r = d - 2(d·n)n
  const dot = dx*nx + dy*ny;
  return { rx: dx - 2*dot*nx, ry: dy - 2*dot*ny };
}

function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }

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

  // laser setup
  let dx = aim.x - origin.x;
  let dy = aim.y - origin.y;
  const dlen = Math.hypot(dx, dy) || 1;
  dx /= dlen; dy /= dlen;

  const MAX_BOUNCES = 6;
  const MAX_LEN = 2400;

  let ox = origin.x, oy = origin.y;
  let remaining = MAX_LEN;

  x.strokeStyle = "rgba(255,255,255,0.55)";
  x.lineWidth = 3;
  x.beginPath();
  x.moveTo(ox, oy);

  // trace with bounces
  for (let b=0; b<=MAX_BOUNCES; b++){
    let best = null;

    // find nearest hit among circles
    for (const o of obstacles){
      const hit = rayCircleHit(ox, oy, dx, dy, o.x, o.y, o.r);
      if (!hit) continue;

      // distance along ray in pixels since dx,dy unit
      const dist = hit.t;
      if (dist > remaining) continue;

      if (!best || dist < best.dist){
        best = { dist, ...hit };
      }
    }

    if (!best){
      // no hit: go to end
      const ex = ox + dx*remaining;
      const ey = oy + dy*remaining;
      x.lineTo(ex, ey);
      remaining = 0;
      break;
    } else {
      // hit point
      x.lineTo(best.hx, best.hy);

      // reflect direction
      const r = reflect(dx, dy, best.nx, best.ny);
      dx = r.rx; dy = r.ry;

      // move origin slightly off surface to avoid re-hitting
      ox = best.hx + dx*0.5;
      oy = best.hy + dy*0.5;

      remaining -= best.dist;

      // draw hit marker
      x.stroke();
      x.fillStyle = "rgba(255,255,255,0.75)";
      x.beginPath();
      x.arc(best.hx, best.hy, 4, 0, Math.PI*2);
      x.fill();

      // continue line
      x.beginPath();
      x.moveTo(best.hx, best.hy);
    }

    if (remaining <= 0) break;
  }

  x.stroke();

  // origin marker
  x.fillStyle = "#fff";
  x.beginPath();
  x.arc(origin.x, origin.y, 7, 0, Math.PI*2);
  x.fill();

  // aim marker
  x.strokeStyle = locked ? "rgba(255,255,255,0.65)" : "rgba(255,255,255,0.25)";
  x.lineWidth = 2;
  x.beginPath();
  x.arc(aim.x, aim.y, 10, 0, Math.PI*2);
  x.stroke();

  // HUD
  x.fillStyle = "rgba(255,255,255,0.6)";
  x.font = "14px system-ui";
  x.fillText(`locked=${locked}  bounces<=${MAX_BOUNCES}`, 12, 24);

  requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
'></iframe>
</div>