<!-- Band structure demo — nearly-free electron model -->
<div class="controls">
<label>
<span class="lbl">Potential strength V₀</span>
<input id="potSlider" type="range" min="0" max="100" value="0" step="1">
<span id="potVal" class="val">0</span>
</label>
<label>
<span class="lbl">Electron fill (band fraction)</span>
<input id="fillSlider" type="range" min="0" max="2" value="1" step="0.01">
<span id="fillVal" class="val">1.00</span>
</label>
</div>
<canvas id="canvas" width="480" height="280" aria-label="Energy band diagram showing allowed bands and forbidden gaps"></canvas>
<div id="material" class="material-label"></div>
<div class="legend">
<span class="dot dot-band"></span> Allowed band
<span class="dot dot-gap"></span> Forbidden gap
<span class="dot dot-fermi"></span> Fermi level
</div>
/* Styles for the band-structure interactive demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f7f9fb; color: #1d2d3e; }
.controls { display: flex; flex-direction: column; gap: .5rem; padding: .6rem .8rem .2rem; }
label { display: flex; align-items: center; gap: .5rem; font-size: .88rem; }
.lbl { width: 10rem; flex-shrink: 0; }
input[type=range] { flex: 1; accent-color: #1d6fa4; }
.val { width: 2.8rem; text-align: right; font-variant-numeric: tabular-nums; font-size: .88rem; }
canvas { display: block; margin: .3rem auto 0; border-radius: 8px;
background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.12); }
.material-label { text-align: center; font-size: 1rem; font-weight: 700;
min-height: 1.6em; margin: .35rem 0 .1rem; letter-spacing: .02em; }
.material-label.metal { color: #b8860b; }
.material-label.semi { color: #1d6fa4; }
.material-label.ins { color: #c0392b; }
.legend { display: flex; align-items: center; gap: 1rem; justify-content: center;
font-size: .8rem; color: #555; padding: .15rem 0 .4rem; flex-wrap: wrap; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: .2rem; }
.dot-band { background: #1d6fa4; }
.dot-gap { background: #fce4d6; border: 1px solid #e8967a; }
.dot-fermi { background: #e05c2b; height: 3px; border-radius: 1px; width: 18px; }
/* Nearly-free electron model band structure visualizer */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const potSlider = document.getElementById('potSlider');
const fillSlider = document.getElementById('fillSlider');
const potValEl = document.getElementById('potVal');
const fillValEl = document.getElementById('fillVal');
const materialEl = document.getElementById('material');
/* Canvas geometry constants */
const W = canvas.width, H = canvas.height;
const PAD = { l: 48, r: 16, t: 20, b: 28 };
const plotW = W - PAD.l - PAD.r;
const plotH = H - PAD.t - PAD.b;
/* Compute band energies using the 2x2 nearly-free-electron model */
function bandEnergies(k, V0) {
/* Two parabolas coupled by the periodic potential V0 */
const hbar2_2m = 1.0; // energy units set so hbar2/2m = 1
const G = 2 * Math.PI; // first reciprocal lattice vector (2*pi/a, with a=1)
/* free-electron energies for k and k-G (folded into first Brillouin zone) */
const E0_minus = hbar2_2m * Math.pow(k - G, 2);
const E0_plus = hbar2_2m * Math.pow(k, 2);
const E0_plus2 = hbar2_2m * Math.pow(k + G, 2);
const E0_minus2 = hbar2_2m * Math.pow(k - 2*G, 2);
/* 2x2 degenerate perturbation theory — E = avg +/- sqrt(delta2 + V02) */
const avg1 = (E0_minus + E0_plus) / 2;
const gap1 = Math.sqrt(Math.pow((E0_minus - E0_plus)/2, 2) + V0*V0);
const E1 = avg1 - gap1;
const E2 = avg1 + gap1;
const avg2 = (E0_plus2 + E0_plus) / 2;
const gap2 = Math.sqrt(Math.pow((E0_plus2 - E0_plus)/2, 2) + V0*V0);
const E3 = avg2 - gap2;
const E4 = avg2 + gap2;
const avg3 = (E0_minus2 + E0_minus) / 2;
const gap3 = Math.sqrt(Math.pow((E0_minus2 - E0_minus)/2, 2) + V0*V0);
const E5 = avg3 - gap3;
return [E1, E2, E3, E4, E5];
}
function getV0() {
return parseFloat(potSlider.value) / 100 * 0.8;
}
function getFillFraction() {
return parseFloat(fillSlider.value);
}
/* Main draw function — called on every slider change */
function draw() {
ctx.clearRect(0, 0, W, H);
const V0 = getV0();
const fill = getFillFraction();
/* Sample the full first Brillouin zone */
const NK = 200;
const kMin = -Math.PI, kMax = Math.PI;
const bands = [[], [], [], [], []];
for (let i = 0; i <= NK; i++) {
const k = kMin + (kMax - kMin) * i / NK;
const en = bandEnergies(k, V0);
for (let b = 0; b < 5; b++) bands[b].push({ k, e: en[b] });
}
/* Find energy range for scaling the y-axis */
let eMin = Infinity, eMax = -Infinity;
for (const band of bands) for (const p of band) {
if (p.e < eMin) eMin = p.e;
if (p.e > eMax) eMax = p.e;
}
const ePad = (eMax - eMin) * 0.05;
eMin -= ePad; eMax += ePad;
const toX = (k) => PAD.l + (k - kMin) / (kMax - kMin) * plotW;
const toY = (e) => PAD.t + (1 - (e - eMin) / (eMax - eMin)) * plotH;
/* Shade the first forbidden gap between band 1 and band 2 */
if (V0 > 0.01) {
const gapTop = bands[1].reduce((mn, p) => Math.min(mn, p.e), Infinity);
const gapBot = bands[0].reduce((mx, p) => Math.max(mx, p.e), -Infinity);
if (gapTop > gapBot) {
const y1 = toY(gapTop), y2 = toY(gapBot);
ctx.fillStyle = 'rgba(252,228,214,0.75)';
ctx.fillRect(PAD.l, y1, plotW, y2 - y1);
ctx.strokeStyle = '#e8967a';
ctx.lineWidth = 0.8;
ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(PAD.l, y1); ctx.lineTo(PAD.l + plotW, y1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(PAD.l, y2); ctx.lineTo(PAD.l + plotW, y2); ctx.stroke();
ctx.setLineDash([]);
}
}
/* Draw axes and k-point labels */
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1; ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(PAD.l, PAD.t); ctx.lineTo(PAD.l, PAD.t + plotH);
ctx.moveTo(PAD.l, PAD.t + plotH); ctx.lineTo(PAD.l + plotW, PAD.t + plotH);
ctx.stroke();
ctx.fillStyle = '#888'; ctx.font = '11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('-π/a', toX(kMin), PAD.t + plotH + 16);
ctx.fillText('0', toX(0), PAD.t + plotH + 16);
ctx.fillText('+π/a', toX(kMax), PAD.t + plotH + 16);
ctx.textAlign = 'right';
ctx.fillText('E', PAD.l - 4, PAD.t + 6);
ctx.textAlign = 'left';
/* Draw each energy band as a colored curve */
const colors = ['#1d6fa4', '#2e8b57', '#7b2d8b', '#b8860b', '#555'];
for (let b = 0; b < 5; b++) {
ctx.beginPath();
ctx.strokeStyle = colors[b % colors.length];
ctx.lineWidth = 2;
for (let i = 0; i < bands[b].length; i++) {
const { k, e } = bands[b][i];
const x = toX(k), y = toY(e);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
}
/* Draw the Fermi level at the fill fraction of all sampled states */
const allE = bands.flat().map(p => p.e).sort((a, b) => a - b);
const fermiIdx = Math.floor(fill * (allE.length - 1));
const fermiE = allE[Math.max(0, Math.min(fermiIdx, allE.length - 1))];
const yF = toY(fermiE);
ctx.strokeStyle = '#e05c2b'; ctx.lineWidth = 2; ctx.setLineDash([6, 4]);
ctx.beginPath(); ctx.moveTo(PAD.l, yF); ctx.lineTo(PAD.l + plotW, yF); ctx.stroke();
ctx.setLineDash([]);
/* Classify material based on Fermi level position and gap width */
const gapTop2 = V0 > 0.01 ? bands[1].reduce((mn, p) => Math.min(mn, p.e), Infinity) : null;
const gapBot2 = V0 > 0.01 ? bands[0].reduce((mx, p) => Math.max(mx, p.e), -Infinity) : null;
const inGap = gapTop2 !== null && fermiE > gapBot2 && fermiE < gapTop2;
const gapWidth = (gapTop2 !== null && V0 > 0.01) ? (gapTop2 - gapBot2) : 0;
let matClass = 'metal', matText;
if (V0 < 0.01) {
matText = i18nFmt('Free electrons — no gap (not a real crystal)', {});
} else if (inGap && gapWidth > 0.3) {
matClass = 'ins'; matText = i18nFmt('Insulator — Fermi level sits inside a large gap', {});
} else if (inGap && gapWidth <= 0.3) {
matClass = 'semi'; matText = i18nFmt('Semiconductor — Fermi level sits inside a small gap', {});
} else {
matClass = 'metal'; matText = i18nFmt('Metal — Fermi level inside a band', {});
}
materialEl.className = 'material-label ' + matClass;
materialEl.textContent = matText;
}
potSlider.addEventListener('input', () => {
potValEl.textContent = potSlider.value;
draw();
});
fillSlider.addEventListener('input', () => {
fillValEl.textContent = parseFloat(fillSlider.value).toFixed(2);
draw();
});
draw();
<!-- Band structure demo — nearly-free electron model -->
<div class="controls">
<label>
<span class="lbl">Potential strength V₀</span>
<input id="potSlider" type="range" min="0" max="100" value="0" step="1">
<span id="potVal" class="val">0</span>
</label>
<label>
<span class="lbl">Electron fill (band fraction)</span>
<input id="fillSlider" type="range" min="0" max="2" value="1" step="0.01">
<span id="fillVal" class="val">1.00</span>
</label>
</div>
<canvas id="canvas" width="480" height="280" aria-label="Energy band diagram showing allowed bands and forbidden gaps"></canvas>
<div id="material" class="material-label"></div>
<div class="legend">
<span class="dot dot-band"></span> Allowed band
<span class="dot dot-gap"></span> Forbidden gap
<span class="dot dot-fermi"></span> Fermi level
</div>
/* Styles for the band-structure interactive demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f7f9fb; color: #1d2d3e; }
.controls { display: flex; flex-direction: column; gap: .5rem; padding: .6rem .8rem .2rem; }
label { display: flex; align-items: center; gap: .5rem; font-size: .88rem; }
.lbl { width: 10rem; flex-shrink: 0; }
input[type=range] { flex: 1; accent-color: #1d6fa4; }
.val { width: 2.8rem; text-align: right; font-variant-numeric: tabular-nums; font-size: .88rem; }
canvas { display: block; margin: .3rem auto 0; border-radius: 8px;
background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.12); }
.material-label { text-align: center; font-size: 1rem; font-weight: 700;
min-height: 1.6em; margin: .35rem 0 .1rem; letter-spacing: .02em; }
.material-label.metal { color: #b8860b; }
.material-label.semi { color: #1d6fa4; }
.material-label.ins { color: #c0392b; }
.legend { display: flex; align-items: center; gap: 1rem; justify-content: center;
font-size: .8rem; color: #555; padding: .15rem 0 .4rem; flex-wrap: wrap; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: .2rem; }
.dot-band { background: #1d6fa4; }
.dot-gap { background: #fce4d6; border: 1px solid #e8967a; }
.dot-fermi { background: #e05c2b; height: 3px; border-radius: 1px; width: 18px; }
/* Nearly-free electron model band structure visualizer */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const potSlider = document.getElementById('potSlider');
const fillSlider = document.getElementById('fillSlider');
const potValEl = document.getElementById('potVal');
const fillValEl = document.getElementById('fillVal');
const materialEl = document.getElementById('material');
/* Canvas geometry constants */
const W = canvas.width, H = canvas.height;
const PAD = { l: 48, r: 16, t: 20, b: 28 };
const plotW = W - PAD.l - PAD.r;
const plotH = H - PAD.t - PAD.b;
/* Compute band energies using the 2x2 nearly-free-electron model */
function bandEnergies(k, V0) {
/* Two parabolas coupled by the periodic potential V0 */
const hbar2_2m = 1.0; // energy units set so hbar2/2m = 1
const G = 2 * Math.PI; // first reciprocal lattice vector (2*pi/a, with a=1)
/* free-electron energies for k and k-G (folded into first Brillouin zone) */
const E0_minus = hbar2_2m * Math.pow(k - G, 2);
const E0_plus = hbar2_2m * Math.pow(k, 2);
const E0_plus2 = hbar2_2m * Math.pow(k + G, 2);
const E0_minus2 = hbar2_2m * Math.pow(k - 2*G, 2);
/* 2x2 degenerate perturbation theory — E = avg +/- sqrt(delta2 + V02) */
const avg1 = (E0_minus + E0_plus) / 2;
const gap1 = Math.sqrt(Math.pow((E0_minus - E0_plus)/2, 2) + V0*V0);
const E1 = avg1 - gap1;
const E2 = avg1 + gap1;
const avg2 = (E0_plus2 + E0_plus) / 2;
const gap2 = Math.sqrt(Math.pow((E0_plus2 - E0_plus)/2, 2) + V0*V0);
const E3 = avg2 - gap2;
const E4 = avg2 + gap2;
const avg3 = (E0_minus2 + E0_minus) / 2;
const gap3 = Math.sqrt(Math.pow((E0_minus2 - E0_minus)/2, 2) + V0*V0);
const E5 = avg3 - gap3;
return [E1, E2, E3, E4, E5];
}
function getV0() {
return parseFloat(potSlider.value) / 100 * 0.8;
}
function getFillFraction() {
return parseFloat(fillSlider.value);
}
/* Main draw function — called on every slider change */
function draw() {
ctx.clearRect(0, 0, W, H);
const V0 = getV0();
const fill = getFillFraction();
/* Sample the full first Brillouin zone */
const NK = 200;
const kMin = -Math.PI, kMax = Math.PI;
const bands = [[], [], [], [], []];
for (let i = 0; i <= NK; i++) {
const k = kMin + (kMax - kMin) * i / NK;
const en = bandEnergies(k, V0);
for (let b = 0; b < 5; b++) bands[b].push({ k, e: en[b] });
}
/* Find energy range for scaling the y-axis */
let eMin = Infinity, eMax = -Infinity;
for (const band of bands) for (const p of band) {
if (p.e < eMin) eMin = p.e;
if (p.e > eMax) eMax = p.e;
}
const ePad = (eMax - eMin) * 0.05;
eMin -= ePad; eMax += ePad;
const toX = (k) => PAD.l + (k - kMin) / (kMax - kMin) * plotW;
const toY = (e) => PAD.t + (1 - (e - eMin) / (eMax - eMin)) * plotH;
/* Shade the first forbidden gap between band 1 and band 2 */
if (V0 > 0.01) {
const gapTop = bands[1].reduce((mn, p) => Math.min(mn, p.e), Infinity);
const gapBot = bands[0].reduce((mx, p) => Math.max(mx, p.e), -Infinity);
if (gapTop > gapBot) {
const y1 = toY(gapTop), y2 = toY(gapBot);
ctx.fillStyle = 'rgba(252,228,214,0.75)';
ctx.fillRect(PAD.l, y1, plotW, y2 - y1);
ctx.strokeStyle = '#e8967a';
ctx.lineWidth = 0.8;
ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(PAD.l, y1); ctx.lineTo(PAD.l + plotW, y1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(PAD.l, y2); ctx.lineTo(PAD.l + plotW, y2); ctx.stroke();
ctx.setLineDash([]);
}
}
/* Draw axes and k-point labels */
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1; ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(PAD.l, PAD.t); ctx.lineTo(PAD.l, PAD.t + plotH);
ctx.moveTo(PAD.l, PAD.t + plotH); ctx.lineTo(PAD.l + plotW, PAD.t + plotH);
ctx.stroke();
ctx.fillStyle = '#888'; ctx.font = '11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('-π/a', toX(kMin), PAD.t + plotH + 16);
ctx.fillText('0', toX(0), PAD.t + plotH + 16);
ctx.fillText('+π/a', toX(kMax), PAD.t + plotH + 16);
ctx.textAlign = 'right';
ctx.fillText('E', PAD.l - 4, PAD.t + 6);
ctx.textAlign = 'left';
/* Draw each energy band as a colored curve */
const colors = ['#1d6fa4', '#2e8b57', '#7b2d8b', '#b8860b', '#555'];
for (let b = 0; b < 5; b++) {
ctx.beginPath();
ctx.strokeStyle = colors[b % colors.length];
ctx.lineWidth = 2;
for (let i = 0; i < bands[b].length; i++) {
const { k, e } = bands[b][i];
const x = toX(k), y = toY(e);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
}
/* Draw the Fermi level at the fill fraction of all sampled states */
const allE = bands.flat().map(p => p.e).sort((a, b) => a - b);
const fermiIdx = Math.floor(fill * (allE.length - 1));
const fermiE = allE[Math.max(0, Math.min(fermiIdx, allE.length - 1))];
const yF = toY(fermiE);
ctx.strokeStyle = '#e05c2b'; ctx.lineWidth = 2; ctx.setLineDash([6, 4]);
ctx.beginPath(); ctx.moveTo(PAD.l, yF); ctx.lineTo(PAD.l + plotW, yF); ctx.stroke();
ctx.setLineDash([]);
/* Classify material based on Fermi level position and gap width */
const gapTop2 = V0 > 0.01 ? bands[1].reduce((mn, p) => Math.min(mn, p.e), Infinity) : null;
const gapBot2 = V0 > 0.01 ? bands[0].reduce((mx, p) => Math.max(mx, p.e), -Infinity) : null;
const inGap = gapTop2 !== null && fermiE > gapBot2 && fermiE < gapTop2;
const gapWidth = (gapTop2 !== null && V0 > 0.01) ? (gapTop2 - gapBot2) : 0;
let matClass = 'metal', matText;
if (V0 < 0.01) {
matText = i18nFmt('Free electrons — no gap (not a real crystal)', {});
} else if (inGap && gapWidth > 0.3) {
matClass = 'ins'; matText = i18nFmt('Insulator — Fermi level sits inside a large gap', {});
} else if (inGap && gapWidth <= 0.3) {
matClass = 'semi'; matText = i18nFmt('Semiconductor — Fermi level sits inside a small gap', {});
} else {
matClass = 'metal'; matText = i18nFmt('Metal — Fermi level inside a band', {});
}
materialEl.className = 'material-label ' + matClass;
materialEl.textContent = matText;
}
potSlider.addEventListener('input', () => {
potValEl.textContent = potSlider.value;
draw();
});
fillSlider.addEventListener('input', () => {
fillValEl.textContent = parseFloat(fillSlider.value).toFixed(2);
draw();
});
draw();
Watch how the first gap (between the first and second band) widens as the potential grows. The Fermi level line shows where electrons fill up to at absolute zero. Drag it into a gap and you have an insulator or semiconductor; leave it inside a band and you have a metal.
Comments
Loading comments...