<p class="hint">Drag the <b style="color:#e07b39">orange robot</b> around the canvas. The
<b style="color:#c0392b">red shaded region</b> is the Minkowski sum of the obstacle and the
robot — the set of positions the robot's center must avoid. When the center is outside the
red zone the robot is guaranteed collision-free.</p>
<canvas id="canvas" width="420" height="320"></canvas>
<div class="info" id="info">Drag the robot!</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f8f9fa; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.5; }
canvas { display: block; border: 1.5px solid #cdd; border-radius: 10px;
background: #fff; cursor: grab; touch-action: none; max-width: 100%; }
canvas:active { cursor: grabbing; }
.info { font-size: .9rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; color: #333; }
.safe { color: #0a7d33; }
.collide{ color: #c0392b; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Minkowski sum demo: robot (square) navigating near an obstacle (square).
// The Minkowski sum of obstacle O and robot R (reflected) gives the C-obstacle:
// the set of robot-center positions that cause a collision.
// For axis-aligned rectangles this is just another rectangle:
// C-obstacle top-left = O.top-left - R.half
// "Collision" iff robot center is inside C-obstacle.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Obstacle: fixed blue square (center, half-width)
const OBS = { cx: 210, cy: 160, hw: 60 };
// Robot: orange square (half-width = 22)
const ROB_HW = 22;
let rob = { cx: 80, cy: 80 };
// C-obstacle (Minkowski sum of obstacle with robot reflected = robot shape):
// For equal axis-aligned squares: C-obs is centered at OBS center,
// half-width = OBS.hw + ROB_HW
const COBS_HW = OBS.hw + ROB_HW;
function inCObs(cx, cy) {
return Math.abs(cx - OBS.cx) < COBS_HW && Math.abs(cy - OBS.cy) < COBS_HW;
}
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw C-obstacle (Minkowski sum region) — filled red, semi-transparent
ctx.save();
ctx.fillStyle = 'rgba(192,57,43,0.18)';
ctx.strokeStyle = 'rgba(192,57,43,0.55)';
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 4]);
ctx.beginPath();
ctx.rect(OBS.cx - COBS_HW, OBS.cy - COBS_HW, COBS_HW * 2, COBS_HW * 2);
ctx.fill();
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
// Label for C-obstacle
ctx.save();
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = 'rgba(192,57,43,0.85)';
ctx.fillText('C-obstacle (Minkowski sum)', OBS.cx - COBS_HW + 4, OBS.cy - COBS_HW - 5);
ctx.restore();
// Draw obstacle (blue filled square)
ctx.save();
ctx.fillStyle = 'rgba(41,128,185,0.22)';
ctx.strokeStyle = '#2980b9';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.rect(OBS.cx - OBS.hw, OBS.cy - OBS.hw, OBS.hw * 2, OBS.hw * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
// Obstacle label
ctx.save();
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.fillStyle = '#2980b9';
ctx.textAlign = 'center';
ctx.fillText('Obstacle', OBS.cx, OBS.cy + 4);
ctx.restore();
// Draw robot (orange square)
const colliding = inCObs(rob.cx, rob.cy);
ctx.save();
ctx.fillStyle = colliding ? 'rgba(231,76,60,0.6)' : 'rgba(224,123,57,0.65)';
ctx.strokeStyle = colliding ? '#c0392b' : '#e07b39';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.rect(rob.cx - ROB_HW, rob.cy - ROB_HW, ROB_HW * 2, ROB_HW * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
// Robot label
ctx.save();
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.fillStyle = colliding ? '#c0392b' : '#8b4513';
ctx.textAlign = 'center';
ctx.fillText('Robot', rob.cx, rob.cy + 4);
ctx.restore();
// Center dot on robot
ctx.save();
ctx.beginPath();
ctx.arc(rob.cx, rob.cy, 3, 0, Math.PI * 2);
ctx.fillStyle = colliding ? '#c0392b' : '#8b4513';
ctx.fill();
ctx.restore();
// Arrow from robot center to C-obs boundary (nearest point) when outside
if (!colliding) {
const dx = clamp(rob.cx, OBS.cx - COBS_HW, OBS.cx + COBS_HW) - rob.cx;
const dy = clamp(rob.cy, OBS.cy - COBS_HW, OBS.cy + COBS_HW) - rob.cy;
const dist = Math.sqrt(dx * dx + dy * dy);
ctx.save();
ctx.strokeStyle = 'rgba(10,125,51,0.5)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.moveTo(rob.cx, rob.cy);
ctx.lineTo(rob.cx + dx, rob.cy + dy);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
// Distance label
ctx.save();
ctx.font = '10px system-ui, sans-serif';
ctx.fillStyle = 'rgba(10,125,51,0.8)';
ctx.fillText(fmt('d=%round%px', { round: Math.round(dist) }), rob.cx + dx / 2 + 4, rob.cy + dy / 2 - 3);
ctx.restore();
}
// Status
const info = document.getElementById('info');
if (colliding) {
info.textContent = '✗ Collision! Robot center is inside the C-obstacle.';
info.className = 'info collide';
} else {
const dx = clamp(rob.cx, OBS.cx - COBS_HW, OBS.cx + COBS_HW) - rob.cx;
const dy = clamp(rob.cy, OBS.cy - COBS_HW, OBS.cy + COBS_HW) - rob.cy;
const dist = Math.round(Math.sqrt(dx * dx + dy * dy));
info.textContent = fmt('✓ Safe — robot center is %dist%px from the C-obstacle boundary.', { dist: dist });
info.className = 'info safe';
}
}
// Drag handling (mouse + touch)
let dragging = false;
function getPos(e) {
const r = canvas.getBoundingClientRect();
const scaleX = W / r.width, scaleY = H / r.height;
if (e.touches) {
return { x: (e.touches[0].clientX - r.left) * scaleX,
y: (e.touches[0].clientY - r.top) * scaleY };
}
return { x: (e.clientX - r.left) * scaleX, y: (e.clientY - r.top) * scaleY };
}
function onStart(e) {
const p = getPos(e);
if (Math.abs(p.x - rob.cx) < ROB_HW + 8 && Math.abs(p.y - rob.cy) < ROB_HW + 8) {
dragging = true; e.preventDefault();
}
}
function onMove(e) {
if (!dragging) return; e.preventDefault();
const p = getPos(e);
rob.cx = clamp(p.x, ROB_HW, W - ROB_HW);
rob.cy = clamp(p.y, ROB_HW, H - ROB_HW);
draw();
}
function onEnd() { dragging = false; }
canvas.addEventListener('mousedown', onStart);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onEnd);
canvas.addEventListener('touchstart', onStart, { passive: false });
canvas.addEventListener('touchmove', onMove, { passive: false });
canvas.addEventListener('touchend', onEnd);
draw();
// Verify (for testing): C-obstacle half-width must equal OBS.hw + ROB_HW
console.assert(COBS_HW === OBS.hw + ROB_HW,
fmt('C-obstacle size mismatch: expected %hw%%ROB_HW%, got %COBS_HW%', { hw: OBS.hw, ROB_HW: ROB_HW, COBS_HW: COBS_HW }));
// When robot center is exactly at OBS.cx - COBS_HW it is ON the boundary (touching)
const boundaryX = OBS.cx - COBS_HW;
const boundaryCollide = Math.abs(boundaryX - OBS.cx) < COBS_HW; // strict < so boundary = not inside
console.assert(!boundaryCollide, 'Boundary point should NOT be strictly inside C-obstacle');
console.log('Minkowski sum demo: C-obstacle hw =', COBS_HW,
fmt('= OBS.hw(%hw%) + ROB_HW(%ROB_HW%)', { hw: OBS.hw, ROB_HW: ROB_HW }));
<p class="hint">Drag the <b style="color:#e07b39">orange robot</b> around the canvas. The
<b style="color:#c0392b">red shaded region</b> is the Minkowski sum of the obstacle and the
robot — the set of positions the robot's center must avoid. When the center is outside the
red zone the robot is guaranteed collision-free.</p>
<canvas id="canvas" width="420" height="320"></canvas>
<div class="info" id="info">Drag the robot!</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f8f9fa; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.5; }
canvas { display: block; border: 1.5px solid #cdd; border-radius: 10px;
background: #fff; cursor: grab; touch-action: none; max-width: 100%; }
canvas:active { cursor: grabbing; }
.info { font-size: .9rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; color: #333; }
.safe { color: #0a7d33; }
.collide{ color: #c0392b; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Minkowski sum demo: robot (square) navigating near an obstacle (square).
// The Minkowski sum of obstacle O and robot R (reflected) gives the C-obstacle:
// the set of robot-center positions that cause a collision.
// For axis-aligned rectangles this is just another rectangle:
// C-obstacle top-left = O.top-left - R.half
// "Collision" iff robot center is inside C-obstacle.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Obstacle: fixed blue square (center, half-width)
const OBS = { cx: 210, cy: 160, hw: 60 };
// Robot: orange square (half-width = 22)
const ROB_HW = 22;
let rob = { cx: 80, cy: 80 };
// C-obstacle (Minkowski sum of obstacle with robot reflected = robot shape):
// For equal axis-aligned squares: C-obs is centered at OBS center,
// half-width = OBS.hw + ROB_HW
const COBS_HW = OBS.hw + ROB_HW;
function inCObs(cx, cy) {
return Math.abs(cx - OBS.cx) < COBS_HW && Math.abs(cy - OBS.cy) < COBS_HW;
}
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw C-obstacle (Minkowski sum region) — filled red, semi-transparent
ctx.save();
ctx.fillStyle = 'rgba(192,57,43,0.18)';
ctx.strokeStyle = 'rgba(192,57,43,0.55)';
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 4]);
ctx.beginPath();
ctx.rect(OBS.cx - COBS_HW, OBS.cy - COBS_HW, COBS_HW * 2, COBS_HW * 2);
ctx.fill();
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
// Label for C-obstacle
ctx.save();
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = 'rgba(192,57,43,0.85)';
ctx.fillText('C-obstacle (Minkowski sum)', OBS.cx - COBS_HW + 4, OBS.cy - COBS_HW - 5);
ctx.restore();
// Draw obstacle (blue filled square)
ctx.save();
ctx.fillStyle = 'rgba(41,128,185,0.22)';
ctx.strokeStyle = '#2980b9';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.rect(OBS.cx - OBS.hw, OBS.cy - OBS.hw, OBS.hw * 2, OBS.hw * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
// Obstacle label
ctx.save();
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.fillStyle = '#2980b9';
ctx.textAlign = 'center';
ctx.fillText('Obstacle', OBS.cx, OBS.cy + 4);
ctx.restore();
// Draw robot (orange square)
const colliding = inCObs(rob.cx, rob.cy);
ctx.save();
ctx.fillStyle = colliding ? 'rgba(231,76,60,0.6)' : 'rgba(224,123,57,0.65)';
ctx.strokeStyle = colliding ? '#c0392b' : '#e07b39';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.rect(rob.cx - ROB_HW, rob.cy - ROB_HW, ROB_HW * 2, ROB_HW * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
// Robot label
ctx.save();
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.fillStyle = colliding ? '#c0392b' : '#8b4513';
ctx.textAlign = 'center';
ctx.fillText('Robot', rob.cx, rob.cy + 4);
ctx.restore();
// Center dot on robot
ctx.save();
ctx.beginPath();
ctx.arc(rob.cx, rob.cy, 3, 0, Math.PI * 2);
ctx.fillStyle = colliding ? '#c0392b' : '#8b4513';
ctx.fill();
ctx.restore();
// Arrow from robot center to C-obs boundary (nearest point) when outside
if (!colliding) {
const dx = clamp(rob.cx, OBS.cx - COBS_HW, OBS.cx + COBS_HW) - rob.cx;
const dy = clamp(rob.cy, OBS.cy - COBS_HW, OBS.cy + COBS_HW) - rob.cy;
const dist = Math.sqrt(dx * dx + dy * dy);
ctx.save();
ctx.strokeStyle = 'rgba(10,125,51,0.5)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.moveTo(rob.cx, rob.cy);
ctx.lineTo(rob.cx + dx, rob.cy + dy);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
// Distance label
ctx.save();
ctx.font = '10px system-ui, sans-serif';
ctx.fillStyle = 'rgba(10,125,51,0.8)';
ctx.fillText(fmt('d=%round%px', { round: Math.round(dist) }), rob.cx + dx / 2 + 4, rob.cy + dy / 2 - 3);
ctx.restore();
}
// Status
const info = document.getElementById('info');
if (colliding) {
info.textContent = '✗ Collision! Robot center is inside the C-obstacle.';
info.className = 'info collide';
} else {
const dx = clamp(rob.cx, OBS.cx - COBS_HW, OBS.cx + COBS_HW) - rob.cx;
const dy = clamp(rob.cy, OBS.cy - COBS_HW, OBS.cy + COBS_HW) - rob.cy;
const dist = Math.round(Math.sqrt(dx * dx + dy * dy));
info.textContent = fmt('✓ Safe — robot center is %dist%px from the C-obstacle boundary.', { dist: dist });
info.className = 'info safe';
}
}
// Drag handling (mouse + touch)
let dragging = false;
function getPos(e) {
const r = canvas.getBoundingClientRect();
const scaleX = W / r.width, scaleY = H / r.height;
if (e.touches) {
return { x: (e.touches[0].clientX - r.left) * scaleX,
y: (e.touches[0].clientY - r.top) * scaleY };
}
return { x: (e.clientX - r.left) * scaleX, y: (e.clientY - r.top) * scaleY };
}
function onStart(e) {
const p = getPos(e);
if (Math.abs(p.x - rob.cx) < ROB_HW + 8 && Math.abs(p.y - rob.cy) < ROB_HW + 8) {
dragging = true; e.preventDefault();
}
}
function onMove(e) {
if (!dragging) return; e.preventDefault();
const p = getPos(e);
rob.cx = clamp(p.x, ROB_HW, W - ROB_HW);
rob.cy = clamp(p.y, ROB_HW, H - ROB_HW);
draw();
}
function onEnd() { dragging = false; }
canvas.addEventListener('mousedown', onStart);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onEnd);
canvas.addEventListener('touchstart', onStart, { passive: false });
canvas.addEventListener('touchmove', onMove, { passive: false });
canvas.addEventListener('touchend', onEnd);
draw();
// Verify (for testing): C-obstacle half-width must equal OBS.hw + ROB_HW
console.assert(COBS_HW === OBS.hw + ROB_HW,
fmt('C-obstacle size mismatch: expected %hw%%ROB_HW%, got %COBS_HW%', { hw: OBS.hw, ROB_HW: ROB_HW, COBS_HW: COBS_HW }));
// When robot center is exactly at OBS.cx - COBS_HW it is ON the boundary (touching)
const boundaryX = OBS.cx - COBS_HW;
const boundaryCollide = Math.abs(boundaryX - OBS.cx) < COBS_HW; // strict < so boundary = not inside
console.assert(!boundaryCollide, 'Boundary point should NOT be strictly inside C-obstacle');
console.log('Minkowski sum demo: C-obstacle hw =', COBS_HW,
fmt('= OBS.hw(%hw%) + ROB_HW(%ROB_HW%)', { hw: OBS.hw, ROB_HW: ROB_HW }));
Comments
Loading comments...