Choose a radius and watch the midpoint algorithm place pixels one octant at a time, then mirror them across all eight symmetry axes simultaneously.
<div class="controls">
<label for="radius-slider">Radius r = <span id="radius-val">40</span></label>
<input id="radius-slider" type="range" min="10" max="80" value="40" step="1">
<button id="btn-step" type="button">Step</button>
<button id="btn-play" type="button">Play</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="360" height="280"></canvas>
<div id="status" class="status"></div>
<div id="dvar" class="dvar"></div>
/* layout & theme */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: .5rem; }
label { font-size: .88rem; color: #444; white-space: nowrap; }
input[type=range] { flex: 1 1 120px; min-width: 80px; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; background: #f4f6f8; border-radius: 8px; border: 1px solid #dde3ea;
max-width: 100%; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin-top: .4rem; color: #1d3557; }
.dvar { font-size: .82rem; color: #555; min-height: 1.2em; font-family: ui-monospace, monospace; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Bresenham midpoint circle: draw 1/8 of the circle (first octant, x<=y) */
/* then mirror each point to all 8 octants. Decision variable d = F(x+1, y-0.5) */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const CX = W / 2, CY = H / 2; // canvas center = circle center
// algorithm state
let r, x, y, d, pixels, step, timer, running;
function getRadius() {
return parseInt(document.getElementById('radius-slider').value, 10);
}
// reset algorithm state and redraw
function init() {
r = getRadius();
x = 0;
y = r;
d = 1 - r; // initial decision variable d = 1 - r
pixels = [];
step = 0;
running = false;
clearInterval(timer);
document.getElementById('btn-play').textContent = 'Play';
document.getElementById('radius-val').textContent = r;
updateStatus();
draw();
}
// expand one octant pixel (px,py) to all 8 symmetric copies
function octantPixels(px, py) {
return [
[ px, py], [-px, py], [ px, -py], [-px, -py],
[ py, px], [-py, px], [ py, -px], [-py, -px]
];
}
// compute one octant step: plot current (x,y), then update d, x, y
function advance() {
if (x > y) return false; // first octant complete
octantPixels(x, y).forEach(([a, b]) => pixels.push([a, b]));
step++;
if (d < 0) {
d += 2 * x + 3; // d < 0: midpoint inside circle, stay at same y
} else {
d += 2 * (x - y) + 5; // d >= 0: midpoint outside circle, step y down
y -= 1;
}
x += 1;
return true;
}
function draw() {
ctx.clearRect(0, 0, W, H);
// draw pixel grid
ctx.strokeStyle = '#dde3ea';
ctx.lineWidth = 0.5;
const CELL = 8;
for (let gx = 0; gx <= W; gx += CELL) { ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, H); ctx.stroke(); }
for (let gy = 0; gy <= H; gy += CELL) { ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(W, gy); ctx.stroke(); }
// draw coordinate axes
ctx.strokeStyle = '#aab5c0';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, CY); ctx.lineTo(W, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, 0); ctx.lineTo(CX, H); ctx.stroke();
// draw faint reference circle (true mathematical circle)
ctx.beginPath();
ctx.arc(CX, CY, r, 0, 2 * Math.PI);
ctx.strokeStyle = 'rgba(200,60,60,0.25)';
ctx.lineWidth = 1;
ctx.stroke();
// draw placed pixels (already computed)
ctx.fillStyle = '#1d3557';
pixels.forEach(([px, py]) => {
ctx.fillRect(CX + px * CELL, CY - py * CELL, CELL - 1, CELL - 1);
});
// highlight the current pixel and its 7 mirrors
if (x <= y) {
ctx.fillStyle = 'rgba(230,57,70,0.85)';
octantPixels(x, y).forEach(([px, py]) => {
ctx.fillRect(CX + px * CELL, CY - py * CELL, CELL - 1, CELL - 1);
});
}
}
function updateStatus() {
const done = x > y;
const statusEl = document.getElementById('status');
const dvarEl = document.getElementById('dvar');
if (done) {
statusEl.textContent = i18nFmt('Done — %step% octant steps, circle complete!', { step: step });
dvarEl.textContent = '';
} else {
statusEl.textContent = i18nFmt('Octant pixel: x = %x%, y = %y%', { x: x, y: y });
dvarEl.textContent = 'd = ' + d + (d < 0 ? ' → move right only' : ' → move right + down');
}
}
function stepOnce() {
if (x > y) return;
advance();
draw();
updateStatus();
}
function togglePlay() {
running = !running;
document.getElementById('btn-play').textContent = running ? 'Pause' : 'Play';
if (running) {
timer = setInterval(() => {
if (x > y) { clearInterval(timer); running = false;
document.getElementById('btn-play').textContent = 'Play'; return; }
advance(); draw(); updateStatus();
}, 80);
} else {
clearInterval(timer);
}
}
document.getElementById('btn-step').addEventListener('click', stepOnce);
document.getElementById('btn-play').addEventListener('click', togglePlay);
document.getElementById('btn-reset').addEventListener('click', init);
document.getElementById('radius-slider').addEventListener('input', () => {
document.getElementById('radius-val').textContent = getRadius();
init();
});
init();
<div class="controls">
<label for="radius-slider">Radius r = <span id="radius-val">40</span></label>
<input id="radius-slider" type="range" min="10" max="80" value="40" step="1">
<button id="btn-step" type="button">Step</button>
<button id="btn-play" type="button">Play</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="360" height="280"></canvas>
<div id="status" class="status"></div>
<div id="dvar" class="dvar"></div>
/* layout & theme */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: .5rem; }
label { font-size: .88rem; color: #444; white-space: nowrap; }
input[type=range] { flex: 1 1 120px; min-width: 80px; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; background: #f4f6f8; border-radius: 8px; border: 1px solid #dde3ea;
max-width: 100%; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin-top: .4rem; color: #1d3557; }
.dvar { font-size: .82rem; color: #555; min-height: 1.2em; font-family: ui-monospace, monospace; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Bresenham midpoint circle: draw 1/8 of the circle (first octant, x<=y) */
/* then mirror each point to all 8 octants. Decision variable d = F(x+1, y-0.5) */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const CX = W / 2, CY = H / 2; // canvas center = circle center
// algorithm state
let r, x, y, d, pixels, step, timer, running;
function getRadius() {
return parseInt(document.getElementById('radius-slider').value, 10);
}
// reset algorithm state and redraw
function init() {
r = getRadius();
x = 0;
y = r;
d = 1 - r; // initial decision variable d = 1 - r
pixels = [];
step = 0;
running = false;
clearInterval(timer);
document.getElementById('btn-play').textContent = 'Play';
document.getElementById('radius-val').textContent = r;
updateStatus();
draw();
}
// expand one octant pixel (px,py) to all 8 symmetric copies
function octantPixels(px, py) {
return [
[ px, py], [-px, py], [ px, -py], [-px, -py],
[ py, px], [-py, px], [ py, -px], [-py, -px]
];
}
// compute one octant step: plot current (x,y), then update d, x, y
function advance() {
if (x > y) return false; // first octant complete
octantPixels(x, y).forEach(([a, b]) => pixels.push([a, b]));
step++;
if (d < 0) {
d += 2 * x + 3; // d < 0: midpoint inside circle, stay at same y
} else {
d += 2 * (x - y) + 5; // d >= 0: midpoint outside circle, step y down
y -= 1;
}
x += 1;
return true;
}
function draw() {
ctx.clearRect(0, 0, W, H);
// draw pixel grid
ctx.strokeStyle = '#dde3ea';
ctx.lineWidth = 0.5;
const CELL = 8;
for (let gx = 0; gx <= W; gx += CELL) { ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, H); ctx.stroke(); }
for (let gy = 0; gy <= H; gy += CELL) { ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(W, gy); ctx.stroke(); }
// draw coordinate axes
ctx.strokeStyle = '#aab5c0';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, CY); ctx.lineTo(W, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, 0); ctx.lineTo(CX, H); ctx.stroke();
// draw faint reference circle (true mathematical circle)
ctx.beginPath();
ctx.arc(CX, CY, r, 0, 2 * Math.PI);
ctx.strokeStyle = 'rgba(200,60,60,0.25)';
ctx.lineWidth = 1;
ctx.stroke();
// draw placed pixels (already computed)
ctx.fillStyle = '#1d3557';
pixels.forEach(([px, py]) => {
ctx.fillRect(CX + px * CELL, CY - py * CELL, CELL - 1, CELL - 1);
});
// highlight the current pixel and its 7 mirrors
if (x <= y) {
ctx.fillStyle = 'rgba(230,57,70,0.85)';
octantPixels(x, y).forEach(([px, py]) => {
ctx.fillRect(CX + px * CELL, CY - py * CELL, CELL - 1, CELL - 1);
});
}
}
function updateStatus() {
const done = x > y;
const statusEl = document.getElementById('status');
const dvarEl = document.getElementById('dvar');
if (done) {
statusEl.textContent = i18nFmt('Done — %step% octant steps, circle complete!', { step: step });
dvarEl.textContent = '';
} else {
statusEl.textContent = i18nFmt('Octant pixel: x = %x%, y = %y%', { x: x, y: y });
dvarEl.textContent = 'd = ' + d + (d < 0 ? ' → move right only' : ' → move right + down');
}
}
function stepOnce() {
if (x > y) return;
advance();
draw();
updateStatus();
}
function togglePlay() {
running = !running;
document.getElementById('btn-play').textContent = running ? 'Pause' : 'Play';
if (running) {
timer = setInterval(() => {
if (x > y) { clearInterval(timer); running = false;
document.getElementById('btn-play').textContent = 'Play'; return; }
advance(); draw(); updateStatus();
}, 80);
} else {
clearInterval(timer);
}
}
document.getElementById('btn-step').addEventListener('click', stepOnce);
document.getElementById('btn-play').addEventListener('click', togglePlay);
document.getElementById('btn-reset').addEventListener('click', init);
document.getElementById('radius-slider').addEventListener('input', () => {
document.getElementById('radius-val').textContent = getRadius();
init();
});
init();
Comments
Loading comments...