edb4c211a0
- Add js/sidebar.js: generates full sidebar HTML into #app-sidebar, handles role-based visibility, active link (with prefix matching), toggle wiring, collapsed state, board/features/notif init - Replace <aside class="sidebar">...</aside> with <aside id="app-sidebar"> across all 35 standard-layout pages via scripts/apply-sidebar.js - Add notifications.js to 5 pages that were missing it - Fix api.js initPage(): skip toggle re-wiring if data-sb-wired set, fix active link selector .sb-item → .sb-link - Remove stale sbl-*/nav-admin/btn-upload-nav getElementById calls that crashed after sidebar replacement (lab, classes, collection, crossword, hangman, knowledge-map, library, pet, profile) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
558 lines
27 KiB
HTML
558 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Свойства молекул — LearnSpace</title>
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||
<link rel="stylesheet" href="/css/ls.css" />
|
||
<style>
|
||
html, body { height: 100%; overflow: hidden; }
|
||
.app-layout { height: 100vh; overflow: hidden; }
|
||
.sb-content { background: #0d0d1a; overflow: hidden; display: flex; flex-direction: column; }
|
||
.sb-sub-link { padding-left: 28px !important; font-size: 0.76rem !important; opacity: .75; }
|
||
.sb-sub-link:hover { opacity: 1; }
|
||
|
||
/* ── Layout ── */
|
||
.props-layout {
|
||
display: flex; flex: 1; min-height: 0; overflow: hidden; background: #0d0d1a;
|
||
}
|
||
|
||
/* ── Left: molecule search list ── */
|
||
.props-sidebar {
|
||
width: 260px; flex-shrink: 0;
|
||
background: rgba(8,8,20,.95);
|
||
border-right: 1px solid rgba(155,93,229,.12);
|
||
display: flex; flex-direction: column; overflow: hidden;
|
||
}
|
||
.ps-head {
|
||
padding: 14px 14px 10px;
|
||
border-bottom: 1px solid rgba(255,255,255,.05);
|
||
flex-shrink: 0;
|
||
}
|
||
.ps-title {
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.68rem; font-weight: 700;
|
||
color: #9B5DE5; letter-spacing: .05em; margin-bottom: 8px;
|
||
}
|
||
.ps-search {
|
||
width: 100%; padding: 7px 10px; border-radius: 8px;
|
||
background: rgba(255,255,255,.06); border: 1.5px solid rgba(255,255,255,.1);
|
||
color: #ddd; font-family: 'Manrope', sans-serif; font-size: 0.8rem;
|
||
margin-bottom: 8px;
|
||
}
|
||
.ps-search:focus { outline: none; border-color: rgba(155,93,229,.5); }
|
||
.ps-cats { display: flex; gap: 4px; flex-wrap: wrap; }
|
||
.ps-cat {
|
||
font-size: 0.64rem; font-weight: 700; padding: 2px 7px; border-radius: 999px;
|
||
border: 1px solid rgba(255,255,255,.1); cursor: pointer; transition: all .15s;
|
||
background: rgba(255,255,255,.04); color: #666;
|
||
}
|
||
.ps-cat.active { background: rgba(155,93,229,.2); border-color: var(--violet); color: #c084fc; }
|
||
.ps-list { flex: 1; overflow-y: auto; padding: 6px 8px; }
|
||
.mol-row {
|
||
display: flex; align-items: center; gap: 8px; padding: 7px 8px;
|
||
border-radius: 8px; border: 1px solid transparent;
|
||
cursor: pointer; transition: all .15s; margin-bottom: 2px;
|
||
}
|
||
.mol-row:hover { background: rgba(155,93,229,.1); border-color: rgba(155,93,229,.15); }
|
||
.mol-row.in-compare { background: rgba(155,93,229,.18); border-color: var(--violet); }
|
||
.mol-thumb { width: 34px; height: 34px; border-radius: 8px; flex-shrink: 0; background: rgba(255,255,255,.04); }
|
||
.mol-info { flex: 1; min-width: 0; }
|
||
.mol-name { font-size: 0.74rem; font-weight: 700; color: #ddd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.mol-formula { font-size: 0.68rem; color: #06D6E0; font-family: monospace; }
|
||
|
||
/* ── Right: comparison area ── */
|
||
.props-main {
|
||
flex: 1; overflow-y: auto; padding: 20px 24px;
|
||
display: flex; flex-direction: column; gap: 20px;
|
||
}
|
||
.props-hero {
|
||
display: flex; align-items: center; gap: 14px;
|
||
}
|
||
.props-hero-title {
|
||
font-family: 'Unbounded', sans-serif; font-size: 1.1rem; font-weight: 800;
|
||
color: #fff;
|
||
}
|
||
.props-hero-sub { font-size: 0.78rem; color: #666; margin-top: 2px; }
|
||
.compare-hint {
|
||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||
flex-direction: column; gap: 10px; color: #444;
|
||
padding: 60px 24px; text-align: center; border: 1.5px dashed rgba(255,255,255,.06);
|
||
border-radius: 16px;
|
||
}
|
||
.compare-hint-icon { font-size: 2.5rem; }
|
||
.compare-hint-text { font-size: 0.82rem; }
|
||
|
||
/* ── Comparison cards ── */
|
||
.compare-grid {
|
||
display: grid; gap: 16px;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
}
|
||
.compare-card {
|
||
background: rgba(15,15,35,.9); border: 1.5px solid rgba(155,93,229,.15);
|
||
border-radius: 16px; overflow: hidden; position: relative;
|
||
}
|
||
.compare-card .cc-canvas-wrap {
|
||
height: 160px; background: #08080f; position: relative;
|
||
}
|
||
.compare-card canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||
.cc-remove {
|
||
position: absolute; top: 8px; right: 8px; width: 24px; height: 24px;
|
||
border-radius: 6px; background: rgba(239,68,68,.2); border: 1px solid rgba(239,68,68,.3);
|
||
color: #f87171; font-size: 12px; cursor: pointer; display: flex; align-items: center;
|
||
justify-content: center; transition: all .15s; z-index: 5;
|
||
}
|
||
.cc-remove:hover { background: rgba(239,68,68,.4); }
|
||
.cc-body { padding: 12px 14px 14px; }
|
||
.cc-formula {
|
||
font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800;
|
||
background: linear-gradient(135deg,#a78bfa,#06D6E0);
|
||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||
background-clip: text; margin-bottom: 2px;
|
||
}
|
||
.cc-name { font-size: 0.78rem; font-weight: 700; color: #ddd; margin-bottom: 10px; }
|
||
.cc-props { display: flex; flex-direction: column; gap: 5px; }
|
||
.cc-prop { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }
|
||
.cc-prop-label { font-size: 0.66rem; font-weight: 700; color: #555; text-transform: uppercase; letter-spacing: .04em; }
|
||
.cc-prop-val { font-size: 0.78rem; font-weight: 700; color: #ccc; text-align: right; }
|
||
.cc-prop-val.highlight { color: #c084fc; }
|
||
.cc-cat {
|
||
display: inline-block; font-size: 0.62rem; font-weight: 700; padding: 2px 7px;
|
||
border-radius: 999px; margin-top: 6px;
|
||
}
|
||
.cc-cat.inorganic { background: rgba(59,130,246,.15); color: #60a5fa; border: 1px solid rgba(59,130,246,.2); }
|
||
.cc-cat.organic { background: rgba(74,222,128,.12); color: #4ade80; border: 1px solid rgba(74,222,128,.2); }
|
||
.cc-cat.biomolecule{ background: rgba(251,191,36,.12); color: #fbbf24; border: 1px solid rgba(251,191,36,.2); }
|
||
.cc-cat.amino_acid { background: rgba(244,114,182,.12); color: #f472b6; border: 1px solid rgba(244,114,182,.2); }
|
||
|
||
/* ── Comparison table (when 2+ molecules) ── */
|
||
.compare-table-wrap { overflow-x: auto; }
|
||
.compare-table {
|
||
width: 100%; border-collapse: collapse; font-size: 0.78rem;
|
||
}
|
||
.compare-table th, .compare-table td {
|
||
padding: 9px 14px; text-align: left;
|
||
border-bottom: 1px solid rgba(255,255,255,.05);
|
||
}
|
||
.compare-table th {
|
||
background: rgba(155,93,229,.1); color: #c084fc;
|
||
font-weight: 700; font-size: 0.7rem; text-transform: uppercase; letter-spacing: .05em;
|
||
white-space: nowrap;
|
||
}
|
||
.compare-table th.mol-col { color: #06D6E0; background: rgba(6,214,224,.06); }
|
||
.compare-table td.row-label { color: #666; font-weight: 700; font-size: 0.69rem; white-space: nowrap; }
|
||
.compare-table td.mol-val { color: #ddd; font-weight: 600; }
|
||
.compare-table tr:hover td { background: rgba(255,255,255,.02); }
|
||
|
||
/* ── Add placeholder card ── */
|
||
.add-card {
|
||
border: 1.5px dashed rgba(155,93,229,.2); border-radius: 16px;
|
||
display: flex; align-items: center; justify-content: center; flex-direction: column;
|
||
gap: 8px; color: #444; cursor: pointer; transition: all .2s;
|
||
min-height: 240px; font-size: 0.78rem; padding: 20px;
|
||
}
|
||
.add-card:hover { border-color: rgba(155,93,229,.5); color: #9B5DE5; background: rgba(155,93,229,.05); }
|
||
.add-card-icon { font-size: 2rem; }
|
||
|
||
/* ── Mobile ── */
|
||
@media (max-width: 768px) {
|
||
html, body { overflow: auto; }
|
||
.app-layout { height: auto; overflow: visible; }
|
||
.sb-content { overflow: auto; }
|
||
.props-layout { flex-direction: column; overflow: visible; }
|
||
.props-sidebar { width: 100%; height: auto; max-height: 45vh; border-right: none; border-bottom: 1px solid rgba(155,93,229,.12); flex-shrink: 0; }
|
||
.ps-list { max-height: 30vh; }
|
||
.props-main { overflow-y: auto; }
|
||
.compare-table { font-size: 0.72rem; }
|
||
.compare-table th, .compare-table td { padding: 6px 10px; }
|
||
}
|
||
@media (max-width: 480px) {
|
||
.props-sidebar { max-height: 40vh; }
|
||
.mol-card-grid { grid-template-columns: 1fr !important; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-layout" id="app">
|
||
<aside class="sidebar" id="app-sidebar"></aside>
|
||
<div class="notif-drop" id="notif-drop"></div>
|
||
|
||
<div class="sb-content" style="overflow:hidden;display:flex;flex-direction:column">
|
||
<div class="props-layout">
|
||
<!-- Left: molecule list -->
|
||
<div class="props-sidebar">
|
||
<div class="ps-head">
|
||
<div class="ps-title">Молекулы</div>
|
||
<input type="text" class="ps-search" placeholder="Поиск…" oninput="filterMols(this.value)" id="mol-search" />
|
||
<div class="ps-cats">
|
||
<button class="ps-cat active" data-cat="" onclick="setCat(this,'')">Все</button>
|
||
<button class="ps-cat" data-cat="inorganic" onclick="setCat(this,'inorganic')">Неорг.</button>
|
||
<button class="ps-cat" data-cat="organic" onclick="setCat(this,'organic')">Орган.</button>
|
||
<button class="ps-cat" data-cat="biomolecule" onclick="setCat(this,'biomolecule')">Биомол.</button>
|
||
<button class="ps-cat" data-cat="amino_acid" onclick="setCat(this,'amino_acid')">АК</button>
|
||
</div>
|
||
</div>
|
||
<div class="ps-list" id="mol-list"><div style="padding:20px;color:#555;font-size:.8rem">Загрузка…</div></div>
|
||
</div>
|
||
|
||
<!-- Right: comparison -->
|
||
<div class="props-main" id="props-main">
|
||
<div class="props-hero">
|
||
<div>
|
||
<div class="props-hero-title">Сравнение молекул</div>
|
||
<div class="props-hero-sub">Добавь до 4 молекул из списка слева</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="compare-area">
|
||
<div class="compare-hint" id="compare-empty">
|
||
<div class="compare-hint-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg></div>
|
||
<div class="compare-hint-text">Выбери молекулы из списка, чтобы сравнить их свойства</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/api.js"></script>
|
||
<script src="/js/sidebar.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||
<script>
|
||
'use strict';
|
||
const { user, isTeacher, isAdmin } = LS.initPage();
|
||
if (!user) location.href = '/login';
|
||
|
||
const nav = document.getElementById('nav-user');
|
||
const ava = document.getElementById('nav-avatar');
|
||
if (nav) nav.textContent = user?.name?.split(' ')[0] || 'Пользователь';
|
||
if (ava) ava.textContent = (user?.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||
if (isAdmin) document.getElementById('btn-admin').style.display = '';
|
||
if (isTeacher) document.getElementById('btn-classes').style.display = '';
|
||
LS.showBoardIfAllowed();
|
||
|
||
// ── Atomic masses (g/mol) ──
|
||
const ATOMIC_MASS = {
|
||
H:1.008, C:12.011, N:14.007, O:15.999, P:30.974, S:32.06,
|
||
Cl:35.45, Na:22.990, Ca:40.078, K:39.098, Mg:24.305, Fe:55.845,
|
||
Br:79.904, I:126.904, F:18.998, Al:26.982, Cu:63.546, Zn:65.38,
|
||
Ag:107.868, Ba:137.327, Mn:54.938,
|
||
};
|
||
|
||
// ── CPK colors (for canvas thumbnails) ──
|
||
const CPK = {
|
||
H:'#D4D4D4', C:'#555555', N:'#4060FF', O:'#EE2020', P:'#FF8000',
|
||
S:'#C8B400', Cl:'#00A860', Na:'#8040C0', Ca:'#707070', K:'#8040C0',
|
||
Mg:'#1E8A1E', Fe:'#B03010', Br:'#8B2222', F:'#90E050', Al:'#BFA6A6',
|
||
};
|
||
|
||
function cpkColor(sym) { return CPK[sym] || '#888'; }
|
||
|
||
function molarMass(formula) {
|
||
let mass = 0;
|
||
const re = /([A-Z][a-z]*)(\d*)/g;
|
||
let m;
|
||
while ((m = re.exec(formula)) !== null) {
|
||
const el = m[1], n = parseInt(m[2])||1;
|
||
mass += (ATOMIC_MASS[el]||0) * n;
|
||
}
|
||
return mass;
|
||
}
|
||
|
||
function catLabel(cat) {
|
||
return {inorganic:'Неорганика', organic:'Органика', biomolecule:'Биомолекула', amino_acid:'Аминокислота'}[cat] || cat || '—';
|
||
}
|
||
|
||
// ── Known physical properties ──
|
||
const PHYS_PROPS = {
|
||
'H2O': { state:'жидкость', solubility:'растворитель', bp:'100°C', mp:'0°C' },
|
||
'CO2': { state:'газ', solubility:'растворим', bp:'-78.5°C(субл.)', mp:'—' },
|
||
'O2': { state:'газ', solubility:'мало растворим', bp:'-183°C', mp:'-218.4°C' },
|
||
'H2': { state:'газ', solubility:'мало растворим', bp:'-252.9°C', mp:'-259.2°C' },
|
||
'N2': { state:'газ', solubility:'мало растворим', bp:'-195.8°C', mp:'-210°C' },
|
||
'NH3': { state:'газ', solubility:'хорошо растворим', bp:'-33.4°C', mp:'-77.7°C' },
|
||
'HCl': { state:'газ', solubility:'хорошо растворим', bp:'-85.1°C', mp:'-114.2°C' },
|
||
'H2SO4': { state:'жидкость', solubility:'растворитель', bp:'337°C', mp:'10°C' },
|
||
'HNO3': { state:'жидкость', solubility:'смешивается', bp:'83°C', mp:'-42°C' },
|
||
'NaOH': { state:'твёрдое', solubility:'хорошо растворим', bp:'1388°C', mp:'318°C' },
|
||
'NaCl': { state:'твёрдое', solubility:'растворим', bp:'1413°C', mp:'801°C' },
|
||
'CH4': { state:'газ', solubility:'мало растворим', bp:'-161.5°C', mp:'-182.5°C' },
|
||
'C2H5OH':{ state:'жидкость', solubility:'смешивается', bp:'78.4°C', mp:'-114.1°C' },
|
||
'C6H6': { state:'жидкость', solubility:'не растворим', bp:'80.1°C', mp:'5.5°C' },
|
||
'C6H12O6':{ state:'твёрдое', solubility:'растворим', bp:'разлагается', mp:'146°C' },
|
||
};
|
||
|
||
function getPhysProps(formula) {
|
||
return PHYS_PROPS[formula] || { state:'—', solubility:'—', bp:'—', mp:'—' };
|
||
}
|
||
|
||
// ── State ──
|
||
let _allMols = [];
|
||
let _filtered = [];
|
||
let _catFilter = '';
|
||
let _searchQ = '';
|
||
let _compare = []; // array of mol objects, max 4
|
||
|
||
async function init() {
|
||
try {
|
||
_allMols = await LS.biochemGetMolecules();
|
||
applyFilter();
|
||
} catch(e) {
|
||
document.getElementById('mol-list').innerHTML = '<div style="padding:20px;color:#666;font-size:.8rem">Ошибка загрузки</div>';
|
||
}
|
||
}
|
||
|
||
function applyFilter() {
|
||
_filtered = _allMols.filter(m => {
|
||
const q = _searchQ.toLowerCase();
|
||
const matchQ = !q || m.name_ru.toLowerCase().includes(q) || m.formula.toLowerCase().includes(q);
|
||
const matchCat = !_catFilter || m.category === _catFilter;
|
||
return matchQ && matchCat;
|
||
});
|
||
renderMolList();
|
||
}
|
||
|
||
function filterMols(q) { _searchQ = q; applyFilter(); }
|
||
function setCat(btn, cat) {
|
||
_catFilter = cat;
|
||
document.querySelectorAll('.ps-cat').forEach(b => b.classList.toggle('active', b.dataset.cat===cat));
|
||
applyFilter();
|
||
}
|
||
|
||
function renderMolList() {
|
||
const list = document.getElementById('mol-list');
|
||
if (!_filtered.length) {
|
||
list.innerHTML = '<div style="padding:20px 8px;color:#555;font-size:.78rem">Ничего не найдено</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = _filtered.map(m => {
|
||
const inCompare = _compare.some(c => c.id === m.id);
|
||
return `<div class="mol-row ${inCompare?'in-compare':''}" onclick="toggleCompare(${m.id})">
|
||
<canvas class="mol-thumb" id="thumb-${m.id}" width="34" height="34"></canvas>
|
||
<div class="mol-info">
|
||
<div class="mol-name">${m.name_ru}</div>
|
||
<div class="mol-formula">${m.formula}</div>
|
||
</div>
|
||
${inCompare ? '<span style="color:#c084fc;font-size:11px"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></span>' : ''}
|
||
</div>`;
|
||
}).join('');
|
||
// draw thumbnails
|
||
setTimeout(() => { _filtered.forEach(m => drawThumb(m)); }, 0);
|
||
}
|
||
|
||
function drawThumb(mol) {
|
||
const canvas = document.getElementById('thumb-'+mol.id);
|
||
if (!canvas) return;
|
||
const ctx = canvas.getContext('2d');
|
||
const W = 34, H = 34;
|
||
ctx.clearRect(0,0,W,H);
|
||
ctx.fillStyle = '#08080f';
|
||
ctx.fillRect(0,0,W,H);
|
||
const atoms = mol.atoms_json || [];
|
||
const bonds = mol.bonds_json || [];
|
||
if (!atoms.length) return;
|
||
let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
|
||
for (const a of atoms) { minX=Math.min(minX,a.x??a.id); minY=Math.min(minY,a.y??0); maxX=Math.max(maxX,a.x??a.id); maxY=Math.max(maxY,a.y??0); }
|
||
const pad=6, molW=Math.max(maxX-minX,1), molH=Math.max(maxY-minY,1);
|
||
const sc=Math.min((W-pad*2)/molW,(H-pad*2)/molH);
|
||
const ox=pad+(W-pad*2-molW*sc)/2, oy=pad+(H-pad*2-molH*sc)/2;
|
||
const sx=a=>ox+(a.x-minX)*sc, sy=a=>oy+(a.y-minY)*sc;
|
||
// bonds
|
||
ctx.strokeStyle='rgba(180,180,200,.5)'; ctx.lineWidth=1;
|
||
for (const b of bonds) {
|
||
const a1=atoms.find(a=>a.id===b.from||(b.f&&a.id===b.f)), a2=atoms.find(a=>a.id===b.to||(b.t&&a.id===b.t));
|
||
if (!a1||!a2) continue;
|
||
ctx.beginPath(); ctx.moveTo(sx(a1),sy(a1)); ctx.lineTo(sx(a2),sy(a2)); ctx.stroke();
|
||
}
|
||
// atoms
|
||
for (const a of atoms) {
|
||
const r = Math.max(2, 4*sc);
|
||
const col = cpkColor(a.s);
|
||
ctx.beginPath(); ctx.arc(sx(a),sy(a),r,0,Math.PI*2);
|
||
const [r0,g0,b0] = hexRgb(col);
|
||
const grd = ctx.createRadialGradient(sx(a)-r*.3,sy(a)-r*.35,r*.05,sx(a),sy(a),r);
|
||
grd.addColorStop(0,`rgb(${Math.min(255,r0+80)},${Math.min(255,g0+80)},${Math.min(255,b0+80)})`);
|
||
grd.addColorStop(1,`rgb(${Math.round(r0*.3)},${Math.round(g0*.3)},${Math.round(b0*.3)})`);
|
||
ctx.fillStyle=grd; ctx.fill();
|
||
}
|
||
}
|
||
|
||
function hexRgb(hex) {
|
||
hex=hex.replace('#','');
|
||
if(hex.length===3)hex=hex.split('').map(c=>c+c).join('');
|
||
const n=parseInt(hex,16);
|
||
return[(n>>16)&255,(n>>8)&255,n&255];
|
||
}
|
||
|
||
function toggleCompare(id) {
|
||
const mol = _allMols.find(m => m.id===id);
|
||
if (!mol) return;
|
||
const idx = _compare.findIndex(c => c.id===id);
|
||
if (idx >= 0) {
|
||
_compare.splice(idx, 1);
|
||
} else {
|
||
if (_compare.length >= 4) { LS.toast('Максимум 4 молекулы', 'info'); return; }
|
||
_compare.push(mol);
|
||
}
|
||
renderMolList();
|
||
renderCompare();
|
||
}
|
||
|
||
function removeFromCompare(id) {
|
||
_compare = _compare.filter(c => c.id !== id);
|
||
renderMolList();
|
||
renderCompare();
|
||
}
|
||
|
||
function renderCompare() {
|
||
const area = document.getElementById('compare-area');
|
||
if (!_compare.length) {
|
||
area.innerHTML = `<div class="compare-hint" id="compare-empty">
|
||
<div class="compare-hint-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg></div>
|
||
<div class="compare-hint-text">Выбери молекулы из списка, чтобы сравнить их свойства</div>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="compare-grid">';
|
||
for (const mol of _compare) {
|
||
const mm = molarMass(mol.formula);
|
||
const phys = getPhysProps(mol.formula);
|
||
html += `
|
||
<div class="compare-card">
|
||
<div class="cc-canvas-wrap">
|
||
<canvas id="ccc-${mol.id}" width="200" height="160"></canvas>
|
||
<button class="cc-remove" onclick="removeFromCompare(${mol.id})" title="Убрать"><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>
|
||
<div class="cc-body">
|
||
<div class="cc-formula">${mol.formula}</div>
|
||
<div class="cc-name">${mol.name_ru}</div>
|
||
<div class="cc-props">
|
||
<div class="cc-prop">
|
||
<span class="cc-prop-label">М. масса</span>
|
||
<span class="cc-prop-val highlight">${mm > 0 ? mm.toFixed(2) + ' г/моль' : '—'}</span>
|
||
</div>
|
||
<div class="cc-prop">
|
||
<span class="cc-prop-label">Состояние</span>
|
||
<span class="cc-prop-val">${phys.state}</span>
|
||
</div>
|
||
<div class="cc-prop">
|
||
<span class="cc-prop-label">Растворимость</span>
|
||
<span class="cc-prop-val">${phys.solubility}</span>
|
||
</div>
|
||
<div class="cc-prop">
|
||
<span class="cc-prop-label">Ткип</span>
|
||
<span class="cc-prop-val">${phys.bp}</span>
|
||
</div>
|
||
<div class="cc-prop">
|
||
<span class="cc-prop-label">Тпл</span>
|
||
<span class="cc-prop-val">${phys.mp}</span>
|
||
</div>
|
||
<div class="cc-prop">
|
||
<span class="cc-prop-label">Сложность</span>
|
||
<span class="cc-prop-val">${['','<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>','<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>','<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>'][mol.difficulty]||'—'}</span>
|
||
</div>
|
||
</div>
|
||
<span class="cc-cat ${mol.category||''}">${catLabel(mol.category)}</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
// Add placeholder if < 4
|
||
if (_compare.length < 4) {
|
||
html += `<div class="add-card" onclick="document.getElementById('mol-search').focus()">
|
||
<div class="add-card-icon">+</div>
|
||
<div>Добавить молекулу</div>
|
||
</div>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
// Comparison table (when 2+)
|
||
if (_compare.length >= 2) {
|
||
html += `<div class="compare-table-wrap"><table class="compare-table">
|
||
<thead><tr>
|
||
<th>Свойство</th>
|
||
${_compare.map(m=>`<th class="mol-col">${m.formula}</th>`).join('')}
|
||
</tr></thead>
|
||
<tbody>
|
||
<tr><td class="row-label">Название</td>${_compare.map(m=>`<td class="mol-val">${m.name_ru}</td>`).join('')}</tr>
|
||
<tr><td class="row-label">Молярная масса</td>${_compare.map(m=>{const mm=molarMass(m.formula);return`<td class="mol-val">${mm>0?mm.toFixed(2)+' г/моль':'—'}</td>`}).join('')}</tr>
|
||
<tr><td class="row-label">Агр. состояние</td>${_compare.map(m=>`<td class="mol-val">${getPhysProps(m.formula).state}</td>`).join('')}</tr>
|
||
<tr><td class="row-label">Растворимость</td>${_compare.map(m=>`<td class="mol-val">${getPhysProps(m.formula).solubility}</td>`).join('')}</tr>
|
||
<tr><td class="row-label">Температура кипения</td>${_compare.map(m=>`<td class="mol-val">${getPhysProps(m.formula).bp}</td>`).join('')}</tr>
|
||
<tr><td class="row-label">Температура плавления</td>${_compare.map(m=>`<td class="mol-val">${getPhysProps(m.formula).mp}</td>`).join('')}</tr>
|
||
<tr><td class="row-label">Категория</td>${_compare.map(m=>`<td class="mol-val">${catLabel(m.category)}</td>`).join('')}</tr>
|
||
<tr><td class="row-label">Сложность</td>${_compare.map(m=>`<td class="mol-val">${['','<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>','<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>','<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>'][m.difficulty]||'—'}</td>`).join('')}</tr>
|
||
<tr><td class="row-label">Описание</td>${_compare.map(m=>`<td class="mol-val" style="font-size:.7rem;color:#888">${m.description||'—'}</td>`).join('')}</tr>
|
||
</tbody>
|
||
</table></div>`;
|
||
}
|
||
|
||
area.innerHTML = html;
|
||
// Draw compare canvases
|
||
setTimeout(() => {
|
||
for (const mol of _compare) drawCompareCanvas(mol);
|
||
}, 0);
|
||
}
|
||
|
||
function drawCompareCanvas(mol) {
|
||
const canvas = document.getElementById('ccc-'+mol.id);
|
||
if (!canvas) return;
|
||
const W = canvas.offsetWidth || 200, H = canvas.offsetHeight || 160;
|
||
canvas.width = W; canvas.height = H;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.fillStyle = '#08080f';
|
||
ctx.fillRect(0,0,W,H);
|
||
const atoms = mol.atoms_json || [];
|
||
const bonds = mol.bonds_json || [];
|
||
if (!atoms.length) {
|
||
ctx.fillStyle = '#444';
|
||
ctx.font = '0.8rem Manrope,sans-serif';
|
||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||
ctx.fillText('Нет структуры', W/2, H/2);
|
||
return;
|
||
}
|
||
let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
|
||
for (const a of atoms) { minX=Math.min(minX,a.x); minY=Math.min(minY,a.y); maxX=Math.max(maxX,a.x); maxY=Math.max(maxY,a.y); }
|
||
const pad=20, molW=Math.max(maxX-minX,1), molH=Math.max(maxY-minY,1);
|
||
const sc=Math.min((W-pad*2)/molW,(H-pad*2)/molH, 2.5);
|
||
const ox=W/2-(minX+maxX)*sc/2, oy=H/2-(minY+maxY)*sc/2;
|
||
const sx=a=>ox+a.x*sc, sy=a=>oy+a.y*sc;
|
||
// bonds
|
||
for (const b of bonds) {
|
||
const a1=atoms.find(a=>a.id===(b.from??b.f)), a2=atoms.find(a=>a.id===(b.to??b.t));
|
||
if (!a1||!a2) continue;
|
||
ctx.strokeStyle='rgba(180,180,200,.55)'; ctx.lineWidth=Math.max(1.5,2*sc);
|
||
ctx.lineCap='round';
|
||
ctx.beginPath(); ctx.moveTo(sx(a1),sy(a1)); ctx.lineTo(sx(a2),sy(a2)); ctx.stroke();
|
||
}
|
||
// atoms
|
||
for (const a of atoms) {
|
||
const r = Math.max(6, 9*sc);
|
||
const col = cpkColor(a.s);
|
||
const [r0,g0,b0] = hexRgb(col);
|
||
const grd = ctx.createRadialGradient(sx(a)-r*.32,sy(a)-r*.38,r*.05,sx(a),sy(a),r);
|
||
grd.addColorStop(0,`rgb(${Math.min(255,r0+100)},${Math.min(255,g0+100)},${Math.min(255,b0+100)})`);
|
||
grd.addColorStop(.4, col);
|
||
grd.addColorStop(1,`rgb(${Math.round(r0*.2)},${Math.round(g0*.2)},${Math.round(b0*.2)})`);
|
||
ctx.beginPath(); ctx.arc(sx(a),sy(a),r,0,Math.PI*2);
|
||
ctx.fillStyle=grd; ctx.fill();
|
||
if (atoms.length <= 15) {
|
||
ctx.fillStyle = a.s==='C'||a.s==='N'||a.s==='Fe'||a.s==='Mg' ? '#fff' : '#111';
|
||
ctx.font=`bold ${Math.max(8,Math.round(r*.7))}px Manrope,sans-serif`;
|
||
ctx.textAlign='center'; ctx.textBaseline='middle';
|
||
ctx.fillText(a.s,sx(a),sy(a));
|
||
}
|
||
}
|
||
}
|
||
|
||
init();
|
||
if (window.lucide) lucide.createIcons();
|
||
LS.notif?.init();
|
||
LS.hideDisabledFeatures?.();
|
||
</script>
|
||
<script src="/js/notifications.js"></script>
|
||
<script src="/js/mobile.js"></script>
|
||
</body>
|
||
</html>
|