2552c8a90e
outline:2px + outlineOffset:1px давал 3px рамку поверх 2px-зазора → визуально перекрывал соседей. Заменил на inset box-shadow — рамка внутри ячейки.
3238 lines
175 KiB
JavaScript
3238 lines
175 KiB
JavaScript
'use strict';
|
||
/* ══════════════════════════════════════════════════════════════
|
||
PeriodicTableSim — Периодическая таблица (118 элементов)
|
||
Режимы: стандартный вид, подсветка по типам/блокам,
|
||
графики свойств, боровские оболочки, поиск элементов
|
||
══════════════════════════════════════════════════════════════ */
|
||
|
||
/* ── Element data ───────────────────────────────────────────── */
|
||
const ELEMENTS = [
|
||
{ Z:1, symbol:'H', name:'Водород', mass:1.008, group:1, period:1, block:'s', config:'1s¹', oxStates:[-1,+1], En:2.20, density:0.0899, melt:14.01, boil:20.28, type:'nonmetal', discovered:1766, by:'Кавендиш' },
|
||
{ Z:2, symbol:'He', name:'Гелий', mass:4.003, group:18, period:1, block:'s', config:'1s²', oxStates:[0], En:null, density:0.1786, melt:0.95, boil:4.22, type:'noble', discovered:1868, by:'Жансен' },
|
||
{ Z:3, symbol:'Li', name:'Литий', mass:6.941, group:1, period:2, block:'s', config:'[He]2s¹', oxStates:[+1], En:0.98, density:0.534, melt:453.65, boil:1603, type:'alkali', discovered:1817, by:'Арфведсон' },
|
||
{ Z:4, symbol:'Be', name:'Бериллий', mass:9.012, group:2, period:2, block:'s', config:'[He]2s²', oxStates:[+2], En:1.57, density:1.85, melt:1560, boil:2742, type:'alkaline', discovered:1798, by:'Воклен' },
|
||
{ Z:5, symbol:'B', name:'Бор', mass:10.811, group:13, period:2, block:'p', config:'[He]2s²2p¹', oxStates:[+3], En:2.04, density:2.34, melt:2349, boil:4200, type:'metalloid', discovered:1808, by:'Гей-Люссак' },
|
||
{ Z:6, symbol:'C', name:'Углерод', mass:12.011, group:14, period:2, block:'p', config:'[He]2s²2p²', oxStates:[-4,+4], En:2.55, density:2.267, melt:3823, boil:4098, type:'nonmetal', discovered:null, by:'Древний мир' },
|
||
{ Z:7, symbol:'N', name:'Азот', mass:14.007, group:15, period:2, block:'p', config:'[He]2s²2p³', oxStates:[-3,+5], En:3.04, density:1.251, melt:63.15, boil:77.36, type:'nonmetal', discovered:1772, by:'Резерфорд' },
|
||
{ Z:8, symbol:'O', name:'Кислород', mass:15.999, group:16, period:2, block:'p', config:'[He]2s²2p⁴', oxStates:[-2], En:3.44, density:1.429, melt:54.36, boil:90.20, type:'nonmetal', discovered:1774, by:'Пристли' },
|
||
{ Z:9, symbol:'F', name:'Фтор', mass:18.998, group:17, period:2, block:'p', config:'[He]2s²2p⁵', oxStates:[-1], En:3.98, density:1.696, melt:53.48, boil:85.03, type:'halogen', discovered:1886, by:'Муассан' },
|
||
{ Z:10, symbol:'Ne', name:'Неон', mass:20.180, group:18, period:2, block:'p', config:'[He]2s²2p⁶', oxStates:[0], En:null, density:0.9002, melt:24.56, boil:27.07, type:'noble', discovered:1898, by:'Рамзай' },
|
||
{ Z:11, symbol:'Na', name:'Натрий', mass:22.990, group:1, period:3, block:'s', config:'[Ne]3s¹', oxStates:[+1], En:0.93, density:0.971, melt:370.87, boil:1156, type:'alkali', discovered:1807, by:'Дэви' },
|
||
{ Z:12, symbol:'Mg', name:'Магний', mass:24.305, group:2, period:3, block:'s', config:'[Ne]3s²', oxStates:[+2], En:1.31, density:1.738, melt:923, boil:1363, type:'alkaline', discovered:1755, by:'Блэк' },
|
||
{ Z:13, symbol:'Al', name:'Алюминий', mass:26.982, group:13, period:3, block:'p', config:'[Ne]3s²3p¹', oxStates:[+3], En:1.61, density:2.70, melt:933.47, boil:2792, type:'posttransition',discovered:1825, by:'Эрстед' },
|
||
{ Z:14, symbol:'Si', name:'Кремний', mass:28.086, group:14, period:3, block:'p', config:'[Ne]3s²3p²', oxStates:[-4,+4], En:1.90, density:2.329, melt:1687, boil:3538, type:'metalloid', discovered:1824, by:'Берцелиус' },
|
||
{ Z:15, symbol:'P', name:'Фосфор', mass:30.974, group:15, period:3, block:'p', config:'[Ne]3s²3p³', oxStates:[-3,+5], En:2.19, density:1.823, melt:317.30, boil:553.65, type:'nonmetal', discovered:1669, by:'Бранд' },
|
||
{ Z:16, symbol:'S', name:'Сера', mass:32.065, group:16, period:3, block:'p', config:'[Ne]3s²3p⁴', oxStates:[-2,+6], En:2.58, density:2.07, melt:388.36, boil:717.87, type:'nonmetal', discovered:null, by:'Древний мир' },
|
||
{ Z:17, symbol:'Cl', name:'Хлор', mass:35.453, group:17, period:3, block:'p', config:'[Ne]3s²3p⁵', oxStates:[-1,+7], En:3.16, density:3.214, melt:171.65, boil:239.11, type:'halogen', discovered:1774, by:'Шееле' },
|
||
{ Z:18, symbol:'Ar', name:'Аргон', mass:39.948, group:18, period:3, block:'p', config:'[Ne]3s²3p⁶', oxStates:[0], En:null, density:1.784, melt:83.80, boil:87.30, type:'noble', discovered:1894, by:'Рэлей' },
|
||
{ Z:19, symbol:'K', name:'Калий', mass:39.098, group:1, period:4, block:'s', config:'[Ar]4s¹', oxStates:[+1], En:0.82, density:0.862, melt:336.53, boil:1032, type:'alkali', discovered:1807, by:'Дэви' },
|
||
{ Z:20, symbol:'Ca', name:'Кальций', mass:40.078, group:2, period:4, block:'s', config:'[Ar]4s²', oxStates:[+2], En:1.00, density:1.55, melt:1115, boil:1757, type:'alkaline', discovered:1808, by:'Дэви' },
|
||
{ Z:21, symbol:'Sc', name:'Скандий', mass:44.956, group:3, period:4, block:'d', config:'[Ar]3d¹4s²', oxStates:[+3], En:1.36, density:2.985, melt:1814, boil:3109, type:'transition', discovered:1879, by:'Нильсон' },
|
||
{ Z:22, symbol:'Ti', name:'Титан', mass:47.867, group:4, period:4, block:'d', config:'[Ar]3d²4s²', oxStates:[+4], En:1.54, density:4.507, melt:1941, boil:3560, type:'transition', discovered:1791, by:'Грегор' },
|
||
{ Z:23, symbol:'V', name:'Ванадий', mass:50.942, group:5, period:4, block:'d', config:'[Ar]3d³4s²', oxStates:[+5], En:1.63, density:6.11, melt:2183, boil:3680, type:'transition', discovered:1830, by:'Сефстрём' },
|
||
{ Z:24, symbol:'Cr', name:'Хром', mass:51.996, group:6, period:4, block:'d', config:'[Ar]3d⁵4s¹', oxStates:[+3,+6], En:1.66, density:7.19, melt:2180, boil:2944, type:'transition', discovered:1798, by:'Воклен' },
|
||
{ Z:25, symbol:'Mn', name:'Марганец', mass:54.938, group:7, period:4, block:'d', config:'[Ar]3d⁵4s²', oxStates:[+2,+7], En:1.55, density:7.21, melt:1519, boil:2334, type:'transition', discovered:1774, by:'Ган' },
|
||
{ Z:26, symbol:'Fe', name:'Железо', mass:55.845, group:8, period:4, block:'d', config:'[Ar]3d⁶4s²', oxStates:[+2,+3], En:1.83, density:7.874, melt:1811, boil:3134, type:'transition', discovered:null, by:'Древний мир' },
|
||
{ Z:27, symbol:'Co', name:'Кобальт', mass:58.933, group:9, period:4, block:'d', config:'[Ar]3d⁷4s²', oxStates:[+2,+3], En:1.88, density:8.90, melt:1768, boil:3200, type:'transition', discovered:1735, by:'Брандт' },
|
||
{ Z:28, symbol:'Ni', name:'Никель', mass:58.693, group:10, period:4, block:'d', config:'[Ar]3d⁸4s²', oxStates:[+2], En:1.91, density:8.908, melt:1728, boil:3186, type:'transition', discovered:1751, by:'Кронстедт' },
|
||
{ Z:29, symbol:'Cu', name:'Медь', mass:63.546, group:11, period:4, block:'d', config:'[Ar]3d¹⁰4s¹', oxStates:[+1,+2], En:1.90, density:8.96, melt:1357.77,boil:2835, type:'transition', discovered:null, by:'Древний мир' },
|
||
{ Z:30, symbol:'Zn', name:'Цинк', mass:65.38, group:12, period:4, block:'d', config:'[Ar]3d¹⁰4s²', oxStates:[+2], En:1.65, density:7.14, melt:692.68, boil:1180, type:'transition', discovered:1746, by:'Марграф' },
|
||
{ Z:31, symbol:'Ga', name:'Галлий', mass:69.723, group:13, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p¹', oxStates:[+3], En:1.81, density:5.91, melt:302.91, boil:2477, type:'posttransition',discovered:1875, by:'Де Буабодран' },
|
||
{ Z:32, symbol:'Ge', name:'Германий', mass:72.630, group:14, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p²', oxStates:[+4], En:2.01, density:5.323, melt:1211.40,boil:3106, type:'metalloid', discovered:1886, by:'Винклер' },
|
||
{ Z:33, symbol:'As', name:'Мышьяк', mass:74.922, group:15, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p³', oxStates:[-3,+5], En:2.18, density:5.776, melt:1090, boil:887, type:'metalloid', discovered:1250, by:'Альберт Великий' },
|
||
{ Z:34, symbol:'Se', name:'Селен', mass:78.971, group:16, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p⁴', oxStates:[-2,+6], En:2.55, density:4.809, melt:493.65, boil:958, type:'nonmetal', discovered:1817, by:'Берцелиус' },
|
||
{ Z:35, symbol:'Br', name:'Бром', mass:79.904, group:17, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p⁵', oxStates:[-1,+5], En:2.96, density:3.122, melt:265.95, boil:332.00, type:'halogen', discovered:1826, by:'Балар' },
|
||
{ Z:36, symbol:'Kr', name:'Криптон', mass:83.798, group:18, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p⁶', oxStates:[0], En:3.00, density:3.749, melt:115.79, boil:119.93, type:'noble', discovered:1898, by:'Рамзай' },
|
||
{ Z:37, symbol:'Rb', name:'Рубидий', mass:85.468, group:1, period:5, block:'s', config:'[Kr]5s¹', oxStates:[+1], En:0.82, density:1.532, melt:312.46, boil:961, type:'alkali', discovered:1861, by:'Бунзен' },
|
||
{ Z:38, symbol:'Sr', name:'Стронций', mass:87.62, group:2, period:5, block:'s', config:'[Kr]5s²', oxStates:[+2], En:0.95, density:2.64, melt:1050, boil:1655, type:'alkaline', discovered:1790, by:'Кроуфорд' },
|
||
{ Z:39, symbol:'Y', name:'Иттрий', mass:88.906, group:3, period:5, block:'d', config:'[Kr]4d¹5s²', oxStates:[+3], En:1.22, density:4.472, melt:1799, boil:3609, type:'transition', discovered:1794, by:'Гадолин' },
|
||
{ Z:40, symbol:'Zr', name:'Цирконий', mass:91.224, group:4, period:5, block:'d', config:'[Kr]4d²5s²', oxStates:[+4], En:1.33, density:6.52, melt:2128, boil:4682, type:'transition', discovered:1789, by:'Клапрот' },
|
||
{ Z:41, symbol:'Nb', name:'Ниобий', mass:92.906, group:5, period:5, block:'d', config:'[Kr]4d⁴5s¹', oxStates:[+5], En:1.6, density:8.57, melt:2750, boil:5017, type:'transition', discovered:1801, by:'Хатчетт' },
|
||
{ Z:42, symbol:'Mo', name:'Молибден', mass:95.95, group:6, period:5, block:'d', config:'[Kr]4d⁵5s¹', oxStates:[+6], En:2.16, density:10.28, melt:2896, boil:4912, type:'transition', discovered:1781, by:'Шееле' },
|
||
{ Z:43, symbol:'Tc', name:'Технеций', mass:98, group:7, period:5, block:'d', config:'[Kr]4d⁵5s²', oxStates:[+7], En:1.9, density:11.50, melt:2430, boil:4538, type:'transition', discovered:1937, by:'Перье' },
|
||
{ Z:44, symbol:'Ru', name:'Рутений', mass:101.07, group:8, period:5, block:'d', config:'[Kr]4d⁷5s¹', oxStates:[+4], En:2.2, density:12.45, melt:2607, boil:4423, type:'transition', discovered:1844, by:'Клаус' },
|
||
{ Z:45, symbol:'Rh', name:'Родий', mass:102.906, group:9, period:5, block:'d', config:'[Kr]4d⁸5s¹', oxStates:[+3], En:2.28, density:12.41, melt:2237, boil:3968, type:'transition', discovered:1803, by:'Воластон' },
|
||
{ Z:46, symbol:'Pd', name:'Палладий', mass:106.42, group:10, period:5, block:'d', config:'[Kr]4d¹⁰', oxStates:[+2], En:2.20, density:12.023, melt:1828.05,boil:3236, type:'transition', discovered:1803, by:'Воластон' },
|
||
{ Z:47, symbol:'Ag', name:'Серебро', mass:107.868, group:11, period:5, block:'d', config:'[Kr]4d¹⁰5s¹', oxStates:[+1], En:1.93, density:10.49, melt:1234.93,boil:2435, type:'transition', discovered:null, by:'Древний мир' },
|
||
{ Z:48, symbol:'Cd', name:'Кадмий', mass:112.414, group:12, period:5, block:'d', config:'[Kr]4d¹⁰5s²', oxStates:[+2], En:1.69, density:8.65, melt:594.22, boil:1040, type:'transition', discovered:1817, by:'Штромейер' },
|
||
{ Z:49, symbol:'In', name:'Индий', mass:114.818, group:13, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p¹', oxStates:[+3], En:1.78, density:7.31, melt:429.75, boil:2345, type:'posttransition',discovered:1863, by:'Рейх' },
|
||
{ Z:50, symbol:'Sn', name:'Олово', mass:118.710, group:14, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p²', oxStates:[+2,+4], En:1.96, density:7.287, melt:505.08, boil:2875, type:'posttransition',discovered:null, by:'Древний мир' },
|
||
{ Z:51, symbol:'Sb', name:'Сурьма', mass:121.760, group:15, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p³', oxStates:[-3,+5], En:2.05, density:6.697, melt:903.78, boil:1860, type:'metalloid', discovered:null, by:'Древний мир' },
|
||
{ Z:52, symbol:'Te', name:'Теллур', mass:127.60, group:16, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p⁴', oxStates:[-2,+6], En:2.1, density:6.24, melt:722.66, boil:1261, type:'metalloid', discovered:1782, by:'фон Райхенштайн' },
|
||
{ Z:53, symbol:'I', name:'Йод', mass:126.904, group:17, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p⁵', oxStates:[-1,+7], En:2.66, density:4.933, melt:386.85, boil:457.55, type:'halogen', discovered:1811, by:'Куртуа' },
|
||
{ Z:54, symbol:'Xe', name:'Ксенон', mass:131.293, group:18, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p⁶', oxStates:[0], En:2.6, density:5.894, melt:161.40, boil:165.05, type:'noble', discovered:1898, by:'Рамзай' },
|
||
{ Z:55, symbol:'Cs', name:'Цезий', mass:132.905, group:1, period:6, block:'s', config:'[Xe]6s¹', oxStates:[+1], En:0.79, density:1.873, melt:301.59, boil:944, type:'alkali', discovered:1860, by:'Бунзен' },
|
||
{ Z:56, symbol:'Ba', name:'Барий', mass:137.327, group:2, period:6, block:'s', config:'[Xe]6s²', oxStates:[+2], En:0.89, density:3.594, melt:1000, boil:2170, type:'alkaline', discovered:1808, by:'Дэви' },
|
||
{ Z:57, symbol:'La', name:'Лантан', mass:138.905, group:3, period:6, block:'f', config:'[Xe]5d¹6s²', oxStates:[+3], En:1.10, density:6.162, melt:1193, boil:3737, type:'lanthanide', discovered:1839, by:'Мосандер' },
|
||
{ Z:58, symbol:'Ce', name:'Церий', mass:140.116, group:null,period:6, block:'f', config:'[Xe]4f¹5d¹6s²', oxStates:[+3,+4], En:1.12, density:6.770, melt:1068, boil:3716, type:'lanthanide', discovered:1803, by:'Берцелиус' },
|
||
{ Z:59, symbol:'Pr', name:'Празеодим', mass:140.908, group:null,period:6, block:'f', config:'[Xe]4f³6s²', oxStates:[+3], En:1.13, density:6.77, melt:1208, boil:3793, type:'lanthanide', discovered:1885, by:'фон Вельсбах' },
|
||
{ Z:60, symbol:'Nd', name:'Неодим', mass:144.242, group:null,period:6, block:'f', config:'[Xe]4f⁴6s²', oxStates:[+3], En:1.14, density:7.01, melt:1297, boil:3347, type:'lanthanide', discovered:1885, by:'фон Вельсбах' },
|
||
{ Z:61, symbol:'Pm', name:'Прометий', mass:145, group:null,period:6, block:'f', config:'[Xe]4f⁵6s²', oxStates:[+3], En:1.13, density:7.26, melt:1315, boil:3273, type:'lanthanide', discovered:1945, by:'Маринский' },
|
||
{ Z:62, symbol:'Sm', name:'Самарий', mass:150.36, group:null,period:6, block:'f', config:'[Xe]4f⁶6s²', oxStates:[+2,+3], En:1.17, density:7.52, melt:1345, boil:2067, type:'lanthanide', discovered:1879, by:'Буабодран' },
|
||
{ Z:63, symbol:'Eu', name:'Европий', mass:151.964, group:null,period:6, block:'f', config:'[Xe]4f⁷6s²', oxStates:[+2,+3], En:1.20, density:5.244, melt:1099, boil:1802, type:'lanthanide', discovered:1901, by:'Демарсе' },
|
||
{ Z:64, symbol:'Gd', name:'Гадолиний', mass:157.25, group:null,period:6, block:'f', config:'[Xe]4f⁷5d¹6s²', oxStates:[+3], En:1.20, density:7.90, melt:1585, boil:3546, type:'lanthanide', discovered:1880, by:'Мариньяк' },
|
||
{ Z:65, symbol:'Tb', name:'Тербий', mass:158.925, group:null,period:6, block:'f', config:'[Xe]4f⁹6s²', oxStates:[+3], En:1.10, density:8.23, melt:1629, boil:3503, type:'lanthanide', discovered:1843, by:'Мосандер' },
|
||
{ Z:66, symbol:'Dy', name:'Диспрозий', mass:162.500, group:null,period:6, block:'f', config:'[Xe]4f¹⁰6s²', oxStates:[+3], En:1.22, density:8.540, melt:1680, boil:2840, type:'lanthanide', discovered:1886, by:'Буабодран' },
|
||
{ Z:67, symbol:'Ho', name:'Гольмий', mass:164.930, group:null,period:6, block:'f', config:'[Xe]4f¹¹6s²', oxStates:[+3], En:1.23, density:8.795, melt:1734, boil:2993, type:'lanthanide', discovered:1878, by:'Клеве' },
|
||
{ Z:68, symbol:'Er', name:'Эрбий', mass:167.259, group:null,period:6, block:'f', config:'[Xe]4f¹²6s²', oxStates:[+3], En:1.24, density:9.066, melt:1802, boil:3141, type:'lanthanide', discovered:1843, by:'Мосандер' },
|
||
{ Z:69, symbol:'Tm', name:'Тулий', mass:168.934, group:null,period:6, block:'f', config:'[Xe]4f¹³6s²', oxStates:[+3], En:1.25, density:9.32, melt:1818, boil:2223, type:'lanthanide', discovered:1879, by:'Клеве' },
|
||
{ Z:70, symbol:'Yb', name:'Иттербий', mass:173.054, group:null,period:6, block:'f', config:'[Xe]4f¹⁴6s²', oxStates:[+2,+3], En:1.10, density:6.90, melt:1097, boil:1469, type:'lanthanide', discovered:1878, by:'Мариньяк' },
|
||
{ Z:71, symbol:'Lu', name:'Лютеций', mass:174.967, group:3, period:6, block:'d', config:'[Xe]4f¹⁴5d¹6s²', oxStates:[+3], En:1.27, density:9.841, melt:1925, boil:3675, type:'lanthanide', discovered:1907, by:'Урбен' },
|
||
{ Z:72, symbol:'Hf', name:'Гафний', mass:178.49, group:4, period:6, block:'d', config:'[Xe]4f¹⁴5d²6s²', oxStates:[+4], En:1.3, density:13.31, melt:2506, boil:4876, type:'transition', discovered:1923, by:'Костер' },
|
||
{ Z:73, symbol:'Ta', name:'Тантал', mass:180.948, group:5, period:6, block:'d', config:'[Xe]4f¹⁴5d³6s²', oxStates:[+5], En:1.5, density:16.69, melt:3290, boil:5731, type:'transition', discovered:1802, by:'Экеберг' },
|
||
{ Z:74, symbol:'W', name:'Вольфрам', mass:183.84, group:6, period:6, block:'d', config:'[Xe]4f¹⁴5d⁴6s²', oxStates:[+6], En:2.36, density:19.25, melt:3695, boil:5828, type:'transition', discovered:1783, by:'Братья дель Риo' },
|
||
{ Z:75, symbol:'Re', name:'Рений', mass:186.207, group:7, period:6, block:'d', config:'[Xe]4f¹⁴5d⁵6s²', oxStates:[+7], En:1.9, density:21.02, melt:3459, boil:5869, type:'transition', discovered:1925, by:'Ноддак' },
|
||
{ Z:76, symbol:'Os', name:'Осмий', mass:190.23, group:8, period:6, block:'d', config:'[Xe]4f¹⁴5d⁶6s²', oxStates:[+4], En:2.2, density:22.59, melt:3306, boil:5285, type:'transition', discovered:1803, by:'Теннант' },
|
||
{ Z:77, symbol:'Ir', name:'Иридий', mass:192.217, group:9, period:6, block:'d', config:'[Xe]4f¹⁴5d⁷6s²', oxStates:[+4], En:2.20, density:22.56, melt:2719, boil:4701, type:'transition', discovered:1803, by:'Теннант' },
|
||
{ Z:78, symbol:'Pt', name:'Платина', mass:195.084, group:10, period:6, block:'d', config:'[Xe]4f¹⁴5d⁹6s¹', oxStates:[+2,+4], En:2.28, density:21.45, melt:2041.4, boil:4098, type:'transition', discovered:1735, by:'де Улоа' },
|
||
{ Z:79, symbol:'Au', name:'Золото', mass:196.967, group:11, period:6, block:'d', config:'[Xe]4f¹⁴5d¹⁰6s¹',oxStates:[+1,+3], En:2.54, density:19.30, melt:1337.33,boil:3129, type:'transition', discovered:null, by:'Древний мир' },
|
||
{ Z:80, symbol:'Hg', name:'Ртуть', mass:200.592, group:12, period:6, block:'d', config:'[Xe]4f¹⁴5d¹⁰6s²',oxStates:[+1,+2], En:2.00, density:13.534, melt:234.32, boil:629.88, type:'transition', discovered:null, by:'Древний мир' },
|
||
{ Z:81, symbol:'Tl', name:'Таллий', mass:204.38, group:13, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p¹',oxStates:[+1,+3],En:1.62,density:11.85, melt:577, boil:1746, type:'posttransition',discovered:1861, by:'Крукс' },
|
||
{ Z:82, symbol:'Pb', name:'Свинец', mass:207.2, group:14, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p²',oxStates:[+2,+4],En:2.33,density:11.34, melt:600.61, boil:2022, type:'posttransition',discovered:null, by:'Древний мир' },
|
||
{ Z:83, symbol:'Bi', name:'Висмут', mass:208.980, group:15, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p³',oxStates:[+3], En:2.02,density:9.747, melt:544.55, boil:1837, type:'posttransition',discovered:1753, by:'Жоффруа' },
|
||
{ Z:84, symbol:'Po', name:'Полоний', mass:209, group:16, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p⁴',oxStates:[+4], En:2.0, density:9.32, melt:527, boil:1235, type:'metalloid', discovered:1898, by:'Кюри' },
|
||
{ Z:85, symbol:'At', name:'Астат', mass:210, group:17, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p⁵',oxStates:[-1,+1],En:2.2, density:null, melt:575, boil:null, type:'halogen', discovered:1940, by:'Корсон' },
|
||
{ Z:86, symbol:'Rn', name:'Радон', mass:222, group:18, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p⁶',oxStates:[0], En:null,density:9.73, melt:202, boil:211.45, type:'noble', discovered:1900, by:'Дорн' },
|
||
{ Z:87, symbol:'Fr', name:'Франций', mass:223, group:1, period:7, block:'s', config:'[Rn]7s¹', oxStates:[+1], En:0.7, density:null, melt:300, boil:950, type:'alkali', discovered:1939, by:'Перей' },
|
||
{ Z:88, symbol:'Ra', name:'Радий', mass:226, group:2, period:7, block:'s', config:'[Rn]7s²', oxStates:[+2], En:0.9, density:5.0, melt:973, boil:2010, type:'alkaline', discovered:1898, by:'Кюри' },
|
||
{ Z:89, symbol:'Ac', name:'Актиний', mass:227, group:3, period:7, block:'f', config:'[Rn]6d¹7s²', oxStates:[+3], En:1.1, density:10.07, melt:1323, boil:3471, type:'actinide', discovered:1899, by:'Дебьерн' },
|
||
{ Z:90, symbol:'Th', name:'Торий', mass:232.038, group:null,period:7, block:'f', config:'[Rn]6d²7s²', oxStates:[+4], En:1.3, density:11.72, melt:2115, boil:5061, type:'actinide', discovered:1828, by:'Берцелиус' },
|
||
{ Z:91, symbol:'Pa', name:'Протактиний', mass:231.036, group:null,period:7, block:'f', config:'[Rn]5f²6d¹7s²', oxStates:[+5], En:1.5, density:15.37, melt:1841, boil:4300, type:'actinide', discovered:1913, by:'Фаянс' },
|
||
{ Z:92, symbol:'U', name:'Уран', mass:238.029, group:null,period:7, block:'f', config:'[Rn]5f³6d¹7s²', oxStates:[+6], En:1.38, density:19.05, melt:1405.3, boil:4404, type:'actinide', discovered:1789, by:'Клапрот' },
|
||
{ Z:93, symbol:'Np', name:'Нептуний', mass:237, group:null,period:7, block:'f', config:'[Rn]5f⁴6d¹7s²', oxStates:[+5], En:1.36, density:20.25, melt:913, boil:4273, type:'actinide', discovered:1940, by:'МакМиллан' },
|
||
{ Z:94, symbol:'Pu', name:'Плутоний', mass:244, group:null,period:7, block:'f', config:'[Rn]5f⁶7s²', oxStates:[+4], En:1.28, density:19.84, melt:912.5, boil:3501, type:'actinide', discovered:1940, by:'Сиборг' },
|
||
{ Z:95, symbol:'Am', name:'Америций', mass:243, group:null,period:7, block:'f', config:'[Rn]5f⁷7s²', oxStates:[+3], En:1.3, density:13.67, melt:1449, boil:2880, type:'actinide', discovered:1944, by:'Сиборг' },
|
||
{ Z:96, symbol:'Cm', name:'Кюрий', mass:247, group:null,period:7, block:'f', config:'[Rn]5f⁷6d¹7s²', oxStates:[+3], En:1.3, density:13.51, melt:1613, boil:3383, type:'actinide', discovered:1944, by:'Сиборг' },
|
||
{ Z:97, symbol:'Bk', name:'Берклий', mass:247, group:null,period:7, block:'f', config:'[Rn]5f⁹7s²', oxStates:[+3], En:1.3, density:14.79, melt:1259, boil:null, type:'actinide', discovered:1949, by:'Сиборг' },
|
||
{ Z:98, symbol:'Cf', name:'Калифорний', mass:251, group:null,period:7, block:'f', config:'[Rn]5f¹⁰7s²', oxStates:[+3], En:1.3, density:15.1, melt:1173, boil:null, type:'actinide', discovered:1950, by:'Сиборг' },
|
||
{ Z:99, symbol:'Es', name:'Эйнштейний', mass:252, group:null,period:7, block:'f', config:'[Rn]5f¹¹7s²', oxStates:[+3], En:1.3, density:null, melt:1133, boil:null, type:'actinide', discovered:1952, by:'Гиорсо' },
|
||
{ Z:100,symbol:'Fm', name:'Фермий', mass:257, group:null,period:7, block:'f', config:'[Rn]5f¹²7s²', oxStates:[+3], En:1.3, density:null, melt:1800, boil:null, type:'actinide', discovered:1952, by:'Гиорсо' },
|
||
{ Z:101,symbol:'Md', name:'Менделевий', mass:258, group:null,period:7, block:'f', config:'[Rn]5f¹³7s²', oxStates:[+3], En:1.3, density:null, melt:1100, boil:null, type:'actinide', discovered:1955, by:'Гиорсо' },
|
||
{ Z:102,symbol:'No', name:'Нобелий', mass:259, group:null,period:7, block:'f', config:'[Rn]5f¹⁴7s²', oxStates:[+2], En:1.3, density:null, melt:1100, boil:null, type:'actinide', discovered:1958, by:'Флёров' },
|
||
{ Z:103,symbol:'Lr', name:'Лоуренсий', mass:266, group:3, period:7, block:'d', config:'[Rn]5f¹⁴7s²7p¹', oxStates:[+3], En:1.3, density:null, melt:1900, boil:null, type:'actinide', discovered:1961, by:'Гиорсо' },
|
||
{ Z:104,symbol:'Rf', name:'Резерфордий', mass:267, group:4, period:7, block:'d', config:'[Rn]5f¹⁴6d²7s²', oxStates:[+4], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1964, by:'Флёров' },
|
||
{ Z:105,symbol:'Db', name:'Дубний', mass:268, group:5, period:7, block:'d', config:'[Rn]5f¹⁴6d³7s²', oxStates:[+5], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1968, by:'Флёров' },
|
||
{ Z:106,symbol:'Sg', name:'Сиборгий', mass:269, group:6, period:7, block:'d', config:'[Rn]5f¹⁴6d⁴7s²', oxStates:[+6], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1974, by:'Флёров' },
|
||
{ Z:107,symbol:'Bh', name:'Борий', mass:270, group:7, period:7, block:'d', config:'[Rn]5f¹⁴6d⁵7s²', oxStates:[+7], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1981, by:'ГСИ' },
|
||
{ Z:108,symbol:'Hs', name:'Хассий', mass:277, group:8, period:7, block:'d', config:'[Rn]5f¹⁴6d⁶7s²', oxStates:[+8], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1984, by:'ГСИ' },
|
||
{ Z:109,symbol:'Mt', name:'Мейтнерий', mass:278, group:9, period:7, block:'d', config:'[Rn]5f¹⁴6d⁷7s²', oxStates:[null], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1982, by:'ГСИ' },
|
||
{ Z:110,symbol:'Ds', name:'Дармштадтий', mass:281, group:10, period:7, block:'d', config:'[Rn]5f¹⁴6d⁸7s²', oxStates:[null], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1994, by:'ГСИ' },
|
||
{ Z:111,symbol:'Rg', name:'Рентгений', mass:282, group:11, period:7, block:'d', config:'[Rn]5f¹⁴6d⁹7s²', oxStates:[null], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1994, by:'ГСИ' },
|
||
{ Z:112,symbol:'Cn', name:'Коперниций', mass:285, group:12, period:7, block:'d', config:'[Rn]5f¹⁴6d¹⁰7s²',oxStates:[null], En:null, density:null, melt:null, boil:null, type:'transition', discovered:1996, by:'ГСИ' },
|
||
{ Z:113,symbol:'Nh', name:'Нихоний', mass:286, group:13, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p¹',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'posttransition',discovered:2004, by:'РИКЕН' },
|
||
{ Z:114,symbol:'Fl', name:'Флеровий', mass:289, group:14, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p²',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'posttransition',discovered:1998, by:'Флёров' },
|
||
{ Z:115,symbol:'Mc', name:'Московий', mass:290, group:15, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p³',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'posttransition',discovered:2003, by:'Флёров' },
|
||
{ Z:116,symbol:'Lv', name:'Ливерморий', mass:293, group:16, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p⁴',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'posttransition',discovered:2000, by:'Флёров' },
|
||
{ Z:117,symbol:'Ts', name:'Теннессин', mass:294, group:17, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p⁵',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'halogen', discovered:2010, by:'Флёров' },
|
||
{ Z:118,symbol:'Og', name:'Оганессон', mass:294, group:18, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p⁶',oxStates:[null],En:null, density:null, melt:null, boil:null, type:'noble', discovered:2002, by:'Флёров' },
|
||
];
|
||
|
||
/* ── Colour palette per type ──────────────────────────────── */
|
||
const TYPE_COLORS = {
|
||
alkali: '#EF476F',
|
||
alkaline: '#FF6B35',
|
||
transition: '#7B8EF7',
|
||
posttransition:'#06D6E0',
|
||
metalloid: '#7BF5A4',
|
||
nonmetal: '#FFD166',
|
||
halogen: '#C77DFF',
|
||
noble: '#A8DADC',
|
||
lanthanide: '#9B5DE5',
|
||
actinide: '#F15BB5',
|
||
metal: '#7B8EF7',
|
||
};
|
||
const TYPE_LABELS = {
|
||
alkali: 'Щелочные металлы',
|
||
alkaline: 'Щёлочноземельные',
|
||
transition: 'Переходные металлы',
|
||
posttransition:'Постпереходные',
|
||
metalloid: 'Металлоиды',
|
||
nonmetal: 'Неметаллы',
|
||
halogen: 'Галогены',
|
||
noble: 'Благородные газы',
|
||
lanthanide: 'Лантаноиды',
|
||
actinide: 'Актиноиды',
|
||
};
|
||
const BLOCK_COLORS = {
|
||
s: '#EF476F',
|
||
p: '#06D6E0',
|
||
d: '#7B8EF7',
|
||
f: '#9B5DE5',
|
||
};
|
||
|
||
/* ── Электронные оболочки (K,L,M,N,O,P,Q) ── */
|
||
const SHELL_CAPACITY = [2, 8, 18, 32, 32, 18, 8];
|
||
|
||
function getShellFill(Z) {
|
||
const caps = SHELL_CAPACITY;
|
||
const shells = [];
|
||
let rem = Z;
|
||
for (let i = 0; i < caps.length && rem > 0; i++) {
|
||
const n = Math.min(rem, caps[i]);
|
||
shells.push(n);
|
||
rem -= n;
|
||
}
|
||
return shells;
|
||
}
|
||
|
||
/* ── Layout helpers ──────────────────────────────────────────── */
|
||
/* Standard 18-column layout: returns {col, row} for each element */
|
||
function getCell(el) {
|
||
if (el.type === 'lanthanide' && el.Z !== 57 && el.Z !== 71) {
|
||
return { col: el.Z - 57 + 3, row: 9 }; // lanthanide row
|
||
}
|
||
if (el.type === 'actinide' && el.Z !== 89 && el.Z !== 103) {
|
||
return { col: el.Z - 89 + 3, row: 10 }; // actinide row
|
||
}
|
||
const g = el.group;
|
||
const p = el.period;
|
||
if (!g) return null;
|
||
return { col: g, row: p };
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
CLASS
|
||
══════════════════════════════════════════════════════════════ */
|
||
class PeriodicTableSim {
|
||
constructor(wrap) {
|
||
this._wrap = wrap;
|
||
this._mode = 'type'; // type | block | none
|
||
this._selected = null; // element Z
|
||
this._searchQ = '';
|
||
this._highlighted = new Set(); // Zs matching search
|
||
this._propKey = 'En'; // property for chart
|
||
this._chartBy = 'period';
|
||
this._chartN = 2; // period number or group number
|
||
this._bohrZ = null; // Z for Bohr shell panel
|
||
this._bohrRaf = null;
|
||
this._bohrAngle = 0;
|
||
|
||
// visual modes (wave B)
|
||
this._heatProp = 'En'; // heatmap property key
|
||
this._heatLog = false; // log scale
|
||
this._heatActive = false; // heatmap mode on
|
||
this._trendProp = 'radius'; // trend arrows property
|
||
this._trendOn = false; // trend arrows visible
|
||
this._tableShape = 'std'; // std | long | short
|
||
this._3dActive = false; // three.js 3D mode
|
||
this._3dMode = 'bar'; // bar | wave | stack
|
||
this._3dScene = null;
|
||
this._3dRaf = null;
|
||
this._heatTweens = []; // active tween handles
|
||
|
||
// build
|
||
this._buildUI();
|
||
this._buildTable();
|
||
this._updateCard(null);
|
||
this._buildVisualModes();
|
||
|
||
// chart defaults
|
||
this._drawChart();
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────
|
||
UI BUILD
|
||
───────────────────────────────────────────────────── */
|
||
_buildUI() {
|
||
this._wrap.innerHTML = '';
|
||
this._wrap.style.cssText = 'display:flex;flex-direction:column;height:100%;min-height:0;background:#0D0D1A;overflow:hidden;';
|
||
|
||
/* top toolbar */
|
||
const toolbar = document.createElement('div');
|
||
toolbar.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.08);flex-wrap:wrap;flex-shrink:0;';
|
||
toolbar.innerHTML = `
|
||
<span style="font-size:.72rem;font-weight:700;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:.07em">Режим:</span>
|
||
<button class="ptbl-mode-btn active" data-m="type">По типу</button>
|
||
<button class="ptbl-mode-btn" data-m="block">По блоку</button>
|
||
<button class="ptbl-mode-btn" data-m="none">Без подсветки</button>
|
||
<span style="margin-left:8px;font-size:.72rem;font-weight:700;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:.07em">Поиск:</span>
|
||
<input id="ptbl-search" type="text" placeholder="Символ / название / Z / масса"
|
||
style="padding:4px 8px;border-radius:6px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.06);color:#fff;font-size:.78rem;width:200px;outline:none;">
|
||
<button id="ptbl-search-clear" style="padding:3px 7px;border-radius:5px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#aaa;font-size:.72rem;cursor:pointer">
|
||
<svg class="ic" viewBox="0 0 24 24" style="width:12px;height:12px"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||
</button>`;
|
||
this._wrap.appendChild(toolbar);
|
||
|
||
/* mode buttons */
|
||
toolbar.querySelectorAll('.ptbl-mode-btn').forEach(btn => {
|
||
btn.style.cssText = 'padding:4px 10px;border-radius:6px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#aaa;font-size:.75rem;cursor:pointer;transition:all .15s';
|
||
btn.addEventListener('click', () => {
|
||
toolbar.querySelectorAll('.ptbl-mode-btn').forEach(b => { b.style.background='transparent'; b.style.color='#aaa'; b.classList.remove('active'); });
|
||
btn.style.background='rgba(155,93,229,0.25)'; btn.style.color='#fff'; btn.classList.add('active');
|
||
this._mode = btn.dataset.m;
|
||
if (window.LabFX) LabFX.sound.play('whoosh', { pitch: 1.2, volume: 0.25 });
|
||
this._colorTable();
|
||
});
|
||
});
|
||
/* set initial active style */
|
||
toolbar.querySelector('.ptbl-mode-btn.active').style.background = 'rgba(155,93,229,0.25)';
|
||
toolbar.querySelector('.ptbl-mode-btn.active').style.color = '#fff';
|
||
|
||
/* search */
|
||
const si = toolbar.querySelector('#ptbl-search');
|
||
si.addEventListener('input', () => { this._searchQ = si.value.trim().toLowerCase(); this._applySearch(); });
|
||
toolbar.querySelector('#ptbl-search-clear').addEventListener('click', () => { si.value=''; this._searchQ=''; this._applySearch(); });
|
||
|
||
/* main area: table + right panel */
|
||
const main = document.createElement('div');
|
||
main.style.cssText = 'display:flex;flex:1;min-height:0;overflow:hidden;';
|
||
this._wrap.appendChild(main);
|
||
|
||
/* left: table + legend */
|
||
const leftCol = document.createElement('div');
|
||
leftCol.style.cssText = 'flex:1;display:flex;flex-direction:column;min-width:0;overflow:auto;padding:8px 4px 8px 8px;';
|
||
main.appendChild(leftCol);
|
||
|
||
/* table grid container */
|
||
this._tableEl = document.createElement('div');
|
||
this._tableEl.id = 'ptbl-grid';
|
||
this._tableEl.style.cssText = 'display:grid;grid-template-columns:repeat(18,1fr);gap:2px;min-width:540px;';
|
||
leftCol.appendChild(this._tableEl);
|
||
|
||
/* gap filler row */
|
||
const gapDiv = document.createElement('div');
|
||
gapDiv.style.cssText = 'height:8px;';
|
||
leftCol.appendChild(gapDiv);
|
||
|
||
/* f-block (lanthanide/actinide) */
|
||
this._fblockEl = document.createElement('div');
|
||
this._fblockEl.id = 'ptbl-fblock';
|
||
this._fblockEl.style.cssText = 'display:grid;grid-template-columns:repeat(15,1fr);gap:2px;min-width:540px;margin-left:calc(2*100%/18 + 4px);';
|
||
leftCol.appendChild(this._fblockEl);
|
||
|
||
/* legend */
|
||
this._legendEl = document.createElement('div');
|
||
this._legendEl.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;min-width:540px;';
|
||
leftCol.appendChild(this._legendEl);
|
||
|
||
/* right panel */
|
||
const rightCol = document.createElement('div');
|
||
rightCol.style.cssText = 'width:280px;flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid rgba(255,255,255,0.07);overflow:hidden;';
|
||
main.appendChild(rightCol);
|
||
|
||
/* element card */
|
||
this._cardEl = document.createElement('div');
|
||
this._cardEl.id = 'ptbl-card';
|
||
this._cardEl.style.cssText = 'flex:1;overflow:hidden;display:flex;flex-direction:column;font-size:.78rem;color:#ccc;';
|
||
rightCol.appendChild(this._cardEl);
|
||
|
||
/* Bohr shells canvas */
|
||
const bohrWrap = document.createElement('div');
|
||
bohrWrap.style.cssText = 'height:150px;flex-shrink:0;border-top:1px solid rgba(255,255,255,0.07);background:rgba(0,0,0,0.3);position:relative;';
|
||
this._bohrCanvas = document.createElement('canvas');
|
||
this._bohrCanvas.style.cssText = 'width:100%;height:100%;';
|
||
bohrWrap.appendChild(this._bohrCanvas);
|
||
rightCol.appendChild(bohrWrap);
|
||
|
||
/* chart panel */
|
||
const chartPan = document.createElement('div');
|
||
chartPan.style.cssText = 'flex-shrink:0;border-top:1px solid rgba(255,255,255,0.07);padding:8px;background:rgba(0,0,0,0.2);';
|
||
chartPan.innerHTML = `
|
||
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:6px;">
|
||
<span style="font-size:.7rem;color:rgba(255,255,255,0.4)">Свойство:</span>
|
||
<select id="ptbl-prop-sel" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
|
||
<option value="En">ЭО (Полинг)</option>
|
||
<option value="mass">Масса</option>
|
||
<option value="melt">T плавл. (K)</option>
|
||
<option value="boil">T кип. (K)</option>
|
||
<option value="density">Плотность</option>
|
||
</select>
|
||
<select id="ptbl-by-sel" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
|
||
<option value="period">По периоду</option>
|
||
<option value="group">По группе</option>
|
||
</select>
|
||
<select id="ptbl-n-sel" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
|
||
${[1,2,3,4,5,6,7].map(n=>`<option value="${n}" ${n===2?'selected':''}>№ ${n}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<canvas id="ptbl-chart" style="width:100%;height:90px;display:block;"></canvas>`;
|
||
rightCol.appendChild(chartPan);
|
||
|
||
chartPan.querySelector('#ptbl-prop-sel').addEventListener('change', e => { this._propKey=e.target.value; this._drawChart(); });
|
||
chartPan.querySelector('#ptbl-by-sel').addEventListener('change', e => {
|
||
this._chartBy=e.target.value;
|
||
const nSel = chartPan.querySelector('#ptbl-n-sel');
|
||
if (this._chartBy === 'group') {
|
||
nSel.innerHTML = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18].map(n=>`<option value="${n}" ${n===1?'selected':''}>Гр. ${n}</option>`).join('');
|
||
this._chartN = 1;
|
||
} else {
|
||
nSel.innerHTML = [1,2,3,4,5,6,7].map(n=>`<option value="${n}" ${n===2?'selected':''}>№ ${n}</option>`).join('');
|
||
this._chartN = 2;
|
||
}
|
||
this._drawChart();
|
||
});
|
||
chartPan.querySelector('#ptbl-n-sel').addEventListener('change', e => { this._chartN=+e.target.value; this._drawChart(); });
|
||
|
||
this._chartCanvas = chartPan.querySelector('#ptbl-chart');
|
||
new ResizeObserver(() => this._drawChart()).observe(this._chartCanvas);
|
||
new ResizeObserver(() => this._drawBohr()).observe(this._bohrCanvas);
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────
|
||
TABLE BUILD
|
||
───────────────────────────────────────────────────── */
|
||
_buildTable() {
|
||
/* create placeholder grid for rows 1-7 × cols 1-18 */
|
||
const cells = {}; // 'row,col' → div
|
||
for (let r = 1; r <= 7; r++) {
|
||
for (let c = 1; c <= 18; c++) {
|
||
const d = document.createElement('div');
|
||
d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
|
||
cells[`${r},${c}`] = d;
|
||
this._tableEl.appendChild(d);
|
||
}
|
||
}
|
||
|
||
/* f-block rows */
|
||
const fCells = { 9: {}, 10: {} }; // lanthanides / actinides
|
||
for (let fc = 1; fc <= 15; fc++) {
|
||
for (const fr of [9, 10]) {
|
||
const d = document.createElement('div');
|
||
d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
|
||
fCells[fr][fc] = d;
|
||
this._fblockEl.appendChild(d);
|
||
}
|
||
}
|
||
|
||
/* place elements */
|
||
this._cellMap = {}; // Z → div
|
||
for (const el of ELEMENTS) {
|
||
const pos = getCell(el);
|
||
if (!pos) continue;
|
||
let div;
|
||
if (pos.row <= 7) {
|
||
div = cells[`${pos.row},${pos.col}`];
|
||
} else {
|
||
const fCol = pos.col - 2; // 3..17 → 1..15
|
||
div = fCells[pos.row][fCol];
|
||
}
|
||
if (!div) continue;
|
||
this._cellMap[el.Z] = div;
|
||
div.dataset.z = el.Z;
|
||
div.title = `${el.name} (${el.symbol})`;
|
||
div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;';
|
||
div.innerHTML = `
|
||
<span style="font-size:.55em;opacity:.7;line-height:1">${el.Z}</span>
|
||
<span style="font-size:.85em;font-weight:800;line-height:1.1">${el.symbol}</span>
|
||
<span style="font-size:.42em;opacity:.65;line-height:1.2;text-align:center;max-width:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${el.name}</span>`;
|
||
div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
|
||
div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
|
||
div.addEventListener('click', () => this._selectElement(el.Z));
|
||
}
|
||
|
||
this._colorTable();
|
||
this._buildLegend();
|
||
}
|
||
|
||
_colorTable() {
|
||
for (const el of ELEMENTS) {
|
||
const div = this._cellMap[el.Z];
|
||
if (!div) continue;
|
||
let bg, textC = '#fff';
|
||
if (this._mode === 'type') {
|
||
bg = TYPE_COLORS[el.type] || '#555';
|
||
} else if (this._mode === 'block') {
|
||
bg = BLOCK_COLORS[el.block] || '#555';
|
||
} else {
|
||
bg = '#2a2a3e';
|
||
}
|
||
div.style.background = bg + '33'; // 20% opacity base
|
||
div.style.border = `1px solid ${bg}88`;
|
||
div.style.color = '#fff';
|
||
if (this._highlighted.size > 0) {
|
||
if (this._highlighted.has(el.Z)) {
|
||
div.style.background = bg + 'cc';
|
||
div.style.border = `2px solid ${bg}`;
|
||
div.style.boxShadow = `0 0 8px ${bg}99`;
|
||
} else {
|
||
div.style.opacity = '0.25';
|
||
}
|
||
} else {
|
||
div.style.opacity = '1';
|
||
div.style.boxShadow = '';
|
||
}
|
||
}
|
||
this._buildLegend();
|
||
}
|
||
|
||
_buildLegend() {
|
||
this._legendEl.innerHTML = '';
|
||
const map = this._mode === 'block' ? BLOCK_COLORS : TYPE_COLORS;
|
||
const labels = this._mode === 'block' ? { s:'s-блок', p:'p-блок', d:'d-блок', f:'f-блок' } : TYPE_LABELS;
|
||
for (const [k, col] of Object.entries(map)) {
|
||
if (!labels[k]) continue;
|
||
const d = document.createElement('div');
|
||
d.style.cssText = `display:flex;align-items:center;gap:4px;font-size:.68rem;color:#bbb;cursor:pointer;`;
|
||
d.innerHTML = `<span style="width:12px;height:12px;border-radius:3px;background:${col};display:inline-block;flex-shrink:0;"></span>${labels[k]}`;
|
||
/* highlight on hover */
|
||
d.addEventListener('mouseenter', () => this._highlightType(k));
|
||
d.addEventListener('mouseleave', () => this._unhighlightType());
|
||
this._legendEl.appendChild(d);
|
||
}
|
||
}
|
||
|
||
_highlightType(key) {
|
||
for (const el of ELEMENTS) {
|
||
const div = this._cellMap[el.Z];
|
||
if (!div) continue;
|
||
const match = this._mode === 'block' ? el.block === key : el.type === key;
|
||
if (match) {
|
||
div.style.filter = 'brightness(1.5)';
|
||
div.style.transform = 'scale(1.05)';
|
||
div.style.zIndex = '5';
|
||
} else {
|
||
div.style.opacity = '0.2';
|
||
}
|
||
}
|
||
}
|
||
_unhighlightType() {
|
||
for (const el of ELEMENTS) {
|
||
const div = this._cellMap[el.Z];
|
||
if (!div) continue;
|
||
div.style.filter = '';
|
||
div.style.transform = '';
|
||
div.style.zIndex = '';
|
||
div.style.opacity = this._highlighted.has(el.Z) ? '1' : (this._highlighted.size > 0 ? '0.25' : '1');
|
||
}
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────
|
||
SELECT / SEARCH
|
||
───────────────────────────────────────────────────── */
|
||
_selectElement(Z) {
|
||
this._selected = Z;
|
||
if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.8 + Z * 0.008, volume: 0.25 });
|
||
/* highlight selected cell */
|
||
for (const el of ELEMENTS) {
|
||
const div = this._cellMap[el.Z];
|
||
if (!div) continue;
|
||
if (el.Z === Z) {
|
||
div.style.boxShadow = 'inset 0 0 0 2px #fff';
|
||
} else {
|
||
div.style.boxShadow = '';
|
||
}
|
||
}
|
||
this._updateCard(ELEMENTS.find(e => e.Z === Z));
|
||
this._bohrZ = Z;
|
||
this._startBohr();
|
||
}
|
||
|
||
_applySearch() {
|
||
this._highlighted.clear();
|
||
if (this._searchQ) {
|
||
for (const el of ELEMENTS) {
|
||
const q = this._searchQ;
|
||
if (
|
||
el.symbol.toLowerCase() === q ||
|
||
el.name.toLowerCase().includes(q) ||
|
||
String(el.Z) === q ||
|
||
String(el.mass).startsWith(q)
|
||
) {
|
||
this._highlighted.add(el.Z);
|
||
}
|
||
}
|
||
}
|
||
this._colorTable();
|
||
/* auto-select if single result */
|
||
if (this._highlighted.size === 1) {
|
||
this._selectElement([...this._highlighted][0]);
|
||
}
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────
|
||
ELEMENT CARD (legacy — no-selection state only)
|
||
───────────────────────────────────────────────────── */
|
||
_updateCard(el) {
|
||
if (!el) {
|
||
this._cardEl.innerHTML = `<div style="color:rgba(255,255,255,0.25);font-size:.8rem;text-align:center;padding:20px 0">Кликните на элемент</div>`;
|
||
this._bohrZ = null;
|
||
cancelAnimationFrame(this._bohrRaf);
|
||
this._bohrRaf = null;
|
||
this._drawBohr();
|
||
return;
|
||
}
|
||
this._renderCardV2(el);
|
||
}
|
||
|
||
_row(label, val) {
|
||
return `<tr>
|
||
<td style="padding:3px 4px 3px 0;color:rgba(255,255,255,0.45);border-bottom:1px solid rgba(255,255,255,0.05);white-space:nowrap">${label}</td>
|
||
<td style="padding:3px 0 3px 4px;color:#ddd;border-bottom:1px solid rgba(255,255,255,0.05)">${val}</td>
|
||
</tr>`;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────
|
||
TABBED CARD V2
|
||
───────────────────────────────────────────────────── */
|
||
|
||
/* Tab definitions: id, label */
|
||
_cardTabs() {
|
||
return [
|
||
{ id: 'overview', label: 'Обзор' },
|
||
{ id: 'properties', label: 'Свойства' },
|
||
{ id: 'electronics', label: 'Электроника' },
|
||
{ id: 'isotopes', label: 'Изотопы' },
|
||
{ id: 'history', label: 'История' },
|
||
{ id: 'applications', label: 'Применения' },
|
||
{ id: 'biology', label: 'Биология' },
|
||
{ id: 'minerals', label: 'Минералы' },
|
||
{ id: 'spectrum', label: 'Спектр' },
|
||
{ id: 'flame', label: 'Пламя' },
|
||
{ id: 'reactions', label: 'Реакции' },
|
||
];
|
||
}
|
||
|
||
_renderCardV2(el) {
|
||
const col = TYPE_COLORS[el.type] || '#888';
|
||
this._cardActiveTab = this._cardActiveTab || 'overview';
|
||
|
||
/* build outer shell */
|
||
this._cardEl.innerHTML = `
|
||
<div class="ptbl-card-v2" style="--el-col:${col}">
|
||
<button class="ptbl-card-close" aria-label="Закрыть">
|
||
<svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||
</button>
|
||
<div class="ptbl-hero">
|
||
<div class="ptbl-hero-z">${el.Z}</div>
|
||
<div class="ptbl-hero-sym">${el.symbol}</div>
|
||
<div class="ptbl-hero-name">${el.name}</div>
|
||
<div class="ptbl-hero-mass">${el.mass} а.е.м.</div>
|
||
<div class="ptbl-hero-badge" style="background:${col}22;border-color:${col}55;color:${col}">${TYPE_LABELS[el.type] || el.type}</div>
|
||
</div>
|
||
<div class="ptbl-tabs" role="tablist">
|
||
${this._cardTabs().map(t => `<button class="ptbl-tab${t.id === this._cardActiveTab ? ' active' : ''}" data-tab="${t.id}" role="tab">${t.label}</button>`).join('')}
|
||
</div>
|
||
<div class="ptbl-tab-body" id="ptbl-tab-body"></div>
|
||
</div>`;
|
||
|
||
/* close button */
|
||
this._cardEl.querySelector('.ptbl-card-close').addEventListener('click', () => {
|
||
this._updateCard(null);
|
||
for (const e2 of ELEMENTS) {
|
||
const div = this._cellMap[e2.Z];
|
||
if (div) { div.style.boxShadow = ''; div.style.outline = ''; div.style.outlineOffset = ''; }
|
||
}
|
||
});
|
||
|
||
/* tab switching */
|
||
this._cardEl.querySelectorAll('.ptbl-tab').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
this._cardActiveTab = btn.dataset.tab;
|
||
this._cardEl.querySelectorAll('.ptbl-tab').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
this._switchTabContent(el, btn.dataset.tab);
|
||
});
|
||
});
|
||
|
||
/* render initial tab */
|
||
this._switchTabContent(el, this._cardActiveTab);
|
||
}
|
||
|
||
_switchTabContent(el, tabId) {
|
||
const body = this._cardEl.querySelector('#ptbl-tab-body');
|
||
if (!body) return;
|
||
body.style.opacity = '0';
|
||
setTimeout(() => {
|
||
let html = '';
|
||
switch (tabId) {
|
||
case 'overview': html = this._renderTab_overview(el); break;
|
||
case 'properties': html = this._renderTab_properties(el); break;
|
||
case 'electronics': html = this._renderTab_electronics(el); break;
|
||
case 'isotopes': html = this._renderTab_isotopes(el); break;
|
||
case 'history': html = this._renderTab_history(el); break;
|
||
case 'applications': html = this._renderTab_applications(el); break;
|
||
case 'biology': html = this._renderTab_biology(el); break;
|
||
case 'minerals': html = this._renderTab_minerals(el); break;
|
||
case 'spectrum': html = this._renderTab_spectrum(el); break;
|
||
case 'flame': html = this._renderTab_flame(el); break;
|
||
case 'reactions': html = this._renderTab_reactions(el); break;
|
||
default: html = this._renderTab_overview(el);
|
||
}
|
||
body.innerHTML = html;
|
||
body.style.opacity = '1';
|
||
if (tabId === 'electronics') this._postRenderElectronics(el);
|
||
if (tabId === 'isotopes') this._postRenderIsotopesChart(el);
|
||
if (tabId === 'spectrum') this._postRenderSpectrum(el);
|
||
if (tabId === 'reactions' && window.renderMathInElement) {
|
||
renderMathInElement(body, { delimiters: [{ left: '$$', right: '$$', display: false }] });
|
||
}
|
||
}, 100);
|
||
}
|
||
|
||
/* ── Tab 1: Обзор ── */
|
||
_renderTab_overview(el) {
|
||
const col = TYPE_COLORS[el.type] || '#888';
|
||
const fmt = v => (v !== null && v !== undefined) ? v : '—';
|
||
const ox = el.oxStates && el.oxStates[0] !== null
|
||
? el.oxStates.map(s => (s > 0 ? '+' : '') + s).join(', ')
|
||
: '—';
|
||
const summary = el.summary || el.description || '';
|
||
return `
|
||
<div class="ptbl-overview">
|
||
${summary ? `<p class="ptbl-summary">${summary}</p>` : ''}
|
||
<div class="ptbl-quick-grid">
|
||
<div class="ptbl-quick-item">
|
||
<span class="ptbl-quick-label">Тип</span>
|
||
<span class="ptbl-quick-val" style="color:${col}">${TYPE_LABELS[el.type] || el.type}</span>
|
||
</div>
|
||
<div class="ptbl-quick-item">
|
||
<span class="ptbl-quick-label">Конфигурация</span>
|
||
<span class="ptbl-quick-val"><code>${el.config}</code></span>
|
||
</div>
|
||
<div class="ptbl-quick-item">
|
||
<span class="ptbl-quick-label">Ст. окисления</span>
|
||
<span class="ptbl-quick-val">${ox}</span>
|
||
</div>
|
||
<div class="ptbl-quick-item">
|
||
<span class="ptbl-quick-label">ЭО (Полинг)</span>
|
||
<span class="ptbl-quick-val">${fmt(el.En)}</span>
|
||
</div>
|
||
<div class="ptbl-quick-item">
|
||
<span class="ptbl-quick-label">Период / Группа</span>
|
||
<span class="ptbl-quick-val">${el.period} / ${el.group || '—'}</span>
|
||
</div>
|
||
<div class="ptbl-quick-item">
|
||
<span class="ptbl-quick-label">Блок</span>
|
||
<span class="ptbl-quick-val">${el.block}-блок</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Tab 2: Свойства ── */
|
||
_renderTab_properties(el) {
|
||
const fmt = v => (v !== null && v !== undefined) ? v : '—';
|
||
const rows = [
|
||
['Атомная масса, а.е.м.', fmt(el.mass)],
|
||
['Плотность, г/см³', fmt(el.density)],
|
||
['T<sub>пл</sub>, K', fmt(el.melt)],
|
||
['T<sub>кип</sub>, K', fmt(el.boil)],
|
||
['ЭО (Полинг)', fmt(el.En)],
|
||
['Атомный радиус, пм', el.radius?.atomic ?? '—'],
|
||
['Ковалентный радиус, пм',el.radius?.covalent ?? '—'],
|
||
['Ионный радиус, пм', el.radius?.ionic ?? '—'],
|
||
['1-я эн. ионизации, эВ', el.ionization?.[0] ?? '—'],
|
||
['2-я эн. ионизации, эВ', el.ionization?.[1] ?? '—'],
|
||
['Электронное сродство', el.electronAffinity ?? '—'],
|
||
['Теплоёмкость, Дж/(г·K)',el.heatCapacity ?? '—'],
|
||
['Теплопроводность', el.thermalConductivity ?? '—'],
|
||
['Электросопротивление', el.electricalResistivity ?? '—'],
|
||
['Кристаллич. структура', el.crystalStructure ?? '—'],
|
||
['Параметр решётки, пм', el.latticeParam ?? '—'],
|
||
['Открыт', el.discovered ? `${el.discovered}, ${el.by}` : (el.by || '—')],
|
||
];
|
||
return `
|
||
<table class="ptbl-prop-table">
|
||
<tbody>
|
||
${rows.map(([l, v]) => `<tr><td>${l}</td><td>${v}</td></tr>`).join('')}
|
||
</tbody>
|
||
</table>`;
|
||
}
|
||
|
||
/* ── Tab 3: Электроника ── */
|
||
_renderTab_electronics(el) {
|
||
return `
|
||
<div class="ptbl-electronics">
|
||
<div class="ptbl-bohr-inline-wrap">
|
||
<canvas id="ptbl-bohr-inline" style="width:100%;height:160px;display:block;"></canvas>
|
||
</div>
|
||
<div class="ptbl-el-config">
|
||
<span class="ptbl-quick-label">Электронная конфигурация</span>
|
||
<code class="ptbl-el-config-code">${el.config}</code>
|
||
</div>
|
||
<div class="ptbl-el-config" style="margin-top:6px;">
|
||
<span class="ptbl-quick-label">Оболочки (K, L, M, …)</span>
|
||
<code class="ptbl-el-config-code">${getShellFill(el.Z).join(' | ')}</code>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
_postRenderElectronics(el) {
|
||
const canvas = this._cardEl.querySelector('#ptbl-bohr-inline');
|
||
if (!canvas) return;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const W = canvas.offsetWidth || 220;
|
||
const H = 160;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr);
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
const shells = getShellFill(el.Z);
|
||
const col = TYPE_COLORS[el.type] || '#7B8EF7';
|
||
const cx = W / 2, cy = H / 2;
|
||
const maxR = Math.min(W, H) * 0.44;
|
||
const nShells = shells.length;
|
||
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, nShells > 0 ? 5 + nShells * 1.5 : 6, 0, Math.PI * 2);
|
||
ctx.fillStyle = col;
|
||
ctx.fill();
|
||
|
||
shells.forEach((count, i) => {
|
||
const r = maxR * (i + 1) / nShells;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
for (let e = 0; e < count; e++) {
|
||
const a = (2 * Math.PI * e) / count - Math.PI / 2;
|
||
const ex = cx + r * Math.cos(a);
|
||
const ey = cy + r * Math.sin(a);
|
||
ctx.beginPath();
|
||
ctx.arc(ex, ey, 2.5, 0, Math.PI * 2);
|
||
ctx.fillStyle = '#06D6E0';
|
||
ctx.fill();
|
||
}
|
||
});
|
||
|
||
ctx.font = `700 10px Manrope,sans-serif`;
|
||
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(shells.join(','), cx, H - 4);
|
||
}
|
||
|
||
/* ── Tab 4: Изотопы ── */
|
||
_renderTab_isotopes(el) {
|
||
const isos = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || [];
|
||
if (!isos.length) {
|
||
return `<p class="ptbl-empty">Данные об изотопах не загружены.</p>`;
|
||
}
|
||
|
||
let avgMass = null;
|
||
const stableIsos = isos.filter(iso => iso.abundance != null && iso.abundance > 0);
|
||
if (stableIsos.length) {
|
||
const total = stableIsos.reduce((s, iso) => s + iso.abundance, 0);
|
||
avgMass = stableIsos.reduce((s, iso) => s + iso.mass * iso.abundance, 0) / total;
|
||
}
|
||
|
||
const rows = isos.map(iso => {
|
||
const abStr = iso.abundance != null ? (iso.abundance * 100).toFixed(2) + '%' : '—';
|
||
const hlStr = iso.halfLife || '—';
|
||
const decStr = iso.decay || '—';
|
||
return `<tr>
|
||
<td><sup>${iso.massNum ?? iso.mass}</sup>${el.symbol}</td>
|
||
<td>${typeof iso.mass === 'number' ? iso.mass.toFixed(4) : (iso.mass ?? '—')}</td>
|
||
<td>${abStr}</td>
|
||
<td>${hlStr}</td>
|
||
<td>${decStr}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
|
||
return `
|
||
<div class="ptbl-isotopes">
|
||
<table class="ptbl-prop-table ptbl-iso-table">
|
||
<thead><tr><th>Изотоп</th><th>Масса</th><th>Распр.</th><th>T½</th><th>Распад</th></tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
${avgMass != null
|
||
? `<div class="ptbl-iso-avg">Средняя атомная масса (взвеш.): <strong>${avgMass.toFixed(4)}</strong> а.е.м.</div>`
|
||
: ''}
|
||
<canvas id="ptbl-iso-chart" style="width:100%;height:70px;margin-top:8px;display:block;"></canvas>
|
||
</div>`;
|
||
}
|
||
|
||
_postRenderIsotopesChart(el) {
|
||
const canvas = this._cardEl.querySelector('#ptbl-iso-chart');
|
||
if (!canvas) return;
|
||
const isos = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || [];
|
||
const stable = isos.filter(iso => iso.abundance != null && iso.abundance > 0);
|
||
if (!stable.length) return;
|
||
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const W = canvas.offsetWidth || 220;
|
||
const H = 70;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr);
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
const col = TYPE_COLORS[el.type] || '#7B8EF7';
|
||
const pad = { t: 4, r: 4, b: 18, l: 4 };
|
||
const gW = W - pad.l - pad.r;
|
||
const gH = H - pad.t - pad.b;
|
||
const n = stable.length;
|
||
const bw = Math.max(2, (gW / n) - 2);
|
||
const maxA = Math.max(...stable.map(iso => iso.abundance));
|
||
|
||
stable.forEach((iso, i) => {
|
||
const bh = (iso.abundance / maxA) * gH;
|
||
const x = pad.l + i * (gW / n) + (gW / n - bw) / 2;
|
||
const y = pad.t + gH - bh;
|
||
ctx.fillStyle = col + 'bb';
|
||
ctx.fillRect(x, y, bw, bh);
|
||
ctx.font = '8px Manrope,sans-serif';
|
||
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(String(iso.massNum ?? ''), x + bw / 2, H - 4);
|
||
});
|
||
}
|
||
|
||
/* ── Tab 5: История ── */
|
||
_renderTab_history(el) {
|
||
const year = el.discovered ? String(el.discovered) : null;
|
||
const by = el.by || '—';
|
||
const hist = el.historyText || '';
|
||
const etym = el.etymology || '';
|
||
|
||
return `
|
||
<div class="ptbl-history">
|
||
<div class="ptbl-timeline-card">
|
||
<div class="ptbl-timeline-year">${year || 'Древний мир'}</div>
|
||
<div class="ptbl-timeline-who">${by}</div>
|
||
${el.discoveryCountry ? `<div class="ptbl-timeline-country">${el.discoveryCountry}</div>` : ''}
|
||
</div>
|
||
${hist ? `<p class="ptbl-hist-text">${hist}</p>` : '<p class="ptbl-empty">История не указана.</p>'}
|
||
${etym ? `<div class="ptbl-etymology"><span class="ptbl-quick-label">Этимология: </span>${etym}</div>` : ''}
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Tab 6: Применения ── */
|
||
_renderTab_applications(el) {
|
||
const apps = el.applications || [];
|
||
const desc = el.applicationsDescription || '';
|
||
const iconMap = {
|
||
battery: '<svg class="ic" viewBox="0 0 24 24"><rect x="2" y="7" width="18" height="10" rx="2"/><path d="M22 11v2"/><line x1="7" y1="12" x2="11" y2="12"/><line x1="9" y1="10" x2="9" y2="14"/></svg>',
|
||
medicine: '<svg class="ic" viewBox="0 0 24 24"><path d="M12 2a5 5 0 0 1 5 5v1h2a1 1 0 0 1 1 1v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9a1 1 0 0 1 1-1h2V7a5 5 0 0 1 5-5z"/><line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/></svg>',
|
||
electronics: '<svg class="ic" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><circle cx="9" cy="9" r="1"/><circle cx="15" cy="9" r="1"/><circle cx="9" cy="15" r="1"/><circle cx="15" cy="15" r="1"/></svg>',
|
||
metallurgy: '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/><line x1="12" y1="10" x2="12" y2="16"/></svg>',
|
||
construction: '<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="12" width="18" height="8" rx="1"/><path d="M3 12l4-8h10l4 8"/></svg>',
|
||
nuclear: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="9" fill="none"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="3" y1="12" x2="21" y2="12"/></svg>',
|
||
lighting: '<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 1 7 7c0 2.5-1.3 4.7-3.3 6L15 17H9l-.7-2C6.3 13.7 5 11.5 5 9a7 7 0 0 1 7-7z"/></svg>',
|
||
catalyst: '<svg class="ic" viewBox="0 0 24 24"><path d="M8 3L4 10h16L16 3z"/><path d="M4 14l4 7h8l4-7z"/></svg>',
|
||
jewelry: '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 16 9 22 9 17 14 19 21 12 17 5 21 7 14 2 9 8 9"/></svg>',
|
||
fertilizer: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/></svg>',
|
||
semiconductor: '<svg class="ic" viewBox="0 0 24 24"><rect x="7" y="7" width="10" height="10" rx="1"/><line x1="7" y1="12" x2="3" y2="12"/><line x1="17" y1="12" x2="21" y2="12"/><line x1="12" y1="7" x2="12" y2="3"/><line x1="12" y1="17" x2="12" y2="21"/></svg>',
|
||
pigment: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 3c2.5 2 4 5 4 9s-1.5 7-4 9"/></svg>',
|
||
aerospace: '<svg class="ic" viewBox="0 0 24 24"><path d="M12 2l2 6h6l-5 4 2 6-5-4-5 4 2-6-5-4h6z"/></svg>',
|
||
optical: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/></svg>',
|
||
food: '<svg class="ic" viewBox="0 0 24 24"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>',
|
||
};
|
||
const defaultIcon = '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';
|
||
|
||
if (!apps.length && !desc) {
|
||
return `<p class="ptbl-empty">Применения не указаны.</p>`;
|
||
}
|
||
|
||
const cards = apps.map(tag => {
|
||
const ico = iconMap[tag] || defaultIcon;
|
||
const label = tag.charAt(0).toUpperCase() + tag.slice(1);
|
||
return `<div class="ptbl-app-chip">${ico}<span>${label}</span></div>`;
|
||
}).join('');
|
||
|
||
return `
|
||
<div class="ptbl-applications">
|
||
${cards ? `<div class="ptbl-app-grid">${cards}</div>` : ''}
|
||
${desc ? `<p class="ptbl-app-desc">${desc}</p>` : ''}
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Tab 7: Биология ── */
|
||
_renderTab_biology(el) {
|
||
const bio = el.biological || null;
|
||
const role = el.biologicalRole || '';
|
||
const bioLabels = {
|
||
macro: 'Макроэлемент (жизненно важен)',
|
||
micro: 'Микроэлемент (жизненно важен)',
|
||
trace: 'Следовой элемент',
|
||
toxic: 'Токсичен для живых организмов',
|
||
inert: 'Биологически инертен',
|
||
radioactive: 'Радиоактивен / опасен',
|
||
};
|
||
const bioColors = {
|
||
macro: '#06D6E0', micro: '#7BF5A4', trace: '#FFD166',
|
||
toxic: '#EF476F', inert: '#888', radioactive: '#F15BB5',
|
||
};
|
||
const bioColor = bio ? (bioColors[bio] || '#888') : '#888';
|
||
const bioLabel = bio ? (bioLabels[bio] || bio) : 'Нет данных';
|
||
|
||
return `
|
||
<div class="ptbl-biology">
|
||
<div class="ptbl-bio-badge" style="border-color:${bioColor};color:${bioColor}">${bioLabel}</div>
|
||
${role ? `<p class="ptbl-bio-role">${role}</p>` : '<p class="ptbl-empty">Биологическая роль не указана.</p>'}
|
||
${el.toxicity ? `<div class="ptbl-bio-toxicity"><span class="ptbl-quick-label">Токсичность: </span>${el.toxicity}</div>` : ''}
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Tab 8: Минералы ── */
|
||
_renderTab_minerals(el) {
|
||
const mins = el.mineralForms || [];
|
||
const sources = el.mineralSources || '';
|
||
|
||
if (!mins.length && !sources) {
|
||
return `<p class="ptbl-empty">Данные о минералах не указаны.</p>`;
|
||
}
|
||
|
||
const items = mins.map(m => {
|
||
const name = typeof m === 'object' ? (m.name || '—') : m;
|
||
const formula = typeof m === 'object' ? (m.formula || '') : '';
|
||
return `<div class="ptbl-mineral-item">
|
||
<span class="ptbl-mineral-name">${name}</span>
|
||
${formula ? `<code class="ptbl-mineral-formula">${formula}</code>` : ''}
|
||
</div>`;
|
||
}).join('');
|
||
|
||
return `
|
||
<div class="ptbl-minerals">
|
||
${items ? `<div class="ptbl-mineral-list">${items}</div>` : ''}
|
||
${sources ? `<p class="ptbl-mineral-sources"><span class="ptbl-quick-label">Источники: </span>${sources}</p>` : ''}
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Tab 9: Спектр ── */
|
||
_renderTab_spectrum(el) {
|
||
if (!el.spectrum || !el.spectrum.length) {
|
||
return `<p class="ptbl-empty">Спектральные данные не указаны.</p>`;
|
||
}
|
||
return `
|
||
<div class="ptbl-spectrum">
|
||
<canvas id="ptbl-spec-canvas" style="width:100%;height:80px;display:block;"></canvas>
|
||
<div class="ptbl-spec-lines-list">
|
||
${el.spectrum.map(s => {
|
||
const nm = typeof s === 'object' ? s.nm : s;
|
||
return `<span class="ptbl-spec-tag">${nm} нм</span>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
_postRenderSpectrum(el) {
|
||
const canvas = this._cardEl.querySelector('#ptbl-spec-canvas');
|
||
if (!canvas || !el.spectrum || !el.spectrum.length) return;
|
||
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const W = canvas.offsetWidth || 220;
|
||
const H = 80;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr);
|
||
|
||
/* rainbow background 380-780 nm */
|
||
const grad = ctx.createLinearGradient(0, 0, W, 0);
|
||
grad.addColorStop(0, '#6600ff');
|
||
grad.addColorStop(0.10, '#4400ff');
|
||
grad.addColorStop(0.20, '#0000ff');
|
||
grad.addColorStop(0.30, '#00aaff');
|
||
grad.addColorStop(0.40, '#00ffcc');
|
||
grad.addColorStop(0.50, '#00ff00');
|
||
grad.addColorStop(0.60, '#aaff00');
|
||
grad.addColorStop(0.70, '#ffff00');
|
||
grad.addColorStop(0.80, '#ff8800');
|
||
grad.addColorStop(0.90, '#ff2200');
|
||
grad.addColorStop(1.0, '#880000');
|
||
ctx.fillStyle = grad;
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
ctx.fillStyle = 'rgba(0,0,0,0.55)';
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
el.spectrum.forEach(s => {
|
||
const nm = typeof s === 'object' ? s.nm : s;
|
||
const int = typeof s === 'object' ? (s.intensity ?? 1) : 1;
|
||
if (!nm || nm < 380 || nm > 780) return;
|
||
const x = ((nm - 380) / 400) * W;
|
||
const col = this._nmToRGB(nm);
|
||
ctx.strokeStyle = col;
|
||
ctx.lineWidth = Math.max(1, int * 2);
|
||
ctx.globalAlpha = 0.7 + int * 0.3;
|
||
ctx.beginPath();
|
||
ctx.moveTo(x, 0);
|
||
ctx.lineTo(x, H - 18);
|
||
ctx.stroke();
|
||
ctx.globalAlpha = 1;
|
||
ctx.font = '7px Manrope,sans-serif';
|
||
ctx.fillStyle = col;
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(String(nm), x, H - 4);
|
||
});
|
||
}
|
||
|
||
_nmToRGB(nm) {
|
||
if (nm < 380) return '#8800ff';
|
||
if (nm < 440) return `hsl(${270 + (nm - 380) * 0.5},100%,60%)`;
|
||
if (nm < 490) return `hsl(${240 - (nm - 440) * 1.2},100%,55%)`;
|
||
if (nm < 510) return `hsl(${180 - (nm - 490)},100%,50%)`;
|
||
if (nm < 580) return `hsl(${120 - (nm - 510) * 0.7},100%,45%)`;
|
||
if (nm < 645) return `hsl(${60 - (nm - 580) * 0.92},100%,50%)`;
|
||
return `hsl(0,100%,${Math.max(20, 50 - (nm - 645) * 0.4)}%)`;
|
||
}
|
||
|
||
/* ── Tab 10: Пламя ── */
|
||
_renderTab_flame(el) {
|
||
if (!el.flameColor) {
|
||
return `<p class="ptbl-empty">Данные об окраске пламени не указаны.</p>`;
|
||
}
|
||
return `
|
||
<div class="ptbl-flame">
|
||
<div class="ptbl-flame-swatch" style="background:${el.flameColor};box-shadow:0 0 24px ${el.flameColor}88"></div>
|
||
<div class="ptbl-flame-label">Окраска пламени: <strong>${el.flameColorName || el.flameColor}</strong></div>
|
||
<p class="ptbl-flame-desc">
|
||
При внесении соединений ${el.name} в пламя горелки оно окрашивается
|
||
в характерный цвет — применяется в качественном анализе и в пиротехнике.
|
||
</p>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Tab 11: Реакции ── */
|
||
_renderTab_reactions(el) {
|
||
const col = TYPE_COLORS[el.type] || '#888';
|
||
const reactionsByType = {
|
||
alkali: [
|
||
{ label: 'С водой', eq: `2${el.symbol} + 2H₂O → 2${el.symbol}OH + H₂↑` },
|
||
{ label: 'С кислородом', eq: `4${el.symbol} + O₂ → 2${el.symbol}₂O` },
|
||
{ label: 'С хлором', eq: `2${el.symbol} + Cl₂ → 2${el.symbol}Cl` },
|
||
],
|
||
alkaline: [
|
||
{ label: 'С водой', eq: `${el.symbol} + 2H₂O → ${el.symbol}(OH)₂ + H₂↑` },
|
||
{ label: 'С кислородом', eq: `2${el.symbol} + O₂ → 2${el.symbol}O` },
|
||
{ label: 'С кислотой', eq: `${el.symbol} + 2HCl → ${el.symbol}Cl₂ + H₂↑` },
|
||
],
|
||
halogen: [
|
||
{ label: 'С натрием', eq: `2Na + ${el.symbol}₂ → 2Na${el.symbol}` },
|
||
{ label: 'С водородом', eq: `H₂ + ${el.symbol}₂ → 2H${el.symbol}` },
|
||
{ label: 'С водой', eq: `${el.symbol}₂ + H₂O → H${el.symbol} + H${el.symbol}O` },
|
||
],
|
||
transition: [
|
||
{ label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` },
|
||
{ label: 'С кислотой', eq: `${el.symbol} + H₂SO₄ → ${el.symbol}SO₄ + H₂↑` },
|
||
],
|
||
nonmetal: [
|
||
{ label: 'С металлом', eq: `Me + ${el.symbol} → Me–${el.symbol}` },
|
||
{ label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` },
|
||
],
|
||
};
|
||
const reactions = reactionsByType[el.type] || [
|
||
{ label: 'Реакции', eq: `${el.symbol} + реагент → продукт` },
|
||
];
|
||
|
||
const items = reactions.map(r => `
|
||
<div class="ptbl-reaction-item">
|
||
<div class="ptbl-reaction-label">${r.label}</div>
|
||
<div class="ptbl-reaction-eq" style="color:${col}">${r.eq}</div>
|
||
</div>`).join('');
|
||
|
||
return `
|
||
<div class="ptbl-reactions">
|
||
${items}
|
||
<p class="ptbl-reaction-note">Уравнения приведены в общем виде для ознакомления.</p>
|
||
</div>`;
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────
|
||
BOHR SHELLS ANIMATION
|
||
───────────────────────────────────────────────────── */
|
||
_startBohr() {
|
||
cancelAnimationFrame(this._bohrRaf);
|
||
this._bohrAngle = 0;
|
||
this._animBohr();
|
||
}
|
||
|
||
_animBohr() {
|
||
this._bohrAngle += 0.018;
|
||
this._drawBohr();
|
||
this._bohrRaf = requestAnimationFrame(() => this._animBohr());
|
||
}
|
||
|
||
_drawBohr() {
|
||
const canvas = this._bohrCanvas;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const W = canvas.offsetWidth || 240;
|
||
const H = canvas.offsetHeight || 150;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr);
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
if (!this._bohrZ) return;
|
||
|
||
const el = ELEMENTS.find(e => e.Z === this._bohrZ);
|
||
if (!el) return;
|
||
|
||
const shells = getShellFill(el.Z);
|
||
const cx = W / 2, cy = H / 2;
|
||
const maxR = Math.min(W, H) * 0.44;
|
||
const nShells = shells.length;
|
||
const col = TYPE_COLORS[el.type] || '#7B8EF7';
|
||
|
||
/* nucleus */
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, nShells > 0 ? 5 + nShells * 1.5 : 6, 0, Math.PI * 2);
|
||
ctx.fillStyle = col;
|
||
ctx.fill();
|
||
|
||
shells.forEach((count, i) => {
|
||
const r = maxR * (i + 1) / nShells;
|
||
/* orbit ring */
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
|
||
/* electrons */
|
||
const speed = 1 - i * 0.12; // inner shells faster
|
||
for (let e = 0; e < count; e++) {
|
||
const a = this._bohrAngle * speed + (2 * Math.PI * e) / count;
|
||
const ex = cx + r * Math.cos(a);
|
||
const ey = cy + r * Math.sin(a);
|
||
ctx.beginPath();
|
||
ctx.arc(ex, ey, 2.5, 0, Math.PI * 2);
|
||
ctx.fillStyle = '#06D6E0';
|
||
ctx.fill();
|
||
}
|
||
});
|
||
|
||
/* label */
|
||
ctx.font = `700 10px Manrope,sans-serif`;
|
||
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(shells.join(','), cx, H - 4);
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────
|
||
PROPERTY CHART
|
||
───────────────────────────────────────────────────── */
|
||
_drawChart() {
|
||
const canvas = this._chartCanvas;
|
||
if (!canvas) return;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const W = canvas.offsetWidth || 240;
|
||
const H = canvas.offsetHeight || 90;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr);
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
/* filter elements */
|
||
let els;
|
||
if (this._chartBy === 'period') {
|
||
els = ELEMENTS.filter(e => e.period === this._chartN && e.group !== null);
|
||
els.sort((a, b) => a.group - b.group);
|
||
} else {
|
||
els = ELEMENTS.filter(e => e.group === this._chartN);
|
||
els.sort((a, b) => a.period - b.period);
|
||
}
|
||
|
||
const vals = els.map(e => e[this._propKey]);
|
||
const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v));
|
||
if (validVals.length < 2) {
|
||
ctx.font = '11px Manrope,sans-serif';
|
||
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText('Нет данных', W / 2, H / 2);
|
||
return;
|
||
}
|
||
|
||
const minV = Math.min(...validVals);
|
||
const maxV = Math.max(...validVals);
|
||
const pad = { t: 10, r: 8, b: 20, l: 8 };
|
||
const gW = W - pad.l - pad.r;
|
||
const gH = H - pad.t - pad.b;
|
||
|
||
/* axes */
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath();
|
||
ctx.moveTo(pad.l, pad.t);
|
||
ctx.lineTo(pad.l, pad.t + gH);
|
||
ctx.lineTo(pad.l + gW, pad.t + gH);
|
||
ctx.stroke();
|
||
|
||
/* line */
|
||
ctx.beginPath();
|
||
let first = true;
|
||
const points = [];
|
||
els.forEach((el, i) => {
|
||
const v = el[this._propKey];
|
||
if (v === null || v === undefined || !isFinite(v)) return;
|
||
const x = pad.l + (i / Math.max(els.length - 1, 1)) * gW;
|
||
const y = pad.t + gH - ((v - minV) / (maxV - minV || 1)) * gH;
|
||
points.push({ x, y, el });
|
||
if (first) { ctx.moveTo(x, y); first = false; } else ctx.lineTo(x, y);
|
||
});
|
||
ctx.strokeStyle = '#9B5DE5';
|
||
ctx.lineWidth = 1.5;
|
||
ctx.stroke();
|
||
|
||
/* markers */
|
||
points.forEach(({ x, y, el }) => {
|
||
const col = TYPE_COLORS[el.type] || '#7B8EF7';
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, 3, 0, Math.PI * 2);
|
||
ctx.fillStyle = col;
|
||
ctx.fill();
|
||
});
|
||
|
||
/* x labels (symbols) */
|
||
ctx.font = '8px Manrope,sans-serif';
|
||
ctx.fillStyle = 'rgba(255,255,255,0.35)';
|
||
ctx.textAlign = 'center';
|
||
/* show at most 18 labels */
|
||
const step = Math.ceil(els.length / 18);
|
||
els.forEach((el, i) => {
|
||
if (i % step === 0) {
|
||
const x = pad.l + (i / Math.max(els.length - 1, 1)) * gW;
|
||
ctx.fillText(el.symbol, x, H - 4);
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────
|
||
LIFECYCLE
|
||
───────────────────────────────────────────────────── */
|
||
stop() {
|
||
cancelAnimationFrame(this._bohrRaf);
|
||
this._bohrRaf = null;
|
||
}
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
WAVE B — VISUAL MODES EXTENSION
|
||
Methods: _buildVisualModes, _drawHeatmap, _init3DTable,
|
||
_update3D, _morphToView, _drawTrendArrows
|
||
══════════════════════════════════════════════════════════════ */
|
||
|
||
/* ── Heatmap property config ─────────────────────────────────── */
|
||
const HEATMAP_PROPS = [
|
||
{ key: 'En', label: 'Электроотрицательность', unit: 'П.', get: el => el.En },
|
||
{ key: 'mass', label: 'Атомная масса', unit: 'а.е.м.', get: el => el.mass },
|
||
{ key: 'density', label: 'Плотность', unit: 'г/см³', get: el => el.density },
|
||
{ key: 'melt', label: 'T плавления', unit: 'K', get: el => el.melt },
|
||
{ key: 'boil', label: 'T кипения', unit: 'K', get: el => el.boil },
|
||
{ key: 'discovered', label: 'Год открытия', unit: 'г.', get: el => el.discovered },
|
||
{ key: 'radius.atomic', label: 'Атомный радиус', unit: 'пм', get: el => el.radius && el.radius.atomic ? el.radius.atomic : null },
|
||
{ key: 'ionization.e1', label: '1-я энергия ионизации', unit: 'эВ', get: el => el.ionization && el.ionization.e1 ? el.ionization.e1 : null },
|
||
{ key: 'abundance.crust', label: 'Распространённость в коре',unit: 'мг/кг',get: el => el.abundance && el.abundance.crust ? el.abundance.crust : null },
|
||
];
|
||
|
||
/* ── Trend arrows config ─────────────────────────────────────── */
|
||
const TREND_CONFIG = {
|
||
radius: {
|
||
label: 'Атомный радиус',
|
||
hArrow: { dir: 'left', text: 'Радиус растёт', color: '#06D6E0' },
|
||
vArrow: { dir: 'down', text: 'Радиус растёт', color: '#06D6E0' },
|
||
},
|
||
en: {
|
||
label: 'Электроотрицательность',
|
||
hArrow: { dir: 'right', text: 'ЭО растёт', color: '#FF6B6B' },
|
||
vArrow: { dir: 'up', text: 'ЭО растёт', color: '#FF6B6B' },
|
||
},
|
||
ie: {
|
||
label: 'Энергия ионизации',
|
||
hArrow: { dir: 'right', text: 'ИЕ растёт', color: '#FFD93D' },
|
||
vArrow: { dir: 'up', text: 'ИЕ растёт', color: '#FFD93D' },
|
||
},
|
||
metal: {
|
||
label: 'Металличность',
|
||
hArrow: { dir: 'left', text: 'Металличность растёт', color: '#4CAF50' },
|
||
vArrow: { dir: 'down', text: 'Металличность растёт', color: '#4CAF50' },
|
||
},
|
||
};
|
||
|
||
/* ── Secondary control bar ───────────────────────────────────── */
|
||
PeriodicTableSim.prototype._buildVisualModes = function() {
|
||
const self = this;
|
||
|
||
/* secondary bar container */
|
||
const bar = document.createElement('div');
|
||
bar.id = 'ptbl-vmodes-bar';
|
||
bar.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 12px;border-bottom:1px solid rgba(255,255,255,0.06);flex-wrap:wrap;flex-shrink:0;background:rgba(0,0,0,0.2);';
|
||
|
||
/* ─ 1. HEATMAP section ─ */
|
||
bar.innerHTML = `
|
||
<span style="font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">Тепловая карта:</span>
|
||
|
||
<button id="ptbl-heat-toggle" class="ptbl-vm-btn" title="Тепловая карта">
|
||
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px">
|
||
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
|
||
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
|
||
</svg>
|
||
Окрасить
|
||
</button>
|
||
|
||
<select id="ptbl-heat-prop" style="display:none;background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
|
||
${HEATMAP_PROPS.map(p => `<option value="${p.key}"${p.key==='En'?' selected':''}>${p.label}</option>`).join('')}
|
||
</select>
|
||
|
||
<button id="ptbl-heat-scale" class="ptbl-vm-btn" style="display:none" title="Линейная/Лог шкала">Lin</button>
|
||
|
||
<div id="ptbl-heat-legend" style="display:none;flex-shrink:0;"></div>
|
||
|
||
<span style="margin-left:4px;font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">Вид:</span>
|
||
|
||
<select id="ptbl-shape-sel" class="ptbl-vm-btn" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:3px 6px;font-size:.72rem;cursor:pointer;">
|
||
<option value="std">Стандартная</option>
|
||
<option value="long">Длинная (32 кол.)</option>
|
||
<option value="short">Краткая (8 гр.)</option>
|
||
</select>
|
||
|
||
<span style="margin-left:4px;font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">Тренды:</span>
|
||
|
||
<button id="ptbl-trend-toggle" class="ptbl-vm-btn" title="Показать тренды">
|
||
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width:13px;height:13px">
|
||
<line x1="5" y1="19" x2="19" y2="5"/><polyline points="9 5 19 5 19 15"/>
|
||
</svg>
|
||
Тренды
|
||
</button>
|
||
|
||
<select id="ptbl-trend-prop" style="display:none;background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
|
||
${Object.entries(TREND_CONFIG).map(([k,v]) => `<option value="${k}">${v.label}</option>`).join('')}
|
||
</select>
|
||
|
||
<span style="margin-left:4px;font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">3D:</span>
|
||
|
||
<button id="ptbl-3d-toggle" class="ptbl-vm-btn" title="3D-вид">
|
||
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px">
|
||
<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
|
||
</svg>
|
||
3D
|
||
</button>
|
||
|
||
<select id="ptbl-3d-mode" style="display:none;background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
|
||
<option value="bar">Bar</option>
|
||
<option value="wave">Wave</option>
|
||
<option value="stack">Stack</option>
|
||
</select>
|
||
`;
|
||
|
||
/* style vm buttons */
|
||
const vmStyle = 'padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,0.12);background:transparent;color:#aaa;font-size:.72rem;cursor:pointer;display:inline-flex;align-items:center;gap:4px;transition:all .15s;';
|
||
bar.querySelectorAll('.ptbl-vm-btn').forEach(b => { b.style.cssText = vmStyle; });
|
||
|
||
/* insert bar after toolbar (first child of wrap) */
|
||
const toolbar = this._wrap.querySelector('div');
|
||
this._wrap.insertBefore(bar, toolbar.nextSibling);
|
||
|
||
/* trend canvas overlay */
|
||
this._trendCanvas = document.createElement('canvas');
|
||
this._trendCanvas.id = 'ptbl-trend-canvas';
|
||
this._trendCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0;transition:opacity .3s;';
|
||
const tableGrid = this._tableEl;
|
||
const tableParent = tableGrid.parentElement;
|
||
tableParent.style.position = 'relative';
|
||
tableParent.appendChild(this._trendCanvas);
|
||
|
||
/* 3D canvas */
|
||
this._canvas3d = document.createElement('canvas');
|
||
this._canvas3d.id = 'ptbl-3d-canvas';
|
||
this._canvas3d.style.cssText = 'display:none;flex:1;min-height:0;background:#0D0D1A;';
|
||
/* insert into main area beside table */
|
||
const mainArea = this._tableEl.closest('div[style*="flex:1"]') || tableParent;
|
||
mainArea.parentElement.insertBefore(this._canvas3d, mainArea);
|
||
|
||
/* ── wire events ── */
|
||
|
||
/* heatmap toggle */
|
||
bar.querySelector('#ptbl-heat-toggle').addEventListener('click', () => {
|
||
self._heatActive = !self._heatActive;
|
||
bar.querySelector('#ptbl-heat-toggle').style.background = self._heatActive ? 'rgba(155,93,229,0.3)' : 'transparent';
|
||
bar.querySelector('#ptbl-heat-toggle').style.color = self._heatActive ? '#fff' : '#aaa';
|
||
bar.querySelector('#ptbl-heat-prop').style.display = self._heatActive ? '' : 'none';
|
||
bar.querySelector('#ptbl-heat-scale').style.display = self._heatActive ? '' : 'none';
|
||
bar.querySelector('#ptbl-heat-legend').style.display = self._heatActive ? 'flex' : 'none';
|
||
if (self._heatActive) {
|
||
self._drawHeatmap(true);
|
||
} else {
|
||
self._colorTable();
|
||
bar.querySelector('#ptbl-heat-legend').innerHTML = '';
|
||
}
|
||
});
|
||
|
||
bar.querySelector('#ptbl-heat-prop').addEventListener('change', e => {
|
||
self._heatProp = e.target.value;
|
||
if (self._heatActive) self._drawHeatmap(true);
|
||
});
|
||
|
||
bar.querySelector('#ptbl-heat-scale').addEventListener('click', () => {
|
||
self._heatLog = !self._heatLog;
|
||
bar.querySelector('#ptbl-heat-scale').textContent = self._heatLog ? 'Log' : 'Lin';
|
||
if (self._heatActive) self._drawHeatmap(true);
|
||
});
|
||
|
||
/* shape selector */
|
||
bar.querySelector('#ptbl-shape-sel').addEventListener('change', e => {
|
||
self._tableShape = e.target.value;
|
||
self._morphToView(e.target.value);
|
||
});
|
||
|
||
/* trend toggle */
|
||
bar.querySelector('#ptbl-trend-toggle').addEventListener('click', () => {
|
||
self._trendOn = !self._trendOn;
|
||
bar.querySelector('#ptbl-trend-toggle').style.background = self._trendOn ? 'rgba(155,93,229,0.3)' : 'transparent';
|
||
bar.querySelector('#ptbl-trend-toggle').style.color = self._trendOn ? '#fff' : '#aaa';
|
||
bar.querySelector('#ptbl-trend-prop').style.display = self._trendOn ? '' : 'none';
|
||
self._trendCanvas.style.opacity = self._trendOn ? '1' : '0';
|
||
if (self._trendOn) self._drawTrendArrows();
|
||
});
|
||
|
||
bar.querySelector('#ptbl-trend-prop').addEventListener('change', e => {
|
||
self._trendProp = e.target.value;
|
||
if (self._trendOn) self._drawTrendArrows();
|
||
});
|
||
|
||
/* 3D toggle */
|
||
bar.querySelector('#ptbl-3d-toggle').addEventListener('click', () => {
|
||
if (typeof THREE === 'undefined') {
|
||
bar.querySelector('#ptbl-3d-toggle').textContent = '3D (нет Three.js)';
|
||
return;
|
||
}
|
||
self._3dActive = !self._3dActive;
|
||
bar.querySelector('#ptbl-3d-toggle').style.background = self._3dActive ? 'rgba(155,93,229,0.3)' : 'transparent';
|
||
bar.querySelector('#ptbl-3d-toggle').style.color = self._3dActive ? '#fff' : '#aaa';
|
||
bar.querySelector('#ptbl-3d-mode').style.display = self._3dActive ? '' : 'none';
|
||
|
||
if (self._3dActive) {
|
||
mainArea.style.display = 'none';
|
||
self._canvas3d.style.display = 'block';
|
||
self._init3DTable();
|
||
} else {
|
||
mainArea.style.display = '';
|
||
self._canvas3d.style.display = 'none';
|
||
if (self._3dRaf) { cancelAnimationFrame(self._3dRaf); self._3dRaf = null; }
|
||
if (self._3dRenderer) { self._3dRenderer.dispose(); self._3dRenderer = null; }
|
||
}
|
||
});
|
||
|
||
bar.querySelector('#ptbl-3d-mode').addEventListener('change', e => {
|
||
self._3dMode = e.target.value;
|
||
if (self._3dActive) self._init3DTable();
|
||
});
|
||
|
||
this._vmBar = bar;
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
1. HEATMAP
|
||
══════════════════════════════════════════════════════════════ */
|
||
PeriodicTableSim.prototype._drawHeatmap = function(animate) {
|
||
const self = this;
|
||
const propDef = HEATMAP_PROPS.find(p => p.key === this._heatProp) || HEATMAP_PROPS[0];
|
||
|
||
/* gather values */
|
||
const vals = ELEMENTS.map(el => propDef.get(el));
|
||
const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v) && v > 0);
|
||
if (validVals.length === 0) return;
|
||
|
||
const rawMin = Math.min(...validVals);
|
||
const rawMax = Math.max(...validVals);
|
||
|
||
const norm = v => {
|
||
if (v === null || v === undefined || !isFinite(v)) return null;
|
||
if (this._heatLog) {
|
||
const logV = Math.log(Math.max(v, 1e-9));
|
||
const logMin = Math.log(Math.max(rawMin, 1e-9));
|
||
const logMax = Math.log(Math.max(rawMax, 1e-9));
|
||
return (logV - logMin) / (logMax - logMin || 1);
|
||
}
|
||
return (v - rawMin) / (rawMax - rawMin || 1);
|
||
};
|
||
|
||
const jet = t => {
|
||
/* jet colormap: blue→cyan→green→yellow→red */
|
||
const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 3)));
|
||
const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 2)));
|
||
const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 1)));
|
||
return `rgba(${(r*255)|0},${(g*255)|0},${(b*255)|0},0.85)`;
|
||
};
|
||
|
||
/* cancel previous tweens */
|
||
this._heatTweens.forEach(h => h && h.cancel && h.cancel());
|
||
this._heatTweens = [];
|
||
|
||
ELEMENTS.forEach((el, i) => {
|
||
const div = this._cellMap[el.Z];
|
||
if (!div) return;
|
||
const t = norm(propDef.get(el));
|
||
const targetBg = t !== null ? jet(t) : 'rgba(60,60,80,0.5)';
|
||
const targetBorder = t !== null ? jet(Math.min(1, t + 0.1)) : 'rgba(80,80,100,0.4)';
|
||
|
||
div.style.border = `1px solid ${targetBorder}`;
|
||
|
||
if (animate && window.LabFX && LabFX.motion) {
|
||
const tFrom = norm(propDef.get(el)) || 0;
|
||
const delay = i * 4; // stagger
|
||
setTimeout(() => {
|
||
if (!self._heatActive) return;
|
||
const handle = LabFX.motion.tween(0, 1, 400, 'easeInOutCubic', prog => {
|
||
const jt = (tFrom !== null ? tFrom * prog : 0);
|
||
div.style.background = t !== null ? jet(t * prog) : 'rgba(60,60,80,' + (0.5 * prog) + ')';
|
||
});
|
||
self._heatTweens.push(handle);
|
||
}, delay);
|
||
} else {
|
||
div.style.background = targetBg;
|
||
}
|
||
div.style.color = '#fff';
|
||
div.style.opacity = '1';
|
||
div.style.boxShadow = '';
|
||
});
|
||
|
||
/* build gradient legend */
|
||
this._buildHeatLegend(propDef, rawMin, rawMax, jet);
|
||
};
|
||
|
||
PeriodicTableSim.prototype._buildHeatLegend = function(propDef, minV, maxV, jet) {
|
||
const legendEl = this._vmBar.querySelector('#ptbl-heat-legend');
|
||
if (!legendEl) return;
|
||
|
||
/* gradient canvas */
|
||
const W = 120, H = 14;
|
||
legendEl.innerHTML = `
|
||
<span style="font-size:.65rem;color:rgba(255,255,255,0.45);white-space:nowrap">${propDef.unit ? minV.toFixed(1) + ' ' + propDef.unit : minV.toFixed(1)}</span>
|
||
<canvas id="ptbl-heat-grad" width="${W}" height="${H}" style="border-radius:3px;margin:0 4px;"></canvas>
|
||
<span style="font-size:.65rem;color:rgba(255,255,255,0.45);white-space:nowrap">${maxV.toFixed(1)}</span>
|
||
`;
|
||
legendEl.style.cssText = 'display:flex;align-items:center;gap:2px;flex-shrink:0;';
|
||
|
||
const canvas = legendEl.querySelector('#ptbl-heat-grad');
|
||
if (!canvas) return;
|
||
const ctx = canvas.getContext('2d');
|
||
for (let x = 0; x < W; x++) {
|
||
ctx.fillStyle = jet(x / (W - 1));
|
||
ctx.fillRect(x, 0, 1, H);
|
||
}
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
2. 3D TABLE (Three.js)
|
||
══════════════════════════════════════════════════════════════ */
|
||
PeriodicTableSim.prototype._init3DTable = function() {
|
||
const self = this;
|
||
if (typeof THREE === 'undefined') return;
|
||
|
||
/* cleanup previous */
|
||
if (this._3dRaf) { cancelAnimationFrame(this._3dRaf); this._3dRaf = null; }
|
||
if (this._3dRenderer) { this._3dRenderer.dispose(); this._3dRenderer = null; }
|
||
if (this._3dScene) { this._3dScene = null; }
|
||
|
||
const canvas = this._canvas3d;
|
||
const W = canvas.offsetWidth || 600;
|
||
const H = canvas.offsetHeight || 400;
|
||
|
||
const scene = new THREE.Scene();
|
||
scene.background = new THREE.Color(0x0d0d1a);
|
||
this._3dScene = scene;
|
||
|
||
const camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 2000);
|
||
camera.position.set(0, 60, 120);
|
||
camera.lookAt(0, 0, 0);
|
||
this._3dCamera = camera;
|
||
|
||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||
renderer.setPixelRatio(window.devicePixelRatio || 1);
|
||
renderer.setSize(W, H);
|
||
this._3dRenderer = renderer;
|
||
|
||
/* ambient + directional light */
|
||
scene.add(new THREE.AmbientLight(0xffffff, 0.45));
|
||
const dir = new THREE.DirectionalLight(0xffffff, 0.9);
|
||
dir.position.set(20, 40, 20);
|
||
scene.add(dir);
|
||
|
||
/* get height property */
|
||
const propDef = HEATMAP_PROPS.find(p => p.key === this._heatProp) || HEATMAP_PROPS[0];
|
||
const vals = ELEMENTS.map(el => propDef.get(el));
|
||
const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v) && v > 0);
|
||
const vMin = validVals.length ? Math.min(...validVals) : 0;
|
||
const vMax = validVals.length ? Math.max(...validVals) : 1;
|
||
|
||
const normV = v => (v !== null && v !== undefined && isFinite(v)) ? (v - vMin) / (vMax - vMin || 1) : 0;
|
||
|
||
const jet3 = t => {
|
||
const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 3)));
|
||
const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 2)));
|
||
const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 1)));
|
||
return new THREE.Color(r, g, b);
|
||
};
|
||
|
||
const CELL_W = 3.2, GAP = 0.4;
|
||
const STEP = CELL_W + GAP;
|
||
const mode = this._3dMode;
|
||
this._3dMeshes = [];
|
||
|
||
/* helper: text texture for cube face */
|
||
const makeTexture = (el, col) => {
|
||
const tc = document.createElement('canvas');
|
||
tc.width = 64; tc.height = 64;
|
||
const ctx2 = tc.getContext('2d');
|
||
ctx2.fillStyle = '#' + col.getHexString();
|
||
ctx2.fillRect(0, 0, 64, 64);
|
||
ctx2.fillStyle = '#fff';
|
||
ctx2.font = 'bold 22px sans-serif';
|
||
ctx2.textAlign = 'center';
|
||
ctx2.textBaseline = 'middle';
|
||
ctx2.fillText(el.symbol, 32, 32);
|
||
return new THREE.CanvasTexture(tc);
|
||
};
|
||
|
||
ELEMENTS.forEach(el => {
|
||
const pos = getCell(el);
|
||
if (!pos) return;
|
||
|
||
const nt = normV(propDef.get(el));
|
||
const height = Math.max(0.4, nt * 18);
|
||
const col = jet3(nt);
|
||
|
||
let gridRow = pos.row;
|
||
let gridCol = pos.col;
|
||
|
||
/* stack mode: f-block folded into main */
|
||
if (mode === 'stack' && pos.row > 7) {
|
||
if (pos.row === 9) { gridRow = 6; gridCol = pos.col; }
|
||
if (pos.row === 10) { gridRow = 7; gridCol = pos.col; }
|
||
}
|
||
|
||
const x = (gridCol - 9.5) * STEP;
|
||
const z = (gridRow - 4) * STEP;
|
||
|
||
let boxH = height;
|
||
if (mode === 'wave') {
|
||
/* smooth surface: average with neighbors */
|
||
const neighbors = ELEMENTS.filter(ne => {
|
||
const np = getCell(ne);
|
||
if (!np) return false;
|
||
return Math.abs(np.col - pos.col) <= 1 && Math.abs(np.row - pos.row) <= 1 && ne.Z !== el.Z;
|
||
});
|
||
const avgNt = neighbors.length
|
||
? neighbors.reduce((s, ne) => s + normV(propDef.get(ne)), 0) / neighbors.length
|
||
: nt;
|
||
boxH = Math.max(0.4, ((nt + avgNt) / 2) * 18);
|
||
}
|
||
|
||
const geom = new THREE.BoxGeometry(CELL_W, boxH, CELL_W);
|
||
const tex = makeTexture(el, col);
|
||
const mats = [
|
||
new THREE.MeshLambertMaterial({ color: col }), // right
|
||
new THREE.MeshLambertMaterial({ color: col }), // left
|
||
new THREE.MeshLambertMaterial({ map: tex }), // top (symbol)
|
||
new THREE.MeshLambertMaterial({ color: col }), // bottom
|
||
new THREE.MeshLambertMaterial({ color: col }), // front
|
||
new THREE.MeshLambertMaterial({ color: col }), // back
|
||
];
|
||
const mesh = new THREE.Mesh(geom, mats);
|
||
mesh.position.set(x, boxH / 2, z);
|
||
mesh.userData = { z: el.Z, origY: boxH / 2, nt };
|
||
scene.add(mesh);
|
||
this._3dMeshes.push(mesh);
|
||
});
|
||
|
||
/* ── orbit camera (simple drag) ── */
|
||
let isDragging = false, lastX = 0, lastY = 0;
|
||
let camTheta = 0.4, camPhi = Math.PI / 4, camRadius = 140;
|
||
|
||
const updateCam = () => {
|
||
camera.position.set(
|
||
camRadius * Math.sin(camPhi) * Math.sin(camTheta),
|
||
camRadius * Math.cos(camPhi),
|
||
camRadius * Math.sin(camPhi) * Math.cos(camTheta)
|
||
);
|
||
camera.lookAt(0, 0, 0);
|
||
};
|
||
updateCam();
|
||
|
||
canvas.addEventListener('mousedown', e => { isDragging = true; lastX = e.clientX; lastY = e.clientY; });
|
||
canvas.addEventListener('mousemove', e => {
|
||
if (!isDragging) return;
|
||
camTheta -= (e.clientX - lastX) * 0.008;
|
||
camPhi = Math.max(0.15, Math.min(Math.PI / 2, camPhi - (e.clientY - lastY) * 0.005));
|
||
lastX = e.clientX; lastY = e.clientY;
|
||
updateCam();
|
||
});
|
||
canvas.addEventListener('mouseup', () => { isDragging = false; });
|
||
canvas.addEventListener('wheel', e => {
|
||
camRadius = Math.max(30, Math.min(300, camRadius + e.deltaY * 0.3));
|
||
updateCam();
|
||
e.preventDefault();
|
||
}, { passive: false });
|
||
|
||
/* ── raycaster for hover/click ── */
|
||
const raycaster = new THREE.Raycaster();
|
||
const mouse = new THREE.Vector2();
|
||
let hoveredMesh = null;
|
||
|
||
/* 3D tooltip */
|
||
let tip3d = document.getElementById('ptbl-3d-tip');
|
||
if (!tip3d) {
|
||
tip3d = document.createElement('div');
|
||
tip3d.id = 'ptbl-3d-tip';
|
||
tip3d.style.cssText = 'position:fixed;display:none;background:rgba(10,10,30,0.92);border:1px solid rgba(155,93,229,0.5);color:#fff;font-size:.76rem;padding:5px 9px;border-radius:7px;pointer-events:none;z-index:999;';
|
||
document.body.appendChild(tip3d);
|
||
}
|
||
|
||
canvas.addEventListener('mousemove', e => {
|
||
const rect = canvas.getBoundingClientRect();
|
||
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||
raycaster.setFromCamera(mouse, camera);
|
||
const hits = raycaster.intersectObjects(self._3dMeshes);
|
||
if (hoveredMesh) { hoveredMesh.scale.set(1, 1, 1); hoveredMesh = null; }
|
||
if (hits.length) {
|
||
hoveredMesh = hits[0].object;
|
||
hoveredMesh.scale.set(1.08, 1.08, 1.08);
|
||
const elZ = hoveredMesh.userData.z;
|
||
const elObj = ELEMENTS.find(e2 => e2.Z === elZ);
|
||
if (elObj) {
|
||
const vl = propDef.get(elObj);
|
||
tip3d.innerHTML = `<b>${elObj.symbol}</b> — ${elObj.name}<br>${propDef.label}: ${vl !== null ? vl : '—'} ${propDef.unit}`;
|
||
tip3d.style.display = 'block';
|
||
tip3d.style.left = (e.clientX + 12) + 'px';
|
||
tip3d.style.top = (e.clientY - 10) + 'px';
|
||
}
|
||
} else {
|
||
tip3d.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('click', e => {
|
||
const rect = canvas.getBoundingClientRect();
|
||
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||
raycaster.setFromCamera(mouse, camera);
|
||
const hits = raycaster.intersectObjects(self._3dMeshes);
|
||
if (hits.length) {
|
||
const elZ = hits[0].object.userData.z;
|
||
if (elZ) self._selectElement(elZ);
|
||
}
|
||
});
|
||
|
||
/* ── render loop ── */
|
||
const animate3d = () => {
|
||
if (!self._3dActive) return;
|
||
self._3dRaf = requestAnimationFrame(animate3d);
|
||
renderer.render(scene, camera);
|
||
};
|
||
animate3d();
|
||
|
||
/* handle resize */
|
||
const ro3d = new ResizeObserver(() => {
|
||
if (!self._3dActive || !self._3dRenderer) return;
|
||
const nW = canvas.offsetWidth, nH = canvas.offsetHeight;
|
||
if (nW > 0 && nH > 0) {
|
||
renderer.setSize(nW, nH);
|
||
camera.aspect = nW / nH;
|
||
camera.updateProjectionMatrix();
|
||
}
|
||
});
|
||
ro3d.observe(canvas);
|
||
this._3dResizeObs = ro3d;
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
3. TABLE SHAPE MORPH (std / long / short)
|
||
══════════════════════════════════════════════════════════════ */
|
||
|
||
/* positions for LONG form (32-column, f-block inline) */
|
||
function _longFormPos(el) {
|
||
if (el.block === 'f') {
|
||
/* lanthanides: period 6, cols 4-17; actinides: period 7, cols 4-17 */
|
||
const fPos = { 57:4,58:5,59:6,60:7,61:8,62:9,63:10,64:11,65:12,66:13,67:14,68:15,69:16,70:17,71:18,
|
||
89:4,90:5,91:6,92:7,93:8,94:9,95:10,96:11,97:12,98:13,99:14,100:15,101:16,102:17,103:18 };
|
||
return { row: el.period, col: fPos[el.Z] || (el.Z - 54 + 4) };
|
||
}
|
||
/* s and d blocks shift right by 14 (f-block inserted) */
|
||
const stdPos = getCell(el);
|
||
if (!stdPos) return null;
|
||
if (el.block === 's') {
|
||
if (el.group === 1) return { row: stdPos.row, col: 1 };
|
||
if (el.group === 2) return { row: stdPos.row, col: 2 };
|
||
}
|
||
if (el.block === 'd' || (el.block === 'p')) {
|
||
return { row: stdPos.row, col: stdPos.col + 14 };
|
||
}
|
||
return { row: stdPos.row, col: stdPos.col + 14 };
|
||
}
|
||
|
||
/* positions for SHORT (8-group) form — classic Russian table */
|
||
function _shortFormPos(el) {
|
||
/* main group elements: groups 1-8 */
|
||
const SHORT_MAP = {
|
||
1:{1:1,3:1,11:1,19:1,37:1,55:1,87:1},
|
||
2:{4:2,12:2,20:2,38:2,56:2,88:2},
|
||
};
|
||
const stdPos = getCell(el);
|
||
if (!stdPos) return null;
|
||
if (el.block === 'd') {
|
||
/* transition metals: group 3-12 → columns 3-8 with subgroup */
|
||
const subCol = ((el.group - 3) % 8) + 3;
|
||
return { row: stdPos.row, col: subCol };
|
||
}
|
||
if (el.block === 'f') {
|
||
/* lanthanides row 9, actinides row 10 — same as standard */
|
||
return getCell(el);
|
||
}
|
||
/* p and s blocks */
|
||
if (el.group) {
|
||
const shortGroup = el.group <= 2 ? el.group : (el.group - 10 <= 0 ? el.group - 10 + 8 : el.group - 10);
|
||
return { row: stdPos.row, col: Math.max(1, Math.min(8, el.group <= 2 ? el.group : el.group - 10)) };
|
||
}
|
||
return stdPos;
|
||
}
|
||
|
||
PeriodicTableSim.prototype._morphToView = function(shape) {
|
||
const self = this;
|
||
const DURATION = 800;
|
||
|
||
/* snapshot current pixel positions of each cell */
|
||
const gridRect = this._tableEl.getBoundingClientRect();
|
||
|
||
/* we animate by moving each cell div to absolute position during morph
|
||
then snap to new grid layout */
|
||
|
||
/* for std, we just rebuild; for long/short we switch grid and re-place */
|
||
|
||
if (shape === 'std') {
|
||
/* restore normal 18-col grid */
|
||
this._tableEl.style.gridTemplateColumns = 'repeat(18,1fr)';
|
||
if (this._fblockEl) this._fblockEl.style.display = '';
|
||
this._colorTable();
|
||
return;
|
||
}
|
||
|
||
if (shape === 'long') {
|
||
/* switch to 32-column grid, hide f-block row */
|
||
if (this._fblockEl) this._fblockEl.style.display = 'none';
|
||
this._tableEl.style.gridTemplateColumns = 'repeat(32,1fr)';
|
||
|
||
/* re-map cells — rebuild grid items */
|
||
this._tableEl.innerHTML = '';
|
||
|
||
const cells = {};
|
||
for (let r = 1; r <= 7; r++) {
|
||
for (let c = 1; c <= 32; c++) {
|
||
const d = document.createElement('div');
|
||
d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
|
||
cells[`${r},${c}`] = d;
|
||
this._tableEl.appendChild(d);
|
||
}
|
||
}
|
||
|
||
for (const el of ELEMENTS) {
|
||
const pos = _longFormPos(el);
|
||
if (!pos || pos.row > 7) continue;
|
||
const div = cells[`${pos.row},${pos.col}`];
|
||
if (!div) continue;
|
||
this._cellMap[el.Z] = div;
|
||
div.dataset.z = el.Z;
|
||
div.title = `${el.name} (${el.symbol})`;
|
||
div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;opacity:0;';
|
||
div.innerHTML = `
|
||
<span style="font-size:.55em;opacity:.7;line-height:1">${el.Z}</span>
|
||
<span style="font-size:.85em;font-weight:800;line-height:1.1">${el.symbol}</span>
|
||
<span style="font-size:.42em;opacity:.65;line-height:1.2;text-align:center;max-width:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${el.name}</span>`;
|
||
div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
|
||
div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
|
||
div.addEventListener('click', () => self._selectElement(el.Z));
|
||
|
||
/* fade-in staggered */
|
||
const delay = (pos.col + pos.row * 2) * 8;
|
||
setTimeout(() => {
|
||
if (window.LabFX && LabFX.motion) {
|
||
LabFX.motion.tween(0, 1, DURATION, 'easeInOutCubic', v => { div.style.opacity = v; });
|
||
} else {
|
||
div.style.opacity = '1';
|
||
}
|
||
}, delay);
|
||
}
|
||
|
||
this._colorTable();
|
||
return;
|
||
}
|
||
|
||
if (shape === 'short') {
|
||
/* 8-column grid */
|
||
if (this._fblockEl) this._fblockEl.style.display = '';
|
||
this._tableEl.style.gridTemplateColumns = 'repeat(8,1fr)';
|
||
this._tableEl.innerHTML = '';
|
||
|
||
const cells = {};
|
||
for (let r = 1; r <= 7; r++) {
|
||
for (let c = 1; c <= 8; c++) {
|
||
const d = document.createElement('div');
|
||
d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
|
||
cells[`${r},${c}`] = d;
|
||
this._tableEl.appendChild(d);
|
||
}
|
||
}
|
||
|
||
for (const el of ELEMENTS) {
|
||
if (el.block === 'f') continue; /* f-block stays in fblockEl */
|
||
const stdPos = getCell(el);
|
||
if (!stdPos) continue;
|
||
let col;
|
||
if (el.group <= 2) {
|
||
col = el.group;
|
||
} else if (el.group >= 13) {
|
||
col = el.group - 10;
|
||
} else {
|
||
/* d-block: groups 3-12 → compress to cols 3-8 with row offset tricks */
|
||
col = Math.min(8, ((el.group - 3) % 8) + 3);
|
||
}
|
||
col = Math.max(1, Math.min(8, col));
|
||
const div = cells[`${stdPos.row},${col}`] || cells[`${stdPos.row},1`];
|
||
if (!div || div.dataset.z) continue;
|
||
this._cellMap[el.Z] = div;
|
||
div.dataset.z = el.Z;
|
||
div.title = `${el.name} (${el.symbol})`;
|
||
div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;opacity:0;';
|
||
div.innerHTML = `
|
||
<span style="font-size:.55em;opacity:.7;line-height:1">${el.Z}</span>
|
||
<span style="font-size:.85em;font-weight:800;line-height:1.1">${el.symbol}</span>
|
||
<span style="font-size:.42em;opacity:.65;line-height:1.2;text-align:center;max-width:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${el.name}</span>`;
|
||
div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
|
||
div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
|
||
div.addEventListener('click', () => self._selectElement(el.Z));
|
||
|
||
const delay = (col + stdPos.row * 2) * 10;
|
||
setTimeout(() => {
|
||
if (window.LabFX && LabFX.motion) {
|
||
LabFX.motion.tween(0, 1, DURATION, 'easeInOutCubic', v => { div.style.opacity = v; });
|
||
} else {
|
||
div.style.opacity = '1';
|
||
}
|
||
}, delay);
|
||
}
|
||
|
||
this._colorTable();
|
||
}
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
4. TREND ARROWS
|
||
══════════════════════════════════════════════════════════════ */
|
||
PeriodicTableSim.prototype._drawTrendArrows = function() {
|
||
const self = this;
|
||
const canvas = this._trendCanvas;
|
||
if (!canvas) return;
|
||
|
||
const parent = canvas.parentElement;
|
||
const W = parent.offsetWidth || 600;
|
||
const H = parent.offsetHeight || 300;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
canvas.style.width = W + 'px';
|
||
canvas.style.height = H + 'px';
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr);
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
const cfg = TREND_CONFIG[this._trendProp] || TREND_CONFIG.radius;
|
||
const ARROW_W = 12;
|
||
const PADDING = 4;
|
||
const PAD_BOTTOM = 14;
|
||
const PAD_LEFT = 14;
|
||
|
||
/* draw arrow helper: from (x1,y1) to (x2,y2) with gradient and label */
|
||
const drawArrow = (x1, y1, x2, y2, color, label, labelSide) => {
|
||
const dx = x2 - x1, dy = y2 - y1;
|
||
const len = Math.sqrt(dx * dx + dy * dy);
|
||
if (len < 10) return;
|
||
|
||
const grad = ctx.createLinearGradient(x1, y1, x2, y2);
|
||
grad.addColorStop(0, color + '22');
|
||
grad.addColorStop(0.5, color + 'bb');
|
||
grad.addColorStop(1, color + 'ff');
|
||
|
||
/* shaft */
|
||
ctx.beginPath();
|
||
ctx.moveTo(x1, y1);
|
||
ctx.lineTo(x2, y2);
|
||
ctx.strokeStyle = grad;
|
||
ctx.lineWidth = ARROW_W;
|
||
ctx.lineCap = 'round';
|
||
ctx.stroke();
|
||
|
||
/* arrowhead */
|
||
const angle = Math.atan2(dy, dx);
|
||
const AH = 16;
|
||
ctx.beginPath();
|
||
ctx.moveTo(x2, y2);
|
||
ctx.lineTo(x2 - AH * Math.cos(angle - 0.45), y2 - AH * Math.sin(angle - 0.45));
|
||
ctx.lineTo(x2 - AH * Math.cos(angle + 0.45), y2 - AH * Math.sin(angle + 0.45));
|
||
ctx.closePath();
|
||
ctx.fillStyle = color;
|
||
ctx.fill();
|
||
|
||
/* label */
|
||
ctx.save();
|
||
ctx.font = 'bold 11px Manrope,sans-serif';
|
||
ctx.fillStyle = color;
|
||
ctx.shadowColor = 'rgba(0,0,0,0.8)';
|
||
ctx.shadowBlur = 4;
|
||
const mx = (x1 + x2) / 2;
|
||
const my = (y1 + y2) / 2;
|
||
if (labelSide === 'below') {
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(label, mx, my + 20);
|
||
} else if (labelSide === 'left') {
|
||
ctx.textAlign = 'right';
|
||
ctx.fillText(label, mx - 14, my);
|
||
}
|
||
ctx.restore();
|
||
};
|
||
|
||
const hCfg = cfg.hArrow;
|
||
const vCfg = cfg.vArrow;
|
||
|
||
/* horizontal arrow: bottom of table */
|
||
const yBottom = H - PAD_BOTTOM;
|
||
if (hCfg.dir === 'left') {
|
||
drawArrow(W - PADDING, yBottom, PADDING, yBottom, hCfg.color, hCfg.text, 'below');
|
||
} else {
|
||
drawArrow(PADDING, yBottom, W - PADDING, yBottom, hCfg.color, hCfg.text, 'below');
|
||
}
|
||
|
||
/* vertical arrow: left side of table */
|
||
const xLeft = PAD_LEFT;
|
||
if (vCfg.dir === 'down') {
|
||
drawArrow(xLeft, PADDING, xLeft, H - PAD_BOTTOM - 20, vCfg.color, vCfg.text, 'left');
|
||
} else {
|
||
drawArrow(xLeft, H - PAD_BOTTOM - 20, xLeft, PADDING, vCfg.color, vCfg.text, 'left');
|
||
}
|
||
};
|
||
|
||
/* ── patch stop() to clean up 3D ──────────────────────────── */
|
||
const _origStop = PeriodicTableSim.prototype.stop;
|
||
PeriodicTableSim.prototype.stop = function() {
|
||
_origStop.call(this);
|
||
if (this._3dRaf) { cancelAnimationFrame(this._3dRaf); this._3dRaf = null; }
|
||
if (this._3dRenderer) { this._3dRenderer.dispose(); this._3dRenderer = null; }
|
||
if (this._3dResizeObs) { this._3dResizeObs.disconnect(); }
|
||
const tip3d = document.getElementById('ptbl-3d-tip');
|
||
if (tip3d) tip3d.style.display = 'none';
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
WAVE D — ELECTRON-CONFIG DEEP TOOLS
|
||
_periodG_*: orbital filling, aufbau diagram,
|
||
quantum-number hover, Bohr excitation overlay
|
||
══════════════════════════════════════════════════════════════ */
|
||
|
||
/* ── Arrow helper (module-level, used by orbital filling) ────── */
|
||
function _periodG_drawArrow(ctx, ax, ay1, ay2, col) {
|
||
ctx.beginPath(); ctx.moveTo(ax, ay1); ctx.lineTo(ax, ay2);
|
||
ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.stroke();
|
||
const dir = ay2 < ay1 ? -1 : 1;
|
||
ctx.beginPath();
|
||
ctx.moveTo(ax, ay2); ctx.lineTo(ax - 2.5, ay2 - dir * 4); ctx.lineTo(ax + 2.5, ay2 - dir * 4);
|
||
ctx.closePath(); ctx.fillStyle = col; ctx.fill();
|
||
}
|
||
|
||
/* ── Aufbau filling order ─────────────────────────────────────── */
|
||
PeriodicTableSim.prototype._periodG_aufbauOrder = function() {
|
||
return [
|
||
{n:1,l:0,label:'1s',cap:2}, {n:2,l:0,label:'2s',cap:2},
|
||
{n:2,l:1,label:'2p',cap:6}, {n:3,l:0,label:'3s',cap:2},
|
||
{n:3,l:1,label:'3p',cap:6}, {n:4,l:0,label:'4s',cap:2},
|
||
{n:3,l:2,label:'3d',cap:10}, {n:4,l:1,label:'4p',cap:6},
|
||
{n:5,l:0,label:'5s',cap:2}, {n:4,l:2,label:'4d',cap:10},
|
||
{n:5,l:1,label:'5p',cap:6}, {n:6,l:0,label:'6s',cap:2},
|
||
{n:4,l:3,label:'4f',cap:14}, {n:5,l:2,label:'5d',cap:10},
|
||
{n:6,l:1,label:'6p',cap:6}, {n:7,l:0,label:'7s',cap:2},
|
||
{n:5,l:3,label:'5f',cap:14}, {n:6,l:2,label:'6d',cap:10},
|
||
{n:7,l:1,label:'7p',cap:6},
|
||
];
|
||
};
|
||
|
||
PeriodicTableSim.prototype._periodG_mlLabels = function(l) {
|
||
if (l === 0) return [''];
|
||
if (l === 1) return ['px','py','pz'];
|
||
if (l === 2) return ['dxy','dxz','dyz','dx2y2','dz2'];
|
||
const out = [];
|
||
for (let m = -l; m <= l; m++) out.push('f' + (m >= 0 ? '+' : '') + m);
|
||
return out;
|
||
};
|
||
|
||
PeriodicTableSim.prototype._periodG_buildElectronList = function(Z) {
|
||
const order = this._periodG_aufbauOrder();
|
||
const electrons = [];
|
||
let rem = Z;
|
||
for (const sub of order) {
|
||
if (rem <= 0) break;
|
||
const mlList = this._periodG_mlLabels(sub.l);
|
||
const nOrb = mlList.length;
|
||
for (let pass = 0; pass < 2 && rem > 0; pass++) {
|
||
for (let oi = 0; oi < nOrb && rem > 0; oi++) {
|
||
electrons.push({
|
||
n: sub.n, l: sub.l, ml: oi - sub.l,
|
||
ms: pass === 0 ? 0.5 : -0.5,
|
||
subLabel: sub.label, orbIdx: oi, subRef: sub, mlLabel: mlList[oi],
|
||
});
|
||
rem--;
|
||
}
|
||
}
|
||
}
|
||
return electrons;
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
TOOL 1 — Orbital Filling Diagram
|
||
══════════════════════════════════════════════════════════════ */
|
||
PeriodicTableSim.prototype._periodG_drawOrbitalFilling = function(canvas, el) {
|
||
const electrons = this._periodG_buildElectronList(el.Z);
|
||
if (!electrons.length) return;
|
||
const subMap = new Map();
|
||
for (const e of electrons) {
|
||
if (!subMap.has(e.subLabel)) subMap.set(e.subLabel, []);
|
||
subMap.get(e.subLabel).push(e);
|
||
}
|
||
const order = this._periodG_aufbauOrder();
|
||
const subs = order.filter(s => subMap.has(s.label));
|
||
const BOX = 18, GAP = 3, LPAD = 28, VPAD = 5, ROW_H = BOX + VPAD * 2 + 8;
|
||
const W = canvas.offsetWidth || 240;
|
||
const H_needed = subs.length * ROW_H + 10;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
canvas.width = W * dpr; canvas.height = H_needed * dpr;
|
||
canvas.style.height = H_needed + 'px';
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H_needed);
|
||
const hitMap = [];
|
||
|
||
subs.forEach((sub, si) => {
|
||
const y0 = si * ROW_H + VPAD;
|
||
const electronArr = subMap.get(sub.label);
|
||
const nOrb = 2 * sub.l + 1;
|
||
const mlLabels = this._periodG_mlLabels(sub.l);
|
||
const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#aaa';
|
||
ctx.font = 'bold 10px Manrope,sans-serif'; ctx.fillStyle = bColor;
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||
ctx.fillText(sub.label, 2, y0 + BOX / 2);
|
||
const isLastSub = si === subs.length - 1;
|
||
for (let oi = 0; oi < nOrb; oi++) {
|
||
const bx = LPAD + oi * (BOX + GAP), by = y0;
|
||
const inOrb = electronArr.filter(e => e.orbIdx === oi);
|
||
const isLastBox = isLastSub && oi === nOrb - 1 && inOrb.length > 0;
|
||
ctx.fillStyle = 'rgba(255,255,255,0.06)';
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
|
||
ctx.beginPath();
|
||
if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
|
||
ctx.fill(); ctx.stroke();
|
||
if (nOrb > 1) {
|
||
ctx.font = '7px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.25)';
|
||
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||
ctx.fillText(mlLabels[oi], bx + BOX / 2, by + BOX + 1);
|
||
}
|
||
inOrb.forEach((e, ei) => {
|
||
const isUp = e.ms > 0;
|
||
const ax = bx + (ei === 0 ? BOX * 0.35 : BOX * 0.65);
|
||
const ay_t = by + 3, ay_b = by + BOX - 3;
|
||
const col = isLastBox ? '#FFD166' : '#06D6E0';
|
||
if (isLastBox && window.LabFX)
|
||
LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 8 });
|
||
else
|
||
_periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
|
||
hitMap.push({ x: ax - 6, y: by, w: 12, h: BOX, electron: e });
|
||
});
|
||
}
|
||
});
|
||
canvas._hitMap = hitMap; canvas._dpr = dpr;
|
||
};
|
||
|
||
/* ── Highlight variant (single electron lit, rest dimmed) ─────── */
|
||
PeriodicTableSim.prototype._periodG_drawOrbFillingHL = function(canvas, el, hvEl) {
|
||
const electrons = this._periodG_buildElectronList(el.Z);
|
||
const subMap = new Map();
|
||
for (const e of electrons) {
|
||
if (!subMap.has(e.subLabel)) subMap.set(e.subLabel, []);
|
||
subMap.get(e.subLabel).push(e);
|
||
}
|
||
const order = this._periodG_aufbauOrder();
|
||
const subs = order.filter(s => subMap.has(s.label));
|
||
const BOX = 18, GAP = 3, LPAD = 28, VPAD = 5, ROW_H = BOX + VPAD * 2 + 8;
|
||
const W = canvas.offsetWidth || 240;
|
||
const H_needed = subs.length * ROW_H + 10;
|
||
const dpr = canvas._dpr || window.devicePixelRatio || 1;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H_needed);
|
||
subs.forEach((sub, si) => {
|
||
const y0 = si * ROW_H + VPAD;
|
||
const electronArr = subMap.get(sub.label);
|
||
const nOrb = 2 * sub.l + 1;
|
||
const mlLabels = this._periodG_mlLabels(sub.l);
|
||
const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#aaa';
|
||
ctx.font = 'bold 10px Manrope,sans-serif'; ctx.fillStyle = bColor;
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(sub.label, 2, y0 + BOX / 2);
|
||
for (let oi = 0; oi < nOrb; oi++) {
|
||
const bx = LPAD + oi * (BOX + GAP), by = y0;
|
||
const inOrb = electronArr.filter(e => e.orbIdx === oi);
|
||
ctx.fillStyle = 'rgba(255,255,255,0.06)'; ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
|
||
ctx.beginPath();
|
||
if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
|
||
ctx.fill(); ctx.stroke();
|
||
if (nOrb > 1) {
|
||
ctx.font = '7px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.25)';
|
||
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||
ctx.fillText(mlLabels[oi], bx + BOX / 2, by + BOX + 1);
|
||
}
|
||
inOrb.forEach((e, ei) => {
|
||
const isHover = e.n === hvEl.n && e.l === hvEl.l && e.ml === hvEl.ml && e.ms === hvEl.ms;
|
||
const isUp = e.ms > 0;
|
||
const ax = bx + (ei === 0 ? BOX * 0.35 : BOX * 0.65);
|
||
const ay_t = by + 3, ay_b = by + BOX - 3;
|
||
const col = isHover ? '#FFD166' : 'rgba(6,214,224,0.3)';
|
||
if (isHover && window.LabFX)
|
||
LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 10 });
|
||
else
|
||
_periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
TOOL 3 — Quantum-number hover on orbital filling
|
||
══════════════════════════════════════════════════════════════ */
|
||
PeriodicTableSim.prototype._periodG_attachQNHover = function(canvas, el) {
|
||
if (canvas._periodG_mmH) {
|
||
canvas.removeEventListener('mousemove', canvas._periodG_mmH);
|
||
canvas.removeEventListener('mouseleave', canvas._periodG_mlH);
|
||
}
|
||
const tip = this._periodG_qTip;
|
||
canvas._periodG_mmH = (ev) => {
|
||
const rect = canvas.getBoundingClientRect();
|
||
const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
|
||
const hit = (canvas._hitMap || []).find(h => mx >= h.x && mx <= h.x + h.w && my >= h.y && my <= h.y + h.h);
|
||
if (hit) {
|
||
const e = hit.electron;
|
||
const lName = ['s','p','d','f'][e.l] || String(e.l);
|
||
if (tip) {
|
||
tip.innerHTML = `<b style="color:#9B5DE5">${e.subLabel} ${e.mlLabel}</b><br>n = ${e.n}<br>l = ${e.l} (${lName})<br>m<sub>l</sub> = ${e.ml}<br>m<sub>s</sub> = ${e.ms > 0 ? '+1/2' : '-1/2'}`;
|
||
tip.style.display = 'block';
|
||
tip.style.left = (ev.clientX + 12) + 'px';
|
||
tip.style.top = (ev.clientY - 10) + 'px';
|
||
}
|
||
this._periodG_drawOrbFillingHL(canvas, el, hit.electron);
|
||
} else {
|
||
if (tip) tip.style.display = 'none';
|
||
this._periodG_drawOrbitalFilling(canvas, el);
|
||
}
|
||
};
|
||
canvas._periodG_mlH = () => { if (tip) tip.style.display = 'none'; this._periodG_drawOrbitalFilling(canvas, el); };
|
||
canvas.addEventListener('mousemove', canvas._periodG_mmH);
|
||
canvas.addEventListener('mouseleave', canvas._periodG_mlH);
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
TOOL 2 — Aufbau Diagram with Z slider
|
||
══════════════════════════════════════════════════════════════ */
|
||
PeriodicTableSim.prototype._periodG_drawAufbau = function(canvas, Z) {
|
||
const order = this._periodG_aufbauOrder();
|
||
const W = canvas.offsetWidth || 240;
|
||
const BOX = 14, GAP = 2, LPAD = 26, VPAD = 4, ROW_H = BOX + VPAD * 2;
|
||
const H_needed = order.length * ROW_H + 14;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
canvas.width = W * dpr; canvas.height = H_needed * dpr;
|
||
canvas.style.height = H_needed + 'px';
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H_needed);
|
||
const electrons = this._periodG_buildElectronList(Z);
|
||
const subCount = new Map();
|
||
for (const e of electrons) subCount.set(e.subLabel, (subCount.get(e.subLabel) || 0) + 1);
|
||
let lastRow = -1;
|
||
order.forEach((sub, si) => { if ((subCount.get(sub.label) || 0) > 0) lastRow = si; });
|
||
order.forEach((sub, si) => {
|
||
const y0 = si * ROW_H + VPAD;
|
||
const nOrb = 2 * sub.l + 1;
|
||
const filled = subCount.get(sub.label) || 0;
|
||
const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#888';
|
||
ctx.font = 'bold 9px Manrope,sans-serif';
|
||
ctx.fillStyle = si <= lastRow ? bColor : 'rgba(255,255,255,0.2)';
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(sub.label, 2, y0 + BOX / 2);
|
||
for (let oi = 0; oi < nOrb; oi++) {
|
||
const bx = LPAD + oi * (BOX + GAP), by = y0;
|
||
ctx.fillStyle = 'rgba(255,255,255,0.04)';
|
||
ctx.strokeStyle = si <= lastRow ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.06)';
|
||
ctx.lineWidth = 1; ctx.beginPath();
|
||
if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
|
||
ctx.fill(); ctx.stroke();
|
||
}
|
||
let rem = filled;
|
||
const spins = Array(nOrb).fill(0);
|
||
for (let oi = 0; oi < nOrb && rem > 0; oi++) { spins[oi]++; rem--; }
|
||
for (let oi = 0; oi < nOrb && rem > 0; oi++) { spins[oi]++; rem--; }
|
||
for (let oi = 0; oi < nOrb; oi++) {
|
||
const bx = LPAD + oi * (BOX + GAP), by = y0;
|
||
const isLast = si === lastRow && oi === nOrb - 1 && spins[oi] > 0;
|
||
for (let sp = 0; sp < spins[oi]; sp++) {
|
||
const isUp = sp === 0;
|
||
const ax = bx + (sp === 0 ? BOX * 0.35 : BOX * 0.65);
|
||
const ay_t = by + 2, ay_b = by + BOX - 2;
|
||
const col = isLast ? '#FFD166' : bColor;
|
||
if (isLast && window.LabFX)
|
||
LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 6 });
|
||
else
|
||
_periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
|
||
}
|
||
}
|
||
if (si === lastRow && filled < sub.cap) {
|
||
const nx = LPAD + nOrb * (BOX + GAP) + 2, ny = y0 + BOX / 2;
|
||
ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.moveTo(nx, ny); ctx.lineTo(nx + 7, ny); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(nx+7,ny); ctx.lineTo(nx+4,ny-3); ctx.lineTo(nx+4,ny+3);
|
||
ctx.fillStyle = '#9B5DE5'; ctx.fill();
|
||
}
|
||
});
|
||
const now = performance.now();
|
||
if (this._periodG_lastAufbauZ !== Z && window.LabFX) {
|
||
if (!this._periodG_aufbauSoundTs || now - this._periodG_aufbauSoundTs > 80) {
|
||
LabFX.sound.play('tick', { pitch: 0.8 + Z * 0.01, volume: 0.08 });
|
||
this._periodG_aufbauSoundTs = now;
|
||
}
|
||
}
|
||
this._periodG_lastAufbauZ = Z;
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
Tab injection — patches _updateCard to add «Орбитали»/«Aufbau»
|
||
══════════════════════════════════════════════════════════════ */
|
||
(function() {
|
||
const _orig = PeriodicTableSim.prototype._updateCard;
|
||
PeriodicTableSim.prototype._updateCard = function(el) {
|
||
if (this._periodG_cleanupQTip) { this._periodG_cleanupQTip(); this._periodG_cleanupQTip = null; }
|
||
_orig.call(this, el);
|
||
if (!el) return;
|
||
this._periodG_addElecTab(el);
|
||
};
|
||
})();
|
||
|
||
PeriodicTableSim.prototype._periodG_addElecTab = function(el) {
|
||
const card = this._cardEl;
|
||
const tabBar = document.createElement('div');
|
||
tabBar.style.cssText = 'display:flex;gap:0;border-bottom:1px solid rgba(255,255,255,0.1);margin:8px -10px 0;';
|
||
const btns = ['Орбитали','Aufbau'].map((lbl) => {
|
||
const btn = document.createElement('button');
|
||
btn.textContent = lbl;
|
||
btn.style.cssText = 'flex:1;padding:5px 0;border:none;background:transparent;color:rgba(255,255,255,0.4);font-size:.7rem;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;';
|
||
tabBar.appendChild(btn); return btn;
|
||
});
|
||
card.appendChild(tabBar);
|
||
const wrap = document.createElement('div'); wrap.style.cssText = 'position:relative;';
|
||
card.appendChild(wrap);
|
||
|
||
// Panel 0: orbital filling canvas
|
||
const orbPan = document.createElement('div'); orbPan.style.cssText = 'display:none;padding:4px 0 0;';
|
||
const orbCv = document.createElement('canvas'); orbCv.style.cssText = 'width:100%;display:block;cursor:crosshair;';
|
||
orbPan.appendChild(orbCv); wrap.appendChild(orbPan);
|
||
|
||
// Panel 1: Aufbau canvas + Z slider
|
||
const aufPan = document.createElement('div'); aufPan.style.cssText = 'display:none;padding:4px 0 0;';
|
||
const aufCv = document.createElement('canvas'); aufCv.style.cssText = 'width:100%;display:block;';
|
||
aufPan.appendChild(aufCv);
|
||
const sw = document.createElement('div'); sw.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 0;';
|
||
const sld = document.createElement('input'); sld.type='range'; sld.min=1; sld.max=118; sld.value=el.Z;
|
||
sld.style.cssText = 'flex:1;accent-color:#9B5DE5;';
|
||
const slLbl = document.createElement('span'); slLbl.style.cssText = 'font-size:.68rem;color:#9B5DE5;min-width:32px;text-align:right;';
|
||
slLbl.textContent = 'Z=' + el.Z;
|
||
sw.appendChild(sld); sw.appendChild(slLbl); aufPan.appendChild(sw); wrap.appendChild(aufPan);
|
||
|
||
// quantum number tooltip (fixed positioned, cleaned up on new element)
|
||
const qTip = document.createElement('div');
|
||
qTip.style.cssText = 'position:fixed;display:none;padding:5px 9px;background:#1a1a2e;border:1px solid rgba(155,93,229,0.5);border-radius:6px;font-size:.68rem;color:#ccc;pointer-events:none;z-index:9999;line-height:1.65;';
|
||
document.body.appendChild(qTip);
|
||
this._periodG_qTip = qTip;
|
||
this._periodG_cleanupQTip = () => {
|
||
qTip.style.display = 'none';
|
||
if (qTip.parentNode) qTip.parentNode.removeChild(qTip);
|
||
this._periodG_qTip = null;
|
||
};
|
||
|
||
const panels = [orbPan, aufPan];
|
||
const showTab = (idx) => {
|
||
btns.forEach((b, i) => {
|
||
b.style.color = i === idx ? '#fff' : 'rgba(255,255,255,0.4)';
|
||
b.style.borderBottomColor = i === idx ? '#9B5DE5' : 'transparent';
|
||
});
|
||
panels.forEach((p, i) => p.style.display = i === idx ? 'block' : 'none');
|
||
if (idx === 0) setTimeout(() => { this._periodG_drawOrbitalFilling(orbCv, el); this._periodG_attachQNHover(orbCv, el); }, 0);
|
||
if (idx === 1) setTimeout(() => this._periodG_drawAufbau(aufCv, +sld.value), 0);
|
||
};
|
||
btns.forEach((b, i) => b.addEventListener('click', () => showTab(i)));
|
||
sld.addEventListener('input', () => { const z = +sld.value; slLbl.textContent = 'Z='+z; this._periodG_drawAufbau(aufCv, z); });
|
||
showTab(0);
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
TOOL 4 — Bohr excitation (patches _drawBohr)
|
||
══════════════════════════════════════════════════════════════ */
|
||
PeriodicTableSim.prototype._periodG_wavelengthToRGB = function(nm) {
|
||
let R=0,G=0,B=0,a=1;
|
||
if(nm>=380&&nm<440){R=-(nm-440)/(440-380);G=0;B=1;}
|
||
else if(nm<490){R=0;G=(nm-440)/(490-440);B=1;}
|
||
else if(nm<510){R=0;G=1;B=-(nm-510)/(510-490);}
|
||
else if(nm<580){R=(nm-510)/(580-510);G=1;B=0;}
|
||
else if(nm<645){R=1;G=-(nm-645)/(645-580);B=0;}
|
||
else if(nm<781){R=1;G=0;B=0;}
|
||
if(nm>=700)a=0.3+0.7*(780-nm)/(780-700);
|
||
else if(nm<420)a=0.3+0.7*(nm-380)/(420-380);
|
||
return `rgba(${Math.round(R*255*a)},${Math.round(G*255*a)},${Math.round(B*255*a)},1)`;
|
||
};
|
||
|
||
PeriodicTableSim.prototype._periodG_initBohrExcite = function() {
|
||
const canvas = this._bohrCanvas;
|
||
if (canvas._periodG_excite_bound) return;
|
||
canvas._periodG_excite_bound = true;
|
||
this._periodG_excState = null;
|
||
canvas.title = 'Клик на электрон — возбуждение';
|
||
canvas.addEventListener('click', (ev) => {
|
||
if (!this._bohrZ) return;
|
||
const el = ELEMENTS.find(e => e.Z === this._bohrZ); if (!el) return;
|
||
const shells = getShellFill(el.Z);
|
||
const rect = canvas.getBoundingClientRect();
|
||
const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
|
||
const W = canvas.offsetWidth || 240, H = canvas.offsetHeight || 150;
|
||
const cx = W/2, cy = H/2, maxR = Math.min(W,H)*0.44, nShells = shells.length;
|
||
let clickedShell = -1;
|
||
outer: for (let i = 0; i < nShells; i++) {
|
||
const r = maxR*(i+1)/nShells, speed = 1-i*0.12;
|
||
for (let e2 = 0; e2 < shells[i]; e2++) {
|
||
const a = this._bohrAngle*speed + (2*Math.PI*e2)/shells[i];
|
||
const dx = mx-(cx+r*Math.cos(a)), dy = my-(cy+r*Math.sin(a));
|
||
if (Math.sqrt(dx*dx+dy*dy) < 9) { clickedShell = i; break outer; }
|
||
}
|
||
}
|
||
if (clickedShell < 0) {
|
||
for (let i = 0; i < nShells; i++) {
|
||
const r = maxR*(i+1)/nShells;
|
||
const dist = Math.sqrt((mx-cx)**2+(my-cy)**2);
|
||
if (Math.abs(dist-r) < 8 && shells[i] > 0) { clickedShell = i; break; }
|
||
}
|
||
}
|
||
if (clickedShell < 0) return;
|
||
this._periodG_showExciteMenu(el, shells, clickedShell, ev.clientX, ev.clientY);
|
||
});
|
||
};
|
||
|
||
PeriodicTableSim.prototype._periodG_showExciteMenu = function(el, shells, n1idx, px, py) {
|
||
const old = document.getElementById('periodG-excite-menu'); if (old) old.remove();
|
||
const menu = document.createElement('div');
|
||
menu.id = 'periodG-excite-menu';
|
||
menu.style.cssText = `position:fixed;left:${px+6}px;top:${py}px;background:#12122a;border:1px solid rgba(155,93,229,0.55);border-radius:8px;padding:8px;z-index:9999;font-size:.71rem;color:#ccc;min-width:175px;box-shadow:0 4px 18px rgba(0,0,0,0.5);`;
|
||
menu.innerHTML = `<div style="color:#9B5DE5;font-weight:700;margin-bottom:5px;font-size:.7rem">Переход из n=${n1idx+1}:</div>`;
|
||
for (let i = 0; i < shells.length; i++) {
|
||
if (i === n1idx) continue;
|
||
const n = i+1, n1 = n1idx+1;
|
||
const dE = 13.6 * (1/(n1*n1) - 1/(n*n));
|
||
const dE_abs = Math.abs(dE);
|
||
const lam = dE_abs > 0.02 ? Math.round(1240/dE_abs) : 99999;
|
||
const region = lam < 380 ? 'УФ' : lam > 780 ? 'ИК' : 'видим.';
|
||
const abs = dE > 0;
|
||
const btn = document.createElement('button');
|
||
btn.style.cssText = 'display:block;width:100%;text-align:left;padding:3px 7px;border:none;background:transparent;color:#ccc;cursor:pointer;border-radius:4px;font-size:.7rem;';
|
||
btn.innerHTML = `n=${n} → ${lam < 99999 ? lam+'нм ('+region+')' : '—'} ${abs ? '[+ф]' : '[-ф]'}`;
|
||
btn.addEventListener('mouseenter', () => btn.style.background = 'rgba(155,93,229,0.2)');
|
||
btn.addEventListener('mouseleave', () => btn.style.background = 'transparent');
|
||
btn.addEventListener('click', () => { menu.remove(); this._periodG_startExcitation(el, n1idx, i, lam); });
|
||
menu.appendChild(btn);
|
||
}
|
||
const cb = document.createElement('button');
|
||
cb.style.cssText = 'display:block;width:100%;text-align:center;padding:2px 0;border:none;background:transparent;color:rgba(255,255,255,0.3);cursor:pointer;font-size:.68rem;margin-top:5px;';
|
||
cb.textContent = 'Отмена'; cb.addEventListener('click', () => menu.remove());
|
||
menu.appendChild(cb); document.body.appendChild(menu);
|
||
const outside = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', outside); } };
|
||
setTimeout(() => document.addEventListener('click', outside), 60);
|
||
};
|
||
|
||
PeriodicTableSim.prototype._periodG_startExcitation = function(el, n1idx, n2idx, lam) {
|
||
this._periodG_excState = { n1: n1idx, n2: n2idx, lam, phase: 'up', t: performance.now() };
|
||
if (window.LabFX) LabFX.sound.play('chime', { pitch: n2idx > n1idx ? 1.3 : 0.7, volume: 0.3 });
|
||
};
|
||
|
||
/* patch _drawBohr to incorporate excitation physics */
|
||
(function() {
|
||
const _orig = PeriodicTableSim.prototype._drawBohr;
|
||
PeriodicTableSim.prototype._drawBohr = function() {
|
||
if (!this._bohrCanvas._periodG_excite_bound) this._periodG_initBohrExcite();
|
||
const canvas = this._bohrCanvas;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const W = canvas.offsetWidth || 240, H = canvas.offsetHeight || 150;
|
||
canvas.width = W*dpr; canvas.height = H*dpr;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H);
|
||
if (!this._bohrZ) return;
|
||
const el = ELEMENTS.find(e => e.Z === this._bohrZ); if (!el) return;
|
||
const shells = getShellFill(el.Z);
|
||
const cx = W/2, cy = H/2, maxR = Math.min(W,H)*0.44, nShells = shells.length;
|
||
const col = TYPE_COLORS[el.type] || '#7B8EF7';
|
||
const exc = this._periodG_excState;
|
||
const now = performance.now();
|
||
if (exc) {
|
||
const el2 = now - exc.t;
|
||
if (exc.phase === 'up' && el2 > 600) { exc.phase = 'stay'; exc.t = now; }
|
||
else if (exc.phase === 'stay' && el2 > 800) { exc.phase = 'down'; exc.t = now; }
|
||
else if (exc.phase === 'down' && el2 > 600) { exc.phase = 'done'; }
|
||
}
|
||
ctx.beginPath(); ctx.arc(cx, cy, nShells > 0 ? 5+nShells*1.5 : 6, 0, Math.PI*2);
|
||
ctx.fillStyle = col; ctx.fill();
|
||
shells.forEach((count, i) => {
|
||
const r = maxR*(i+1)/nShells;
|
||
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2);
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 1; ctx.stroke();
|
||
const speed = 1-i*0.12;
|
||
for (let e2 = 0; e2 < count; e2++) {
|
||
const a = this._bohrAngle*speed + (2*Math.PI*e2)/count;
|
||
let drawR = r;
|
||
const isExc = exc && exc.phase !== 'done' && i === exc.n1 && e2 === 0;
|
||
if (isExc) {
|
||
const r2 = maxR*(exc.n2+1)/nShells;
|
||
const elapsed2 = now - exc.t;
|
||
let prog = 0;
|
||
if (exc.phase === 'up') prog = Math.min(elapsed2/600, 1);
|
||
else if (exc.phase === 'stay') prog = 1;
|
||
else if (exc.phase === 'down') prog = 1 - Math.min(elapsed2/600, 1);
|
||
drawR = r + (r2-r)*prog;
|
||
if (exc.phase === 'up') {
|
||
const pf = Math.min(elapsed2/600, 1);
|
||
const pm = r + (r2-r)*pf*0.5, pa = a + Math.PI/2;
|
||
const px2 = cx+pm*Math.cos(pa), py2 = cy+pm*Math.sin(pa);
|
||
const lamNm = exc.lam;
|
||
const pcol = (lamNm>=380&&lamNm<=780) ? this._periodG_wavelengthToRGB(lamNm) : (lamNm<380 ? '#cc88ff' : '#ffaaaa');
|
||
if (window.LabFX) {
|
||
LabFX.glow.drawGlow(ctx, () => { ctx.beginPath(); ctx.arc(px2,py2,4,0,Math.PI*2); ctx.fillStyle=pcol; ctx.fill(); }, { color: pcol, intensity: 12 });
|
||
} else { ctx.beginPath(); ctx.arc(px2,py2,4,0,Math.PI*2); ctx.fillStyle=pcol; ctx.fill(); }
|
||
ctx.font='8px Manrope,sans-serif'; ctx.fillStyle=pcol; ctx.textAlign='center';
|
||
ctx.fillText((exc.lam<99999?exc.lam+'нм':'?')+(exc.lam<380?' УФ':exc.lam>780?' ИК':''), cx, H-4);
|
||
}
|
||
}
|
||
const ex2 = cx+drawR*Math.cos(a), ey2 = cy+drawR*Math.sin(a);
|
||
if (isExc && window.LabFX) {
|
||
LabFX.glow.drawGlow(ctx, () => { ctx.beginPath(); ctx.arc(ex2,ey2,3.5,0,Math.PI*2); ctx.fillStyle='#FFD166'; ctx.fill(); }, { color:'#FFD166', intensity:10 });
|
||
} else { ctx.beginPath(); ctx.arc(ex2,ey2,2.5,0,Math.PI*2); ctx.fillStyle='#06D6E0'; ctx.fill(); }
|
||
}
|
||
});
|
||
if (!exc || exc.phase === 'done') {
|
||
if (exc && exc.phase === 'done') {
|
||
this._periodG_excState = null;
|
||
if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.9, volume: 0.18 });
|
||
}
|
||
ctx.font = '700 10px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
||
ctx.textAlign = 'center'; ctx.fillText(shells.join(','), cx, H-4);
|
||
}
|
||
ctx.font = '7.5px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.2)';
|
||
ctx.textAlign = 'center'; ctx.fillText('клик -> возбуждение', cx, 10);
|
||
};
|
||
})();
|
||
|
||
/* patch stop() to clean up quantum tooltip (Wave D) */
|
||
(function() {
|
||
const _origStopD = PeriodicTableSim.prototype.stop;
|
||
PeriodicTableSim.prototype.stop = function() {
|
||
_origStopD.call(this);
|
||
if (this._periodG_cleanupQTip) { this._periodG_cleanupQTip(); this._periodG_cleanupQTip = null; }
|
||
};
|
||
})();
|
||
|
||
/* ── global opener ─────────────────────────────────────────── */
|
||
|
||
/* ══════════════════════════════════════════════════════════════
|
||
WAVE C — INTERACTIVE LEARNING MODES
|
||
_buildInteractiveModeBar, _switchInteractiveMode,
|
||
_modeBinary, _binaryClick, _binaryCalc,
|
||
_modeCompare, _compareClick, _compareRefresh, _compareDraw,
|
||
_modeActivity,
|
||
_modeMendeleev1869, _m1869ShowPrediction,
|
||
_modeTimeline, _timelineUpdate
|
||
══════════════════════════════════════════════════════════════ */
|
||
|
||
/* patch _buildVisualModes to also init interactive bar */
|
||
(function() {
|
||
var _prevBVM = PeriodicTableSim.prototype._buildVisualModes;
|
||
PeriodicTableSim.prototype._buildVisualModes = function() {
|
||
if (_prevBVM) _prevBVM.call(this);
|
||
this._buildInteractiveModeBar();
|
||
};
|
||
})();
|
||
|
||
/* patch stop() to clean up timeline RAF */
|
||
(function() {
|
||
var _prevSC = PeriodicTableSim.prototype.stop;
|
||
PeriodicTableSim.prototype.stop = function() {
|
||
if (_prevSC) _prevSC.call(this);
|
||
if (this._iModeState && this._iModeState.raf) {
|
||
cancelAnimationFrame(this._iModeState.raf);
|
||
this._iModeState.raf = null;
|
||
}
|
||
};
|
||
})();
|
||
|
||
PeriodicTableSim.prototype._buildInteractiveModeBar = function() {
|
||
this._interactiveMode = null;
|
||
this._iModePanel = null;
|
||
this._iModeState = {};
|
||
|
||
var bar = document.createElement('div');
|
||
bar.className = 'ptbl-imode-bar';
|
||
bar.style.cssText = 'display:flex;align-items:center;gap:5px;padding:5px 12px;background:rgba(0,0,0,0.16);border-bottom:1px solid rgba(255,255,255,0.05);flex-wrap:wrap;flex-shrink:0;';
|
||
|
||
var lbl = document.createElement('span');
|
||
lbl.style.cssText = 'font-size:.67rem;font-weight:700;color:rgba(255,255,255,0.28);text-transform:uppercase;letter-spacing:.07em;';
|
||
lbl.textContent = 'Интерактив:';
|
||
bar.appendChild(lbl);
|
||
|
||
var MODES = [
|
||
{ id: null, text: 'Стандартный' },
|
||
{ id: 'binary', text: 'Бинарные соединения' },
|
||
{ id: 'compare', text: 'Сравнить' },
|
||
{ id: 'activity', text: 'Ряд активности' },
|
||
{ id: 'mendeleev', text: 'Таблица 1869' },
|
||
{ id: 'timeline', text: 'Таймлайн' },
|
||
];
|
||
var BASE_S = 'padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:#777;font-size:.69rem;cursor:pointer;transition:all .15s;';
|
||
var ACT_S = 'background:rgba(6,214,224,0.16);color:#06D6E0;border-color:rgba(6,214,224,0.32);';
|
||
|
||
var self = this;
|
||
MODES.forEach(function(m) {
|
||
var btn = document.createElement('button');
|
||
btn.textContent = m.text;
|
||
btn.style.cssText = BASE_S + (m.id === null ? ACT_S : '');
|
||
btn.addEventListener('click', function() {
|
||
bar.querySelectorAll('button').forEach(function(b) { b.style.cssText = BASE_S; });
|
||
btn.style.cssText = BASE_S + ACT_S;
|
||
self._switchInteractiveMode(m.id);
|
||
});
|
||
bar.appendChild(btn);
|
||
});
|
||
|
||
var vbar = this._wrap.querySelector('#ptbl-vmodes-bar');
|
||
var anchor = vbar ? vbar.nextSibling : (this._wrap.children[1] ? this._wrap.children[1].nextSibling : null);
|
||
this._wrap.insertBefore(bar, anchor);
|
||
this._iModeBar = bar;
|
||
};
|
||
|
||
PeriodicTableSim.prototype._switchInteractiveMode = function(modeId) {
|
||
if (this._iModePanel && this._iModePanel.parentNode) {
|
||
this._iModePanel.parentNode.removeChild(this._iModePanel);
|
||
this._iModePanel = null;
|
||
}
|
||
if (this._iModeState && this._iModeState.raf) {
|
||
cancelAnimationFrame(this._iModeState.raf);
|
||
}
|
||
this._interactiveMode = modeId;
|
||
this._iModeState = {};
|
||
|
||
var self = this;
|
||
for (var i = 0; i < ELEMENTS.length; i++) {
|
||
var el = ELEMENTS[i];
|
||
var div = this._cellMap[el.Z];
|
||
if (!div) continue;
|
||
var clone = div.cloneNode(true);
|
||
(function(c, z) {
|
||
c.addEventListener('mouseenter', function() { c.style.filter = 'brightness(1.4)'; c.style.transform = 'scale(1.12)'; c.style.zIndex = '10'; });
|
||
c.addEventListener('mouseleave', function() { c.style.filter = ''; c.style.transform = ''; c.style.zIndex = ''; });
|
||
c.addEventListener('click', function() { self._selectElement(z); });
|
||
})(clone, el.Z);
|
||
if (!div.parentNode) continue;
|
||
div.parentNode.replaceChild(clone, div);
|
||
this._cellMap[el.Z] = clone;
|
||
}
|
||
this._colorTable();
|
||
|
||
if (!modeId) return;
|
||
if (modeId === 'binary') this._modeBinary();
|
||
if (modeId === 'compare') this._modeCompare();
|
||
if (modeId === 'activity') this._modeActivity();
|
||
if (modeId === 'mendeleev') this._modeMendeleev1869();
|
||
if (modeId === 'timeline') this._modeTimeline();
|
||
};
|
||
|
||
/* ── MODE 1: BINARY COMPOUNDS ── */
|
||
PeriodicTableSim.prototype._modeBinary = function() {
|
||
this._iModeState = { first: null };
|
||
var panel = document.createElement('div');
|
||
panel.className = 'ptbl-imode-panel';
|
||
panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:9px 14px;flex-shrink:0;font-size:.75rem;color:#ccc;min-height:78px;';
|
||
panel.innerHTML = '<div class="ptbl-bin-hint" style="color:rgba(255,255,255,0.36);font-size:.7rem;">Кликните первый элемент (реагент A)</div>' +
|
||
'<div class="ptbl-bin-result" style="margin-top:6px;"></div>';
|
||
this._wrap.appendChild(panel);
|
||
this._iModePanel = panel;
|
||
var self = this;
|
||
for (var i = 0; i < ELEMENTS.length; i++) {
|
||
var div = this._cellMap[ELEMENTS[i].Z];
|
||
if (!div) continue;
|
||
(function(z) { div.addEventListener('click', function() { self._binaryClick(z); }); })(ELEMENTS[i].Z);
|
||
}
|
||
};
|
||
|
||
PeriodicTableSim.prototype._binaryClick = function(Z) {
|
||
var st = this._iModeState;
|
||
var panel = this._iModePanel;
|
||
if (!panel) return;
|
||
var hint = panel.querySelector('.ptbl-bin-hint');
|
||
var res = panel.querySelector('.ptbl-bin-result');
|
||
var self = this;
|
||
|
||
if (!st.first) {
|
||
st.first = Z;
|
||
if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.0, volume: 0.2 });
|
||
ELEMENTS.forEach(function(el) {
|
||
var d = self._cellMap[el.Z]; if (!d) return;
|
||
d.style.outline = el.Z === Z ? '2px solid #FFD166' : '';
|
||
d.style.outlineOffset = el.Z === Z ? '1px' : '';
|
||
});
|
||
var elA = ELEMENTS.find(function(e) { return e.Z === Z; });
|
||
hint.textContent = 'Выбран: ' + elA.symbol + ' (' + elA.name + '). Кликните второй элемент.';
|
||
res.innerHTML = '';
|
||
} else if (st.first === Z) {
|
||
st.first = null;
|
||
ELEMENTS.forEach(function(el) { var d = self._cellMap[el.Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; } });
|
||
hint.textContent = 'Кликните первый элемент (реагент A)';
|
||
res.innerHTML = '';
|
||
} else {
|
||
if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.3, volume: 0.3 });
|
||
var elA2 = ELEMENTS.find(function(e) { return e.Z === st.first; });
|
||
var elB = ELEMENTS.find(function(e) { return e.Z === Z; });
|
||
ELEMENTS.forEach(function(el) {
|
||
var d = self._cellMap[el.Z]; if (!d) return;
|
||
if (el.Z === st.first) { d.style.outline = '2px solid #FFD166'; d.style.outlineOffset = '1px'; }
|
||
else if (el.Z === Z) { d.style.outline = '2px solid #06D6E0'; d.style.outlineOffset = '1px'; }
|
||
else { d.style.outline = ''; d.style.outlineOffset = ''; }
|
||
});
|
||
hint.textContent = elA2.symbol + ' + ' + elB.symbol;
|
||
res.innerHTML = self._binaryCalc(elA2, elB);
|
||
st.first = null;
|
||
}
|
||
};
|
||
|
||
PeriodicTableSim.prototype._binaryCalc = function(elA, elB) {
|
||
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
|
||
var oxA = (elA.oxStates || []).filter(function(s) { return s !== null && s !== 0; });
|
||
var oxB = (elB.oxStates || []).filter(function(s) { return s !== null && s !== 0; });
|
||
var enA = elA.En || 0, enB = elB.En || 0;
|
||
var dEN = Math.abs(enA - enB);
|
||
var bondType, bondColor;
|
||
if (dEN > 1.7) { bondType = 'ионная'; bondColor = '#FF6B35'; }
|
||
else if (dEN >= 0.4) { bondType = 'ковалентная полярная'; bondColor = '#7B8EF7'; }
|
||
else { bondType = 'ковалентная неполярная'; bondColor = '#7BF5A4'; }
|
||
|
||
var seen = {};
|
||
var formulas = [];
|
||
var srcA = oxA.length ? oxA : [0];
|
||
var srcB = oxB.length ? oxB : [0];
|
||
for (var ai = 0; ai < srcA.length; ai++) {
|
||
for (var bi = 0; bi < srcB.length; bi++) {
|
||
var vA = srcA[ai], vB = srcB[bi];
|
||
if (vA === 0 || vB === 0) continue;
|
||
if ((vA > 0 && vB > 0) || (vA < 0 && vB < 0)) continue;
|
||
var posEl = vA > 0 ? elA : elB;
|
||
var negEl = vA > 0 ? elB : elA;
|
||
var posV = Math.abs(vA > 0 ? vA : vB);
|
||
var negV = Math.abs(vA > 0 ? vB : vA);
|
||
var g = gcd(posV, negV);
|
||
var nPos = negV / g, nNeg = posV / g;
|
||
var key = posEl.symbol + nPos + negEl.symbol + nNeg;
|
||
if (seen[key]) continue;
|
||
seen[key] = true;
|
||
var formula = posEl.symbol + (nPos > 1 ? '<sub>' + nPos + '</sub>' : '') +
|
||
negEl.symbol + (nNeg > 1 ? '<sub>' + nNeg + '</sub>' : '');
|
||
var struct = (nPos === 1 && nNeg === 1) ? posEl.symbol + '–' + negEl.symbol :
|
||
(nPos === 1 && nNeg === 2) ? negEl.symbol + '–' + posEl.symbol + '–' + negEl.symbol :
|
||
formula;
|
||
formulas.push({ formula: formula, struct: struct });
|
||
}
|
||
}
|
||
if (!formulas.length) {
|
||
return '<span style="color:rgba(255,255,255,0.28);font-style:italic;font-size:.7rem;">Соединение не образуется (одинаковые знаки ст. окисления)</span>';
|
||
}
|
||
var html = '<div style="margin-bottom:5px;">' +
|
||
'<span style="color:rgba(255,255,255,0.42);font-size:.69rem;">Тип связи: </span>' +
|
||
'<span style="color:' + bondColor + ';font-weight:700;font-size:.73rem;">' + bondType + '</span>';
|
||
if (elA.En && elB.En) html += '<span style="color:rgba(255,255,255,0.28);font-size:.66rem;margin-left:6px;">ΔЭО = ' + dEN.toFixed(2) + '</span>';
|
||
html += '</div><div style="display:flex;flex-wrap:wrap;gap:5px;">';
|
||
formulas.forEach(function(f) {
|
||
html += '<div style="background:rgba(155,93,229,0.13);border:1px solid rgba(155,93,229,0.36);border-radius:5px;padding:4px 8px;">' +
|
||
'<div style="font-weight:700;color:#fff;font-size:.8rem;">' + f.formula + '</div>' +
|
||
'<div style="font-size:.64rem;color:rgba(255,255,255,0.38);margin-top:1px;">' + f.struct + '</div></div>';
|
||
});
|
||
html += '</div>';
|
||
return html;
|
||
};
|
||
|
||
/* ── MODE 2: COMPARE ELEMENTS ── */
|
||
PeriodicTableSim.prototype._modeCompare = function() {
|
||
this._iModeState = { selected: [] };
|
||
var panel = document.createElement('div');
|
||
panel.className = 'ptbl-imode-panel';
|
||
panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;overflow-x:auto;';
|
||
panel.innerHTML = '<div style="display:flex;align-items:center;gap:7px;margin-bottom:6px;flex-wrap:wrap;">' +
|
||
'<span style="color:rgba(255,255,255,0.35);font-size:.7rem;">Выберите до 4 элементов</span>' +
|
||
'<button class="ptbl-cmp-clear" style="padding:2px 8px;border-radius:4px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#777;font-size:.67rem;cursor:pointer;">Очистить</button>' +
|
||
'<span style="font-size:.67rem;color:rgba(255,255,255,0.28);">График:</span>' +
|
||
'<select class="ptbl-cmp-prop" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.13);color:#ccc;border-radius:4px;padding:2px 4px;font-size:.67rem;">' +
|
||
'<option value="En">ЭО</option><option value="mass">Масса</option>' +
|
||
'<option value="density">Плотность</option><option value="melt">T пл.</option><option value="boil">T кип.</option>' +
|
||
'</select></div>' +
|
||
'<div class="ptbl-cmp-table"></div>' +
|
||
'<canvas class="ptbl-cmp-chart" style="width:100%;height:52px;display:block;margin-top:5px;"></canvas>';
|
||
this._wrap.appendChild(panel);
|
||
this._iModePanel = panel;
|
||
var self = this;
|
||
panel.querySelector('.ptbl-cmp-clear').addEventListener('click', function() {
|
||
self._iModeState.selected = [];
|
||
ELEMENTS.forEach(function(el) { var d = self._cellMap[el.Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; } });
|
||
self._compareRefresh();
|
||
});
|
||
panel.querySelector('.ptbl-cmp-prop').addEventListener('change', function() { self._compareRefresh(); });
|
||
ELEMENTS.forEach(function(el) {
|
||
var div = self._cellMap[el.Z]; if (!div) return;
|
||
(function(z) { div.addEventListener('click', function() { self._compareClick(z); }); })(el.Z);
|
||
});
|
||
this._compareRefresh();
|
||
};
|
||
|
||
PeriodicTableSim.prototype._compareClick = function(Z) {
|
||
var st = this._iModeState;
|
||
var idx = st.selected.indexOf(Z);
|
||
if (idx >= 0) {
|
||
st.selected.splice(idx, 1);
|
||
var d = this._cellMap[Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; }
|
||
} else {
|
||
if (st.selected.length >= 4) return;
|
||
st.selected.push(Z);
|
||
if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.85 + st.selected.length * 0.1, volume: 0.2 });
|
||
var d2 = this._cellMap[Z];
|
||
if (d2) { d2.style.outline = '2px solid #06D6E0'; d2.style.outlineOffset = '1px'; }
|
||
}
|
||
this._compareRefresh();
|
||
};
|
||
|
||
PeriodicTableSim.prototype._compareRefresh = function() {
|
||
var panel = this._iModePanel; if (!panel) return;
|
||
var st = this._iModeState;
|
||
var propKey = panel.querySelector('.ptbl-cmp-prop').value;
|
||
var els = st.selected.map(function(z) { return ELEMENTS.find(function(e) { return e.Z === z; }); }).filter(Boolean);
|
||
var tbl = panel.querySelector('.ptbl-cmp-table');
|
||
if (!els.length) {
|
||
tbl.innerHTML = '<div style="color:rgba(255,255,255,0.2);font-size:.68rem;">Кликайте элементы на таблице</div>';
|
||
this._compareDraw(panel.querySelector('.ptbl-cmp-chart'), els, propKey);
|
||
return;
|
||
}
|
||
var PROPS = [
|
||
{ key:'Z', label:'Z' }, { key:'mass', label:'Масса' },
|
||
{ key:'config', label:'Конфиг.' }, { key:'En', label:'ЭО (Полинг)' },
|
||
{ key:'density', label:'Плотн.' }, { key:'melt', label:'T пл. (K)' }, { key:'boil', label:'T кип. (K)' },
|
||
];
|
||
var fmt = function(v) { return (v !== null && v !== undefined) ? v : '—'; };
|
||
var html = '<table style="border-collapse:collapse;font-size:.69rem;"><tr><th style="padding:2px 5px;color:rgba(255,255,255,0.32);border-bottom:1px solid rgba(255,255,255,0.07);text-align:left;">Свойство</th>';
|
||
els.forEach(function(el) {
|
||
var col = TYPE_COLORS[el.type] || '#888';
|
||
html += '<th style="padding:2px 5px;color:' + col + ';border-bottom:1px solid rgba(255,255,255,0.07);text-align:center;">' + el.symbol + '<br><span style="font-size:.58rem;opacity:.5">' + el.name + '</span></th>';
|
||
});
|
||
html += '</tr>';
|
||
PROPS.forEach(function(p) {
|
||
var vals = els.map(function(el) { return el[p.key]; });
|
||
var numVals = vals.filter(function(v) { return typeof v === 'number' && v !== null; });
|
||
var maxV = numVals.length > 1 ? Math.max.apply(null, numVals) : null;
|
||
var minV = numVals.length > 1 ? Math.min.apply(null, numVals) : null;
|
||
html += '<tr><td style="padding:2px 5px;color:rgba(255,255,255,0.38);border-bottom:1px solid rgba(255,255,255,0.04);white-space:nowrap;">' + p.label + '</td>';
|
||
vals.forEach(function(v) {
|
||
var ex = '';
|
||
if (maxV !== null && typeof v === 'number') {
|
||
if (v === maxV) ex = 'background:rgba(123,245,164,0.1);color:#7BF5A4;';
|
||
else if (v === minV) ex = 'background:rgba(239,71,111,0.1);color:#EF476F;';
|
||
}
|
||
html += '<td style="padding:2px 5px;text-align:center;border-bottom:1px solid rgba(255,255,255,0.04);' + ex + '">' + fmt(v) + '</td>';
|
||
});
|
||
html += '</tr>';
|
||
});
|
||
html += '</table>';
|
||
tbl.innerHTML = html;
|
||
this._compareDraw(panel.querySelector('.ptbl-cmp-chart'), els, propKey);
|
||
};
|
||
|
||
PeriodicTableSim.prototype._compareDraw = function(canvas, els, propKey) {
|
||
if (!canvas) return;
|
||
var dpr = window.devicePixelRatio || 1;
|
||
var W = canvas.offsetWidth || 300, H = canvas.offsetHeight || 52;
|
||
canvas.width = W * dpr; canvas.height = H * dpr;
|
||
var ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H);
|
||
if (!els.length) return;
|
||
var vals = els.map(function(e) { return e[propKey]; }).filter(function(v) { return v !== null && v !== undefined && isFinite(v); });
|
||
if (!vals.length) return;
|
||
var minV = Math.min.apply(null, vals), maxV = Math.max.apply(null, vals);
|
||
var pad = { t:4, r:14, b:15, l:5 };
|
||
var gW = W - pad.l - pad.r, gH = H - pad.t - pad.b;
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.moveTo(pad.l, pad.t); ctx.lineTo(pad.l, pad.t+gH); ctx.lineTo(pad.l+gW, pad.t+gH); ctx.stroke();
|
||
var step = els.length > 1 ? gW / (els.length - 1) : gW * 0.5;
|
||
els.forEach(function(el, i) {
|
||
var v = el[propKey];
|
||
if (v === null || v === undefined || !isFinite(v)) return;
|
||
var x = pad.l + i * step;
|
||
var y = pad.t + gH - ((v - minV) / (maxV - minV || 1)) * gH;
|
||
var col = TYPE_COLORS[el.type] || '#7B8EF7';
|
||
ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI*2);
|
||
ctx.fillStyle = col; ctx.fill();
|
||
ctx.strokeStyle = 'rgba(0,0,0,0.4)'; ctx.lineWidth = 1; ctx.stroke();
|
||
ctx.font = '9px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
||
ctx.textAlign = 'center'; ctx.fillText(el.symbol, x, H - 2);
|
||
});
|
||
};
|
||
|
||
/* ── MODE 3: ACTIVITY SERIES ── */
|
||
PeriodicTableSim.prototype._modeActivity = function() {
|
||
var SERIES = [
|
||
{ s:'Li', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
|
||
{ s:'Cs', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
|
||
{ s:'Rb', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
|
||
{ s:'K', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
|
||
{ s:'Ba', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
|
||
{ s:'Sr', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
|
||
{ s:'Ca', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
|
||
{ s:'Na', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
|
||
{ s:'Mg', t:'Реагирует с горячей H₂O; с разб. HCl', g:'active' },
|
||
{ s:'Al', t:'Реагирует с разб. HCl; пассивируется конц. H₂SO₄', g:'active' },
|
||
{ s:'Mn', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
|
||
{ s:'Zn', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
|
||
{ s:'Cr', t:'Реагирует с разб. HCl; пассивируется конц. H₂SO₄', g:'medium' },
|
||
{ s:'Fe', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
|
||
{ s:'Cd', t:'Реагирует с разб. HCl', g:'medium' },
|
||
{ s:'Co', t:'Реагирует с разб. HCl медленно', g:'medium' },
|
||
{ s:'Ni', t:'Реагирует с разб. HCl медленно', g:'medium' },
|
||
{ s:'Sn', t:'Реагирует с разб. HCl медленно', g:'medium' },
|
||
{ s:'Pb', t:'Слабо реагирует с разб. HCl', g:'medium' },
|
||
{ s:'H', t:'Разделитель: металлы левее вытесняют H₂ из кислот', g:'sep' },
|
||
{ s:'Sb', t:'Реагирует только с конц. кислотами', g:'low' },
|
||
{ s:'Bi', t:'Реагирует только с конц. кислотами', g:'low' },
|
||
{ s:'Cu', t:'Не реаг. с HCl; реаг. с конц. H₂SO₄, HNO₃', g:'low' },
|
||
{ s:'Hg', t:'Реагирует с конц. HNO₃ и H₂SO₄', g:'low' },
|
||
{ s:'Ag', t:'Реагирует с HNO₃', g:'low' },
|
||
{ s:'Pd', t:'Реагирует с царской водкой', g:'low' },
|
||
{ s:'Pt', t:'Реагирует только с царской водкой', g:'low' },
|
||
{ s:'Au', t:'Реагирует только с царской водкой', g:'low' },
|
||
];
|
||
var GC = {
|
||
active:{ bg:'rgba(239,71,111,0.1)', bd:'rgba(239,71,111,0.36)', c:'#EF476F', lbl:'Активные (H₂O)' },
|
||
medium:{ bg:'rgba(255,209,102,0.1)', bd:'rgba(255,209,102,0.36)', c:'#FFD166', lbl:'Средние (разб. кислоты)' },
|
||
low: { bg:'rgba(123,142,247,0.1)', bd:'rgba(123,142,247,0.36)', c:'#7B8EF7', lbl:'Малоакт. (конц. / цар. водка)' },
|
||
};
|
||
var panel = document.createElement('div');
|
||
panel.className = 'ptbl-imode-panel';
|
||
panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;overflow-x:auto;';
|
||
var legHtml = '<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:6px;">';
|
||
['active','medium','low'].forEach(function(g) {
|
||
legHtml += '<div style="display:flex;align-items:center;gap:3px;font-size:.65rem;color:' + GC[g].c + ';">' +
|
||
'<span style="width:8px;height:8px;border-radius:2px;background:' + GC[g].bd + ';display:inline-block;"></span>' + GC[g].lbl + '</div>';
|
||
});
|
||
legHtml += '</div>';
|
||
var rowHtml = '<div style="display:flex;align-items:center;gap:2px;flex-wrap:wrap;">';
|
||
SERIES.forEach(function(item, i) {
|
||
if (item.g === 'sep') {
|
||
rowHtml += '<div style="display:flex;flex-direction:column;align-items:center;margin:0 3px;">' +
|
||
'<div style="width:1px;height:28px;background:rgba(255,255,255,0.18);"></div>' +
|
||
'<span style="font-size:.58rem;color:rgba(255,255,255,0.35);margin-top:1px;">H</span></div>';
|
||
return;
|
||
}
|
||
if (i > 0 && SERIES[i-1].g !== 'sep') rowHtml += '<div style="color:rgba(255,255,255,0.15);font-size:.72rem;align-self:center;">›</div>';
|
||
var gc = GC[item.g];
|
||
var el = ELEMENTS.find(function(e) { return e.symbol === item.s; });
|
||
var name = el ? el.name : item.s;
|
||
rowHtml += '<div class="ptbl-act-item" data-sym="' + item.s + '" title="' + item.t + '" ' +
|
||
'style="min-width:24px;padding:3px 4px;border-radius:4px;background:' + gc.bg + ';border:1px solid ' + gc.bd + ';text-align:center;cursor:pointer;transition:all .12s;">' +
|
||
'<div style="font-size:.74rem;font-weight:800;color:' + gc.c + ';line-height:1.1;">' + item.s + '</div>' +
|
||
'<div style="font-size:.5rem;color:rgba(255,255,255,0.32);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:34px;">' + name + '</div></div>';
|
||
});
|
||
rowHtml += '</div>';
|
||
panel.innerHTML = legHtml + rowHtml + '<div class="ptbl-act-tip" style="margin-top:5px;font-size:.68rem;color:rgba(255,255,255,0.42);min-height:14px;"></div>';
|
||
this._wrap.appendChild(panel);
|
||
this._iModePanel = panel;
|
||
var self = this;
|
||
panel.querySelectorAll('.ptbl-act-item').forEach(function(item) {
|
||
item.addEventListener('mouseenter', function() {
|
||
panel.querySelector('.ptbl-act-tip').textContent = item.title;
|
||
item.style.filter = 'brightness(1.5)'; item.style.transform = 'scale(1.08)';
|
||
var el = ELEMENTS.find(function(e) { return e.symbol === item.dataset.sym; });
|
||
if (el && self._cellMap[el.Z]) { self._cellMap[el.Z].style.outline = '2px solid #FFD166'; self._cellMap[el.Z].style.outlineOffset = '1px'; }
|
||
});
|
||
item.addEventListener('mouseleave', function() {
|
||
panel.querySelector('.ptbl-act-tip').textContent = '';
|
||
item.style.filter = ''; item.style.transform = '';
|
||
var el = ELEMENTS.find(function(e) { return e.symbol === item.dataset.sym; });
|
||
if (el && self._cellMap[el.Z]) { self._cellMap[el.Z].style.outline = ''; self._cellMap[el.Z].style.outlineOffset = ''; }
|
||
});
|
||
});
|
||
};
|
||
|
||
/* ── MODE 4: MENDELEEV 1869 ── */
|
||
PeriodicTableSim.prototype._modeMendeleev1869 = function() {
|
||
var KNOWN_1869 = new Set([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,
|
||
22,23,24,25,26,27,28,29,30,33,34,35,36,37,38,40,41,42,
|
||
44,45,47,48,50,51,52,53,55,56,57,60,62,64,65,66,68,70,72,74,76,78,79,80,82,83]);
|
||
var PREDS = {
|
||
31: { name:'Экаалюминий (Ga)', pred:{ mass:68, density:5.9, melt:302 }, act:{ mass:69.72, density:5.91, melt:303 }, year:1875, who:'Лекок де Буабодран (Франция)' },
|
||
21: { name:'Экабор (Sc)', pred:{ mass:44, density:3.5, melt:null }, act:{ mass:44.96, density:2.99, melt:1814 }, year:1879, who:'Ларс Нильсон (Швеция)' },
|
||
32: { name:'Экасилиций (Ge)', pred:{ mass:72, density:5.5, melt:null }, act:{ mass:72.63, density:5.32, melt:1211 }, year:1886, who:'Клеменс Винклер (Германия)' },
|
||
43: { name:'Экамарганец (Tc)', pred:{ mass:100, density:null, melt:null }, act:{ mass:98, density:11.5, melt:2430 }, year:1937, who:'Перье и Сегре (Италия)' },
|
||
};
|
||
var panel = document.createElement('div');
|
||
panel.className = 'ptbl-imode-panel';
|
||
panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;position:relative;min-height:38px;';
|
||
panel.innerHTML = '<div style="color:rgba(255,255,255,0.32);font-size:.68rem;margin-bottom:5px;">' +
|
||
'Таблица Менделеева 1869: 63 известных элемента. Фиол. «?» — предсказания Менделеева. Кликните «?».' +
|
||
'</div>' +
|
||
'<div class="ptbl-m1869-popup" style="display:none;position:absolute;top:8px;right:8px;width:230px;background:#1a1a2e;border:1px solid rgba(155,93,229,0.48);border-radius:8px;padding:10px;font-size:.7rem;z-index:20;box-shadow:0 4px 16px rgba(0,0,0,0.6);"></div>';
|
||
this._wrap.appendChild(panel);
|
||
this._iModePanel = panel;
|
||
var self = this;
|
||
ELEMENTS.forEach(function(el) {
|
||
var div = self._cellMap[el.Z]; if (!div) return;
|
||
if (KNOWN_1869.has(el.Z)) {
|
||
div.style.opacity = '1';
|
||
} else if (PREDS[el.Z]) {
|
||
div.style.background = 'rgba(155,93,229,0.09)';
|
||
div.style.border = '1px dashed rgba(155,93,229,0.48)';
|
||
div.style.opacity = '1';
|
||
div.innerHTML = '<span style="font-size:.76em;font-weight:900;color:rgba(155,93,229,0.88);">?</span>' +
|
||
'<span style="font-size:.5em;color:rgba(155,93,229,0.5);margin-top:1px;">' + el.symbol + '</span>';
|
||
div.title = el.name + ' — предсказан Менделеевым (кликните)';
|
||
(function(z) { div.addEventListener('click', function() { self._m1869ShowPrediction(z, PREDS[z], panel); }); })(el.Z);
|
||
} else {
|
||
div.style.opacity = '0.13';
|
||
div.style.background = 'rgba(255,255,255,0.02)';
|
||
div.style.border = '1px solid rgba(255,255,255,0.05)';
|
||
}
|
||
});
|
||
};
|
||
|
||
PeriodicTableSim.prototype._m1869ShowPrediction = function(Z, pred, panel) {
|
||
if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.15, volume: 0.3 });
|
||
var popup = panel.querySelector('.ptbl-m1869-popup');
|
||
var fmt = function(v) { return (v !== null && v !== undefined) ? v : '—'; };
|
||
var diff = function(p, a) {
|
||
if (p == null || a == null || !isFinite(+p) || !isFinite(+a) || +a === 0) return '';
|
||
var pct = (Math.abs(+p - +a) / +a * 100).toFixed(1);
|
||
var good = Math.abs(+p - +a) / +a < 0.05;
|
||
return '<span style="color:' + (good ? '#7BF5A4' : '#FFD166') + ';font-size:.62rem;margin-left:3px;">(' + (good ? 'точно' : pct + '% откл.') + ')</span>';
|
||
};
|
||
popup.innerHTML = '<div style="font-weight:700;color:#9B5DE5;margin-bottom:5px;">' + pred.name + '</div>' +
|
||
'<table style="width:100%;border-collapse:collapse;font-size:.68rem;">' +
|
||
'<tr><th style="text-align:left;color:rgba(255,255,255,0.32);padding:2px 3px;border-bottom:1px solid rgba(255,255,255,0.07);">Свойство</th>' +
|
||
'<th style="text-align:center;color:#FFD166;padding:2px 3px;border-bottom:1px solid rgba(255,255,255,0.07);">Предсказано</th>' +
|
||
'<th style="text-align:center;color:#7BF5A4;padding:2px 3px;border-bottom:1px solid rgba(255,255,255,0.07);">Реально</th></tr>' +
|
||
'<tr><td style="padding:2px 3px;color:rgba(255,255,255,0.42);">Масса</td>' +
|
||
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.pred.mass) + '</td>' +
|
||
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.act.mass) + diff(pred.pred.mass, pred.act.mass) + '</td></tr>' +
|
||
'<tr><td style="padding:2px 3px;color:rgba(255,255,255,0.42);">Плотность</td>' +
|
||
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.pred.density) + '</td>' +
|
||
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.act.density) + diff(pred.pred.density, pred.act.density) + '</td></tr>' +
|
||
'<tr><td style="padding:2px 3px;color:rgba(255,255,255,0.42);">T пл. (K)</td>' +
|
||
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.pred.melt) + '</td>' +
|
||
'<td style="text-align:center;padding:2px 3px;">' + fmt(pred.act.melt) + '</td></tr>' +
|
||
'</table>' +
|
||
'<div style="margin-top:5px;font-size:.64rem;color:rgba(255,255,255,0.32);">Открыт: ' + pred.year + ' г., ' + pred.who + '</div>' +
|
||
'<button onclick="this.closest(".ptbl-m1869-popup").style.display="none"" ' +
|
||
'style="margin-top:5px;padding:2px 7px;border-radius:4px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#777;font-size:.65rem;cursor:pointer;">Закрыть</button>';
|
||
popup.style.display = 'block';
|
||
};
|
||
|
||
/* ── MODE 5: TIMELINE ── */
|
||
PeriodicTableSim.prototype._modeTimeline = function() {
|
||
var MIN_Y = 1660, MAX_Y = 2024;
|
||
this._iModeState = { year: MAX_Y, playing: false, raf: null };
|
||
var panel = document.createElement('div');
|
||
panel.className = 'ptbl-imode-panel';
|
||
panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;';
|
||
panel.innerHTML = '<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;">' +
|
||
'<span style="font-size:.68rem;color:rgba(255,255,255,0.32);">Год:</span>' +
|
||
'<input type="range" class="ptbl-tl-slider" min="' + MIN_Y + '" max="' + MAX_Y + '" value="' + MAX_Y + '" style="flex:1;min-width:90px;accent-color:#9B5DE5;">' +
|
||
'<span class="ptbl-tl-year" style="font-size:.8rem;font-weight:700;color:#9B5DE5;min-width:34px;">' + MAX_Y + '</span>' +
|
||
'<button class="ptbl-tl-play" style="padding:3px 8px;border-radius:5px;border:1px solid rgba(155,93,229,0.36);background:rgba(155,93,229,0.12);color:#9B5DE5;font-size:.68rem;cursor:pointer;display:inline-flex;align-items:center;gap:3px;">' +
|
||
'<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><polygon points="5,3 19,12 5,21"/></svg>Авто' +
|
||
'</button>' +
|
||
'<span class="ptbl-tl-count" style="font-size:.68rem;color:rgba(255,255,255,0.32);">Открыто 0 / 118</span>' +
|
||
'</div>' +
|
||
'<div class="ptbl-tl-info" style="font-size:.68rem;color:#FFD166;margin-top:3px;min-height:13px;"></div>';
|
||
this._wrap.appendChild(panel);
|
||
this._iModePanel = panel;
|
||
var self = this;
|
||
var slider = panel.querySelector('.ptbl-tl-slider');
|
||
var yearLbl = panel.querySelector('.ptbl-tl-year');
|
||
var playBtn = panel.querySelector('.ptbl-tl-play');
|
||
var info = panel.querySelector('.ptbl-tl-info');
|
||
var countLbl = panel.querySelector('.ptbl-tl-count');
|
||
var update = function() {
|
||
var y = +slider.value;
|
||
self._iModeState.year = y;
|
||
yearLbl.textContent = y;
|
||
self._timelineUpdate(y, info, countLbl);
|
||
};
|
||
slider.addEventListener('input', update);
|
||
update();
|
||
playBtn.addEventListener('click', function() {
|
||
var st = self._iModeState;
|
||
if (st.playing) {
|
||
st.playing = false;
|
||
cancelAnimationFrame(st.raf);
|
||
playBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><polygon points="5,3 19,12 5,21"/></svg>Авто';
|
||
} else {
|
||
if (+slider.value >= MAX_Y) slider.value = MIN_Y;
|
||
st.playing = true;
|
||
playBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>Стоп';
|
||
var last = null;
|
||
var tick = function(ts) {
|
||
if (!st.playing) return;
|
||
if (!last) last = ts;
|
||
if (ts - last > 38) {
|
||
var cur = +slider.value;
|
||
if (cur >= MAX_Y) {
|
||
st.playing = false;
|
||
playBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><polygon points="5,3 19,12 5,21"/></svg>Авто';
|
||
return;
|
||
}
|
||
slider.value = cur + 2;
|
||
update();
|
||
last = ts;
|
||
}
|
||
st.raf = requestAnimationFrame(tick);
|
||
};
|
||
st.raf = requestAnimationFrame(tick);
|
||
}
|
||
});
|
||
};
|
||
|
||
PeriodicTableSim.prototype._timelineUpdate = function(year, info, countLbl) {
|
||
var count = 0, lastEl = null, lastYear = -Infinity;
|
||
ELEMENTS.forEach(function(el) {
|
||
var div = this._cellMap[el.Z]; if (!div) return;
|
||
var known = (el.discovered === null) || (el.discovered <= year);
|
||
if (known) {
|
||
count++;
|
||
div.style.opacity = '1';
|
||
var col = TYPE_COLORS[el.type] || '#555';
|
||
div.style.background = col + '44';
|
||
div.style.border = '1px solid ' + col + '88';
|
||
div.style.outline = ''; div.style.outlineOffset = '';
|
||
if (el.discovered !== null && el.discovered <= year && el.discovered > lastYear) {
|
||
lastYear = el.discovered; lastEl = el;
|
||
}
|
||
} else {
|
||
div.style.opacity = '0.09';
|
||
div.style.background = 'rgba(255,255,255,0.015)';
|
||
div.style.border = '1px solid rgba(255,255,255,0.04)';
|
||
div.style.outline = ''; div.style.outlineOffset = '';
|
||
}
|
||
}, this);
|
||
if (countLbl) countLbl.textContent = 'Открыто ' + count + ' / 118';
|
||
if (lastEl && lastYear > -Infinity) {
|
||
var d = this._cellMap[lastEl.Z];
|
||
if (d) { d.style.outline = '2px solid #FFD166'; d.style.outlineOffset = '1px'; }
|
||
if (info) info.textContent = lastYear + ' г.: ' + lastEl.name + ' (' + lastEl.symbol + ') — ' + (lastEl.by || '?');
|
||
} else if (info) {
|
||
info.textContent = '';
|
||
}
|
||
};
|
||
|
||
var periodicSim = null;
|
||
|
||
function _openPeriodic() {
|
||
document.getElementById('sim-periodic').style.display = 'flex';
|
||
if (!periodicSim) {
|
||
periodicSim = new PeriodicTableSim(document.getElementById('periodic-wrap'));
|
||
}
|
||
}
|