The demo below builds a probabilistic roadmap in a 2-D obstacle field. Click Build Roadmap to scatter random collision-free nodes and connect nearby pairs. Then click Find Path to run a shortest-path search from the green start to the red goal.
<!-- PRM demo layout - canvas for drawing, controls, and status line -->
<div class="controls">
<button id="btnBuild" type="button">Build Roadmap</button>
<button id="btnPath" type="button">Find Path</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="380" height="320"></canvas>
<div id="status" class="status">Click "Build Roadmap" to scatter random nodes and connect nearby pairs.</div>
/* PRM demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; display: block;
background: #f5f8fa; max-width: 100%; }
.status { font-size: .9rem; font-weight: 600; margin-top: .45rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Probabilistic Roadmap (PRM) - build a random graph through free space once, */
/* then answer many path queries cheaply with shortest-path search. */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
/* Rectangular obstacle definitions [x, y, width, height] */
const OBSTACLES = [
{ x: 60, y: 60, w: 70, h: 50 },
{ x: 200, y: 80, w: 60, h: 90 },
{ x: 80, y: 200, w: 80, h: 55 },
{ x: 270, y: 190, w: 75, h: 60 },
{ x: 155, y: 155, w: 50, h: 45 }
];
/* Fixed start (S) and goal (G) nodes - always added first as nodes[0] and nodes[1] */
const START = { x: 20, y: 20 };
const GOAL = { x: 355, y: 295 };
const MARGIN = 6; /* collision margin in pixels - keep nodes away from obstacle edges */
const SAMPLES = 120;
const CONN_RADIUS = 90; /* connect nodes within this pixel radius if the straight edge is clear */
/* Global roadmap state */
let nodes = [];
let edges = [];
let path = [];
let built = false;
/* Point-in-obstacle test with margin */
function hitRect(px, py, obs) {
return px >= obs.x - MARGIN && px <= obs.x + obs.w + MARGIN &&
py >= obs.y - MARGIN && py <= obs.y + obs.h + MARGIN;
}
function inObstacle(px, py) {
return OBSTACLES.some(o => hitRect(px, py, o));
}
/* Walk along the edge and reject if any sample point hits an obstacle */
function edgeClear(a, b) {
const steps = Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / 4);
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const x = a.x + (b.x - a.x) * t;
const y = a.y + (b.y - a.y) * t;
if (inObstacle(x, y)) return false;
}
return true;
}
/* Euclidean distance between two nodes */
function dist(a, b) {
return Math.hypot(b.x - a.x, b.y - a.y);
}
/* Build the roadmap - sample free-space nodes then connect nearby pairs */
function buildRoadmap() {
nodes = [START, GOAL];
edges = [];
path = [];
/* Sample random collision-free nodes */
let attempts = 0;
while (nodes.length < SAMPLES + 2 && attempts < SAMPLES * 10) {
attempts++;
const px = 5 + Math.random() * (W - 10);
const py = 5 + Math.random() * (H - 10);
if (!inObstacle(px, py)) nodes.push({ x: px, y: py });
}
/* Connect all pairs within CONN_RADIUS if the straight edge is clear */
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
if (dist(nodes[i], nodes[j]) <= CONN_RADIUS && edgeClear(nodes[i], nodes[j])) {
edges.push([i, j]);
}
}
}
built = true;
}
/* Dijkstra shortest-path on the roadmap graph */
function dijkstra() {
const n = nodes.length;
const adj = Array.from({ length: n }, () => []);
for (const [a, b] of edges) {
adj[a].push(b);
adj[b].push(a);
}
const INF = Infinity;
const d = Array(n).fill(INF);
const prev = Array(n).fill(-1);
d[0] = 0;
/* Simple priority queue (array sorted on demand - fine for < 200 nodes) */
const pq = [[0, 0]]; /* [cost, nodeIndex] */
while (pq.length) {
pq.sort((a, b) => a[0] - b[0]);
const [dd, u] = pq.shift();
if (dd > d[u]) continue;
for (const v of adj[u]) {
const nd = d[u] + dist(nodes[u], nodes[v]);
if (nd < d[v]) { d[v] = nd; prev[v] = u; pq.push([nd, v]); }
}
}
/* Reconstruct path from goal (node 1) back to start (node 0) */
if (d[1] === INF) return [];
const p = [];
for (let cur = 1; cur !== -1; cur = prev[cur]) p.unshift(cur);
return p;
}
/* Render the scene - obstacles, edges, nodes, path, start and goal */
function draw() {
ctx.clearRect(0, 0, W, H);
/* Draw obstacles as filled rectangles */
ctx.fillStyle = '#c8d6e0';
ctx.strokeStyle = '#8fa5b2';
ctx.lineWidth = 1;
for (const o of OBSTACLES) {
ctx.fillRect(o.x, o.y, o.w, o.h);
ctx.strokeRect(o.x, o.y, o.w, o.h);
}
if (built) {
/* Draw roadmap edges in light blue */
ctx.strokeStyle = 'rgba(100,140,180,0.35)';
ctx.lineWidth = 1;
for (const [a, b] of edges) {
ctx.beginPath();
ctx.moveTo(nodes[a].x, nodes[a].y);
ctx.lineTo(nodes[b].x, nodes[b].y);
ctx.stroke();
}
/* Draw sampled nodes as small circles */
ctx.fillStyle = 'rgba(80,120,180,0.65)';
for (let i = 2; i < nodes.length; i++) {
ctx.beginPath();
ctx.arc(nodes[i].x, nodes[i].y, 3, 0, Math.PI * 2);
ctx.fill();
}
}
/* Draw the found path in orange */
if (path.length > 1) {
ctx.strokeStyle = '#e07b00';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(nodes[path[0]].x, nodes[path[0]].y);
for (let i = 1; i < path.length; i++) {
ctx.lineTo(nodes[path[i]].x, nodes[path[i]].y);
}
ctx.stroke();
}
/* Draw start (green) and goal (red) markers */
ctx.fillStyle = '#1a9e4c';
ctx.beginPath();
ctx.arc(START.x, START.y, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#c92f3c';
ctx.beginPath();
ctx.arc(GOAL.x, GOAL.y, 7, 0, Math.PI * 2);
ctx.fill();
/* Label start S and goal G */
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('S', START.x, START.y);
ctx.fillText('G', GOAL.x, GOAL.y);
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
/* Build roadmap on button click */
document.getElementById('btnBuild').addEventListener('click', () => {
buildRoadmap();
draw();
setStatus(i18nFmt('Roadmap ready: %n% nodes, %e% edges.', { n: nodes.length - 2, e: edges.length }), 'info');
document.getElementById('btnPath').disabled = false;
});
/* Find shortest path on button click */
document.getElementById('btnPath').addEventListener('click', () => {
if (!built) return;
path = dijkstra();
draw();
if (path.length > 1) {
const hops = path.length - 1;
setStatus(i18nFmt('Path found in %hops% hops.', { hops: hops }), 'ok');
} else {
setStatus('No path found — try rebuilding the roadmap.', 'bad');
}
});
/* Reset everything on button click */
document.getElementById('btnReset').addEventListener('click', () => {
nodes = []; edges = []; path = []; built = false;
document.getElementById('btnPath').disabled = true;
draw();
setStatus('Click "Build Roadmap" to scatter random nodes and connect nearby pairs.', '');
});
/* Initialise - disable Find Path until roadmap is built */
document.getElementById('btnPath').disabled = true;
draw();
<!-- PRM demo layout - canvas for drawing, controls, and status line -->
<div class="controls">
<button id="btnBuild" type="button">Build Roadmap</button>
<button id="btnPath" type="button">Find Path</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="380" height="320"></canvas>
<div id="status" class="status">Click "Build Roadmap" to scatter random nodes and connect nearby pairs.</div>
/* PRM demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; display: block;
background: #f5f8fa; max-width: 100%; }
.status { font-size: .9rem; font-weight: 600; margin-top: .45rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Probabilistic Roadmap (PRM) - build a random graph through free space once, */
/* then answer many path queries cheaply with shortest-path search. */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
/* Rectangular obstacle definitions [x, y, width, height] */
const OBSTACLES = [
{ x: 60, y: 60, w: 70, h: 50 },
{ x: 200, y: 80, w: 60, h: 90 },
{ x: 80, y: 200, w: 80, h: 55 },
{ x: 270, y: 190, w: 75, h: 60 },
{ x: 155, y: 155, w: 50, h: 45 }
];
/* Fixed start (S) and goal (G) nodes - always added first as nodes[0] and nodes[1] */
const START = { x: 20, y: 20 };
const GOAL = { x: 355, y: 295 };
const MARGIN = 6; /* collision margin in pixels - keep nodes away from obstacle edges */
const SAMPLES = 120;
const CONN_RADIUS = 90; /* connect nodes within this pixel radius if the straight edge is clear */
/* Global roadmap state */
let nodes = [];
let edges = [];
let path = [];
let built = false;
/* Point-in-obstacle test with margin */
function hitRect(px, py, obs) {
return px >= obs.x - MARGIN && px <= obs.x + obs.w + MARGIN &&
py >= obs.y - MARGIN && py <= obs.y + obs.h + MARGIN;
}
function inObstacle(px, py) {
return OBSTACLES.some(o => hitRect(px, py, o));
}
/* Walk along the edge and reject if any sample point hits an obstacle */
function edgeClear(a, b) {
const steps = Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / 4);
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const x = a.x + (b.x - a.x) * t;
const y = a.y + (b.y - a.y) * t;
if (inObstacle(x, y)) return false;
}
return true;
}
/* Euclidean distance between two nodes */
function dist(a, b) {
return Math.hypot(b.x - a.x, b.y - a.y);
}
/* Build the roadmap - sample free-space nodes then connect nearby pairs */
function buildRoadmap() {
nodes = [START, GOAL];
edges = [];
path = [];
/* Sample random collision-free nodes */
let attempts = 0;
while (nodes.length < SAMPLES + 2 && attempts < SAMPLES * 10) {
attempts++;
const px = 5 + Math.random() * (W - 10);
const py = 5 + Math.random() * (H - 10);
if (!inObstacle(px, py)) nodes.push({ x: px, y: py });
}
/* Connect all pairs within CONN_RADIUS if the straight edge is clear */
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
if (dist(nodes[i], nodes[j]) <= CONN_RADIUS && edgeClear(nodes[i], nodes[j])) {
edges.push([i, j]);
}
}
}
built = true;
}
/* Dijkstra shortest-path on the roadmap graph */
function dijkstra() {
const n = nodes.length;
const adj = Array.from({ length: n }, () => []);
for (const [a, b] of edges) {
adj[a].push(b);
adj[b].push(a);
}
const INF = Infinity;
const d = Array(n).fill(INF);
const prev = Array(n).fill(-1);
d[0] = 0;
/* Simple priority queue (array sorted on demand - fine for < 200 nodes) */
const pq = [[0, 0]]; /* [cost, nodeIndex] */
while (pq.length) {
pq.sort((a, b) => a[0] - b[0]);
const [dd, u] = pq.shift();
if (dd > d[u]) continue;
for (const v of adj[u]) {
const nd = d[u] + dist(nodes[u], nodes[v]);
if (nd < d[v]) { d[v] = nd; prev[v] = u; pq.push([nd, v]); }
}
}
/* Reconstruct path from goal (node 1) back to start (node 0) */
if (d[1] === INF) return [];
const p = [];
for (let cur = 1; cur !== -1; cur = prev[cur]) p.unshift(cur);
return p;
}
/* Render the scene - obstacles, edges, nodes, path, start and goal */
function draw() {
ctx.clearRect(0, 0, W, H);
/* Draw obstacles as filled rectangles */
ctx.fillStyle = '#c8d6e0';
ctx.strokeStyle = '#8fa5b2';
ctx.lineWidth = 1;
for (const o of OBSTACLES) {
ctx.fillRect(o.x, o.y, o.w, o.h);
ctx.strokeRect(o.x, o.y, o.w, o.h);
}
if (built) {
/* Draw roadmap edges in light blue */
ctx.strokeStyle = 'rgba(100,140,180,0.35)';
ctx.lineWidth = 1;
for (const [a, b] of edges) {
ctx.beginPath();
ctx.moveTo(nodes[a].x, nodes[a].y);
ctx.lineTo(nodes[b].x, nodes[b].y);
ctx.stroke();
}
/* Draw sampled nodes as small circles */
ctx.fillStyle = 'rgba(80,120,180,0.65)';
for (let i = 2; i < nodes.length; i++) {
ctx.beginPath();
ctx.arc(nodes[i].x, nodes[i].y, 3, 0, Math.PI * 2);
ctx.fill();
}
}
/* Draw the found path in orange */
if (path.length > 1) {
ctx.strokeStyle = '#e07b00';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(nodes[path[0]].x, nodes[path[0]].y);
for (let i = 1; i < path.length; i++) {
ctx.lineTo(nodes[path[i]].x, nodes[path[i]].y);
}
ctx.stroke();
}
/* Draw start (green) and goal (red) markers */
ctx.fillStyle = '#1a9e4c';
ctx.beginPath();
ctx.arc(START.x, START.y, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#c92f3c';
ctx.beginPath();
ctx.arc(GOAL.x, GOAL.y, 7, 0, Math.PI * 2);
ctx.fill();
/* Label start S and goal G */
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('S', START.x, START.y);
ctx.fillText('G', GOAL.x, GOAL.y);
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
/* Build roadmap on button click */
document.getElementById('btnBuild').addEventListener('click', () => {
buildRoadmap();
draw();
setStatus(i18nFmt('Roadmap ready: %n% nodes, %e% edges.', { n: nodes.length - 2, e: edges.length }), 'info');
document.getElementById('btnPath').disabled = false;
});
/* Find shortest path on button click */
document.getElementById('btnPath').addEventListener('click', () => {
if (!built) return;
path = dijkstra();
draw();
if (path.length > 1) {
const hops = path.length - 1;
setStatus(i18nFmt('Path found in %hops% hops.', { hops: hops }), 'ok');
} else {
setStatus('No path found — try rebuilding the roadmap.', 'bad');
}
});
/* Reset everything on button click */
document.getElementById('btnReset').addEventListener('click', () => {
nodes = []; edges = []; path = []; built = false;
document.getElementById('btnPath').disabled = true;
draw();
setStatus('Click "Build Roadmap" to scatter random nodes and connect nearby pairs.', '');
});
/* Initialise - disable Find Path until roadmap is built */
document.getElementById('btnPath').disabled = true;
draw();
Notice the asymmetry. Building the roadmap takes a moment — each new node is tested for collisions and each candidate edge is checked along its length. Querying a path is then nearly instant: the roadmap is a plain graph and any shortest-path algorithm handles it in milliseconds. Build once, query many times.
Comments
Loading comments...