diff --git a/frontend/css/lab.css b/frontend/css/lab.css
index 4379de4..f0ab663 100644
--- a/frontend/css/lab.css
+++ b/frontend/css/lab.css
@@ -1447,6 +1447,42 @@
.cur-cross { cursor: crosshair !important; }
.cur-move { cursor: move !important; }
+/* ── Periodic Table — Visual Modes (Wave B) ─────────────────── */
+#ptbl-vmodes-bar {
+ user-select: none;
+}
+#ptbl-vmodes-bar .ptbl-vm-btn:hover {
+ background: rgba(155,93,229,0.18) !important;
+ color: #e0e0ff !important;
+ border-color: rgba(155,93,229,0.4) !important;
+}
+#ptbl-vmodes-bar .ptbl-vm-btn:active {
+ transform: scale(0.97);
+}
+#ptbl-heat-legend {
+ align-items: center;
+ gap: 4px;
+}
+#ptbl-heat-grad {
+ image-rendering: crisp-edges;
+}
+#ptbl-3d-canvas {
+ outline: none;
+ cursor: grab;
+}
+#ptbl-3d-canvas:active {
+ cursor: grabbing;
+}
+#ptbl-trend-canvas {
+ z-index: 5;
+ transition: opacity 0.3s ease;
+}
+#ptbl-3d-tip {
+ max-width: 200px;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
/* Draggable canvas elements within sims */
[draggable="true"] canvas,
canvas[data-draggable] { cursor: grab; }
@@ -1556,3 +1592,352 @@ canvas[data-draggable]:active { cursor: grabbing; }
height: 13px;
cursor: pointer;
}
+
+/* ═══════════════════════════════════════════════════════════
+ PERIODIC TABLE — Tabbed Card V2
+ ═══════════════════════════════════════════════════════════ */
+
+/* ── Card wrapper ── */
+.ptbl-card-v2 {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: color-mix(in srgb, var(--el-col, #7B8EF7) 10%, #0D0D1A 90%);
+ border-radius: 0;
+ overflow: hidden;
+}
+
+/* ── Close button ── */
+.ptbl-card-close {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ z-index: 10;
+ width: 22px;
+ height: 22px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255,255,255,0.06);
+ border: 1px solid rgba(255,255,255,0.12);
+ border-radius: 5px;
+ cursor: pointer;
+ color: rgba(255,255,255,0.5);
+ transition: background .12s, color .12s;
+ padding: 0;
+}
+.ptbl-card-close:hover { background: rgba(255,255,255,0.14); color: #fff; }
+.ptbl-card-close .ic { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2.2; }
+
+/* ── Hero section ── */
+.ptbl-hero {
+ position: relative;
+ text-align: center;
+ padding: 14px 10px 10px;
+ border-bottom: 1px solid rgba(255,255,255,0.07);
+ flex-shrink: 0;
+}
+.ptbl-hero-z {
+ position: absolute;
+ top: 8px;
+ left: 10px;
+ font-size: .72rem;
+ font-weight: 700;
+ color: rgba(255,255,255,0.35);
+ line-height: 1;
+}
+.ptbl-hero-sym {
+ font-size: 6rem;
+ font-weight: 900;
+ color: var(--el-col, #7B8EF7);
+ line-height: 1;
+ letter-spacing: -2px;
+}
+.ptbl-hero-name {
+ font-size: 1rem;
+ font-weight: 700;
+ color: #fff;
+ margin-top: 2px;
+}
+.ptbl-hero-mass {
+ font-size: .72rem;
+ color: rgba(255,255,255,0.4);
+ margin-top: 1px;
+}
+.ptbl-hero-badge {
+ display: inline-block;
+ margin-top: 6px;
+ padding: 2px 9px;
+ border-radius: 12px;
+ border: 1px solid;
+ font-size: .68rem;
+ font-weight: 600;
+}
+
+/* ── Tab bar ── */
+.ptbl-tabs {
+ display: flex;
+ overflow-x: auto;
+ flex-shrink: 0;
+ border-bottom: 1px solid rgba(255,255,255,0.07);
+ scrollbar-width: none;
+}
+.ptbl-tabs::-webkit-scrollbar { display: none; }
+
+.ptbl-tab {
+ flex-shrink: 0;
+ padding: 6px 9px;
+ font-size: .67rem;
+ font-weight: 600;
+ color: rgba(255,255,255,0.4);
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ cursor: pointer;
+ transition: color .12s, border-color .12s;
+ white-space: nowrap;
+}
+.ptbl-tab:hover { color: rgba(255,255,255,0.75); }
+.ptbl-tab.active {
+ color: var(--el-col, #7B8EF7);
+ border-bottom-color: var(--el-col, #7B8EF7);
+}
+
+/* ── Tab body ── */
+.ptbl-tab-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 10px;
+ font-size: .76rem;
+ color: #ccc;
+ transition: opacity .2s;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(255,255,255,0.1) transparent;
+}
+
+/* ── Shared helpers ── */
+.ptbl-quick-label {
+ font-size: .67rem;
+ color: rgba(255,255,255,0.4);
+ display: block;
+ margin-bottom: 1px;
+}
+.ptbl-quick-val { font-weight: 600; color: #e0e0e0; font-size: .76rem; }
+.ptbl-empty { color: rgba(255,255,255,0.3); font-size: .75rem; font-style: italic; }
+.ptbl-summary { color: #bbb; font-size: .75rem; line-height: 1.5; margin-bottom: 8px; }
+
+/* ── Overview tab ── */
+.ptbl-overview { }
+.ptbl-quick-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+}
+.ptbl-quick-item {
+ background: rgba(255,255,255,0.04);
+ border: 1px solid rgba(255,255,255,0.07);
+ border-radius: 6px;
+ padding: 6px 8px;
+}
+
+/* ── Properties tab ── */
+.ptbl-prop-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: .73rem;
+}
+.ptbl-prop-table td, .ptbl-prop-table th {
+ padding: 4px 4px;
+ border-bottom: 1px solid rgba(255,255,255,0.05);
+ vertical-align: top;
+}
+.ptbl-prop-table td:first-child {
+ color: rgba(255,255,255,0.45);
+ padding-right: 6px;
+ white-space: nowrap;
+}
+.ptbl-prop-table td:last-child { color: #ddd; }
+.ptbl-prop-table th {
+ color: rgba(255,255,255,0.35);
+ font-size: .67rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+}
+
+/* ── Electronics tab ── */
+.ptbl-electronics { }
+.ptbl-bohr-inline-wrap {
+ background: rgba(0,0,0,0.3);
+ border-radius: 6px;
+ overflow: hidden;
+ margin-bottom: 8px;
+}
+.ptbl-el-config {
+ background: rgba(255,255,255,0.04);
+ border: 1px solid rgba(255,255,255,0.07);
+ border-radius: 6px;
+ padding: 6px 8px;
+}
+.ptbl-el-config-code {
+ display: block;
+ margin-top: 2px;
+ font-size: .75rem;
+ color: var(--el-col, #7B8EF7);
+ word-break: break-all;
+}
+
+/* ── Isotopes tab ── */
+.ptbl-iso-table thead th { text-align: left; }
+.ptbl-iso-avg {
+ margin-top: 6px;
+ font-size: .73rem;
+ color: rgba(255,255,255,0.55);
+ text-align: right;
+}
+
+/* ── History tab ── */
+.ptbl-history { }
+.ptbl-timeline-card {
+ background: rgba(255,255,255,0.05);
+ border-left: 3px solid var(--el-col, #7B8EF7);
+ border-radius: 4px;
+ padding: 8px 10px;
+ margin-bottom: 10px;
+}
+.ptbl-timeline-year {
+ font-size: 1.3rem;
+ font-weight: 900;
+ color: var(--el-col, #7B8EF7);
+ line-height: 1;
+}
+.ptbl-timeline-who { font-size: .78rem; color: #ccc; margin-top: 2px; }
+.ptbl-timeline-country { font-size: .7rem; color: rgba(255,255,255,0.4); margin-top: 1px; }
+.ptbl-hist-text { font-size: .75rem; color: #bbb; line-height: 1.55; }
+.ptbl-etymology {
+ margin-top: 8px;
+ padding: 6px 8px;
+ background: rgba(255,255,255,0.03);
+ border-radius: 5px;
+ font-size: .73rem;
+ color: #bbb;
+ line-height: 1.5;
+}
+
+/* ── Applications tab ── */
+.ptbl-app-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 8px;
+}
+.ptbl-app-chip {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 3px;
+ padding: 6px 8px;
+ background: rgba(255,255,255,0.05);
+ border: 1px solid rgba(255,255,255,0.09);
+ border-radius: 8px;
+ font-size: .64rem;
+ color: #bbb;
+ min-width: 48px;
+ text-align: center;
+ transition: background .12s;
+}
+.ptbl-app-chip:hover { background: rgba(255,255,255,0.10); }
+.ptbl-app-chip .ic {
+ width: 18px;
+ height: 18px;
+ stroke: var(--el-col, #7B8EF7);
+ fill: none;
+ stroke-width: 1.6;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+.ptbl-app-desc { font-size: .74rem; color: #bbb; line-height: 1.55; margin-top: 4px; }
+
+/* ── Biology tab ── */
+.ptbl-bio-badge {
+ display: inline-block;
+ padding: 4px 10px;
+ border-radius: 12px;
+ border: 1px solid;
+ font-size: .73rem;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+.ptbl-bio-role { font-size: .74rem; color: #bbb; line-height: 1.55; }
+.ptbl-bio-toxicity {
+ margin-top: 6px;
+ font-size: .73rem;
+ color: rgba(255,255,255,0.55);
+}
+
+/* ── Minerals tab ── */
+.ptbl-mineral-list {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ margin-bottom: 8px;
+}
+.ptbl-mineral-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: rgba(255,255,255,0.04);
+ border: 1px solid rgba(255,255,255,0.07);
+ border-radius: 5px;
+ padding: 5px 8px;
+}
+.ptbl-mineral-name { color: #ddd; font-size: .75rem; }
+.ptbl-mineral-formula {
+ font-size: .7rem;
+ color: var(--el-col, #7B8EF7);
+ background: rgba(255,255,255,0.05);
+ padding: 1px 5px;
+ border-radius: 4px;
+}
+.ptbl-mineral-sources { font-size: .74rem; color: #bbb; }
+
+/* ── Spectrum tab ── */
+.ptbl-spectrum { }
+.ptbl-spec-lines-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 6px;
+}
+.ptbl-spec-tag {
+ padding: 2px 6px;
+ background: rgba(255,255,255,0.06);
+ border-radius: 4px;
+ font-size: .68rem;
+ color: #bbb;
+ font-variant-numeric: tabular-nums;
+}
+
+/* ── Flame tab ── */
+.ptbl-flame { text-align: center; padding: 10px 0; }
+.ptbl-flame-swatch {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ margin: 0 auto 10px;
+}
+.ptbl-flame-label { font-size: .78rem; color: #ddd; font-weight: 600; }
+.ptbl-flame-desc { font-size: .73rem; color: #aaa; line-height: 1.55; margin-top: 6px; text-align: left; }
+
+/* ── Reactions tab ── */
+.ptbl-reactions { display: flex; flex-direction: column; gap: 8px; }
+.ptbl-reaction-item {
+ background: rgba(255,255,255,0.04);
+ border: 1px solid rgba(255,255,255,0.07);
+ border-radius: 6px;
+ padding: 7px 9px;
+}
+.ptbl-reaction-label { font-size: .68rem; color: rgba(255,255,255,0.4); margin-bottom: 3px; }
+.ptbl-reaction-eq { font-size: .78rem; font-weight: 600; font-family: 'Courier New', monospace; }
+.ptbl-reaction-note { font-size: .68rem; color: rgba(255,255,255,0.3); font-style: italic; margin-top: 4px; }
diff --git a/frontend/js/labs/_periodic_data.js b/frontend/js/labs/_periodic_data.js
new file mode 100644
index 0000000..b6c6146
--- /dev/null
+++ b/frontend/js/labs/_periodic_data.js
@@ -0,0 +1,1233 @@
+'use strict';
+/* ══════════════════════════════════════════════════════════════
+ _periodic_data.js — расширенные данные для PeriodicTableSim
+ Должен загружаться ПЕРЕД periodic.js
+ ══════════════════════════════════════════════════════════════ */
+
+/* ── Расширенные свойства элементов ─────────────────────────── */
+window.PERIODIC_EXT_DATA = {
+ byZ: {
+
+ /* ══ Z = 1 Водород ════════════════════════════════════════ */
+ 1: {
+ radius: { atomic: 53, covalent: 31, vanderwaals: 120 },
+ ionization: { e1: 1312, e2: null, e3: null },
+ electronAffinity: 72,
+ heatCapacity: 14.30,
+ thermalConductivity: 0.1805,
+ crystalStructure: 'hexagonal',
+ latticeParam: 376,
+ abundance: 1400,
+ biological: 'macro',
+ biologicalRole: 'Входит в состав воды и всех органических молекул; участвует в водородных связях.',
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['energy', 'chemistry', 'food'],
+ applicationsText: 'Ракетное топливо, синтез аммиака (процесс Хабера), гидрогенизация жиров, водородная энергетика.',
+ historyText: 'Открыт Г. Кавендишем в 1766 г. как «горючий воздух». В 1783 г. А. Лавуазье установил его состав при сгорании и дал современное название.',
+ etymology: 'Греч. ὕδωρ (hydōr) — вода + γεννάω — рождать; «рождающий воду».',
+ mineralForms: [],
+ reactions: ['2H₂ + O₂ → 2H₂O', 'H₂ + Cl₂ → 2HCl', 'N₂ + 3H₂ → 2NH₃', 'H₂ + S → H₂S']
+ },
+
+ /* ══ Z = 2 Гелий ══════════════════════════════════════════ */
+ 2: {
+ radius: { atomic: 31, covalent: 28, vanderwaals: 140 },
+ ionization: { e1: 2372, e2: 5250, e3: null },
+ electronAffinity: 0,
+ heatCapacity: 5.19,
+ thermalConductivity: 0.1513,
+ crystalStructure: 'hexagonal',
+ latticeParam: 357,
+ abundance: 0.008,
+ biological: 'inert',
+ biologicalRole: 'Биологически инертен; используется в медицинской диагностике (МРТ).',
+ toxicity: 'inert',
+ flameColor: null,
+ flameName: null,
+ applications: ['industry', 'medicine', 'science'],
+ applicationsText: 'Охлаждение сверхпроводящих магнитов МРТ, заполнение аэростатов и дирижаблей, защитная атмосфера при сварке.',
+ historyText: 'Первоначально обнаружен в спектре Солнца в 1868 г. Жансеном и Локьером. На Земле выделен Рамзаем в 1895 г. из минерала клевеита.',
+ etymology: 'Греч. Ἥλιος (Helios) — Солнце; обнаружен прежде всего в солнечном спектре.',
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 3 Литий ══════════════════════════════════════════ */
+ 3: {
+ radius: { atomic: 167, covalent: 128, vanderwaals: 182 },
+ ionization: { e1: 520, e2: 7298, e3: 11815 },
+ electronAffinity: 60,
+ heatCapacity: 3.58,
+ thermalConductivity: 84.8,
+ crystalStructure: 'bcc',
+ latticeParam: 351,
+ abundance: 20,
+ biological: 'micro',
+ biologicalRole: 'Микроэлемент; соли лития применяются в психиатрии для лечения маниакально-депрессивного расстройства.',
+ toxicity: 'medium',
+ flameColor: '#CC0000',
+ flameName: 'Ярко-красный',
+ applications: ['batteries', 'medicine', 'glass'],
+ applicationsText: 'Литий-ионные аккумуляторы, антидепрессанты, жаропрочные стёкла и керамика, смазки для авиации.',
+ historyText: 'Открыт в 1817 г. И.А. Арфведсоном в минерале петалите. Металлический литий выделен Дэви в 1818 г. электролизом.',
+ etymology: 'Греч. λίθος (lithos) — камень; первый щелочной металл, найденный в минералах (не в живой природе).',
+ mineralForms: [
+ { name: 'Сподумен', formula: 'LiAlSi₂O₆' },
+ { name: 'Петалит', formula: 'LiAlSi₄O₁₀' },
+ { name: 'Лепидолит', formula: 'K(Li,Al)₃(Si,Al)₄O₁₀(F,OH)₂' }
+ ],
+ reactions: ['2Li + 2H₂O → 2LiOH + H₂↑', '4Li + O₂ → 2Li₂O', '2Li + Cl₂ → 2LiCl']
+ },
+
+ /* ══ Z = 4 Бериллий (minimum) ═════════════════════════════ */
+ 4: {
+ radius: { atomic: 112, covalent: 96, vanderwaals: null },
+ ionization: { e1: 900, e2: null, e3: null },
+ electronAffinity: 0,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 2.8,
+ biological: 'toxic',
+ biologicalRole: null,
+ toxicity: 'high',
+ flameColor: null,
+ flameName: null,
+ applications: ['aerospace', 'electronics', 'nuclear'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 5 Бор (minimum) ══════════════════════════════════ */
+ 5: {
+ radius: { atomic: 87, covalent: 84, vanderwaals: null },
+ ionization: { e1: 801, e2: null, e3: null },
+ electronAffinity: 27,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 10,
+ biological: 'micro',
+ biologicalRole: null,
+ toxicity: 'low',
+ flameColor: '#00CC00',
+ flameName: 'Зелёный',
+ applications: ['glass', 'agriculture', 'semiconductors'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 6 Углерод ════════════════════════════════════════ */
+ 6: {
+ radius: { atomic: 67, covalent: 77, vanderwaals: 170 },
+ ionization: { e1: 1086, e2: 2353, e3: 4620 },
+ electronAffinity: 122,
+ heatCapacity: 8.53,
+ thermalConductivity: 5.0,
+ crystalStructure: 'diamond cubic',
+ latticeParam: 357,
+ abundance: 200,
+ biological: 'macro',
+ biologicalRole: 'Основа всех органических молекул: белков, нуклеиновых кислот, липидов и углеводов.',
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['materials', 'energy', 'chemistry'],
+ applicationsText: 'Алмазы (ювелирное дело, абразивы), графит (карандаши, электроды, смазка), сажа (резина), углеродные волокна, органический синтез, ядерная техника.',
+ historyText: 'Известен с глубокой древности в виде угля и алмаза. Выявлен как элемент А. Лавуазье в 1789 г. Фуллерены открыты Кёрлом, Смолли и Крото в 1985 г.',
+ etymology: 'Лат. carbo — уголь, древесный уголь.',
+ mineralForms: [
+ { name: 'Алмаз', formula: 'C' },
+ { name: 'Графит', formula: 'C' },
+ { name: 'Кальцит', formula: 'CaCO₃' }
+ ],
+ reactions: ['C + O₂ → CO₂', '2C + O₂ → 2CO', 'C + CO₂ → 2CO', 'C + 4H₂ → CH₄']
+ },
+
+ /* ══ Z = 7 Азот ═══════════════════════════════════════════ */
+ 7: {
+ radius: { atomic: 56, covalent: 75, vanderwaals: 155 },
+ ionization: { e1: 1402, e2: 2856, e3: 4578 },
+ electronAffinity: 7,
+ heatCapacity: 1.04,
+ thermalConductivity: 0.02583,
+ crystalStructure: 'hexagonal',
+ latticeParam: 386,
+ abundance: 19,
+ biological: 'macro',
+ biologicalRole: 'Входит в состав аминокислот, белков, ДНК и РНК; обязательный компонент всех живых клеток.',
+ toxicity: 'inert',
+ flameColor: null,
+ flameName: null,
+ applications: ['chemistry', 'food', 'medicine'],
+ applicationsText: 'Синтез аммиака и азотных удобрений, жидкий азот в криогенике и медицине, инертная атмосфера в пищевой и электронной промышленности.',
+ historyText: 'Открыт в 1772 г. Д. Резерфордом как «удушливый воздух». Независимо получен Шееле, Пристли и Кавендишем. Назван азотом («безжизненным») Лавуазье.',
+ etymology: 'Греч. ἀ- (а-) — без + ζωή (zoe) — жизнь; «не поддерживающий жизнь».',
+ mineralForms: [
+ { name: 'Нитрат калия (селитра)', formula: 'KNO₃' },
+ { name: 'Нитрат натрия', formula: 'NaNO₃' }
+ ],
+ reactions: ['N₂ + 3H₂ → 2NH₃', '4NH₃ + 5O₂ → 4NO + 6H₂O', 'N₂ + O₂ → 2NO (при t°)', '3NO₂ + H₂O → 2HNO₃ + NO']
+ },
+
+ /* ══ Z = 8 Кислород ════════════════════════════════════════ */
+ 8: {
+ radius: { atomic: 48, covalent: 66, vanderwaals: 152 },
+ ionization: { e1: 1314, e2: 3388, e3: 5300 },
+ electronAffinity: 141,
+ heatCapacity: 0.918,
+ thermalConductivity: 0.02658,
+ crystalStructure: 'cubic',
+ latticeParam: 683,
+ abundance: 461000,
+ biological: 'macro',
+ biologicalRole: 'Необходим для клеточного дыхания; входит в состав воды, белков, нуклеиновых кислот и большинства биомолекул.',
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['medicine', 'industry', 'chemistry'],
+ applicationsText: 'Медицинский кислород для дыхания, сжигание топлива (металлургия), производство стали (кислородный конвертер), синтез химических соединений.',
+ historyText: 'Открыт в 1774 г. Дж. Пристли (нагреванием оксида ртути). Независимо получен К. Шееле в 1772 г. Название «кислород» дано Лавуазье.',
+ etymology: 'Греч. ὀξύς (oxys) — острый, кислый + γεννάω — рождать; «порождающий кислоту».',
+ mineralForms: [
+ { name: 'Кварц', formula: 'SiO₂' },
+ { name: 'Гематит', formula: 'Fe₂O₃' },
+ { name: 'Корунд', formula: 'Al₂O₃' }
+ ],
+ reactions: ['2H₂ + O₂ → 2H₂O', 'C + O₂ → CO₂', '4Fe + 3O₂ → 2Fe₂O₃', '2SO₂ + O₂ → 2SO₃']
+ },
+
+ /* ══ Z = 9 Фтор ═══════════════════════════════════════════ */
+ 9: {
+ radius: { atomic: 42, covalent: 64, vanderwaals: 147 },
+ ionization: { e1: 1681, e2: 3374, e3: null },
+ electronAffinity: 328,
+ heatCapacity: 0.824,
+ thermalConductivity: 0.02591,
+ crystalStructure: 'cubic',
+ latticeParam: 550,
+ abundance: 585,
+ biological: 'micro',
+ biologicalRole: 'Участвует в минерализации зубов и костей; фторид-ион укрепляет зубную эмаль.',
+ toxicity: 'high',
+ flameColor: null,
+ flameName: null,
+ applications: ['chemistry', 'materials', 'medicine'],
+ applicationsText: 'Производство фторопластов (тефлон), фреонов (хладагенты), HF (травление стекла и кремния), фторирование воды, антикариесные зубные пасты.',
+ historyText: 'Выделен А. Муассаном в 1886 г. электролизом фторида калия в HF (за что получил Нобелевскую премию). Токсичность фтора унесла жизни многих исследователей.',
+ etymology: 'Греч. φθόρος (phthoros) — разрушение; за исключительную коррозионную активность.',
+ mineralForms: [
+ { name: 'Флюорит', formula: 'CaF₂' },
+ { name: 'Апатит', formula: 'Ca₅(PO₄)₃F' },
+ { name: 'Криолит', formula: 'Na₃AlF₆' }
+ ],
+ reactions: ['F₂ + H₂ → 2HF', 'F₂ + 2NaOH → 2NaF + H₂O + ½O₂', '2F₂ + 2H₂O → 4HF + O₂', 'F₂ + Xe → XeF₂']
+ },
+
+ /* ══ Z = 10 Неон ══════════════════════════════════════════ */
+ 10: {
+ radius: { atomic: 38, covalent: 58, vanderwaals: 154 },
+ ionization: { e1: 2081, e2: 3952, e3: null },
+ electronAffinity: 0,
+ heatCapacity: 1.03,
+ thermalConductivity: 0.0491,
+ crystalStructure: 'fcc',
+ latticeParam: 443,
+ abundance: 0.005,
+ biological: 'inert',
+ biologicalRole: 'Биологически инертен.',
+ toxicity: 'inert',
+ flameColor: null,
+ flameName: null,
+ applications: ['lighting', 'science', 'lasers'],
+ applicationsText: 'Неоновые рекламные трубки (оранжево-красное свечение), He-Ne лазеры, криогенные исследования.',
+ historyText: 'Открыт в 1898 г. У. Рамзаем и М. Траверсом при дробной перегонке жидкого воздуха. Третий благородный газ, обнаруженный после аргона и гелия.',
+ etymology: 'Греч. νέος (neos) — новый.',
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 11 Натрий ════════════════════════════════════════ */
+ 11: {
+ radius: { atomic: 186, covalent: 166, vanderwaals: 227 },
+ ionization: { e1: 496, e2: 4562, e3: null },
+ electronAffinity: 53,
+ heatCapacity: 1.23,
+ thermalConductivity: 142,
+ crystalStructure: 'bcc',
+ latticeParam: 430,
+ abundance: 23600,
+ biological: 'macro',
+ biologicalRole: 'Основной внеклеточный катион; регулирует осмотическое давление крови и нервные импульсы (Na⁺/K⁺-насос).',
+ toxicity: 'low',
+ flameColor: '#FFCC00',
+ flameName: 'Ярко-жёлтый',
+ applications: ['chemistry', 'food', 'metallurgy'],
+ applicationsText: 'Поваренная соль (NaCl), производство соды (NaOH, Na₂CO₃), натриевые лампы уличного освещения, теплоноситель в ядерных реакторах.',
+ historyText: 'Выделен Г. Дэви в 1807 г. электролизом расплава NaOH. Название «натрий» — от лат. natrium; символ Na сохраняет латинское происхождение.',
+ etymology: 'Лат. natrium ← арабск. natrun ← егип. ntry — природная сода.',
+ mineralForms: [
+ { name: 'Галит', formula: 'NaCl' },
+ { name: 'Сода', formula: 'Na₂CO₃' },
+ { name: 'Бура', formula: 'Na₂B₄O₇·10H₂O' }
+ ],
+ reactions: ['2Na + 2H₂O → 2NaOH + H₂↑', '4Na + O₂ → 2Na₂O', '2Na + Cl₂ → 2NaCl', 'Na + O₂ → NaO₂']
+ },
+
+ /* ══ Z = 12 Магний ════════════════════════════════════════ */
+ 12: {
+ radius: { atomic: 160, covalent: 141, vanderwaals: 173 },
+ ionization: { e1: 738, e2: 1451, e3: 7733 },
+ electronAffinity: 0,
+ heatCapacity: 1.02,
+ thermalConductivity: 156,
+ crystalStructure: 'hexagonal',
+ latticeParam: 321,
+ abundance: 27640,
+ biological: 'macro',
+ biologicalRole: 'Кофактор более 300 ферментов; входит в состав хлорофилла; необходим для синтеза АТФ и белков.',
+ toxicity: 'low',
+ flameColor: '#FFFFFF',
+ flameName: 'Белый (ослепительный)',
+ applications: ['aerospace', 'chemistry', 'medicine'],
+ applicationsText: 'Лёгкие конструкционные сплавы (авиация, автомобилестроение), пиротехника и осветительные ракеты, производство алюминия (десульфурация), минеральные удобрения.',
+ historyText: 'Как элемент выявлен Дж. Блэком в 1755 г. Металлический магний получен Г. Дэви в 1808 г. Промышленное производство освоено в XIX веке.',
+ etymology: 'От греч. Μαγνησία (Magnesia) — историческая область в Греции, где добывали минерал магнезит.',
+ mineralForms: [
+ { name: 'Магнезит', formula: 'MgCO₃' },
+ { name: 'Доломит', formula: 'CaMg(CO₃)₂' },
+ { name: 'Серпентин', formula: 'Mg₃Si₂O₅(OH)₄' }
+ ],
+ reactions: ['2Mg + O₂ → 2MgO', 'Mg + 2HCl → MgCl₂ + H₂↑', 'Mg + CO₂ → MgO + C', 'Mg + 2H₂O → Mg(OH)₂ + H₂↑']
+ },
+
+ /* ══ Z = 13 Алюминий ═══════════════════════════════════════ */
+ 13: {
+ radius: { atomic: 143, covalent: 121, vanderwaals: 184 },
+ ionization: { e1: 578, e2: 1817, e3: 2745 },
+ electronAffinity: 43,
+ heatCapacity: 0.897,
+ thermalConductivity: 237,
+ crystalStructure: 'fcc',
+ latticeParam: 405,
+ abundance: 82300,
+ biological: 'trace',
+ biologicalRole: 'Не является жизненно необходимым; в больших дозах оказывает нейротоксическое действие.',
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['aerospace', 'construction', 'packaging'],
+ applicationsText: 'Авиационные и строительные сплавы (дюраль), упаковочная фольга и банки, электрические провода, зеркала, термит (восстановление металлов).',
+ historyText: 'Открыт Х.К. Эрстедом в 1825 г. восстановлением AlCl₃ амальгамой калия. Промышленный способ (электролиз глинозёма, процесс Холла–Эру) разработан в 1886 г.',
+ etymology: 'Лат. alumen — квасцы; минерал, давно известный как протрава при крашении.',
+ mineralForms: [
+ { name: 'Корунд', formula: 'Al₂O₃' },
+ { name: 'Боксит', formula: 'Al₂O₃·nH₂O' },
+ { name: 'Полевой шпат', formula: 'KAlSi₃O₈' }
+ ],
+ reactions: ['4Al + 3O₂ → 2Al₂O₃', '2Al + 6HCl → 2AlCl₃ + 3H₂↑', '2Al + 2NaOH + 2H₂O → 2NaAlO₂ + 3H₂↑', '8Al + 3Fe₃O₄ → 4Al₂O₃ + 9Fe']
+ },
+
+ /* ══ Z = 14 Кремний ════════════════════════════════════════ */
+ 14: {
+ radius: { atomic: 111, covalent: 111, vanderwaals: 210 },
+ ionization: { e1: 787, e2: 1577, e3: 3232 },
+ electronAffinity: 134,
+ heatCapacity: 0.712,
+ thermalConductivity: 148,
+ crystalStructure: 'diamond cubic',
+ latticeParam: 543,
+ abundance: 282000,
+ biological: 'trace',
+ biologicalRole: 'Микроэлемент; участвует в формировании соединительной ткани и скелетных структур некоторых организмов.',
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['electronics', 'solar', 'construction'],
+ applicationsText: 'Полупроводниковые чипы и транзисторы, солнечные батареи, оптоволокно (SiO₂), стекло, цемент, строительные материалы.',
+ historyText: 'Выделен Й.Й. Берцелиусом в 1824 г. восстановлением фторида кремния калием. Второй по распространённости элемент в земной коре после кислорода.',
+ etymology: 'Лат. silex, silicis — кремень, твёрдый камень.',
+ mineralForms: [
+ { name: 'Кварц', formula: 'SiO₂' },
+ { name: 'Ортоклаз', formula: 'KAlSi₃O₈' },
+ { name: 'Тальк', formula: 'Mg₃Si₄O₁₀(OH)₂' }
+ ],
+ reactions: ['Si + O₂ → SiO₂', 'Si + 2Cl₂ → SiCl₄', 'SiO₂ + 2NaOH → Na₂SiO₃ + H₂O', 'SiO₂ + 4HF → SiF₄ + 2H₂O']
+ },
+
+ /* ══ Z = 15 Фосфор ════════════════════════════════════════ */
+ 15: {
+ radius: { atomic: 98, covalent: 107, vanderwaals: 180 },
+ ionization: { e1: 1012, e2: 1907, e3: 2914 },
+ electronAffinity: 72,
+ heatCapacity: 0.770,
+ thermalConductivity: 0.236,
+ crystalStructure: 'orthorhombic',
+ latticeParam: 1145,
+ abundance: 1050,
+ biological: 'macro',
+ biologicalRole: 'Входит в состав ДНК, РНК, АТФ, фосфолипидов мембран и минерала костной ткани — гидроксиапатита.',
+ toxicity: 'medium',
+ flameColor: null,
+ flameName: null,
+ applications: ['agriculture', 'chemistry', 'food'],
+ applicationsText: 'Фосфорные удобрения (суперфосфат, аммофос), производство фосфорной кислоты, спички, средства защиты растений, пищевые добавки (E338–E341).',
+ historyText: 'Открыт в 1669 г. алхимиком Х. Брандом при перегонке мочи — первое самосветящееся вещество, открытое в Новое время. Название связано со свечением белого фосфора.',
+ etymology: 'Греч. φωσφόρος (phosphoros) — несущий свет; φῶς — свет + φέρω — нести.',
+ mineralForms: [
+ { name: 'Апатит', formula: 'Ca₅(PO₄)₃(F,Cl,OH)' },
+ { name: 'Фосфорит', formula: 'Ca₃(PO₄)₂' }
+ ],
+ reactions: ['4P + 5O₂ → 2P₂O₅', 'P₄ + 6Cl₂ → 4PCl₃', 'P₂O₅ + 3H₂O → 2H₃PO₄', '2P + 3H₂ → 2PH₃']
+ },
+
+ /* ══ Z = 16 Сера ══════════════════════════════════════════ */
+ 16: {
+ radius: { atomic: 88, covalent: 105, vanderwaals: 180 },
+ ionization: { e1: 1000, e2: 2252, e3: 3357 },
+ electronAffinity: 200,
+ heatCapacity: 0.708,
+ thermalConductivity: 0.205,
+ crystalStructure: 'orthorhombic',
+ latticeParam: 1046,
+ abundance: 350,
+ biological: 'macro',
+ biologicalRole: 'Входит в состав аминокислот цистеина и метионина; участвует в формировании дисульфидных мостиков белков.',
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['chemistry', 'agriculture', 'vulcanization'],
+ applicationsText: 'Производство серной кислоты (важнейший химический продукт), вулканизация каучука, фунгициды и инсектициды, производство бумаги (сульфитный метод).',
+ historyText: 'Известна с глубокой древности; упоминается в Библии и греческих текстах. Лавуазье признал серу элементом в 1789 г. Добывалась у вулканов.',
+ etymology: 'Лат. sulphur/sulfur — сера; возможно, от санскр. shulbari — враг меди.',
+ mineralForms: [
+ { name: 'Сера самородная', formula: 'S' },
+ { name: 'Пирит', formula: 'FeS₂' },
+ { name: 'Гипс', formula: 'CaSO₄·2H₂O' }
+ ],
+ reactions: ['S + O₂ → SO₂', '2SO₂ + O₂ → 2SO₃ (кат.)', 'SO₃ + H₂O → H₂SO₄', 'Fe + S → FeS']
+ },
+
+ /* ══ Z = 17 Хлор ══════════════════════════════════════════ */
+ 17: {
+ radius: { atomic: 79, covalent: 102, vanderwaals: 175 },
+ ionization: { e1: 1251, e2: 2298, e3: null },
+ electronAffinity: 349,
+ heatCapacity: 0.479,
+ thermalConductivity: 0.00889,
+ crystalStructure: 'orthorhombic',
+ latticeParam: 624,
+ abundance: 145,
+ biological: 'macro',
+ biologicalRole: 'Хлорид-ион — основной анион крови и внеклеточной жидкости; участвует в регуляции pH (кислотность желудочного сока — HCl).',
+ toxicity: 'high',
+ flameColor: null,
+ flameName: null,
+ applications: ['chemistry', 'water', 'medicine'],
+ applicationsText: 'Дезинфекция питьевой воды, производство ПВХ и хлорорганики, отбеливающие средства (белизна, хлорная известь), HCl для металлургии и синтеза.',
+ historyText: 'Открыт К. Шееле в 1774 г. реакцией пиролюзита с HCl. Природу элемента установил Г. Дэви в 1810 г. В Первую мировую использован как первое боевое ОВ (1915 г., Ипр).',
+ etymology: 'Греч. χλωρός (chloros) — жёлто-зелёный; по характерному цвету газа.',
+ mineralForms: [
+ { name: 'Галит', formula: 'NaCl' },
+ { name: 'Сильвин', formula: 'KCl' },
+ { name: 'Карналлит', formula: 'KMgCl₃·6H₂O' }
+ ],
+ reactions: ['H₂ + Cl₂ → 2HCl', 'Cl₂ + 2NaOH → NaCl + NaOCl + H₂O', '2Fe + 3Cl₂ → 2FeCl₃', 'Cl₂ + H₂O ⇌ HCl + HClO']
+ },
+
+ /* ══ Z = 18 Аргон ══════════════════════════════════════════ */
+ 18: {
+ radius: { atomic: 71, covalent: 106, vanderwaals: 188 },
+ ionization: { e1: 1521, e2: 2666, e3: null },
+ electronAffinity: 0,
+ heatCapacity: 0.520,
+ thermalConductivity: 0.01772,
+ crystalStructure: 'fcc',
+ latticeParam: 526,
+ abundance: 3.5,
+ biological: 'inert',
+ biologicalRole: 'Биологически инертен.',
+ toxicity: 'inert',
+ flameColor: null,
+ flameName: null,
+ applications: ['welding', 'lighting', 'science'],
+ applicationsText: 'Защитный газ при сварке MIG/TIG, газовое наполнение ламп накаливания и люминесцентных ламп, хроматография (газ-носитель), аргон-аргоновое датирование.',
+ historyText: 'Предсказан Кавендишем в 1785 г. (нереакционная часть воздуха). Открыт в 1894 г. У. Рэлеем и У. Рамзаем по отличию плотности «атмосферного азота» от чистого N₂.',
+ etymology: 'Греч. ἀργός (argos) — ленивый, инертный.',
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 19 Калий ══════════════════════════════════════════ */
+ 19: {
+ radius: { atomic: 243, covalent: 203, vanderwaals: 275 },
+ ionization: { e1: 419, e2: 3052, e3: null },
+ electronAffinity: 48,
+ heatCapacity: 0.757,
+ thermalConductivity: 102.5,
+ crystalStructure: 'bcc',
+ latticeParam: 533,
+ abundance: 20900,
+ biological: 'macro',
+ biologicalRole: 'Главный внутриклеточный катион; участвует в генерации мембранного потенциала, регуляции сердечного ритма и мышечных сокращений.',
+ toxicity: 'low',
+ flameColor: '#CC44FF',
+ flameName: 'Фиолетовый (сиреневый)',
+ applications: ['agriculture', 'chemistry', 'food'],
+ applicationsText: 'Калийные удобрения (хлорид, сульфат и нитрат калия), производство KOH и KHCO₃, порох (KNO₃), фармацевтика.',
+ historyText: 'Выделен Г. Дэви в 1807 г. электролизом расплава KOH — в одну ночь с натрием. Первый металл, полученный электролизом.',
+ etymology: 'Лат. kalium ← арабск. al-qali — зола растений; символ K от Kalium.',
+ mineralForms: [
+ { name: 'Сильвин', formula: 'KCl' },
+ { name: 'Карналлит', formula: 'KMgCl₃·6H₂O' },
+ { name: 'Ортоклаз', formula: 'KAlSi₃O₈' }
+ ],
+ reactions: ['2K + 2H₂O → 2KOH + H₂↑', '4K + O₂ → 2K₂O', 'K + O₂ → KO₂', '2K + Cl₂ → 2KCl']
+ },
+
+ /* ══ Z = 20 Кальций ════════════════════════════════════════ */
+ 20: {
+ radius: { atomic: 194, covalent: 176, vanderwaals: 231 },
+ ionization: { e1: 590, e2: 1145, e3: 4912 },
+ electronAffinity: 2,
+ heatCapacity: 0.647,
+ thermalConductivity: 201,
+ crystalStructure: 'fcc',
+ latticeParam: 558,
+ abundance: 41500,
+ biological: 'macro',
+ biologicalRole: 'Основной структурный элемент костей и зубов (гидроксиапатит); внутриклеточный вторичный мессенджер; участвует в свёртывании крови.',
+ toxicity: 'low',
+ flameColor: '#FF4400',
+ flameName: 'Оранжево-красный',
+ applications: ['construction', 'metallurgy', 'chemistry'],
+ applicationsText: 'Цемент и бетон (CaO, Ca(OH)₂, CaSO₄), металлургический флюс, производство стекла, нейтрализация кислых почв, медицина (препараты кальция).',
+ historyText: 'Металлический кальций получен Г. Дэви в 1808 г. электролизом, однако оксид кальция (негашёная известь) известен с античности.',
+ etymology: 'Лат. calx, calcis — известняк, обожжённая известь.',
+ mineralForms: [
+ { name: 'Кальцит', formula: 'CaCO₃' },
+ { name: 'Гипс', formula: 'CaSO₄·2H₂O' },
+ { name: 'Флюорит', formula: 'CaF₂' },
+ { name: 'Апатит', formula: 'Ca₅(PO₄)₃(F,OH)' }
+ ],
+ reactions: ['Ca + 2H₂O → Ca(OH)₂ + H₂↑', '2Ca + O₂ → 2CaO', 'CaO + H₂O → Ca(OH)₂', 'Ca(OH)₂ + CO₂ → CaCO₃ + H₂O']
+ },
+
+ /* ══ Z = 21 Скандий (minimum) ══════════════════════════════ */
+ 21: {
+ radius: { atomic: 184, covalent: 170, vanderwaals: null },
+ ionization: { e1: 633, e2: null, e3: null },
+ electronAffinity: 18,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 22,
+ biological: 'trace',
+ biologicalRole: null,
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['aerospace', 'lighting'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 22 Титан (minimum) ════════════════════════════════ */
+ 22: {
+ radius: { atomic: 176, covalent: 160, vanderwaals: null },
+ ionization: { e1: 659, e2: null, e3: null },
+ electronAffinity: 8,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 5650,
+ biological: 'inert',
+ biologicalRole: null,
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['aerospace', 'medicine', 'construction'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 23 Ванадий (minimum) ═════════════════════════════ */
+ 23: {
+ radius: { atomic: 171, covalent: 153, vanderwaals: null },
+ ionization: { e1: 651, e2: null, e3: null },
+ electronAffinity: 51,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 120,
+ biological: 'micro',
+ biologicalRole: null,
+ toxicity: 'medium',
+ flameColor: null,
+ flameName: null,
+ applications: ['metallurgy', 'chemistry'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 24 Хром ══════════════════════════════════════════ */
+ 24: {
+ radius: { atomic: 166, covalent: 139, vanderwaals: null },
+ ionization: { e1: 653, e2: 1591, e3: 2987 },
+ electronAffinity: 65,
+ heatCapacity: 0.449,
+ thermalConductivity: 93.9,
+ crystalStructure: 'bcc',
+ latticeParam: 288,
+ abundance: 102,
+ biological: 'micro',
+ biologicalRole: 'Участвует в метаболизме глюкозы; Cr³⁺ усиливает действие инсулина. Cr⁶⁺ — токсичен и канцерогенен.',
+ toxicity: 'medium',
+ flameColor: null,
+ flameName: null,
+ applications: ['metallurgy', 'plating', 'pigments'],
+ applicationsText: 'Нержавеющая сталь, хромирование поверхностей, тугоплавкие сплавы, пигменты (хромат свинца — жёлтый, оксид хрома — зелёный).',
+ historyText: 'Открыт Л. Никола Вокленом в 1798 г. в минерале крокоите (хромат свинца). Назван за богатство соединений разных цветов.',
+ etymology: 'Греч. χρῶμα (chroma) — цвет; за разнообразие окраски соединений.',
+ mineralForms: [
+ { name: 'Хромит', formula: 'FeCr₂O₄' },
+ { name: 'Крокоит', formula: 'PbCrO₄' }
+ ],
+ reactions: ['4Cr + 3O₂ → 2Cr₂O₃', '2Cr + 3Cl₂ → 2CrCl₃', 'Cr₂O₃ + 2Al → Al₂O₃ + 2Cr (алюминотермия)']
+ },
+
+ /* ══ Z = 25 Марганец ═══════════════════════════════════════ */
+ 25: {
+ radius: { atomic: 161, covalent: 139, vanderwaals: null },
+ ionization: { e1: 717, e2: 1509, e3: 3248 },
+ electronAffinity: 0,
+ heatCapacity: 0.479,
+ thermalConductivity: 7.81,
+ crystalStructure: 'cubic (complex)',
+ latticeParam: 891,
+ abundance: 950,
+ biological: 'micro',
+ biologicalRole: 'Кофактор многих ферментов (аргиназа, пируват-карбоксилаза); участвует в синтезе глюкозы и антиоксидантной защите (MnSOD).',
+ toxicity: 'medium',
+ flameColor: null,
+ flameName: null,
+ applications: ['metallurgy', 'batteries', 'chemistry'],
+ applicationsText: 'Легирование стали (прочность, износостойкость), катод в щелочных батареях (MnO₂), производство сухих элементов, окислитель KMnO₄.',
+ historyText: 'Выделен К.В. Шееле в 1774 г., однако до чистого металла довёл его И.Г. Ган в том же году. Название от города Магнезия в Греции.',
+ etymology: 'Искажённое от лат. Magnesia (Магнезия); близко к магнию и магниту.',
+ mineralForms: [
+ { name: 'Пиролюзит', formula: 'MnO₂' },
+ { name: 'Родонит', formula: 'MnSiO₃' }
+ ],
+ reactions: ['Mn + 2HCl → MnCl₂ + H₂↑', '3MnO₂ → Mn₃O₄ + O₂ (при t°)', '2KMnO₄ → K₂MnO₄ + MnO₂ + O₂']
+ },
+
+ /* ══ Z = 26 Железо ════════════════════════════════════════ */
+ 26: {
+ radius: { atomic: 156, covalent: 132, vanderwaals: null },
+ ionization: { e1: 762, e2: 1562, e3: 2957 },
+ electronAffinity: 16,
+ heatCapacity: 0.449,
+ thermalConductivity: 80.4,
+ crystalStructure: 'bcc',
+ latticeParam: 287,
+ abundance: 56300,
+ biological: 'micro',
+ biologicalRole: 'Входит в состав гемоглобина (перенос O₂), миоглобина и цитохромов (клеточное дыхание); катализатор многих ферментативных реакций.',
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['construction', 'manufacturing', 'magnets'],
+ applicationsText: 'Чугун и сталь (строительство, машиностроение, инструменты), постоянные магниты (Fe-Nd-B), катализатор синтеза аммиака, пигменты (оксиды Fe).',
+ historyText: 'Известно с 3500–4000 лет до н.э. (железный век). Одно из самых распространённых в земной коре переходных металлов. Основа современной цивилизации.',
+ etymology: 'Лат. ferrum — железо; символ Fe. Рус. «железо» — из праслав. *želězo.',
+ mineralForms: [
+ { name: 'Гематит', formula: 'Fe₂O₃' },
+ { name: 'Магнетит', formula: 'Fe₃O₄' },
+ { name: 'Пирит', formula: 'FeS₂' },
+ { name: 'Лимонит', formula: 'FeO(OH)·nH₂O' }
+ ],
+ reactions: ['3Fe + 4H₂O → Fe₃O₄ + 4H₂', '4Fe + 3O₂ + 6H₂O → 4Fe(OH)₃', 'Fe + CuSO₄ → FeSO₄ + Cu', 'Fe + H₂SO₄(разб.) → FeSO₄ + H₂↑']
+ },
+
+ /* ══ Z = 27 Кобальт ════════════════════════════════════════ */
+ 27: {
+ radius: { atomic: 152, covalent: 126, vanderwaals: null },
+ ionization: { e1: 760, e2: 1648, e3: null },
+ electronAffinity: 64,
+ heatCapacity: 0.421,
+ thermalConductivity: 100,
+ crystalStructure: 'hexagonal',
+ latticeParam: 251,
+ abundance: 25,
+ biological: 'micro',
+ biologicalRole: 'Входит в состав витамина B₁₂ (кобаламин); необходим для синтеза эритроцитов.',
+ toxicity: 'medium',
+ flameColor: null,
+ flameName: null,
+ applications: ['magnets', 'batteries', 'medicine'],
+ applicationsText: 'Твёрдые постоянные магниты (AlNiCo, SmCo), Li-Co аккумуляторы, жаропрочные сплавы (суперсплавы для турбин), кобальт-60 в лучевой терапии.',
+ historyText: 'Открыт Г. Брандтом в 1735 г. Название от нем. Kobold — горный гном; горняки принимали кобальтовые руды за серебряные и «вину» списывали на гномов.',
+ etymology: 'Нем. Kobold — горный гоблин (рудничный дух), злой дух горняков.',
+ mineralForms: [
+ { name: 'Кобальтин', formula: 'CoAsS' },
+ { name: 'Эритрин', formula: 'Co₃(AsO₄)₂·8H₂O' }
+ ],
+ reactions: ['3Co + 4H₂O → Co₃O₄ + 4H₂ (при t°)', 'Co + Cl₂ → CoCl₂', '2Co + O₂ → 2CoO']
+ },
+
+ /* ══ Z = 28 Никель ════════════════════════════════════════ */
+ 28: {
+ radius: { atomic: 149, covalent: 124, vanderwaals: 163 },
+ ionization: { e1: 737, e2: 1753, e3: null },
+ electronAffinity: 112,
+ heatCapacity: 0.444,
+ thermalConductivity: 90.9,
+ crystalStructure: 'fcc',
+ latticeParam: 352,
+ abundance: 84,
+ biological: 'micro',
+ biologicalRole: 'В малых дозах активирует ферменты уреазу и гидрогеназу; в высоких концентрациях — канцероген.',
+ toxicity: 'medium',
+ flameColor: null,
+ flameName: null,
+ applications: ['alloys', 'plating', 'batteries'],
+ applicationsText: 'Нержавеющая сталь и жаропрочные сплавы, никелирование (антикоррозионное покрытие), монеты, Ni-Cd и Ni-MH аккумуляторы, катализатор гидрирования.',
+ historyText: 'Открыт А.Ф. Кронстедтом в 1751 г. при изучении минерала купферникель (никелин). Горняки долго путали его с медной рудой.',
+ etymology: 'Нем. Nickel — сокращение от Kupfernickel; Kupfer — медь + Nickel — злой дух (который «обманывал» горняков).',
+ mineralForms: [
+ { name: 'Пентландит', formula: '(Fe,Ni)₉S₈' },
+ { name: 'Миллерит', formula: 'NiS' },
+ { name: 'Никелин', formula: 'NiAs' }
+ ],
+ reactions: ['Ni + 2HCl → NiCl₂ + H₂↑', 'Ni + 4CO → Ni(CO)₄ (кат. Монд)', 'Ni(CO)₄ → Ni + 4CO (термическое разложение)', 'Ni + Cl₂ → NiCl₂']
+ },
+
+ /* ══ Z = 29 Медь ══════════════════════════════════════════ */
+ 29: {
+ radius: { atomic: 145, covalent: 138, vanderwaals: 140 },
+ ionization: { e1: 745, e2: 1958, e3: 3555 },
+ electronAffinity: 119,
+ heatCapacity: 0.385,
+ thermalConductivity: 401,
+ crystalStructure: 'fcc',
+ latticeParam: 362,
+ abundance: 60,
+ biological: 'micro',
+ biologicalRole: 'Входит в состав цероплазмина и цитохром-c-оксидазы; участвует в метаболизме железа и синтезе гемоглобина.',
+ toxicity: 'low',
+ flameColor: '#007FFF',
+ flameName: 'Зелёно-синий',
+ applications: ['electrical', 'plumbing', 'alloys'],
+ applicationsText: 'Электрические провода и кабели, трубопроводы, радиаторы, монеты и украшения, бронза (Cu-Sn) и латунь (Cu-Zn), противогрибковые средства.',
+ historyText: 'Один из первых металлов, освоенных человечеством (медный век, ок. 5000 лет до н.э.). Название Cuprum от острова Кипр — главного источника меди в античности.',
+ etymology: 'Лат. cuprum от Κύπρος (Kypros) — Кипр; символ Cu.',
+ mineralForms: [
+ { name: 'Халькопирит', formula: 'CuFeS₂' },
+ { name: 'Малахит', formula: 'Cu₂(CO₃)(OH)₂' },
+ { name: 'Азурит', formula: 'Cu₃(CO₃)₂(OH)₂' },
+ { name: 'Куприт', formula: 'Cu₂O' }
+ ],
+ reactions: ['2Cu + O₂ → 2CuO', 'Cu + 2H₂SO₄(конц.) → CuSO₄ + SO₂ + 2H₂O', 'Cu + 4HNO₃(конц.) → Cu(NO₃)₂ + 2NO₂ + 2H₂O', 'Cu + 2AgNO₃ → Cu(NO₃)₂ + 2Ag']
+ },
+
+ /* ══ Z = 30 Цинк ══════════════════════════════════════════ */
+ 30: {
+ radius: { atomic: 142, covalent: 131, vanderwaals: 139 },
+ ionization: { e1: 906, e2: 1733, e3: 3833 },
+ electronAffinity: 0,
+ heatCapacity: 0.388,
+ thermalConductivity: 116,
+ crystalStructure: 'hexagonal',
+ latticeParam: 266,
+ abundance: 70,
+ biological: 'micro',
+ biologicalRole: 'Кофактор более 300 ферментов (карбоангидраза, ДНК-полимераза); необходим для иммунитета и заживления ран.',
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['galvanizing', 'alloys', 'medicine'],
+ applicationsText: 'Цинкование стали (антикоррозионное покрытие), латунь (Cu-Zn), цинк-угольные батареи, белила (ZnO), противовоспалительные мази, солнцезащитные кремы.',
+ historyText: 'Цинк получали в Индии ещё в XIII в. В Европе выделен А. Маргграфом в 1746 г. перегонкой ZnO с углём. Название предположительно от нем. Zinke — шип, зубец.',
+ etymology: 'Нем. Zinke — зубец; по форме кристаллов, либо от перс. Seng — камень.',
+ mineralForms: [
+ { name: 'Сфалерит', formula: 'ZnS' },
+ { name: 'Смитсонит', formula: 'ZnCO₃' },
+ { name: 'Гемиморфит', formula: 'Zn₄Si₂O₇(OH)₂·H₂O' }
+ ],
+ reactions: ['2Zn + O₂ → 2ZnO', 'Zn + 2HCl → ZnCl₂ + H₂↑', 'Zn + 2NaOH → Na₂ZnO₂ + H₂↑', 'Zn + CuSO₄ → ZnSO₄ + Cu']
+ },
+
+ /* ══ Z = 33 Мышьяк (minimum) ══════════════════════════════ */
+ 33: {
+ radius: { atomic: 119, covalent: 119, vanderwaals: 185 },
+ ionization: { e1: 947, e2: null, e3: null },
+ electronAffinity: 78,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 1.8,
+ biological: 'toxic',
+ biologicalRole: null,
+ toxicity: 'high',
+ flameColor: null,
+ flameName: null,
+ applications: ['semiconductors', 'pesticides'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 34 Селен (minimum) ════════════════════════════════ */
+ 34: {
+ radius: { atomic: 120, covalent: 120, vanderwaals: 190 },
+ ionization: { e1: 941, e2: null, e3: null },
+ electronAffinity: 195,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 0.05,
+ biological: 'micro',
+ biologicalRole: null,
+ toxicity: 'medium',
+ flameColor: null,
+ flameName: null,
+ applications: ['electronics', 'glass', 'agriculture'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 35 Бром ══════════════════════════════════════════ */
+ 35: {
+ radius: { atomic: 120, covalent: 120, vanderwaals: 185 },
+ ionization: { e1: 1140, e2: 2103, e3: null },
+ electronAffinity: 325,
+ heatCapacity: 0.474,
+ thermalConductivity: 0.122,
+ crystalStructure: 'orthorhombic',
+ latticeParam: 672,
+ abundance: 2.4,
+ biological: 'trace',
+ biologicalRole: 'Бромид-ион встречается в некоторых морских организмах; роль в высших животных невелика.',
+ toxicity: 'high',
+ flameColor: null,
+ flameName: null,
+ applications: ['chemistry', 'flame_retardants', 'photography'],
+ applicationsText: 'Антипирены (бромированные ингибиторы горения), AgBr в фотографии (чёрно-белая плёнка), синтез красителей и лекарств, добыча брома из морской воды.',
+ historyText: 'Открыт А.Ж. Баларом в 1826 г. при обработке солеварных рассолов хлором. Второй жидкий при комнатной температуре элемент (после ртути).',
+ etymology: 'Греч. βρῶμος (bromos) — зловоние; по резкому запаху паров.',
+ mineralForms: [
+ { name: 'Бромаргирит', formula: 'AgBr' }
+ ],
+ reactions: ['Br₂ + H₂ → 2HBr', 'Br₂ + 2KI → 2KBr + I₂', 'Br₂ + 2NaOH → NaBr + NaBrO + H₂O', 'Fe + Br₂ → FeBr₂']
+ },
+
+ /* ══ Z = 36 Криптон (minimum) ══════════════════════════════ */
+ 36: {
+ radius: { atomic: 88, covalent: 116, vanderwaals: 202 },
+ ionization: { e1: 1351, e2: null, e3: null },
+ electronAffinity: 0,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 0.0001,
+ biological: 'inert',
+ biologicalRole: null,
+ toxicity: 'inert',
+ flameColor: null,
+ flameName: null,
+ applications: ['lighting', 'lasers'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 47 Серебро ════════════════════════════════════════ */
+ 47: {
+ radius: { atomic: 165, covalent: 153, vanderwaals: 172 },
+ ionization: { e1: 731, e2: 2073, e3: null },
+ electronAffinity: 126,
+ heatCapacity: 0.235,
+ thermalConductivity: 429,
+ crystalStructure: 'fcc',
+ latticeParam: 409,
+ abundance: 0.075,
+ biological: 'trace',
+ biologicalRole: 'Биологически несущественно; Ag⁺ обладает выраженным антибактериальным действием (олигодинамия).',
+ toxicity: 'low',
+ flameColor: null,
+ flameName: null,
+ applications: ['electronics', 'medicine', 'jewelry'],
+ applicationsText: 'Электрические контакты и припои, AgBr в фотографии, антибактериальные покрытия и перевязочные материалы, ювелирные изделия, столовое серебро, зеркала.',
+ historyText: 'Известно с ок. 4000 г. до н.э. в Малой Азии. Долгое время второй по ценности монетный металл после золота. Самый высокий из всех элементов коэффициент тепло- и электропроводности.',
+ etymology: 'Лат. argentum ← праиндоевр. *h₂erǵ- — белый, блестящий; символ Ag.',
+ mineralForms: [
+ { name: 'Аргентит', formula: 'Ag₂S' },
+ { name: 'Хлораргирит', formula: 'AgCl' },
+ { name: 'Серебро самородное', formula: 'Ag' }
+ ],
+ reactions: ['2Ag + 2H₂SO₄(конц.) → Ag₂SO₄ + SO₂ + 2H₂O', '3Ag + 4HNO₃(разб.) → 3AgNO₃ + NO + 2H₂O', 'Ag + Cl₂ → AgCl₂ → нет (AgCl образуется иначе)', '2Ag + S → Ag₂S']
+ },
+
+ /* ══ Z = 53 Йод (minimum) ══════════════════════════════════ */
+ 53: {
+ radius: { atomic: 140, covalent: 139, vanderwaals: 198 },
+ ionization: { e1: 1008, e2: null, e3: null },
+ electronAffinity: 295,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 0.45,
+ biological: 'micro',
+ biologicalRole: 'Необходим для синтеза гормонов щитовидной железы (тироксин T₄ и трийодтиронин T₃).',
+ toxicity: 'medium',
+ flameColor: null,
+ flameName: null,
+ applications: ['medicine', 'photography', 'chemistry'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 79 Золото ════════════════════════════════════════ */
+ 79: {
+ radius: { atomic: 174, covalent: 136, vanderwaals: 166 },
+ ionization: { e1: 890, e2: 1980, e3: null },
+ electronAffinity: 223,
+ heatCapacity: 0.129,
+ thermalConductivity: 318,
+ crystalStructure: 'fcc',
+ latticeParam: 408,
+ abundance: 0.004,
+ biological: 'inert',
+ biologicalRole: 'Биологически инертно; соединения золота (ауранофин) применяются при ревматоидном артрите.',
+ toxicity: 'inert',
+ flameColor: null,
+ flameName: null,
+ applications: ['jewelry', 'electronics', 'medicine'],
+ applicationsText: 'Ювелирное дело, позолота, контакты и разъёмы в электронике (коррозионностойкость), зубные протезы, препараты для лечения артрита, стандарт денежного обращения.',
+ historyText: 'Известно с ок. 5000 лет до н.э. как «вечный металл», не темнеющий и не корродирующий. Один из первых металлов, освоенных человечеством. Не растворяется в большинстве кислот (кроме царской водки).',
+ etymology: 'Лат. aurum ← праиндоевр. *h₂é-h₂us-o- — рассвет; символ Au. Рус. «золото» от «зелёный» (предположительно).',
+ mineralForms: [
+ { name: 'Золото самородное', formula: 'Au' },
+ { name: 'Калаверит', formula: 'AuTe₂' },
+ { name: 'Сильванит', formula: 'AuAgTe₄' }
+ ],
+ reactions: ['2Au + 3Cl₂ → 2AuCl₃', 'Au + HNO₃ + 3HCl → AuCl₃ + NO + 2H₂O (царская водка)', '4Au + 8NaCN + O₂ + 2H₂O → 4Na[Au(CN)₂] + 4NaOH']
+ },
+
+ /* ══ Z = 80 Ртуть (minimum) ════════════════════════════════ */
+ 80: {
+ radius: { atomic: 171, covalent: 132, vanderwaals: 155 },
+ ionization: { e1: 1007, e2: null, e3: null },
+ electronAffinity: 0,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 0.085,
+ biological: 'toxic',
+ biologicalRole: null,
+ toxicity: 'high',
+ flameColor: null,
+ flameName: null,
+ applications: ['instruments', 'lighting', 'chemistry'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ },
+
+ /* ══ Z = 82 Свинец (minimum) ═══════════════════════════════ */
+ 82: {
+ radius: { atomic: 180, covalent: 146, vanderwaals: 202 },
+ ionization: { e1: 716, e2: null, e3: null },
+ electronAffinity: 35,
+ heatCapacity: null,
+ thermalConductivity: null,
+ crystalStructure: null,
+ latticeParam: null,
+ abundance: 14,
+ biological: 'toxic',
+ biologicalRole: null,
+ toxicity: 'high',
+ flameColor: null,
+ flameName: null,
+ applications: ['batteries', 'radiation_shielding', 'construction'],
+ applicationsText: null,
+ historyText: null,
+ etymology: null,
+ mineralForms: [],
+ reactions: []
+ }
+
+ } /* end byZ */
+}; /* end PERIODIC_EXT_DATA */
+
+
+/* ══════════════════════════════════════════════════════════════
+ PERIODIC_ISOTOPES — база изотопов
+ ══════════════════════════════════════════════════════════════ */
+window.PERIODIC_ISOTOPES = {
+
+ /* Водород */
+ 1: [
+ { A: 1, mass: 1.00794, abundance: 99.985, halfLife: 'stable', decay: null },
+ { A: 2, mass: 2.01410, abundance: 0.015, halfLife: 'stable', decay: null },
+ { A: 3, mass: 3.01605, abundance: null, halfLife: '12.32 лет', decay: 'β−' }
+ ],
+
+ /* Гелий */
+ 2: [
+ { A: 3, mass: 3.01603, abundance: 0.0002, halfLife: 'stable', decay: null },
+ { A: 4, mass: 4.00260, abundance: 99.9998,halfLife: 'stable', decay: null }
+ ],
+
+ /* Литий */
+ 3: [
+ { A: 6, mass: 6.01512, abundance: 7.59, halfLife: 'stable', decay: null },
+ { A: 7, mass: 7.01601, abundance: 92.41, halfLife: 'stable', decay: null }
+ ],
+
+ /* Углерод */
+ 6: [
+ { A: 12, mass: 12.00000, abundance: 98.93, halfLife: 'stable', decay: null },
+ { A: 13, mass: 13.00335, abundance: 1.07, halfLife: 'stable', decay: null },
+ { A: 14, mass: 14.00324, abundance: null, halfLife: '5730 лет', decay: 'β−' }
+ ],
+
+ /* Азот */
+ 7: [
+ { A: 14, mass: 14.00307, abundance: 99.636, halfLife: 'stable', decay: null },
+ { A: 15, mass: 15.00011, abundance: 0.364, halfLife: 'stable', decay: null }
+ ],
+
+ /* Кислород */
+ 8: [
+ { A: 16, mass: 15.99491, abundance: 99.762, halfLife: 'stable', decay: null },
+ { A: 17, mass: 16.99913, abundance: 0.038, halfLife: 'stable', decay: null },
+ { A: 18, mass: 17.99916, abundance: 0.200, halfLife: 'stable', decay: null }
+ ],
+
+ /* Фтор */
+ 9: [
+ { A: 19, mass: 18.99840, abundance: 100.0, halfLife: 'stable', decay: null }
+ ],
+
+ /* Неон */
+ 10: [
+ { A: 20, mass: 19.99244, abundance: 90.48, halfLife: 'stable', decay: null },
+ { A: 21, mass: 20.99385, abundance: 0.27, halfLife: 'stable', decay: null },
+ { A: 22, mass: 21.99139, abundance: 9.25, halfLife: 'stable', decay: null }
+ ],
+
+ /* Натрий */
+ 11: [
+ { A: 23, mass: 22.98977, abundance: 100.0, halfLife: 'stable', decay: null }
+ ],
+
+ /* Магний */
+ 12: [
+ { A: 24, mass: 23.98504, abundance: 78.99, halfLife: 'stable', decay: null },
+ { A: 25, mass: 24.98584, abundance: 10.00, halfLife: 'stable', decay: null },
+ { A: 26, mass: 25.98259, abundance: 11.01, halfLife: 'stable', decay: null }
+ ],
+
+ /* Кобальт — ⁶⁰Co (медицинский) */
+ 27: [
+ { A: 59, mass: 58.93320, abundance: 100.0, halfLife: 'stable', decay: null },
+ { A: 60, mass: 59.93382, abundance: null, halfLife: '5.27 лет', decay: 'β−' }
+ ],
+
+ /* Стронций — ⁹⁰Sr (Чернобыль) */
+ 38: [
+ { A: 84, mass: 83.91342, abundance: 0.56, halfLife: 'stable', decay: null },
+ { A: 86, mass: 85.90926, abundance: 9.86, halfLife: 'stable', decay: null },
+ { A: 87, mass: 86.90888, abundance: 7.00, halfLife: 'stable', decay: null },
+ { A: 88, mass: 87.90561, abundance: 82.58, halfLife: 'stable', decay: null },
+ { A: 90, mass: 89.90773, abundance: null, halfLife: '28.8 лет', decay: 'β−' }
+ ],
+
+ /* Технеций — все изотопы радиоактивны */
+ 43: [
+ { A: 97, mass: 96.90637, abundance: null, halfLife: '4.2 млн лет', decay: 'EC' },
+ { A: 98, mass: 97.90722, abundance: null, halfLife: '4.2 млн лет', decay: 'β−' },
+ { A: 99, mass: 98.90625, abundance: null, halfLife: '2.11×10⁵ лет',decay: 'β−' },
+ { A: 99, mass: 98.90625, abundance: null, halfLife: '6.01 ч', decay: 'γ' }
+ ],
+
+ /* Йод — ¹³¹I (медицинский/ядерный) */
+ 53: [
+ { A: 127, mass: 126.90448, abundance: 100.0, halfLife: 'stable', decay: null },
+ { A: 131, mass: 130.90612, abundance: null, halfLife: '8.02 дней', decay: 'β−' }
+ ],
+
+ /* Цезий — ¹³⁷Cs (Чернобыль) */
+ 55: [
+ { A: 133, mass: 132.90545, abundance: 100.0, halfLife: 'stable', decay: null },
+ { A: 137, mass: 136.90709, abundance: null, halfLife: '30.2 лет', decay: 'β−' }
+ ],
+
+ /* Калий */
+ 19: [
+ { A: 39, mass: 38.96371, abundance: 93.258, halfLife: 'stable', decay: null },
+ { A: 40, mass: 39.96400, abundance: 0.012, halfLife: '1.25×10⁹ лет', decay: 'β−' },
+ { A: 41, mass: 40.96183, abundance: 6.730, halfLife: 'stable', decay: null }
+ ],
+
+ /* Кальций */
+ 20: [
+ { A: 40, mass: 39.96259, abundance: 96.941, halfLife: 'stable', decay: null },
+ { A: 42, mass: 41.95862, abundance: 0.647, halfLife: 'stable', decay: null },
+ { A: 43, mass: 42.95877, abundance: 0.135, halfLife: 'stable', decay: null },
+ { A: 44, mass: 43.95548, abundance: 2.086, halfLife: 'stable', decay: null },
+ { A: 46, mass: 45.95369, abundance: 0.004, halfLife: 'stable', decay: null },
+ { A: 48, mass: 47.95253, abundance: 0.187, halfLife: 'stable', decay: null }
+ ],
+
+ /* Свинец — цепочка распада */
+ 82: [
+ { A: 204, mass: 203.97304, abundance: 1.4, halfLife: 'stable', decay: null },
+ { A: 206, mass: 205.97447, abundance: 24.1, halfLife: 'stable', decay: null },
+ { A: 207, mass: 206.97589, abundance: 22.1, halfLife: 'stable', decay: null },
+ { A: 208, mass: 207.97667, abundance: 52.4, halfLife: 'stable', decay: null },
+ { A: 210, mass: 209.98419, abundance: null, halfLife: '22.3 лет', decay: 'β−' },
+ { A: 214, mass: 213.99980, abundance: null, halfLife: '26.8 мин', decay: 'β−' }
+ ],
+
+ /* Уран */
+ 92: [
+ { A: 234, mass: 234.04095, abundance: 0.005, halfLife: '2.46×10⁵ лет', decay: 'α' },
+ { A: 235, mass: 235.04393, abundance: 0.720, halfLife: '7.04×10⁸ лет', decay: 'α' },
+ { A: 238, mass: 238.05079, abundance: 99.275,halfLife: '4.47×10⁹ лет', decay: 'α' }
+ ],
+
+ /* Плутоний */
+ 94: [
+ { A: 238, mass: 238.04956, abundance: null, halfLife: '87.7 лет', decay: 'α' },
+ { A: 239, mass: 239.05216, abundance: null, halfLife: '2.41×10⁴ лет', decay: 'α' },
+ { A: 240, mass: 240.05381, abundance: null, halfLife: '6.56×10³ лет', decay: 'α' },
+ { A: 241, mass: 241.05685, abundance: null, halfLife: '14.4 лет', decay: 'β−' }
+ ]
+
+}; /* end PERIODIC_ISOTOPES */
+
+
+/* ══════════════════════════════════════════════════════════════
+ PERIODIC_SPECTRA — линии эмиссионных спектров (видимый диапазон)
+ wavelength в нм, intensity 0–1 (относительная)
+ ══════════════════════════════════════════════════════════════ */
+window.PERIODIC_SPECTRA = {
+
+ /* Водород — серия Бальмера */
+ 1: [
+ { wavelength: 656.3, intensity: 1.00, label: 'Hα' },
+ { wavelength: 486.1, intensity: 0.36, label: 'Hβ' },
+ { wavelength: 434.0, intensity: 0.17, label: 'Hγ' },
+ { wavelength: 410.2, intensity: 0.09, label: 'Hδ' },
+ { wavelength: 397.0, intensity: 0.05, label: 'Hε' }
+ ],
+
+ /* Гелий */
+ 2: [
+ { wavelength: 667.8, intensity: 0.60, label: 'He 667' },
+ { wavelength: 587.6, intensity: 1.00, label: 'D₃' },
+ { wavelength: 501.6, intensity: 0.40, label: 'He 501' },
+ { wavelength: 492.2, intensity: 0.25, label: 'He 492' },
+ { wavelength: 447.1, intensity: 0.45, label: 'He 447' },
+ { wavelength: 402.6, intensity: 0.20, label: 'He 402' }
+ ],
+
+ /* Литий */
+ 3: [
+ { wavelength: 670.8, intensity: 1.00, label: 'Li 671' },
+ { wavelength: 610.4, intensity: 0.12, label: 'Li 610' },
+ { wavelength: 460.3, intensity: 0.08, label: 'Li 460' }
+ ],
+
+ /* Натрий */
+ 11: [
+ { wavelength: 589.6, intensity: 0.85, label: 'D₂' },
+ { wavelength: 589.0, intensity: 1.00, label: 'D₁' },
+ { wavelength: 568.3, intensity: 0.08, label: 'Na 568' },
+ { wavelength: 498.3, intensity: 0.05, label: 'Na 498' },
+ { wavelength: 466.5, intensity: 0.05, label: 'Na 466' }
+ ],
+
+ /* Калий */
+ 19: [
+ { wavelength: 769.9, intensity: 1.00, label: 'K 770' },
+ { wavelength: 766.5, intensity: 0.90, label: 'K 766' },
+ { wavelength: 693.9, intensity: 0.12, label: 'K 694' },
+ { wavelength: 578.2, intensity: 0.06, label: 'K 578' },
+ { wavelength: 404.4, intensity: 0.15, label: 'K 404' }
+ ],
+
+ /* Неон */
+ 10: [
+ { wavelength: 703.2, intensity: 0.90, label: 'Ne 703' },
+ { wavelength: 671.7, intensity: 0.60, label: 'Ne 671' },
+ { wavelength: 667.8, intensity: 0.55, label: 'Ne 667' },
+ { wavelength: 640.2, intensity: 1.00, label: 'Ne 640' },
+ { wavelength: 614.3, intensity: 0.85, label: 'Ne 614' },
+ { wavelength: 585.2, intensity: 0.50, label: 'Ne 585' },
+ { wavelength: 540.1, intensity: 0.30, label: 'Ne 540' }
+ ],
+
+ /* Аргон */
+ 18: [
+ { wavelength: 763.5, intensity: 1.00, label: 'Ar 763' },
+ { wavelength: 811.5, intensity: 0.90, label: 'Ar 811' },
+ { wavelength: 750.4, intensity: 0.75, label: 'Ar 750' },
+ { wavelength: 696.5, intensity: 0.60, label: 'Ar 696' },
+ { wavelength: 706.7, intensity: 0.55, label: 'Ar 706' },
+ { wavelength: 727.3, intensity: 0.45, label: 'Ar 727' }
+ ],
+
+ /* Ртуть */
+ 80: [
+ { wavelength: 623.4, intensity: 0.20, label: 'Hg 623' },
+ { wavelength: 579.1, intensity: 0.90, label: 'Hg 579' },
+ { wavelength: 577.0, intensity: 0.85, label: 'Hg 577' },
+ { wavelength: 546.1, intensity: 1.00, label: 'Hg 546' },
+ { wavelength: 435.8, intensity: 0.70, label: 'Hg 435' },
+ { wavelength: 404.7, intensity: 0.55, label: 'Hg 404' }
+ ]
+
+}; /* end PERIODIC_SPECTRA */
diff --git a/frontend/js/labs/periodic.js b/frontend/js/labs/periodic.js
index fb5f8cf..23ccd0f 100644
--- a/frontend/js/labs/periodic.js
+++ b/frontend/js/labs/periodic.js
@@ -207,10 +207,24 @@ class PeriodicTableSim {
this._bohrRaf = null;
this._bohrAngle = 0;
+ // visual modes (wave B)
+ this._heatProp = 'En'; // heatmap property key
+ this._heatLog = false; // log scale
+ this._heatActive = false; // heatmap mode on
+ this._trendProp = 'radius'; // trend arrows property
+ this._trendOn = false; // trend arrows visible
+ this._tableShape = 'std'; // std | long | short
+ this._3dActive = false; // three.js 3D mode
+ this._3dMode = 'bar'; // bar | wave | stack
+ this._3dScene = null;
+ this._3dRaf = null;
+ this._heatTweens = []; // active tween handles
+
// build
this._buildUI();
this._buildTable();
this._updateCard(null);
+ this._buildVisualModes();
// chart defaults
this._drawChart();
@@ -293,13 +307,13 @@ class PeriodicTableSim {
/* right panel */
const rightCol = document.createElement('div');
- rightCol.style.cssText = 'width:260px;flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid rgba(255,255,255,0.07);overflow:hidden;';
+ rightCol.style.cssText = 'width:280px;flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid rgba(255,255,255,0.07);overflow:hidden;';
main.appendChild(rightCol);
/* element card */
this._cardEl = document.createElement('div');
this._cardEl.id = 'ptbl-card';
- this._cardEl.style.cssText = 'flex:1;overflow-y:auto;padding:12px 10px 8px;font-size:.78rem;color:#ccc;';
+ this._cardEl.style.cssText = 'flex:1;overflow:hidden;display:flex;flex-direction:column;font-size:.78rem;color:#ccc;';
rightCol.appendChild(this._cardEl);
/* Bohr shells canvas */
@@ -528,7 +542,7 @@ class PeriodicTableSim {
}
/* ─────────────────────────────────────────────────────
- ELEMENT CARD
+ ELEMENT CARD (legacy — no-selection state only)
───────────────────────────────────────────────────── */
_updateCard(el) {
if (!el) {
@@ -539,27 +553,7 @@ class PeriodicTableSim {
this._drawBohr();
return;
}
- const col = TYPE_COLORS[el.type] || '#888';
- const ox = el.oxStates && el.oxStates[0] !== null ? el.oxStates.map(s => (s > 0 ? '+' : '') + s).join(', ') : '—';
- const fmt = v => v !== null && v !== undefined ? v : '—';
- this._cardEl.innerHTML = `
-
-
${el.symbol}
-
${el.name}
-
Z = ${el.Z} · ${el.mass} а.е.м.
-
${TYPE_LABELS[el.type] || el.type}
-
-
- ${this._row('Конфигурация', `${el.config}`)}
- ${this._row('Блок', el.block + '-блок')}
- ${this._row('Период / Группа', `${el.period} / ${el.group || '—'}`)}
- ${this._row('Ст. окисления', ox)}
- ${this._row('ЭО (Полинг)', fmt(el.En))}
- ${this._row('Плотность, г/см³', fmt(el.density))}
- ${this._row('Tпл , K', fmt(el.melt))}
- ${this._row('Tкип , K', fmt(el.boil))}
- ${this._row('Открыт', el.discovered ? `${el.discovered}, ${el.by}` : el.by)}
-
`;
+ this._renderCardV2(el);
}
_row(label, val) {
@@ -569,6 +563,567 @@ class PeriodicTableSim {
`;
}
+ /* ─────────────────────────────────────────────────────
+ TABBED CARD V2
+ ───────────────────────────────────────────────────── */
+
+ /* Tab definitions: id, label */
+ _cardTabs() {
+ return [
+ { id: 'overview', label: 'Обзор' },
+ { id: 'properties', label: 'Свойства' },
+ { id: 'electronics', label: 'Электроника' },
+ { id: 'isotopes', label: 'Изотопы' },
+ { id: 'history', label: 'История' },
+ { id: 'applications', label: 'Применения' },
+ { id: 'biology', label: 'Биология' },
+ { id: 'minerals', label: 'Минералы' },
+ { id: 'spectrum', label: 'Спектр' },
+ { id: 'flame', label: 'Пламя' },
+ { id: 'reactions', label: 'Реакции' },
+ ];
+ }
+
+ _renderCardV2(el) {
+ const col = TYPE_COLORS[el.type] || '#888';
+ this._cardActiveTab = this._cardActiveTab || 'overview';
+
+ /* build outer shell */
+ this._cardEl.innerHTML = `
+
+
+
+
+
+
${el.Z}
+
${el.symbol}
+
${el.name}
+
${el.mass} а.е.м.
+
${TYPE_LABELS[el.type] || el.type}
+
+
+ ${this._cardTabs().map(t => `${t.label} `).join('')}
+
+
+
`;
+
+ /* close button */
+ this._cardEl.querySelector('.ptbl-card-close').addEventListener('click', () => {
+ this._updateCard(null);
+ for (const e2 of ELEMENTS) {
+ const div = this._cellMap[e2.Z];
+ if (div) { div.style.outline = ''; div.style.outlineOffset = ''; }
+ }
+ });
+
+ /* tab switching */
+ this._cardEl.querySelectorAll('.ptbl-tab').forEach(btn => {
+ btn.addEventListener('click', () => {
+ this._cardActiveTab = btn.dataset.tab;
+ this._cardEl.querySelectorAll('.ptbl-tab').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ this._switchTabContent(el, btn.dataset.tab);
+ });
+ });
+
+ /* render initial tab */
+ this._switchTabContent(el, this._cardActiveTab);
+ }
+
+ _switchTabContent(el, tabId) {
+ const body = this._cardEl.querySelector('#ptbl-tab-body');
+ if (!body) return;
+ body.style.opacity = '0';
+ setTimeout(() => {
+ let html = '';
+ switch (tabId) {
+ case 'overview': html = this._renderTab_overview(el); break;
+ case 'properties': html = this._renderTab_properties(el); break;
+ case 'electronics': html = this._renderTab_electronics(el); break;
+ case 'isotopes': html = this._renderTab_isotopes(el); break;
+ case 'history': html = this._renderTab_history(el); break;
+ case 'applications': html = this._renderTab_applications(el); break;
+ case 'biology': html = this._renderTab_biology(el); break;
+ case 'minerals': html = this._renderTab_minerals(el); break;
+ case 'spectrum': html = this._renderTab_spectrum(el); break;
+ case 'flame': html = this._renderTab_flame(el); break;
+ case 'reactions': html = this._renderTab_reactions(el); break;
+ default: html = this._renderTab_overview(el);
+ }
+ body.innerHTML = html;
+ body.style.opacity = '1';
+ if (tabId === 'electronics') this._postRenderElectronics(el);
+ if (tabId === 'isotopes') this._postRenderIsotopesChart(el);
+ if (tabId === 'spectrum') this._postRenderSpectrum(el);
+ if (tabId === 'reactions' && window.renderMathInElement) {
+ renderMathInElement(body, { delimiters: [{ left: '$$', right: '$$', display: false }] });
+ }
+ }, 100);
+ }
+
+ /* ── Tab 1: Обзор ── */
+ _renderTab_overview(el) {
+ const col = TYPE_COLORS[el.type] || '#888';
+ const fmt = v => (v !== null && v !== undefined) ? v : '—';
+ const ox = el.oxStates && el.oxStates[0] !== null
+ ? el.oxStates.map(s => (s > 0 ? '+' : '') + s).join(', ')
+ : '—';
+ const summary = el.summary || el.description || '';
+ return `
+
+ ${summary ? `
${summary}
` : ''}
+
+
+ Тип
+ ${TYPE_LABELS[el.type] || el.type}
+
+
+ Конфигурация
+ ${el.config}
+
+
+ Ст. окисления
+ ${ox}
+
+
+ ЭО (Полинг)
+ ${fmt(el.En)}
+
+
+ Период / Группа
+ ${el.period} / ${el.group || '—'}
+
+
+ Блок
+ ${el.block}-блок
+
+
+
`;
+ }
+
+ /* ── Tab 2: Свойства ── */
+ _renderTab_properties(el) {
+ const fmt = v => (v !== null && v !== undefined) ? v : '—';
+ const rows = [
+ ['Атомная масса, а.е.м.', fmt(el.mass)],
+ ['Плотность, г/см³', fmt(el.density)],
+ ['Tпл , K', fmt(el.melt)],
+ ['Tкип , K', fmt(el.boil)],
+ ['ЭО (Полинг)', fmt(el.En)],
+ ['Атомный радиус, пм', el.radius?.atomic ?? '—'],
+ ['Ковалентный радиус, пм',el.radius?.covalent ?? '—'],
+ ['Ионный радиус, пм', el.radius?.ionic ?? '—'],
+ ['1-я эн. ионизации, эВ', el.ionization?.[0] ?? '—'],
+ ['2-я эн. ионизации, эВ', el.ionization?.[1] ?? '—'],
+ ['Электронное сродство', el.electronAffinity ?? '—'],
+ ['Теплоёмкость, Дж/(г·K)',el.heatCapacity ?? '—'],
+ ['Теплопроводность', el.thermalConductivity ?? '—'],
+ ['Электросопротивление', el.electricalResistivity ?? '—'],
+ ['Кристаллич. структура', el.crystalStructure ?? '—'],
+ ['Параметр решётки, пм', el.latticeParam ?? '—'],
+ ['Открыт', el.discovered ? `${el.discovered}, ${el.by}` : (el.by || '—')],
+ ];
+ return `
+
+
+ ${rows.map(([l, v]) => `${l} ${v} `).join('')}
+
+
`;
+ }
+
+ /* ── Tab 3: Электроника ── */
+ _renderTab_electronics(el) {
+ return `
+
+
+
+
+
+ Электронная конфигурация
+ ${el.config}
+
+
+ Оболочки (K, L, M, …)
+ ${getShellFill(el.Z).join(' | ')}
+
+
`;
+ }
+
+ _postRenderElectronics(el) {
+ const canvas = this._cardEl.querySelector('#ptbl-bohr-inline');
+ if (!canvas) return;
+ const dpr = window.devicePixelRatio || 1;
+ const W = canvas.offsetWidth || 220;
+ const H = 160;
+ canvas.width = W * dpr;
+ canvas.height = H * dpr;
+ const ctx = canvas.getContext('2d');
+ ctx.scale(dpr, dpr);
+ ctx.clearRect(0, 0, W, H);
+
+ const shells = getShellFill(el.Z);
+ const col = TYPE_COLORS[el.type] || '#7B8EF7';
+ const cx = W / 2, cy = H / 2;
+ const maxR = Math.min(W, H) * 0.44;
+ const nShells = shells.length;
+
+ ctx.beginPath();
+ ctx.arc(cx, cy, nShells > 0 ? 5 + nShells * 1.5 : 6, 0, Math.PI * 2);
+ ctx.fillStyle = col;
+ ctx.fill();
+
+ shells.forEach((count, i) => {
+ const r = maxR * (i + 1) / nShells;
+ ctx.beginPath();
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
+ ctx.strokeStyle = 'rgba(255,255,255,0.12)';
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ for (let e = 0; e < count; e++) {
+ const a = (2 * Math.PI * e) / count - Math.PI / 2;
+ const ex = cx + r * Math.cos(a);
+ const ey = cy + r * Math.sin(a);
+ ctx.beginPath();
+ ctx.arc(ex, ey, 2.5, 0, Math.PI * 2);
+ ctx.fillStyle = '#06D6E0';
+ ctx.fill();
+ }
+ });
+
+ ctx.font = `700 10px Manrope,sans-serif`;
+ ctx.fillStyle = 'rgba(255,255,255,0.5)';
+ ctx.textAlign = 'center';
+ ctx.fillText(shells.join(','), cx, H - 4);
+ }
+
+ /* ── Tab 4: Изотопы ── */
+ _renderTab_isotopes(el) {
+ const isos = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || [];
+ if (!isos.length) {
+ return `Данные об изотопах не загружены.
`;
+ }
+
+ let avgMass = null;
+ const stableIsos = isos.filter(iso => iso.abundance != null && iso.abundance > 0);
+ if (stableIsos.length) {
+ const total = stableIsos.reduce((s, iso) => s + iso.abundance, 0);
+ avgMass = stableIsos.reduce((s, iso) => s + iso.mass * iso.abundance, 0) / total;
+ }
+
+ const rows = isos.map(iso => {
+ const abStr = iso.abundance != null ? (iso.abundance * 100).toFixed(2) + '%' : '—';
+ const hlStr = iso.halfLife || '—';
+ const decStr = iso.decay || '—';
+ return `
+ ${iso.massNum ?? iso.mass} ${el.symbol}
+ ${typeof iso.mass === 'number' ? iso.mass.toFixed(4) : (iso.mass ?? '—')}
+ ${abStr}
+ ${hlStr}
+ ${decStr}
+ `;
+ }).join('');
+
+ return `
+
+
+ Изотоп Масса Распр. T½ Распад
+ ${rows}
+
+ ${avgMass != null
+ ? `
Средняя атомная масса (взвеш.): ${avgMass.toFixed(4)} а.е.м.
`
+ : ''}
+
+
`;
+ }
+
+ _postRenderIsotopesChart(el) {
+ const canvas = this._cardEl.querySelector('#ptbl-iso-chart');
+ if (!canvas) return;
+ const isos = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || [];
+ const stable = isos.filter(iso => iso.abundance != null && iso.abundance > 0);
+ if (!stable.length) return;
+
+ const dpr = window.devicePixelRatio || 1;
+ const W = canvas.offsetWidth || 220;
+ const H = 70;
+ canvas.width = W * dpr;
+ canvas.height = H * dpr;
+ const ctx = canvas.getContext('2d');
+ ctx.scale(dpr, dpr);
+ ctx.clearRect(0, 0, W, H);
+
+ const col = TYPE_COLORS[el.type] || '#7B8EF7';
+ const pad = { t: 4, r: 4, b: 18, l: 4 };
+ const gW = W - pad.l - pad.r;
+ const gH = H - pad.t - pad.b;
+ const n = stable.length;
+ const bw = Math.max(2, (gW / n) - 2);
+ const maxA = Math.max(...stable.map(iso => iso.abundance));
+
+ stable.forEach((iso, i) => {
+ const bh = (iso.abundance / maxA) * gH;
+ const x = pad.l + i * (gW / n) + (gW / n - bw) / 2;
+ const y = pad.t + gH - bh;
+ ctx.fillStyle = col + 'bb';
+ ctx.fillRect(x, y, bw, bh);
+ ctx.font = '8px Manrope,sans-serif';
+ ctx.fillStyle = 'rgba(255,255,255,0.5)';
+ ctx.textAlign = 'center';
+ ctx.fillText(String(iso.massNum ?? ''), x + bw / 2, H - 4);
+ });
+ }
+
+ /* ── Tab 5: История ── */
+ _renderTab_history(el) {
+ const year = el.discovered ? String(el.discovered) : null;
+ const by = el.by || '—';
+ const hist = el.historyText || '';
+ const etym = el.etymology || '';
+
+ return `
+
+
+
${year || 'Древний мир'}
+
${by}
+ ${el.discoveryCountry ? `
${el.discoveryCountry}
` : ''}
+
+ ${hist ? `
${hist}
` : '
История не указана.
'}
+ ${etym ? `
Этимология: ${etym}
` : ''}
+
`;
+ }
+
+ /* ── Tab 6: Применения ── */
+ _renderTab_applications(el) {
+ const apps = el.applications || [];
+ const desc = el.applicationsDescription || '';
+ const iconMap = {
+ battery: ' ',
+ medicine: ' ',
+ electronics: ' ',
+ metallurgy: ' ',
+ construction: ' ',
+ nuclear: ' ',
+ lighting: ' ',
+ catalyst: ' ',
+ jewelry: ' ',
+ fertilizer: ' ',
+ semiconductor: ' ',
+ pigment: ' ',
+ aerospace: ' ',
+ optical: ' ',
+ food: ' ',
+ };
+ const defaultIcon = ' ';
+
+ if (!apps.length && !desc) {
+ return `Применения не указаны.
`;
+ }
+
+ const cards = apps.map(tag => {
+ const ico = iconMap[tag] || defaultIcon;
+ const label = tag.charAt(0).toUpperCase() + tag.slice(1);
+ return `${ico}${label}
`;
+ }).join('');
+
+ return `
+
+ ${cards ? `
${cards}
` : ''}
+ ${desc ? `
${desc}
` : ''}
+
`;
+ }
+
+ /* ── Tab 7: Биология ── */
+ _renderTab_biology(el) {
+ const bio = el.biological || null;
+ const role = el.biologicalRole || '';
+ const bioLabels = {
+ macro: 'Макроэлемент (жизненно важен)',
+ micro: 'Микроэлемент (жизненно важен)',
+ trace: 'Следовой элемент',
+ toxic: 'Токсичен для живых организмов',
+ inert: 'Биологически инертен',
+ radioactive: 'Радиоактивен / опасен',
+ };
+ const bioColors = {
+ macro: '#06D6E0', micro: '#7BF5A4', trace: '#FFD166',
+ toxic: '#EF476F', inert: '#888', radioactive: '#F15BB5',
+ };
+ const bioColor = bio ? (bioColors[bio] || '#888') : '#888';
+ const bioLabel = bio ? (bioLabels[bio] || bio) : 'Нет данных';
+
+ return `
+
+
${bioLabel}
+ ${role ? `
${role}
` : '
Биологическая роль не указана.
'}
+ ${el.toxicity ? `
Токсичность: ${el.toxicity}
` : ''}
+
`;
+ }
+
+ /* ── Tab 8: Минералы ── */
+ _renderTab_minerals(el) {
+ const mins = el.mineralForms || [];
+ const sources = el.mineralSources || '';
+
+ if (!mins.length && !sources) {
+ return `Данные о минералах не указаны.
`;
+ }
+
+ const items = mins.map(m => {
+ const name = typeof m === 'object' ? (m.name || '—') : m;
+ const formula = typeof m === 'object' ? (m.formula || '') : '';
+ return `
+ ${name}
+ ${formula ? `${formula}` : ''}
+
`;
+ }).join('');
+
+ return `
+
+ ${items ? `
${items}
` : ''}
+ ${sources ? `
Источники: ${sources}
` : ''}
+
`;
+ }
+
+ /* ── Tab 9: Спектр ── */
+ _renderTab_spectrum(el) {
+ if (!el.spectrum || !el.spectrum.length) {
+ return `Спектральные данные не указаны.
`;
+ }
+ return `
+
+
+
+ ${el.spectrum.map(s => {
+ const nm = typeof s === 'object' ? s.nm : s;
+ return `${nm} нм `;
+ }).join('')}
+
+
`;
+ }
+
+ _postRenderSpectrum(el) {
+ const canvas = this._cardEl.querySelector('#ptbl-spec-canvas');
+ if (!canvas || !el.spectrum || !el.spectrum.length) return;
+
+ const dpr = window.devicePixelRatio || 1;
+ const W = canvas.offsetWidth || 220;
+ const H = 80;
+ canvas.width = W * dpr;
+ canvas.height = H * dpr;
+ const ctx = canvas.getContext('2d');
+ ctx.scale(dpr, dpr);
+
+ /* rainbow background 380-780 nm */
+ const grad = ctx.createLinearGradient(0, 0, W, 0);
+ grad.addColorStop(0, '#6600ff');
+ grad.addColorStop(0.10, '#4400ff');
+ grad.addColorStop(0.20, '#0000ff');
+ grad.addColorStop(0.30, '#00aaff');
+ grad.addColorStop(0.40, '#00ffcc');
+ grad.addColorStop(0.50, '#00ff00');
+ grad.addColorStop(0.60, '#aaff00');
+ grad.addColorStop(0.70, '#ffff00');
+ grad.addColorStop(0.80, '#ff8800');
+ grad.addColorStop(0.90, '#ff2200');
+ grad.addColorStop(1.0, '#880000');
+ ctx.fillStyle = grad;
+ ctx.fillRect(0, 0, W, H);
+
+ ctx.fillStyle = 'rgba(0,0,0,0.55)';
+ ctx.fillRect(0, 0, W, H);
+
+ el.spectrum.forEach(s => {
+ const nm = typeof s === 'object' ? s.nm : s;
+ const int = typeof s === 'object' ? (s.intensity ?? 1) : 1;
+ if (!nm || nm < 380 || nm > 780) return;
+ const x = ((nm - 380) / 400) * W;
+ const col = this._nmToRGB(nm);
+ ctx.strokeStyle = col;
+ ctx.lineWidth = Math.max(1, int * 2);
+ ctx.globalAlpha = 0.7 + int * 0.3;
+ ctx.beginPath();
+ ctx.moveTo(x, 0);
+ ctx.lineTo(x, H - 18);
+ ctx.stroke();
+ ctx.globalAlpha = 1;
+ ctx.font = '7px Manrope,sans-serif';
+ ctx.fillStyle = col;
+ ctx.textAlign = 'center';
+ ctx.fillText(String(nm), x, H - 4);
+ });
+ }
+
+ _nmToRGB(nm) {
+ if (nm < 380) return '#8800ff';
+ if (nm < 440) return `hsl(${270 + (nm - 380) * 0.5},100%,60%)`;
+ if (nm < 490) return `hsl(${240 - (nm - 440) * 1.2},100%,55%)`;
+ if (nm < 510) return `hsl(${180 - (nm - 490)},100%,50%)`;
+ if (nm < 580) return `hsl(${120 - (nm - 510) * 0.7},100%,45%)`;
+ if (nm < 645) return `hsl(${60 - (nm - 580) * 0.92},100%,50%)`;
+ return `hsl(0,100%,${Math.max(20, 50 - (nm - 645) * 0.4)}%)`;
+ }
+
+ /* ── Tab 10: Пламя ── */
+ _renderTab_flame(el) {
+ if (!el.flameColor) {
+ return `Данные об окраске пламени не указаны.
`;
+ }
+ return `
+
+
+
Окраска пламени: ${el.flameColorName || el.flameColor}
+
+ При внесении соединений ${el.name} в пламя горелки оно окрашивается
+ в характерный цвет — применяется в качественном анализе и в пиротехнике.
+
+
`;
+ }
+
+ /* ── Tab 11: Реакции ── */
+ _renderTab_reactions(el) {
+ const col = TYPE_COLORS[el.type] || '#888';
+ const reactionsByType = {
+ alkali: [
+ { label: 'С водой', eq: `2${el.symbol} + 2H₂O → 2${el.symbol}OH + H₂↑` },
+ { label: 'С кислородом', eq: `4${el.symbol} + O₂ → 2${el.symbol}₂O` },
+ { label: 'С хлором', eq: `2${el.symbol} + Cl₂ → 2${el.symbol}Cl` },
+ ],
+ alkaline: [
+ { label: 'С водой', eq: `${el.symbol} + 2H₂O → ${el.symbol}(OH)₂ + H₂↑` },
+ { label: 'С кислородом', eq: `2${el.symbol} + O₂ → 2${el.symbol}O` },
+ { label: 'С кислотой', eq: `${el.symbol} + 2HCl → ${el.symbol}Cl₂ + H₂↑` },
+ ],
+ halogen: [
+ { label: 'С натрием', eq: `2Na + ${el.symbol}₂ → 2Na${el.symbol}` },
+ { label: 'С водородом', eq: `H₂ + ${el.symbol}₂ → 2H${el.symbol}` },
+ { label: 'С водой', eq: `${el.symbol}₂ + H₂O → H${el.symbol} + H${el.symbol}O` },
+ ],
+ transition: [
+ { label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` },
+ { label: 'С кислотой', eq: `${el.symbol} + H₂SO₄ → ${el.symbol}SO₄ + H₂↑` },
+ ],
+ nonmetal: [
+ { label: 'С металлом', eq: `Me + ${el.symbol} → Me–${el.symbol}` },
+ { label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` },
+ ],
+ };
+ const reactions = reactionsByType[el.type] || [
+ { label: 'Реакции', eq: `${el.symbol} + реагент → продукт` },
+ ];
+
+ const items = reactions.map(r => `
+ `).join('');
+
+ return `
+
+ ${items}
+
Уравнения приведены в общем виде для ознакомления.
+
`;
+ }
+
/* ─────────────────────────────────────────────────────
BOHR SHELLS ANIMATION
───────────────────────────────────────────────────── */
@@ -739,7 +1294,1940 @@ class PeriodicTableSim {
}
}
+/* ══════════════════════════════════════════════════════════════
+ WAVE B — VISUAL MODES EXTENSION
+ Methods: _buildVisualModes, _drawHeatmap, _init3DTable,
+ _update3D, _morphToView, _drawTrendArrows
+ ══════════════════════════════════════════════════════════════ */
+
+/* ── Heatmap property config ─────────────────────────────────── */
+const HEATMAP_PROPS = [
+ { key: 'En', label: 'Электроотрицательность', unit: 'П.', get: el => el.En },
+ { key: 'mass', label: 'Атомная масса', unit: 'а.е.м.', get: el => el.mass },
+ { key: 'density', label: 'Плотность', unit: 'г/см³', get: el => el.density },
+ { key: 'melt', label: 'T плавления', unit: 'K', get: el => el.melt },
+ { key: 'boil', label: 'T кипения', unit: 'K', get: el => el.boil },
+ { key: 'discovered', label: 'Год открытия', unit: 'г.', get: el => el.discovered },
+ { key: 'radius.atomic', label: 'Атомный радиус', unit: 'пм', get: el => el.radius && el.radius.atomic ? el.radius.atomic : null },
+ { key: 'ionization.e1', label: '1-я энергия ионизации', unit: 'эВ', get: el => el.ionization && el.ionization.e1 ? el.ionization.e1 : null },
+ { key: 'abundance.crust', label: 'Распространённость в коре',unit: 'мг/кг',get: el => el.abundance && el.abundance.crust ? el.abundance.crust : null },
+];
+
+/* ── Trend arrows config ─────────────────────────────────────── */
+const TREND_CONFIG = {
+ radius: {
+ label: 'Атомный радиус',
+ hArrow: { dir: 'left', text: 'Радиус растёт', color: '#06D6E0' },
+ vArrow: { dir: 'down', text: 'Радиус растёт', color: '#06D6E0' },
+ },
+ en: {
+ label: 'Электроотрицательность',
+ hArrow: { dir: 'right', text: 'ЭО растёт', color: '#FF6B6B' },
+ vArrow: { dir: 'up', text: 'ЭО растёт', color: '#FF6B6B' },
+ },
+ ie: {
+ label: 'Энергия ионизации',
+ hArrow: { dir: 'right', text: 'ИЕ растёт', color: '#FFD93D' },
+ vArrow: { dir: 'up', text: 'ИЕ растёт', color: '#FFD93D' },
+ },
+ metal: {
+ label: 'Металличность',
+ hArrow: { dir: 'left', text: 'Металличность растёт', color: '#4CAF50' },
+ vArrow: { dir: 'down', text: 'Металличность растёт', color: '#4CAF50' },
+ },
+};
+
+/* ── Secondary control bar ───────────────────────────────────── */
+PeriodicTableSim.prototype._buildVisualModes = function() {
+ const self = this;
+
+ /* secondary bar container */
+ const bar = document.createElement('div');
+ bar.id = 'ptbl-vmodes-bar';
+ bar.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 12px;border-bottom:1px solid rgba(255,255,255,0.06);flex-wrap:wrap;flex-shrink:0;background:rgba(0,0,0,0.2);';
+
+ /* ─ 1. HEATMAP section ─ */
+ bar.innerHTML = `
+ Тепловая карта:
+
+
+
+
+
+
+ Окрасить
+
+
+
+ ${HEATMAP_PROPS.map(p => `${p.label} `).join('')}
+
+
+ Lin
+
+
+
+ Вид:
+
+
+ Стандартная
+ Длинная (32 кол.)
+ Краткая (8 гр.)
+
+
+ Тренды:
+
+
+
+
+
+ Тренды
+
+
+
+ ${Object.entries(TREND_CONFIG).map(([k,v]) => `${v.label} `).join('')}
+
+
+ 3D:
+
+
+
+
+
+ 3D
+
+
+
+ Bar
+ Wave
+ Stack
+
+ `;
+
+ /* style vm buttons */
+ const vmStyle = 'padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,0.12);background:transparent;color:#aaa;font-size:.72rem;cursor:pointer;display:inline-flex;align-items:center;gap:4px;transition:all .15s;';
+ bar.querySelectorAll('.ptbl-vm-btn').forEach(b => { b.style.cssText = vmStyle; });
+
+ /* insert bar after toolbar (first child of wrap) */
+ const toolbar = this._wrap.querySelector('div');
+ this._wrap.insertBefore(bar, toolbar.nextSibling);
+
+ /* trend canvas overlay */
+ this._trendCanvas = document.createElement('canvas');
+ this._trendCanvas.id = 'ptbl-trend-canvas';
+ this._trendCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0;transition:opacity .3s;';
+ const tableGrid = this._tableEl;
+ const tableParent = tableGrid.parentElement;
+ tableParent.style.position = 'relative';
+ tableParent.appendChild(this._trendCanvas);
+
+ /* 3D canvas */
+ this._canvas3d = document.createElement('canvas');
+ this._canvas3d.id = 'ptbl-3d-canvas';
+ this._canvas3d.style.cssText = 'display:none;flex:1;min-height:0;background:#0D0D1A;';
+ /* insert into main area beside table */
+ const mainArea = this._tableEl.closest('div[style*="flex:1"]') || tableParent;
+ mainArea.parentElement.insertBefore(this._canvas3d, mainArea);
+
+ /* ── wire events ── */
+
+ /* heatmap toggle */
+ bar.querySelector('#ptbl-heat-toggle').addEventListener('click', () => {
+ self._heatActive = !self._heatActive;
+ bar.querySelector('#ptbl-heat-toggle').style.background = self._heatActive ? 'rgba(155,93,229,0.3)' : 'transparent';
+ bar.querySelector('#ptbl-heat-toggle').style.color = self._heatActive ? '#fff' : '#aaa';
+ bar.querySelector('#ptbl-heat-prop').style.display = self._heatActive ? '' : 'none';
+ bar.querySelector('#ptbl-heat-scale').style.display = self._heatActive ? '' : 'none';
+ bar.querySelector('#ptbl-heat-legend').style.display = self._heatActive ? 'flex' : 'none';
+ if (self._heatActive) {
+ self._drawHeatmap(true);
+ } else {
+ self._colorTable();
+ bar.querySelector('#ptbl-heat-legend').innerHTML = '';
+ }
+ });
+
+ bar.querySelector('#ptbl-heat-prop').addEventListener('change', e => {
+ self._heatProp = e.target.value;
+ if (self._heatActive) self._drawHeatmap(true);
+ });
+
+ bar.querySelector('#ptbl-heat-scale').addEventListener('click', () => {
+ self._heatLog = !self._heatLog;
+ bar.querySelector('#ptbl-heat-scale').textContent = self._heatLog ? 'Log' : 'Lin';
+ if (self._heatActive) self._drawHeatmap(true);
+ });
+
+ /* shape selector */
+ bar.querySelector('#ptbl-shape-sel').addEventListener('change', e => {
+ self._tableShape = e.target.value;
+ self._morphToView(e.target.value);
+ });
+
+ /* trend toggle */
+ bar.querySelector('#ptbl-trend-toggle').addEventListener('click', () => {
+ self._trendOn = !self._trendOn;
+ bar.querySelector('#ptbl-trend-toggle').style.background = self._trendOn ? 'rgba(155,93,229,0.3)' : 'transparent';
+ bar.querySelector('#ptbl-trend-toggle').style.color = self._trendOn ? '#fff' : '#aaa';
+ bar.querySelector('#ptbl-trend-prop').style.display = self._trendOn ? '' : 'none';
+ self._trendCanvas.style.opacity = self._trendOn ? '1' : '0';
+ if (self._trendOn) self._drawTrendArrows();
+ });
+
+ bar.querySelector('#ptbl-trend-prop').addEventListener('change', e => {
+ self._trendProp = e.target.value;
+ if (self._trendOn) self._drawTrendArrows();
+ });
+
+ /* 3D toggle */
+ bar.querySelector('#ptbl-3d-toggle').addEventListener('click', () => {
+ if (typeof THREE === 'undefined') {
+ bar.querySelector('#ptbl-3d-toggle').textContent = '3D (нет Three.js)';
+ return;
+ }
+ self._3dActive = !self._3dActive;
+ bar.querySelector('#ptbl-3d-toggle').style.background = self._3dActive ? 'rgba(155,93,229,0.3)' : 'transparent';
+ bar.querySelector('#ptbl-3d-toggle').style.color = self._3dActive ? '#fff' : '#aaa';
+ bar.querySelector('#ptbl-3d-mode').style.display = self._3dActive ? '' : 'none';
+
+ if (self._3dActive) {
+ mainArea.style.display = 'none';
+ self._canvas3d.style.display = 'block';
+ self._init3DTable();
+ } else {
+ mainArea.style.display = '';
+ self._canvas3d.style.display = 'none';
+ if (self._3dRaf) { cancelAnimationFrame(self._3dRaf); self._3dRaf = null; }
+ if (self._3dRenderer) { self._3dRenderer.dispose(); self._3dRenderer = null; }
+ }
+ });
+
+ bar.querySelector('#ptbl-3d-mode').addEventListener('change', e => {
+ self._3dMode = e.target.value;
+ if (self._3dActive) self._init3DTable();
+ });
+
+ this._vmBar = bar;
+};
+
+/* ══════════════════════════════════════════════════════════════
+ 1. HEATMAP
+ ══════════════════════════════════════════════════════════════ */
+PeriodicTableSim.prototype._drawHeatmap = function(animate) {
+ const self = this;
+ const propDef = HEATMAP_PROPS.find(p => p.key === this._heatProp) || HEATMAP_PROPS[0];
+
+ /* gather values */
+ const vals = ELEMENTS.map(el => propDef.get(el));
+ const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v) && v > 0);
+ if (validVals.length === 0) return;
+
+ const rawMin = Math.min(...validVals);
+ const rawMax = Math.max(...validVals);
+
+ const norm = v => {
+ if (v === null || v === undefined || !isFinite(v)) return null;
+ if (this._heatLog) {
+ const logV = Math.log(Math.max(v, 1e-9));
+ const logMin = Math.log(Math.max(rawMin, 1e-9));
+ const logMax = Math.log(Math.max(rawMax, 1e-9));
+ return (logV - logMin) / (logMax - logMin || 1);
+ }
+ return (v - rawMin) / (rawMax - rawMin || 1);
+ };
+
+ const jet = t => {
+ /* jet colormap: blue→cyan→green→yellow→red */
+ const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 3)));
+ const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 2)));
+ const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 1)));
+ return `rgba(${(r*255)|0},${(g*255)|0},${(b*255)|0},0.85)`;
+ };
+
+ /* cancel previous tweens */
+ this._heatTweens.forEach(h => h && h.cancel && h.cancel());
+ this._heatTweens = [];
+
+ ELEMENTS.forEach((el, i) => {
+ const div = this._cellMap[el.Z];
+ if (!div) return;
+ const t = norm(propDef.get(el));
+ const targetBg = t !== null ? jet(t) : 'rgba(60,60,80,0.5)';
+ const targetBorder = t !== null ? jet(Math.min(1, t + 0.1)) : 'rgba(80,80,100,0.4)';
+
+ div.style.border = `1px solid ${targetBorder}`;
+
+ if (animate && window.LabFX && LabFX.motion) {
+ const tFrom = norm(propDef.get(el)) || 0;
+ const delay = i * 4; // stagger
+ setTimeout(() => {
+ if (!self._heatActive) return;
+ const handle = LabFX.motion.tween(0, 1, 400, 'easeInOutCubic', prog => {
+ const jt = (tFrom !== null ? tFrom * prog : 0);
+ div.style.background = t !== null ? jet(t * prog) : 'rgba(60,60,80,' + (0.5 * prog) + ')';
+ });
+ self._heatTweens.push(handle);
+ }, delay);
+ } else {
+ div.style.background = targetBg;
+ }
+ div.style.color = '#fff';
+ div.style.opacity = '1';
+ div.style.boxShadow = '';
+ });
+
+ /* build gradient legend */
+ this._buildHeatLegend(propDef, rawMin, rawMax, jet);
+};
+
+PeriodicTableSim.prototype._buildHeatLegend = function(propDef, minV, maxV, jet) {
+ const legendEl = this._vmBar.querySelector('#ptbl-heat-legend');
+ if (!legendEl) return;
+
+ /* gradient canvas */
+ const W = 120, H = 14;
+ legendEl.innerHTML = `
+ ${propDef.unit ? minV.toFixed(1) + ' ' + propDef.unit : minV.toFixed(1)}
+
+ ${maxV.toFixed(1)}
+ `;
+ legendEl.style.cssText = 'display:flex;align-items:center;gap:2px;flex-shrink:0;';
+
+ const canvas = legendEl.querySelector('#ptbl-heat-grad');
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+ for (let x = 0; x < W; x++) {
+ ctx.fillStyle = jet(x / (W - 1));
+ ctx.fillRect(x, 0, 1, H);
+ }
+};
+
+/* ══════════════════════════════════════════════════════════════
+ 2. 3D TABLE (Three.js)
+ ══════════════════════════════════════════════════════════════ */
+PeriodicTableSim.prototype._init3DTable = function() {
+ const self = this;
+ if (typeof THREE === 'undefined') return;
+
+ /* cleanup previous */
+ if (this._3dRaf) { cancelAnimationFrame(this._3dRaf); this._3dRaf = null; }
+ if (this._3dRenderer) { this._3dRenderer.dispose(); this._3dRenderer = null; }
+ if (this._3dScene) { this._3dScene = null; }
+
+ const canvas = this._canvas3d;
+ const W = canvas.offsetWidth || 600;
+ const H = canvas.offsetHeight || 400;
+
+ const scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x0d0d1a);
+ this._3dScene = scene;
+
+ const camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 2000);
+ camera.position.set(0, 60, 120);
+ camera.lookAt(0, 0, 0);
+ this._3dCamera = camera;
+
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
+ renderer.setPixelRatio(window.devicePixelRatio || 1);
+ renderer.setSize(W, H);
+ this._3dRenderer = renderer;
+
+ /* ambient + directional light */
+ scene.add(new THREE.AmbientLight(0xffffff, 0.45));
+ const dir = new THREE.DirectionalLight(0xffffff, 0.9);
+ dir.position.set(20, 40, 20);
+ scene.add(dir);
+
+ /* get height property */
+ const propDef = HEATMAP_PROPS.find(p => p.key === this._heatProp) || HEATMAP_PROPS[0];
+ const vals = ELEMENTS.map(el => propDef.get(el));
+ const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v) && v > 0);
+ const vMin = validVals.length ? Math.min(...validVals) : 0;
+ const vMax = validVals.length ? Math.max(...validVals) : 1;
+
+ const normV = v => (v !== null && v !== undefined && isFinite(v)) ? (v - vMin) / (vMax - vMin || 1) : 0;
+
+ const jet3 = t => {
+ const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 3)));
+ const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 2)));
+ const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 1)));
+ return new THREE.Color(r, g, b);
+ };
+
+ const CELL_W = 3.2, GAP = 0.4;
+ const STEP = CELL_W + GAP;
+ const mode = this._3dMode;
+ this._3dMeshes = [];
+
+ /* helper: text texture for cube face */
+ const makeTexture = (el, col) => {
+ const tc = document.createElement('canvas');
+ tc.width = 64; tc.height = 64;
+ const ctx2 = tc.getContext('2d');
+ ctx2.fillStyle = '#' + col.getHexString();
+ ctx2.fillRect(0, 0, 64, 64);
+ ctx2.fillStyle = '#fff';
+ ctx2.font = 'bold 22px sans-serif';
+ ctx2.textAlign = 'center';
+ ctx2.textBaseline = 'middle';
+ ctx2.fillText(el.symbol, 32, 32);
+ return new THREE.CanvasTexture(tc);
+ };
+
+ ELEMENTS.forEach(el => {
+ const pos = getCell(el);
+ if (!pos) return;
+
+ const nt = normV(propDef.get(el));
+ const height = Math.max(0.4, nt * 18);
+ const col = jet3(nt);
+
+ let gridRow = pos.row;
+ let gridCol = pos.col;
+
+ /* stack mode: f-block folded into main */
+ if (mode === 'stack' && pos.row > 7) {
+ if (pos.row === 9) { gridRow = 6; gridCol = pos.col; }
+ if (pos.row === 10) { gridRow = 7; gridCol = pos.col; }
+ }
+
+ const x = (gridCol - 9.5) * STEP;
+ const z = (gridRow - 4) * STEP;
+
+ let boxH = height;
+ if (mode === 'wave') {
+ /* smooth surface: average with neighbors */
+ const neighbors = ELEMENTS.filter(ne => {
+ const np = getCell(ne);
+ if (!np) return false;
+ return Math.abs(np.col - pos.col) <= 1 && Math.abs(np.row - pos.row) <= 1 && ne.Z !== el.Z;
+ });
+ const avgNt = neighbors.length
+ ? neighbors.reduce((s, ne) => s + normV(propDef.get(ne)), 0) / neighbors.length
+ : nt;
+ boxH = Math.max(0.4, ((nt + avgNt) / 2) * 18);
+ }
+
+ const geom = new THREE.BoxGeometry(CELL_W, boxH, CELL_W);
+ const tex = makeTexture(el, col);
+ const mats = [
+ new THREE.MeshLambertMaterial({ color: col }), // right
+ new THREE.MeshLambertMaterial({ color: col }), // left
+ new THREE.MeshLambertMaterial({ map: tex }), // top (symbol)
+ new THREE.MeshLambertMaterial({ color: col }), // bottom
+ new THREE.MeshLambertMaterial({ color: col }), // front
+ new THREE.MeshLambertMaterial({ color: col }), // back
+ ];
+ const mesh = new THREE.Mesh(geom, mats);
+ mesh.position.set(x, boxH / 2, z);
+ mesh.userData = { z: el.Z, origY: boxH / 2, nt };
+ scene.add(mesh);
+ this._3dMeshes.push(mesh);
+ });
+
+ /* ── orbit camera (simple drag) ── */
+ let isDragging = false, lastX = 0, lastY = 0;
+ let camTheta = 0.4, camPhi = Math.PI / 4, camRadius = 140;
+
+ const updateCam = () => {
+ camera.position.set(
+ camRadius * Math.sin(camPhi) * Math.sin(camTheta),
+ camRadius * Math.cos(camPhi),
+ camRadius * Math.sin(camPhi) * Math.cos(camTheta)
+ );
+ camera.lookAt(0, 0, 0);
+ };
+ updateCam();
+
+ canvas.addEventListener('mousedown', e => { isDragging = true; lastX = e.clientX; lastY = e.clientY; });
+ canvas.addEventListener('mousemove', e => {
+ if (!isDragging) return;
+ camTheta -= (e.clientX - lastX) * 0.008;
+ camPhi = Math.max(0.15, Math.min(Math.PI / 2, camPhi - (e.clientY - lastY) * 0.005));
+ lastX = e.clientX; lastY = e.clientY;
+ updateCam();
+ });
+ canvas.addEventListener('mouseup', () => { isDragging = false; });
+ canvas.addEventListener('wheel', e => {
+ camRadius = Math.max(30, Math.min(300, camRadius + e.deltaY * 0.3));
+ updateCam();
+ e.preventDefault();
+ }, { passive: false });
+
+ /* ── raycaster for hover/click ── */
+ const raycaster = new THREE.Raycaster();
+ const mouse = new THREE.Vector2();
+ let hoveredMesh = null;
+
+ /* 3D tooltip */
+ let tip3d = document.getElementById('ptbl-3d-tip');
+ if (!tip3d) {
+ tip3d = document.createElement('div');
+ tip3d.id = 'ptbl-3d-tip';
+ tip3d.style.cssText = 'position:fixed;display:none;background:rgba(10,10,30,0.92);border:1px solid rgba(155,93,229,0.5);color:#fff;font-size:.76rem;padding:5px 9px;border-radius:7px;pointer-events:none;z-index:999;';
+ document.body.appendChild(tip3d);
+ }
+
+ canvas.addEventListener('mousemove', e => {
+ const rect = canvas.getBoundingClientRect();
+ mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
+ raycaster.setFromCamera(mouse, camera);
+ const hits = raycaster.intersectObjects(self._3dMeshes);
+ if (hoveredMesh) { hoveredMesh.scale.set(1, 1, 1); hoveredMesh = null; }
+ if (hits.length) {
+ hoveredMesh = hits[0].object;
+ hoveredMesh.scale.set(1.08, 1.08, 1.08);
+ const elZ = hoveredMesh.userData.z;
+ const elObj = ELEMENTS.find(e2 => e2.Z === elZ);
+ if (elObj) {
+ const vl = propDef.get(elObj);
+ tip3d.innerHTML = `${elObj.symbol} — ${elObj.name} ${propDef.label}: ${vl !== null ? vl : '—'} ${propDef.unit}`;
+ tip3d.style.display = 'block';
+ tip3d.style.left = (e.clientX + 12) + 'px';
+ tip3d.style.top = (e.clientY - 10) + 'px';
+ }
+ } else {
+ tip3d.style.display = 'none';
+ }
+ });
+
+ canvas.addEventListener('click', e => {
+ const rect = canvas.getBoundingClientRect();
+ mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
+ raycaster.setFromCamera(mouse, camera);
+ const hits = raycaster.intersectObjects(self._3dMeshes);
+ if (hits.length) {
+ const elZ = hits[0].object.userData.z;
+ if (elZ) self._selectElement(elZ);
+ }
+ });
+
+ /* ── render loop ── */
+ const animate3d = () => {
+ if (!self._3dActive) return;
+ self._3dRaf = requestAnimationFrame(animate3d);
+ renderer.render(scene, camera);
+ };
+ animate3d();
+
+ /* handle resize */
+ const ro3d = new ResizeObserver(() => {
+ if (!self._3dActive || !self._3dRenderer) return;
+ const nW = canvas.offsetWidth, nH = canvas.offsetHeight;
+ if (nW > 0 && nH > 0) {
+ renderer.setSize(nW, nH);
+ camera.aspect = nW / nH;
+ camera.updateProjectionMatrix();
+ }
+ });
+ ro3d.observe(canvas);
+ this._3dResizeObs = ro3d;
+};
+
+/* ══════════════════════════════════════════════════════════════
+ 3. TABLE SHAPE MORPH (std / long / short)
+ ══════════════════════════════════════════════════════════════ */
+
+/* positions for LONG form (32-column, f-block inline) */
+function _longFormPos(el) {
+ if (el.block === 'f') {
+ /* lanthanides: period 6, cols 4-17; actinides: period 7, cols 4-17 */
+ const fPos = { 57:4,58:5,59:6,60:7,61:8,62:9,63:10,64:11,65:12,66:13,67:14,68:15,69:16,70:17,71:18,
+ 89:4,90:5,91:6,92:7,93:8,94:9,95:10,96:11,97:12,98:13,99:14,100:15,101:16,102:17,103:18 };
+ return { row: el.period, col: fPos[el.Z] || (el.Z - 54 + 4) };
+ }
+ /* s and d blocks shift right by 14 (f-block inserted) */
+ const stdPos = getCell(el);
+ if (!stdPos) return null;
+ if (el.block === 's') {
+ if (el.group === 1) return { row: stdPos.row, col: 1 };
+ if (el.group === 2) return { row: stdPos.row, col: 2 };
+ }
+ if (el.block === 'd' || (el.block === 'p')) {
+ return { row: stdPos.row, col: stdPos.col + 14 };
+ }
+ return { row: stdPos.row, col: stdPos.col + 14 };
+}
+
+/* positions for SHORT (8-group) form — classic Russian table */
+function _shortFormPos(el) {
+ /* main group elements: groups 1-8 */
+ const SHORT_MAP = {
+ 1:{1:1,3:1,11:1,19:1,37:1,55:1,87:1},
+ 2:{4:2,12:2,20:2,38:2,56:2,88:2},
+ };
+ const stdPos = getCell(el);
+ if (!stdPos) return null;
+ if (el.block === 'd') {
+ /* transition metals: group 3-12 → columns 3-8 with subgroup */
+ const subCol = ((el.group - 3) % 8) + 3;
+ return { row: stdPos.row, col: subCol };
+ }
+ if (el.block === 'f') {
+ /* lanthanides row 9, actinides row 10 — same as standard */
+ return getCell(el);
+ }
+ /* p and s blocks */
+ if (el.group) {
+ const shortGroup = el.group <= 2 ? el.group : (el.group - 10 <= 0 ? el.group - 10 + 8 : el.group - 10);
+ return { row: stdPos.row, col: Math.max(1, Math.min(8, el.group <= 2 ? el.group : el.group - 10)) };
+ }
+ return stdPos;
+}
+
+PeriodicTableSim.prototype._morphToView = function(shape) {
+ const self = this;
+ const DURATION = 800;
+
+ /* snapshot current pixel positions of each cell */
+ const gridRect = this._tableEl.getBoundingClientRect();
+
+ /* we animate by moving each cell div to absolute position during morph
+ then snap to new grid layout */
+
+ /* for std, we just rebuild; for long/short we switch grid and re-place */
+
+ if (shape === 'std') {
+ /* restore normal 18-col grid */
+ this._tableEl.style.gridTemplateColumns = 'repeat(18,1fr)';
+ if (this._fblockEl) this._fblockEl.style.display = '';
+ this._colorTable();
+ return;
+ }
+
+ if (shape === 'long') {
+ /* switch to 32-column grid, hide f-block row */
+ if (this._fblockEl) this._fblockEl.style.display = 'none';
+ this._tableEl.style.gridTemplateColumns = 'repeat(32,1fr)';
+
+ /* re-map cells — rebuild grid items */
+ this._tableEl.innerHTML = '';
+
+ const cells = {};
+ for (let r = 1; r <= 7; r++) {
+ for (let c = 1; c <= 32; c++) {
+ const d = document.createElement('div');
+ d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
+ cells[`${r},${c}`] = d;
+ this._tableEl.appendChild(d);
+ }
+ }
+
+ for (const el of ELEMENTS) {
+ const pos = _longFormPos(el);
+ if (!pos || pos.row > 7) continue;
+ const div = cells[`${pos.row},${pos.col}`];
+ if (!div) continue;
+ this._cellMap[el.Z] = div;
+ div.dataset.z = el.Z;
+ div.title = `${el.name} (${el.symbol})`;
+ div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;opacity:0;';
+ div.innerHTML = `
+ ${el.Z}
+ ${el.symbol}
+ ${el.name} `;
+ div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
+ div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
+ div.addEventListener('click', () => self._selectElement(el.Z));
+
+ /* fade-in staggered */
+ const delay = (pos.col + pos.row * 2) * 8;
+ setTimeout(() => {
+ if (window.LabFX && LabFX.motion) {
+ LabFX.motion.tween(0, 1, DURATION, 'easeInOutCubic', v => { div.style.opacity = v; });
+ } else {
+ div.style.opacity = '1';
+ }
+ }, delay);
+ }
+
+ this._colorTable();
+ return;
+ }
+
+ if (shape === 'short') {
+ /* 8-column grid */
+ if (this._fblockEl) this._fblockEl.style.display = '';
+ this._tableEl.style.gridTemplateColumns = 'repeat(8,1fr)';
+ this._tableEl.innerHTML = '';
+
+ const cells = {};
+ for (let r = 1; r <= 7; r++) {
+ for (let c = 1; c <= 8; c++) {
+ const d = document.createElement('div');
+ d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
+ cells[`${r},${c}`] = d;
+ this._tableEl.appendChild(d);
+ }
+ }
+
+ for (const el of ELEMENTS) {
+ if (el.block === 'f') continue; /* f-block stays in fblockEl */
+ const stdPos = getCell(el);
+ if (!stdPos) continue;
+ let col;
+ if (el.group <= 2) {
+ col = el.group;
+ } else if (el.group >= 13) {
+ col = el.group - 10;
+ } else {
+ /* d-block: groups 3-12 → compress to cols 3-8 with row offset tricks */
+ col = Math.min(8, ((el.group - 3) % 8) + 3);
+ }
+ col = Math.max(1, Math.min(8, col));
+ const div = cells[`${stdPos.row},${col}`] || cells[`${stdPos.row},1`];
+ if (!div || div.dataset.z) continue;
+ this._cellMap[el.Z] = div;
+ div.dataset.z = el.Z;
+ div.title = `${el.name} (${el.symbol})`;
+ div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;opacity:0;';
+ div.innerHTML = `
+ ${el.Z}
+ ${el.symbol}
+ ${el.name} `;
+ div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
+ div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
+ div.addEventListener('click', () => self._selectElement(el.Z));
+
+ const delay = (col + stdPos.row * 2) * 10;
+ setTimeout(() => {
+ if (window.LabFX && LabFX.motion) {
+ LabFX.motion.tween(0, 1, DURATION, 'easeInOutCubic', v => { div.style.opacity = v; });
+ } else {
+ div.style.opacity = '1';
+ }
+ }, delay);
+ }
+
+ this._colorTable();
+ }
+};
+
+/* ══════════════════════════════════════════════════════════════
+ 4. TREND ARROWS
+ ══════════════════════════════════════════════════════════════ */
+PeriodicTableSim.prototype._drawTrendArrows = function() {
+ const self = this;
+ const canvas = this._trendCanvas;
+ if (!canvas) return;
+
+ const parent = canvas.parentElement;
+ const W = parent.offsetWidth || 600;
+ const H = parent.offsetHeight || 300;
+ const dpr = window.devicePixelRatio || 1;
+ canvas.width = W * dpr;
+ canvas.height = H * dpr;
+ canvas.style.width = W + 'px';
+ canvas.style.height = H + 'px';
+ const ctx = canvas.getContext('2d');
+ ctx.scale(dpr, dpr);
+ ctx.clearRect(0, 0, W, H);
+
+ const cfg = TREND_CONFIG[this._trendProp] || TREND_CONFIG.radius;
+ const ARROW_W = 12;
+ const PADDING = 4;
+ const PAD_BOTTOM = 14;
+ const PAD_LEFT = 14;
+
+ /* draw arrow helper: from (x1,y1) to (x2,y2) with gradient and label */
+ const drawArrow = (x1, y1, x2, y2, color, label, labelSide) => {
+ const dx = x2 - x1, dy = y2 - y1;
+ const len = Math.sqrt(dx * dx + dy * dy);
+ if (len < 10) return;
+
+ const grad = ctx.createLinearGradient(x1, y1, x2, y2);
+ grad.addColorStop(0, color + '22');
+ grad.addColorStop(0.5, color + 'bb');
+ grad.addColorStop(1, color + 'ff');
+
+ /* shaft */
+ ctx.beginPath();
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ ctx.strokeStyle = grad;
+ ctx.lineWidth = ARROW_W;
+ ctx.lineCap = 'round';
+ ctx.stroke();
+
+ /* arrowhead */
+ const angle = Math.atan2(dy, dx);
+ const AH = 16;
+ ctx.beginPath();
+ ctx.moveTo(x2, y2);
+ ctx.lineTo(x2 - AH * Math.cos(angle - 0.45), y2 - AH * Math.sin(angle - 0.45));
+ ctx.lineTo(x2 - AH * Math.cos(angle + 0.45), y2 - AH * Math.sin(angle + 0.45));
+ ctx.closePath();
+ ctx.fillStyle = color;
+ ctx.fill();
+
+ /* label */
+ ctx.save();
+ ctx.font = 'bold 11px Manrope,sans-serif';
+ ctx.fillStyle = color;
+ ctx.shadowColor = 'rgba(0,0,0,0.8)';
+ ctx.shadowBlur = 4;
+ const mx = (x1 + x2) / 2;
+ const my = (y1 + y2) / 2;
+ if (labelSide === 'below') {
+ ctx.textAlign = 'center';
+ ctx.fillText(label, mx, my + 20);
+ } else if (labelSide === 'left') {
+ ctx.textAlign = 'right';
+ ctx.fillText(label, mx - 14, my);
+ }
+ ctx.restore();
+ };
+
+ const hCfg = cfg.hArrow;
+ const vCfg = cfg.vArrow;
+
+ /* horizontal arrow: bottom of table */
+ const yBottom = H - PAD_BOTTOM;
+ if (hCfg.dir === 'left') {
+ drawArrow(W - PADDING, yBottom, PADDING, yBottom, hCfg.color, hCfg.text, 'below');
+ } else {
+ drawArrow(PADDING, yBottom, W - PADDING, yBottom, hCfg.color, hCfg.text, 'below');
+ }
+
+ /* vertical arrow: left side of table */
+ const xLeft = PAD_LEFT;
+ if (vCfg.dir === 'down') {
+ drawArrow(xLeft, PADDING, xLeft, H - PAD_BOTTOM - 20, vCfg.color, vCfg.text, 'left');
+ } else {
+ drawArrow(xLeft, H - PAD_BOTTOM - 20, xLeft, PADDING, vCfg.color, vCfg.text, 'left');
+ }
+};
+
+/* ── patch stop() to clean up 3D ──────────────────────────── */
+const _origStop = PeriodicTableSim.prototype.stop;
+PeriodicTableSim.prototype.stop = function() {
+ _origStop.call(this);
+ if (this._3dRaf) { cancelAnimationFrame(this._3dRaf); this._3dRaf = null; }
+ if (this._3dRenderer) { this._3dRenderer.dispose(); this._3dRenderer = null; }
+ if (this._3dResizeObs) { this._3dResizeObs.disconnect(); }
+ const tip3d = document.getElementById('ptbl-3d-tip');
+ if (tip3d) tip3d.style.display = 'none';
+};
+
+/* ══════════════════════════════════════════════════════════════
+ WAVE D — ELECTRON-CONFIG DEEP TOOLS
+ _periodG_*: orbital filling, aufbau diagram,
+ quantum-number hover, Bohr excitation overlay
+ ══════════════════════════════════════════════════════════════ */
+
+/* ── Arrow helper (module-level, used by orbital filling) ────── */
+function _periodG_drawArrow(ctx, ax, ay1, ay2, col) {
+ ctx.beginPath(); ctx.moveTo(ax, ay1); ctx.lineTo(ax, ay2);
+ ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.stroke();
+ const dir = ay2 < ay1 ? -1 : 1;
+ ctx.beginPath();
+ ctx.moveTo(ax, ay2); ctx.lineTo(ax - 2.5, ay2 - dir * 4); ctx.lineTo(ax + 2.5, ay2 - dir * 4);
+ ctx.closePath(); ctx.fillStyle = col; ctx.fill();
+}
+
+/* ── Aufbau filling order ─────────────────────────────────────── */
+PeriodicTableSim.prototype._periodG_aufbauOrder = function() {
+ return [
+ {n:1,l:0,label:'1s',cap:2}, {n:2,l:0,label:'2s',cap:2},
+ {n:2,l:1,label:'2p',cap:6}, {n:3,l:0,label:'3s',cap:2},
+ {n:3,l:1,label:'3p',cap:6}, {n:4,l:0,label:'4s',cap:2},
+ {n:3,l:2,label:'3d',cap:10}, {n:4,l:1,label:'4p',cap:6},
+ {n:5,l:0,label:'5s',cap:2}, {n:4,l:2,label:'4d',cap:10},
+ {n:5,l:1,label:'5p',cap:6}, {n:6,l:0,label:'6s',cap:2},
+ {n:4,l:3,label:'4f',cap:14}, {n:5,l:2,label:'5d',cap:10},
+ {n:6,l:1,label:'6p',cap:6}, {n:7,l:0,label:'7s',cap:2},
+ {n:5,l:3,label:'5f',cap:14}, {n:6,l:2,label:'6d',cap:10},
+ {n:7,l:1,label:'7p',cap:6},
+ ];
+};
+
+PeriodicTableSim.prototype._periodG_mlLabels = function(l) {
+ if (l === 0) return [''];
+ if (l === 1) return ['px','py','pz'];
+ if (l === 2) return ['dxy','dxz','dyz','dx2y2','dz2'];
+ const out = [];
+ for (let m = -l; m <= l; m++) out.push('f' + (m >= 0 ? '+' : '') + m);
+ return out;
+};
+
+PeriodicTableSim.prototype._periodG_buildElectronList = function(Z) {
+ const order = this._periodG_aufbauOrder();
+ const electrons = [];
+ let rem = Z;
+ for (const sub of order) {
+ if (rem <= 0) break;
+ const mlList = this._periodG_mlLabels(sub.l);
+ const nOrb = mlList.length;
+ for (let pass = 0; pass < 2 && rem > 0; pass++) {
+ for (let oi = 0; oi < nOrb && rem > 0; oi++) {
+ electrons.push({
+ n: sub.n, l: sub.l, ml: oi - sub.l,
+ ms: pass === 0 ? 0.5 : -0.5,
+ subLabel: sub.label, orbIdx: oi, subRef: sub, mlLabel: mlList[oi],
+ });
+ rem--;
+ }
+ }
+ }
+ return electrons;
+};
+
+/* ══════════════════════════════════════════════════════════════
+ TOOL 1 — Orbital Filling Diagram
+ ══════════════════════════════════════════════════════════════ */
+PeriodicTableSim.prototype._periodG_drawOrbitalFilling = function(canvas, el) {
+ const electrons = this._periodG_buildElectronList(el.Z);
+ if (!electrons.length) return;
+ const subMap = new Map();
+ for (const e of electrons) {
+ if (!subMap.has(e.subLabel)) subMap.set(e.subLabel, []);
+ subMap.get(e.subLabel).push(e);
+ }
+ const order = this._periodG_aufbauOrder();
+ const subs = order.filter(s => subMap.has(s.label));
+ const BOX = 18, GAP = 3, LPAD = 28, VPAD = 5, ROW_H = BOX + VPAD * 2 + 8;
+ const W = canvas.offsetWidth || 240;
+ const H_needed = subs.length * ROW_H + 10;
+ const dpr = window.devicePixelRatio || 1;
+ canvas.width = W * dpr; canvas.height = H_needed * dpr;
+ canvas.style.height = H_needed + 'px';
+ const ctx = canvas.getContext('2d');
+ ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H_needed);
+ const hitMap = [];
+
+ subs.forEach((sub, si) => {
+ const y0 = si * ROW_H + VPAD;
+ const electronArr = subMap.get(sub.label);
+ const nOrb = 2 * sub.l + 1;
+ const mlLabels = this._periodG_mlLabels(sub.l);
+ const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#aaa';
+ ctx.font = 'bold 10px Manrope,sans-serif'; ctx.fillStyle = bColor;
+ ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
+ ctx.fillText(sub.label, 2, y0 + BOX / 2);
+ const isLastSub = si === subs.length - 1;
+ for (let oi = 0; oi < nOrb; oi++) {
+ const bx = LPAD + oi * (BOX + GAP), by = y0;
+ const inOrb = electronArr.filter(e => e.orbIdx === oi);
+ const isLastBox = isLastSub && oi === nOrb - 1 && inOrb.length > 0;
+ ctx.fillStyle = 'rgba(255,255,255,0.06)';
+ ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
+ ctx.beginPath();
+ if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
+ ctx.fill(); ctx.stroke();
+ if (nOrb > 1) {
+ ctx.font = '7px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.25)';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'top';
+ ctx.fillText(mlLabels[oi], bx + BOX / 2, by + BOX + 1);
+ }
+ inOrb.forEach((e, ei) => {
+ const isUp = e.ms > 0;
+ const ax = bx + (ei === 0 ? BOX * 0.35 : BOX * 0.65);
+ const ay_t = by + 3, ay_b = by + BOX - 3;
+ const col = isLastBox ? '#FFD166' : '#06D6E0';
+ if (isLastBox && window.LabFX)
+ LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 8 });
+ else
+ _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
+ hitMap.push({ x: ax - 6, y: by, w: 12, h: BOX, electron: e });
+ });
+ }
+ });
+ canvas._hitMap = hitMap; canvas._dpr = dpr;
+};
+
+/* ── Highlight variant (single electron lit, rest dimmed) ─────── */
+PeriodicTableSim.prototype._periodG_drawOrbFillingHL = function(canvas, el, hvEl) {
+ const electrons = this._periodG_buildElectronList(el.Z);
+ const subMap = new Map();
+ for (const e of electrons) {
+ if (!subMap.has(e.subLabel)) subMap.set(e.subLabel, []);
+ subMap.get(e.subLabel).push(e);
+ }
+ const order = this._periodG_aufbauOrder();
+ const subs = order.filter(s => subMap.has(s.label));
+ const BOX = 18, GAP = 3, LPAD = 28, VPAD = 5, ROW_H = BOX + VPAD * 2 + 8;
+ const W = canvas.offsetWidth || 240;
+ const H_needed = subs.length * ROW_H + 10;
+ const dpr = canvas._dpr || window.devicePixelRatio || 1;
+ const ctx = canvas.getContext('2d');
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H_needed);
+ subs.forEach((sub, si) => {
+ const y0 = si * ROW_H + VPAD;
+ const electronArr = subMap.get(sub.label);
+ const nOrb = 2 * sub.l + 1;
+ const mlLabels = this._periodG_mlLabels(sub.l);
+ const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#aaa';
+ ctx.font = 'bold 10px Manrope,sans-serif'; ctx.fillStyle = bColor;
+ ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(sub.label, 2, y0 + BOX / 2);
+ for (let oi = 0; oi < nOrb; oi++) {
+ const bx = LPAD + oi * (BOX + GAP), by = y0;
+ const inOrb = electronArr.filter(e => e.orbIdx === oi);
+ ctx.fillStyle = 'rgba(255,255,255,0.06)'; ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
+ ctx.beginPath();
+ if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
+ ctx.fill(); ctx.stroke();
+ if (nOrb > 1) {
+ ctx.font = '7px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.25)';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'top';
+ ctx.fillText(mlLabels[oi], bx + BOX / 2, by + BOX + 1);
+ }
+ inOrb.forEach((e, ei) => {
+ const isHover = e.n === hvEl.n && e.l === hvEl.l && e.ml === hvEl.ml && e.ms === hvEl.ms;
+ const isUp = e.ms > 0;
+ const ax = bx + (ei === 0 ? BOX * 0.35 : BOX * 0.65);
+ const ay_t = by + 3, ay_b = by + BOX - 3;
+ const col = isHover ? '#FFD166' : 'rgba(6,214,224,0.3)';
+ if (isHover && window.LabFX)
+ LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 10 });
+ else
+ _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
+ });
+ }
+ });
+};
+
+/* ══════════════════════════════════════════════════════════════
+ TOOL 3 — Quantum-number hover on orbital filling
+ ══════════════════════════════════════════════════════════════ */
+PeriodicTableSim.prototype._periodG_attachQNHover = function(canvas, el) {
+ if (canvas._periodG_mmH) {
+ canvas.removeEventListener('mousemove', canvas._periodG_mmH);
+ canvas.removeEventListener('mouseleave', canvas._periodG_mlH);
+ }
+ const tip = this._periodG_qTip;
+ canvas._periodG_mmH = (ev) => {
+ const rect = canvas.getBoundingClientRect();
+ const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
+ const hit = (canvas._hitMap || []).find(h => mx >= h.x && mx <= h.x + h.w && my >= h.y && my <= h.y + h.h);
+ if (hit) {
+ const e = hit.electron;
+ const lName = ['s','p','d','f'][e.l] || String(e.l);
+ if (tip) {
+ tip.innerHTML = `${e.subLabel} ${e.mlLabel} n = ${e.n} l = ${e.l} (${lName}) ml = ${e.ml} ms = ${e.ms > 0 ? '+1/2' : '-1/2'}`;
+ tip.style.display = 'block';
+ tip.style.left = (ev.clientX + 12) + 'px';
+ tip.style.top = (ev.clientY - 10) + 'px';
+ }
+ this._periodG_drawOrbFillingHL(canvas, el, hit.electron);
+ } else {
+ if (tip) tip.style.display = 'none';
+ this._periodG_drawOrbitalFilling(canvas, el);
+ }
+ };
+ canvas._periodG_mlH = () => { if (tip) tip.style.display = 'none'; this._periodG_drawOrbitalFilling(canvas, el); };
+ canvas.addEventListener('mousemove', canvas._periodG_mmH);
+ canvas.addEventListener('mouseleave', canvas._periodG_mlH);
+};
+
+/* ══════════════════════════════════════════════════════════════
+ TOOL 2 — Aufbau Diagram with Z slider
+ ══════════════════════════════════════════════════════════════ */
+PeriodicTableSim.prototype._periodG_drawAufbau = function(canvas, Z) {
+ const order = this._periodG_aufbauOrder();
+ const W = canvas.offsetWidth || 240;
+ const BOX = 14, GAP = 2, LPAD = 26, VPAD = 4, ROW_H = BOX + VPAD * 2;
+ const H_needed = order.length * ROW_H + 14;
+ const dpr = window.devicePixelRatio || 1;
+ canvas.width = W * dpr; canvas.height = H_needed * dpr;
+ canvas.style.height = H_needed + 'px';
+ const ctx = canvas.getContext('2d');
+ ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H_needed);
+ const electrons = this._periodG_buildElectronList(Z);
+ const subCount = new Map();
+ for (const e of electrons) subCount.set(e.subLabel, (subCount.get(e.subLabel) || 0) + 1);
+ let lastRow = -1;
+ order.forEach((sub, si) => { if ((subCount.get(sub.label) || 0) > 0) lastRow = si; });
+ order.forEach((sub, si) => {
+ const y0 = si * ROW_H + VPAD;
+ const nOrb = 2 * sub.l + 1;
+ const filled = subCount.get(sub.label) || 0;
+ const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#888';
+ ctx.font = 'bold 9px Manrope,sans-serif';
+ ctx.fillStyle = si <= lastRow ? bColor : 'rgba(255,255,255,0.2)';
+ ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(sub.label, 2, y0 + BOX / 2);
+ for (let oi = 0; oi < nOrb; oi++) {
+ const bx = LPAD + oi * (BOX + GAP), by = y0;
+ ctx.fillStyle = 'rgba(255,255,255,0.04)';
+ ctx.strokeStyle = si <= lastRow ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.06)';
+ ctx.lineWidth = 1; ctx.beginPath();
+ if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
+ ctx.fill(); ctx.stroke();
+ }
+ let rem = filled;
+ const spins = Array(nOrb).fill(0);
+ for (let oi = 0; oi < nOrb && rem > 0; oi++) { spins[oi]++; rem--; }
+ for (let oi = 0; oi < nOrb && rem > 0; oi++) { spins[oi]++; rem--; }
+ for (let oi = 0; oi < nOrb; oi++) {
+ const bx = LPAD + oi * (BOX + GAP), by = y0;
+ const isLast = si === lastRow && oi === nOrb - 1 && spins[oi] > 0;
+ for (let sp = 0; sp < spins[oi]; sp++) {
+ const isUp = sp === 0;
+ const ax = bx + (sp === 0 ? BOX * 0.35 : BOX * 0.65);
+ const ay_t = by + 2, ay_b = by + BOX - 2;
+ const col = isLast ? '#FFD166' : bColor;
+ if (isLast && window.LabFX)
+ LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 6 });
+ else
+ _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
+ }
+ }
+ if (si === lastRow && filled < sub.cap) {
+ const nx = LPAD + nOrb * (BOX + GAP) + 2, ny = y0 + BOX / 2;
+ ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 1;
+ ctx.beginPath(); ctx.moveTo(nx, ny); ctx.lineTo(nx + 7, ny); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(nx+7,ny); ctx.lineTo(nx+4,ny-3); ctx.lineTo(nx+4,ny+3);
+ ctx.fillStyle = '#9B5DE5'; ctx.fill();
+ }
+ });
+ const now = performance.now();
+ if (this._periodG_lastAufbauZ !== Z && window.LabFX) {
+ if (!this._periodG_aufbauSoundTs || now - this._periodG_aufbauSoundTs > 80) {
+ LabFX.sound.play('tick', { pitch: 0.8 + Z * 0.01, volume: 0.08 });
+ this._periodG_aufbauSoundTs = now;
+ }
+ }
+ this._periodG_lastAufbauZ = Z;
+};
+
+/* ══════════════════════════════════════════════════════════════
+ Tab injection — patches _updateCard to add «Орбитали»/«Aufbau»
+ ══════════════════════════════════════════════════════════════ */
+(function() {
+ const _orig = PeriodicTableSim.prototype._updateCard;
+ PeriodicTableSim.prototype._updateCard = function(el) {
+ if (this._periodG_cleanupQTip) { this._periodG_cleanupQTip(); this._periodG_cleanupQTip = null; }
+ _orig.call(this, el);
+ if (!el) return;
+ this._periodG_addElecTab(el);
+ };
+})();
+
+PeriodicTableSim.prototype._periodG_addElecTab = function(el) {
+ const card = this._cardEl;
+ const tabBar = document.createElement('div');
+ tabBar.style.cssText = 'display:flex;gap:0;border-bottom:1px solid rgba(255,255,255,0.1);margin:8px -10px 0;';
+ const btns = ['Орбитали','Aufbau'].map((lbl) => {
+ const btn = document.createElement('button');
+ btn.textContent = lbl;
+ btn.style.cssText = 'flex:1;padding:5px 0;border:none;background:transparent;color:rgba(255,255,255,0.4);font-size:.7rem;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;';
+ tabBar.appendChild(btn); return btn;
+ });
+ card.appendChild(tabBar);
+ const wrap = document.createElement('div'); wrap.style.cssText = 'position:relative;';
+ card.appendChild(wrap);
+
+ // Panel 0: orbital filling canvas
+ const orbPan = document.createElement('div'); orbPan.style.cssText = 'display:none;padding:4px 0 0;';
+ const orbCv = document.createElement('canvas'); orbCv.style.cssText = 'width:100%;display:block;cursor:crosshair;';
+ orbPan.appendChild(orbCv); wrap.appendChild(orbPan);
+
+ // Panel 1: Aufbau canvas + Z slider
+ const aufPan = document.createElement('div'); aufPan.style.cssText = 'display:none;padding:4px 0 0;';
+ const aufCv = document.createElement('canvas'); aufCv.style.cssText = 'width:100%;display:block;';
+ aufPan.appendChild(aufCv);
+ const sw = document.createElement('div'); sw.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 0;';
+ const sld = document.createElement('input'); sld.type='range'; sld.min=1; sld.max=118; sld.value=el.Z;
+ sld.style.cssText = 'flex:1;accent-color:#9B5DE5;';
+ const slLbl = document.createElement('span'); slLbl.style.cssText = 'font-size:.68rem;color:#9B5DE5;min-width:32px;text-align:right;';
+ slLbl.textContent = 'Z=' + el.Z;
+ sw.appendChild(sld); sw.appendChild(slLbl); aufPan.appendChild(sw); wrap.appendChild(aufPan);
+
+ // quantum number tooltip (fixed positioned, cleaned up on new element)
+ const qTip = document.createElement('div');
+ qTip.style.cssText = 'position:fixed;display:none;padding:5px 9px;background:#1a1a2e;border:1px solid rgba(155,93,229,0.5);border-radius:6px;font-size:.68rem;color:#ccc;pointer-events:none;z-index:9999;line-height:1.65;';
+ document.body.appendChild(qTip);
+ this._periodG_qTip = qTip;
+ this._periodG_cleanupQTip = () => {
+ qTip.style.display = 'none';
+ if (qTip.parentNode) qTip.parentNode.removeChild(qTip);
+ this._periodG_qTip = null;
+ };
+
+ const panels = [orbPan, aufPan];
+ const showTab = (idx) => {
+ btns.forEach((b, i) => {
+ b.style.color = i === idx ? '#fff' : 'rgba(255,255,255,0.4)';
+ b.style.borderBottomColor = i === idx ? '#9B5DE5' : 'transparent';
+ });
+ panels.forEach((p, i) => p.style.display = i === idx ? 'block' : 'none');
+ if (idx === 0) setTimeout(() => { this._periodG_drawOrbitalFilling(orbCv, el); this._periodG_attachQNHover(orbCv, el); }, 0);
+ if (idx === 1) setTimeout(() => this._periodG_drawAufbau(aufCv, +sld.value), 0);
+ };
+ btns.forEach((b, i) => b.addEventListener('click', () => showTab(i)));
+ sld.addEventListener('input', () => { const z = +sld.value; slLbl.textContent = 'Z='+z; this._periodG_drawAufbau(aufCv, z); });
+ showTab(0);
+};
+
+/* ══════════════════════════════════════════════════════════════
+ TOOL 4 — Bohr excitation (patches _drawBohr)
+ ══════════════════════════════════════════════════════════════ */
+PeriodicTableSim.prototype._periodG_wavelengthToRGB = function(nm) {
+ let R=0,G=0,B=0,a=1;
+ if(nm>=380&&nm<440){R=-(nm-440)/(440-380);G=0;B=1;}
+ else if(nm<490){R=0;G=(nm-440)/(490-440);B=1;}
+ else if(nm<510){R=0;G=1;B=-(nm-510)/(510-490);}
+ else if(nm<580){R=(nm-510)/(580-510);G=1;B=0;}
+ else if(nm<645){R=1;G=-(nm-645)/(645-580);B=0;}
+ else if(nm<781){R=1;G=0;B=0;}
+ if(nm>=700)a=0.3+0.7*(780-nm)/(780-700);
+ else if(nm<420)a=0.3+0.7*(nm-380)/(420-380);
+ return `rgba(${Math.round(R*255*a)},${Math.round(G*255*a)},${Math.round(B*255*a)},1)`;
+};
+
+PeriodicTableSim.prototype._periodG_initBohrExcite = function() {
+ const canvas = this._bohrCanvas;
+ if (canvas._periodG_excite_bound) return;
+ canvas._periodG_excite_bound = true;
+ this._periodG_excState = null;
+ canvas.title = 'Клик на электрон — возбуждение';
+ canvas.addEventListener('click', (ev) => {
+ if (!this._bohrZ) return;
+ const el = ELEMENTS.find(e => e.Z === this._bohrZ); if (!el) return;
+ const shells = getShellFill(el.Z);
+ const rect = canvas.getBoundingClientRect();
+ const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
+ const W = canvas.offsetWidth || 240, H = canvas.offsetHeight || 150;
+ const cx = W/2, cy = H/2, maxR = Math.min(W,H)*0.44, nShells = shells.length;
+ let clickedShell = -1;
+ outer: for (let i = 0; i < nShells; i++) {
+ const r = maxR*(i+1)/nShells, speed = 1-i*0.12;
+ for (let e2 = 0; e2 < shells[i]; e2++) {
+ const a = this._bohrAngle*speed + (2*Math.PI*e2)/shells[i];
+ const dx = mx-(cx+r*Math.cos(a)), dy = my-(cy+r*Math.sin(a));
+ if (Math.sqrt(dx*dx+dy*dy) < 9) { clickedShell = i; break outer; }
+ }
+ }
+ if (clickedShell < 0) {
+ for (let i = 0; i < nShells; i++) {
+ const r = maxR*(i+1)/nShells;
+ const dist = Math.sqrt((mx-cx)**2+(my-cy)**2);
+ if (Math.abs(dist-r) < 8 && shells[i] > 0) { clickedShell = i; break; }
+ }
+ }
+ if (clickedShell < 0) return;
+ this._periodG_showExciteMenu(el, shells, clickedShell, ev.clientX, ev.clientY);
+ });
+};
+
+PeriodicTableSim.prototype._periodG_showExciteMenu = function(el, shells, n1idx, px, py) {
+ const old = document.getElementById('periodG-excite-menu'); if (old) old.remove();
+ const menu = document.createElement('div');
+ menu.id = 'periodG-excite-menu';
+ menu.style.cssText = `position:fixed;left:${px+6}px;top:${py}px;background:#12122a;border:1px solid rgba(155,93,229,0.55);border-radius:8px;padding:8px;z-index:9999;font-size:.71rem;color:#ccc;min-width:175px;box-shadow:0 4px 18px rgba(0,0,0,0.5);`;
+ menu.innerHTML = `Переход из n=${n1idx+1}:
`;
+ for (let i = 0; i < shells.length; i++) {
+ if (i === n1idx) continue;
+ const n = i+1, n1 = n1idx+1;
+ const dE = 13.6 * (1/(n1*n1) - 1/(n*n));
+ const dE_abs = Math.abs(dE);
+ const lam = dE_abs > 0.02 ? Math.round(1240/dE_abs) : 99999;
+ const region = lam < 380 ? 'УФ' : lam > 780 ? 'ИК' : 'видим.';
+ const abs = dE > 0;
+ const btn = document.createElement('button');
+ btn.style.cssText = 'display:block;width:100%;text-align:left;padding:3px 7px;border:none;background:transparent;color:#ccc;cursor:pointer;border-radius:4px;font-size:.7rem;';
+ btn.innerHTML = `n=${n} → ${lam < 99999 ? lam+'нм ('+region+')' : '—'} ${abs ? '[+ф]' : '[-ф]'}`;
+ btn.addEventListener('mouseenter', () => btn.style.background = 'rgba(155,93,229,0.2)');
+ btn.addEventListener('mouseleave', () => btn.style.background = 'transparent');
+ btn.addEventListener('click', () => { menu.remove(); this._periodG_startExcitation(el, n1idx, i, lam); });
+ menu.appendChild(btn);
+ }
+ const cb = document.createElement('button');
+ cb.style.cssText = 'display:block;width:100%;text-align:center;padding:2px 0;border:none;background:transparent;color:rgba(255,255,255,0.3);cursor:pointer;font-size:.68rem;margin-top:5px;';
+ cb.textContent = 'Отмена'; cb.addEventListener('click', () => menu.remove());
+ menu.appendChild(cb); document.body.appendChild(menu);
+ const outside = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', outside); } };
+ setTimeout(() => document.addEventListener('click', outside), 60);
+};
+
+PeriodicTableSim.prototype._periodG_startExcitation = function(el, n1idx, n2idx, lam) {
+ this._periodG_excState = { n1: n1idx, n2: n2idx, lam, phase: 'up', t: performance.now() };
+ if (window.LabFX) LabFX.sound.play('chime', { pitch: n2idx > n1idx ? 1.3 : 0.7, volume: 0.3 });
+};
+
+/* patch _drawBohr to incorporate excitation physics */
+(function() {
+ const _orig = PeriodicTableSim.prototype._drawBohr;
+ PeriodicTableSim.prototype._drawBohr = function() {
+ if (!this._bohrCanvas._periodG_excite_bound) this._periodG_initBohrExcite();
+ const canvas = this._bohrCanvas;
+ const dpr = window.devicePixelRatio || 1;
+ const W = canvas.offsetWidth || 240, H = canvas.offsetHeight || 150;
+ canvas.width = W*dpr; canvas.height = H*dpr;
+ const ctx = canvas.getContext('2d');
+ ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H);
+ if (!this._bohrZ) return;
+ const el = ELEMENTS.find(e => e.Z === this._bohrZ); if (!el) return;
+ const shells = getShellFill(el.Z);
+ const cx = W/2, cy = H/2, maxR = Math.min(W,H)*0.44, nShells = shells.length;
+ const col = TYPE_COLORS[el.type] || '#7B8EF7';
+ const exc = this._periodG_excState;
+ const now = performance.now();
+ if (exc) {
+ const el2 = now - exc.t;
+ if (exc.phase === 'up' && el2 > 600) { exc.phase = 'stay'; exc.t = now; }
+ else if (exc.phase === 'stay' && el2 > 800) { exc.phase = 'down'; exc.t = now; }
+ else if (exc.phase === 'down' && el2 > 600) { exc.phase = 'done'; }
+ }
+ ctx.beginPath(); ctx.arc(cx, cy, nShells > 0 ? 5+nShells*1.5 : 6, 0, Math.PI*2);
+ ctx.fillStyle = col; ctx.fill();
+ shells.forEach((count, i) => {
+ const r = maxR*(i+1)/nShells;
+ ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2);
+ ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 1; ctx.stroke();
+ const speed = 1-i*0.12;
+ for (let e2 = 0; e2 < count; e2++) {
+ const a = this._bohrAngle*speed + (2*Math.PI*e2)/count;
+ let drawR = r;
+ const isExc = exc && exc.phase !== 'done' && i === exc.n1 && e2 === 0;
+ if (isExc) {
+ const r2 = maxR*(exc.n2+1)/nShells;
+ const elapsed2 = now - exc.t;
+ let prog = 0;
+ if (exc.phase === 'up') prog = Math.min(elapsed2/600, 1);
+ else if (exc.phase === 'stay') prog = 1;
+ else if (exc.phase === 'down') prog = 1 - Math.min(elapsed2/600, 1);
+ drawR = r + (r2-r)*prog;
+ if (exc.phase === 'up') {
+ const pf = Math.min(elapsed2/600, 1);
+ const pm = r + (r2-r)*pf*0.5, pa = a + Math.PI/2;
+ const px2 = cx+pm*Math.cos(pa), py2 = cy+pm*Math.sin(pa);
+ const lamNm = exc.lam;
+ const pcol = (lamNm>=380&&lamNm<=780) ? this._periodG_wavelengthToRGB(lamNm) : (lamNm<380 ? '#cc88ff' : '#ffaaaa');
+ if (window.LabFX) {
+ LabFX.glow.drawGlow(ctx, () => { ctx.beginPath(); ctx.arc(px2,py2,4,0,Math.PI*2); ctx.fillStyle=pcol; ctx.fill(); }, { color: pcol, intensity: 12 });
+ } else { ctx.beginPath(); ctx.arc(px2,py2,4,0,Math.PI*2); ctx.fillStyle=pcol; ctx.fill(); }
+ ctx.font='8px Manrope,sans-serif'; ctx.fillStyle=pcol; ctx.textAlign='center';
+ ctx.fillText((exc.lam<99999?exc.lam+'нм':'?')+(exc.lam<380?' УФ':exc.lam>780?' ИК':''), cx, H-4);
+ }
+ }
+ const ex2 = cx+drawR*Math.cos(a), ey2 = cy+drawR*Math.sin(a);
+ if (isExc && window.LabFX) {
+ LabFX.glow.drawGlow(ctx, () => { ctx.beginPath(); ctx.arc(ex2,ey2,3.5,0,Math.PI*2); ctx.fillStyle='#FFD166'; ctx.fill(); }, { color:'#FFD166', intensity:10 });
+ } else { ctx.beginPath(); ctx.arc(ex2,ey2,2.5,0,Math.PI*2); ctx.fillStyle='#06D6E0'; ctx.fill(); }
+ }
+ });
+ if (!exc || exc.phase === 'done') {
+ if (exc && exc.phase === 'done') {
+ this._periodG_excState = null;
+ if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.9, volume: 0.18 });
+ }
+ ctx.font = '700 10px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)';
+ ctx.textAlign = 'center'; ctx.fillText(shells.join(','), cx, H-4);
+ }
+ ctx.font = '7.5px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.2)';
+ ctx.textAlign = 'center'; ctx.fillText('клик -> возбуждение', cx, 10);
+ };
+})();
+
+/* patch stop() to clean up quantum tooltip (Wave D) */
+(function() {
+ const _origStopD = PeriodicTableSim.prototype.stop;
+ PeriodicTableSim.prototype.stop = function() {
+ _origStopD.call(this);
+ if (this._periodG_cleanupQTip) { this._periodG_cleanupQTip(); this._periodG_cleanupQTip = null; }
+ };
+})();
+
/* ── global opener ─────────────────────────────────────────── */
+
+/* ══════════════════════════════════════════════════════════════
+ WAVE C — INTERACTIVE LEARNING MODES
+ _buildInteractiveModeBar, _switchInteractiveMode,
+ _modeBinary, _binaryClick, _binaryCalc,
+ _modeCompare, _compareClick, _compareRefresh, _compareDraw,
+ _modeActivity,
+ _modeMendeleev1869, _m1869ShowPrediction,
+ _modeTimeline, _timelineUpdate
+ ══════════════════════════════════════════════════════════════ */
+
+/* patch _buildVisualModes to also init interactive bar */
+(function() {
+ var _prevBVM = PeriodicTableSim.prototype._buildVisualModes;
+ PeriodicTableSim.prototype._buildVisualModes = function() {
+ if (_prevBVM) _prevBVM.call(this);
+ this._buildInteractiveModeBar();
+ };
+})();
+
+/* patch stop() to clean up timeline RAF */
+(function() {
+ var _prevSC = PeriodicTableSim.prototype.stop;
+ PeriodicTableSim.prototype.stop = function() {
+ if (_prevSC) _prevSC.call(this);
+ if (this._iModeState && this._iModeState.raf) {
+ cancelAnimationFrame(this._iModeState.raf);
+ this._iModeState.raf = null;
+ }
+ };
+})();
+
+PeriodicTableSim.prototype._buildInteractiveModeBar = function() {
+ this._interactiveMode = null;
+ this._iModePanel = null;
+ this._iModeState = {};
+
+ var bar = document.createElement('div');
+ bar.className = 'ptbl-imode-bar';
+ bar.style.cssText = 'display:flex;align-items:center;gap:5px;padding:5px 12px;background:rgba(0,0,0,0.16);border-bottom:1px solid rgba(255,255,255,0.05);flex-wrap:wrap;flex-shrink:0;';
+
+ var lbl = document.createElement('span');
+ lbl.style.cssText = 'font-size:.67rem;font-weight:700;color:rgba(255,255,255,0.28);text-transform:uppercase;letter-spacing:.07em;';
+ lbl.textContent = 'Интерактив:';
+ bar.appendChild(lbl);
+
+ var MODES = [
+ { id: null, text: 'Стандартный' },
+ { id: 'binary', text: 'Бинарные соединения' },
+ { id: 'compare', text: 'Сравнить' },
+ { id: 'activity', text: 'Ряд активности' },
+ { id: 'mendeleev', text: 'Таблица 1869' },
+ { id: 'timeline', text: 'Таймлайн' },
+ ];
+ var BASE_S = 'padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:#777;font-size:.69rem;cursor:pointer;transition:all .15s;';
+ var ACT_S = 'background:rgba(6,214,224,0.16);color:#06D6E0;border-color:rgba(6,214,224,0.32);';
+
+ var self = this;
+ MODES.forEach(function(m) {
+ var btn = document.createElement('button');
+ btn.textContent = m.text;
+ btn.style.cssText = BASE_S + (m.id === null ? ACT_S : '');
+ btn.addEventListener('click', function() {
+ bar.querySelectorAll('button').forEach(function(b) { b.style.cssText = BASE_S; });
+ btn.style.cssText = BASE_S + ACT_S;
+ self._switchInteractiveMode(m.id);
+ });
+ bar.appendChild(btn);
+ });
+
+ var vbar = this._wrap.querySelector('#ptbl-vmodes-bar');
+ var anchor = vbar ? vbar.nextSibling : (this._wrap.children[1] ? this._wrap.children[1].nextSibling : null);
+ this._wrap.insertBefore(bar, anchor);
+ this._iModeBar = bar;
+};
+
+PeriodicTableSim.prototype._switchInteractiveMode = function(modeId) {
+ if (this._iModePanel && this._iModePanel.parentNode) {
+ this._iModePanel.parentNode.removeChild(this._iModePanel);
+ this._iModePanel = null;
+ }
+ if (this._iModeState && this._iModeState.raf) {
+ cancelAnimationFrame(this._iModeState.raf);
+ }
+ this._interactiveMode = modeId;
+ this._iModeState = {};
+
+ var self = this;
+ for (var i = 0; i < ELEMENTS.length; i++) {
+ var el = ELEMENTS[i];
+ var div = this._cellMap[el.Z];
+ if (!div) continue;
+ var clone = div.cloneNode(true);
+ (function(c, z) {
+ c.addEventListener('mouseenter', function() { c.style.filter = 'brightness(1.4)'; c.style.transform = 'scale(1.12)'; c.style.zIndex = '10'; });
+ c.addEventListener('mouseleave', function() { c.style.filter = ''; c.style.transform = ''; c.style.zIndex = ''; });
+ c.addEventListener('click', function() { self._selectElement(z); });
+ })(clone, el.Z);
+ div.parentNode.replaceChild(clone, div);
+ this._cellMap[el.Z] = clone;
+ }
+ this._colorTable();
+
+ if (!modeId) return;
+ if (modeId === 'binary') this._modeBinary();
+ if (modeId === 'compare') this._modeCompare();
+ if (modeId === 'activity') this._modeActivity();
+ if (modeId === 'mendeleev') this._modeMendeleev1869();
+ if (modeId === 'timeline') this._modeTimeline();
+};
+
+/* ── MODE 1: BINARY COMPOUNDS ── */
+PeriodicTableSim.prototype._modeBinary = function() {
+ this._iModeState = { first: null };
+ var panel = document.createElement('div');
+ panel.className = 'ptbl-imode-panel';
+ panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:9px 14px;flex-shrink:0;font-size:.75rem;color:#ccc;min-height:78px;';
+ panel.innerHTML = 'Кликните первый элемент (реагент A)
' +
+ '
';
+ this._wrap.appendChild(panel);
+ this._iModePanel = panel;
+ var self = this;
+ for (var i = 0; i < ELEMENTS.length; i++) {
+ var div = this._cellMap[ELEMENTS[i].Z];
+ if (!div) continue;
+ (function(z) { div.addEventListener('click', function() { self._binaryClick(z); }); })(ELEMENTS[i].Z);
+ }
+};
+
+PeriodicTableSim.prototype._binaryClick = function(Z) {
+ var st = this._iModeState;
+ var panel = this._iModePanel;
+ if (!panel) return;
+ var hint = panel.querySelector('.ptbl-bin-hint');
+ var res = panel.querySelector('.ptbl-bin-result');
+ var self = this;
+
+ if (!st.first) {
+ st.first = Z;
+ if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.0, volume: 0.2 });
+ ELEMENTS.forEach(function(el) {
+ var d = self._cellMap[el.Z]; if (!d) return;
+ d.style.outline = el.Z === Z ? '2px solid #FFD166' : '';
+ d.style.outlineOffset = el.Z === Z ? '1px' : '';
+ });
+ var elA = ELEMENTS.find(function(e) { return e.Z === Z; });
+ hint.textContent = 'Выбран: ' + elA.symbol + ' (' + elA.name + '). Кликните второй элемент.';
+ res.innerHTML = '';
+ } else if (st.first === Z) {
+ st.first = null;
+ ELEMENTS.forEach(function(el) { var d = self._cellMap[el.Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; } });
+ hint.textContent = 'Кликните первый элемент (реагент A)';
+ res.innerHTML = '';
+ } else {
+ if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.3, volume: 0.3 });
+ var elA2 = ELEMENTS.find(function(e) { return e.Z === st.first; });
+ var elB = ELEMENTS.find(function(e) { return e.Z === Z; });
+ ELEMENTS.forEach(function(el) {
+ var d = self._cellMap[el.Z]; if (!d) return;
+ if (el.Z === st.first) { d.style.outline = '2px solid #FFD166'; d.style.outlineOffset = '1px'; }
+ else if (el.Z === Z) { d.style.outline = '2px solid #06D6E0'; d.style.outlineOffset = '1px'; }
+ else { d.style.outline = ''; d.style.outlineOffset = ''; }
+ });
+ hint.textContent = elA2.symbol + ' + ' + elB.symbol;
+ res.innerHTML = self._binaryCalc(elA2, elB);
+ st.first = null;
+ }
+};
+
+PeriodicTableSim.prototype._binaryCalc = function(elA, elB) {
+ function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
+ var oxA = (elA.oxStates || []).filter(function(s) { return s !== null && s !== 0; });
+ var oxB = (elB.oxStates || []).filter(function(s) { return s !== null && s !== 0; });
+ var enA = elA.En || 0, enB = elB.En || 0;
+ var dEN = Math.abs(enA - enB);
+ var bondType, bondColor;
+ if (dEN > 1.7) { bondType = 'ионная'; bondColor = '#FF6B35'; }
+ else if (dEN >= 0.4) { bondType = 'ковалентная полярная'; bondColor = '#7B8EF7'; }
+ else { bondType = 'ковалентная неполярная'; bondColor = '#7BF5A4'; }
+
+ var seen = {};
+ var formulas = [];
+ var srcA = oxA.length ? oxA : [0];
+ var srcB = oxB.length ? oxB : [0];
+ for (var ai = 0; ai < srcA.length; ai++) {
+ for (var bi = 0; bi < srcB.length; bi++) {
+ var vA = srcA[ai], vB = srcB[bi];
+ if (vA === 0 || vB === 0) continue;
+ if ((vA > 0 && vB > 0) || (vA < 0 && vB < 0)) continue;
+ var posEl = vA > 0 ? elA : elB;
+ var negEl = vA > 0 ? elB : elA;
+ var posV = Math.abs(vA > 0 ? vA : vB);
+ var negV = Math.abs(vA > 0 ? vB : vA);
+ var g = gcd(posV, negV);
+ var nPos = negV / g, nNeg = posV / g;
+ var key = posEl.symbol + nPos + negEl.symbol + nNeg;
+ if (seen[key]) continue;
+ seen[key] = true;
+ var formula = posEl.symbol + (nPos > 1 ? '' + nPos + ' ' : '') +
+ negEl.symbol + (nNeg > 1 ? '' + nNeg + ' ' : '');
+ var struct = (nPos === 1 && nNeg === 1) ? posEl.symbol + '–' + negEl.symbol :
+ (nPos === 1 && nNeg === 2) ? negEl.symbol + '–' + posEl.symbol + '–' + negEl.symbol :
+ formula;
+ formulas.push({ formula: formula, struct: struct });
+ }
+ }
+ if (!formulas.length) {
+ return 'Соединение не образуется (одинаковые знаки ст. окисления) ';
+ }
+ var html = '' +
+ 'Тип связи: ' +
+ '' + bondType + ' ';
+ if (elA.En && elB.En) html += 'ΔЭО = ' + dEN.toFixed(2) + ' ';
+ html += '
';
+ formulas.forEach(function(f) {
+ html += '
' +
+ '
' + f.formula + '
' +
+ '
' + f.struct + '
';
+ });
+ html += '
';
+ return html;
+};
+
+/* ── MODE 2: COMPARE ELEMENTS ── */
+PeriodicTableSim.prototype._modeCompare = function() {
+ this._iModeState = { selected: [] };
+ var panel = document.createElement('div');
+ panel.className = 'ptbl-imode-panel';
+ panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;overflow-x:auto;';
+ panel.innerHTML = '' +
+ 'Выберите до 4 элементов ' +
+ 'Очистить ' +
+ 'График: ' +
+ '' +
+ 'ЭО Масса ' +
+ 'Плотность T пл. T кип. ' +
+ '
' +
+ '
' +
+ ' ';
+ this._wrap.appendChild(panel);
+ this._iModePanel = panel;
+ var self = this;
+ panel.querySelector('.ptbl-cmp-clear').addEventListener('click', function() {
+ self._iModeState.selected = [];
+ ELEMENTS.forEach(function(el) { var d = self._cellMap[el.Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; } });
+ self._compareRefresh();
+ });
+ panel.querySelector('.ptbl-cmp-prop').addEventListener('change', function() { self._compareRefresh(); });
+ ELEMENTS.forEach(function(el) {
+ var div = self._cellMap[el.Z]; if (!div) return;
+ (function(z) { div.addEventListener('click', function() { self._compareClick(z); }); })(el.Z);
+ });
+ this._compareRefresh();
+};
+
+PeriodicTableSim.prototype._compareClick = function(Z) {
+ var st = this._iModeState;
+ var idx = st.selected.indexOf(Z);
+ if (idx >= 0) {
+ st.selected.splice(idx, 1);
+ var d = this._cellMap[Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; }
+ } else {
+ if (st.selected.length >= 4) return;
+ st.selected.push(Z);
+ if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.85 + st.selected.length * 0.1, volume: 0.2 });
+ var d2 = this._cellMap[Z];
+ if (d2) { d2.style.outline = '2px solid #06D6E0'; d2.style.outlineOffset = '1px'; }
+ }
+ this._compareRefresh();
+};
+
+PeriodicTableSim.prototype._compareRefresh = function() {
+ var panel = this._iModePanel; if (!panel) return;
+ var st = this._iModeState;
+ var propKey = panel.querySelector('.ptbl-cmp-prop').value;
+ var els = st.selected.map(function(z) { return ELEMENTS.find(function(e) { return e.Z === z; }); }).filter(Boolean);
+ var tbl = panel.querySelector('.ptbl-cmp-table');
+ if (!els.length) {
+ tbl.innerHTML = 'Кликайте элементы на таблице
';
+ this._compareDraw(panel.querySelector('.ptbl-cmp-chart'), els, propKey);
+ return;
+ }
+ var PROPS = [
+ { key:'Z', label:'Z' }, { key:'mass', label:'Масса' },
+ { key:'config', label:'Конфиг.' }, { key:'En', label:'ЭО (Полинг)' },
+ { key:'density', label:'Плотн.' }, { key:'melt', label:'T пл. (K)' }, { key:'boil', label:'T кип. (K)' },
+ ];
+ var fmt = function(v) { return (v !== null && v !== undefined) ? v : '—'; };
+ var html = 'Свойство ';
+ els.forEach(function(el) {
+ var col = TYPE_COLORS[el.type] || '#888';
+ html += '' + el.symbol + '' + el.name + ' ';
+ });
+ html += ' ';
+ PROPS.forEach(function(p) {
+ var vals = els.map(function(el) { return el[p.key]; });
+ var numVals = vals.filter(function(v) { return typeof v === 'number' && v !== null; });
+ var maxV = numVals.length > 1 ? Math.max.apply(null, numVals) : null;
+ var minV = numVals.length > 1 ? Math.min.apply(null, numVals) : null;
+ html += '' + p.label + ' ';
+ vals.forEach(function(v) {
+ var ex = '';
+ if (maxV !== null && typeof v === 'number') {
+ if (v === maxV) ex = 'background:rgba(123,245,164,0.1);color:#7BF5A4;';
+ else if (v === minV) ex = 'background:rgba(239,71,111,0.1);color:#EF476F;';
+ }
+ html += '' + fmt(v) + ' ';
+ });
+ html += ' ';
+ });
+ html += '
';
+ tbl.innerHTML = html;
+ this._compareDraw(panel.querySelector('.ptbl-cmp-chart'), els, propKey);
+};
+
+PeriodicTableSim.prototype._compareDraw = function(canvas, els, propKey) {
+ if (!canvas) return;
+ var dpr = window.devicePixelRatio || 1;
+ var W = canvas.offsetWidth || 300, H = canvas.offsetHeight || 52;
+ canvas.width = W * dpr; canvas.height = H * dpr;
+ var ctx = canvas.getContext('2d');
+ ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H);
+ if (!els.length) return;
+ var vals = els.map(function(e) { return e[propKey]; }).filter(function(v) { return v !== null && v !== undefined && isFinite(v); });
+ if (!vals.length) return;
+ var minV = Math.min.apply(null, vals), maxV = Math.max.apply(null, vals);
+ var pad = { t:4, r:14, b:15, l:5 };
+ var gW = W - pad.l - pad.r, gH = H - pad.t - pad.b;
+ ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1;
+ ctx.beginPath(); ctx.moveTo(pad.l, pad.t); ctx.lineTo(pad.l, pad.t+gH); ctx.lineTo(pad.l+gW, pad.t+gH); ctx.stroke();
+ var step = els.length > 1 ? gW / (els.length - 1) : gW * 0.5;
+ els.forEach(function(el, i) {
+ var v = el[propKey];
+ if (v === null || v === undefined || !isFinite(v)) return;
+ var x = pad.l + i * step;
+ var y = pad.t + gH - ((v - minV) / (maxV - minV || 1)) * gH;
+ var col = TYPE_COLORS[el.type] || '#7B8EF7';
+ ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI*2);
+ ctx.fillStyle = col; ctx.fill();
+ ctx.strokeStyle = 'rgba(0,0,0,0.4)'; ctx.lineWidth = 1; ctx.stroke();
+ ctx.font = '9px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)';
+ ctx.textAlign = 'center'; ctx.fillText(el.symbol, x, H - 2);
+ });
+};
+
+/* ── MODE 3: ACTIVITY SERIES ── */
+PeriodicTableSim.prototype._modeActivity = function() {
+ var SERIES = [
+ { s:'Li', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
+ { s:'Cs', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
+ { s:'Rb', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
+ { s:'K', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
+ { s:'Ba', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
+ { s:'Sr', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
+ { s:'Ca', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
+ { s:'Na', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
+ { s:'Mg', t:'Реагирует с горячей H₂O; с разб. HCl', g:'active' },
+ { s:'Al', t:'Реагирует с разб. HCl; пассивируется конц. H₂SO₄', g:'active' },
+ { s:'Mn', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
+ { s:'Zn', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
+ { s:'Cr', t:'Реагирует с разб. HCl; пассивируется конц. H₂SO₄', g:'medium' },
+ { s:'Fe', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
+ { s:'Cd', t:'Реагирует с разб. HCl', g:'medium' },
+ { s:'Co', t:'Реагирует с разб. HCl медленно', g:'medium' },
+ { s:'Ni', t:'Реагирует с разб. HCl медленно', g:'medium' },
+ { s:'Sn', t:'Реагирует с разб. HCl медленно', g:'medium' },
+ { s:'Pb', t:'Слабо реагирует с разб. HCl', g:'medium' },
+ { s:'H', t:'Разделитель: металлы левее вытесняют H₂ из кислот', g:'sep' },
+ { s:'Sb', t:'Реагирует только с конц. кислотами', g:'low' },
+ { s:'Bi', t:'Реагирует только с конц. кислотами', g:'low' },
+ { s:'Cu', t:'Не реаг. с HCl; реаг. с конц. H₂SO₄, HNO₃', g:'low' },
+ { s:'Hg', t:'Реагирует с конц. HNO₃ и H₂SO₄', g:'low' },
+ { s:'Ag', t:'Реагирует с HNO₃', g:'low' },
+ { s:'Pd', t:'Реагирует с царской водкой', g:'low' },
+ { s:'Pt', t:'Реагирует только с царской водкой', g:'low' },
+ { s:'Au', t:'Реагирует только с царской водкой', g:'low' },
+ ];
+ var GC = {
+ active:{ bg:'rgba(239,71,111,0.1)', bd:'rgba(239,71,111,0.36)', c:'#EF476F', lbl:'Активные (H₂O)' },
+ medium:{ bg:'rgba(255,209,102,0.1)', bd:'rgba(255,209,102,0.36)', c:'#FFD166', lbl:'Средние (разб. кислоты)' },
+ low: { bg:'rgba(123,142,247,0.1)', bd:'rgba(123,142,247,0.36)', c:'#7B8EF7', lbl:'Малоакт. (конц. / цар. водка)' },
+ };
+ var panel = document.createElement('div');
+ panel.className = 'ptbl-imode-panel';
+ panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;overflow-x:auto;';
+ var legHtml = '';
+ ['active','medium','low'].forEach(function(g) {
+ legHtml += '
' +
+ ' ' + GC[g].lbl + '
';
+ });
+ legHtml += '
';
+ var rowHtml = '';
+ SERIES.forEach(function(item, i) {
+ if (item.g === 'sep') {
+ rowHtml += '
';
+ return;
+ }
+ if (i > 0 && SERIES[i-1].g !== 'sep') rowHtml += '
›
';
+ var gc = GC[item.g];
+ var el = ELEMENTS.find(function(e) { return e.symbol === item.s; });
+ var name = el ? el.name : item.s;
+ rowHtml += '
' +
+ '
' + item.s + '
' +
+ '
' + name + '
';
+ });
+ rowHtml += '
';
+ panel.innerHTML = legHtml + rowHtml + '
';
+ this._wrap.appendChild(panel);
+ this._iModePanel = panel;
+ var self = this;
+ panel.querySelectorAll('.ptbl-act-item').forEach(function(item) {
+ item.addEventListener('mouseenter', function() {
+ panel.querySelector('.ptbl-act-tip').textContent = item.title;
+ item.style.filter = 'brightness(1.5)'; item.style.transform = 'scale(1.08)';
+ var el = ELEMENTS.find(function(e) { return e.symbol === item.dataset.sym; });
+ if (el && self._cellMap[el.Z]) { self._cellMap[el.Z].style.outline = '2px solid #FFD166'; self._cellMap[el.Z].style.outlineOffset = '1px'; }
+ });
+ item.addEventListener('mouseleave', function() {
+ panel.querySelector('.ptbl-act-tip').textContent = '';
+ item.style.filter = ''; item.style.transform = '';
+ var el = ELEMENTS.find(function(e) { return e.symbol === item.dataset.sym; });
+ if (el && self._cellMap[el.Z]) { self._cellMap[el.Z].style.outline = ''; self._cellMap[el.Z].style.outlineOffset = ''; }
+ });
+ });
+};
+
+/* ── MODE 4: MENDELEEV 1869 ── */
+PeriodicTableSim.prototype._modeMendeleev1869 = function() {
+ var KNOWN_1869 = new Set([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,
+ 22,23,24,25,26,27,28,29,30,33,34,35,36,37,38,40,41,42,
+ 44,45,47,48,50,51,52,53,55,56,57,60,62,64,65,66,68,70,72,74,76,78,79,80,82,83]);
+ var PREDS = {
+ 31: { name:'Экаалюминий (Ga)', pred:{ mass:68, density:5.9, melt:302 }, act:{ mass:69.72, density:5.91, melt:303 }, year:1875, who:'Лекок де Буабодран (Франция)' },
+ 21: { name:'Экабор (Sc)', pred:{ mass:44, density:3.5, melt:null }, act:{ mass:44.96, density:2.99, melt:1814 }, year:1879, who:'Ларс Нильсон (Швеция)' },
+ 32: { name:'Экасилиций (Ge)', pred:{ mass:72, density:5.5, melt:null }, act:{ mass:72.63, density:5.32, melt:1211 }, year:1886, who:'Клеменс Винклер (Германия)' },
+ 43: { name:'Экамарганец (Tc)', pred:{ mass:100, density:null, melt:null }, act:{ mass:98, density:11.5, melt:2430 }, year:1937, who:'Перье и Сегре (Италия)' },
+ };
+ var panel = document.createElement('div');
+ panel.className = 'ptbl-imode-panel';
+ panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;position:relative;min-height:38px;';
+ panel.innerHTML = '' +
+ 'Таблица Менделеева 1869: 63 известных элемента. Фиол. «?» — предсказания Менделеева. Кликните «?».' +
+ '
' +
+ '';
+ this._wrap.appendChild(panel);
+ this._iModePanel = panel;
+ var self = this;
+ ELEMENTS.forEach(function(el) {
+ var div = self._cellMap[el.Z]; if (!div) return;
+ if (KNOWN_1869.has(el.Z)) {
+ div.style.opacity = '1';
+ } else if (PREDS[el.Z]) {
+ div.style.background = 'rgba(155,93,229,0.09)';
+ div.style.border = '1px dashed rgba(155,93,229,0.48)';
+ div.style.opacity = '1';
+ div.innerHTML = '? ' +
+ '' + el.symbol + ' ';
+ div.title = el.name + ' — предсказан Менделеевым (кликните)';
+ (function(z) { div.addEventListener('click', function() { self._m1869ShowPrediction(z, PREDS[z], panel); }); })(el.Z);
+ } else {
+ div.style.opacity = '0.13';
+ div.style.background = 'rgba(255,255,255,0.02)';
+ div.style.border = '1px solid rgba(255,255,255,0.05)';
+ }
+ });
+};
+
+PeriodicTableSim.prototype._m1869ShowPrediction = function(Z, pred, panel) {
+ if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.15, volume: 0.3 });
+ var popup = panel.querySelector('.ptbl-m1869-popup');
+ var fmt = function(v) { return (v !== null && v !== undefined) ? v : '—'; };
+ var diff = function(p, a) {
+ if (p == null || a == null || !isFinite(+p) || !isFinite(+a) || +a === 0) return '';
+ var pct = (Math.abs(+p - +a) / +a * 100).toFixed(1);
+ var good = Math.abs(+p - +a) / +a < 0.05;
+ return '(' + (good ? 'точно' : pct + '% откл.') + ') ';
+ };
+ popup.innerHTML = '' + pred.name + '
' +
+ '' +
+ 'Свойство ' +
+ 'Предсказано ' +
+ 'Реально ' +
+ 'Масса ' +
+ '' + fmt(pred.pred.mass) + ' ' +
+ '' + fmt(pred.act.mass) + diff(pred.pred.mass, pred.act.mass) + ' ' +
+ 'Плотность ' +
+ '' + fmt(pred.pred.density) + ' ' +
+ '' + fmt(pred.act.density) + diff(pred.pred.density, pred.act.density) + ' ' +
+ 'T пл. (K) ' +
+ '' + fmt(pred.pred.melt) + ' ' +
+ '' + fmt(pred.act.melt) + ' ' +
+ '
' +
+ 'Открыт: ' + pred.year + ' г., ' + pred.who + '
' +
+ 'Закрыть ';
+ popup.style.display = 'block';
+};
+
+/* ── MODE 5: TIMELINE ── */
+PeriodicTableSim.prototype._modeTimeline = function() {
+ var MIN_Y = 1660, MAX_Y = 2024;
+ this._iModeState = { year: MAX_Y, playing: false, raf: null };
+ var panel = document.createElement('div');
+ panel.className = 'ptbl-imode-panel';
+ panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;';
+ panel.innerHTML = '' +
+ '
Год: ' +
+ '
' +
+ '
' + MAX_Y + ' ' +
+ '
' +
+ ' Авто' +
+ ' ' +
+ '
Открыто 0 / 118 ' +
+ '
' +
+ '
';
+ this._wrap.appendChild(panel);
+ this._iModePanel = panel;
+ var self = this;
+ var slider = panel.querySelector('.ptbl-tl-slider');
+ var yearLbl = panel.querySelector('.ptbl-tl-year');
+ var playBtn = panel.querySelector('.ptbl-tl-play');
+ var info = panel.querySelector('.ptbl-tl-info');
+ var countLbl = panel.querySelector('.ptbl-tl-count');
+ var update = function() {
+ var y = +slider.value;
+ self._iModeState.year = y;
+ yearLbl.textContent = y;
+ self._timelineUpdate(y, info, countLbl);
+ };
+ slider.addEventListener('input', update);
+ update();
+ playBtn.addEventListener('click', function() {
+ var st = self._iModeState;
+ if (st.playing) {
+ st.playing = false;
+ cancelAnimationFrame(st.raf);
+ playBtn.innerHTML = ' Авто';
+ } else {
+ if (+slider.value >= MAX_Y) slider.value = MIN_Y;
+ st.playing = true;
+ playBtn.innerHTML = ' Стоп';
+ var last = null;
+ var tick = function(ts) {
+ if (!st.playing) return;
+ if (!last) last = ts;
+ if (ts - last > 38) {
+ var cur = +slider.value;
+ if (cur >= MAX_Y) {
+ st.playing = false;
+ playBtn.innerHTML = ' Авто';
+ return;
+ }
+ slider.value = cur + 2;
+ update();
+ last = ts;
+ }
+ st.raf = requestAnimationFrame(tick);
+ };
+ st.raf = requestAnimationFrame(tick);
+ }
+ });
+};
+
+PeriodicTableSim.prototype._timelineUpdate = function(year, info, countLbl) {
+ var count = 0, lastEl = null, lastYear = -Infinity;
+ ELEMENTS.forEach(function(el) {
+ var div = this._cellMap[el.Z]; if (!div) return;
+ var known = (el.discovered === null) || (el.discovered <= year);
+ if (known) {
+ count++;
+ div.style.opacity = '1';
+ var col = TYPE_COLORS[el.type] || '#555';
+ div.style.background = col + '44';
+ div.style.border = '1px solid ' + col + '88';
+ div.style.outline = ''; div.style.outlineOffset = '';
+ if (el.discovered !== null && el.discovered <= year && el.discovered > lastYear) {
+ lastYear = el.discovered; lastEl = el;
+ }
+ } else {
+ div.style.opacity = '0.09';
+ div.style.background = 'rgba(255,255,255,0.015)';
+ div.style.border = '1px solid rgba(255,255,255,0.04)';
+ div.style.outline = ''; div.style.outlineOffset = '';
+ }
+ }, this);
+ if (countLbl) countLbl.textContent = 'Открыто ' + count + ' / 118';
+ if (lastEl && lastYear > -Infinity) {
+ var d = this._cellMap[lastEl.Z];
+ if (d) { d.style.outline = '2px solid #FFD166'; d.style.outlineOffset = '1px'; }
+ if (info) info.textContent = lastYear + ' г.: ' + lastEl.name + ' (' + lastEl.symbol + ') — ' + (lastEl.by || '?');
+ } else if (info) {
+ info.textContent = '';
+ }
+};
+
var periodicSim = null;
function _openPeriodic() {
diff --git a/frontend/lab.html b/frontend/lab.html
index ba7ebba..35558d3 100644
--- a/frontend/lab.html
+++ b/frontend/lab.html
@@ -4340,6 +4340,7 @@
+