Packing is easiest to feel in two dimensions, where circles stand in for spheres. Lay circles on a square grid and there are wide gaps; shift every other row so circles nestle into the dips and you get the hexagonal packing — the flat cousin of the grocer's pyramid. The demo computes the fraction of area each layout actually covers.
<p class="hint">Circles stand in for spheres. A <b>square</b> grid leaves wide gaps;
the <b>hexagonal</b> packing nestles every other row into the dips — the flat
version of the grocer’s pyramid. Watch how much of the area each one fills.</p>
<div class="btns">
<button id="square" type="button">Square grid</button>
<button id="hex" type="button">Hexagonal packing</button>
</div>
<canvas id="cv" width="360" height="240"></canvas>
<div class="status" id="status"></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; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 8px; cursor: pointer; }
button.active { background: #1d3557; color: #fff; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f4f7fa; display: block; max-width: 100%; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Sphere packing, felt in 2D. The grocer's pyramid (face-centered cubic) fills
// pi/sqrt(18) ~= 74.05% of 3D space (Kepler 1611; proved by Hales 1998, formally
// verified by Flyspeck 2014). The flat analogues: a square grid of circles fills
// pi/4 ~= 78.54%; the hexagonal packing fills pi/sqrt(12) ~= 90.69% (Thue 1910).
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
// Theoretical infinite-lattice densities (what the labels report).
const SQUARE_DENSITY = Math.PI / 4; // 0.785398...
const HEX_DENSITY = Math.PI / Math.sqrt(12); // 0.906899...
function drawSquare(){
ctx.clearRect(0,0,W,H);
const r = 20, d = 2*r;
ctx.fillStyle = '#457b9d'; ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 1.5;
for(let y = r; y < H + r; y += d)
for(let x = r; x < W + r; x += d){
ctx.beginPath(); ctx.arc(x, y, r, 0, 2*Math.PI); ctx.fill(); ctx.stroke();
}
report('Square grid', SQUARE_DENSITY);
}
function drawHex(){
ctx.clearRect(0,0,W,H);
const r = 20, d = 2*r;
// Rows are spaced by r*sqrt(3); odd rows shifted by r so circles nestle.
const rowGap = r * Math.sqrt(3);
let row = 0;
ctx.fillStyle = '#2a9d8f'; ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 1.5;
for(let y = r; y < H + r; y += rowGap, row++){
const off = (row % 2) ? r : 0;
for(let x = r + off; x < W + r; x += d){
ctx.beginPath(); ctx.arc(x, y, r, 0, 2*Math.PI); ctx.fill(); ctx.stroke();
}
}
report('Hexagonal packing', HEX_DENSITY);
}
function report(name, density){
const pct = (density * 100).toFixed(2);
document.getElementById('status').textContent =
fmt('%name% fills %pct%% of the plane.', { name: name, pct: pct });
console.log(name, '->', pct + '%');
}
function setActive(id){
['square','hex'].forEach(b => document.getElementById(b).classList.toggle('active', b === id));
}
document.getElementById('square').addEventListener('click', () => { setActive('square'); drawSquare(); });
document.getElementById('hex').addEventListener('click', () => { setActive('hex'); drawHex(); });
// Start on the hexagonal (densest) packing.
setActive('hex'); drawHex();
<p class="hint">Circles stand in for spheres. A <b>square</b> grid leaves wide gaps;
the <b>hexagonal</b> packing nestles every other row into the dips — the flat
version of the grocer’s pyramid. Watch how much of the area each one fills.</p>
<div class="btns">
<button id="square" type="button">Square grid</button>
<button id="hex" type="button">Hexagonal packing</button>
</div>
<canvas id="cv" width="360" height="240"></canvas>
<div class="status" id="status"></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; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 8px; cursor: pointer; }
button.active { background: #1d3557; color: #fff; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f4f7fa; display: block; max-width: 100%; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Sphere packing, felt in 2D. The grocer's pyramid (face-centered cubic) fills
// pi/sqrt(18) ~= 74.05% of 3D space (Kepler 1611; proved by Hales 1998, formally
// verified by Flyspeck 2014). The flat analogues: a square grid of circles fills
// pi/4 ~= 78.54%; the hexagonal packing fills pi/sqrt(12) ~= 90.69% (Thue 1910).
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
// Theoretical infinite-lattice densities (what the labels report).
const SQUARE_DENSITY = Math.PI / 4; // 0.785398...
const HEX_DENSITY = Math.PI / Math.sqrt(12); // 0.906899...
function drawSquare(){
ctx.clearRect(0,0,W,H);
const r = 20, d = 2*r;
ctx.fillStyle = '#457b9d'; ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 1.5;
for(let y = r; y < H + r; y += d)
for(let x = r; x < W + r; x += d){
ctx.beginPath(); ctx.arc(x, y, r, 0, 2*Math.PI); ctx.fill(); ctx.stroke();
}
report('Square grid', SQUARE_DENSITY);
}
function drawHex(){
ctx.clearRect(0,0,W,H);
const r = 20, d = 2*r;
// Rows are spaced by r*sqrt(3); odd rows shifted by r so circles nestle.
const rowGap = r * Math.sqrt(3);
let row = 0;
ctx.fillStyle = '#2a9d8f'; ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 1.5;
for(let y = r; y < H + r; y += rowGap, row++){
const off = (row % 2) ? r : 0;
for(let x = r + off; x < W + r; x += d){
ctx.beginPath(); ctx.arc(x, y, r, 0, 2*Math.PI); ctx.fill(); ctx.stroke();
}
}
report('Hexagonal packing', HEX_DENSITY);
}
function report(name, density){
const pct = (density * 100).toFixed(2);
document.getElementById('status').textContent =
fmt('%name% fills %pct%% of the plane.', { name: name, pct: pct });
console.log(name, '->', pct + '%');
}
function setActive(id){
['square','hex'].forEach(b => document.getElementById(b).classList.toggle('active', b === id));
}
document.getElementById('square').addEventListener('click', () => { setActive('square'); drawSquare(); });
document.getElementById('hex').addEventListener('click', () => { setActive('hex'); drawHex(); });
// Start on the hexagonal (densest) packing.
setActive('hex'); drawHex();
Switch between the two arrangements and watch the number move. The square grid fills about 78.5% of the plane; the hexagonal one reaches 90.7% — provably the best possible for circles (Thue, 1910). Spheres in 3D tell the same story, with the optimum dropping to 74.05%. The pattern is easy to see and easy to believe. Proving that nothing beats it is the hard part.
Comments
Loading comments...