The car starts at the left, facing right. Click anywhere to set a new target parking spot (position and heading). The demo computes all six Dubins path candidates and draws the shortest one.
<!-- Dubins path demo — canvas with start/goal arrows and the optimal path -->
<p class="hint">A car cannot reverse and has a minimum turning radius. Click anywhere on the canvas to set the goal (position + heading). The demo finds the shortest Dubins path — always one of six three-segment types.</p>
<canvas id="canvas" width="480" height="320"></canvas>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-random" type="button">Random goal</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Layout for Dubins demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f4f7fa; cursor: crosshair; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Dubins shortest-path solver + canvas renderer */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
/* Minimum turning radius in canvas pixels */
const R = 60;
/* Initial car state (start) and goal state */
let start = { x: 80, y: H / 2, th: 0 };
let goal = { x: W - 80, y: H / 2, th: 0 };
let best = null;
/* ── Normalize angle to [0, 2*pi) ── */
function mod2pi(a) { return ((a % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI); }
/* ── LSL candidate (Left arc - Straight - Left arc) ── */
function dubinsLSL(dx, dy, d, sa, ca, sb, cb) {
const p2 = 2 + d * d - 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sa) - Math.sin(sb));
if (p2 < 0) return null;
const tmp = Math.atan2(cb - ca, d + sa - sb);
const t = mod2pi(-sa + tmp);
const p = Math.sqrt(p2);
const q = mod2pi(sb - tmp);
return { t, p, q, type: 'LSL', len: t + p + q };
}
/* ── RSR candidate (Right arc - Straight - Right arc) ── */
function dubinsRSR(dx, dy, d, sa, ca, sb, cb) {
const p2 = 2 + d * d - 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sb) - Math.sin(sa));
if (p2 < 0) return null;
const tmp = Math.atan2(ca - cb, d - sa + sb);
const t = mod2pi(sa - tmp);
const p = Math.sqrt(p2);
const q = mod2pi(-sb + tmp);
return { t, p, q, type: 'RSR', len: t + p + q };
}
/* ── LSR candidate (Left arc - Straight - Right arc) ── */
function dubinsLSR(dx, dy, d, sa, ca, sb, cb) {
const p2 = -2 + d * d + 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sa) + Math.sin(sb));
if (p2 < 0) return null;
const p = Math.sqrt(p2);
const tmp = Math.atan2(-ca - cb, d + sa + sb) - Math.atan2(-2, p);
const t = mod2pi(-sa + tmp);
const q = mod2pi(-sb + tmp);
return { t, p, q, type: 'LSR', len: t + p + q };
}
/* ── RSL candidate (Right arc - Straight - Left arc) ── */
function dubinsRSL(dx, dy, d, sa, ca, sb, cb) {
const p2 = -2 + d * d + 2 * Math.cos(sa - sb) - 2 * d * (Math.sin(sa) + Math.sin(sb));
if (p2 < 0) return null;
const p = Math.sqrt(p2);
const tmp = Math.atan2(ca + cb, d - sa - sb) - Math.atan2(2, p);
const t = mod2pi(sa - tmp);
const q = mod2pi(sb - tmp);
return { t, p, q, type: 'RSL', len: t + p + q };
}
/* ── LRL candidate (Left - Right - Left, no straight) ── */
function dubinsLRL(dx, dy, d, sa, ca, sb, cb) {
const tmp = (6 - d * d + 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sa) - Math.sin(sb))) / 8;
if (Math.abs(tmp) > 1) return null;
const p = mod2pi(2 * Math.PI - Math.acos(tmp));
const t = mod2pi(-sa + Math.atan2(ca - cb, d - sa + sb) + p / 2);
const q = mod2pi(sa - sb - t + p);
return { t, p, q, type: 'LRL', len: t + p + q };
}
/* ── RLR candidate (Right - Left - Right, no straight) ── */
function dubinsRLR(dx, dy, d, sa, ca, sb, cb) {
const tmp = (6 - d * d + 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sb) - Math.sin(sa))) / 8;
if (Math.abs(tmp) > 1) return null;
const p = mod2pi(2 * Math.PI - Math.acos(tmp));
const t = mod2pi(sa - Math.atan2(ca - cb, d + sa - sb) + p / 2);
const q = mod2pi(sa - sb - t + p);
return { t, p, q, type: 'RLR', len: t + p + q };
}
/* ── Normalize to unit-radius frame, compute all six candidates, return shortest ── */
function computeDubins(s, g) {
const dx = (g.x - s.x) / R, dy = (g.y - s.y) / R;
const d = Math.hypot(dx, dy);
const th = Math.atan2(dy, dx);
const sa = mod2pi(s.th - th), sb = mod2pi(g.th - th);
const ca = Math.cos(sa), cb = Math.cos(sb);
const candidates = [
dubinsLSL(dx, dy, d, sa, ca, sb, cb),
dubinsRSR(dx, dy, d, sa, ca, sb, cb),
dubinsLSR(dx, dy, d, sa, ca, sb, cb),
dubinsRSL(dx, dy, d, sa, ca, sb, cb),
dubinsLRL(dx, dy, d, sa, ca, sb, cb),
dubinsRLR(dx, dy, d, sa, ca, sb, cb),
].filter(Boolean);
if (!candidates.length) return null;
return candidates.reduce((a, b) => (a.len < b.len ? a : b));
}
/* ── Sample points along a circular arc for drawing ── */
function sampleArc(cx, cy, radius, a0, a1, left, pts) {
const steps = Math.max(2, Math.ceil(Math.abs(a1 - a0) / (Math.PI / 60)));
for (let i = 0; i <= steps; i++) {
const a = a0 + (a1 - a0) * (i / steps);
pts.push({ x: cx + radius * Math.cos(a), y: cy + radius * Math.sin(a) });
}
}
/* ── Walk the three segments of the winning path and collect canvas points ── */
function dubinsPoints(s, path) {
const pts = [];
let cx = s.x, cy = s.y, cth = s.th;
function doSeg(type, len) {
if (len < 1e-9) return;
const arcLen = len * R;
if (type === 'S') {
pts.push({ x: cx, y: cy });
cx += arcLen * Math.cos(cth);
cy += arcLen * Math.sin(cth);
pts.push({ x: cx, y: cy });
} else {
const left = (type === 'L');
const sign = left ? 1 : -1;
/* Center of the turning circle */
const ocx = cx - sign * R * Math.sin(cth);
const ocy = cy + sign * R * Math.cos(cth);
const a0 = Math.atan2(cy - ocy, cx - ocx);
const dAngle = sign * len;
const a1 = a0 + dAngle;
sampleArc(ocx, ocy, R, a0, a1, left, pts);
cx = ocx + R * Math.cos(a1);
cy = ocy + R * Math.sin(a1);
cth += dAngle;
}
}
const t = path.type;
/* Dispatch to the correct segment order based on path type */
if (t === 'LSL') { doSeg('L', path.t); doSeg('S', path.p); doSeg('L', path.q); }
else if (t === 'RSR') { doSeg('R', path.t); doSeg('S', path.p); doSeg('R', path.q); }
else if (t === 'LSR') { doSeg('L', path.t); doSeg('S', path.p); doSeg('R', path.q); }
else if (t === 'RSL') { doSeg('R', path.t); doSeg('S', path.p); doSeg('L', path.q); }
else if (t === 'LRL') { doSeg('L', path.t); doSeg('R', path.p); doSeg('L', path.q); }
else if (t === 'RLR') { doSeg('R', path.t); doSeg('L', path.p); doSeg('R', path.q); }
return pts;
}
/* ── Draw a direction arrow with optional label ── */
function drawArrow(x, y, th, color, label) {
const len = 18, hw = 6;
const ex = x + len * Math.cos(th), ey = y + len * Math.sin(th);
ctx.save();
ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(ex, ey); ctx.stroke();
/* Draw filled arrowhead */
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ex - hw * Math.cos(th - 0.4), ey - hw * Math.sin(th - 0.4));
ctx.lineTo(ex - hw * Math.cos(th + 0.4), ey - hw * Math.sin(th + 0.4));
ctx.closePath(); ctx.fill();
if (label) {
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.fillText(label, x + 2, y - 8);
}
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
/* Light background grid */
ctx.save();
ctx.strokeStyle = '#dde4ec'; ctx.lineWidth = 1;
for (let x = 0; x < W; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
for (let y = 0; y < H; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
ctx.restore();
/* Draw the optimal Dubins path */
if (best) {
const pts = dubinsPoints(start, best);
ctx.save();
ctx.strokeStyle = '#2d6a4f'; ctx.lineWidth = 3;
ctx.setLineDash([]);
ctx.beginPath();
pts.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
ctx.stroke();
ctx.restore();
}
/* Show minimum turning radius circle around start */
ctx.save();
ctx.strokeStyle = '#b0c4d8'; ctx.lineWidth = 1; ctx.setLineDash([4, 4]);
ctx.beginPath(); ctx.arc(start.x, start.y, R, 0, 2 * Math.PI); ctx.stroke();
ctx.restore();
/* Draw start (blue) and goal (red) arrows */
drawArrow(start.x, start.y, start.th, '#1d3557', 'Start');
drawArrow(goal.x, goal.y, goal.th, '#e63946', 'Goal');
}
function updateStatus() {
const el = document.getElementById('status');
if (!best) { el.textContent = 'No valid path found.'; return; }
const lenPx = Math.round(best.len * R);
el.textContent = i18nFmt('Shortest path: %type% — length %len% px', { type: best.type, len: lenPx });
}
function recompute() {
best = computeDubins(start, goal);
draw();
updateStatus();
}
/* ── Click sets new goal; heading points away from previous goal position ── */
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
/* Aim the goal arrow in the direction of the click movement */
const dx = mx - goal.x, dy = my - goal.y;
goal = { x: mx, y: my, th: Math.atan2(dy, dx) };
recompute();
});
document.getElementById('btn-random').addEventListener('click', function() {
/* Place a random goal anywhere on the canvas with a random heading */
goal = {
x: 60 + Math.random() * (W - 120),
y: 40 + Math.random() * (H - 80),
th: (Math.random() * 2 - 1) * Math.PI
};
recompute();
});
document.getElementById('btn-reset').addEventListener('click', function() {
goal = { x: W - 80, y: H / 2, th: 0 };
recompute();
});
recompute();
<!-- Dubins path demo — canvas with start/goal arrows and the optimal path -->
<p class="hint">A car cannot reverse and has a minimum turning radius. Click anywhere on the canvas to set the goal (position + heading). The demo finds the shortest Dubins path — always one of six three-segment types.</p>
<canvas id="canvas" width="480" height="320"></canvas>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-random" type="button">Random goal</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Layout for Dubins demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f4f7fa; cursor: crosshair; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Dubins shortest-path solver + canvas renderer */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
/* Minimum turning radius in canvas pixels */
const R = 60;
/* Initial car state (start) and goal state */
let start = { x: 80, y: H / 2, th: 0 };
let goal = { x: W - 80, y: H / 2, th: 0 };
let best = null;
/* ── Normalize angle to [0, 2*pi) ── */
function mod2pi(a) { return ((a % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI); }
/* ── LSL candidate (Left arc - Straight - Left arc) ── */
function dubinsLSL(dx, dy, d, sa, ca, sb, cb) {
const p2 = 2 + d * d - 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sa) - Math.sin(sb));
if (p2 < 0) return null;
const tmp = Math.atan2(cb - ca, d + sa - sb);
const t = mod2pi(-sa + tmp);
const p = Math.sqrt(p2);
const q = mod2pi(sb - tmp);
return { t, p, q, type: 'LSL', len: t + p + q };
}
/* ── RSR candidate (Right arc - Straight - Right arc) ── */
function dubinsRSR(dx, dy, d, sa, ca, sb, cb) {
const p2 = 2 + d * d - 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sb) - Math.sin(sa));
if (p2 < 0) return null;
const tmp = Math.atan2(ca - cb, d - sa + sb);
const t = mod2pi(sa - tmp);
const p = Math.sqrt(p2);
const q = mod2pi(-sb + tmp);
return { t, p, q, type: 'RSR', len: t + p + q };
}
/* ── LSR candidate (Left arc - Straight - Right arc) ── */
function dubinsLSR(dx, dy, d, sa, ca, sb, cb) {
const p2 = -2 + d * d + 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sa) + Math.sin(sb));
if (p2 < 0) return null;
const p = Math.sqrt(p2);
const tmp = Math.atan2(-ca - cb, d + sa + sb) - Math.atan2(-2, p);
const t = mod2pi(-sa + tmp);
const q = mod2pi(-sb + tmp);
return { t, p, q, type: 'LSR', len: t + p + q };
}
/* ── RSL candidate (Right arc - Straight - Left arc) ── */
function dubinsRSL(dx, dy, d, sa, ca, sb, cb) {
const p2 = -2 + d * d + 2 * Math.cos(sa - sb) - 2 * d * (Math.sin(sa) + Math.sin(sb));
if (p2 < 0) return null;
const p = Math.sqrt(p2);
const tmp = Math.atan2(ca + cb, d - sa - sb) - Math.atan2(2, p);
const t = mod2pi(sa - tmp);
const q = mod2pi(sb - tmp);
return { t, p, q, type: 'RSL', len: t + p + q };
}
/* ── LRL candidate (Left - Right - Left, no straight) ── */
function dubinsLRL(dx, dy, d, sa, ca, sb, cb) {
const tmp = (6 - d * d + 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sa) - Math.sin(sb))) / 8;
if (Math.abs(tmp) > 1) return null;
const p = mod2pi(2 * Math.PI - Math.acos(tmp));
const t = mod2pi(-sa + Math.atan2(ca - cb, d - sa + sb) + p / 2);
const q = mod2pi(sa - sb - t + p);
return { t, p, q, type: 'LRL', len: t + p + q };
}
/* ── RLR candidate (Right - Left - Right, no straight) ── */
function dubinsRLR(dx, dy, d, sa, ca, sb, cb) {
const tmp = (6 - d * d + 2 * Math.cos(sa - sb) + 2 * d * (Math.sin(sb) - Math.sin(sa))) / 8;
if (Math.abs(tmp) > 1) return null;
const p = mod2pi(2 * Math.PI - Math.acos(tmp));
const t = mod2pi(sa - Math.atan2(ca - cb, d + sa - sb) + p / 2);
const q = mod2pi(sa - sb - t + p);
return { t, p, q, type: 'RLR', len: t + p + q };
}
/* ── Normalize to unit-radius frame, compute all six candidates, return shortest ── */
function computeDubins(s, g) {
const dx = (g.x - s.x) / R, dy = (g.y - s.y) / R;
const d = Math.hypot(dx, dy);
const th = Math.atan2(dy, dx);
const sa = mod2pi(s.th - th), sb = mod2pi(g.th - th);
const ca = Math.cos(sa), cb = Math.cos(sb);
const candidates = [
dubinsLSL(dx, dy, d, sa, ca, sb, cb),
dubinsRSR(dx, dy, d, sa, ca, sb, cb),
dubinsLSR(dx, dy, d, sa, ca, sb, cb),
dubinsRSL(dx, dy, d, sa, ca, sb, cb),
dubinsLRL(dx, dy, d, sa, ca, sb, cb),
dubinsRLR(dx, dy, d, sa, ca, sb, cb),
].filter(Boolean);
if (!candidates.length) return null;
return candidates.reduce((a, b) => (a.len < b.len ? a : b));
}
/* ── Sample points along a circular arc for drawing ── */
function sampleArc(cx, cy, radius, a0, a1, left, pts) {
const steps = Math.max(2, Math.ceil(Math.abs(a1 - a0) / (Math.PI / 60)));
for (let i = 0; i <= steps; i++) {
const a = a0 + (a1 - a0) * (i / steps);
pts.push({ x: cx + radius * Math.cos(a), y: cy + radius * Math.sin(a) });
}
}
/* ── Walk the three segments of the winning path and collect canvas points ── */
function dubinsPoints(s, path) {
const pts = [];
let cx = s.x, cy = s.y, cth = s.th;
function doSeg(type, len) {
if (len < 1e-9) return;
const arcLen = len * R;
if (type === 'S') {
pts.push({ x: cx, y: cy });
cx += arcLen * Math.cos(cth);
cy += arcLen * Math.sin(cth);
pts.push({ x: cx, y: cy });
} else {
const left = (type === 'L');
const sign = left ? 1 : -1;
/* Center of the turning circle */
const ocx = cx - sign * R * Math.sin(cth);
const ocy = cy + sign * R * Math.cos(cth);
const a0 = Math.atan2(cy - ocy, cx - ocx);
const dAngle = sign * len;
const a1 = a0 + dAngle;
sampleArc(ocx, ocy, R, a0, a1, left, pts);
cx = ocx + R * Math.cos(a1);
cy = ocy + R * Math.sin(a1);
cth += dAngle;
}
}
const t = path.type;
/* Dispatch to the correct segment order based on path type */
if (t === 'LSL') { doSeg('L', path.t); doSeg('S', path.p); doSeg('L', path.q); }
else if (t === 'RSR') { doSeg('R', path.t); doSeg('S', path.p); doSeg('R', path.q); }
else if (t === 'LSR') { doSeg('L', path.t); doSeg('S', path.p); doSeg('R', path.q); }
else if (t === 'RSL') { doSeg('R', path.t); doSeg('S', path.p); doSeg('L', path.q); }
else if (t === 'LRL') { doSeg('L', path.t); doSeg('R', path.p); doSeg('L', path.q); }
else if (t === 'RLR') { doSeg('R', path.t); doSeg('L', path.p); doSeg('R', path.q); }
return pts;
}
/* ── Draw a direction arrow with optional label ── */
function drawArrow(x, y, th, color, label) {
const len = 18, hw = 6;
const ex = x + len * Math.cos(th), ey = y + len * Math.sin(th);
ctx.save();
ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(ex, ey); ctx.stroke();
/* Draw filled arrowhead */
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ex - hw * Math.cos(th - 0.4), ey - hw * Math.sin(th - 0.4));
ctx.lineTo(ex - hw * Math.cos(th + 0.4), ey - hw * Math.sin(th + 0.4));
ctx.closePath(); ctx.fill();
if (label) {
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.fillText(label, x + 2, y - 8);
}
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
/* Light background grid */
ctx.save();
ctx.strokeStyle = '#dde4ec'; ctx.lineWidth = 1;
for (let x = 0; x < W; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
for (let y = 0; y < H; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
ctx.restore();
/* Draw the optimal Dubins path */
if (best) {
const pts = dubinsPoints(start, best);
ctx.save();
ctx.strokeStyle = '#2d6a4f'; ctx.lineWidth = 3;
ctx.setLineDash([]);
ctx.beginPath();
pts.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
ctx.stroke();
ctx.restore();
}
/* Show minimum turning radius circle around start */
ctx.save();
ctx.strokeStyle = '#b0c4d8'; ctx.lineWidth = 1; ctx.setLineDash([4, 4]);
ctx.beginPath(); ctx.arc(start.x, start.y, R, 0, 2 * Math.PI); ctx.stroke();
ctx.restore();
/* Draw start (blue) and goal (red) arrows */
drawArrow(start.x, start.y, start.th, '#1d3557', 'Start');
drawArrow(goal.x, goal.y, goal.th, '#e63946', 'Goal');
}
function updateStatus() {
const el = document.getElementById('status');
if (!best) { el.textContent = 'No valid path found.'; return; }
const lenPx = Math.round(best.len * R);
el.textContent = i18nFmt('Shortest path: %type% — length %len% px', { type: best.type, len: lenPx });
}
function recompute() {
best = computeDubins(start, goal);
draw();
updateStatus();
}
/* ── Click sets new goal; heading points away from previous goal position ── */
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
/* Aim the goal arrow in the direction of the click movement */
const dx = mx - goal.x, dy = my - goal.y;
goal = { x: mx, y: my, th: Math.atan2(dy, dx) };
recompute();
});
document.getElementById('btn-random').addEventListener('click', function() {
/* Place a random goal anywhere on the canvas with a random heading */
goal = {
x: 60 + Math.random() * (W - 120),
y: 40 + Math.random() * (H - 80),
th: (Math.random() * 2 - 1) * Math.PI
};
recompute();
});
document.getElementById('btn-reset').addEventListener('click', function() {
goal = { x: W - 80, y: H / 2, th: 0 };
recompute();
});
recompute();
Comments
Loading comments...