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(){} update(){}
render(){ render(){
if (!this.el) return; if (!this.el) return;
const W = this.W, H = this.H; const W = this.W;
const padTop = 36, padBot = 30; /* Внутренняя высота увеличена для читаемости */
const H = Math.max(this.H, 440);
const padTop = 36, padBot = 32;
const bandW = 16, bandLeft = 4; const bandW = 16, bandLeft = 4;
const leftLine = 170, rightLine = W - 80; const leftLine = 170, rightLine = W - 82;
const nMax = 6; const nMax = 6;
const nFrom = this.n_from, nTo = this.n_to; const nFrom = this.n_from, nTo = this.n_to;
function E(n){ return -13.6 / (n * n); } 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){ function yE(En){
const minE = -14.2, maxE = 0.4; if (En <= E2 - 0.05){
return padTop + (maxE - En) / (maxE - minE) * (H - padTop - padBot); /* Нижняя зона: линейная 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">'; 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 += '<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 = [ const seriesData = [
{ n1: 1, n2: 2, fill: '#ede9fe', textFill: '#6d28d9', label: 'Лайман' }, { yTop: yE(E2), yBot: y1fixed, fill: '#ede9fe', tc: '#6d28d9', label: 'Лайман' },
{ n1: 2, n2: 3, fill: '#dbeafe', textFill: '#1d4ed8', label: 'Бальмер' }, { yTop: yE(E(3)), yBot: yE(E2), fill: '#dbeafe', tc: '#1d4ed8', label: 'Бальмер' },
{ n1: 3, n2: 6, fill: '#d1fae5', textFill: '#065f46', label: 'Пашен' }, { yTop: padTop, yBot: yE(E(3)),fill: '#d1fae5', tc: '#065f46', label: 'Пашен' },
]; ];
for (const b of seriesBands){ for (const b of seriesData){
const yTop = yE(E(b.n2)); const bh = b.yBot - b.yTop;
const yBot = yE(E(b.n1)); if (bh < 6) continue;
const bx = bandLeft; svg += '<rect x="' + bandLeft + '" y="' + b.yTop.toFixed(1) + '" width="' + bandW + '" height="' + bh.toFixed(1) + '" rx="3" fill="' + b.fill + '"/>';
const bh = yBot - yTop; const ym = ((b.yTop + b.yBot) / 2).toFixed(1);
svg += '<rect x="' + bx + '" y="' + yTop.toFixed(1) + '" width="' + bandW + '" height="' + bh.toFixed(1) + '" rx="3" fill="' + b.fill + '"/>'; 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 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>';
} }
/* Вертикальная ось слева от уровней */ /* Вертикальная ось — верхняя часть */
const axisX = leftLine - 3; 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); 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 += '<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 + 4) + '" y="' + (y0 - 3).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="9" fill="#dc2626" font-weight="700">E = 0 (ионизация)</text>'; 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 */ /* Уровни n=1..6 */
for (let n = 1; n <= nMax; n++){ for (let n = 1; n <= nMax; n++){
const En = E(n); const En = E(n);
const yL = yE(En); const yL = yE(En);
const isFrom = (n === nFrom); const isFrom = (n === nFrom), isTo = (n === nTo);
const isTo = (n === nTo);
const active = isFrom || isTo; const active = isFrom || isTo;
const lineColor = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#94a3b8'; const lc = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#94a3b8';
const sw = active ? 2.4 : 1; const sw = active ? 2.4 : 1;
/* Glow под активными */
if (active){ 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 + '"/>'; svg += '<line x1="' + leftLine + '" y1="' + yL.toFixed(1) + '" x2="' + rightLine + '" y2="' + yL.toFixed(1) + '" stroke="' + lc + '" stroke-width="' + sw + '"/>';
/* Метка n= */
/* Метка n= слева от оси */ const lx = axisX - 4;
const labelX = axisX - 4; const textC = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#475569';
const labelY = yL.toFixed(1);
const labelColor = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#475569';
const labelFW = active ? '800' : '500';
if (active){ 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>'; 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';
const eColor = 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>';
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>';
} }
/* Стрелка перехода */ /* Стрелка перехода */
if (nFrom !== nTo){ if (nFrom !== nTo){
const Ef = E(nFrom), Et = E(nTo); const Ef = E(nFrom), Et = E(nTo);
const yf = yE(Ef), yt = yE(Et); const yf = yE(Ef), yt = yE(Et);
const emission = Ef > Et; /* испускание: падаем вниз по энергии */ const emission = Ef > Et;
const arrowColor = emission ? '#ef4444' : '#16a34a'; const ac = emission ? '#ef4444' : '#16a34a';
const xT = leftLine + (rightLine - leftLine) * 0.42; 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="' + ac + '" stroke-width="2.4" stroke-linecap="round"/>';
/* Линия стрелки */ const dir = yt < yf ? -1 : 1;
svg += '<line x1="' + xT.toFixed(1) + '" y1="' + yf.toFixed(1) + '" x2="' + xT.toFixed(1) + '" y2="' + yt.toFixed(1) + '" stroke="' + arrowColor + '" stroke-width="2.2"/>'; svg += '<polygon points="' + xT + ',' + yt.toFixed(1) + ' ' + (xT - 7) + ',' + (yt + 11 * dir).toFixed(1) + ' ' + (xT + 7) + ',' + (yt + 11 * dir).toFixed(1) + '" fill="' + ac + '"/>';
/* Наконечник в точке назначения 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 + '"/>';
/* Info-box */ /* Info-box */
const dE = Math.abs(Ef - Et); const dE = Math.abs(Ef - Et);
const lam = 1240 / dE; 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; const yMid = (yf + yt) / 2;
let bx = xT + 10; let bxLeft = xT + 12;
if (bx + boxW > W - 4) bx = xT - boxW - 10; if (bxLeft + bxW > W - 4) bxLeft = xT - bxW - 12;
const boxFill = emission ? '#fff5f5' : '#f0fdf4'; const bf = emission ? '#fff5f5' : '#f0fdf4';
const boxStroke = emission ? '#fca5a5' : '#86efac'; const bs = 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 += '<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="' + (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="' + (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="' + (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>'; 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){ if (lam >= 380 && lam <= 700){
const hue = Math.round(270 - (lam - 380) / (700 - 380) * 270); const hue = Math.round(270 - (lam - 380) / 320 * 270);
const cy = yMid - boxH / 2 + 39; const cy = yMid - bxH / 2 + 40;
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 += '<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="' + (bx + 7).toFixed(1) + '" y="' + cy.toFixed(1) + '" width="' + (boxW - 14) + '" height="6" rx="3" fill="url(#vis_g)"/>'; svg += '<rect x="' + (bxLeft + 8).toFixed(1) + '" y="' + cy.toFixed(1) + '" width="' + (bxW - 16) + '" height="6" rx="3" fill="url(#vg)"/>';
} }
} }