Files
Maxim Dolgolyov 2552c8a90e fix(labs): убрать перекрытие соседних элементов при выборе ячейки в таблице Менделеева
outline:2px + outlineOffset:1px давал 3px рамку поверх 2px-зазора → визуально перекрывал соседей.
Заменил на inset box-shadow — рамка внутри ячейки.
2026-05-26 16:06:49 +03:00

3238 lines
175 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* ══════════════════════════════════════════════════════════════
PeriodicTableSim — Периодическая таблица (118 элементов)
Режимы: стандартный вид, подсветка по типам/блокам,
графики свойств, боровские оболочки, поиск элементов
══════════════════════════════════════════════════════════════ */
/* ── Element data ───────────────────────────────────────────── */
const ELEMENTS = [
{ Z:1, symbol:'H', name:'Водород', mass:1.008, group:1, period:1, block:'s', config:'1s¹', oxStates:[-1,+1], En:2.20, density:0.0899, melt:14.01, boil:20.28, type:'nonmetal', discovered:1766, by:'Кавендиш' },
{ Z:2, symbol:'He', name:'Гелий', mass:4.003, group:18, period:1, block:'s', config:'1s²', oxStates:[0], En:null, density:0.1786, melt:0.95, boil:4.22, type:'noble', discovered:1868, by:'Жансен' },
{ Z:3, symbol:'Li', name:'Литий', mass:6.941, group:1, period:2, block:'s', config:'[He]2s¹', oxStates:[+1], En:0.98, density:0.534, melt:453.65, boil:1603, type:'alkali', discovered:1817, by:'Арфведсон' },
{ Z:4, symbol:'Be', name:'Бериллий', mass:9.012, group:2, period:2, block:'s', config:'[He]2s²', oxStates:[+2], En:1.57, density:1.85, melt:1560, boil:2742, type:'alkaline', discovered:1798, by:'Воклен' },
{ Z:5, symbol:'B', name:'Бор', mass:10.811, group:13, period:2, block:'p', config:'[He]2s²2p¹', oxStates:[+3], En:2.04, density:2.34, melt:2349, boil:4200, type:'metalloid', discovered:1808, by:'Гей-Люссак' },
{ Z:6, symbol:'C', name:'Углерод', mass:12.011, group:14, period:2, block:'p', config:'[He]2s²2p²', oxStates:[-4,+4], En:2.55, density:2.267, melt:3823, boil:4098, type:'nonmetal', discovered:null, by:'Древний мир' },
{ Z:7, symbol:'N', name:'Азот', mass:14.007, group:15, period:2, block:'p', config:'[He]2s²2p³', oxStates:[-3,+5], En:3.04, density:1.251, melt:63.15, boil:77.36, type:'nonmetal', discovered:1772, by:'Резерфорд' },
{ Z:8, symbol:'O', name:'Кислород', mass:15.999, group:16, period:2, block:'p', config:'[He]2s²2p⁴', oxStates:[-2], En:3.44, density:1.429, melt:54.36, boil:90.20, type:'nonmetal', discovered:1774, by:'Пристли' },
{ Z:9, symbol:'F', name:'Фтор', mass:18.998, group:17, period:2, block:'p', config:'[He]2s²2p⁵', oxStates:[-1], En:3.98, density:1.696, melt:53.48, boil:85.03, type:'halogen', discovered:1886, by:'Муассан' },
{ Z:10, symbol:'Ne', name:'Неон', mass:20.180, group:18, period:2, block:'p', config:'[He]2s²2p⁶', oxStates:[0], En:null, density:0.9002, melt:24.56, boil:27.07, type:'noble', discovered:1898, by:'Рамзай' },
{ Z:11, symbol:'Na', name:'Натрий', mass:22.990, group:1, period:3, block:'s', config:'[Ne]3s¹', oxStates:[+1], En:0.93, density:0.971, melt:370.87, boil:1156, type:'alkali', discovered:1807, by:'Дэви' },
{ Z:12, symbol:'Mg', name:'Магний', mass:24.305, group:2, period:3, block:'s', config:'[Ne]3s²', oxStates:[+2], En:1.31, density:1.738, melt:923, boil:1363, type:'alkaline', discovered:1755, by:'Блэк' },
{ Z:13, symbol:'Al', name:'Алюминий', mass:26.982, group:13, period:3, block:'p', config:'[Ne]3s²3p¹', oxStates:[+3], En:1.61, density:2.70, melt:933.47, boil:2792, type:'posttransition',discovered:1825, by:'Эрстед' },
{ Z:14, symbol:'Si', name:'Кремний', mass:28.086, group:14, period:3, block:'p', config:'[Ne]3s²3p²', oxStates:[-4,+4], En:1.90, density:2.329, melt:1687, boil:3538, type:'metalloid', discovered:1824, by:'Берцелиус' },
{ Z:15, symbol:'P', name:'Фосфор', mass:30.974, group:15, period:3, block:'p', config:'[Ne]3s²3p³', oxStates:[-3,+5], En:2.19, density:1.823, melt:317.30, boil:553.65, type:'nonmetal', discovered:1669, by:'Бранд' },
{ Z:16, symbol:'S', name:'Сера', mass:32.065, group:16, period:3, block:'p', config:'[Ne]3s²3p⁴', oxStates:[-2,+6], En:2.58, density:2.07, melt:388.36, boil:717.87, type:'nonmetal', discovered:null, by:'Древний мир' },
{ Z:17, symbol:'Cl', name:'Хлор', mass:35.453, group:17, period:3, block:'p', config:'[Ne]3s²3p⁵', oxStates:[-1,+7], En:3.16, density:3.214, melt:171.65, boil:239.11, type:'halogen', discovered:1774, by:'Шееле' },
{ Z:18, symbol:'Ar', name:'Аргон', mass:39.948, group:18, period:3, block:'p', config:'[Ne]3s²3p⁶', oxStates:[0], En:null, density:1.784, melt:83.80, boil:87.30, type:'noble', discovered:1894, by:'Рэлей' },
{ Z:19, symbol:'K', name:'Калий', mass:39.098, group:1, period:4, block:'s', config:'[Ar]4s¹', oxStates:[+1], En:0.82, density:0.862, melt:336.53, boil:1032, type:'alkali', discovered:1807, by:'Дэви' },
{ Z:20, symbol:'Ca', name:'Кальций', mass:40.078, group:2, period:4, block:'s', config:'[Ar]4s²', oxStates:[+2], En:1.00, density:1.55, melt:1115, boil:1757, type:'alkaline', discovered:1808, by:'Дэви' },
{ Z:21, symbol:'Sc', name:'Скандий', mass:44.956, group:3, period:4, block:'d', config:'[Ar]3d¹4s²', oxStates:[+3], En:1.36, density:2.985, melt:1814, boil:3109, type:'transition', discovered:1879, by:'Нильсон' },
{ Z:22, symbol:'Ti', name:'Титан', mass:47.867, group:4, period:4, block:'d', config:'[Ar]3d²4s²', oxStates:[+4], En:1.54, density:4.507, melt:1941, boil:3560, type:'transition', discovered:1791, by:'Грегор' },
{ Z:23, symbol:'V', name:'Ванадий', mass:50.942, group:5, period:4, block:'d', config:'[Ar]3d³4s²', oxStates:[+5], En:1.63, density:6.11, melt:2183, boil:3680, type:'transition', discovered:1830, by:'Сефстрём' },
{ Z:24, symbol:'Cr', name:'Хром', mass:51.996, group:6, period:4, block:'d', config:'[Ar]3d⁵4s¹', oxStates:[+3,+6], En:1.66, density:7.19, melt:2180, boil:2944, type:'transition', discovered:1798, by:'Воклен' },
{ Z:25, symbol:'Mn', name:'Марганец', mass:54.938, group:7, period:4, block:'d', config:'[Ar]3d⁵4s²', oxStates:[+2,+7], En:1.55, density:7.21, melt:1519, boil:2334, type:'transition', discovered:1774, by:'Ган' },
{ Z:26, symbol:'Fe', name:'Железо', mass:55.845, group:8, period:4, block:'d', config:'[Ar]3d⁶4s²', oxStates:[+2,+3], En:1.83, density:7.874, melt:1811, boil:3134, type:'transition', discovered:null, by:'Древний мир' },
{ Z:27, symbol:'Co', name:'Кобальт', mass:58.933, group:9, period:4, block:'d', config:'[Ar]3d⁷4s²', oxStates:[+2,+3], En:1.88, density:8.90, melt:1768, boil:3200, type:'transition', discovered:1735, by:'Брандт' },
{ Z:28, symbol:'Ni', name:'Никель', mass:58.693, group:10, period:4, block:'d', config:'[Ar]3d⁸4s²', oxStates:[+2], En:1.91, density:8.908, melt:1728, boil:3186, type:'transition', discovered:1751, by:'Кронстедт' },
{ Z:29, symbol:'Cu', name:'Медь', mass:63.546, group:11, period:4, block:'d', config:'[Ar]3d¹⁰4s¹', oxStates:[+1,+2], En:1.90, density:8.96, melt:1357.77,boil:2835, type:'transition', discovered:null, by:'Древний мир' },
{ Z:30, symbol:'Zn', name:'Цинк', mass:65.38, group:12, period:4, block:'d', config:'[Ar]3d¹⁰4s²', oxStates:[+2], En:1.65, density:7.14, melt:692.68, boil:1180, type:'transition', discovered:1746, by:'Марграф' },
{ Z:31, symbol:'Ga', name:'Галлий', mass:69.723, group:13, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p¹', oxStates:[+3], En:1.81, density:5.91, melt:302.91, boil:2477, type:'posttransition',discovered:1875, by:'Де Буабодран' },
{ Z:32, symbol:'Ge', name:'Германий', mass:72.630, group:14, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p²', oxStates:[+4], En:2.01, density:5.323, melt:1211.40,boil:3106, type:'metalloid', discovered:1886, by:'Винклер' },
{ Z:33, symbol:'As', name:'Мышьяк', mass:74.922, group:15, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p³', oxStates:[-3,+5], En:2.18, density:5.776, melt:1090, boil:887, type:'metalloid', discovered:1250, by:'Альберт Великий' },
{ Z:34, symbol:'Se', name:'Селен', mass:78.971, group:16, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p⁴', oxStates:[-2,+6], En:2.55, density:4.809, melt:493.65, boil:958, type:'nonmetal', discovered:1817, by:'Берцелиус' },
{ Z:35, symbol:'Br', name:'Бром', mass:79.904, group:17, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p⁵', oxStates:[-1,+5], En:2.96, density:3.122, melt:265.95, boil:332.00, type:'halogen', discovered:1826, by:'Балар' },
{ Z:36, symbol:'Kr', name:'Криптон', mass:83.798, group:18, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p⁶', oxStates:[0], En:3.00, density:3.749, melt:115.79, boil:119.93, type:'noble', discovered:1898, by:'Рамзай' },
{ Z:37, symbol:'Rb', name:'Рубидий', mass:85.468, group:1, period:5, block:'s', config:'[Kr]5s¹', oxStates:[+1], En:0.82, density:1.532, melt:312.46, boil:961, type:'alkali', discovered:1861, by:'Бунзен' },
{ Z:38, symbol:'Sr', name:'Стронций', mass:87.62, group:2, period:5, block:'s', config:'[Kr]5s²', oxStates:[+2], En:0.95, density:2.64, melt:1050, boil:1655, type:'alkaline', discovered:1790, by:'Кроуфорд' },
{ Z:39, symbol:'Y', name:'Иттрий', mass:88.906, group:3, period:5, block:'d', config:'[Kr]4d¹5s²', oxStates:[+3], En:1.22, density:4.472, melt:1799, boil:3609, type:'transition', discovered:1794, by:'Гадолин' },
{ Z:40, symbol:'Zr', name:'Цирконий', mass:91.224, group:4, period:5, block:'d', config:'[Kr]4d²5s²', oxStates:[+4], En:1.33, density:6.52, melt:2128, boil:4682, type:'transition', discovered:1789, by:'Клапрот' },
{ Z:41, symbol:'Nb', name:'Ниобий', mass:92.906, group:5, period:5, block:'d', config:'[Kr]4d⁴5s¹', oxStates:[+5], En:1.6, density:8.57, melt:2750, boil:5017, type:'transition', discovered:1801, by:'Хатчетт' },
{ Z:42, symbol:'Mo', name:'Молибден', mass:95.95, group:6, period:5, block:'d', config:'[Kr]4d⁵5s¹', oxStates:[+6], En:2.16, density:10.28, melt:2896, boil:4912, type:'transition', discovered:1781, by:'Шееле' },
{ Z:43, symbol:'Tc', name:'Технеций', mass:98, group:7, period:5, block:'d', config:'[Kr]4d⁵5s²', oxStates:[+7], En:1.9, density:11.50, melt:2430, boil:4538, type:'transition', discovered:1937, by:'Перье' },
{ Z:44, symbol:'Ru', name:'Рутений', mass:101.07, group:8, period:5, block:'d', config:'[Kr]4d⁷5s¹', oxStates:[+4], En:2.2, density:12.45, melt:2607, boil:4423, type:'transition', discovered:1844, by:'Клаус' },
{ Z:45, symbol:'Rh', name:'Родий', mass:102.906, group:9, period:5, block:'d', config:'[Kr]4d⁸5s¹', oxStates:[+3], En:2.28, density:12.41, melt:2237, boil:3968, type:'transition', discovered:1803, by:'Воластон' },
{ Z:46, symbol:'Pd', name:'Палладий', mass:106.42, group:10, period:5, block:'d', config:'[Kr]4d¹⁰', oxStates:[+2], En:2.20, density:12.023, melt:1828.05,boil:3236, type:'transition', discovered:1803, by:'Воластон' },
{ Z:47, symbol:'Ag', name:'Серебро', mass:107.868, group:11, period:5, block:'d', config:'[Kr]4d¹⁰5s¹', oxStates:[+1], En:1.93, density:10.49, melt:1234.93,boil:2435, type:'transition', discovered:null, by:'Древний мир' },
{ Z:48, symbol:'Cd', name:'Кадмий', mass:112.414, group:12, period:5, block:'d', config:'[Kr]4d¹⁰5s²', oxStates:[+2], En:1.69, density:8.65, melt:594.22, boil:1040, type:'transition', discovered:1817, by:'Штромейер' },
{ Z:49, symbol:'In', name:'Индий', mass:114.818, group:13, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p¹', oxStates:[+3], En:1.78, density:7.31, melt:429.75, boil:2345, type:'posttransition',discovered:1863, by:'Рейх' },
{ Z:50, symbol:'Sn', name:'Олово', mass:118.710, group:14, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p²', oxStates:[+2,+4], En:1.96, density:7.287, melt:505.08, boil:2875, type:'posttransition',discovered:null, by:'Древний мир' },
{ Z:51, symbol:'Sb', name:'Сурьма', mass:121.760, group:15, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p³', oxStates:[-3,+5], En:2.05, density:6.697, melt:903.78, boil:1860, type:'metalloid', discovered:null, by:'Древний мир' },
{ Z:52, symbol:'Te', name:'Теллур', mass:127.60, group:16, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p⁴', oxStates:[-2,+6], En:2.1, density:6.24, melt:722.66, boil:1261, type:'metalloid', discovered:1782, by:'фон Райхенштайн' },
{ Z:53, symbol:'I', name:'Йод', mass:126.904, group:17, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p⁵', oxStates:[-1,+7], En:2.66, density:4.933, melt:386.85, boil:457.55, type:'halogen', discovered:1811, by:'Куртуа' },
{ Z:54, symbol:'Xe', name:'Ксенон', mass:131.293, group:18, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p⁶', oxStates:[0], En:2.6, density:5.894, melt:161.40, boil:165.05, type:'noble', discovered:1898, by:'Рамзай' },
{ Z:55, symbol:'Cs', name:'Цезий', mass:132.905, group:1, period:6, block:'s', config:'[Xe]6s¹', oxStates:[+1], En:0.79, density:1.873, melt:301.59, boil:944, type:'alkali', discovered:1860, by:'Бунзен' },
{ Z:56, symbol:'Ba', name:'Барий', mass:137.327, group:2, period:6, block:'s', config:'[Xe]6s²', oxStates:[+2], En:0.89, density:3.594, melt:1000, boil:2170, type:'alkaline', discovered:1808, by:'Дэви' },
{ Z:57, symbol:'La', name:'Лантан', mass:138.905, group:3, period:6, block:'f', config:'[Xe]5d¹6s²', oxStates:[+3], En:1.10, density:6.162, melt:1193, boil:3737, type:'lanthanide', discovered:1839, by:'Мосандер' },
{ Z:58, symbol:'Ce', name:'Церий', mass:140.116, group:null,period:6, block:'f', config:'[Xe]4f¹5d¹6s²', oxStates:[+3,+4], En:1.12, density:6.770, melt:1068, boil:3716, type:'lanthanide', discovered:1803, by:'Берцелиус' },
{ Z:59, symbol:'Pr', name:'Празеодим', mass:140.908, group:null,period:6, block:'f', config:'[Xe]4f³6s²', oxStates:[+3], En:1.13, density:6.77, melt:1208, boil:3793, type:'lanthanide', discovered:1885, by:'фон Вельсбах' },
{ Z:60, symbol:'Nd', name:'Неодим', mass:144.242, group:null,period:6, block:'f', config:'[Xe]4f⁴6s²', oxStates:[+3], En:1.14, density:7.01, melt:1297, boil:3347, type:'lanthanide', discovered:1885, by:'фон Вельсбах' },
{ Z:61, symbol:'Pm', name:'Прометий', mass:145, group:null,period:6, block:'f', config:'[Xe]4f⁵6s²', oxStates:[+3], En:1.13, density:7.26, melt:1315, boil:3273, type:'lanthanide', discovered:1945, by:'Маринский' },
{ Z:62, symbol:'Sm', name:'Самарий', mass:150.36, group:null,period:6, block:'f', config:'[Xe]4f⁶6s²', oxStates:[+2,+3], En:1.17, density:7.52, melt:1345, boil:2067, type:'lanthanide', discovered:1879, by:'Буабодран' },
{ Z:63, symbol:'Eu', name:'Европий', mass:151.964, group:null,period:6, block:'f', config:'[Xe]4f⁷6s²', oxStates:[+2,+3], En:1.20, density:5.244, melt:1099, boil:1802, type:'lanthanide', discovered:1901, by:'Демарсе' },
{ Z:64, symbol:'Gd', name:'Гадолиний', mass:157.25, group:null,period:6, block:'f', config:'[Xe]4f⁷5d¹6s²', oxStates:[+3], En:1.20, density:7.90, melt:1585, boil:3546, type:'lanthanide', discovered:1880, by:'Мариньяк' },
{ Z:65, symbol:'Tb', name:'Тербий', mass:158.925, group:null,period:6, block:'f', config:'[Xe]4f⁹6s²', oxStates:[+3], En:1.10, density:8.23, melt:1629, boil:3503, type:'lanthanide', discovered:1843, by:'Мосандер' },
{ Z:66, symbol:'Dy', name:'Диспрозий', mass:162.500, group:null,period:6, block:'f', config:'[Xe]4f¹⁰6s²', oxStates:[+3], En:1.22, density:8.540, melt:1680, boil:2840, type:'lanthanide', discovered:1886, by:'Буабодран' },
{ Z:67, symbol:'Ho', name:'Гольмий', mass:164.930, group:null,period:6, block:'f', config:'[Xe]4f¹¹6s²', oxStates:[+3], En:1.23, density:8.795, melt:1734, boil:2993, type:'lanthanide', discovered:1878, by:'Клеве' },
{ Z:68, symbol:'Er', name:'Эрбий', mass:167.259, group:null,period:6, block:'f', config:'[Xe]4f¹²6s²', oxStates:[+3], En:1.24, density:9.066, melt:1802, boil:3141, type:'lanthanide', discovered:1843, by:'Мосандер' },
{ Z:69, symbol:'Tm', name:'Тулий', mass:168.934, group:null,period:6, block:'f', config:'[Xe]4f¹³6s²', oxStates:[+3], En:1.25, density:9.32, melt:1818, boil:2223, type:'lanthanide', discovered:1879, by:'Клеве' },
{ Z:70, symbol:'Yb', name:'Иттербий', mass:173.054, group:null,period:6, block:'f', config:'[Xe]4f¹⁴6s²', oxStates:[+2,+3], En:1.10, density:6.90, melt:1097, boil:1469, type:'lanthanide', discovered:1878, by:'Мариньяк' },
{ Z:71, symbol:'Lu', name:'Лютеций', mass:174.967, group:3, period:6, block:'d', config:'[Xe]4f¹⁴5d¹6s²', oxStates:[+3], En:1.27, density:9.841, melt:1925, boil:3675, type:'lanthanide', discovered:1907, by:'Урбен' },
{ Z:72, symbol:'Hf', name:'Гафний', mass:178.49, group:4, period:6, block:'d', config:'[Xe]4f¹⁴5d²6s²', oxStates:[+4], En:1.3, density:13.31, melt:2506, boil:4876, type:'transition', discovered:1923, by:'Костер' },
{ Z:73, symbol:'Ta', name:'Тантал', mass:180.948, group:5, period:6, block:'d', config:'[Xe]4f¹⁴5d³6s²', oxStates:[+5], En:1.5, density:16.69, melt:3290, boil:5731, type:'transition', discovered:1802, by:'Экеберг' },
{ Z:74, symbol:'W', name:'Вольфрам', mass:183.84, group:6, period:6, block:'d', config:'[Xe]4f¹⁴5d⁴6s²', oxStates:[+6], En:2.36, density:19.25, melt:3695, boil:5828, type:'transition', discovered:1783, by:'Братья дель Риo' },
{ Z:75, symbol:'Re', name:'Рений', mass:186.207, group:7, period:6, block:'d', config:'[Xe]4f¹⁴5d⁵6s²', oxStates:[+7], En:1.9, density:21.02, melt:3459, boil:5869, type:'transition', discovered:1925, by:'Ноддак' },
{ Z:76, symbol:'Os', name:'Осмий', mass:190.23, group:8, period:6, block:'d', config:'[Xe]4f¹⁴5d⁶6s²', oxStates:[+4], En:2.2, density:22.59, melt:3306, boil:5285, type:'transition', discovered:1803, by:'Теннант' },
{ Z:77, symbol:'Ir', name:'Иридий', mass:192.217, group:9, period:6, block:'d', config:'[Xe]4f¹⁴5d⁷6s²', oxStates:[+4], En:2.20, density:22.56, melt:2719, boil:4701, type:'transition', discovered:1803, by:'Теннант' },
{ Z:78, symbol:'Pt', name:'Платина', mass:195.084, group:10, period:6, block:'d', config:'[Xe]4f¹⁴5d⁹6s¹', oxStates:[+2,+4], En:2.28, density:21.45, melt:2041.4, boil:4098, type:'transition', discovered:1735, by:'де Улоа' },
{ Z:79, symbol:'Au', name:'Золото', mass:196.967, group:11, period:6, block:'d', config:'[Xe]4f¹⁴5d¹⁰6s¹',oxStates:[+1,+3], En:2.54, density:19.30, melt:1337.33,boil:3129, type:'transition', discovered:null, by:'Древний мир' },
{ Z:80, symbol:'Hg', name:'Ртуть', mass:200.592, group:12, period:6, block:'d', config:'[Xe]4f¹⁴5d¹⁰6s²',oxStates:[+1,+2], En:2.00, density:13.534, melt:234.32, boil:629.88, type:'transition', discovered:null, by:'Древний мир' },
{ Z:81, symbol:'Tl', name:'Таллий', mass:204.38, group:13, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p¹',oxStates:[+1,+3],En:1.62,density:11.85, melt:577, boil:1746, type:'posttransition',discovered:1861, by:'Крукс' },
{ Z:82, symbol:'Pb', name:'Свинец', mass:207.2, group:14, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p²',oxStates:[+2,+4],En:2.33,density:11.34, melt:600.61, boil:2022, type:'posttransition',discovered:null, by:'Древний мир' },
{ Z:83, symbol:'Bi', name:'Висмут', mass:208.980, group:15, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p³',oxStates:[+3], En:2.02,density:9.747, melt:544.55, boil:1837, type:'posttransition',discovered:1753, by:'Жоффруа' },
{ Z:84, symbol:'Po', name:'Полоний', mass:209, group:16, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p⁴',oxStates:[+4], En:2.0, density:9.32, melt:527, boil:1235, type:'metalloid', discovered:1898, by:'Кюри' },
{ Z:85, symbol:'At', name:'Астат', mass:210, group:17, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p⁵',oxStates:[-1,+1],En:2.2, density:null, melt:575, boil:null, type:'halogen', discovered:1940, by:'Корсон' },
{ Z:86, symbol:'Rn', name:'Радон', mass:222, group:18, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p⁶',oxStates:[0], En:null,density:9.73, melt:202, boil:211.45, type:'noble', discovered:1900, by:'Дорн' },
{ Z:87, symbol:'Fr', name:'Франций', mass:223, group:1, period:7, block:'s', config:'[Rn]7s¹', oxStates:[+1], En:0.7, density:null, melt:300, boil:950, type:'alkali', discovered:1939, by:'Перей' },
{ Z:88, symbol:'Ra', name:'Радий', mass:226, group:2, period:7, block:'s', config:'[Rn]7s²', oxStates:[+2], En:0.9, density:5.0, melt:973, boil:2010, type:'alkaline', discovered:1898, by:'Кюри' },
{ Z:89, symbol:'Ac', name:'Актиний', mass:227, group:3, period:7, block:'f', config:'[Rn]6d¹7s²', oxStates:[+3], En:1.1, density:10.07, melt:1323, boil:3471, type:'actinide', discovered:1899, by:'Дебьерн' },
{ Z:90, symbol:'Th', name:'Торий', mass:232.038, group:null,period:7, block:'f', config:'[Rn]6d²7s²', oxStates:[+4], En:1.3, density:11.72, melt:2115, boil:5061, type:'actinide', discovered:1828, by:'Берцелиус' },
{ Z:91, symbol:'Pa', name:'Протактиний', mass:231.036, group:null,period:7, block:'f', config:'[Rn]5f²6d¹7s²', oxStates:[+5], En:1.5, density:15.37, melt:1841, boil:4300, type:'actinide', discovered:1913, by:'Фаянс' },
{ Z:92, symbol:'U', name:'Уран', mass:238.029, group:null,period:7, block:'f', config:'[Rn]5f³6d¹7s²', oxStates:[+6], En:1.38, density:19.05, melt:1405.3, boil:4404, type:'actinide', discovered:1789, by:'Клапрот' },
{ Z:93, symbol:'Np', name:'Нептуний', mass:237, group:null,period:7, block:'f', config:'[Rn]5f⁴6d¹7s²', oxStates:[+5], En:1.36, density:20.25, melt:913, boil:4273, type:'actinide', discovered:1940, by:'МакМиллан' },
{ Z:94, symbol:'Pu', name:'Плутоний', mass:244, group:null,period:7, block:'f', config:'[Rn]5f⁶7s²', oxStates:[+4], En:1.28, density:19.84, melt:912.5, boil:3501, type:'actinide', discovered:1940, by:'Сиборг' },
{ Z:95, symbol:'Am', name:'Америций', mass:243, group:null,period:7, block:'f', config:'[Rn]5f⁷7s²', oxStates:[+3], En:1.3, density:13.67, melt:1449, boil:2880, type:'actinide', discovered:1944, by:'Сиборг' },
{ Z:96, symbol:'Cm', name:'Кюрий', mass:247, group:null,period:7, block:'f', config:'[Rn]5f⁷6d¹7s²', oxStates:[+3], En:1.3, density:13.51, melt:1613, boil:3383, type:'actinide', discovered:1944, by:'Сиборг' },
{ Z:97, symbol:'Bk', name:'Берклий', mass:247, group:null,period:7, block:'f', config:'[Rn]5f⁹7s²', oxStates:[+3], En:1.3, density:14.79, melt:1259, boil:null, type:'actinide', discovered:1949, by:'Сиборг' },
{ Z:98, symbol:'Cf', name:'Калифорний', mass:251, group:null,period:7, block:'f', config:'[Rn]5f¹⁰7s²', oxStates:[+3], En:1.3, density:15.1, melt:1173, boil:null, type:'actinide', discovered:1950, by:'Сиборг' },
{ Z:99, symbol:'Es', name:'Эйнштейний', mass:252, group:null,period:7, block:'f', config:'[Rn]5f¹¹7s²', oxStates:[+3], En:1.3, density:null, melt:1133, boil:null, type:'actinide', discovered:1952, by:'Гиорсо' },
{ Z:100,symbol:'Fm', name:'Фермий', mass:257, group:null,period:7, block:'f', config:'[Rn]5f¹²7s²', oxStates:[+3], En:1.3, density:null, melt:1800, boil:null, type:'actinide', discovered:1952, by:'Гиорсо' },
{ Z:101,symbol:'Md', name:'Менделевий', mass:258, group:null,period:7, block:'f', config:'[Rn]5f¹³7s²', oxStates:[+3], En:1.3, density:null, melt:1100, boil:null, type:'actinide', discovered:1955, by:'Гиорсо' },
{ Z:102,symbol:'No', name:'Нобелий', mass:259, group:null,period:7, block:'f', config:'[Rn]5f¹⁴7s²', oxStates:[+2], En:1.3, density:null, melt:1100, boil:null, type:'actinide', discovered:1958, by:'Флёров' },
{ Z:103,symbol:'Lr', name:'Лоуренсий', mass:266, group:3, period:7, block:'d', config:'[Rn]5f¹⁴7s²7p¹', oxStates:[+3], En:1.3, density:null, melt:1900, boil:null, type:'actinide', discovered:1961, by:'Гиорсо' },
{ Z:104,symbol:'Rf', name:'Резерфордий', mass:267, group:4, period:7, block:'d', config:'[Rn]5f¹⁴6d²7s²', oxStates:[+4], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1964, by:'Флёров' },
{ Z:105,symbol:'Db', name:'Дубний', mass:268, group:5, period:7, block:'d', config:'[Rn]5f¹⁴6d³7s²', oxStates:[+5], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1968, by:'Флёров' },
{ Z:106,symbol:'Sg', name:'Сиборгий', mass:269, group:6, period:7, block:'d', config:'[Rn]5f¹⁴6d⁴7s²', oxStates:[+6], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1974, by:'Флёров' },
{ Z:107,symbol:'Bh', name:'Борий', mass:270, group:7, period:7, block:'d', config:'[Rn]5f¹⁴6d⁵7s²', oxStates:[+7], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1981, by:'ГСИ' },
{ Z:108,symbol:'Hs', name:'Хассий', mass:277, group:8, period:7, block:'d', config:'[Rn]5f¹⁴6d⁶7s²', oxStates:[+8], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1984, by:'ГСИ' },
{ Z:109,symbol:'Mt', name:'Мейтнерий', mass:278, group:9, period:7, block:'d', config:'[Rn]5f¹⁴6d⁷7s²', oxStates:[null], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1982, by:'ГСИ' },
{ Z:110,symbol:'Ds', name:'Дармштадтий', mass:281, group:10, period:7, block:'d', config:'[Rn]5f¹⁴6d⁸7s²', oxStates:[null], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1994, by:'ГСИ' },
{ Z:111,symbol:'Rg', name:'Рентгений', mass:282, group:11, period:7, block:'d', config:'[Rn]5f¹⁴6d⁹7s²', oxStates:[null], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1994, by:'ГСИ' },
{ Z:112,symbol:'Cn', name:'Коперниций', mass:285, group:12, period:7, block:'d', config:'[Rn]5f¹⁴6d¹⁰7s²',oxStates:[null], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1996, by:'ГСИ' },
{ Z:113,symbol:'Nh', name:'Нихоний', mass:286, group:13, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p¹',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'posttransition',discovered:2004, by:'РИКЕН' },
{ Z:114,symbol:'Fl', name:'Флеровий', mass:289, group:14, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p²',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'posttransition',discovered:1998, by:'Флёров' },
{ Z:115,symbol:'Mc', name:'Московий', mass:290, group:15, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p³',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'posttransition',discovered:2003, by:'Флёров' },
{ Z:116,symbol:'Lv', name:'Ливерморий', mass:293, group:16, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p⁴',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'posttransition',discovered:2000, by:'Флёров' },
{ Z:117,symbol:'Ts', name:'Теннессин', mass:294, group:17, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p⁵',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'halogen', discovered:2010, by:'Флёров' },
{ Z:118,symbol:'Og', name:'Оганессон', mass:294, group:18, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p⁶',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'noble', discovered:2002, by:'Флёров' },
];
/* ── Colour palette per type ──────────────────────────────── */
const TYPE_COLORS = {
alkali: '#EF476F',
alkaline: '#FF6B35',
transition: '#7B8EF7',
posttransition:'#06D6E0',
metalloid: '#7BF5A4',
nonmetal: '#FFD166',
halogen: '#C77DFF',
noble: '#A8DADC',
lanthanide: '#9B5DE5',
actinide: '#F15BB5',
metal: '#7B8EF7',
};
const TYPE_LABELS = {
alkali: 'Щелочные металлы',
alkaline: 'Щёлочноземельные',
transition: 'Переходные металлы',
posttransition:'Постпереходные',
metalloid: 'Металлоиды',
nonmetal: 'Неметаллы',
halogen: 'Галогены',
noble: 'Благородные газы',
lanthanide: 'Лантаноиды',
actinide: 'Актиноиды',
};
const BLOCK_COLORS = {
s: '#EF476F',
p: '#06D6E0',
d: '#7B8EF7',
f: '#9B5DE5',
};
/* ── Электронные оболочки (K,L,M,N,O,P,Q) ── */
const SHELL_CAPACITY = [2, 8, 18, 32, 32, 18, 8];
function getShellFill(Z) {
const caps = SHELL_CAPACITY;
const shells = [];
let rem = Z;
for (let i = 0; i < caps.length && rem > 0; i++) {
const n = Math.min(rem, caps[i]);
shells.push(n);
rem -= n;
}
return shells;
}
/* ── Layout helpers ──────────────────────────────────────────── */
/* Standard 18-column layout: returns {col, row} for each element */
function getCell(el) {
if (el.type === 'lanthanide' && el.Z !== 57 && el.Z !== 71) {
return { col: el.Z - 57 + 3, row: 9 }; // lanthanide row
}
if (el.type === 'actinide' && el.Z !== 89 && el.Z !== 103) {
return { col: el.Z - 89 + 3, row: 10 }; // actinide row
}
const g = el.group;
const p = el.period;
if (!g) return null;
return { col: g, row: p };
}
/* ══════════════════════════════════════════════════════════════
CLASS
══════════════════════════════════════════════════════════════ */
class PeriodicTableSim {
constructor(wrap) {
this._wrap = wrap;
this._mode = 'type'; // type | block | none
this._selected = null; // element Z
this._searchQ = '';
this._highlighted = new Set(); // Zs matching search
this._propKey = 'En'; // property for chart
this._chartBy = 'period';
this._chartN = 2; // period number or group number
this._bohrZ = null; // Z for Bohr shell panel
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();
}
/* ─────────────────────────────────────────────────────
UI BUILD
───────────────────────────────────────────────────── */
_buildUI() {
this._wrap.innerHTML = '';
this._wrap.style.cssText = 'display:flex;flex-direction:column;height:100%;min-height:0;background:#0D0D1A;overflow:hidden;';
/* top toolbar */
const toolbar = document.createElement('div');
toolbar.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.08);flex-wrap:wrap;flex-shrink:0;';
toolbar.innerHTML = `
<span style="font-size:.72rem;font-weight:700;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:.07em">Режим:</span>
<button class="ptbl-mode-btn active" data-m="type">По типу</button>
<button class="ptbl-mode-btn" data-m="block">По блоку</button>
<button class="ptbl-mode-btn" data-m="none">Без подсветки</button>
<span style="margin-left:8px;font-size:.72rem;font-weight:700;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:.07em">Поиск:</span>
<input id="ptbl-search" type="text" placeholder="Символ / название / Z / масса"
style="padding:4px 8px;border-radius:6px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.06);color:#fff;font-size:.78rem;width:200px;outline:none;">
<button id="ptbl-search-clear" style="padding:3px 7px;border-radius:5px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#aaa;font-size:.72rem;cursor:pointer">
<svg class="ic" viewBox="0 0 24 24" style="width:12px;height:12px"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>`;
this._wrap.appendChild(toolbar);
/* mode buttons */
toolbar.querySelectorAll('.ptbl-mode-btn').forEach(btn => {
btn.style.cssText = 'padding:4px 10px;border-radius:6px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#aaa;font-size:.75rem;cursor:pointer;transition:all .15s';
btn.addEventListener('click', () => {
toolbar.querySelectorAll('.ptbl-mode-btn').forEach(b => { b.style.background='transparent'; b.style.color='#aaa'; b.classList.remove('active'); });
btn.style.background='rgba(155,93,229,0.25)'; btn.style.color='#fff'; btn.classList.add('active');
this._mode = btn.dataset.m;
if (window.LabFX) LabFX.sound.play('whoosh', { pitch: 1.2, volume: 0.25 });
this._colorTable();
});
});
/* set initial active style */
toolbar.querySelector('.ptbl-mode-btn.active').style.background = 'rgba(155,93,229,0.25)';
toolbar.querySelector('.ptbl-mode-btn.active').style.color = '#fff';
/* search */
const si = toolbar.querySelector('#ptbl-search');
si.addEventListener('input', () => { this._searchQ = si.value.trim().toLowerCase(); this._applySearch(); });
toolbar.querySelector('#ptbl-search-clear').addEventListener('click', () => { si.value=''; this._searchQ=''; this._applySearch(); });
/* main area: table + right panel */
const main = document.createElement('div');
main.style.cssText = 'display:flex;flex:1;min-height:0;overflow:hidden;';
this._wrap.appendChild(main);
/* left: table + legend */
const leftCol = document.createElement('div');
leftCol.style.cssText = 'flex:1;display:flex;flex-direction:column;min-width:0;overflow:auto;padding:8px 4px 8px 8px;';
main.appendChild(leftCol);
/* table grid container */
this._tableEl = document.createElement('div');
this._tableEl.id = 'ptbl-grid';
this._tableEl.style.cssText = 'display:grid;grid-template-columns:repeat(18,1fr);gap:2px;min-width:540px;';
leftCol.appendChild(this._tableEl);
/* gap filler row */
const gapDiv = document.createElement('div');
gapDiv.style.cssText = 'height:8px;';
leftCol.appendChild(gapDiv);
/* f-block (lanthanide/actinide) */
this._fblockEl = document.createElement('div');
this._fblockEl.id = 'ptbl-fblock';
this._fblockEl.style.cssText = 'display:grid;grid-template-columns:repeat(15,1fr);gap:2px;min-width:540px;margin-left:calc(2*100%/18 + 4px);';
leftCol.appendChild(this._fblockEl);
/* legend */
this._legendEl = document.createElement('div');
this._legendEl.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;min-width:540px;';
leftCol.appendChild(this._legendEl);
/* right panel */
const rightCol = document.createElement('div');
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:hidden;display:flex;flex-direction:column;font-size:.78rem;color:#ccc;';
rightCol.appendChild(this._cardEl);
/* Bohr shells canvas */
const bohrWrap = document.createElement('div');
bohrWrap.style.cssText = 'height:150px;flex-shrink:0;border-top:1px solid rgba(255,255,255,0.07);background:rgba(0,0,0,0.3);position:relative;';
this._bohrCanvas = document.createElement('canvas');
this._bohrCanvas.style.cssText = 'width:100%;height:100%;';
bohrWrap.appendChild(this._bohrCanvas);
rightCol.appendChild(bohrWrap);
/* chart panel */
const chartPan = document.createElement('div');
chartPan.style.cssText = 'flex-shrink:0;border-top:1px solid rgba(255,255,255,0.07);padding:8px;background:rgba(0,0,0,0.2);';
chartPan.innerHTML = `
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:6px;">
<span style="font-size:.7rem;color:rgba(255,255,255,0.4)">Свойство:</span>
<select id="ptbl-prop-sel" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
<option value="En">ЭО (Полинг)</option>
<option value="mass">Масса</option>
<option value="melt">T плавл. (K)</option>
<option value="boil">T кип. (K)</option>
<option value="density">Плотность</option>
</select>
<select id="ptbl-by-sel" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
<option value="period">По периоду</option>
<option value="group">По группе</option>
</select>
<select id="ptbl-n-sel" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
${[1,2,3,4,5,6,7].map(n=>`<option value="${n}" ${n===2?'selected':''}>№ ${n}</option>`).join('')}
</select>
</div>
<canvas id="ptbl-chart" style="width:100%;height:90px;display:block;"></canvas>`;
rightCol.appendChild(chartPan);
chartPan.querySelector('#ptbl-prop-sel').addEventListener('change', e => { this._propKey=e.target.value; this._drawChart(); });
chartPan.querySelector('#ptbl-by-sel').addEventListener('change', e => {
this._chartBy=e.target.value;
const nSel = chartPan.querySelector('#ptbl-n-sel');
if (this._chartBy === 'group') {
nSel.innerHTML = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18].map(n=>`<option value="${n}" ${n===1?'selected':''}>Гр. ${n}</option>`).join('');
this._chartN = 1;
} else {
nSel.innerHTML = [1,2,3,4,5,6,7].map(n=>`<option value="${n}" ${n===2?'selected':''}>№ ${n}</option>`).join('');
this._chartN = 2;
}
this._drawChart();
});
chartPan.querySelector('#ptbl-n-sel').addEventListener('change', e => { this._chartN=+e.target.value; this._drawChart(); });
this._chartCanvas = chartPan.querySelector('#ptbl-chart');
new ResizeObserver(() => this._drawChart()).observe(this._chartCanvas);
new ResizeObserver(() => this._drawBohr()).observe(this._bohrCanvas);
}
/* ─────────────────────────────────────────────────────
TABLE BUILD
───────────────────────────────────────────────────── */
_buildTable() {
/* create placeholder grid for rows 1-7 × cols 1-18 */
const cells = {}; // 'row,col' → div
for (let r = 1; r <= 7; r++) {
for (let c = 1; c <= 18; c++) {
const d = document.createElement('div');
d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
cells[`${r},${c}`] = d;
this._tableEl.appendChild(d);
}
}
/* f-block rows */
const fCells = { 9: {}, 10: {} }; // lanthanides / actinides
for (let fc = 1; fc <= 15; fc++) {
for (const fr of [9, 10]) {
const d = document.createElement('div');
d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
fCells[fr][fc] = d;
this._fblockEl.appendChild(d);
}
}
/* place elements */
this._cellMap = {}; // Z → div
for (const el of ELEMENTS) {
const pos = getCell(el);
if (!pos) continue;
let div;
if (pos.row <= 7) {
div = cells[`${pos.row},${pos.col}`];
} else {
const fCol = pos.col - 2; // 3..17 → 1..15
div = fCells[pos.row][fCol];
}
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;';
div.innerHTML = `
<span style="font-size:.55em;opacity:.7;line-height:1">${el.Z}</span>
<span style="font-size:.85em;font-weight:800;line-height:1.1">${el.symbol}</span>
<span style="font-size:.42em;opacity:.65;line-height:1.2;text-align:center;max-width:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${el.name}</span>`;
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', () => this._selectElement(el.Z));
}
this._colorTable();
this._buildLegend();
}
_colorTable() {
for (const el of ELEMENTS) {
const div = this._cellMap[el.Z];
if (!div) continue;
let bg, textC = '#fff';
if (this._mode === 'type') {
bg = TYPE_COLORS[el.type] || '#555';
} else if (this._mode === 'block') {
bg = BLOCK_COLORS[el.block] || '#555';
} else {
bg = '#2a2a3e';
}
div.style.background = bg + '33'; // 20% opacity base
div.style.border = `1px solid ${bg}88`;
div.style.color = '#fff';
if (this._highlighted.size > 0) {
if (this._highlighted.has(el.Z)) {
div.style.background = bg + 'cc';
div.style.border = `2px solid ${bg}`;
div.style.boxShadow = `0 0 8px ${bg}99`;
} else {
div.style.opacity = '0.25';
}
} else {
div.style.opacity = '1';
div.style.boxShadow = '';
}
}
this._buildLegend();
}
_buildLegend() {
this._legendEl.innerHTML = '';
const map = this._mode === 'block' ? BLOCK_COLORS : TYPE_COLORS;
const labels = this._mode === 'block' ? { s:'s-блок', p:'p-блок', d:'d-блок', f:'f-блок' } : TYPE_LABELS;
for (const [k, col] of Object.entries(map)) {
if (!labels[k]) continue;
const d = document.createElement('div');
d.style.cssText = `display:flex;align-items:center;gap:4px;font-size:.68rem;color:#bbb;cursor:pointer;`;
d.innerHTML = `<span style="width:12px;height:12px;border-radius:3px;background:${col};display:inline-block;flex-shrink:0;"></span>${labels[k]}`;
/* highlight on hover */
d.addEventListener('mouseenter', () => this._highlightType(k));
d.addEventListener('mouseleave', () => this._unhighlightType());
this._legendEl.appendChild(d);
}
}
_highlightType(key) {
for (const el of ELEMENTS) {
const div = this._cellMap[el.Z];
if (!div) continue;
const match = this._mode === 'block' ? el.block === key : el.type === key;
if (match) {
div.style.filter = 'brightness(1.5)';
div.style.transform = 'scale(1.05)';
div.style.zIndex = '5';
} else {
div.style.opacity = '0.2';
}
}
}
_unhighlightType() {
for (const el of ELEMENTS) {
const div = this._cellMap[el.Z];
if (!div) continue;
div.style.filter = '';
div.style.transform = '';
div.style.zIndex = '';
div.style.opacity = this._highlighted.has(el.Z) ? '1' : (this._highlighted.size > 0 ? '0.25' : '1');
}
}
/* ─────────────────────────────────────────────────────
SELECT / SEARCH
───────────────────────────────────────────────────── */
_selectElement(Z) {
this._selected = Z;
if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.8 + Z * 0.008, volume: 0.25 });
/* highlight selected cell */
for (const el of ELEMENTS) {
const div = this._cellMap[el.Z];
if (!div) continue;
if (el.Z === Z) {
div.style.boxShadow = 'inset 0 0 0 2px #fff';
} else {
div.style.boxShadow = '';
}
}
this._updateCard(ELEMENTS.find(e => e.Z === Z));
this._bohrZ = Z;
this._startBohr();
}
_applySearch() {
this._highlighted.clear();
if (this._searchQ) {
for (const el of ELEMENTS) {
const q = this._searchQ;
if (
el.symbol.toLowerCase() === q ||
el.name.toLowerCase().includes(q) ||
String(el.Z) === q ||
String(el.mass).startsWith(q)
) {
this._highlighted.add(el.Z);
}
}
}
this._colorTable();
/* auto-select if single result */
if (this._highlighted.size === 1) {
this._selectElement([...this._highlighted][0]);
}
}
/* ─────────────────────────────────────────────────────
ELEMENT CARD (legacy — no-selection state only)
───────────────────────────────────────────────────── */
_updateCard(el) {
if (!el) {
this._cardEl.innerHTML = `<div style="color:rgba(255,255,255,0.25);font-size:.8rem;text-align:center;padding:20px 0">Кликните на элемент</div>`;
this._bohrZ = null;
cancelAnimationFrame(this._bohrRaf);
this._bohrRaf = null;
this._drawBohr();
return;
}
this._renderCardV2(el);
}
_row(label, val) {
return `<tr>
<td style="padding:3px 4px 3px 0;color:rgba(255,255,255,0.45);border-bottom:1px solid rgba(255,255,255,0.05);white-space:nowrap">${label}</td>
<td style="padding:3px 0 3px 4px;color:#ddd;border-bottom:1px solid rgba(255,255,255,0.05)">${val}</td>
</tr>`;
}
/* ─────────────────────────────────────────────────────
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 = `
<div class="ptbl-card-v2" style="--el-col:${col}">
<button class="ptbl-card-close" aria-label="Закрыть">
<svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<div class="ptbl-hero">
<div class="ptbl-hero-z">${el.Z}</div>
<div class="ptbl-hero-sym">${el.symbol}</div>
<div class="ptbl-hero-name">${el.name}</div>
<div class="ptbl-hero-mass">${el.mass} а.е.м.</div>
<div class="ptbl-hero-badge" style="background:${col}22;border-color:${col}55;color:${col}">${TYPE_LABELS[el.type] || el.type}</div>
</div>
<div class="ptbl-tabs" role="tablist">
${this._cardTabs().map(t => `<button class="ptbl-tab${t.id === this._cardActiveTab ? ' active' : ''}" data-tab="${t.id}" role="tab">${t.label}</button>`).join('')}
</div>
<div class="ptbl-tab-body" id="ptbl-tab-body"></div>
</div>`;
/* 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.boxShadow = ''; 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 `
<div class="ptbl-overview">
${summary ? `<p class="ptbl-summary">${summary}</p>` : ''}
<div class="ptbl-quick-grid">
<div class="ptbl-quick-item">
<span class="ptbl-quick-label">Тип</span>
<span class="ptbl-quick-val" style="color:${col}">${TYPE_LABELS[el.type] || el.type}</span>
</div>
<div class="ptbl-quick-item">
<span class="ptbl-quick-label">Конфигурация</span>
<span class="ptbl-quick-val"><code>${el.config}</code></span>
</div>
<div class="ptbl-quick-item">
<span class="ptbl-quick-label">Ст. окисления</span>
<span class="ptbl-quick-val">${ox}</span>
</div>
<div class="ptbl-quick-item">
<span class="ptbl-quick-label">ЭО (Полинг)</span>
<span class="ptbl-quick-val">${fmt(el.En)}</span>
</div>
<div class="ptbl-quick-item">
<span class="ptbl-quick-label">Период / Группа</span>
<span class="ptbl-quick-val">${el.period} / ${el.group || '—'}</span>
</div>
<div class="ptbl-quick-item">
<span class="ptbl-quick-label">Блок</span>
<span class="ptbl-quick-val">${el.block}-блок</span>
</div>
</div>
</div>`;
}
/* ── Tab 2: Свойства ── */
_renderTab_properties(el) {
const fmt = v => (v !== null && v !== undefined) ? v : '—';
const rows = [
['Атомная масса, а.е.м.', fmt(el.mass)],
['Плотность, г/см³', fmt(el.density)],
['T<sub>пл</sub>, K', fmt(el.melt)],
['T<sub>кип</sub>, 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 `
<table class="ptbl-prop-table">
<tbody>
${rows.map(([l, v]) => `<tr><td>${l}</td><td>${v}</td></tr>`).join('')}
</tbody>
</table>`;
}
/* ── Tab 3: Электроника ── */
_renderTab_electronics(el) {
return `
<div class="ptbl-electronics">
<div class="ptbl-bohr-inline-wrap">
<canvas id="ptbl-bohr-inline" style="width:100%;height:160px;display:block;"></canvas>
</div>
<div class="ptbl-el-config">
<span class="ptbl-quick-label">Электронная конфигурация</span>
<code class="ptbl-el-config-code">${el.config}</code>
</div>
<div class="ptbl-el-config" style="margin-top:6px;">
<span class="ptbl-quick-label">Оболочки (K, L, M, …)</span>
<code class="ptbl-el-config-code">${getShellFill(el.Z).join(' | ')}</code>
</div>
</div>`;
}
_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 `<p class="ptbl-empty">Данные об изотопах не загружены.</p>`;
}
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 `<tr>
<td><sup>${iso.massNum ?? iso.mass}</sup>${el.symbol}</td>
<td>${typeof iso.mass === 'number' ? iso.mass.toFixed(4) : (iso.mass ?? '—')}</td>
<td>${abStr}</td>
<td>${hlStr}</td>
<td>${decStr}</td>
</tr>`;
}).join('');
return `
<div class="ptbl-isotopes">
<table class="ptbl-prop-table ptbl-iso-table">
<thead><tr><th>Изотоп</th><th>Масса</th><th>Распр.</th><th>T&#x00BD;</th><th>Распад</th></tr></thead>
<tbody>${rows}</tbody>
</table>
${avgMass != null
? `<div class="ptbl-iso-avg">Средняя атомная масса (взвеш.): <strong>${avgMass.toFixed(4)}</strong> а.е.м.</div>`
: ''}
<canvas id="ptbl-iso-chart" style="width:100%;height:70px;margin-top:8px;display:block;"></canvas>
</div>`;
}
_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 `
<div class="ptbl-history">
<div class="ptbl-timeline-card">
<div class="ptbl-timeline-year">${year || 'Древний мир'}</div>
<div class="ptbl-timeline-who">${by}</div>
${el.discoveryCountry ? `<div class="ptbl-timeline-country">${el.discoveryCountry}</div>` : ''}
</div>
${hist ? `<p class="ptbl-hist-text">${hist}</p>` : '<p class="ptbl-empty">История не указана.</p>'}
${etym ? `<div class="ptbl-etymology"><span class="ptbl-quick-label">Этимология: </span>${etym}</div>` : ''}
</div>`;
}
/* ── Tab 6: Применения ── */
_renderTab_applications(el) {
const apps = el.applications || [];
const desc = el.applicationsDescription || '';
const iconMap = {
battery: '<svg class="ic" viewBox="0 0 24 24"><rect x="2" y="7" width="18" height="10" rx="2"/><path d="M22 11v2"/><line x1="7" y1="12" x2="11" y2="12"/><line x1="9" y1="10" x2="9" y2="14"/></svg>',
medicine: '<svg class="ic" viewBox="0 0 24 24"><path d="M12 2a5 5 0 0 1 5 5v1h2a1 1 0 0 1 1 1v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9a1 1 0 0 1 1-1h2V7a5 5 0 0 1 5-5z"/><line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/></svg>',
electronics: '<svg class="ic" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><circle cx="9" cy="9" r="1"/><circle cx="15" cy="9" r="1"/><circle cx="9" cy="15" r="1"/><circle cx="15" cy="15" r="1"/></svg>',
metallurgy: '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/><line x1="12" y1="10" x2="12" y2="16"/></svg>',
construction: '<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="12" width="18" height="8" rx="1"/><path d="M3 12l4-8h10l4 8"/></svg>',
nuclear: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="9" fill="none"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="3" y1="12" x2="21" y2="12"/></svg>',
lighting: '<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 1 7 7c0 2.5-1.3 4.7-3.3 6L15 17H9l-.7-2C6.3 13.7 5 11.5 5 9a7 7 0 0 1 7-7z"/></svg>',
catalyst: '<svg class="ic" viewBox="0 0 24 24"><path d="M8 3L4 10h16L16 3z"/><path d="M4 14l4 7h8l4-7z"/></svg>',
jewelry: '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 16 9 22 9 17 14 19 21 12 17 5 21 7 14 2 9 8 9"/></svg>',
fertilizer: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/></svg>',
semiconductor: '<svg class="ic" viewBox="0 0 24 24"><rect x="7" y="7" width="10" height="10" rx="1"/><line x1="7" y1="12" x2="3" y2="12"/><line x1="17" y1="12" x2="21" y2="12"/><line x1="12" y1="7" x2="12" y2="3"/><line x1="12" y1="17" x2="12" y2="21"/></svg>',
pigment: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 3c2.5 2 4 5 4 9s-1.5 7-4 9"/></svg>',
aerospace: '<svg class="ic" viewBox="0 0 24 24"><path d="M12 2l2 6h6l-5 4 2 6-5-4-5 4 2-6-5-4h6z"/></svg>',
optical: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/></svg>',
food: '<svg class="ic" viewBox="0 0 24 24"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>',
};
const defaultIcon = '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';
if (!apps.length && !desc) {
return `<p class="ptbl-empty">Применения не указаны.</p>`;
}
const cards = apps.map(tag => {
const ico = iconMap[tag] || defaultIcon;
const label = tag.charAt(0).toUpperCase() + tag.slice(1);
return `<div class="ptbl-app-chip">${ico}<span>${label}</span></div>`;
}).join('');
return `
<div class="ptbl-applications">
${cards ? `<div class="ptbl-app-grid">${cards}</div>` : ''}
${desc ? `<p class="ptbl-app-desc">${desc}</p>` : ''}
</div>`;
}
/* ── 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 `
<div class="ptbl-biology">
<div class="ptbl-bio-badge" style="border-color:${bioColor};color:${bioColor}">${bioLabel}</div>
${role ? `<p class="ptbl-bio-role">${role}</p>` : '<p class="ptbl-empty">Биологическая роль не указана.</p>'}
${el.toxicity ? `<div class="ptbl-bio-toxicity"><span class="ptbl-quick-label">Токсичность: </span>${el.toxicity}</div>` : ''}
</div>`;
}
/* ── Tab 8: Минералы ── */
_renderTab_minerals(el) {
const mins = el.mineralForms || [];
const sources = el.mineralSources || '';
if (!mins.length && !sources) {
return `<p class="ptbl-empty">Данные о минералах не указаны.</p>`;
}
const items = mins.map(m => {
const name = typeof m === 'object' ? (m.name || '—') : m;
const formula = typeof m === 'object' ? (m.formula || '') : '';
return `<div class="ptbl-mineral-item">
<span class="ptbl-mineral-name">${name}</span>
${formula ? `<code class="ptbl-mineral-formula">${formula}</code>` : ''}
</div>`;
}).join('');
return `
<div class="ptbl-minerals">
${items ? `<div class="ptbl-mineral-list">${items}</div>` : ''}
${sources ? `<p class="ptbl-mineral-sources"><span class="ptbl-quick-label">Источники: </span>${sources}</p>` : ''}
</div>`;
}
/* ── Tab 9: Спектр ── */
_renderTab_spectrum(el) {
if (!el.spectrum || !el.spectrum.length) {
return `<p class="ptbl-empty">Спектральные данные не указаны.</p>`;
}
return `
<div class="ptbl-spectrum">
<canvas id="ptbl-spec-canvas" style="width:100%;height:80px;display:block;"></canvas>
<div class="ptbl-spec-lines-list">
${el.spectrum.map(s => {
const nm = typeof s === 'object' ? s.nm : s;
return `<span class="ptbl-spec-tag">${nm} нм</span>`;
}).join('')}
</div>
</div>`;
}
_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 `<p class="ptbl-empty">Данные об окраске пламени не указаны.</p>`;
}
return `
<div class="ptbl-flame">
<div class="ptbl-flame-swatch" style="background:${el.flameColor};box-shadow:0 0 24px ${el.flameColor}88"></div>
<div class="ptbl-flame-label">Окраска пламени: <strong>${el.flameColorName || el.flameColor}</strong></div>
<p class="ptbl-flame-desc">
При внесении соединений ${el.name} в пламя горелки оно окрашивается
в характерный цвет — применяется в качественном анализе и в пиротехнике.
</p>
</div>`;
}
/* ── 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 => `
<div class="ptbl-reaction-item">
<div class="ptbl-reaction-label">${r.label}</div>
<div class="ptbl-reaction-eq" style="color:${col}">${r.eq}</div>
</div>`).join('');
return `
<div class="ptbl-reactions">
${items}
<p class="ptbl-reaction-note">Уравнения приведены в общем виде для ознакомления.</p>
</div>`;
}
/* ─────────────────────────────────────────────────────
BOHR SHELLS ANIMATION
───────────────────────────────────────────────────── */
_startBohr() {
cancelAnimationFrame(this._bohrRaf);
this._bohrAngle = 0;
this._animBohr();
}
_animBohr() {
this._bohrAngle += 0.018;
this._drawBohr();
this._bohrRaf = requestAnimationFrame(() => this._animBohr());
}
_drawBohr() {
const canvas = this._bohrCanvas;
const dpr = window.devicePixelRatio || 1;
const W = canvas.offsetWidth || 240;
const 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;
const maxR = Math.min(W, H) * 0.44;
const nShells = shells.length;
const col = TYPE_COLORS[el.type] || '#7B8EF7';
/* nucleus */
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;
/* orbit ring */
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
ctx.lineWidth = 1;
ctx.stroke();
/* electrons */
const speed = 1 - i * 0.12; // inner shells faster
for (let e = 0; e < count; e++) {
const a = this._bohrAngle * speed + (2 * Math.PI * e) / count;
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();
}
});
/* label */
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);
}
/* ─────────────────────────────────────────────────────
PROPERTY CHART
───────────────────────────────────────────────────── */
_drawChart() {
const canvas = this._chartCanvas;
if (!canvas) return;
const dpr = window.devicePixelRatio || 1;
const W = canvas.offsetWidth || 240;
const H = canvas.offsetHeight || 90;
canvas.width = W * dpr;
canvas.height = H * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, W, H);
/* filter elements */
let els;
if (this._chartBy === 'period') {
els = ELEMENTS.filter(e => e.period === this._chartN && e.group !== null);
els.sort((a, b) => a.group - b.group);
} else {
els = ELEMENTS.filter(e => e.group === this._chartN);
els.sort((a, b) => a.period - b.period);
}
const vals = els.map(e => e[this._propKey]);
const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v));
if (validVals.length < 2) {
ctx.font = '11px Manrope,sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.textAlign = 'center';
ctx.fillText('Нет данных', W / 2, H / 2);
return;
}
const minV = Math.min(...validVals);
const maxV = Math.max(...validVals);
const pad = { t: 10, r: 8, b: 20, l: 8 };
const gW = W - pad.l - pad.r;
const gH = H - pad.t - pad.b;
/* axes */
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
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();
/* line */
ctx.beginPath();
let first = true;
const points = [];
els.forEach((el, i) => {
const v = el[this._propKey];
if (v === null || v === undefined || !isFinite(v)) return;
const x = pad.l + (i / Math.max(els.length - 1, 1)) * gW;
const y = pad.t + gH - ((v - minV) / (maxV - minV || 1)) * gH;
points.push({ x, y, el });
if (first) { ctx.moveTo(x, y); first = false; } else ctx.lineTo(x, y);
});
ctx.strokeStyle = '#9B5DE5';
ctx.lineWidth = 1.5;
ctx.stroke();
/* markers */
points.forEach(({ x, y, el }) => {
const col = TYPE_COLORS[el.type] || '#7B8EF7';
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
});
/* x labels (symbols) */
ctx.font = '8px Manrope,sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.textAlign = 'center';
/* show at most 18 labels */
const step = Math.ceil(els.length / 18);
els.forEach((el, i) => {
if (i % step === 0) {
const x = pad.l + (i / Math.max(els.length - 1, 1)) * gW;
ctx.fillText(el.symbol, x, H - 4);
}
});
}
/* ─────────────────────────────────────────────────────
LIFECYCLE
───────────────────────────────────────────────────── */
stop() {
cancelAnimationFrame(this._bohrRaf);
this._bohrRaf = null;
}
}
/* ══════════════════════════════════════════════════════════════
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 = `
<span style="font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">Тепловая карта:</span>
<button id="ptbl-heat-toggle" class="ptbl-vm-btn" title="Тепловая карта">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
Окрасить
</button>
<select id="ptbl-heat-prop" style="display:none;background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
${HEATMAP_PROPS.map(p => `<option value="${p.key}"${p.key==='En'?' selected':''}>${p.label}</option>`).join('')}
</select>
<button id="ptbl-heat-scale" class="ptbl-vm-btn" style="display:none" title="Линейная/Лог шкала">Lin</button>
<div id="ptbl-heat-legend" style="display:none;flex-shrink:0;"></div>
<span style="margin-left:4px;font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">Вид:</span>
<select id="ptbl-shape-sel" class="ptbl-vm-btn" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:3px 6px;font-size:.72rem;cursor:pointer;">
<option value="std">Стандартная</option>
<option value="long">Длинная (32 кол.)</option>
<option value="short">Краткая (8 гр.)</option>
</select>
<span style="margin-left:4px;font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">Тренды:</span>
<button id="ptbl-trend-toggle" class="ptbl-vm-btn" title="Показать тренды">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width:13px;height:13px">
<line x1="5" y1="19" x2="19" y2="5"/><polyline points="9 5 19 5 19 15"/>
</svg>
Тренды
</button>
<select id="ptbl-trend-prop" style="display:none;background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
${Object.entries(TREND_CONFIG).map(([k,v]) => `<option value="${k}">${v.label}</option>`).join('')}
</select>
<span style="margin-left:4px;font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">3D:</span>
<button id="ptbl-3d-toggle" class="ptbl-vm-btn" title="3D-вид">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px">
<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
</svg>
3D
</button>
<select id="ptbl-3d-mode" style="display:none;background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
<option value="bar">Bar</option>
<option value="wave">Wave</option>
<option value="stack">Stack</option>
</select>
`;
/* 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 = `
<span style="font-size:.65rem;color:rgba(255,255,255,0.45);white-space:nowrap">${propDef.unit ? minV.toFixed(1) + ' ' + propDef.unit : minV.toFixed(1)}</span>
<canvas id="ptbl-heat-grad" width="${W}" height="${H}" style="border-radius:3px;margin:0 4px;"></canvas>
<span style="font-size:.65rem;color:rgba(255,255,255,0.45);white-space:nowrap">${maxV.toFixed(1)}</span>
`;
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 = `<b>${elObj.symbol}</b> — ${elObj.name}<br>${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 = `
<span style="font-size:.55em;opacity:.7;line-height:1">${el.Z}</span>
<span style="font-size:.85em;font-weight:800;line-height:1.1">${el.symbol}</span>
<span style="font-size:.42em;opacity:.65;line-height:1.2;text-align:center;max-width:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${el.name}</span>`;
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 = `
<span style="font-size:.55em;opacity:.7;line-height:1">${el.Z}</span>
<span style="font-size:.85em;font-weight:800;line-height:1.1">${el.symbol}</span>
<span style="font-size:.42em;opacity:.65;line-height:1.2;text-align:center;max-width:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${el.name}</span>`;
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 = `<b style="color:#9B5DE5">${e.subLabel} ${e.mlLabel}</b><br>n = ${e.n}<br>l = ${e.l} (${lName})<br>m<sub>l</sub> = ${e.ml}<br>m<sub>s</sub> = ${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 = `<div style="color:#9B5DE5;font-weight:700;margin-bottom:5px;font-size:.7rem">Переход из n=${n1idx+1}:</div>`;
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} &rarr; ${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);
if (!div.parentNode) continue;
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 = '<div class="ptbl-bin-hint" style="color:rgba(255,255,255,0.36);font-size:.7rem;">Кликните первый элемент (реагент A)</div>' +
'<div class="ptbl-bin-result" style="margin-top:6px;"></div>';
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 ? '<sub>' + nPos + '</sub>' : '') +
negEl.symbol + (nNeg > 1 ? '<sub>' + nNeg + '</sub>' : '');
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 '<span style="color:rgba(255,255,255,0.28);font-style:italic;font-size:.7rem;">Соединение не образуется (одинаковые знаки ст. окисления)</span>';
}
var html = '<div style="margin-bottom:5px;">' +
'<span style="color:rgba(255,255,255,0.42);font-size:.69rem;">Тип связи: </span>' +
'<span style="color:' + bondColor + ';font-weight:700;font-size:.73rem;">' + bondType + '</span>';
if (elA.En && elB.En) html += '<span style="color:rgba(255,255,255,0.28);font-size:.66rem;margin-left:6px;">ΔЭО = ' + dEN.toFixed(2) + '</span>';
html += '</div><div style="display:flex;flex-wrap:wrap;gap:5px;">';
formulas.forEach(function(f) {
html += '<div style="background:rgba(155,93,229,0.13);border:1px solid rgba(155,93,229,0.36);border-radius:5px;padding:4px 8px;">' +
'<div style="font-weight:700;color:#fff;font-size:.8rem;">' + f.formula + '</div>' +
'<div style="font-size:.64rem;color:rgba(255,255,255,0.38);margin-top:1px;">' + f.struct + '</div></div>';
});
html += '</div>';
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 = '<div style="display:flex;align-items:center;gap:7px;margin-bottom:6px;flex-wrap:wrap;">' +
'<span style="color:rgba(255,255,255,0.35);font-size:.7rem;">Выберите до 4 элементов</span>' +
'<button class="ptbl-cmp-clear" style="padding:2px 8px;border-radius:4px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#777;font-size:.67rem;cursor:pointer;">Очистить</button>' +
'<span style="font-size:.67rem;color:rgba(255,255,255,0.28);">График:</span>' +
'<select class="ptbl-cmp-prop" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.13);color:#ccc;border-radius:4px;padding:2px 4px;font-size:.67rem;">' +
'<option value="En">ЭО</option><option value="mass">Масса</option>' +
'<option value="density">Плотность</option><option value="melt">T пл.</option><option value="boil">T кип.</option>' +
'</select></div>' +
'<div class="ptbl-cmp-table"></div>' +
'<canvas class="ptbl-cmp-chart" style="width:100%;height:52px;display:block;margin-top:5px;"></canvas>';
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 = '<div style="color:rgba(255,255,255,0.2);font-size:.68rem;">Кликайте элементы на таблице</div>';
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 = '<table style="border-collapse:collapse;font-size:.69rem;"><tr><th style="padding:2px 5px;color:rgba(255,255,255,0.32);border-bottom:1px solid rgba(255,255,255,0.07);text-align:left;">Свойство</th>';
els.forEach(function(el) {
var col = TYPE_COLORS[el.type] || '#888';
html += '<th style="padding:2px 5px;color:' + col + ';border-bottom:1px solid rgba(255,255,255,0.07);text-align:center;">' + el.symbol + '<br><span style="font-size:.58rem;opacity:.5">' + el.name + '</span></th>';
});
html += '</tr>';
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 += '<tr><td style="padding:2px 5px;color:rgba(255,255,255,0.38);border-bottom:1px solid rgba(255,255,255,0.04);white-space:nowrap;">' + p.label + '</td>';
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 += '<td style="padding:2px 5px;text-align:center;border-bottom:1px solid rgba(255,255,255,0.04);' + ex + '">' + fmt(v) + '</td>';
});
html += '</tr>';
});
html += '</table>';
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 = '<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:6px;">';
['active','medium','low'].forEach(function(g) {
legHtml += '<div style="display:flex;align-items:center;gap:3px;font-size:.65rem;color:' + GC[g].c + ';">' +
'<span style="width:8px;height:8px;border-radius:2px;background:' + GC[g].bd + ';display:inline-block;"></span>' + GC[g].lbl + '</div>';
});
legHtml += '</div>';
var rowHtml = '<div style="display:flex;align-items:center;gap:2px;flex-wrap:wrap;">';
SERIES.forEach(function(item, i) {
if (item.g === 'sep') {
rowHtml += '<div style="display:flex;flex-direction:column;align-items:center;margin:0 3px;">' +
'<div style="width:1px;height:28px;background:rgba(255,255,255,0.18);"></div>' +
'<span style="font-size:.58rem;color:rgba(255,255,255,0.35);margin-top:1px;">H</span></div>';
return;
}
if (i > 0 && SERIES[i-1].g !== 'sep') rowHtml += '<div style="color:rgba(255,255,255,0.15);font-size:.72rem;align-self:center;"></div>';
var gc = GC[item.g];
var el = ELEMENTS.find(function(e) { return e.symbol === item.s; });
var name = el ? el.name : item.s;
rowHtml += '<div class="ptbl-act-item" data-sym="' + item.s + '" title="' + item.t + '" ' +
'style="min-width:24px;padding:3px 4px;border-radius:4px;background:' + gc.bg + ';border:1px solid ' + gc.bd + ';text-align:center;cursor:pointer;transition:all .12s;">' +
'<div style="font-size:.74rem;font-weight:800;color:' + gc.c + ';line-height:1.1;">' + item.s + '</div>' +
'<div style="font-size:.5rem;color:rgba(255,255,255,0.32);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:34px;">' + name + '</div></div>';
});
rowHtml += '</div>';
panel.innerHTML = legHtml + rowHtml + '<div class="ptbl-act-tip" style="margin-top:5px;font-size:.68rem;color:rgba(255,255,255,0.42);min-height:14px;"></div>';
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 = '<div style="color:rgba(255,255,255,0.32);font-size:.68rem;margin-bottom:5px;">' +
'Таблица Менделеева 1869: 63 известных элемента. Фиол. «?» — предсказания Менделеева. Кликните «?».' +
'</div>' +
'<div class="ptbl-m1869-popup" style="display:none;position:absolute;top:8px;right:8px;width:230px;background:#1a1a2e;border:1px solid rgba(155,93,229,0.48);border-radius:8px;padding:10px;font-size:.7rem;z-index:20;box-shadow:0 4px 16px rgba(0,0,0,0.6);"></div>';
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 = '<span style="font-size:.76em;font-weight:900;color:rgba(155,93,229,0.88);">?</span>' +
'<span style="font-size:.5em;color:rgba(155,93,229,0.5);margin-top:1px;">' + el.symbol + '</span>';
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 '<span style="color:' + (good ? '#7BF5A4' : '#FFD166') + ';font-size:.62rem;margin-left:3px;">(' + (good ? 'точно' : pct + '% откл.') + ')</span>';
};
popup.innerHTML = '<div style="font-weight:700;color:#9B5DE5;margin-bottom:5px;">' + pred.name + '</div>' +
'<table style="width:100%;border-collapse:collapse;font-size:.68rem;">' +
'<tr><th style="text-align:left;color:rgba(255,255,255,0.32);padding:2px 3px;border-bottom:1px solid rgba(255,255,255,0.07);">Свойство</th>' +
'<th style="text-align:center;color:#FFD166;padding:2px 3px;border-bottom:1px solid rgba(255,255,255,0.07);">Предсказано</th>' +
'<th style="text-align:center;color:#7BF5A4;padding:2px 3px;border-bottom:1px solid rgba(255,255,255,0.07);">Реально</th></tr>' +
'<tr><td style="padding:2px 3px;color:rgba(255,255,255,0.42);">Масса</td>' +
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.pred.mass) + '</td>' +
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.act.mass) + diff(pred.pred.mass, pred.act.mass) + '</td></tr>' +
'<tr><td style="padding:2px 3px;color:rgba(255,255,255,0.42);">Плотность</td>' +
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.pred.density) + '</td>' +
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.act.density) + diff(pred.pred.density, pred.act.density) + '</td></tr>' +
'<tr><td style="padding:2px 3px;color:rgba(255,255,255,0.42);">T пл. (K)</td>' +
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.pred.melt) + '</td>' +
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.act.melt) + '</td></tr>' +
'</table>' +
'<div style="margin-top:5px;font-size:.64rem;color:rgba(255,255,255,0.32);">Открыт: ' + pred.year + ' г., ' + pred.who + '</div>' +
'<button onclick="this.closest(&quot;.ptbl-m1869-popup&quot;).style.display=&quot;none&quot;" ' +
'style="margin-top:5px;padding:2px 7px;border-radius:4px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#777;font-size:.65rem;cursor:pointer;">Закрыть</button>';
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 = '<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;">' +
'<span style="font-size:.68rem;color:rgba(255,255,255,0.32);">Год:</span>' +
'<input type="range" class="ptbl-tl-slider" min="' + MIN_Y + '" max="' + MAX_Y + '" value="' + MAX_Y + '" style="flex:1;min-width:90px;accent-color:#9B5DE5;">' +
'<span class="ptbl-tl-year" style="font-size:.8rem;font-weight:700;color:#9B5DE5;min-width:34px;">' + MAX_Y + '</span>' +
'<button class="ptbl-tl-play" style="padding:3px 8px;border-radius:5px;border:1px solid rgba(155,93,229,0.36);background:rgba(155,93,229,0.12);color:#9B5DE5;font-size:.68rem;cursor:pointer;display:inline-flex;align-items:center;gap:3px;">' +
'<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><polygon points="5,3 19,12 5,21"/></svg>Авто' +
'</button>' +
'<span class="ptbl-tl-count" style="font-size:.68rem;color:rgba(255,255,255,0.32);">Открыто 0 / 118</span>' +
'</div>' +
'<div class="ptbl-tl-info" style="font-size:.68rem;color:#FFD166;margin-top:3px;min-height:13px;"></div>';
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 = '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><polygon points="5,3 19,12 5,21"/></svg>Авто';
} else {
if (+slider.value >= MAX_Y) slider.value = MIN_Y;
st.playing = true;
playBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>Стоп';
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 = '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><polygon points="5,3 19,12 5,21"/></svg>Авто';
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() {
document.getElementById('sim-periodic').style.display = 'flex';
if (!periodicSim) {
periodicSim = new PeriodicTableSim(document.getElementById('periodic-wrap'));
}
}