Below is a 2D lattice: every gray dot is reachable by combining two basis vectors, drawn as arrows from the origin. The starting basis is deliberately awful — long and almost parallel, so the arrows nearly overlap.
<p class="hint">The gray dots are one 2D lattice — every point reachable as a whole-number combination of the two basis vectors, drawn as red (b1) and blue (b2) arrows. The starting basis is long and nearly parallel.</p>
<div class="controls">
<button id="step" type="button">Step</button>
<button id="reduce" type="button">Reduce fully</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="info-bar">
<span>Lengths: <b id="lens">—</b></span>
<span>Angle: <b id="angle">—</b></span>
<span>Steps: <b id="steps">0</b></span>
</div>
<canvas id="cv" width="480" height="340"></canvas>
<div class="status" id="status">Click Step to run one move of LLL, or Reduce fully to finish at once.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: transparent; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
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; }
button:disabled { opacity: .45; cursor: default; }
.info-bar { display: flex; gap: 1.2rem; flex-wrap: wrap; font-size: .88rem; margin-bottom: .4rem; color: #444; }
.info-bar b { color: #1d3557; }
#cv { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc;
max-width: 100%; height: auto; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0 0; min-height: 1.4em; color: #0a7d33; }
.status.done { color: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2D lattice reduction: LLL untangles a skewed basis into a short, nearly-orthogonal one.
// The algorithm alternates two moves until neither applies:
// Size-reduce: subtract the nearest integer multiple of b1 from b2.
// Swap b1 and b2 whenever that shortens the pair (the Lovasz condition).
// Repeat until a single pass changes nothing.
const DELTA = 0.75; // the Lovasz condition parameter (3/4 is the classic choice)
// A deliberately long, nearly-parallel starting basis for the same lattice.
const INIT = [[27, 11], [35, 14]];
let b1, b2, stepCount, done;
function dot(a, b) { return a[0] * b[0] + a[1] * b[1]; }
function norm2(a) { return dot(a, a); }
function len(a) { return Math.sqrt(norm2(a)); }
function reset() {
b1 = [...INIT[0]];
b2 = [...INIT[1]];
stepCount = 0;
done = false;
document.getElementById('step').disabled = false;
document.getElementById('reduce').disabled = false;
setStatus('Click Step to run one move of LLL, or Reduce fully to finish at once.', '');
updateUI();
}
// Runs exactly one move of the algorithm; returns false once no move changes anything.
function lllStep() {
if (done) return false;
// If b2 has a large projection onto b1, remove the integer part of it.
const mu = dot(b2, b1) / norm2(b1);
const rounded = Math.round(mu);
if (rounded !== 0) {
b2 = [b2[0] - rounded * b1[0], b2[1] - rounded * b1[1]];
stepCount++;
return true;
}
// Otherwise check whether swapping the vectors would shorten the shorter one.
const mu2 = dot(b2, b1) / norm2(b1);
const b2star = [b2[0] - mu2 * b1[0], b2[1] - mu2 * b1[1]];
if (norm2(b2star) < (DELTA - mu2 * mu2) * norm2(b1)) {
const tmp = b1; b1 = b2; b2 = tmp; // swap keeps the same lattice, just reorders the basis
stepCount++;
return true;
}
done = true;
return false;
}
function angleDeg() {
const cosA = dot(b1, b2) / (len(b1) * len(b2));
const a = Math.acos(Math.min(1, Math.max(-1, cosA))) * 180 / Math.PI;
return a > 90 ? 180 - a : a;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
function updateUI() {
document.getElementById('steps').textContent = stepCount;
document.getElementById('lens').textContent =
len(b1).toFixed(2) + ', ' + len(b2).toFixed(2);
document.getElementById('angle').textContent = angleDeg().toFixed(1) + '°';
if (done) {
document.getElementById('step').disabled = true;
document.getElementById('reduce').disabled = true;
setStatus(i18nFmt('Reduced after %n% step(s) — same lattice, short and nearly orthogonal basis.', { n: stepCount }), 'done');
}
draw();
}
// Canvas drawing helpers below - no algorithm logic here.
const CV_W = 480, CV_H = 340;
const ORIGIN = [CV_W / 2, CV_H / 2];
function draw() {
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
ctx.clearRect(0, 0, CV_W, CV_H);
// Pick a zoom level so the arrows and nearby lattice points fit on the canvas.
const maxCoord = Math.max(
Math.abs(INIT[0][0]), Math.abs(INIT[0][1]),
Math.abs(INIT[1][0]), Math.abs(INIT[1][1]), 1
) * 2.6;
const scale = Math.min(CV_W, CV_H) / 2 / maxCoord;
function toPx(p) {
return [ORIGIN[0] + p[0] * scale, ORIGIN[1] - p[1] * scale];
}
// Draw every lattice point reachable with small integer combinations of b1, b2.
ctx.fillStyle = '#c3ccd6';
const range = 6;
for (let i = -range; i <= range; i++) {
for (let j = -range; j <= range; j++) {
const x = i * b1[0] + j * b2[0];
const y = i * b1[1] + j * b2[1];
const [px, py] = toPx([x, y]);
if (px < -10 || px > CV_W + 10 || py < -10 || py > CV_H + 10) continue;
ctx.beginPath();
ctx.arc(px, py, 2.2, 0, Math.PI * 2);
ctx.fill();
}
}
// Faint reference axes through the origin.
ctx.strokeStyle = '#e2e8ef';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, ORIGIN[1]); ctx.lineTo(CV_W, ORIGIN[1]);
ctx.moveTo(ORIGIN[0], 0); ctx.lineTo(ORIGIN[0], CV_H);
ctx.stroke();
// Draw the two current basis vectors as arrows from the origin.
drawArrow(ctx, ORIGIN, toPx(b1), '#e63946', 'b1');
drawArrow(ctx, ORIGIN, toPx(b2), '#457b9d', 'b2');
}
function drawArrow(ctx, from, to, color, label) {
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 2.4;
ctx.beginPath();
ctx.moveTo(from[0], from[1]);
ctx.lineTo(to[0], to[1]);
ctx.stroke();
const dx = to[0] - from[0], dy = to[1] - from[1];
const ang = Math.atan2(dy, dx);
const headLen = 10;
ctx.beginPath();
ctx.moveTo(to[0], to[1]);
ctx.lineTo(to[0] - headLen * Math.cos(ang - Math.PI / 7), to[1] - headLen * Math.sin(ang - Math.PI / 7));
ctx.lineTo(to[0] - headLen * Math.cos(ang + Math.PI / 7), to[1] - headLen * Math.sin(ang + Math.PI / 7));
ctx.closePath();
ctx.fill();
ctx.font = '700 13px ui-monospace, monospace';
ctx.fillText(label, to[0] + 6, to[1] - 6);
}
document.getElementById('step').addEventListener('click', () => {
lllStep();
updateUI();
});
document.getElementById('reduce').addEventListener('click', () => {
let guard = 0;
while (lllStep() && guard < 2000) guard++;
updateUI();
});
document.getElementById('reset').addEventListener('click', reset);
reset();
<p class="hint">The gray dots are one 2D lattice — every point reachable as a whole-number combination of the two basis vectors, drawn as red (b1) and blue (b2) arrows. The starting basis is long and nearly parallel.</p>
<div class="controls">
<button id="step" type="button">Step</button>
<button id="reduce" type="button">Reduce fully</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="info-bar">
<span>Lengths: <b id="lens">—</b></span>
<span>Angle: <b id="angle">—</b></span>
<span>Steps: <b id="steps">0</b></span>
</div>
<canvas id="cv" width="480" height="340"></canvas>
<div class="status" id="status">Click Step to run one move of LLL, or Reduce fully to finish at once.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: transparent; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
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; }
button:disabled { opacity: .45; cursor: default; }
.info-bar { display: flex; gap: 1.2rem; flex-wrap: wrap; font-size: .88rem; margin-bottom: .4rem; color: #444; }
.info-bar b { color: #1d3557; }
#cv { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc;
max-width: 100%; height: auto; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0 0; min-height: 1.4em; color: #0a7d33; }
.status.done { color: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2D lattice reduction: LLL untangles a skewed basis into a short, nearly-orthogonal one.
// The algorithm alternates two moves until neither applies:
// Size-reduce: subtract the nearest integer multiple of b1 from b2.
// Swap b1 and b2 whenever that shortens the pair (the Lovasz condition).
// Repeat until a single pass changes nothing.
const DELTA = 0.75; // the Lovasz condition parameter (3/4 is the classic choice)
// A deliberately long, nearly-parallel starting basis for the same lattice.
const INIT = [[27, 11], [35, 14]];
let b1, b2, stepCount, done;
function dot(a, b) { return a[0] * b[0] + a[1] * b[1]; }
function norm2(a) { return dot(a, a); }
function len(a) { return Math.sqrt(norm2(a)); }
function reset() {
b1 = [...INIT[0]];
b2 = [...INIT[1]];
stepCount = 0;
done = false;
document.getElementById('step').disabled = false;
document.getElementById('reduce').disabled = false;
setStatus('Click Step to run one move of LLL, or Reduce fully to finish at once.', '');
updateUI();
}
// Runs exactly one move of the algorithm; returns false once no move changes anything.
function lllStep() {
if (done) return false;
// If b2 has a large projection onto b1, remove the integer part of it.
const mu = dot(b2, b1) / norm2(b1);
const rounded = Math.round(mu);
if (rounded !== 0) {
b2 = [b2[0] - rounded * b1[0], b2[1] - rounded * b1[1]];
stepCount++;
return true;
}
// Otherwise check whether swapping the vectors would shorten the shorter one.
const mu2 = dot(b2, b1) / norm2(b1);
const b2star = [b2[0] - mu2 * b1[0], b2[1] - mu2 * b1[1]];
if (norm2(b2star) < (DELTA - mu2 * mu2) * norm2(b1)) {
const tmp = b1; b1 = b2; b2 = tmp; // swap keeps the same lattice, just reorders the basis
stepCount++;
return true;
}
done = true;
return false;
}
function angleDeg() {
const cosA = dot(b1, b2) / (len(b1) * len(b2));
const a = Math.acos(Math.min(1, Math.max(-1, cosA))) * 180 / Math.PI;
return a > 90 ? 180 - a : a;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
function updateUI() {
document.getElementById('steps').textContent = stepCount;
document.getElementById('lens').textContent =
len(b1).toFixed(2) + ', ' + len(b2).toFixed(2);
document.getElementById('angle').textContent = angleDeg().toFixed(1) + '°';
if (done) {
document.getElementById('step').disabled = true;
document.getElementById('reduce').disabled = true;
setStatus(i18nFmt('Reduced after %n% step(s) — same lattice, short and nearly orthogonal basis.', { n: stepCount }), 'done');
}
draw();
}
// Canvas drawing helpers below - no algorithm logic here.
const CV_W = 480, CV_H = 340;
const ORIGIN = [CV_W / 2, CV_H / 2];
function draw() {
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
ctx.clearRect(0, 0, CV_W, CV_H);
// Pick a zoom level so the arrows and nearby lattice points fit on the canvas.
const maxCoord = Math.max(
Math.abs(INIT[0][0]), Math.abs(INIT[0][1]),
Math.abs(INIT[1][0]), Math.abs(INIT[1][1]), 1
) * 2.6;
const scale = Math.min(CV_W, CV_H) / 2 / maxCoord;
function toPx(p) {
return [ORIGIN[0] + p[0] * scale, ORIGIN[1] - p[1] * scale];
}
// Draw every lattice point reachable with small integer combinations of b1, b2.
ctx.fillStyle = '#c3ccd6';
const range = 6;
for (let i = -range; i <= range; i++) {
for (let j = -range; j <= range; j++) {
const x = i * b1[0] + j * b2[0];
const y = i * b1[1] + j * b2[1];
const [px, py] = toPx([x, y]);
if (px < -10 || px > CV_W + 10 || py < -10 || py > CV_H + 10) continue;
ctx.beginPath();
ctx.arc(px, py, 2.2, 0, Math.PI * 2);
ctx.fill();
}
}
// Faint reference axes through the origin.
ctx.strokeStyle = '#e2e8ef';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, ORIGIN[1]); ctx.lineTo(CV_W, ORIGIN[1]);
ctx.moveTo(ORIGIN[0], 0); ctx.lineTo(ORIGIN[0], CV_H);
ctx.stroke();
// Draw the two current basis vectors as arrows from the origin.
drawArrow(ctx, ORIGIN, toPx(b1), '#e63946', 'b1');
drawArrow(ctx, ORIGIN, toPx(b2), '#457b9d', 'b2');
}
function drawArrow(ctx, from, to, color, label) {
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 2.4;
ctx.beginPath();
ctx.moveTo(from[0], from[1]);
ctx.lineTo(to[0], to[1]);
ctx.stroke();
const dx = to[0] - from[0], dy = to[1] - from[1];
const ang = Math.atan2(dy, dx);
const headLen = 10;
ctx.beginPath();
ctx.moveTo(to[0], to[1]);
ctx.lineTo(to[0] - headLen * Math.cos(ang - Math.PI / 7), to[1] - headLen * Math.sin(ang - Math.PI / 7));
ctx.lineTo(to[0] - headLen * Math.cos(ang + Math.PI / 7), to[1] - headLen * Math.sin(ang + Math.PI / 7));
ctx.closePath();
ctx.fill();
ctx.font = '700 13px ui-monospace, monospace';
ctx.fillText(label, to[0] + 6, to[1] - 6);
}
document.getElementById('step').addEventListener('click', () => {
lllStep();
updateUI();
});
document.getElementById('reduce').addEventListener('click', () => {
let guard = 0;
while (lllStep() && guard < 2000) guard++;
updateUI();
});
document.getElementById('reset').addEventListener('click', reset);
reset();
Watch the vector lengths and the angle between them in the readout. A skewed basis starts long with an angle near 0° or 180°; a reduced basis ends short with an angle close to 90°. LLL gets there using only two moves, repeated: subtract an integer multiple of one vector from the other, and swap the two vectors when that makes the pair shorter overall.
Comments
Loading comments...