fix(phys-fx): EnergyLevels — разрывная шкала, верхняя зона n=2..6 растянута, маркер разрыва оси

This commit is contained in:
Maxim Dolgolyov
2026-06-01 12:31:44 +03:00
parent 7df33e533e
commit 30626e0928
+80 -71
View File
@@ -2025,119 +2025,128 @@ class EnergyLevels {
update(){}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const padTop = 36, padBot = 30;
const W = this.W;
/* Внутренняя высота увеличена для читаемости */
const H = Math.max(this.H, 440);
const padTop = 36, padBot = 32;
const bandW = 16, bandLeft = 4;
const leftLine = 170, rightLine = W - 80;
const leftLine = 170, rightLine = W - 82;
const nMax = 6;
const nFrom = this.n_from, nTo = this.n_to;
function E(n){ return -13.6 / (n * n); }
/* Разрывная шкала:
— верхняя зона (70% высоты): n=2..6 + ионизация, линейная по E
— нижняя зона (30% высоты): только n=1, сжата с маркером разрыва */
const upperH = Math.round((H - padTop - padBot) * 0.72);
const splitY = padTop + upperH; /* граница зон в пикселях */
const y1fixed = H - padBot - 16; /* фиксированная y для n=1 */
const E2 = E(2), maxE = 0.4;
function yE(En){
const minE = -14.2, maxE = 0.4;
return padTop + (maxE - En) / (maxE - minE) * (H - padTop - padBot);
if (En <= E2 - 0.05){
/* Нижняя зона: линейная E(1)→E(2) → y1fixed→splitY */
const t = (En - E(1)) / (E2 - E(1));
return y1fixed - t * (y1fixed - splitY);
}
/* Верхняя зона: линейная E(2)→maxE → splitY→padTop */
return padTop + (maxE - En) / (maxE - E2) * upperH;
}
/* SVG открытие: белый фон, рамка */
/* SVG */
let svg = '<svg width="' + W + '" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '" xmlns="http://www.w3.org/2000/svg">';
svg += '<rect x="0" y="0" width="' + W + '" height="' + H + '" rx="14" fill="#fff" stroke="#e2e8f0" stroke-width="1.2"/>';
/* Заголовок */
svg += '<text x="' + (W / 2) + '" y="22" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#64748b">E&#8345; = 13,6/n² эВ</text>';
svg += '<text x="' + (W / 2) + '" y="23" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11.5" fill="#64748b" font-weight="600">E&#8345; = 13,6 / эВ (атом водорода)</text>';
/* Серии — цветные полосы между уровнями */
const seriesBands = [
{ n1: 1, n2: 2, fill: '#ede9fe', textFill: '#6d28d9', label: 'Лайман' },
{ n1: 2, n2: 3, fill: '#dbeafe', textFill: '#1d4ed8', label: 'Бальмер' },
{ n1: 3, n2: 6, fill: '#d1fae5', textFill: '#065f46', label: 'Пашен' },
/* Цветные полосы серий */
const seriesData = [
{ yTop: yE(E2), yBot: y1fixed, fill: '#ede9fe', tc: '#6d28d9', label: 'Лайман' },
{ yTop: yE(E(3)), yBot: yE(E2), fill: '#dbeafe', tc: '#1d4ed8', label: 'Бальмер' },
{ yTop: padTop, yBot: yE(E(3)),fill: '#d1fae5', tc: '#065f46', label: 'Пашен' },
];
for (const b of seriesBands){
const yTop = yE(E(b.n2));
const yBot = yE(E(b.n1));
const bx = bandLeft;
const bh = yBot - yTop;
svg += '<rect x="' + bx + '" y="' + yTop.toFixed(1) + '" width="' + bandW + '" height="' + bh.toFixed(1) + '" rx="3" fill="' + b.fill + '"/>';
/* Вертикальная метка внутри полосы */
const ymid = ((yTop + yBot) / 2).toFixed(1);
svg += '<text x="' + (bx + bandW / 2) + '" y="' + ymid + '" text-anchor="middle" dominant-baseline="middle" font-family="JetBrains Mono,monospace" font-size="8.5" fill="' + b.textFill + '" font-weight="700" transform="rotate(-90,' + (bx + bandW / 2) + ',' + ymid + ')">' + b.label + '</text>';
for (const b of seriesData){
const bh = b.yBot - b.yTop;
if (bh < 6) continue;
svg += '<rect x="' + bandLeft + '" y="' + b.yTop.toFixed(1) + '" width="' + bandW + '" height="' + bh.toFixed(1) + '" rx="3" fill="' + b.fill + '"/>';
const ym = ((b.yTop + b.yBot) / 2).toFixed(1);
const bx2 = bandLeft + bandW / 2;
svg += '<text x="' + bx2 + '" y="' + ym + '" text-anchor="middle" dominant-baseline="middle" font-family="JetBrains Mono,monospace" font-size="8.5" fill="' + b.tc + '" font-weight="700" transform="rotate(-90,' + bx2 + ',' + ym + ')">' + b.label + '</text>';
}
/* Вертикальная ось слева от уровней */
/* Вертикальная ось — верхняя часть */
const axisX = leftLine - 3;
svg += '<line x1="' + axisX + '" y1="' + padTop + '" x2="' + axisX + '" y2="' + (H - padBot) + '" stroke="#cbd5e1" stroke-width="1"/>';
svg += '<line x1="' + axisX + '" y1="' + padTop + '" x2="' + axisX + '" y2="' + (splitY - 6) + '" stroke="#cbd5e1" stroke-width="1"/>';
/* Ось нижней части */
svg += '<line x1="' + axisX + '" y1="' + (splitY + 22) + '" x2="' + axisX + '" y2="' + (H - padBot) + '" stroke="#cbd5e1" stroke-width="1"/>';
/* Линия ионизации E=0 */
/* Маркер разрыва оси */
const bMid = (splitY + splitY + 22) / 2;
for (const dy of [-5, 5]){
const my = bMid + dy;
svg += '<line x1="' + (axisX - 5) + '" y1="' + (my - 5) + '" x2="' + (axisX + 5) + '" y2="' + (my + 5) + '" stroke="#94a3b8" stroke-width="1.4" stroke-linecap="round"/>';
}
/* Подпись разрыва */
svg += '<text x="' + (axisX + 8) + '" y="' + (bMid + 4) + '" font-family="JetBrains Mono,monospace" font-size="8" fill="#94a3b8">∿ масштаб</text>';
/* Линия ионизации */
const y0 = yE(0);
svg += '<line x1="' + leftLine + '" y1="' + y0.toFixed(1) + '" x2="' + rightLine + '" y2="' + y0.toFixed(1) + '" stroke="#dc2626" stroke-width="1.4" stroke-dasharray="5 3"/>';
svg += '<text x="' + (leftLine + 4) + '" y="' + (y0 - 3).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="9" fill="#dc2626" font-weight="700">E = 0 (ионизация)</text>';
svg += '<line x1="' + leftLine + '" y1="' + y0.toFixed(1) + '" x2="' + rightLine + '" y2="' + y0.toFixed(1) + '" stroke="#ef4444" stroke-width="1.4" stroke-dasharray="6 3"/>';
svg += '<text x="' + (leftLine + 5) + '" y="' + (y0 - 4).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="9" fill="#ef4444" font-weight="600">E = 0 (ионизация)</text>';
/* Уровни n=1..6 */
for (let n = 1; n <= nMax; n++){
const En = E(n);
const yL = yE(En);
const isFrom = (n === nFrom);
const isTo = (n === nTo);
const isFrom = (n === nFrom), isTo = (n === nTo);
const active = isFrom || isTo;
const lineColor = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#94a3b8';
const lc = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#94a3b8';
const sw = active ? 2.4 : 1;
/* Glow под активными */
if (active){
svg += '<line x1="' + leftLine + '" y1="' + yL.toFixed(1) + '" x2="' + rightLine + '" y2="' + yL.toFixed(1) + '" stroke="' + lineColor + '" stroke-width="7" opacity="0.08"/>';
svg += '<line x1="' + leftLine + '" y1="' + yL.toFixed(1) + '" x2="' + rightLine + '" y2="' + yL.toFixed(1) + '" stroke="' + lc + '" stroke-width="8" opacity="0.07"/>';
}
svg += '<line x1="' + leftLine + '" y1="' + yL.toFixed(1) + '" x2="' + rightLine + '" y2="' + yL.toFixed(1) + '" stroke="' + lineColor + '" stroke-width="' + sw + '"/>';
/* Метка n= слева от оси */
const labelX = axisX - 4;
const labelY = yL.toFixed(1);
const labelColor = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#475569';
const labelFW = active ? '800' : '500';
svg += '<line x1="' + leftLine + '" y1="' + yL.toFixed(1) + '" x2="' + rightLine + '" y2="' + yL.toFixed(1) + '" stroke="' + lc + '" stroke-width="' + sw + '"/>';
/* Метка n= */
const lx = axisX - 4;
const textC = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#475569';
if (active){
svg += '<rect x="' + (labelX - 24) + '" y="' + (yL - 8).toFixed(1) + '" width="26" height="14" rx="3" fill="' + lineColor + '" opacity="0.12"/>';
svg += '<rect x="' + (lx - 24) + '" y="' + (yL - 8).toFixed(1) + '" width="26" height="15" rx="3" fill="' + lc + '" opacity="0.12"/>';
}
svg += '<text x="' + labelX + '" y="' + (yL + 4).toFixed(1) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="' + labelColor + '" font-weight="' + labelFW + '">n=' + n + '</text>';
/* Значения энергии справа */
const eColor = active ? '#334155' : '#94a3b8';
const eFW = active ? '600' : '400';
svg += '<text x="' + (rightLine + 5) + '" y="' + (yL + 4).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="9.5" fill="' + eColor + '" font-weight="' + eFW + '">' + En.toFixed(2) + 'эВ</text>';
svg += '<text x="' + lx + '" y="' + (yL + 4).toFixed(1) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="' + textC + '" font-weight="' + (active ? '800' : '500') + '">n=' + n + '</text>';
/* Значение энергии */
const ec = active ? '#334155' : '#94a3b8';
svg += '<text x="' + (rightLine + 5) + '" y="' + (yL + 4).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="9.5" fill="' + ec + '" font-weight="' + (active ? '600' : '400') + '">' + En.toFixed(2) + ' эВ</text>';
}
/* Стрелка перехода */
if (nFrom !== nTo){
const Ef = E(nFrom), Et = E(nTo);
const yf = yE(Ef), yt = yE(Et);
const emission = Ef > Et; /* испускание: падаем вниз по энергии */
const arrowColor = emission ? '#ef4444' : '#16a34a';
const emission = Ef > Et;
const ac = emission ? '#ef4444' : '#16a34a';
const xT = leftLine + (rightLine - leftLine) * 0.42;
/* Линия стрелки */
svg += '<line x1="' + xT.toFixed(1) + '" y1="' + yf.toFixed(1) + '" x2="' + xT.toFixed(1) + '" y2="' + yt.toFixed(1) + '" stroke="' + arrowColor + '" stroke-width="2.2"/>';
/* Наконечник в точке назначения yt */
const dir = yt < yf ? -1 : 1; /* направление стрелки */
const tip = yt.toFixed(1);
const tipBase = (yt + 10 * dir).toFixed(1);
svg += '<polygon points="' + xT.toFixed(1) + ',' + tip + ' ' + (xT - 6).toFixed(1) + ',' + tipBase + ' ' + (xT + 6).toFixed(1) + ',' + tipBase + '" fill="' + arrowColor + '"/>';
svg += '<line x1="' + xT.toFixed(1) + '" y1="' + yf.toFixed(1) + '" x2="' + xT.toFixed(1) + '" y2="' + yt.toFixed(1) + '" stroke="' + ac + '" stroke-width="2.4" stroke-linecap="round"/>';
const dir = yt < yf ? -1 : 1;
svg += '<polygon points="' + xT + ',' + yt.toFixed(1) + ' ' + (xT - 7) + ',' + (yt + 11 * dir).toFixed(1) + ' ' + (xT + 7) + ',' + (yt + 11 * dir).toFixed(1) + '" fill="' + ac + '"/>';
/* Info-box */
const dE = Math.abs(Ef - Et);
const lam = 1240 / dE;
const boxW = 88, boxH = lam >= 380 && lam <= 700 ? 48 : 40;
const bxW = 94, bxH = lam >= 380 && lam <= 700 ? 50 : 42;
const yMid = (yf + yt) / 2;
let bx = xT + 10;
if (bx + boxW > W - 4) bx = xT - boxW - 10;
const boxFill = emission ? '#fff5f5' : '#f0fdf4';
const boxStroke = emission ? '#fca5a5' : '#86efac';
svg += '<rect x="' + bx.toFixed(1) + '" y="' + (yMid - boxH / 2).toFixed(1) + '" width="' + boxW + '" height="' + boxH + '" rx="6" fill="' + boxFill + '" stroke="' + boxStroke + '" stroke-width="1.2"/>';
svg += '<text x="' + (bx + 7).toFixed(1) + '" y="' + (yMid - boxH / 2 + 15).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11.5" fill="' + arrowColor + '" font-weight="800">hν = ' + dE.toFixed(3) + ' эВ</text>';
svg += '<text x="' + (bx + 7).toFixed(1) + '" y="' + (yMid - boxH / 2 + 29).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="' + arrowColor + '">λ ≈ ' + lam.toFixed(0) + ' нм</text>';
/* Цветовая метка для видимого диапазона */
let bxLeft = xT + 12;
if (bxLeft + bxW > W - 4) bxLeft = xT - bxW - 12;
const bf = emission ? '#fff5f5' : '#f0fdf4';
const bs = emission ? '#fca5a5' : '#86efac';
svg += '<rect x="' + bxLeft.toFixed(1) + '" y="' + (yMid - bxH / 2).toFixed(1) + '" width="' + bxW + '" height="' + bxH + '" rx="7" fill="' + bf + '" stroke="' + bs + '" stroke-width="1.2"/>';
svg += '<text x="' + (bxLeft + 8).toFixed(1) + '" y="' + (yMid - bxH / 2 + 16).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11.5" fill="' + ac + '" font-weight="800">hν = ' + dE.toFixed(3) + ' эВ</text>';
svg += '<text x="' + (bxLeft + 8).toFixed(1) + '" y="' + (yMid - bxH / 2 + 31).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="' + ac + '">λ ≈ ' + lam.toFixed(0) + ' нм</text>';
if (lam >= 380 && lam <= 700){
const hue = Math.round(270 - (lam - 380) / (700 - 380) * 270);
const cy = yMid - boxH / 2 + 39;
svg += '<defs><linearGradient id="vis_g" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="hsl(' + hue + ',90%,55%)"/><stop offset="100%" stop-color="hsl(' + (hue - 20) + ',90%,60%)"/></linearGradient></defs>';
svg += '<rect x="' + (bx + 7).toFixed(1) + '" y="' + cy.toFixed(1) + '" width="' + (boxW - 14) + '" height="6" rx="3" fill="url(#vis_g)"/>';
const hue = Math.round(270 - (lam - 380) / 320 * 270);
const cy = yMid - bxH / 2 + 40;
svg += '<defs><linearGradient id="vg"><stop offset="0%" stop-color="hsl(' + hue + ',88%,54%)"/><stop offset="100%" stop-color="hsl(' + (hue - 18) + ',88%,58%)"/></linearGradient></defs>';
svg += '<rect x="' + (bxLeft + 8).toFixed(1) + '" y="' + cy.toFixed(1) + '" width="' + (bxW - 16) + '" height="6" rx="3" fill="url(#vg)"/>';
}
}