style(phys-fx): редизайн EnergyLevels — белый фон, цветные полосы серий, info-box, glow на активных уровнях

This commit is contained in:
Maxim Dolgolyov
2026-06-01 12:27:20 +03:00
parent 3807c424c9
commit 7df33e533e
+107 -34
View File
@@ -2026,48 +2026,121 @@ class EnergyLevels {
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const pad = 40, leftLine = 200, rightLine = W - 100;
let svg = util.svgFrame(W, H, {bg:'#fef3c7'});
/* Уровни n=1..6 + ионизация */
const padTop = 36, padBot = 30;
const bandW = 16, bandLeft = 4;
const leftLine = 170, rightLine = W - 80;
const nMax = 6;
/* y(n) — нелинейное расположение, E пропорционально -1/n² */
const nFrom = this.n_from, nTo = this.n_to;
function E(n){ return -13.6 / (n * n); }
function y(En){
const minE = -13.6, maxE = 0;
return pad + (maxE - En) / (maxE - minE) * (H - 2 * pad);
function yE(En){
const minE = -14.2, maxE = 0.4;
return padTop + (maxE - En) / (maxE - minE) * (H - padTop - padBot);
}
/* Линия ионизации (E=0) */
const y0 = y(0);
svg += '<line x1="' + leftLine + '" y1="' + y0 + '" x2="' + rightLine + '" y2="' + y0 + '" stroke="#dc2626" stroke-width="1.6" stroke-dasharray="6 4"/>';
svg += '<text x="' + (rightLine + 6) + '" y="' + (y0 + 4) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#dc2626" font-weight="700">E = 0 (ионизация)</text>';
/* Уровни */
/* 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>';
/* Серии — цветные полосы между уровнями */
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: 'Пашен' },
];
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>';
}
/* Вертикальная ось слева от уровней */
const axisX = leftLine - 3;
svg += '<line x1="' + axisX + '" y1="' + padTop + '" x2="' + axisX + '" y2="' + (H - padBot) + '" stroke="#cbd5e1" stroke-width="1"/>';
/* Линия ионизации E=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 += '<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>';
/* Уровни n=1..6 */
for (let n = 1; n <= nMax; n++){
const En = E(n);
const yL = y(En);
const active = (n === this.n_from || n === this.n_to);
svg += '<line x1="' + leftLine + '" y1="' + yL.toFixed(1) + '" x2="' + rightLine + '" y2="' + yL.toFixed(1) + '" stroke="' + (active ? '#dc2626' : '#475569') + '" stroke-width="' + (active ? 2 : 1.2) + '"/>';
svg += '<text x="' + (leftLine - 8) + '" y="' + (yL + 4).toFixed(1) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="' + (active ? '#dc2626' : '#0f172a') + '" font-weight="' + (active ? '700' : '600') + '">n=' + n + '</text>';
svg += '<text x="' + (rightLine + 6) + '" y="' + (yL + 4).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">' + En.toFixed(2) + ' эВ</text>';
const yL = yE(En);
const isFrom = (n === nFrom);
const isTo = (n === nTo);
const active = isFrom || isTo;
const lineColor = 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="' + 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';
if (active){
svg += '<rect x="' + (labelX - 24) + '" y="' + (yL - 8).toFixed(1) + '" width="26" height="14" rx="3" fill="' + lineColor + '" 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>';
}
/* Переход — стрелка */
if (this.n_from !== this.n_to){
const E1 = E(this.n_from), E2 = E(this.n_to);
const y1 = y(E1), y2 = y(E2);
const xT = (leftLine + rightLine) / 2;
const color = E1 > E2 ? '#dc2626' : '#16a34a';
svg += '<line x1="' + xT + '" y1="' + y1.toFixed(1) + '" x2="' + xT + '" y2="' + y2.toFixed(1) + '" stroke="' + color + '" stroke-width="2.6"/>';
const dir = y2 < y1 ? -1 : 1;
svg += '<polygon points="' + xT + ',' + y2.toFixed(1) + ' ' + (xT - 6) + ',' + (y2 + 10 * dir).toFixed(1) + ' ' + (xT + 6) + ',' + (y2 + 10 * dir).toFixed(1) + '" fill="' + color + '"/>';
const dE = Math.abs(E1 - E2);
svg += '<text x="' + (xT + 12) + '" y="' + ((y1 + y2) / 2).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="' + color + '" font-weight="700">hν = ' + dE.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 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 + '"/>';
/* Info-box */
const dE = Math.abs(Ef - Et);
const lam = 1240 / dE;
svg += '<text x="' + (xT + 12) + '" y="' + ((y1 + y2) / 2 + 16).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="' + color + '">λ ≈ ' + lam.toFixed(0) + ' нм</text>';
const boxW = 88, boxH = lam >= 380 && lam <= 700 ? 48 : 40;
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>';
/* Цветовая метка для видимого диапазона */
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)"/>';
}
}
/* Серии */
svg += '<text x="14" y="' + (y(E(1)) + 4) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#9d174d" font-weight="700">Лайман →</text>';
svg += '<text x="14" y="' + (y(E(2)) + 4) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#9d174d" font-weight="700">Бальмер →</text>';
svg += '<text x="14" y="' + (y(E(3)) + 4) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#9d174d" font-weight="700">Пашен →</text>';
svg += '<text x="' + (W/2) + '" y="22" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">E_n = -13,6/n² (атом водорода)</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}