<!-- Line arrangement demo — add lines one at a time to see the plane subdivide -->
<p class="hint">Click <b>Add line</b> to insert one line at a time in general position. Watch vertices (V), edges (E) and faces (F) grow. With n lines: V = n(n-1)/2, E = n², F = n(n-1)/2 + n + 1.</p>
<canvas id="canvas" width="420" height="260"></canvas>
<div class="stats" id="stats">
<span><b>n</b> = <span id="n">0</span></span>
<span><b>V</b> = <span id="V">0</span></span>
<span><b>E</b> = <span id="E">0</span></span>
<span><b>F</b> = <span id="F">1</span></span>
</div>
<div class="formulas" id="formulas">Add lines to see the exact counts.</div>
<div class="btns">
<button id="addLine" type="button">Add line</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the line arrangement interactive demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f7f9fb; width: 100%; max-width: 420px; height: auto; }
.stats { display: flex; gap: 1.2rem; font-size: .95rem; margin: .5rem 0 .2rem;
flex-wrap: wrap; }
.formulas { font-size: .82rem; color: #5a6a7a; margin: 0 0 .5rem;
font-style: italic; min-height: 1.2em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Line arrangement demo — insert lines one by one, track exact V/E/F counts
// Formulas: V=n*(n-1)/2, E=n^2, F=n*(n-1)/2+n+1 (general position)
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Array of inserted lines; each line is { angle (radians), ox, oy (a point on the line) }
// Each line is parameterised as a point + direction so intersections are easy to compute.
const lines = [];
// Pre-set angles and offsets so lines stay in general position (no two parallel, no three concurrent)
// Angles in degrees for up to 10 preset lines
const PRESET_ANGLES = [28, 112, 65, 155, 10, 90, 45, 135, 170, 50];
const PRESET_OFFSETS = [0.38, 0.52, 0.25, 0.62, 0.45, 0.3, 0.7, 0.55, 0.42, 0.65];
// Closed-form cell counts for n lines in general position (Euler characteristic)
// V = n*(n-1)/2, E = n*n, F = n*(n-1)/2 + n + 1
// V = n*(n-1)/2, E = n*n, F = n*(n-1)/2 + n + 1
function countsForN(n) {
const V = n * (n - 1) / 2;
const E = n * n;
const F = V + n + 1;
return { V, E, F };
}
// Compute the intersection point of two infinite lines given as point + direction.
// Returns null if lines are parallel (denominator near zero).
function intersectLines(a, b) {
// Direction vector from angle
const dxA = Math.cos(a.angle), dyA = Math.sin(a.angle);
const dxB = Math.cos(b.angle), dyB = Math.sin(b.angle);
const denom = dxA * dyB - dyA * dxB;
if (Math.abs(denom) < 1e-9) return null; // lines are parallel or nearly so
const t = ((b.ox - a.ox) * dyB - (b.oy - a.oy) * dxB) / denom;
return { x: a.ox + t * dxA, y: a.oy + t * dyA };
}
// Draw a single line extended far beyond the canvas boundaries.
function drawLine(line, color) {
const dx = Math.cos(line.angle), dy = Math.sin(line.angle);
// Extend 1000 px in both directions so the line spans the whole canvas.
const big = 1000;
ctx.beginPath();
ctx.moveTo(line.ox - big * dx, line.oy - big * dy);
ctx.lineTo(line.ox + big * dx, line.oy + big * dy);
ctx.strokeStyle = color;
ctx.lineWidth = 1.4;
ctx.stroke();
}
// Distinct colors for up to 10 lines
const COLORS = ['#1d3557','#e63946','#2a9d8f','#e9c46a','#6a4c93','#f4a261','#264653','#a8dadc','#457b9d','#e76f51'];
function render() {
ctx.clearRect(0, 0, W, H);
// Draw every inserted line
lines.forEach((l, i) => drawLine(l, COLORS[i % COLORS.length]));
// Draw intersection dots on top of the lines
lines.forEach((a, i) => {
lines.forEach((b, j) => {
if (j <= i) return;
const pt = intersectLines(a, b);
if (!pt) return;
if (pt.x < -50 || pt.x > W + 50 || pt.y < -50 || pt.y > H + 50) return;
ctx.beginPath();
ctx.arc(pt.x, pt.y, 3.5, 0, 2 * Math.PI);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.stroke();
});
});
}
function updateStats() {
const n = lines.length;
const { V, E, F } = countsForN(n);
document.getElementById('n').textContent = n;
document.getElementById('V').textContent = V;
document.getElementById('E').textContent = E;
document.getElementById('F').textContent = F;
const fEl = document.getElementById('formulas');
if (n === 0) {
fEl.textContent = 'Add lines to see the exact counts.';
} else {
// Update the formula readout below the stats
fEl.textContent = fmt('n = %n% lines → V=%V%, E=%E%, F=%F%', { n: n, V: V, E: E, F: F });
}
}
document.getElementById('addLine').addEventListener('click', () => {
const i = lines.length;
if (i >= PRESET_ANGLES.length) {
document.getElementById('addLine').disabled = true;
return;
}
// Pick angle and a point from the preset list
const angleDeg = PRESET_ANGLES[i];
const t = PRESET_OFFSETS[i];
// Position the line's reference point in the central region of the canvas
const ox = W * 0.15 + W * 0.7 * t;
const oy = H * 0.15 + H * 0.7 * (1 - t);
lines.push({ angle: angleDeg * Math.PI / 180, ox, oy });
render();
updateStats();
if (lines.length >= PRESET_ANGLES.length) {
document.getElementById('addLine').disabled = true;
}
});
document.getElementById('reset').addEventListener('click', () => {
lines.length = 0;
document.getElementById('addLine').disabled = false;
render();
updateStats();
});
// Initial render — blank canvas with no lines
render();
updateStats();
<!-- Line arrangement demo — add lines one at a time to see the plane subdivide -->
<p class="hint">Click <b>Add line</b> to insert one line at a time in general position. Watch vertices (V), edges (E) and faces (F) grow. With n lines: V = n(n-1)/2, E = n², F = n(n-1)/2 + n + 1.</p>
<canvas id="canvas" width="420" height="260"></canvas>
<div class="stats" id="stats">
<span><b>n</b> = <span id="n">0</span></span>
<span><b>V</b> = <span id="V">0</span></span>
<span><b>E</b> = <span id="E">0</span></span>
<span><b>F</b> = <span id="F">1</span></span>
</div>
<div class="formulas" id="formulas">Add lines to see the exact counts.</div>
<div class="btns">
<button id="addLine" type="button">Add line</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the line arrangement interactive demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f7f9fb; width: 100%; max-width: 420px; height: auto; }
.stats { display: flex; gap: 1.2rem; font-size: .95rem; margin: .5rem 0 .2rem;
flex-wrap: wrap; }
.formulas { font-size: .82rem; color: #5a6a7a; margin: 0 0 .5rem;
font-style: italic; min-height: 1.2em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Line arrangement demo — insert lines one by one, track exact V/E/F counts
// Formulas: V=n*(n-1)/2, E=n^2, F=n*(n-1)/2+n+1 (general position)
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Array of inserted lines; each line is { angle (radians), ox, oy (a point on the line) }
// Each line is parameterised as a point + direction so intersections are easy to compute.
const lines = [];
// Pre-set angles and offsets so lines stay in general position (no two parallel, no three concurrent)
// Angles in degrees for up to 10 preset lines
const PRESET_ANGLES = [28, 112, 65, 155, 10, 90, 45, 135, 170, 50];
const PRESET_OFFSETS = [0.38, 0.52, 0.25, 0.62, 0.45, 0.3, 0.7, 0.55, 0.42, 0.65];
// Closed-form cell counts for n lines in general position (Euler characteristic)
// V = n*(n-1)/2, E = n*n, F = n*(n-1)/2 + n + 1
// V = n*(n-1)/2, E = n*n, F = n*(n-1)/2 + n + 1
function countsForN(n) {
const V = n * (n - 1) / 2;
const E = n * n;
const F = V + n + 1;
return { V, E, F };
}
// Compute the intersection point of two infinite lines given as point + direction.
// Returns null if lines are parallel (denominator near zero).
function intersectLines(a, b) {
// Direction vector from angle
const dxA = Math.cos(a.angle), dyA = Math.sin(a.angle);
const dxB = Math.cos(b.angle), dyB = Math.sin(b.angle);
const denom = dxA * dyB - dyA * dxB;
if (Math.abs(denom) < 1e-9) return null; // lines are parallel or nearly so
const t = ((b.ox - a.ox) * dyB - (b.oy - a.oy) * dxB) / denom;
return { x: a.ox + t * dxA, y: a.oy + t * dyA };
}
// Draw a single line extended far beyond the canvas boundaries.
function drawLine(line, color) {
const dx = Math.cos(line.angle), dy = Math.sin(line.angle);
// Extend 1000 px in both directions so the line spans the whole canvas.
const big = 1000;
ctx.beginPath();
ctx.moveTo(line.ox - big * dx, line.oy - big * dy);
ctx.lineTo(line.ox + big * dx, line.oy + big * dy);
ctx.strokeStyle = color;
ctx.lineWidth = 1.4;
ctx.stroke();
}
// Distinct colors for up to 10 lines
const COLORS = ['#1d3557','#e63946','#2a9d8f','#e9c46a','#6a4c93','#f4a261','#264653','#a8dadc','#457b9d','#e76f51'];
function render() {
ctx.clearRect(0, 0, W, H);
// Draw every inserted line
lines.forEach((l, i) => drawLine(l, COLORS[i % COLORS.length]));
// Draw intersection dots on top of the lines
lines.forEach((a, i) => {
lines.forEach((b, j) => {
if (j <= i) return;
const pt = intersectLines(a, b);
if (!pt) return;
if (pt.x < -50 || pt.x > W + 50 || pt.y < -50 || pt.y > H + 50) return;
ctx.beginPath();
ctx.arc(pt.x, pt.y, 3.5, 0, 2 * Math.PI);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.stroke();
});
});
}
function updateStats() {
const n = lines.length;
const { V, E, F } = countsForN(n);
document.getElementById('n').textContent = n;
document.getElementById('V').textContent = V;
document.getElementById('E').textContent = E;
document.getElementById('F').textContent = F;
const fEl = document.getElementById('formulas');
if (n === 0) {
fEl.textContent = 'Add lines to see the exact counts.';
} else {
// Update the formula readout below the stats
fEl.textContent = fmt('n = %n% lines → V=%V%, E=%E%, F=%F%', { n: n, V: V, E: E, F: F });
}
}
document.getElementById('addLine').addEventListener('click', () => {
const i = lines.length;
if (i >= PRESET_ANGLES.length) {
document.getElementById('addLine').disabled = true;
return;
}
// Pick angle and a point from the preset list
const angleDeg = PRESET_ANGLES[i];
const t = PRESET_OFFSETS[i];
// Position the line's reference point in the central region of the canvas
const ox = W * 0.15 + W * 0.7 * t;
const oy = H * 0.15 + H * 0.7 * (1 - t);
lines.push({ angle: angleDeg * Math.PI / 180, ox, oy });
render();
updateStats();
if (lines.length >= PRESET_ANGLES.length) {
document.getElementById('addLine').disabled = true;
}
});
document.getElementById('reset').addEventListener('click', () => {
lines.length = 0;
document.getElementById('addLine').disabled = false;
render();
updateStats();
});
// Initial render — blank canvas with no lines
render();
updateStats();
Comments
Loading comments...