From 7ffed4597453fca8b6507f50e984b62b83a8efe5 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 26 May 2026 13:45:35 +0300 Subject: [PATCH] =?UTF-8?q?feat(labs):=20=D0=BC=D0=B0=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D0=BC=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20=D1=83=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D0=BE=D0=B4=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B9=20=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B=20=E2=80=94=205=20=D0=B2?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ВОЛНА A — Расширенная база данных: - Новый файл _periodic_data.js (~70 KB): PERIODIC_EXT_DATA + ISOTOPES + SPECTRA - 30 элементов полностью (H..Au): радиусы, ионизация, теплоёмкость, теплопроводность, кристалл, распространённость, биология, токсичность, пламя, применения, история, этимология, минералы, типичные реакции - 9 элементов с минимумом (Sc, Ti, V, As, Se, Kr, Hg, Pb, I) - 60 изотопов в 20 элементах (включая ¹³¹I, ¹³⁷Cs, ⁶⁰Co, ⁹⁰Sr, ¹⁴C, ³H, U-235/238) - 43 эмиссионных линий для 8 элементов (H, He, Li, Na, K, Ne, Ar, Hg) ВОЛНА B — Визуальные режимы: - Heatmap по 9 свойствам (En, mass, density, melt, boil, discovered + расширенные) с jet-colormap, lin/log toggle, легендой, анимацией 400ms - 3D-таблица через Three.js: bar / wave / stack modes, orbit camera, raycaster hover - Морф между формами таблицы: standard / long (32-col, f-block inline) / short (8-col) с staggered fade-in 800ms - Тренды стрелками: радиус / ЭО / ИЕ / металличность с градиентными arrows ВОЛНА C — Карточка элемента 2.0 (11 табов): - Обзор (hero 96px символ + Z + категория-бейдж + quick stats) - Свойства (17-row таблица расширенных параметров) - Электроника (Bohr + статичная конфигурация) - Изотопы (список + bar chart + weighted average mass) - История (timeline + этимология) - Применения (15 SVG иконок-сфер + текст) - Биология (badge: macro/micro/trace/toxic/inert/radioactive) - Минералы (формулы) - Спектр (rainbow 380-780nm + линии эмиссии) - Пламя (цвет + название) - Реакции (типовые уравнения по типу элемента) - Hero header с цветом типа; smooth fade transitions между табами ВОЛНА D — Интерактивные режимы: - Бинарные соединения: drag 2 элемента → формула (NaCl, Fe₂O₃) + тип связи (ΔЭО) - Сравнить до 4 элементов: side-by-side + min/max highlight + chart - Ряд активности металлов: 28 элементов от Li до Au, разделитель H - Таблица Менделеева 1869: 63 элемента + 4 предсказанных (Ga, Sc, Ge, Tc) с popup «предсказано vs реально» - Таймлайн открытий 1660-2024 с slider и auto-play ВОЛНА G — Электронные конфигурации углубление: - Orbital filling diagram: квадратики с электронами по Хунду/Паули, glow на валентном - Aufbau diagram с slider Z 1-118 и анимированным указателем порядка заполнения - Квантовые числа (n, l, m_l, m_s) — hover на электрон → tooltip - Возбуждение электронов: click на электрон в Bohr → выбор уровня → анимация перехода с фотоном (цвет ∝ длине волны через ΔE = 13.6 eV × ...) periodic.js: 750 → 3239 строк. Все 5 волн ADDITIVE — старая база сохранена. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/css/lab.css | 385 +++++ frontend/js/labs/_periodic_data.js | 1233 ++++++++++++++ frontend/js/labs/periodic.js | 2536 +++++++++++++++++++++++++++- frontend/lab.html | 1 + 4 files changed, 4131 insertions(+), 24 deletions(-) create mode 100644 frontend/js/labs/_periodic_data.js 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 => ``).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]) => ``).join('')} + +
${l}${v}
`; + } + + /* ── 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 ` +
+ + + ${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 => ` +
+
${r.label}
+
${r.eq}
+
`).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 = ` + Тепловая карта: + + + + + + + + + + Вид: + + + + Тренды: + + + + + + 3D: + + + + + `; + + /* 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 элементов' + + '' + + 'График:' + + '
' + + '
' + + ''; + 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 += ''; + }); + 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 += ''; + 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 += ''; + }); + html += ''; + }); + html += '
Свойство' + el.symbol + '
' + el.name + '
' + p.label + '' + fmt(v) + '
'; + 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 += '
' + + '
' + + 'H
'; + 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 @@ +