'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 = `
Режим:
По типу
По блоку
Без подсветки
Поиск:
`;
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 = `
Свойство:
ЭО (Полинг)
Масса
T плавл. (K)
T кип. (K)
Плотность
По периоду
По группе
${[1,2,3,4,5,6,7].map(n=>`№ ${n} `).join('')}
`;
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=>`Гр. ${n} `).join('');
this._chartN = 1;
} else {
nSel.innerHTML = [1,2,3,4,5,6,7].map(n=>`№ ${n} `).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 = `
${el.Z}
${el.symbol}
${el.name} `;
div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
div.addEventListener('click', () => 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 = ` ${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 = `Кликните на элемент
`;
this._bohrZ = null;
cancelAnimationFrame(this._bohrRaf);
this._bohrRaf = null;
this._drawBohr();
return;
}
this._renderCardV2(el);
}
_row(label, val) {
return `
${label}
${val}
`;
}
/* ─────────────────────────────────────────────────────
TABBED CARD V2
───────────────────────────────────────────────────── */
/* Tab definitions: id, label */
_cardTabs() {
return [
{ id: 'overview', label: 'Обзор' },
{ id: 'properties', label: 'Свойства' },
{ id: 'electronics', label: 'Электроника' },
{ id: 'isotopes', label: 'Изотопы' },
{ id: 'history', label: 'История' },
{ id: 'applications', label: 'Применения' },
{ id: 'biology', label: 'Биология' },
{ id: 'minerals', label: 'Минералы' },
{ id: 'spectrum', label: 'Спектр' },
{ id: 'flame', label: 'Пламя' },
{ id: 'reactions', label: 'Реакции' },
];
}
_renderCardV2(el) {
const col = TYPE_COLORS[el.type] || '#888';
this._cardActiveTab = this._cardActiveTab || 'overview';
/* build outer shell */
this._cardEl.innerHTML = `
${el.Z}
${el.symbol}
${el.name}
${el.mass} а.е.м.
${TYPE_LABELS[el.type] || el.type}
${this._cardTabs().map(t => `${t.label} `).join('')}
`;
/* close button */
this._cardEl.querySelector('.ptbl-card-close').addEventListener('click', () => {
this._updateCard(null);
for (const e2 of ELEMENTS) {
const div = this._cellMap[e2.Z];
if (div) { div.style.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 `
${summary ? `
${summary}
` : ''}
Тип
${TYPE_LABELS[el.type] || el.type}
Конфигурация
${el.config}
Ст. окисления
${ox}
ЭО (Полинг)
${fmt(el.En)}
Период / Группа
${el.period} / ${el.group || '—'}
Блок
${el.block}-блок
`;
}
/* ── Tab 2: Свойства ── */
_renderTab_properties(el) {
const fmt = v => (v !== null && v !== undefined) ? v : '—';
const rows = [
['Атомная масса, а.е.м.', fmt(el.mass)],
['Плотность, г/см³', fmt(el.density)],
['Tпл , K', fmt(el.melt)],
['Tкип , K', fmt(el.boil)],
['ЭО (Полинг)', fmt(el.En)],
['Атомный радиус, пм', el.radius?.atomic ?? '—'],
['Ковалентный радиус, пм',el.radius?.covalent ?? '—'],
['Ионный радиус, пм', el.radius?.ionic ?? '—'],
['1-я эн. ионизации, эВ', el.ionization?.[0] ?? '—'],
['2-я эн. ионизации, эВ', el.ionization?.[1] ?? '—'],
['Электронное сродство', el.electronAffinity ?? '—'],
['Теплоёмкость, Дж/(г·K)',el.heatCapacity ?? '—'],
['Теплопроводность', el.thermalConductivity ?? '—'],
['Электросопротивление', el.electricalResistivity ?? '—'],
['Кристаллич. структура', el.crystalStructure ?? '—'],
['Параметр решётки, пм', el.latticeParam ?? '—'],
['Открыт', el.discovered ? `${el.discovered}, ${el.by}` : (el.by || '—')],
];
return `
${rows.map(([l, v]) => `${l} ${v} `).join('')}
`;
}
/* ── Tab 3: Электроника ── */
_renderTab_electronics(el) {
return `
Электронная конфигурация
${el.config}
Оболочки (K, L, M, …)
${getShellFill(el.Z).join(' | ')}
`;
}
_postRenderElectronics(el) {
const canvas = this._cardEl.querySelector('#ptbl-bohr-inline');
if (!canvas) return;
const dpr = window.devicePixelRatio || 1;
const W = canvas.offsetWidth || 220;
const H = 160;
canvas.width = W * dpr;
canvas.height = H * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, W, H);
const shells = getShellFill(el.Z);
const col = TYPE_COLORS[el.type] || '#7B8EF7';
const cx = W / 2, cy = H / 2;
const maxR = Math.min(W, H) * 0.44;
const nShells = shells.length;
ctx.beginPath();
ctx.arc(cx, cy, nShells > 0 ? 5 + nShells * 1.5 : 6, 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
shells.forEach((count, i) => {
const r = maxR * (i + 1) / nShells;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
ctx.lineWidth = 1;
ctx.stroke();
for (let e = 0; e < count; e++) {
const a = (2 * Math.PI * e) / count - Math.PI / 2;
const ex = cx + r * Math.cos(a);
const ey = cy + r * Math.sin(a);
ctx.beginPath();
ctx.arc(ex, ey, 2.5, 0, Math.PI * 2);
ctx.fillStyle = '#06D6E0';
ctx.fill();
}
});
ctx.font = `700 10px Manrope,sans-serif`;
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.textAlign = 'center';
ctx.fillText(shells.join(','), cx, H - 4);
}
/* ── Tab 4: Изотопы ── */
_renderTab_isotopes(el) {
const isos = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || [];
if (!isos.length) {
return `Данные об изотопах не загружены.
`;
}
let avgMass = null;
const stableIsos = isos.filter(iso => iso.abundance != null && iso.abundance > 0);
if (stableIsos.length) {
const total = stableIsos.reduce((s, iso) => s + iso.abundance, 0);
avgMass = stableIsos.reduce((s, iso) => s + iso.mass * iso.abundance, 0) / total;
}
const rows = isos.map(iso => {
const abStr = iso.abundance != null ? (iso.abundance * 100).toFixed(2) + '%' : '—';
const hlStr = iso.halfLife || '—';
const decStr = iso.decay || '—';
return `
${iso.massNum ?? iso.mass} ${el.symbol}
${typeof iso.mass === 'number' ? iso.mass.toFixed(4) : (iso.mass ?? '—')}
${abStr}
${hlStr}
${decStr}
`;
}).join('');
return `
Изотоп Масса Распр. T½ Распад
${rows}
${avgMass != null
? `
Средняя атомная масса (взвеш.): ${avgMass.toFixed(4)} а.е.м.
`
: ''}
`;
}
_postRenderIsotopesChart(el) {
const canvas = this._cardEl.querySelector('#ptbl-iso-chart');
if (!canvas) return;
const isos = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || [];
const stable = isos.filter(iso => iso.abundance != null && iso.abundance > 0);
if (!stable.length) return;
const dpr = window.devicePixelRatio || 1;
const W = canvas.offsetWidth || 220;
const H = 70;
canvas.width = W * dpr;
canvas.height = H * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, W, H);
const col = TYPE_COLORS[el.type] || '#7B8EF7';
const pad = { t: 4, r: 4, b: 18, l: 4 };
const gW = W - pad.l - pad.r;
const gH = H - pad.t - pad.b;
const n = stable.length;
const bw = Math.max(2, (gW / n) - 2);
const maxA = Math.max(...stable.map(iso => iso.abundance));
stable.forEach((iso, i) => {
const bh = (iso.abundance / maxA) * gH;
const x = pad.l + i * (gW / n) + (gW / n - bw) / 2;
const y = pad.t + gH - bh;
ctx.fillStyle = col + 'bb';
ctx.fillRect(x, y, bw, bh);
ctx.font = '8px Manrope,sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.textAlign = 'center';
ctx.fillText(String(iso.massNum ?? ''), x + bw / 2, H - 4);
});
}
/* ── Tab 5: История ── */
_renderTab_history(el) {
const year = el.discovered ? String(el.discovered) : null;
const by = el.by || '—';
const hist = el.historyText || '';
const etym = el.etymology || '';
return `
${year || 'Древний мир'}
${by}
${el.discoveryCountry ? `
${el.discoveryCountry}
` : ''}
${hist ? `
${hist}
` : '
История не указана.
'}
${etym ? `
Этимология: ${etym}
` : ''}
`;
}
/* ── Tab 6: Применения ── */
_renderTab_applications(el) {
const apps = el.applications || [];
const desc = el.applicationsDescription || '';
const iconMap = {
battery: ' ',
medicine: ' ',
electronics: ' ',
metallurgy: ' ',
construction: ' ',
nuclear: ' ',
lighting: ' ',
catalyst: ' ',
jewelry: ' ',
fertilizer: ' ',
semiconductor: ' ',
pigment: ' ',
aerospace: ' ',
optical: ' ',
food: ' ',
};
const defaultIcon = ' ';
if (!apps.length && !desc) {
return `Применения не указаны.
`;
}
const cards = apps.map(tag => {
const ico = iconMap[tag] || defaultIcon;
const label = tag.charAt(0).toUpperCase() + tag.slice(1);
return `${ico}${label}
`;
}).join('');
return `
${cards ? `
${cards}
` : ''}
${desc ? `
${desc}
` : ''}
`;
}
/* ── Tab 7: Биология ── */
_renderTab_biology(el) {
const bio = el.biological || null;
const role = el.biologicalRole || '';
const bioLabels = {
macro: 'Макроэлемент (жизненно важен)',
micro: 'Микроэлемент (жизненно важен)',
trace: 'Следовой элемент',
toxic: 'Токсичен для живых организмов',
inert: 'Биологически инертен',
radioactive: 'Радиоактивен / опасен',
};
const bioColors = {
macro: '#06D6E0', micro: '#7BF5A4', trace: '#FFD166',
toxic: '#EF476F', inert: '#888', radioactive: '#F15BB5',
};
const bioColor = bio ? (bioColors[bio] || '#888') : '#888';
const bioLabel = bio ? (bioLabels[bio] || bio) : 'Нет данных';
return `
${bioLabel}
${role ? `
${role}
` : '
Биологическая роль не указана.
'}
${el.toxicity ? `
Токсичность: ${el.toxicity}
` : ''}
`;
}
/* ── Tab 8: Минералы ── */
_renderTab_minerals(el) {
const mins = el.mineralForms || [];
const sources = el.mineralSources || '';
if (!mins.length && !sources) {
return `Данные о минералах не указаны.
`;
}
const items = mins.map(m => {
const name = typeof m === 'object' ? (m.name || '—') : m;
const formula = typeof m === 'object' ? (m.formula || '') : '';
return `
${name}
${formula ? `${formula}` : ''}
`;
}).join('');
return `
${items ? `
${items}
` : ''}
${sources ? `
Источники: ${sources}
` : ''}
`;
}
/* ── Tab 9: Спектр ── */
_renderTab_spectrum(el) {
if (!el.spectrum || !el.spectrum.length) {
return `Спектральные данные не указаны.
`;
}
return `
${el.spectrum.map(s => {
const nm = typeof s === 'object' ? s.nm : s;
return `${nm} нм `;
}).join('')}
`;
}
_postRenderSpectrum(el) {
const canvas = this._cardEl.querySelector('#ptbl-spec-canvas');
if (!canvas || !el.spectrum || !el.spectrum.length) return;
const dpr = window.devicePixelRatio || 1;
const W = canvas.offsetWidth || 220;
const H = 80;
canvas.width = W * dpr;
canvas.height = H * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
/* rainbow background 380-780 nm */
const grad = ctx.createLinearGradient(0, 0, W, 0);
grad.addColorStop(0, '#6600ff');
grad.addColorStop(0.10, '#4400ff');
grad.addColorStop(0.20, '#0000ff');
grad.addColorStop(0.30, '#00aaff');
grad.addColorStop(0.40, '#00ffcc');
grad.addColorStop(0.50, '#00ff00');
grad.addColorStop(0.60, '#aaff00');
grad.addColorStop(0.70, '#ffff00');
grad.addColorStop(0.80, '#ff8800');
grad.addColorStop(0.90, '#ff2200');
grad.addColorStop(1.0, '#880000');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.fillRect(0, 0, W, H);
el.spectrum.forEach(s => {
const nm = typeof s === 'object' ? s.nm : s;
const int = typeof s === 'object' ? (s.intensity ?? 1) : 1;
if (!nm || nm < 380 || nm > 780) return;
const x = ((nm - 380) / 400) * W;
const col = this._nmToRGB(nm);
ctx.strokeStyle = col;
ctx.lineWidth = Math.max(1, int * 2);
ctx.globalAlpha = 0.7 + int * 0.3;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, H - 18);
ctx.stroke();
ctx.globalAlpha = 1;
ctx.font = '7px Manrope,sans-serif';
ctx.fillStyle = col;
ctx.textAlign = 'center';
ctx.fillText(String(nm), x, H - 4);
});
}
_nmToRGB(nm) {
if (nm < 380) return '#8800ff';
if (nm < 440) return `hsl(${270 + (nm - 380) * 0.5},100%,60%)`;
if (nm < 490) return `hsl(${240 - (nm - 440) * 1.2},100%,55%)`;
if (nm < 510) return `hsl(${180 - (nm - 490)},100%,50%)`;
if (nm < 580) return `hsl(${120 - (nm - 510) * 0.7},100%,45%)`;
if (nm < 645) return `hsl(${60 - (nm - 580) * 0.92},100%,50%)`;
return `hsl(0,100%,${Math.max(20, 50 - (nm - 645) * 0.4)}%)`;
}
/* ── Tab 10: Пламя ── */
_renderTab_flame(el) {
if (!el.flameColor) {
return `Данные об окраске пламени не указаны.
`;
}
return `
Окраска пламени: ${el.flameColorName || el.flameColor}
При внесении соединений ${el.name} в пламя горелки оно окрашивается
в характерный цвет — применяется в качественном анализе и в пиротехнике.
`;
}
/* ── Tab 11: Реакции ── */
_renderTab_reactions(el) {
const col = TYPE_COLORS[el.type] || '#888';
const reactionsByType = {
alkali: [
{ label: 'С водой', eq: `2${el.symbol} + 2H₂O → 2${el.symbol}OH + H₂↑` },
{ label: 'С кислородом', eq: `4${el.symbol} + O₂ → 2${el.symbol}₂O` },
{ label: 'С хлором', eq: `2${el.symbol} + Cl₂ → 2${el.symbol}Cl` },
],
alkaline: [
{ label: 'С водой', eq: `${el.symbol} + 2H₂O → ${el.symbol}(OH)₂ + H₂↑` },
{ label: 'С кислородом', eq: `2${el.symbol} + O₂ → 2${el.symbol}O` },
{ label: 'С кислотой', eq: `${el.symbol} + 2HCl → ${el.symbol}Cl₂ + H₂↑` },
],
halogen: [
{ label: 'С натрием', eq: `2Na + ${el.symbol}₂ → 2Na${el.symbol}` },
{ label: 'С водородом', eq: `H₂ + ${el.symbol}₂ → 2H${el.symbol}` },
{ label: 'С водой', eq: `${el.symbol}₂ + H₂O → H${el.symbol} + H${el.symbol}O` },
],
transition: [
{ label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` },
{ label: 'С кислотой', eq: `${el.symbol} + H₂SO₄ → ${el.symbol}SO₄ + H₂↑` },
],
nonmetal: [
{ label: 'С металлом', eq: `Me + ${el.symbol} → Me–${el.symbol}` },
{ label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` },
],
};
const reactions = reactionsByType[el.type] || [
{ label: 'Реакции', eq: `${el.symbol} + реагент → продукт` },
];
const items = reactions.map(r => `
`).join('');
return `
${items}
Уравнения приведены в общем виде для ознакомления.
`;
}
/* ─────────────────────────────────────────────────────
BOHR SHELLS ANIMATION
───────────────────────────────────────────────────── */
_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 = `
Тепловая карта:
Окрасить
${HEATMAP_PROPS.map(p => `${p.label} `).join('')}
Lin
Вид:
Стандартная
Длинная (32 кол.)
Краткая (8 гр.)
Тренды:
Тренды
${Object.entries(TREND_CONFIG).map(([k,v]) => `${v.label} `).join('')}
3D:
3D
Bar
Wave
Stack
`;
/* style vm buttons */
const vmStyle = 'padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,0.12);background:transparent;color:#aaa;font-size:.72rem;cursor:pointer;display:inline-flex;align-items:center;gap:4px;transition:all .15s;';
bar.querySelectorAll('.ptbl-vm-btn').forEach(b => { b.style.cssText = vmStyle; });
/* insert bar after toolbar (first child of wrap) */
const toolbar = this._wrap.querySelector('div');
this._wrap.insertBefore(bar, toolbar.nextSibling);
/* trend canvas overlay */
this._trendCanvas = document.createElement('canvas');
this._trendCanvas.id = 'ptbl-trend-canvas';
this._trendCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0;transition:opacity .3s;';
const tableGrid = this._tableEl;
const tableParent = tableGrid.parentElement;
tableParent.style.position = 'relative';
tableParent.appendChild(this._trendCanvas);
/* 3D canvas */
this._canvas3d = document.createElement('canvas');
this._canvas3d.id = 'ptbl-3d-canvas';
this._canvas3d.style.cssText = 'display:none;flex:1;min-height:0;background:#0D0D1A;';
/* insert into main area beside table */
const mainArea = this._tableEl.closest('div[style*="flex:1"]') || tableParent;
mainArea.parentElement.insertBefore(this._canvas3d, mainArea);
/* ── wire events ── */
/* heatmap toggle */
bar.querySelector('#ptbl-heat-toggle').addEventListener('click', () => {
self._heatActive = !self._heatActive;
bar.querySelector('#ptbl-heat-toggle').style.background = self._heatActive ? 'rgba(155,93,229,0.3)' : 'transparent';
bar.querySelector('#ptbl-heat-toggle').style.color = self._heatActive ? '#fff' : '#aaa';
bar.querySelector('#ptbl-heat-prop').style.display = self._heatActive ? '' : 'none';
bar.querySelector('#ptbl-heat-scale').style.display = self._heatActive ? '' : 'none';
bar.querySelector('#ptbl-heat-legend').style.display = self._heatActive ? 'flex' : 'none';
if (self._heatActive) {
self._drawHeatmap(true);
} else {
self._colorTable();
bar.querySelector('#ptbl-heat-legend').innerHTML = '';
}
});
bar.querySelector('#ptbl-heat-prop').addEventListener('change', e => {
self._heatProp = e.target.value;
if (self._heatActive) self._drawHeatmap(true);
});
bar.querySelector('#ptbl-heat-scale').addEventListener('click', () => {
self._heatLog = !self._heatLog;
bar.querySelector('#ptbl-heat-scale').textContent = self._heatLog ? 'Log' : 'Lin';
if (self._heatActive) self._drawHeatmap(true);
});
/* shape selector */
bar.querySelector('#ptbl-shape-sel').addEventListener('change', e => {
self._tableShape = e.target.value;
self._morphToView(e.target.value);
});
/* trend toggle */
bar.querySelector('#ptbl-trend-toggle').addEventListener('click', () => {
self._trendOn = !self._trendOn;
bar.querySelector('#ptbl-trend-toggle').style.background = self._trendOn ? 'rgba(155,93,229,0.3)' : 'transparent';
bar.querySelector('#ptbl-trend-toggle').style.color = self._trendOn ? '#fff' : '#aaa';
bar.querySelector('#ptbl-trend-prop').style.display = self._trendOn ? '' : 'none';
self._trendCanvas.style.opacity = self._trendOn ? '1' : '0';
if (self._trendOn) self._drawTrendArrows();
});
bar.querySelector('#ptbl-trend-prop').addEventListener('change', e => {
self._trendProp = e.target.value;
if (self._trendOn) self._drawTrendArrows();
});
/* 3D toggle */
bar.querySelector('#ptbl-3d-toggle').addEventListener('click', () => {
if (typeof THREE === 'undefined') {
bar.querySelector('#ptbl-3d-toggle').textContent = '3D (нет Three.js)';
return;
}
self._3dActive = !self._3dActive;
bar.querySelector('#ptbl-3d-toggle').style.background = self._3dActive ? 'rgba(155,93,229,0.3)' : 'transparent';
bar.querySelector('#ptbl-3d-toggle').style.color = self._3dActive ? '#fff' : '#aaa';
bar.querySelector('#ptbl-3d-mode').style.display = self._3dActive ? '' : 'none';
if (self._3dActive) {
mainArea.style.display = 'none';
self._canvas3d.style.display = 'block';
self._init3DTable();
} else {
mainArea.style.display = '';
self._canvas3d.style.display = 'none';
if (self._3dRaf) { cancelAnimationFrame(self._3dRaf); self._3dRaf = null; }
if (self._3dRenderer) { self._3dRenderer.dispose(); self._3dRenderer = null; }
}
});
bar.querySelector('#ptbl-3d-mode').addEventListener('change', e => {
self._3dMode = e.target.value;
if (self._3dActive) self._init3DTable();
});
this._vmBar = bar;
};
/* ══════════════════════════════════════════════════════════════
1. HEATMAP
══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._drawHeatmap = function(animate) {
const self = this;
const propDef = HEATMAP_PROPS.find(p => p.key === this._heatProp) || HEATMAP_PROPS[0];
/* gather values */
const vals = ELEMENTS.map(el => propDef.get(el));
const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v) && v > 0);
if (validVals.length === 0) return;
const rawMin = Math.min(...validVals);
const rawMax = Math.max(...validVals);
const norm = v => {
if (v === null || v === undefined || !isFinite(v)) return null;
if (this._heatLog) {
const logV = Math.log(Math.max(v, 1e-9));
const logMin = Math.log(Math.max(rawMin, 1e-9));
const logMax = Math.log(Math.max(rawMax, 1e-9));
return (logV - logMin) / (logMax - logMin || 1);
}
return (v - rawMin) / (rawMax - rawMin || 1);
};
const jet = t => {
/* jet colormap: blue→cyan→green→yellow→red */
const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 3)));
const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 2)));
const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 1)));
return `rgba(${(r*255)|0},${(g*255)|0},${(b*255)|0},0.85)`;
};
/* cancel previous tweens */
this._heatTweens.forEach(h => h && h.cancel && h.cancel());
this._heatTweens = [];
ELEMENTS.forEach((el, i) => {
const div = this._cellMap[el.Z];
if (!div) return;
const t = norm(propDef.get(el));
const targetBg = t !== null ? jet(t) : 'rgba(60,60,80,0.5)';
const targetBorder = t !== null ? jet(Math.min(1, t + 0.1)) : 'rgba(80,80,100,0.4)';
div.style.border = `1px solid ${targetBorder}`;
if (animate && window.LabFX && LabFX.motion) {
const tFrom = norm(propDef.get(el)) || 0;
const delay = i * 4; // stagger
setTimeout(() => {
if (!self._heatActive) return;
const handle = LabFX.motion.tween(0, 1, 400, 'easeInOutCubic', prog => {
const jt = (tFrom !== null ? tFrom * prog : 0);
div.style.background = t !== null ? jet(t * prog) : 'rgba(60,60,80,' + (0.5 * prog) + ')';
});
self._heatTweens.push(handle);
}, delay);
} else {
div.style.background = targetBg;
}
div.style.color = '#fff';
div.style.opacity = '1';
div.style.boxShadow = '';
});
/* build gradient legend */
this._buildHeatLegend(propDef, rawMin, rawMax, jet);
};
PeriodicTableSim.prototype._buildHeatLegend = function(propDef, minV, maxV, jet) {
const legendEl = this._vmBar.querySelector('#ptbl-heat-legend');
if (!legendEl) return;
/* gradient canvas */
const W = 120, H = 14;
legendEl.innerHTML = `
${propDef.unit ? minV.toFixed(1) + ' ' + propDef.unit : minV.toFixed(1)}
${maxV.toFixed(1)}
`;
legendEl.style.cssText = 'display:flex;align-items:center;gap:2px;flex-shrink:0;';
const canvas = legendEl.querySelector('#ptbl-heat-grad');
if (!canvas) return;
const ctx = canvas.getContext('2d');
for (let x = 0; x < W; x++) {
ctx.fillStyle = jet(x / (W - 1));
ctx.fillRect(x, 0, 1, H);
}
};
/* ══════════════════════════════════════════════════════════════
2. 3D TABLE (Three.js)
══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._init3DTable = function() {
const self = this;
if (typeof THREE === 'undefined') return;
/* cleanup previous */
if (this._3dRaf) { cancelAnimationFrame(this._3dRaf); this._3dRaf = null; }
if (this._3dRenderer) { this._3dRenderer.dispose(); this._3dRenderer = null; }
if (this._3dScene) { this._3dScene = null; }
const canvas = this._canvas3d;
const W = canvas.offsetWidth || 600;
const H = canvas.offsetHeight || 400;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d0d1a);
this._3dScene = scene;
const camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 2000);
camera.position.set(0, 60, 120);
camera.lookAt(0, 0, 0);
this._3dCamera = camera;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio || 1);
renderer.setSize(W, H);
this._3dRenderer = renderer;
/* ambient + directional light */
scene.add(new THREE.AmbientLight(0xffffff, 0.45));
const dir = new THREE.DirectionalLight(0xffffff, 0.9);
dir.position.set(20, 40, 20);
scene.add(dir);
/* get height property */
const propDef = HEATMAP_PROPS.find(p => p.key === this._heatProp) || HEATMAP_PROPS[0];
const vals = ELEMENTS.map(el => propDef.get(el));
const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v) && v > 0);
const vMin = validVals.length ? Math.min(...validVals) : 0;
const vMax = validVals.length ? Math.max(...validVals) : 1;
const normV = v => (v !== null && v !== undefined && isFinite(v)) ? (v - vMin) / (vMax - vMin || 1) : 0;
const jet3 = t => {
const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 3)));
const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 2)));
const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 1)));
return new THREE.Color(r, g, b);
};
const CELL_W = 3.2, GAP = 0.4;
const STEP = CELL_W + GAP;
const mode = this._3dMode;
this._3dMeshes = [];
/* helper: text texture for cube face */
const makeTexture = (el, col) => {
const tc = document.createElement('canvas');
tc.width = 64; tc.height = 64;
const ctx2 = tc.getContext('2d');
ctx2.fillStyle = '#' + col.getHexString();
ctx2.fillRect(0, 0, 64, 64);
ctx2.fillStyle = '#fff';
ctx2.font = 'bold 22px sans-serif';
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
ctx2.fillText(el.symbol, 32, 32);
return new THREE.CanvasTexture(tc);
};
ELEMENTS.forEach(el => {
const pos = getCell(el);
if (!pos) return;
const nt = normV(propDef.get(el));
const height = Math.max(0.4, nt * 18);
const col = jet3(nt);
let gridRow = pos.row;
let gridCol = pos.col;
/* stack mode: f-block folded into main */
if (mode === 'stack' && pos.row > 7) {
if (pos.row === 9) { gridRow = 6; gridCol = pos.col; }
if (pos.row === 10) { gridRow = 7; gridCol = pos.col; }
}
const x = (gridCol - 9.5) * STEP;
const z = (gridRow - 4) * STEP;
let boxH = height;
if (mode === 'wave') {
/* smooth surface: average with neighbors */
const neighbors = ELEMENTS.filter(ne => {
const np = getCell(ne);
if (!np) return false;
return Math.abs(np.col - pos.col) <= 1 && Math.abs(np.row - pos.row) <= 1 && ne.Z !== el.Z;
});
const avgNt = neighbors.length
? neighbors.reduce((s, ne) => s + normV(propDef.get(ne)), 0) / neighbors.length
: nt;
boxH = Math.max(0.4, ((nt + avgNt) / 2) * 18);
}
const geom = new THREE.BoxGeometry(CELL_W, boxH, CELL_W);
const tex = makeTexture(el, col);
const mats = [
new THREE.MeshLambertMaterial({ color: col }), // right
new THREE.MeshLambertMaterial({ color: col }), // left
new THREE.MeshLambertMaterial({ map: tex }), // top (symbol)
new THREE.MeshLambertMaterial({ color: col }), // bottom
new THREE.MeshLambertMaterial({ color: col }), // front
new THREE.MeshLambertMaterial({ color: col }), // back
];
const mesh = new THREE.Mesh(geom, mats);
mesh.position.set(x, boxH / 2, z);
mesh.userData = { z: el.Z, origY: boxH / 2, nt };
scene.add(mesh);
this._3dMeshes.push(mesh);
});
/* ── orbit camera (simple drag) ── */
let isDragging = false, lastX = 0, lastY = 0;
let camTheta = 0.4, camPhi = Math.PI / 4, camRadius = 140;
const updateCam = () => {
camera.position.set(
camRadius * Math.sin(camPhi) * Math.sin(camTheta),
camRadius * Math.cos(camPhi),
camRadius * Math.sin(camPhi) * Math.cos(camTheta)
);
camera.lookAt(0, 0, 0);
};
updateCam();
canvas.addEventListener('mousedown', e => { isDragging = true; lastX = e.clientX; lastY = e.clientY; });
canvas.addEventListener('mousemove', e => {
if (!isDragging) return;
camTheta -= (e.clientX - lastX) * 0.008;
camPhi = Math.max(0.15, Math.min(Math.PI / 2, camPhi - (e.clientY - lastY) * 0.005));
lastX = e.clientX; lastY = e.clientY;
updateCam();
});
canvas.addEventListener('mouseup', () => { isDragging = false; });
canvas.addEventListener('wheel', e => {
camRadius = Math.max(30, Math.min(300, camRadius + e.deltaY * 0.3));
updateCam();
e.preventDefault();
}, { passive: false });
/* ── raycaster for hover/click ── */
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredMesh = null;
/* 3D tooltip */
let tip3d = document.getElementById('ptbl-3d-tip');
if (!tip3d) {
tip3d = document.createElement('div');
tip3d.id = 'ptbl-3d-tip';
tip3d.style.cssText = 'position:fixed;display:none;background:rgba(10,10,30,0.92);border:1px solid rgba(155,93,229,0.5);color:#fff;font-size:.76rem;padding:5px 9px;border-radius:7px;pointer-events:none;z-index:999;';
document.body.appendChild(tip3d);
}
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(self._3dMeshes);
if (hoveredMesh) { hoveredMesh.scale.set(1, 1, 1); hoveredMesh = null; }
if (hits.length) {
hoveredMesh = hits[0].object;
hoveredMesh.scale.set(1.08, 1.08, 1.08);
const elZ = hoveredMesh.userData.z;
const elObj = ELEMENTS.find(e2 => e2.Z === elZ);
if (elObj) {
const vl = propDef.get(elObj);
tip3d.innerHTML = `${elObj.symbol} — ${elObj.name} ${propDef.label}: ${vl !== null ? vl : '—'} ${propDef.unit}`;
tip3d.style.display = 'block';
tip3d.style.left = (e.clientX + 12) + 'px';
tip3d.style.top = (e.clientY - 10) + 'px';
}
} else {
tip3d.style.display = 'none';
}
});
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(self._3dMeshes);
if (hits.length) {
const elZ = hits[0].object.userData.z;
if (elZ) self._selectElement(elZ);
}
});
/* ── render loop ── */
const animate3d = () => {
if (!self._3dActive) return;
self._3dRaf = requestAnimationFrame(animate3d);
renderer.render(scene, camera);
};
animate3d();
/* handle resize */
const ro3d = new ResizeObserver(() => {
if (!self._3dActive || !self._3dRenderer) return;
const nW = canvas.offsetWidth, nH = canvas.offsetHeight;
if (nW > 0 && nH > 0) {
renderer.setSize(nW, nH);
camera.aspect = nW / nH;
camera.updateProjectionMatrix();
}
});
ro3d.observe(canvas);
this._3dResizeObs = ro3d;
};
/* ══════════════════════════════════════════════════════════════
3. TABLE SHAPE MORPH (std / long / short)
══════════════════════════════════════════════════════════════ */
/* positions for LONG form (32-column, f-block inline) */
function _longFormPos(el) {
if (el.block === 'f') {
/* lanthanides: period 6, cols 4-17; actinides: period 7, cols 4-17 */
const fPos = { 57:4,58:5,59:6,60:7,61:8,62:9,63:10,64:11,65:12,66:13,67:14,68:15,69:16,70:17,71:18,
89:4,90:5,91:6,92:7,93:8,94:9,95:10,96:11,97:12,98:13,99:14,100:15,101:16,102:17,103:18 };
return { row: el.period, col: fPos[el.Z] || (el.Z - 54 + 4) };
}
/* s and d blocks shift right by 14 (f-block inserted) */
const stdPos = getCell(el);
if (!stdPos) return null;
if (el.block === 's') {
if (el.group === 1) return { row: stdPos.row, col: 1 };
if (el.group === 2) return { row: stdPos.row, col: 2 };
}
if (el.block === 'd' || (el.block === 'p')) {
return { row: stdPos.row, col: stdPos.col + 14 };
}
return { row: stdPos.row, col: stdPos.col + 14 };
}
/* positions for SHORT (8-group) form — classic Russian table */
function _shortFormPos(el) {
/* main group elements: groups 1-8 */
const SHORT_MAP = {
1:{1:1,3:1,11:1,19:1,37:1,55:1,87:1},
2:{4:2,12:2,20:2,38:2,56:2,88:2},
};
const stdPos = getCell(el);
if (!stdPos) return null;
if (el.block === 'd') {
/* transition metals: group 3-12 → columns 3-8 with subgroup */
const subCol = ((el.group - 3) % 8) + 3;
return { row: stdPos.row, col: subCol };
}
if (el.block === 'f') {
/* lanthanides row 9, actinides row 10 — same as standard */
return getCell(el);
}
/* p and s blocks */
if (el.group) {
const shortGroup = el.group <= 2 ? el.group : (el.group - 10 <= 0 ? el.group - 10 + 8 : el.group - 10);
return { row: stdPos.row, col: Math.max(1, Math.min(8, el.group <= 2 ? el.group : el.group - 10)) };
}
return stdPos;
}
PeriodicTableSim.prototype._morphToView = function(shape) {
const self = this;
const DURATION = 800;
/* snapshot current pixel positions of each cell */
const gridRect = this._tableEl.getBoundingClientRect();
/* we animate by moving each cell div to absolute position during morph
then snap to new grid layout */
/* for std, we just rebuild; for long/short we switch grid and re-place */
if (shape === 'std') {
/* restore normal 18-col grid */
this._tableEl.style.gridTemplateColumns = 'repeat(18,1fr)';
if (this._fblockEl) this._fblockEl.style.display = '';
this._colorTable();
return;
}
if (shape === 'long') {
/* switch to 32-column grid, hide f-block row */
if (this._fblockEl) this._fblockEl.style.display = 'none';
this._tableEl.style.gridTemplateColumns = 'repeat(32,1fr)';
/* re-map cells — rebuild grid items */
this._tableEl.innerHTML = '';
const cells = {};
for (let r = 1; r <= 7; r++) {
for (let c = 1; c <= 32; c++) {
const d = document.createElement('div');
d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
cells[`${r},${c}`] = d;
this._tableEl.appendChild(d);
}
}
for (const el of ELEMENTS) {
const pos = _longFormPos(el);
if (!pos || pos.row > 7) continue;
const div = cells[`${pos.row},${pos.col}`];
if (!div) continue;
this._cellMap[el.Z] = div;
div.dataset.z = el.Z;
div.title = `${el.name} (${el.symbol})`;
div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;opacity:0;';
div.innerHTML = `
${el.Z}
${el.symbol}
${el.name} `;
div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
div.addEventListener('click', () => self._selectElement(el.Z));
/* fade-in staggered */
const delay = (pos.col + pos.row * 2) * 8;
setTimeout(() => {
if (window.LabFX && LabFX.motion) {
LabFX.motion.tween(0, 1, DURATION, 'easeInOutCubic', v => { div.style.opacity = v; });
} else {
div.style.opacity = '1';
}
}, delay);
}
this._colorTable();
return;
}
if (shape === 'short') {
/* 8-column grid */
if (this._fblockEl) this._fblockEl.style.display = '';
this._tableEl.style.gridTemplateColumns = 'repeat(8,1fr)';
this._tableEl.innerHTML = '';
const cells = {};
for (let r = 1; r <= 7; r++) {
for (let c = 1; c <= 8; c++) {
const d = document.createElement('div');
d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
cells[`${r},${c}`] = d;
this._tableEl.appendChild(d);
}
}
for (const el of ELEMENTS) {
if (el.block === 'f') continue; /* f-block stays in fblockEl */
const stdPos = getCell(el);
if (!stdPos) continue;
let col;
if (el.group <= 2) {
col = el.group;
} else if (el.group >= 13) {
col = el.group - 10;
} else {
/* d-block: groups 3-12 → compress to cols 3-8 with row offset tricks */
col = Math.min(8, ((el.group - 3) % 8) + 3);
}
col = Math.max(1, Math.min(8, col));
const div = cells[`${stdPos.row},${col}`] || cells[`${stdPos.row},1`];
if (!div || div.dataset.z) continue;
this._cellMap[el.Z] = div;
div.dataset.z = el.Z;
div.title = `${el.name} (${el.symbol})`;
div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;opacity:0;';
div.innerHTML = `
${el.Z}
${el.symbol}
${el.name} `;
div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
div.addEventListener('click', () => self._selectElement(el.Z));
const delay = (col + stdPos.row * 2) * 10;
setTimeout(() => {
if (window.LabFX && LabFX.motion) {
LabFX.motion.tween(0, 1, DURATION, 'easeInOutCubic', v => { div.style.opacity = v; });
} else {
div.style.opacity = '1';
}
}, delay);
}
this._colorTable();
}
};
/* ══════════════════════════════════════════════════════════════
4. TREND ARROWS
══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._drawTrendArrows = function() {
const self = this;
const canvas = this._trendCanvas;
if (!canvas) return;
const parent = canvas.parentElement;
const W = parent.offsetWidth || 600;
const H = parent.offsetHeight || 300;
const dpr = window.devicePixelRatio || 1;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, W, H);
const cfg = TREND_CONFIG[this._trendProp] || TREND_CONFIG.radius;
const ARROW_W = 12;
const PADDING = 4;
const PAD_BOTTOM = 14;
const PAD_LEFT = 14;
/* draw arrow helper: from (x1,y1) to (x2,y2) with gradient and label */
const drawArrow = (x1, y1, x2, y2, color, label, labelSide) => {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx * dx + dy * dy);
if (len < 10) return;
const grad = ctx.createLinearGradient(x1, y1, x2, y2);
grad.addColorStop(0, color + '22');
grad.addColorStop(0.5, color + 'bb');
grad.addColorStop(1, color + 'ff');
/* shaft */
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = grad;
ctx.lineWidth = ARROW_W;
ctx.lineCap = 'round';
ctx.stroke();
/* arrowhead */
const angle = Math.atan2(dy, dx);
const AH = 16;
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - AH * Math.cos(angle - 0.45), y2 - AH * Math.sin(angle - 0.45));
ctx.lineTo(x2 - AH * Math.cos(angle + 0.45), y2 - AH * Math.sin(angle + 0.45));
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
/* label */
ctx.save();
ctx.font = 'bold 11px Manrope,sans-serif';
ctx.fillStyle = color;
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 4;
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2;
if (labelSide === 'below') {
ctx.textAlign = 'center';
ctx.fillText(label, mx, my + 20);
} else if (labelSide === 'left') {
ctx.textAlign = 'right';
ctx.fillText(label, mx - 14, my);
}
ctx.restore();
};
const hCfg = cfg.hArrow;
const vCfg = cfg.vArrow;
/* horizontal arrow: bottom of table */
const yBottom = H - PAD_BOTTOM;
if (hCfg.dir === 'left') {
drawArrow(W - PADDING, yBottom, PADDING, yBottom, hCfg.color, hCfg.text, 'below');
} else {
drawArrow(PADDING, yBottom, W - PADDING, yBottom, hCfg.color, hCfg.text, 'below');
}
/* vertical arrow: left side of table */
const xLeft = PAD_LEFT;
if (vCfg.dir === 'down') {
drawArrow(xLeft, PADDING, xLeft, H - PAD_BOTTOM - 20, vCfg.color, vCfg.text, 'left');
} else {
drawArrow(xLeft, H - PAD_BOTTOM - 20, xLeft, PADDING, vCfg.color, vCfg.text, 'left');
}
};
/* ── patch stop() to clean up 3D ──────────────────────────── */
const _origStop = PeriodicTableSim.prototype.stop;
PeriodicTableSim.prototype.stop = function() {
_origStop.call(this);
if (this._3dRaf) { cancelAnimationFrame(this._3dRaf); this._3dRaf = null; }
if (this._3dRenderer) { this._3dRenderer.dispose(); this._3dRenderer = null; }
if (this._3dResizeObs) { this._3dResizeObs.disconnect(); }
const tip3d = document.getElementById('ptbl-3d-tip');
if (tip3d) tip3d.style.display = 'none';
};
/* ══════════════════════════════════════════════════════════════
WAVE D — ELECTRON-CONFIG DEEP TOOLS
_periodG_*: orbital filling, aufbau diagram,
quantum-number hover, Bohr excitation overlay
══════════════════════════════════════════════════════════════ */
/* ── Arrow helper (module-level, used by orbital filling) ────── */
function _periodG_drawArrow(ctx, ax, ay1, ay2, col) {
ctx.beginPath(); ctx.moveTo(ax, ay1); ctx.lineTo(ax, ay2);
ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.stroke();
const dir = ay2 < ay1 ? -1 : 1;
ctx.beginPath();
ctx.moveTo(ax, ay2); ctx.lineTo(ax - 2.5, ay2 - dir * 4); ctx.lineTo(ax + 2.5, ay2 - dir * 4);
ctx.closePath(); ctx.fillStyle = col; ctx.fill();
}
/* ── Aufbau filling order ─────────────────────────────────────── */
PeriodicTableSim.prototype._periodG_aufbauOrder = function() {
return [
{n:1,l:0,label:'1s',cap:2}, {n:2,l:0,label:'2s',cap:2},
{n:2,l:1,label:'2p',cap:6}, {n:3,l:0,label:'3s',cap:2},
{n:3,l:1,label:'3p',cap:6}, {n:4,l:0,label:'4s',cap:2},
{n:3,l:2,label:'3d',cap:10}, {n:4,l:1,label:'4p',cap:6},
{n:5,l:0,label:'5s',cap:2}, {n:4,l:2,label:'4d',cap:10},
{n:5,l:1,label:'5p',cap:6}, {n:6,l:0,label:'6s',cap:2},
{n:4,l:3,label:'4f',cap:14}, {n:5,l:2,label:'5d',cap:10},
{n:6,l:1,label:'6p',cap:6}, {n:7,l:0,label:'7s',cap:2},
{n:5,l:3,label:'5f',cap:14}, {n:6,l:2,label:'6d',cap:10},
{n:7,l:1,label:'7p',cap:6},
];
};
PeriodicTableSim.prototype._periodG_mlLabels = function(l) {
if (l === 0) return [''];
if (l === 1) return ['px','py','pz'];
if (l === 2) return ['dxy','dxz','dyz','dx2y2','dz2'];
const out = [];
for (let m = -l; m <= l; m++) out.push('f' + (m >= 0 ? '+' : '') + m);
return out;
};
PeriodicTableSim.prototype._periodG_buildElectronList = function(Z) {
const order = this._periodG_aufbauOrder();
const electrons = [];
let rem = Z;
for (const sub of order) {
if (rem <= 0) break;
const mlList = this._periodG_mlLabels(sub.l);
const nOrb = mlList.length;
for (let pass = 0; pass < 2 && rem > 0; pass++) {
for (let oi = 0; oi < nOrb && rem > 0; oi++) {
electrons.push({
n: sub.n, l: sub.l, ml: oi - sub.l,
ms: pass === 0 ? 0.5 : -0.5,
subLabel: sub.label, orbIdx: oi, subRef: sub, mlLabel: mlList[oi],
});
rem--;
}
}
}
return electrons;
};
/* ══════════════════════════════════════════════════════════════
TOOL 1 — Orbital Filling Diagram
══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._periodG_drawOrbitalFilling = function(canvas, el) {
const electrons = this._periodG_buildElectronList(el.Z);
if (!electrons.length) return;
const subMap = new Map();
for (const e of electrons) {
if (!subMap.has(e.subLabel)) subMap.set(e.subLabel, []);
subMap.get(e.subLabel).push(e);
}
const order = this._periodG_aufbauOrder();
const subs = order.filter(s => subMap.has(s.label));
const BOX = 18, GAP = 3, LPAD = 28, VPAD = 5, ROW_H = BOX + VPAD * 2 + 8;
const W = canvas.offsetWidth || 240;
const H_needed = subs.length * ROW_H + 10;
const dpr = window.devicePixelRatio || 1;
canvas.width = W * dpr; canvas.height = H_needed * dpr;
canvas.style.height = H_needed + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H_needed);
const hitMap = [];
subs.forEach((sub, si) => {
const y0 = si * ROW_H + VPAD;
const electronArr = subMap.get(sub.label);
const nOrb = 2 * sub.l + 1;
const mlLabels = this._periodG_mlLabels(sub.l);
const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#aaa';
ctx.font = 'bold 10px Manrope,sans-serif'; ctx.fillStyle = bColor;
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
ctx.fillText(sub.label, 2, y0 + BOX / 2);
const isLastSub = si === subs.length - 1;
for (let oi = 0; oi < nOrb; oi++) {
const bx = LPAD + oi * (BOX + GAP), by = y0;
const inOrb = electronArr.filter(e => e.orbIdx === oi);
const isLastBox = isLastSub && oi === nOrb - 1 && inOrb.length > 0;
ctx.fillStyle = 'rgba(255,255,255,0.06)';
ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
ctx.fill(); ctx.stroke();
if (nOrb > 1) {
ctx.font = '7px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.25)';
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
ctx.fillText(mlLabels[oi], bx + BOX / 2, by + BOX + 1);
}
inOrb.forEach((e, ei) => {
const isUp = e.ms > 0;
const ax = bx + (ei === 0 ? BOX * 0.35 : BOX * 0.65);
const ay_t = by + 3, ay_b = by + BOX - 3;
const col = isLastBox ? '#FFD166' : '#06D6E0';
if (isLastBox && window.LabFX)
LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 8 });
else
_periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
hitMap.push({ x: ax - 6, y: by, w: 12, h: BOX, electron: e });
});
}
});
canvas._hitMap = hitMap; canvas._dpr = dpr;
};
/* ── Highlight variant (single electron lit, rest dimmed) ─────── */
PeriodicTableSim.prototype._periodG_drawOrbFillingHL = function(canvas, el, hvEl) {
const electrons = this._periodG_buildElectronList(el.Z);
const subMap = new Map();
for (const e of electrons) {
if (!subMap.has(e.subLabel)) subMap.set(e.subLabel, []);
subMap.get(e.subLabel).push(e);
}
const order = this._periodG_aufbauOrder();
const subs = order.filter(s => subMap.has(s.label));
const BOX = 18, GAP = 3, LPAD = 28, VPAD = 5, ROW_H = BOX + VPAD * 2 + 8;
const W = canvas.offsetWidth || 240;
const H_needed = subs.length * ROW_H + 10;
const dpr = canvas._dpr || window.devicePixelRatio || 1;
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H_needed);
subs.forEach((sub, si) => {
const y0 = si * ROW_H + VPAD;
const electronArr = subMap.get(sub.label);
const nOrb = 2 * sub.l + 1;
const mlLabels = this._periodG_mlLabels(sub.l);
const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#aaa';
ctx.font = 'bold 10px Manrope,sans-serif'; ctx.fillStyle = bColor;
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(sub.label, 2, y0 + BOX / 2);
for (let oi = 0; oi < nOrb; oi++) {
const bx = LPAD + oi * (BOX + GAP), by = y0;
const inOrb = electronArr.filter(e => e.orbIdx === oi);
ctx.fillStyle = 'rgba(255,255,255,0.06)'; ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
ctx.fill(); ctx.stroke();
if (nOrb > 1) {
ctx.font = '7px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.25)';
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
ctx.fillText(mlLabels[oi], bx + BOX / 2, by + BOX + 1);
}
inOrb.forEach((e, ei) => {
const isHover = e.n === hvEl.n && e.l === hvEl.l && e.ml === hvEl.ml && e.ms === hvEl.ms;
const isUp = e.ms > 0;
const ax = bx + (ei === 0 ? BOX * 0.35 : BOX * 0.65);
const ay_t = by + 3, ay_b = by + BOX - 3;
const col = isHover ? '#FFD166' : 'rgba(6,214,224,0.3)';
if (isHover && window.LabFX)
LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 10 });
else
_periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
});
}
});
};
/* ══════════════════════════════════════════════════════════════
TOOL 3 — Quantum-number hover on orbital filling
══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._periodG_attachQNHover = function(canvas, el) {
if (canvas._periodG_mmH) {
canvas.removeEventListener('mousemove', canvas._periodG_mmH);
canvas.removeEventListener('mouseleave', canvas._periodG_mlH);
}
const tip = this._periodG_qTip;
canvas._periodG_mmH = (ev) => {
const rect = canvas.getBoundingClientRect();
const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
const hit = (canvas._hitMap || []).find(h => mx >= h.x && mx <= h.x + h.w && my >= h.y && my <= h.y + h.h);
if (hit) {
const e = hit.electron;
const lName = ['s','p','d','f'][e.l] || String(e.l);
if (tip) {
tip.innerHTML = `${e.subLabel} ${e.mlLabel} n = ${e.n} l = ${e.l} (${lName}) ml = ${e.ml} ms = ${e.ms > 0 ? '+1/2' : '-1/2'}`;
tip.style.display = 'block';
tip.style.left = (ev.clientX + 12) + 'px';
tip.style.top = (ev.clientY - 10) + 'px';
}
this._periodG_drawOrbFillingHL(canvas, el, hit.electron);
} else {
if (tip) tip.style.display = 'none';
this._periodG_drawOrbitalFilling(canvas, el);
}
};
canvas._periodG_mlH = () => { if (tip) tip.style.display = 'none'; this._periodG_drawOrbitalFilling(canvas, el); };
canvas.addEventListener('mousemove', canvas._periodG_mmH);
canvas.addEventListener('mouseleave', canvas._periodG_mlH);
};
/* ══════════════════════════════════════════════════════════════
TOOL 2 — Aufbau Diagram with Z slider
══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._periodG_drawAufbau = function(canvas, Z) {
const order = this._periodG_aufbauOrder();
const W = canvas.offsetWidth || 240;
const BOX = 14, GAP = 2, LPAD = 26, VPAD = 4, ROW_H = BOX + VPAD * 2;
const H_needed = order.length * ROW_H + 14;
const dpr = window.devicePixelRatio || 1;
canvas.width = W * dpr; canvas.height = H_needed * dpr;
canvas.style.height = H_needed + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H_needed);
const electrons = this._periodG_buildElectronList(Z);
const subCount = new Map();
for (const e of electrons) subCount.set(e.subLabel, (subCount.get(e.subLabel) || 0) + 1);
let lastRow = -1;
order.forEach((sub, si) => { if ((subCount.get(sub.label) || 0) > 0) lastRow = si; });
order.forEach((sub, si) => {
const y0 = si * ROW_H + VPAD;
const nOrb = 2 * sub.l + 1;
const filled = subCount.get(sub.label) || 0;
const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#888';
ctx.font = 'bold 9px Manrope,sans-serif';
ctx.fillStyle = si <= lastRow ? bColor : 'rgba(255,255,255,0.2)';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(sub.label, 2, y0 + BOX / 2);
for (let oi = 0; oi < nOrb; oi++) {
const bx = LPAD + oi * (BOX + GAP), by = y0;
ctx.fillStyle = 'rgba(255,255,255,0.04)';
ctx.strokeStyle = si <= lastRow ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.06)';
ctx.lineWidth = 1; ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
ctx.fill(); ctx.stroke();
}
let rem = filled;
const spins = Array(nOrb).fill(0);
for (let oi = 0; oi < nOrb && rem > 0; oi++) { spins[oi]++; rem--; }
for (let oi = 0; oi < nOrb && rem > 0; oi++) { spins[oi]++; rem--; }
for (let oi = 0; oi < nOrb; oi++) {
const bx = LPAD + oi * (BOX + GAP), by = y0;
const isLast = si === lastRow && oi === nOrb - 1 && spins[oi] > 0;
for (let sp = 0; sp < spins[oi]; sp++) {
const isUp = sp === 0;
const ax = bx + (sp === 0 ? BOX * 0.35 : BOX * 0.65);
const ay_t = by + 2, ay_b = by + BOX - 2;
const col = isLast ? '#FFD166' : bColor;
if (isLast && window.LabFX)
LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 6 });
else
_periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
}
}
if (si === lastRow && filled < sub.cap) {
const nx = LPAD + nOrb * (BOX + GAP) + 2, ny = y0 + BOX / 2;
ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(nx, ny); ctx.lineTo(nx + 7, ny); ctx.stroke();
ctx.beginPath(); ctx.moveTo(nx+7,ny); ctx.lineTo(nx+4,ny-3); ctx.lineTo(nx+4,ny+3);
ctx.fillStyle = '#9B5DE5'; ctx.fill();
}
});
const now = performance.now();
if (this._periodG_lastAufbauZ !== Z && window.LabFX) {
if (!this._periodG_aufbauSoundTs || now - this._periodG_aufbauSoundTs > 80) {
LabFX.sound.play('tick', { pitch: 0.8 + Z * 0.01, volume: 0.08 });
this._periodG_aufbauSoundTs = now;
}
}
this._periodG_lastAufbauZ = Z;
};
/* ══════════════════════════════════════════════════════════════
Tab injection — patches _updateCard to add «Орбитали»/«Aufbau»
══════════════════════════════════════════════════════════════ */
(function() {
const _orig = PeriodicTableSim.prototype._updateCard;
PeriodicTableSim.prototype._updateCard = function(el) {
if (this._periodG_cleanupQTip) { this._periodG_cleanupQTip(); this._periodG_cleanupQTip = null; }
_orig.call(this, el);
if (!el) return;
this._periodG_addElecTab(el);
};
})();
PeriodicTableSim.prototype._periodG_addElecTab = function(el) {
const card = this._cardEl;
const tabBar = document.createElement('div');
tabBar.style.cssText = 'display:flex;gap:0;border-bottom:1px solid rgba(255,255,255,0.1);margin:8px -10px 0;';
const btns = ['Орбитали','Aufbau'].map((lbl) => {
const btn = document.createElement('button');
btn.textContent = lbl;
btn.style.cssText = 'flex:1;padding:5px 0;border:none;background:transparent;color:rgba(255,255,255,0.4);font-size:.7rem;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;';
tabBar.appendChild(btn); return btn;
});
card.appendChild(tabBar);
const wrap = document.createElement('div'); wrap.style.cssText = 'position:relative;';
card.appendChild(wrap);
// Panel 0: orbital filling canvas
const orbPan = document.createElement('div'); orbPan.style.cssText = 'display:none;padding:4px 0 0;';
const orbCv = document.createElement('canvas'); orbCv.style.cssText = 'width:100%;display:block;cursor:crosshair;';
orbPan.appendChild(orbCv); wrap.appendChild(orbPan);
// Panel 1: Aufbau canvas + Z slider
const aufPan = document.createElement('div'); aufPan.style.cssText = 'display:none;padding:4px 0 0;';
const aufCv = document.createElement('canvas'); aufCv.style.cssText = 'width:100%;display:block;';
aufPan.appendChild(aufCv);
const sw = document.createElement('div'); sw.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 0;';
const sld = document.createElement('input'); sld.type='range'; sld.min=1; sld.max=118; sld.value=el.Z;
sld.style.cssText = 'flex:1;accent-color:#9B5DE5;';
const slLbl = document.createElement('span'); slLbl.style.cssText = 'font-size:.68rem;color:#9B5DE5;min-width:32px;text-align:right;';
slLbl.textContent = 'Z=' + el.Z;
sw.appendChild(sld); sw.appendChild(slLbl); aufPan.appendChild(sw); wrap.appendChild(aufPan);
// quantum number tooltip (fixed positioned, cleaned up on new element)
const qTip = document.createElement('div');
qTip.style.cssText = 'position:fixed;display:none;padding:5px 9px;background:#1a1a2e;border:1px solid rgba(155,93,229,0.5);border-radius:6px;font-size:.68rem;color:#ccc;pointer-events:none;z-index:9999;line-height:1.65;';
document.body.appendChild(qTip);
this._periodG_qTip = qTip;
this._periodG_cleanupQTip = () => {
qTip.style.display = 'none';
if (qTip.parentNode) qTip.parentNode.removeChild(qTip);
this._periodG_qTip = null;
};
const panels = [orbPan, aufPan];
const showTab = (idx) => {
btns.forEach((b, i) => {
b.style.color = i === idx ? '#fff' : 'rgba(255,255,255,0.4)';
b.style.borderBottomColor = i === idx ? '#9B5DE5' : 'transparent';
});
panels.forEach((p, i) => p.style.display = i === idx ? 'block' : 'none');
if (idx === 0) setTimeout(() => { this._periodG_drawOrbitalFilling(orbCv, el); this._periodG_attachQNHover(orbCv, el); }, 0);
if (idx === 1) setTimeout(() => this._periodG_drawAufbau(aufCv, +sld.value), 0);
};
btns.forEach((b, i) => b.addEventListener('click', () => showTab(i)));
sld.addEventListener('input', () => { const z = +sld.value; slLbl.textContent = 'Z='+z; this._periodG_drawAufbau(aufCv, z); });
showTab(0);
};
/* ══════════════════════════════════════════════════════════════
TOOL 4 — Bohr excitation (patches _drawBohr)
══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._periodG_wavelengthToRGB = function(nm) {
let R=0,G=0,B=0,a=1;
if(nm>=380&&nm<440){R=-(nm-440)/(440-380);G=0;B=1;}
else if(nm<490){R=0;G=(nm-440)/(490-440);B=1;}
else if(nm<510){R=0;G=1;B=-(nm-510)/(510-490);}
else if(nm<580){R=(nm-510)/(580-510);G=1;B=0;}
else if(nm<645){R=1;G=-(nm-645)/(645-580);B=0;}
else if(nm<781){R=1;G=0;B=0;}
if(nm>=700)a=0.3+0.7*(780-nm)/(780-700);
else if(nm<420)a=0.3+0.7*(nm-380)/(420-380);
return `rgba(${Math.round(R*255*a)},${Math.round(G*255*a)},${Math.round(B*255*a)},1)`;
};
PeriodicTableSim.prototype._periodG_initBohrExcite = function() {
const canvas = this._bohrCanvas;
if (canvas._periodG_excite_bound) return;
canvas._periodG_excite_bound = true;
this._periodG_excState = null;
canvas.title = 'Клик на электрон — возбуждение';
canvas.addEventListener('click', (ev) => {
if (!this._bohrZ) return;
const el = ELEMENTS.find(e => e.Z === this._bohrZ); if (!el) return;
const shells = getShellFill(el.Z);
const rect = canvas.getBoundingClientRect();
const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
const W = canvas.offsetWidth || 240, H = canvas.offsetHeight || 150;
const cx = W/2, cy = H/2, maxR = Math.min(W,H)*0.44, nShells = shells.length;
let clickedShell = -1;
outer: for (let i = 0; i < nShells; i++) {
const r = maxR*(i+1)/nShells, speed = 1-i*0.12;
for (let e2 = 0; e2 < shells[i]; e2++) {
const a = this._bohrAngle*speed + (2*Math.PI*e2)/shells[i];
const dx = mx-(cx+r*Math.cos(a)), dy = my-(cy+r*Math.sin(a));
if (Math.sqrt(dx*dx+dy*dy) < 9) { clickedShell = i; break outer; }
}
}
if (clickedShell < 0) {
for (let i = 0; i < nShells; i++) {
const r = maxR*(i+1)/nShells;
const dist = Math.sqrt((mx-cx)**2+(my-cy)**2);
if (Math.abs(dist-r) < 8 && shells[i] > 0) { clickedShell = i; break; }
}
}
if (clickedShell < 0) return;
this._periodG_showExciteMenu(el, shells, clickedShell, ev.clientX, ev.clientY);
});
};
PeriodicTableSim.prototype._periodG_showExciteMenu = function(el, shells, n1idx, px, py) {
const old = document.getElementById('periodG-excite-menu'); if (old) old.remove();
const menu = document.createElement('div');
menu.id = 'periodG-excite-menu';
menu.style.cssText = `position:fixed;left:${px+6}px;top:${py}px;background:#12122a;border:1px solid rgba(155,93,229,0.55);border-radius:8px;padding:8px;z-index:9999;font-size:.71rem;color:#ccc;min-width:175px;box-shadow:0 4px 18px rgba(0,0,0,0.5);`;
menu.innerHTML = `Переход из n=${n1idx+1}:
`;
for (let i = 0; i < shells.length; i++) {
if (i === n1idx) continue;
const n = i+1, n1 = n1idx+1;
const dE = 13.6 * (1/(n1*n1) - 1/(n*n));
const dE_abs = Math.abs(dE);
const lam = dE_abs > 0.02 ? Math.round(1240/dE_abs) : 99999;
const region = lam < 380 ? 'УФ' : lam > 780 ? 'ИК' : 'видим.';
const abs = dE > 0;
const btn = document.createElement('button');
btn.style.cssText = 'display:block;width:100%;text-align:left;padding:3px 7px;border:none;background:transparent;color:#ccc;cursor:pointer;border-radius:4px;font-size:.7rem;';
btn.innerHTML = `n=${n} → ${lam < 99999 ? lam+'нм ('+region+')' : '—'} ${abs ? '[+ф]' : '[-ф]'}`;
btn.addEventListener('mouseenter', () => btn.style.background = 'rgba(155,93,229,0.2)');
btn.addEventListener('mouseleave', () => btn.style.background = 'transparent');
btn.addEventListener('click', () => { menu.remove(); this._periodG_startExcitation(el, n1idx, i, lam); });
menu.appendChild(btn);
}
const cb = document.createElement('button');
cb.style.cssText = 'display:block;width:100%;text-align:center;padding:2px 0;border:none;background:transparent;color:rgba(255,255,255,0.3);cursor:pointer;font-size:.68rem;margin-top:5px;';
cb.textContent = 'Отмена'; cb.addEventListener('click', () => menu.remove());
menu.appendChild(cb); document.body.appendChild(menu);
const outside = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', outside); } };
setTimeout(() => document.addEventListener('click', outside), 60);
};
PeriodicTableSim.prototype._periodG_startExcitation = function(el, n1idx, n2idx, lam) {
this._periodG_excState = { n1: n1idx, n2: n2idx, lam, phase: 'up', t: performance.now() };
if (window.LabFX) LabFX.sound.play('chime', { pitch: n2idx > n1idx ? 1.3 : 0.7, volume: 0.3 });
};
/* patch _drawBohr to incorporate excitation physics */
(function() {
const _orig = PeriodicTableSim.prototype._drawBohr;
PeriodicTableSim.prototype._drawBohr = function() {
if (!this._bohrCanvas._periodG_excite_bound) this._periodG_initBohrExcite();
const canvas = this._bohrCanvas;
const dpr = window.devicePixelRatio || 1;
const W = canvas.offsetWidth || 240, H = canvas.offsetHeight || 150;
canvas.width = W*dpr; canvas.height = H*dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H);
if (!this._bohrZ) return;
const el = ELEMENTS.find(e => e.Z === this._bohrZ); if (!el) return;
const shells = getShellFill(el.Z);
const cx = W/2, cy = H/2, maxR = Math.min(W,H)*0.44, nShells = shells.length;
const col = TYPE_COLORS[el.type] || '#7B8EF7';
const exc = this._periodG_excState;
const now = performance.now();
if (exc) {
const el2 = now - exc.t;
if (exc.phase === 'up' && el2 > 600) { exc.phase = 'stay'; exc.t = now; }
else if (exc.phase === 'stay' && el2 > 800) { exc.phase = 'down'; exc.t = now; }
else if (exc.phase === 'down' && el2 > 600) { exc.phase = 'done'; }
}
ctx.beginPath(); ctx.arc(cx, cy, nShells > 0 ? 5+nShells*1.5 : 6, 0, Math.PI*2);
ctx.fillStyle = col; ctx.fill();
shells.forEach((count, i) => {
const r = maxR*(i+1)/nShells;
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2);
ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 1; ctx.stroke();
const speed = 1-i*0.12;
for (let e2 = 0; e2 < count; e2++) {
const a = this._bohrAngle*speed + (2*Math.PI*e2)/count;
let drawR = r;
const isExc = exc && exc.phase !== 'done' && i === exc.n1 && e2 === 0;
if (isExc) {
const r2 = maxR*(exc.n2+1)/nShells;
const elapsed2 = now - exc.t;
let prog = 0;
if (exc.phase === 'up') prog = Math.min(elapsed2/600, 1);
else if (exc.phase === 'stay') prog = 1;
else if (exc.phase === 'down') prog = 1 - Math.min(elapsed2/600, 1);
drawR = r + (r2-r)*prog;
if (exc.phase === 'up') {
const pf = Math.min(elapsed2/600, 1);
const pm = r + (r2-r)*pf*0.5, pa = a + Math.PI/2;
const px2 = cx+pm*Math.cos(pa), py2 = cy+pm*Math.sin(pa);
const lamNm = exc.lam;
const pcol = (lamNm>=380&&lamNm<=780) ? this._periodG_wavelengthToRGB(lamNm) : (lamNm<380 ? '#cc88ff' : '#ffaaaa');
if (window.LabFX) {
LabFX.glow.drawGlow(ctx, () => { ctx.beginPath(); ctx.arc(px2,py2,4,0,Math.PI*2); ctx.fillStyle=pcol; ctx.fill(); }, { color: pcol, intensity: 12 });
} else { ctx.beginPath(); ctx.arc(px2,py2,4,0,Math.PI*2); ctx.fillStyle=pcol; ctx.fill(); }
ctx.font='8px Manrope,sans-serif'; ctx.fillStyle=pcol; ctx.textAlign='center';
ctx.fillText((exc.lam<99999?exc.lam+'нм':'?')+(exc.lam<380?' УФ':exc.lam>780?' ИК':''), cx, H-4);
}
}
const ex2 = cx+drawR*Math.cos(a), ey2 = cy+drawR*Math.sin(a);
if (isExc && window.LabFX) {
LabFX.glow.drawGlow(ctx, () => { ctx.beginPath(); ctx.arc(ex2,ey2,3.5,0,Math.PI*2); ctx.fillStyle='#FFD166'; ctx.fill(); }, { color:'#FFD166', intensity:10 });
} else { ctx.beginPath(); ctx.arc(ex2,ey2,2.5,0,Math.PI*2); ctx.fillStyle='#06D6E0'; ctx.fill(); }
}
});
if (!exc || exc.phase === 'done') {
if (exc && exc.phase === 'done') {
this._periodG_excState = null;
if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.9, volume: 0.18 });
}
ctx.font = '700 10px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.textAlign = 'center'; ctx.fillText(shells.join(','), cx, H-4);
}
ctx.font = '7.5px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.textAlign = 'center'; ctx.fillText('клик -> возбуждение', cx, 10);
};
})();
/* patch stop() to clean up quantum tooltip (Wave D) */
(function() {
const _origStopD = PeriodicTableSim.prototype.stop;
PeriodicTableSim.prototype.stop = function() {
_origStopD.call(this);
if (this._periodG_cleanupQTip) { this._periodG_cleanupQTip(); this._periodG_cleanupQTip = null; }
};
})();
/* ── global opener ─────────────────────────────────────────── */
/* ══════════════════════════════════════════════════════════════
WAVE C — INTERACTIVE LEARNING MODES
_buildInteractiveModeBar, _switchInteractiveMode,
_modeBinary, _binaryClick, _binaryCalc,
_modeCompare, _compareClick, _compareRefresh, _compareDraw,
_modeActivity,
_modeMendeleev1869, _m1869ShowPrediction,
_modeTimeline, _timelineUpdate
══════════════════════════════════════════════════════════════ */
/* patch _buildVisualModes to also init interactive bar */
(function() {
var _prevBVM = PeriodicTableSim.prototype._buildVisualModes;
PeriodicTableSim.prototype._buildVisualModes = function() {
if (_prevBVM) _prevBVM.call(this);
this._buildInteractiveModeBar();
};
})();
/* patch stop() to clean up timeline RAF */
(function() {
var _prevSC = PeriodicTableSim.prototype.stop;
PeriodicTableSim.prototype.stop = function() {
if (_prevSC) _prevSC.call(this);
if (this._iModeState && this._iModeState.raf) {
cancelAnimationFrame(this._iModeState.raf);
this._iModeState.raf = null;
}
};
})();
PeriodicTableSim.prototype._buildInteractiveModeBar = function() {
this._interactiveMode = null;
this._iModePanel = null;
this._iModeState = {};
var bar = document.createElement('div');
bar.className = 'ptbl-imode-bar';
bar.style.cssText = 'display:flex;align-items:center;gap:5px;padding:5px 12px;background:rgba(0,0,0,0.16);border-bottom:1px solid rgba(255,255,255,0.05);flex-wrap:wrap;flex-shrink:0;';
var lbl = document.createElement('span');
lbl.style.cssText = 'font-size:.67rem;font-weight:700;color:rgba(255,255,255,0.28);text-transform:uppercase;letter-spacing:.07em;';
lbl.textContent = 'Интерактив:';
bar.appendChild(lbl);
var MODES = [
{ id: null, text: 'Стандартный' },
{ id: 'binary', text: 'Бинарные соединения' },
{ id: 'compare', text: 'Сравнить' },
{ id: 'activity', text: 'Ряд активности' },
{ id: 'mendeleev', text: 'Таблица 1869' },
{ id: 'timeline', text: 'Таймлайн' },
];
var BASE_S = 'padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:#777;font-size:.69rem;cursor:pointer;transition:all .15s;';
var ACT_S = 'background:rgba(6,214,224,0.16);color:#06D6E0;border-color:rgba(6,214,224,0.32);';
var self = this;
MODES.forEach(function(m) {
var btn = document.createElement('button');
btn.textContent = m.text;
btn.style.cssText = BASE_S + (m.id === null ? ACT_S : '');
btn.addEventListener('click', function() {
bar.querySelectorAll('button').forEach(function(b) { b.style.cssText = BASE_S; });
btn.style.cssText = BASE_S + ACT_S;
self._switchInteractiveMode(m.id);
});
bar.appendChild(btn);
});
var vbar = this._wrap.querySelector('#ptbl-vmodes-bar');
var anchor = vbar ? vbar.nextSibling : (this._wrap.children[1] ? this._wrap.children[1].nextSibling : null);
this._wrap.insertBefore(bar, anchor);
this._iModeBar = bar;
};
PeriodicTableSim.prototype._switchInteractiveMode = function(modeId) {
if (this._iModePanel && this._iModePanel.parentNode) {
this._iModePanel.parentNode.removeChild(this._iModePanel);
this._iModePanel = null;
}
if (this._iModeState && this._iModeState.raf) {
cancelAnimationFrame(this._iModeState.raf);
}
this._interactiveMode = modeId;
this._iModeState = {};
var self = this;
for (var i = 0; i < ELEMENTS.length; i++) {
var el = ELEMENTS[i];
var div = this._cellMap[el.Z];
if (!div) continue;
var clone = div.cloneNode(true);
(function(c, z) {
c.addEventListener('mouseenter', function() { c.style.filter = 'brightness(1.4)'; c.style.transform = 'scale(1.12)'; c.style.zIndex = '10'; });
c.addEventListener('mouseleave', function() { c.style.filter = ''; c.style.transform = ''; c.style.zIndex = ''; });
c.addEventListener('click', function() { self._selectElement(z); });
})(clone, el.Z);
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 = 'Кликните первый элемент (реагент A)
' +
'
';
this._wrap.appendChild(panel);
this._iModePanel = panel;
var self = this;
for (var i = 0; i < ELEMENTS.length; i++) {
var div = this._cellMap[ELEMENTS[i].Z];
if (!div) continue;
(function(z) { div.addEventListener('click', function() { self._binaryClick(z); }); })(ELEMENTS[i].Z);
}
};
PeriodicTableSim.prototype._binaryClick = function(Z) {
var st = this._iModeState;
var panel = this._iModePanel;
if (!panel) return;
var hint = panel.querySelector('.ptbl-bin-hint');
var res = panel.querySelector('.ptbl-bin-result');
var self = this;
if (!st.first) {
st.first = Z;
if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.0, volume: 0.2 });
ELEMENTS.forEach(function(el) {
var d = self._cellMap[el.Z]; if (!d) return;
d.style.outline = el.Z === Z ? '2px solid #FFD166' : '';
d.style.outlineOffset = el.Z === Z ? '1px' : '';
});
var elA = ELEMENTS.find(function(e) { return e.Z === Z; });
hint.textContent = 'Выбран: ' + elA.symbol + ' (' + elA.name + '). Кликните второй элемент.';
res.innerHTML = '';
} else if (st.first === Z) {
st.first = null;
ELEMENTS.forEach(function(el) { var d = self._cellMap[el.Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; } });
hint.textContent = 'Кликните первый элемент (реагент A)';
res.innerHTML = '';
} else {
if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.3, volume: 0.3 });
var elA2 = ELEMENTS.find(function(e) { return e.Z === st.first; });
var elB = ELEMENTS.find(function(e) { return e.Z === Z; });
ELEMENTS.forEach(function(el) {
var d = self._cellMap[el.Z]; if (!d) return;
if (el.Z === st.first) { d.style.outline = '2px solid #FFD166'; d.style.outlineOffset = '1px'; }
else if (el.Z === Z) { d.style.outline = '2px solid #06D6E0'; d.style.outlineOffset = '1px'; }
else { d.style.outline = ''; d.style.outlineOffset = ''; }
});
hint.textContent = elA2.symbol + ' + ' + elB.symbol;
res.innerHTML = self._binaryCalc(elA2, elB);
st.first = null;
}
};
PeriodicTableSim.prototype._binaryCalc = function(elA, elB) {
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
var oxA = (elA.oxStates || []).filter(function(s) { return s !== null && s !== 0; });
var oxB = (elB.oxStates || []).filter(function(s) { return s !== null && s !== 0; });
var enA = elA.En || 0, enB = elB.En || 0;
var dEN = Math.abs(enA - enB);
var bondType, bondColor;
if (dEN > 1.7) { bondType = 'ионная'; bondColor = '#FF6B35'; }
else if (dEN >= 0.4) { bondType = 'ковалентная полярная'; bondColor = '#7B8EF7'; }
else { bondType = 'ковалентная неполярная'; bondColor = '#7BF5A4'; }
var seen = {};
var formulas = [];
var srcA = oxA.length ? oxA : [0];
var srcB = oxB.length ? oxB : [0];
for (var ai = 0; ai < srcA.length; ai++) {
for (var bi = 0; bi < srcB.length; bi++) {
var vA = srcA[ai], vB = srcB[bi];
if (vA === 0 || vB === 0) continue;
if ((vA > 0 && vB > 0) || (vA < 0 && vB < 0)) continue;
var posEl = vA > 0 ? elA : elB;
var negEl = vA > 0 ? elB : elA;
var posV = Math.abs(vA > 0 ? vA : vB);
var negV = Math.abs(vA > 0 ? vB : vA);
var g = gcd(posV, negV);
var nPos = negV / g, nNeg = posV / g;
var key = posEl.symbol + nPos + negEl.symbol + nNeg;
if (seen[key]) continue;
seen[key] = true;
var formula = posEl.symbol + (nPos > 1 ? '' + nPos + ' ' : '') +
negEl.symbol + (nNeg > 1 ? '' + nNeg + ' ' : '');
var struct = (nPos === 1 && nNeg === 1) ? posEl.symbol + '–' + negEl.symbol :
(nPos === 1 && nNeg === 2) ? negEl.symbol + '–' + posEl.symbol + '–' + negEl.symbol :
formula;
formulas.push({ formula: formula, struct: struct });
}
}
if (!formulas.length) {
return 'Соединение не образуется (одинаковые знаки ст. окисления) ';
}
var html = '' +
'Тип связи: ' +
'' + bondType + ' ';
if (elA.En && elB.En) html += 'ΔЭО = ' + dEN.toFixed(2) + ' ';
html += '
';
formulas.forEach(function(f) {
html += '
' +
'
' + f.formula + '
' +
'
' + f.struct + '
';
});
html += '
';
return html;
};
/* ── MODE 2: COMPARE ELEMENTS ── */
PeriodicTableSim.prototype._modeCompare = function() {
this._iModeState = { selected: [] };
var panel = document.createElement('div');
panel.className = 'ptbl-imode-panel';
panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;overflow-x:auto;';
panel.innerHTML = '' +
'Выберите до 4 элементов ' +
'Очистить ' +
'График: ' +
'' +
'ЭО Масса ' +
'Плотность T пл. T кип. ' +
'
' +
'
' +
' ';
this._wrap.appendChild(panel);
this._iModePanel = panel;
var self = this;
panel.querySelector('.ptbl-cmp-clear').addEventListener('click', function() {
self._iModeState.selected = [];
ELEMENTS.forEach(function(el) { var d = self._cellMap[el.Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; } });
self._compareRefresh();
});
panel.querySelector('.ptbl-cmp-prop').addEventListener('change', function() { self._compareRefresh(); });
ELEMENTS.forEach(function(el) {
var div = self._cellMap[el.Z]; if (!div) return;
(function(z) { div.addEventListener('click', function() { self._compareClick(z); }); })(el.Z);
});
this._compareRefresh();
};
PeriodicTableSim.prototype._compareClick = function(Z) {
var st = this._iModeState;
var idx = st.selected.indexOf(Z);
if (idx >= 0) {
st.selected.splice(idx, 1);
var d = this._cellMap[Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; }
} else {
if (st.selected.length >= 4) return;
st.selected.push(Z);
if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.85 + st.selected.length * 0.1, volume: 0.2 });
var d2 = this._cellMap[Z];
if (d2) { d2.style.outline = '2px solid #06D6E0'; d2.style.outlineOffset = '1px'; }
}
this._compareRefresh();
};
PeriodicTableSim.prototype._compareRefresh = function() {
var panel = this._iModePanel; if (!panel) return;
var st = this._iModeState;
var propKey = panel.querySelector('.ptbl-cmp-prop').value;
var els = st.selected.map(function(z) { return ELEMENTS.find(function(e) { return e.Z === z; }); }).filter(Boolean);
var tbl = panel.querySelector('.ptbl-cmp-table');
if (!els.length) {
tbl.innerHTML = 'Кликайте элементы на таблице
';
this._compareDraw(panel.querySelector('.ptbl-cmp-chart'), els, propKey);
return;
}
var PROPS = [
{ key:'Z', label:'Z' }, { key:'mass', label:'Масса' },
{ key:'config', label:'Конфиг.' }, { key:'En', label:'ЭО (Полинг)' },
{ key:'density', label:'Плотн.' }, { key:'melt', label:'T пл. (K)' }, { key:'boil', label:'T кип. (K)' },
];
var fmt = function(v) { return (v !== null && v !== undefined) ? v : '—'; };
var html = 'Свойство ';
els.forEach(function(el) {
var col = TYPE_COLORS[el.type] || '#888';
html += '' + el.symbol + '' + el.name + ' ';
});
html += ' ';
PROPS.forEach(function(p) {
var vals = els.map(function(el) { return el[p.key]; });
var numVals = vals.filter(function(v) { return typeof v === 'number' && v !== null; });
var maxV = numVals.length > 1 ? Math.max.apply(null, numVals) : null;
var minV = numVals.length > 1 ? Math.min.apply(null, numVals) : null;
html += '' + p.label + ' ';
vals.forEach(function(v) {
var ex = '';
if (maxV !== null && typeof v === 'number') {
if (v === maxV) ex = 'background:rgba(123,245,164,0.1);color:#7BF5A4;';
else if (v === minV) ex = 'background:rgba(239,71,111,0.1);color:#EF476F;';
}
html += '' + fmt(v) + ' ';
});
html += ' ';
});
html += '
';
tbl.innerHTML = html;
this._compareDraw(panel.querySelector('.ptbl-cmp-chart'), els, propKey);
};
PeriodicTableSim.prototype._compareDraw = function(canvas, els, propKey) {
if (!canvas) return;
var dpr = window.devicePixelRatio || 1;
var W = canvas.offsetWidth || 300, H = canvas.offsetHeight || 52;
canvas.width = W * dpr; canvas.height = H * dpr;
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H);
if (!els.length) return;
var vals = els.map(function(e) { return e[propKey]; }).filter(function(v) { return v !== null && v !== undefined && isFinite(v); });
if (!vals.length) return;
var minV = Math.min.apply(null, vals), maxV = Math.max.apply(null, vals);
var pad = { t:4, r:14, b:15, l:5 };
var gW = W - pad.l - pad.r, gH = H - pad.t - pad.b;
ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(pad.l, pad.t); ctx.lineTo(pad.l, pad.t+gH); ctx.lineTo(pad.l+gW, pad.t+gH); ctx.stroke();
var step = els.length > 1 ? gW / (els.length - 1) : gW * 0.5;
els.forEach(function(el, i) {
var v = el[propKey];
if (v === null || v === undefined || !isFinite(v)) return;
var x = pad.l + i * step;
var y = pad.t + gH - ((v - minV) / (maxV - minV || 1)) * gH;
var col = TYPE_COLORS[el.type] || '#7B8EF7';
ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI*2);
ctx.fillStyle = col; ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.4)'; ctx.lineWidth = 1; ctx.stroke();
ctx.font = '9px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.textAlign = 'center'; ctx.fillText(el.symbol, x, H - 2);
});
};
/* ── MODE 3: ACTIVITY SERIES ── */
PeriodicTableSim.prototype._modeActivity = function() {
var SERIES = [
{ s:'Li', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
{ s:'Cs', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
{ s:'Rb', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
{ s:'K', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
{ s:'Ba', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
{ s:'Sr', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
{ s:'Ca', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
{ s:'Na', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
{ s:'Mg', t:'Реагирует с горячей H₂O; с разб. HCl', g:'active' },
{ s:'Al', t:'Реагирует с разб. HCl; пассивируется конц. H₂SO₄', g:'active' },
{ s:'Mn', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
{ s:'Zn', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
{ s:'Cr', t:'Реагирует с разб. HCl; пассивируется конц. H₂SO₄', g:'medium' },
{ s:'Fe', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
{ s:'Cd', t:'Реагирует с разб. HCl', g:'medium' },
{ s:'Co', t:'Реагирует с разб. HCl медленно', g:'medium' },
{ s:'Ni', t:'Реагирует с разб. HCl медленно', g:'medium' },
{ s:'Sn', t:'Реагирует с разб. HCl медленно', g:'medium' },
{ s:'Pb', t:'Слабо реагирует с разб. HCl', g:'medium' },
{ s:'H', t:'Разделитель: металлы левее вытесняют H₂ из кислот', g:'sep' },
{ s:'Sb', t:'Реагирует только с конц. кислотами', g:'low' },
{ s:'Bi', t:'Реагирует только с конц. кислотами', g:'low' },
{ s:'Cu', t:'Не реаг. с HCl; реаг. с конц. H₂SO₄, HNO₃', g:'low' },
{ s:'Hg', t:'Реагирует с конц. HNO₃ и H₂SO₄', g:'low' },
{ s:'Ag', t:'Реагирует с HNO₃', g:'low' },
{ s:'Pd', t:'Реагирует с царской водкой', g:'low' },
{ s:'Pt', t:'Реагирует только с царской водкой', g:'low' },
{ s:'Au', t:'Реагирует только с царской водкой', g:'low' },
];
var GC = {
active:{ bg:'rgba(239,71,111,0.1)', bd:'rgba(239,71,111,0.36)', c:'#EF476F', lbl:'Активные (H₂O)' },
medium:{ bg:'rgba(255,209,102,0.1)', bd:'rgba(255,209,102,0.36)', c:'#FFD166', lbl:'Средние (разб. кислоты)' },
low: { bg:'rgba(123,142,247,0.1)', bd:'rgba(123,142,247,0.36)', c:'#7B8EF7', lbl:'Малоакт. (конц. / цар. водка)' },
};
var panel = document.createElement('div');
panel.className = 'ptbl-imode-panel';
panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;overflow-x:auto;';
var legHtml = '';
['active','medium','low'].forEach(function(g) {
legHtml += '
' +
' ' + GC[g].lbl + '
';
});
legHtml += '
';
var rowHtml = '';
SERIES.forEach(function(item, i) {
if (item.g === 'sep') {
rowHtml += '
';
return;
}
if (i > 0 && SERIES[i-1].g !== 'sep') rowHtml += '
›
';
var gc = GC[item.g];
var el = ELEMENTS.find(function(e) { return e.symbol === item.s; });
var name = el ? el.name : item.s;
rowHtml += '
' +
'
' + item.s + '
' +
'
' + name + '
';
});
rowHtml += '
';
panel.innerHTML = legHtml + rowHtml + '
';
this._wrap.appendChild(panel);
this._iModePanel = panel;
var self = this;
panel.querySelectorAll('.ptbl-act-item').forEach(function(item) {
item.addEventListener('mouseenter', function() {
panel.querySelector('.ptbl-act-tip').textContent = item.title;
item.style.filter = 'brightness(1.5)'; item.style.transform = 'scale(1.08)';
var el = ELEMENTS.find(function(e) { return e.symbol === item.dataset.sym; });
if (el && self._cellMap[el.Z]) { self._cellMap[el.Z].style.outline = '2px solid #FFD166'; self._cellMap[el.Z].style.outlineOffset = '1px'; }
});
item.addEventListener('mouseleave', function() {
panel.querySelector('.ptbl-act-tip').textContent = '';
item.style.filter = ''; item.style.transform = '';
var el = ELEMENTS.find(function(e) { return e.symbol === item.dataset.sym; });
if (el && self._cellMap[el.Z]) { self._cellMap[el.Z].style.outline = ''; self._cellMap[el.Z].style.outlineOffset = ''; }
});
});
};
/* ── MODE 4: MENDELEEV 1869 ── */
PeriodicTableSim.prototype._modeMendeleev1869 = function() {
var KNOWN_1869 = new Set([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,
22,23,24,25,26,27,28,29,30,33,34,35,36,37,38,40,41,42,
44,45,47,48,50,51,52,53,55,56,57,60,62,64,65,66,68,70,72,74,76,78,79,80,82,83]);
var PREDS = {
31: { name:'Экаалюминий (Ga)', pred:{ mass:68, density:5.9, melt:302 }, act:{ mass:69.72, density:5.91, melt:303 }, year:1875, who:'Лекок де Буабодран (Франция)' },
21: { name:'Экабор (Sc)', pred:{ mass:44, density:3.5, melt:null }, act:{ mass:44.96, density:2.99, melt:1814 }, year:1879, who:'Ларс Нильсон (Швеция)' },
32: { name:'Экасилиций (Ge)', pred:{ mass:72, density:5.5, melt:null }, act:{ mass:72.63, density:5.32, melt:1211 }, year:1886, who:'Клеменс Винклер (Германия)' },
43: { name:'Экамарганец (Tc)', pred:{ mass:100, density:null, melt:null }, act:{ mass:98, density:11.5, melt:2430 }, year:1937, who:'Перье и Сегре (Италия)' },
};
var panel = document.createElement('div');
panel.className = 'ptbl-imode-panel';
panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;position:relative;min-height:38px;';
panel.innerHTML = '' +
'Таблица Менделеева 1869: 63 известных элемента. Фиол. «?» — предсказания Менделеева. Кликните «?».' +
'
' +
'';
this._wrap.appendChild(panel);
this._iModePanel = panel;
var self = this;
ELEMENTS.forEach(function(el) {
var div = self._cellMap[el.Z]; if (!div) return;
if (KNOWN_1869.has(el.Z)) {
div.style.opacity = '1';
} else if (PREDS[el.Z]) {
div.style.background = 'rgba(155,93,229,0.09)';
div.style.border = '1px dashed rgba(155,93,229,0.48)';
div.style.opacity = '1';
div.innerHTML = '? ' +
'' + el.symbol + ' ';
div.title = el.name + ' — предсказан Менделеевым (кликните)';
(function(z) { div.addEventListener('click', function() { self._m1869ShowPrediction(z, PREDS[z], panel); }); })(el.Z);
} else {
div.style.opacity = '0.13';
div.style.background = 'rgba(255,255,255,0.02)';
div.style.border = '1px solid rgba(255,255,255,0.05)';
}
});
};
PeriodicTableSim.prototype._m1869ShowPrediction = function(Z, pred, panel) {
if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.15, volume: 0.3 });
var popup = panel.querySelector('.ptbl-m1869-popup');
var fmt = function(v) { return (v !== null && v !== undefined) ? v : '—'; };
var diff = function(p, a) {
if (p == null || a == null || !isFinite(+p) || !isFinite(+a) || +a === 0) return '';
var pct = (Math.abs(+p - +a) / +a * 100).toFixed(1);
var good = Math.abs(+p - +a) / +a < 0.05;
return '(' + (good ? 'точно' : pct + '% откл.') + ') ';
};
popup.innerHTML = '' + pred.name + '
' +
'' +
'Свойство ' +
'Предсказано ' +
'Реально ' +
'Масса ' +
'' + fmt(pred.pred.mass) + ' ' +
'' + fmt(pred.act.mass) + diff(pred.pred.mass, pred.act.mass) + ' ' +
'Плотность ' +
'' + fmt(pred.pred.density) + ' ' +
'' + fmt(pred.act.density) + diff(pred.pred.density, pred.act.density) + ' ' +
'T пл. (K) ' +
'' + fmt(pred.pred.melt) + ' ' +
'' + fmt(pred.act.melt) + ' ' +
'
' +
'Открыт: ' + pred.year + ' г., ' + pred.who + '
' +
'Закрыть ';
popup.style.display = 'block';
};
/* ── MODE 5: TIMELINE ── */
PeriodicTableSim.prototype._modeTimeline = function() {
var MIN_Y = 1660, MAX_Y = 2024;
this._iModeState = { year: MAX_Y, playing: false, raf: null };
var panel = document.createElement('div');
panel.className = 'ptbl-imode-panel';
panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;';
panel.innerHTML = '' +
'
Год: ' +
'
' +
'
' + MAX_Y + ' ' +
'
' +
' Авто' +
' ' +
'
Открыто 0 / 118 ' +
'
' +
'
';
this._wrap.appendChild(panel);
this._iModePanel = panel;
var self = this;
var slider = panel.querySelector('.ptbl-tl-slider');
var yearLbl = panel.querySelector('.ptbl-tl-year');
var playBtn = panel.querySelector('.ptbl-tl-play');
var info = panel.querySelector('.ptbl-tl-info');
var countLbl = panel.querySelector('.ptbl-tl-count');
var update = function() {
var y = +slider.value;
self._iModeState.year = y;
yearLbl.textContent = y;
self._timelineUpdate(y, info, countLbl);
};
slider.addEventListener('input', update);
update();
playBtn.addEventListener('click', function() {
var st = self._iModeState;
if (st.playing) {
st.playing = false;
cancelAnimationFrame(st.raf);
playBtn.innerHTML = ' Авто';
} else {
if (+slider.value >= MAX_Y) slider.value = MIN_Y;
st.playing = true;
playBtn.innerHTML = ' Стоп';
var last = null;
var tick = function(ts) {
if (!st.playing) return;
if (!last) last = ts;
if (ts - last > 38) {
var cur = +slider.value;
if (cur >= MAX_Y) {
st.playing = false;
playBtn.innerHTML = ' Авто';
return;
}
slider.value = cur + 2;
update();
last = ts;
}
st.raf = requestAnimationFrame(tick);
};
st.raf = requestAnimationFrame(tick);
}
});
};
PeriodicTableSim.prototype._timelineUpdate = function(year, info, countLbl) {
var count = 0, lastEl = null, lastYear = -Infinity;
ELEMENTS.forEach(function(el) {
var div = this._cellMap[el.Z]; if (!div) return;
var known = (el.discovered === null) || (el.discovered <= year);
if (known) {
count++;
div.style.opacity = '1';
var col = TYPE_COLORS[el.type] || '#555';
div.style.background = col + '44';
div.style.border = '1px solid ' + col + '88';
div.style.outline = ''; div.style.outlineOffset = '';
if (el.discovered !== null && el.discovered <= year && el.discovered > lastYear) {
lastYear = el.discovered; lastEl = el;
}
} else {
div.style.opacity = '0.09';
div.style.background = 'rgba(255,255,255,0.015)';
div.style.border = '1px solid rgba(255,255,255,0.04)';
div.style.outline = ''; div.style.outlineOffset = '';
}
}, this);
if (countLbl) countLbl.textContent = 'Открыто ' + count + ' / 118';
if (lastEl && lastYear > -Infinity) {
var d = this._cellMap[lastEl.Z];
if (d) { d.style.outline = '2px solid #FFD166'; d.style.outlineOffset = '1px'; }
if (info) info.textContent = lastYear + ' г.: ' + lastEl.name + ' (' + lastEl.symbol + ') — ' + (lastEl.by || '?');
} else if (info) {
info.textContent = '';
}
};
var periodicSim = null;
function _openPeriodic() {
document.getElementById('sim-periodic').style.display = 'flex';
if (!periodicSim) {
periodicSim = new PeriodicTableSim(document.getElementById('periodic-wrap'));
}
}