'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 = `
Свойство:
`; 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=>``).join(''); this._chartN = 1; } else { nSel.innerHTML = [1,2,3,4,5,6,7].map(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 => ``).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]) => ``).join('')}
${l}${v}
`; } /* ── Tab 3: Электроника ── */ _renderTab_electronics(el) { return `
Электронная конфигурация ${el.config}
Оболочки (K, L, M, …) ${getShellFill(el.Z).join(' | ')}
`; } _postRenderElectronics(el) { const canvas = this._cardEl.querySelector('#ptbl-bohr-inline'); if (!canvas) return; const dpr = window.devicePixelRatio || 1; const W = canvas.offsetWidth || 220; const H = 160; canvas.width = W * dpr; canvas.height = H * dpr; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); const shells = getShellFill(el.Z); const col = TYPE_COLORS[el.type] || '#7B8EF7'; const cx = W / 2, cy = H / 2; const maxR = Math.min(W, H) * 0.44; const nShells = shells.length; ctx.beginPath(); ctx.arc(cx, cy, nShells > 0 ? 5 + nShells * 1.5 : 6, 0, Math.PI * 2); ctx.fillStyle = col; ctx.fill(); shells.forEach((count, i) => { const r = maxR * (i + 1) / nShells; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 1; ctx.stroke(); for (let e = 0; e < count; e++) { const a = (2 * Math.PI * e) / count - Math.PI / 2; const ex = cx + r * Math.cos(a); const ey = cy + r * Math.sin(a); ctx.beginPath(); ctx.arc(ex, ey, 2.5, 0, Math.PI * 2); ctx.fillStyle = '#06D6E0'; ctx.fill(); } }); ctx.font = `700 10px Manrope,sans-serif`; ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.textAlign = 'center'; ctx.fillText(shells.join(','), cx, H - 4); } /* ── Tab 4: Изотопы ── */ _renderTab_isotopes(el) { const isos = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || []; if (!isos.length) { return `

Данные об изотопах не загружены.

`; } let avgMass = null; const stableIsos = isos.filter(iso => iso.abundance != null && iso.abundance > 0); if (stableIsos.length) { const total = stableIsos.reduce((s, iso) => s + iso.abundance, 0); avgMass = stableIsos.reduce((s, iso) => s + iso.mass * iso.abundance, 0) / total; } const rows = isos.map(iso => { const abStr = iso.abundance != null ? (iso.abundance * 100).toFixed(2) + '%' : '—'; const hlStr = iso.halfLife || '—'; const decStr = iso.decay || '—'; return ` ${iso.massNum ?? iso.mass}${el.symbol} ${typeof iso.mass === 'number' ? iso.mass.toFixed(4) : (iso.mass ?? '—')} ${abStr} ${hlStr} ${decStr} `; }).join(''); return `
${rows}
ИзотопМассаРаспр.Распад
${avgMass != null ? `
Средняя атомная масса (взвеш.): ${avgMass.toFixed(4)} а.е.м.
` : ''}
`; } _postRenderIsotopesChart(el) { const canvas = this._cardEl.querySelector('#ptbl-iso-chart'); if (!canvas) return; const isos = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || []; const stable = isos.filter(iso => iso.abundance != null && iso.abundance > 0); if (!stable.length) return; const dpr = window.devicePixelRatio || 1; const W = canvas.offsetWidth || 220; const H = 70; canvas.width = W * dpr; canvas.height = H * dpr; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); const col = TYPE_COLORS[el.type] || '#7B8EF7'; const pad = { t: 4, r: 4, b: 18, l: 4 }; const gW = W - pad.l - pad.r; const gH = H - pad.t - pad.b; const n = stable.length; const bw = Math.max(2, (gW / n) - 2); const maxA = Math.max(...stable.map(iso => iso.abundance)); stable.forEach((iso, i) => { const bh = (iso.abundance / maxA) * gH; const x = pad.l + i * (gW / n) + (gW / n - bw) / 2; const y = pad.t + gH - bh; ctx.fillStyle = col + 'bb'; ctx.fillRect(x, y, bw, bh); ctx.font = '8px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.textAlign = 'center'; ctx.fillText(String(iso.massNum ?? ''), x + bw / 2, H - 4); }); } /* ── Tab 5: История ── */ _renderTab_history(el) { const year = el.discovered ? String(el.discovered) : null; const by = el.by || '—'; const hist = el.historyText || ''; const etym = el.etymology || ''; return `
${year || 'Древний мир'}
${by}
${el.discoveryCountry ? `
${el.discoveryCountry}
` : ''}
${hist ? `

${hist}

` : '

История не указана.

'} ${etym ? `
Этимология: ${etym}
` : ''}
`; } /* ── Tab 6: Применения ── */ _renderTab_applications(el) { const apps = el.applications || []; const desc = el.applicationsDescription || ''; const iconMap = { battery: '', medicine: '', electronics: '', metallurgy: '', construction: '', nuclear: '', lighting: '', catalyst: '', jewelry: '', fertilizer: '', semiconductor: '', pigment: '', aerospace: '', optical: '', food: '', }; const defaultIcon = ''; if (!apps.length && !desc) { return `

Применения не указаны.

`; } const cards = apps.map(tag => { const ico = iconMap[tag] || defaultIcon; const label = tag.charAt(0).toUpperCase() + tag.slice(1); return `
${ico}${label}
`; }).join(''); return `
${cards ? `
${cards}
` : ''} ${desc ? `

${desc}

` : ''}
`; } /* ── Tab 7: Биология ── */ _renderTab_biology(el) { const bio = el.biological || null; const role = el.biologicalRole || ''; const bioLabels = { macro: 'Макроэлемент (жизненно важен)', micro: 'Микроэлемент (жизненно важен)', trace: 'Следовой элемент', toxic: 'Токсичен для живых организмов', inert: 'Биологически инертен', radioactive: 'Радиоактивен / опасен', }; const bioColors = { macro: '#06D6E0', micro: '#7BF5A4', trace: '#FFD166', toxic: '#EF476F', inert: '#888', radioactive: '#F15BB5', }; const bioColor = bio ? (bioColors[bio] || '#888') : '#888'; const bioLabel = bio ? (bioLabels[bio] || bio) : 'Нет данных'; return `
${bioLabel}
${role ? `

${role}

` : '

Биологическая роль не указана.

'} ${el.toxicity ? `
Токсичность: ${el.toxicity}
` : ''}
`; } /* ── Tab 8: Минералы ── */ _renderTab_minerals(el) { const mins = el.mineralForms || []; const sources = el.mineralSources || ''; if (!mins.length && !sources) { return `

Данные о минералах не указаны.

`; } const items = mins.map(m => { const name = typeof m === 'object' ? (m.name || '—') : m; const formula = typeof m === 'object' ? (m.formula || '') : ''; return `
${name} ${formula ? `${formula}` : ''}
`; }).join(''); return `
${items ? `
${items}
` : ''} ${sources ? `

Источники: ${sources}

` : ''}
`; } /* ── Tab 9: Спектр ── */ _renderTab_spectrum(el) { if (!el.spectrum || !el.spectrum.length) { return `

Спектральные данные не указаны.

`; } return `
${el.spectrum.map(s => { const nm = typeof s === 'object' ? s.nm : s; return `${nm} нм`; }).join('')}
`; } _postRenderSpectrum(el) { const canvas = this._cardEl.querySelector('#ptbl-spec-canvas'); if (!canvas || !el.spectrum || !el.spectrum.length) return; const dpr = window.devicePixelRatio || 1; const W = canvas.offsetWidth || 220; const H = 80; canvas.width = W * dpr; canvas.height = H * dpr; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); /* rainbow background 380-780 nm */ const grad = ctx.createLinearGradient(0, 0, W, 0); grad.addColorStop(0, '#6600ff'); grad.addColorStop(0.10, '#4400ff'); grad.addColorStop(0.20, '#0000ff'); grad.addColorStop(0.30, '#00aaff'); grad.addColorStop(0.40, '#00ffcc'); grad.addColorStop(0.50, '#00ff00'); grad.addColorStop(0.60, '#aaff00'); grad.addColorStop(0.70, '#ffff00'); grad.addColorStop(0.80, '#ff8800'); grad.addColorStop(0.90, '#ff2200'); grad.addColorStop(1.0, '#880000'); ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H); ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fillRect(0, 0, W, H); el.spectrum.forEach(s => { const nm = typeof s === 'object' ? s.nm : s; const int = typeof s === 'object' ? (s.intensity ?? 1) : 1; if (!nm || nm < 380 || nm > 780) return; const x = ((nm - 380) / 400) * W; const col = this._nmToRGB(nm); ctx.strokeStyle = col; ctx.lineWidth = Math.max(1, int * 2); ctx.globalAlpha = 0.7 + int * 0.3; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H - 18); ctx.stroke(); ctx.globalAlpha = 1; ctx.font = '7px Manrope,sans-serif'; ctx.fillStyle = col; ctx.textAlign = 'center'; ctx.fillText(String(nm), x, H - 4); }); } _nmToRGB(nm) { if (nm < 380) return '#8800ff'; if (nm < 440) return `hsl(${270 + (nm - 380) * 0.5},100%,60%)`; if (nm < 490) return `hsl(${240 - (nm - 440) * 1.2},100%,55%)`; if (nm < 510) return `hsl(${180 - (nm - 490)},100%,50%)`; if (nm < 580) return `hsl(${120 - (nm - 510) * 0.7},100%,45%)`; if (nm < 645) return `hsl(${60 - (nm - 580) * 0.92},100%,50%)`; return `hsl(0,100%,${Math.max(20, 50 - (nm - 645) * 0.4)}%)`; } /* ── Tab 10: Пламя ── */ _renderTab_flame(el) { if (!el.flameColor) { return `

Данные об окраске пламени не указаны.

`; } return `
Окраска пламени: ${el.flameColorName || el.flameColor}

При внесении соединений ${el.name} в пламя горелки оно окрашивается в характерный цвет — применяется в качественном анализе и в пиротехнике.

`; } /* ── Tab 11: Реакции ── */ _renderTab_reactions(el) { const col = TYPE_COLORS[el.type] || '#888'; const reactionsByType = { alkali: [ { label: 'С водой', eq: `2${el.symbol} + 2H₂O → 2${el.symbol}OH + H₂↑` }, { label: 'С кислородом', eq: `4${el.symbol} + O₂ → 2${el.symbol}₂O` }, { label: 'С хлором', eq: `2${el.symbol} + Cl₂ → 2${el.symbol}Cl` }, ], alkaline: [ { label: 'С водой', eq: `${el.symbol} + 2H₂O → ${el.symbol}(OH)₂ + H₂↑` }, { label: 'С кислородом', eq: `2${el.symbol} + O₂ → 2${el.symbol}O` }, { label: 'С кислотой', eq: `${el.symbol} + 2HCl → ${el.symbol}Cl₂ + H₂↑` }, ], halogen: [ { label: 'С натрием', eq: `2Na + ${el.symbol}₂ → 2Na${el.symbol}` }, { label: 'С водородом', eq: `H₂ + ${el.symbol}₂ → 2H${el.symbol}` }, { label: 'С водой', eq: `${el.symbol}₂ + H₂O → H${el.symbol} + H${el.symbol}O` }, ], transition: [ { label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` }, { label: 'С кислотой', eq: `${el.symbol} + H₂SO₄ → ${el.symbol}SO₄ + H₂↑` }, ], nonmetal: [ { label: 'С металлом', eq: `Me + ${el.symbol} → Me–${el.symbol}` }, { label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` }, ], }; const reactions = reactionsByType[el.type] || [ { label: 'Реакции', eq: `${el.symbol} + реагент → продукт` }, ]; const items = reactions.map(r => `
${r.label}
${r.eq}
`).join(''); return `
${items}

Уравнения приведены в общем виде для ознакомления.

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