Click inside the box to drop points. After each click the smallest enclosing circle is recomputed and the points that actually define it — the ones touching the boundary — light up. Watch how only two or three points ever matter, no matter how many you add.
<p class="hint">Click inside the box to drop points. The <b>smallest enclosing circle</b> is recomputed after every click, and the at-most-three points that actually define it (the ones touching the boundary) light up.</p>
<canvas id="cv" width="440" height="300"></canvas>
<div class="status" id="status">Click to add your first point.</div>
<div class="btns">
<button id="random" type="button">Add 5 random</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
canvas { width: 100%; max-width: 440px; height: auto; background: #f3f6f9;
border: 1px solid #cdd9e3; border-radius: 10px; cursor: crosshair; display: block; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Smallest enclosing circle via Welzl's randomized incremental algorithm.
// Expected O(n) time (Welzl, 1991). The circle is always defined by at most
// 3 of the points (the "support" set) lying on its boundary.
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
let pts = [];
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
function circleFrom2(a, b) {
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, r: dist(a, b) / 2 };
}
function circleFrom3(a, b, c) {
const ax = a.x, ay = a.y, bx = b.x, by = b.y, cx = c.x, cy = c.y;
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if (Math.abs(d) < 1e-9) return null; // collinear
const ux = ((ax*ax+ay*ay)*(by-cy) + (bx*bx+by*by)*(cy-ay) + (cx*cx+cy*cy)*(ay-by)) / d;
const uy = ((ax*ax+ay*ay)*(cx-bx) + (bx*bx+by*by)*(ax-cx) + (cx*cx+cy*cy)*(bx-ax)) / d;
const cen = { x: ux, y: uy };
return { x: ux, y: uy, r: dist(cen, a) };
}
const inCircle = (c, p) => c && dist(c, p) <= c.r + 1e-7;
// Build the smallest circle whose boundary set R (size <=3) is honored.
function trivial(R) {
if (R.length === 0) return { x: 0, y: 0, r: 0 };
if (R.length === 1) return { x: R[0].x, y: R[0].y, r: 0 };
if (R.length === 2) return circleFrom2(R[0], R[1]);
// 3 points: the unique circle through them, or fall back to best pair.
const c = circleFrom3(R[0], R[1], R[2]);
if (c) return c;
let best = circleFrom2(R[0], R[1]);
const c2 = circleFrom2(R[0], R[2]); if (c2.r < best.r) best = c2;
const c3 = circleFrom2(R[1], R[2]); if (c3.r < best.r) best = c3;
return best;
}
// Iterative Welzl: incremental with at most 3 boundary points.
function welzl(points) {
const P = points.slice();
for (let i = P.length - 1; i > 0; i--) { // shuffle
const j = Math.floor(Math.random() * (i + 1));
[P[i], P[j]] = [P[j], P[i]];
}
let c = trivial(P.slice(0, Math.min(1, P.length)));
const support = P.length ? [P[0]] : [];
let circle = P.length ? { x: P[0].x, y: P[0].y, r: 0 } : { x: 0, y: 0, r: 0 };
let supp = P.length ? [P[0]] : [];
for (let i = 1; i < P.length; i++) {
if (inCircle(circle, P[i])) continue;
// P[i] must be on the boundary.
circle = { x: P[i].x, y: P[i].y, r: 0 }; supp = [P[i]];
for (let j = 0; j < i; j++) {
if (inCircle(circle, P[j])) continue;
circle = circleFrom2(P[i], P[j]); supp = [P[i], P[j]];
for (let k = 0; k < j; k++) {
if (inCircle(circle, P[k])) continue;
const c3 = circleFrom3(P[i], P[j], P[k]);
if (c3) { circle = c3; supp = [P[i], P[j], P[k]]; }
}
}
}
return { circle, supp };
}
function isSupport(p, supp) { return supp.some(s => Math.abs(s.x - p.x) < 1e-6 && Math.abs(s.y - p.y) < 1e-6); }
function draw() {
ctx.clearRect(0, 0, cv.width, cv.height);
let res = { circle: { x: 0, y: 0, r: 0 }, supp: [] };
if (pts.length >= 1) res = welzl(pts);
const { circle, supp } = res;
if (circle.r > 0) {
ctx.beginPath(); ctx.arc(circle.x, circle.y, circle.r, 0, 7);
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2; ctx.stroke();
ctx.beginPath(); ctx.arc(circle.x, circle.y, 3, 0, 7);
ctx.fillStyle = '#1d3557'; ctx.fill();
}
for (const p of pts) {
const on = isSupport(p, supp);
ctx.beginPath(); ctx.arc(p.x, p.y, on ? 6 : 4, 0, 7);
ctx.fillStyle = on ? '#e63946' : '#5a7088'; ctx.fill();
}
const s = document.getElementById('status');
if (!pts.length) s.textContent = 'Click to add your first point.';
else s.textContent = fmt('%length% {{pt_word_%v%}} · circle pinned by %length2% {{bpt_word_%v2%}} · radius %round%', { length: pts.length, v: pts.length>1?'pl':'sg', length2: supp.length, v2: supp.length===1?'sg':'pl', round: Math.round(circle.r) });
}
cv.addEventListener('click', e => {
const rect = cv.getBoundingClientRect();
const sx = cv.width / rect.width, sy = cv.height / rect.height;
pts.push({ x: (e.clientX - rect.left) * sx, y: (e.clientY - rect.top) * sy });
draw();
});
document.getElementById('random').addEventListener('click', () => {
for (let i = 0; i < 5; i++)
pts.push({ x: 30 + Math.random() * (cv.width - 60), y: 30 + Math.random() * (cv.height - 60) });
draw();
});
document.getElementById('reset').addEventListener('click', () => { pts = []; draw(); });
draw();
<p class="hint">Click inside the box to drop points. The <b>smallest enclosing circle</b> is recomputed after every click, and the at-most-three points that actually define it (the ones touching the boundary) light up.</p>
<canvas id="cv" width="440" height="300"></canvas>
<div class="status" id="status">Click to add your first point.</div>
<div class="btns">
<button id="random" type="button">Add 5 random</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
canvas { width: 100%; max-width: 440px; height: auto; background: #f3f6f9;
border: 1px solid #cdd9e3; border-radius: 10px; cursor: crosshair; display: block; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Smallest enclosing circle via Welzl's randomized incremental algorithm.
// Expected O(n) time (Welzl, 1991). The circle is always defined by at most
// 3 of the points (the "support" set) lying on its boundary.
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
let pts = [];
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
function circleFrom2(a, b) {
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, r: dist(a, b) / 2 };
}
function circleFrom3(a, b, c) {
const ax = a.x, ay = a.y, bx = b.x, by = b.y, cx = c.x, cy = c.y;
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if (Math.abs(d) < 1e-9) return null; // collinear
const ux = ((ax*ax+ay*ay)*(by-cy) + (bx*bx+by*by)*(cy-ay) + (cx*cx+cy*cy)*(ay-by)) / d;
const uy = ((ax*ax+ay*ay)*(cx-bx) + (bx*bx+by*by)*(ax-cx) + (cx*cx+cy*cy)*(bx-ax)) / d;
const cen = { x: ux, y: uy };
return { x: ux, y: uy, r: dist(cen, a) };
}
const inCircle = (c, p) => c && dist(c, p) <= c.r + 1e-7;
// Build the smallest circle whose boundary set R (size <=3) is honored.
function trivial(R) {
if (R.length === 0) return { x: 0, y: 0, r: 0 };
if (R.length === 1) return { x: R[0].x, y: R[0].y, r: 0 };
if (R.length === 2) return circleFrom2(R[0], R[1]);
// 3 points: the unique circle through them, or fall back to best pair.
const c = circleFrom3(R[0], R[1], R[2]);
if (c) return c;
let best = circleFrom2(R[0], R[1]);
const c2 = circleFrom2(R[0], R[2]); if (c2.r < best.r) best = c2;
const c3 = circleFrom2(R[1], R[2]); if (c3.r < best.r) best = c3;
return best;
}
// Iterative Welzl: incremental with at most 3 boundary points.
function welzl(points) {
const P = points.slice();
for (let i = P.length - 1; i > 0; i--) { // shuffle
const j = Math.floor(Math.random() * (i + 1));
[P[i], P[j]] = [P[j], P[i]];
}
let c = trivial(P.slice(0, Math.min(1, P.length)));
const support = P.length ? [P[0]] : [];
let circle = P.length ? { x: P[0].x, y: P[0].y, r: 0 } : { x: 0, y: 0, r: 0 };
let supp = P.length ? [P[0]] : [];
for (let i = 1; i < P.length; i++) {
if (inCircle(circle, P[i])) continue;
// P[i] must be on the boundary.
circle = { x: P[i].x, y: P[i].y, r: 0 }; supp = [P[i]];
for (let j = 0; j < i; j++) {
if (inCircle(circle, P[j])) continue;
circle = circleFrom2(P[i], P[j]); supp = [P[i], P[j]];
for (let k = 0; k < j; k++) {
if (inCircle(circle, P[k])) continue;
const c3 = circleFrom3(P[i], P[j], P[k]);
if (c3) { circle = c3; supp = [P[i], P[j], P[k]]; }
}
}
}
return { circle, supp };
}
function isSupport(p, supp) { return supp.some(s => Math.abs(s.x - p.x) < 1e-6 && Math.abs(s.y - p.y) < 1e-6); }
function draw() {
ctx.clearRect(0, 0, cv.width, cv.height);
let res = { circle: { x: 0, y: 0, r: 0 }, supp: [] };
if (pts.length >= 1) res = welzl(pts);
const { circle, supp } = res;
if (circle.r > 0) {
ctx.beginPath(); ctx.arc(circle.x, circle.y, circle.r, 0, 7);
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2; ctx.stroke();
ctx.beginPath(); ctx.arc(circle.x, circle.y, 3, 0, 7);
ctx.fillStyle = '#1d3557'; ctx.fill();
}
for (const p of pts) {
const on = isSupport(p, supp);
ctx.beginPath(); ctx.arc(p.x, p.y, on ? 6 : 4, 0, 7);
ctx.fillStyle = on ? '#e63946' : '#5a7088'; ctx.fill();
}
const s = document.getElementById('status');
if (!pts.length) s.textContent = 'Click to add your first point.';
else s.textContent = fmt('%length% {{pt_word_%v%}} · circle pinned by %length2% {{bpt_word_%v2%}} · radius %round%', { length: pts.length, v: pts.length>1?'pl':'sg', length2: supp.length, v2: supp.length===1?'sg':'pl', round: Math.round(circle.r) });
}
cv.addEventListener('click', e => {
const rect = cv.getBoundingClientRect();
const sx = cv.width / rect.width, sy = cv.height / rect.height;
pts.push({ x: (e.clientX - rect.left) * sx, y: (e.clientY - rect.top) * sy });
draw();
});
document.getElementById('random').addEventListener('click', () => {
for (let i = 0; i < 5; i++)
pts.push({ x: 30 + Math.random() * (cv.width - 60), y: 30 + Math.random() * (cv.height - 60) });
draw();
});
document.getElementById('reset').addEventListener('click', () => { pts = []; draw(); });
draw();
Notice the behavior. Most points you add do nothing: they fall inside the current circle and leave it unchanged. Only when a new point lands outside does the circle jump, re-snapping to a fresh set of at most three boundary points. That "usually nothing happens" pattern is exactly why Welzl's randomized version is so fast on average.
Comments
Loading comments...