be4d43105e
Node.js/Express backend + vanilla JS frontend. Features: real-time collaborative whiteboard (SSE), multi-page support, LaTeX formulas, shapes/connectors, coordinate systems, number lines, compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with rotation & resize controls, minimap navigation overlay, auto-measurements, multi-page thumbnails sidebar, PNG export, page templates. Student/teacher workflows: classes, assignments, library, dashboard. Mobile responsive. SQLite (better-sqlite3). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1289 lines
85 KiB
HTML
1289 lines
85 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" />
|
||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||
<style>
|
||
html, body { height: 100%; overflow: hidden; }
|
||
.app-layout { height: 100vh; overflow: hidden; }
|
||
.sb-content { padding: 0 !important; overflow: hidden; display: flex; flex-direction: column; background: #07070f; }
|
||
.sb-sub-link { padding-left: 28px !important; font-size: 0.76rem !important; opacity: .75; }
|
||
.sb-sub-link:hover { opacity: 1; }
|
||
|
||
/* ── Page header ── */
|
||
.page-header {
|
||
padding: 18px 28px 14px;
|
||
background: linear-gradient(135deg, rgba(16,185,129,.1) 0%, rgba(155,93,229,.08) 100%);
|
||
border-bottom: 1px solid rgba(16,185,129,.15);
|
||
flex-shrink: 0;
|
||
display: flex; align-items: center; gap: 16px;
|
||
}
|
||
.page-header-icon {
|
||
width: 44px; height: 44px; border-radius: 14px;
|
||
background: rgba(16,185,129,.12); border: 1.5px solid rgba(16,185,129,.25);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 1.35rem; flex-shrink: 0;
|
||
}
|
||
.page-title {
|
||
font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800;
|
||
background: linear-gradient(135deg,#34d399,#a78bfa); -webkit-background-clip:text; -webkit-text-fill-color:transparent;
|
||
margin-bottom: 2px;
|
||
}
|
||
.page-subtitle { font-size: 0.78rem; color: #555; }
|
||
.header-right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
|
||
|
||
/* ── Pathway selector ── */
|
||
.path-selector {
|
||
display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
|
||
padding: 10px 20px;
|
||
background: rgba(7,7,20,.95);
|
||
border-bottom: 1px solid rgba(255,255,255,.06);
|
||
flex-shrink: 0;
|
||
}
|
||
.path-chip {
|
||
padding: 5px 14px; border-radius: 999px;
|
||
border: 1.5px solid rgba(255,255,255,.09);
|
||
background: rgba(255,255,255,.04); color: #666;
|
||
font-family: 'Manrope', sans-serif; font-size: 0.78rem; font-weight: 700;
|
||
cursor: pointer; transition: all .18s; white-space: nowrap;
|
||
display: flex; align-items: center; gap: 5px;
|
||
}
|
||
.path-chip:hover { border-color: rgba(52,211,153,.35); color: #ccc; background: rgba(52,211,153,.07); }
|
||
.path-chip.active { background: rgba(52,211,153,.16); border-color: rgba(52,211,153,.55); color: #34d399; }
|
||
.path-chip .chip-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
|
||
.path-chip[data-path="glycolysis"].active { color: #f59e0b; border-color: rgba(245,158,11,.5); background: rgba(245,158,11,.12); }
|
||
.path-chip[data-path="glycolysis"] .chip-dot { background: #f59e0b; }
|
||
.path-chip[data-path="krebs"].active { color: #06b6d4; border-color: rgba(6,182,212,.5); background: rgba(6,182,212,.12); }
|
||
.path-chip[data-path="krebs"] .chip-dot { background: #06b6d4; }
|
||
.path-chip[data-path="oxidation"].active { color: #fb923c; border-color: rgba(251,146,60,.5); background: rgba(251,146,60,.12); }
|
||
.path-chip[data-path="oxidation"] .chip-dot { background: #fb923c; }
|
||
.path-chip[data-path="synthesis"].active { color: #a78bfa; border-color: rgba(167,139,250,.5); background: rgba(167,139,250,.12); }
|
||
.path-chip[data-path="synthesis"] .chip-dot { background: #a78bfa; }
|
||
.sel-divider { width: 1px; height: 20px; background: rgba(255,255,255,.07); margin: 0 4px; }
|
||
.learn-btn {
|
||
margin-left: auto; padding: 5px 16px; border-radius: 8px;
|
||
background: linear-gradient(135deg,#34d399,#059669); border: none;
|
||
color: #fff; font-family:'Manrope',sans-serif; font-size:.8rem; font-weight:700;
|
||
cursor: pointer; transition: opacity .18s; display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.learn-btn:hover { opacity: .85; }
|
||
.anim-btn {
|
||
padding: 5px 14px; border-radius: 8px;
|
||
background: rgba(255,255,255,.05); border: 1.5px solid rgba(255,255,255,.1);
|
||
color: #aaa; font-family:'Manrope',sans-serif; font-size:.78rem; font-weight:600;
|
||
cursor: pointer; transition: all .18s; display: flex; align-items: center; gap: 5px;
|
||
}
|
||
.anim-btn.on { background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.4); color: #34d399; }
|
||
.anim-btn:hover { border-color: rgba(255,255,255,.2); color: #ccc; }
|
||
|
||
/* ── Main layout ── */
|
||
.paths-main {
|
||
flex: 1; display: flex; overflow: hidden; min-height: 0;
|
||
}
|
||
|
||
/* ── SVG Canvas area ── */
|
||
.svg-area {
|
||
flex: 1; overflow: hidden; position: relative;
|
||
background: radial-gradient(ellipse at 40% 40%, rgba(16,185,129,.04) 0%, transparent 60%),
|
||
radial-gradient(ellipse at 70% 70%, rgba(155,93,229,.04) 0%, transparent 60%),
|
||
#07070f;
|
||
cursor: grab;
|
||
}
|
||
.svg-area:active { cursor: grabbing; }
|
||
#pathway-svg {
|
||
width: 100%; height: 100%;
|
||
overflow: visible;
|
||
}
|
||
|
||
/* SVG node styles */
|
||
.mol-node { cursor: pointer; transition: filter .18s; }
|
||
.mol-node:hover .node-circle { filter: brightness(1.4); }
|
||
.mol-node .node-circle { transition: r .15s; }
|
||
.mol-node.active .node-circle { filter: drop-shadow(0 0 8px currentColor); }
|
||
.node-label {
|
||
font-family: 'Manrope', sans-serif; font-size: 11px; font-weight: 700;
|
||
fill: #e0e0f0; pointer-events: none; dominant-baseline: central; text-anchor: middle;
|
||
}
|
||
.node-formula {
|
||
font-family: 'Manrope', sans-serif; font-size: 9.5px;
|
||
fill: #777; pointer-events: none; dominant-baseline: central; text-anchor: middle;
|
||
}
|
||
.edge-arrow { marker-end: url(#arrowhead); }
|
||
.edge-label {
|
||
font-family: 'Manrope', sans-serif; font-size: 9px;
|
||
fill: #555; dominant-baseline: central; text-anchor: middle;
|
||
pointer-events: none;
|
||
}
|
||
.edge-enzyme {
|
||
font-family: 'Manrope', sans-serif; font-size: 8.5px; font-style: italic;
|
||
fill: #494; pointer-events: none; dominant-baseline: central; text-anchor: middle;
|
||
}
|
||
/* Particle animation */
|
||
.flow-particle { pointer-events: none; }
|
||
@keyframes particleAppear { from { opacity:0; transform:scale(0); } to { opacity:1; transform:scale(1); } }
|
||
|
||
/* ── Side panel ── */
|
||
.side-panel {
|
||
width: 320px; flex-shrink: 0;
|
||
border-left: 1px solid rgba(255,255,255,.07);
|
||
background: rgba(8,8,20,.9);
|
||
display: flex; flex-direction: column; overflow: hidden;
|
||
transition: width .25s;
|
||
}
|
||
.side-panel.hidden { width: 0; overflow: hidden; }
|
||
|
||
.panel-tabs {
|
||
display: flex; border-bottom: 1px solid rgba(255,255,255,.07); flex-shrink: 0;
|
||
}
|
||
.ptab {
|
||
flex: 1; padding: 10px 8px; text-align: center;
|
||
font-family: 'Manrope', sans-serif; font-size: .75rem; font-weight: 700;
|
||
color: #555; cursor: pointer; border-bottom: 2px solid transparent;
|
||
transition: all .18s; white-space: nowrap;
|
||
}
|
||
.ptab.active { color: #34d399; border-bottom-color: #34d399; }
|
||
.ptab:hover { color: #aaa; }
|
||
|
||
.panel-body { flex: 1; overflow-y: auto; padding: 16px; }
|
||
.panel-body::-webkit-scrollbar { width: 4px; }
|
||
.panel-body::-webkit-scrollbar-thumb { background: rgba(52,211,153,.25); border-radius: 4px; }
|
||
|
||
/* Mol popup card */
|
||
.mol-card { display: none; }
|
||
.mol-card.show { display: block; }
|
||
.mol-card-name {
|
||
font-family: 'Unbounded', sans-serif; font-size: .9rem; font-weight: 800;
|
||
color: #e0e0f0; margin-bottom: 3px;
|
||
}
|
||
.mol-card-formula {
|
||
font-size: .8rem; color: #888; margin-bottom: 12px; font-family: 'Manrope', sans-serif;
|
||
}
|
||
.mol-card-desc {
|
||
font-size: .8rem; color: #aaa; line-height: 1.55; font-family: 'Manrope', sans-serif;
|
||
margin-bottom: 12px;
|
||
}
|
||
.mol-card-props { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||
.mol-prop { padding: 3px 10px; border-radius: 6px; font-size: .72rem; font-weight: 600;
|
||
font-family: 'Manrope',sans-serif; }
|
||
.mol-prop.atp { background: rgba(245,158,11,.15); color: #f59e0b; border: 1px solid rgba(245,158,11,.25); }
|
||
.mol-prop.nadh { background: rgba(6,182,212,.15); color: #06b6d4; border: 1px solid rgba(6,182,212,.25); }
|
||
.mol-prop.co2 { background: rgba(239,68,68,.12); color: #f87171; border: 1px solid rgba(239,68,68,.2); }
|
||
.mol-prop.key { background: rgba(52,211,153,.12); color: #34d399; border: 1px solid rgba(52,211,153,.2); }
|
||
.mol-card-btn {
|
||
display: block; width: 100%; padding: 7px; border-radius: 9px; text-align: center;
|
||
background: rgba(52,211,153,.1); border: 1.5px solid rgba(52,211,153,.25);
|
||
color: #34d399; font-family:'Manrope',sans-serif; font-size:.78rem; font-weight:700;
|
||
cursor: pointer; text-decoration: none; transition: all .18s; margin-top: 4px;
|
||
}
|
||
.mol-card-btn:hover { background: rgba(52,211,153,.2); }
|
||
.mol-empty {
|
||
text-align: center; padding: 40px 20px;
|
||
color: #444; font-family:'Manrope',sans-serif; font-size:.83rem;
|
||
}
|
||
.mol-empty .hint-icon { font-size: 2.2rem; margin-bottom: 8px; }
|
||
|
||
/* ── Learn mode ── */
|
||
.learn-panel { display: none; }
|
||
.learn-panel.active { display: block; }
|
||
.learn-progress-bar {
|
||
height: 4px; border-radius: 2px; background: rgba(255,255,255,.07); margin-bottom: 14px;
|
||
}
|
||
.learn-progress-fill {
|
||
height: 100%; border-radius: 2px;
|
||
background: linear-gradient(90deg,#34d399,#a78bfa);
|
||
transition: width .4s;
|
||
}
|
||
.step-counter {
|
||
font-family:'Manrope',sans-serif; font-size:.72rem; color:#555; margin-bottom:10px;
|
||
}
|
||
.step-num {
|
||
display: inline-flex; align-items:center; justify-content:center;
|
||
width:28px; height:28px; border-radius:50%;
|
||
background: linear-gradient(135deg,#34d399,#059669);
|
||
color:#fff; font-family:'Unbounded',sans-serif; font-size:.7rem; font-weight:800;
|
||
margin-right: 8px; flex-shrink:0;
|
||
}
|
||
.step-title-row { display:flex; align-items:center; margin-bottom:10px; }
|
||
.step-title {
|
||
font-family:'Unbounded',sans-serif; font-size:.82rem; font-weight:800; color:#e0e0f0;
|
||
}
|
||
.step-desc {
|
||
font-family:'Manrope',sans-serif; font-size:.8rem; color:#aaa; line-height:1.6;
|
||
margin-bottom: 12px;
|
||
}
|
||
.step-energy {
|
||
display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px;
|
||
}
|
||
.energy-badge {
|
||
padding: 3px 10px; border-radius:6px; font-family:'Manrope',sans-serif;
|
||
font-size:.72rem; font-weight:700;
|
||
}
|
||
.energy-badge.atp-prod { background:rgba(245,158,11,.15); color:#f59e0b; }
|
||
.energy-badge.atp-used { background:rgba(239,68,68,.12); color:#f87171; }
|
||
.energy-badge.nadh { background:rgba(6,182,212,.12); color:#06b6d4; }
|
||
.energy-badge.fadh2 { background:rgba(167,139,250,.12);color:#a78bfa; }
|
||
.energy-badge.co2 { background:rgba(74,222,128,.1); color:#4ade80; }
|
||
|
||
/* Mini-quiz */
|
||
.mini-quiz { margin-top:12px; }
|
||
.quiz-q {
|
||
font-family:'Manrope',sans-serif; font-size:.8rem; font-weight:700; color:#ddd;
|
||
margin-bottom:8px;
|
||
}
|
||
.quiz-opts { display:flex; flex-direction:column; gap:5px; }
|
||
.quiz-opt {
|
||
padding:7px 12px; border-radius:8px;
|
||
border:1.5px solid rgba(255,255,255,.08); background:rgba(255,255,255,.03);
|
||
color:#aaa; font-family:'Manrope',sans-serif; font-size:.78rem;
|
||
cursor:pointer; transition:all .16s; text-align:left;
|
||
}
|
||
.quiz-opt:hover { border-color:rgba(52,211,153,.35); color:#ccc; background:rgba(52,211,153,.06); }
|
||
.quiz-opt.correct { border-color:rgba(52,211,153,.6) !important; background:rgba(52,211,153,.15) !important; color:#34d399 !important; }
|
||
.quiz-opt.wrong { border-color:rgba(239,68,68,.5) !important; background:rgba(239,68,68,.1) !important; color:#f87171 !important; }
|
||
.quiz-feedback {
|
||
margin-top:8px; font-family:'Manrope',sans-serif; font-size:.78rem;
|
||
padding:7px 12px; border-radius:8px;
|
||
}
|
||
.quiz-feedback.ok { background:rgba(52,211,153,.1); color:#34d399; }
|
||
.quiz-feedback.err { background:rgba(239,68,68,.08); color:#f87171; }
|
||
.step-nav { display:flex; gap:8px; margin-top:14px; }
|
||
.step-btn {
|
||
flex:1; padding:8px; border-radius:9px;
|
||
font-family:'Manrope',sans-serif; font-size:.78rem; font-weight:700;
|
||
cursor:pointer; transition:all .18s; border:none;
|
||
}
|
||
.step-btn.prev { background:rgba(255,255,255,.06); color:#888; }
|
||
.step-btn.prev:hover { background:rgba(255,255,255,.1); color:#ccc; }
|
||
.step-btn.next { background:linear-gradient(135deg,#34d399,#059669); color:#fff; }
|
||
.step-btn.next:hover { opacity:.88; }
|
||
.step-btn:disabled { opacity:.3; cursor:default; }
|
||
.learn-complete {
|
||
text-align:center; padding:20px 10px;
|
||
}
|
||
.complete-icon { font-size:2.5rem; margin-bottom:10px; }
|
||
.complete-title {
|
||
font-family:'Unbounded',sans-serif; font-size:.9rem; font-weight:800; color:#34d399;
|
||
margin-bottom:6px;
|
||
}
|
||
.complete-text { font-family:'Manrope',sans-serif; font-size:.78rem; color:#888; }
|
||
|
||
/* ── Path legend ── */
|
||
.path-legend {
|
||
padding: 12px 16px;
|
||
border-top: 1px solid rgba(255,255,255,.06);
|
||
flex-shrink: 0;
|
||
}
|
||
.legend-title {
|
||
font-family:'Manrope',sans-serif; font-size:.7rem; font-weight:700; color:#444;
|
||
text-transform:uppercase; letter-spacing:.06em; margin-bottom:6px;
|
||
}
|
||
.legend-items { display:flex; flex-wrap:wrap; gap:6px; }
|
||
.legend-item {
|
||
display:flex; align-items:center; gap:4px;
|
||
font-family:'Manrope',sans-serif; font-size:.72rem; color:#666;
|
||
}
|
||
.legend-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
||
.legend-line { width:18px; height:2px; flex-shrink:0; border-radius:1px; }
|
||
|
||
/* zoom controls */
|
||
.zoom-controls {
|
||
position: absolute; bottom: 16px; right: 16px;
|
||
display: flex; gap: 6px;
|
||
}
|
||
.zoom-btn {
|
||
width: 34px; height: 34px; border-radius: 9px;
|
||
background: rgba(15,15,30,.9); border: 1.5px solid rgba(255,255,255,.09);
|
||
color: #aaa; font-size: 1.1rem; cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
transition: all .16s;
|
||
}
|
||
.zoom-btn:hover { background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.3); color: #34d399; }
|
||
|
||
/* path stats bar */
|
||
.path-stats {
|
||
position: absolute; top: 12px; left: 16px;
|
||
display: flex; gap: 8px; pointer-events: none;
|
||
}
|
||
.pstat {
|
||
padding: 4px 12px; border-radius: 8px;
|
||
background: rgba(8,8,20,.85); border: 1px solid rgba(255,255,255,.08);
|
||
font-family:'Manrope',sans-serif; font-size:.73rem; font-weight:700;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.pstat.atp { color:#f59e0b; }
|
||
.pstat.nadh { color:#06b6d4; }
|
||
.pstat.co2 { color:#f87171; }
|
||
|
||
/* ── Mobile ── */
|
||
@media (max-width: 768px) {
|
||
.sb-content { flex-direction: column; overflow: auto; }
|
||
.page-header { padding: 10px 14px 0; gap: 8px; }
|
||
.page-title { font-size: 0.85rem !important; }
|
||
.page-subtitle { display: none; }
|
||
.header-right { gap: 4px; }
|
||
.path-selector { padding: 8px 12px; gap: 5px; flex-wrap: wrap; }
|
||
.paths-main { flex-direction: column-reverse; }
|
||
.svg-area { min-height: 55vw; flex: none; height: 55vw; }
|
||
.side-panel { width: 100% !important; max-height: 40vh; border-left: none !important; border-top: 1px solid rgba(255,255,255,.07); overflow-y: auto; }
|
||
.path-stats { top: 6px; left: 8px; gap: 4px; }
|
||
.zoom-controls { bottom: 8px; right: 8px; }
|
||
}
|
||
@media (max-width: 480px) {
|
||
.svg-area { min-height: 260px; height: 260px; }
|
||
.path-chip { font-size: 0.68rem; padding: 4px 10px; }
|
||
.side-panel { max-height: 45vh; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-layout" id="app">
|
||
<aside class="sidebar">
|
||
<div class="sb-brand">
|
||
<a href="/dashboard" class="sb-logo"><span class="sb-lbl">Learn<span>Space</span></span></a>
|
||
<button class="sb-toggle" title="Свернуть меню"><i data-lucide="chevron-left" class="sb-icon"></i></button>
|
||
</div>
|
||
<nav class="sb-nav">
|
||
<button class="sb-link" onclick="lsSearchOpen()" title="Ctrl+K"><i data-lucide="search" class="sb-icon"></i><span class="sb-lbl">Поиск</span></button>
|
||
<a href="/dashboard" class="sb-link"><i data-lucide="home" class="sb-icon"></i><span class="sb-lbl">Дашборд</span></a>
|
||
<a href="/board" class="sb-link" id="btn-board" style="display:none"><i data-lucide="layout-dashboard" class="sb-icon"></i><span class="sb-lbl">Доска</span></a>
|
||
<a href="/classes" class="sb-link" id="btn-classes" style="display:none"><i data-lucide="graduation-cap" class="sb-icon"></i><span class="sb-lbl">Классы</span></a>
|
||
<a href="/library" class="sb-link"><i data-lucide="book-open" class="sb-icon"></i><span class="sb-lbl">Библиотека</span></a>
|
||
<a href="/theory" class="sb-link"><i data-lucide="brain" class="sb-icon"></i><span class="sb-lbl">Теория</span></a>
|
||
<a href="/lab" class="sb-link"><i data-lucide="atom" class="sb-icon"></i><span class="sb-lbl">Лаборатория</span></a>
|
||
<a href="/biochem" class="sb-link"><i data-lucide="flask-conical" class="sb-icon"></i><span class="sb-lbl">Биохимия</span></a>
|
||
<a href="/biochem-library" class="sb-link sb-sub-link"><i data-lucide="library" class="sb-icon"></i><span class="sb-lbl">↳ Библиотека</span></a>
|
||
<a href="/biochem-reactions" class="sb-link sb-sub-link"><i data-lucide="arrow-right-left" class="sb-icon"></i><span class="sb-lbl">↳ Реакции</span></a>
|
||
<a href="/biochem-properties" class="sb-link sb-sub-link"><i data-lucide="table-properties" class="sb-icon"></i><span class="sb-lbl">↳ Свойства</span></a>
|
||
<a href="/biochem-pathways" class="sb-link sb-sub-link active"><i data-lucide="route" class="sb-icon"></i><span class="sb-lbl">↳ Пути</span></a>
|
||
<a href="/hangman" class="sb-link"><i data-lucide="gamepad-2" class="sb-icon"></i><span class="sb-lbl">Виселица</span></a>
|
||
<a href="/crossword" class="sb-link"><i data-lucide="grid-3x3" class="sb-icon"></i><span class="sb-lbl">Кроссворд</span></a>
|
||
<a href="/pet" class="sb-link"><i data-lucide="heart" class="sb-icon"></i><span class="sb-lbl">Питомец</span></a>
|
||
<a href="/collection" class="sb-link"><i data-lucide="layers" class="sb-icon"></i><span class="sb-lbl">Коллекция</span></a>
|
||
<a href="/knowledge-map" class="sb-link"><i data-lucide="share-2" class="sb-icon"></i><span class="sb-lbl">Карта знаний</span></a>
|
||
<a href="/red-book.html" class="sb-link"><i data-lucide="leaf" class="sb-icon"></i><span class="sb-lbl">Красная книга</span></a>
|
||
<a href="/classroom" class="sb-link"><i data-lucide="presentation" class="sb-icon"></i><span class="sb-lbl">Онлайн-урок</span></a>
|
||
<div class="sb-divider"></div>
|
||
<a href="/analytics" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="bar-chart-2" class="sb-icon"></i><span class="sb-lbl">Аналитика</span></a>
|
||
<a href="/question-bank" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="database" class="sb-icon"></i><span class="sb-lbl">Банк вопросов</span></a>
|
||
<a href="/live-quiz" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="radio" class="sb-icon"></i><span class="sb-lbl">Live-квиз</span></a>
|
||
<a href="/gradebook" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="table" class="sb-icon"></i><span class="sb-lbl">Журнал</span></a>
|
||
<a href="/admin" class="sb-link" id="btn-admin" style="display:none"><i data-lucide="settings" class="sb-icon"></i><span class="sb-lbl">Управление</span></a>
|
||
</nav>
|
||
<div style="padding:4px 2px">
|
||
<div id="notif-wrap">
|
||
<button class="sb-link" id="notif-btn" onclick="LS.notif.toggle()">
|
||
<i data-lucide="bell" class="sb-icon"></i><span class="sb-lbl">Уведомления</span>
|
||
<span class="sb-badge" id="notif-badge" style="display:none"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="sb-foot">
|
||
<a href="/profile" class="sb-user-row" style="text-decoration:none">
|
||
<div class="sb-avatar" id="nav-avatar">?</div>
|
||
<div class="sb-user-info">
|
||
<div class="sb-user-name" id="nav-user">—</div>
|
||
<span class="sb-logout" style="pointer-events:none">Мой профиль</span>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
</aside>
|
||
<div class="notif-drop" id="notif-drop"></div>
|
||
|
||
<div class="sb-content">
|
||
<!-- Header -->
|
||
<div class="page-header">
|
||
<div class="page-header-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M6 18h8"/><path d="M3 22h18"/><path d="M14 22a7 7 0 1 0 0-14h-1"/><path d="M9 14l2-7"/><path d="M12 14l2-7"/></svg></div>
|
||
<div>
|
||
<div class="page-title">Метаболические пути</div>
|
||
<div class="page-subtitle" id="path-subtitle">Интерактивные схемы обмена веществ</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<div class="nav-user-chip">
|
||
<div class="nav-avatar" id="nav-avatar2"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pathway selector -->
|
||
<div class="path-selector">
|
||
<span style="font-family:'Manrope',sans-serif;font-size:.75rem;color:#444;font-weight:700;">Путь:</span>
|
||
<button class="path-chip active" data-path="glycolysis" onclick="selectPath('glycolysis')">
|
||
<span class="chip-dot"></span>Гликолиз
|
||
</button>
|
||
<button class="path-chip" data-path="krebs" onclick="selectPath('krebs')">
|
||
<span class="chip-dot"></span>Цикл Кребса
|
||
</button>
|
||
<button class="path-chip" data-path="oxidation" onclick="selectPath('oxidation')">
|
||
<span class="chip-dot"></span>β-Окисление
|
||
</button>
|
||
<button class="path-chip" data-path="synthesis" onclick="selectPath('synthesis')">
|
||
<span class="chip-dot"></span>Синтез белка
|
||
</button>
|
||
<div class="sel-divider"></div>
|
||
<button class="anim-btn on" id="anim-toggle" onclick="toggleAnimation()">
|
||
<i data-lucide="play-circle" style="width:14px;height:14px"></i> Анимация
|
||
</button>
|
||
<button class="learn-btn" onclick="startLearn()">
|
||
<i data-lucide="graduation-cap" style="width:14px;height:14px"></i> Пройти путь
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Main content -->
|
||
<div class="paths-main">
|
||
<!-- SVG area -->
|
||
<div class="svg-area" id="svg-area">
|
||
<svg id="pathway-svg" xmlns="http://www.w3.org/2000/svg">
|
||
<defs>
|
||
<marker id="arrowhead" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||
<polygon points="0 0, 8 3, 0 6" fill="rgba(120,120,160,0.7)" />
|
||
</marker>
|
||
<marker id="arrowhead-active" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||
<polygon points="0 0, 8 3, 0 6" fill="#34d399" />
|
||
</marker>
|
||
<marker id="arrowhead-gly" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||
<polygon points="0 0, 8 3, 0 6" fill="#f59e0b" />
|
||
</marker>
|
||
<marker id="arrowhead-krebs" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||
<polygon points="0 0, 8 3, 0 6" fill="#06b6d4" />
|
||
</marker>
|
||
<filter id="glow-green">
|
||
<feGaussianBlur stdDeviation="3" result="blur"/>
|
||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||
</filter>
|
||
<filter id="glow-amber">
|
||
<feGaussianBlur stdDeviation="3" result="blur"/>
|
||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||
</filter>
|
||
</defs>
|
||
<g id="svg-root" transform="translate(0,0) scale(1)">
|
||
<g id="edges-layer"></g>
|
||
<g id="nodes-layer"></g>
|
||
<g id="particles-layer"></g>
|
||
</g>
|
||
</svg>
|
||
<!-- Path stats overlay -->
|
||
<div class="path-stats" id="path-stats"></div>
|
||
<!-- Zoom controls -->
|
||
<div class="zoom-controls">
|
||
<button class="zoom-btn" onclick="zoom(1.2)" title="Увеличить">+</button>
|
||
<button class="zoom-btn" onclick="zoom(0.83)" title="Уменьшить">−</button>
|
||
<button class="zoom-btn" onclick="resetView()" title="Сбросить">⊙</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Side panel -->
|
||
<div class="side-panel" id="side-panel">
|
||
<div class="panel-tabs">
|
||
<div class="ptab active" id="tab-mol" onclick="switchTab('mol')">Молекула</div>
|
||
<div class="ptab" id="tab-learn" onclick="switchTab('learn')">Обучение</div>
|
||
<div class="ptab" id="tab-info" onclick="switchTab('info')">Путь</div>
|
||
</div>
|
||
<div class="panel-body" id="panel-mol">
|
||
<div class="mol-empty" id="mol-empty">
|
||
<div class="hint-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M4 4l7.07 17 2.51-7.39L21 11.07z"/></svg></div>
|
||
<div>Кликни на молекулу<br/>на схеме</div>
|
||
</div>
|
||
<div class="mol-card" id="mol-card">
|
||
<div class="mol-card-name" id="mc-name"></div>
|
||
<div class="mol-card-formula" id="mc-formula"></div>
|
||
<div class="mol-card-desc" id="mc-desc"></div>
|
||
<div class="mol-card-props" id="mc-props"></div>
|
||
<a class="mol-card-btn" id="mc-link">Открыть в редакторе <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></a>
|
||
</div>
|
||
</div>
|
||
<div class="panel-body" id="panel-learn" style="display:none">
|
||
<div class="learn-panel" id="learn-inactive" style="display:block">
|
||
<div class="mol-empty">
|
||
<div class="hint-icon"><svg class="ic" viewBox="0 0 24 24"><polygon points="22 10 12 5 2 10 12 15 22 10"/><polyline points="6 12 6 17"/><path d="M18 13.5V17c-3 1.5-9 1.5-12 0v-3.5"/></svg></div>
|
||
<div>Нажми «Пройти путь»<br/>чтобы начать обучение</div>
|
||
</div>
|
||
</div>
|
||
<div class="learn-panel" id="learn-active"></div>
|
||
</div>
|
||
<div class="panel-body" id="panel-info" style="display:none">
|
||
<div id="path-info-content"></div>
|
||
</div>
|
||
|
||
<div class="path-legend" id="path-legend">
|
||
<div class="legend-title">Обозначения</div>
|
||
<div class="legend-items" id="legend-items"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/api.js"></script>
|
||
<script>
|
||
// ═══════════════════════════════════════════════════════
|
||
// PATHWAY DATA
|
||
// ═══════════════════════════════════════════════════════
|
||
const PATHWAYS = {
|
||
glycolysis: {
|
||
name: 'Гликолиз',
|
||
color: '#f59e0b',
|
||
colorRgb: '245,158,11',
|
||
desc: '10 реакций расщепления глюкозы до пирувата. Происходит в цитоплазме. Выход: 2 АТФ (нетто), 2 НАДН, 2 пируват.',
|
||
stats: [
|
||
{ label: '−2 АТФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> +4 АТФ', cls: 'atp' },
|
||
{ label: '2 НАДН', cls: 'nadh' },
|
||
],
|
||
legend: [
|
||
{ color: '#f59e0b', type: 'circle', label: 'Метаболит' },
|
||
{ color: '#f59e0b', type: 'line', label: 'Реакция' },
|
||
{ color: '#f59e0b88', type: 'circle-sm', label: 'Кофактор (АТФ/НАД)' },
|
||
],
|
||
// nodes: id, label, formula, x, y, role
|
||
nodes: [
|
||
{ id:'glc', label:'Глюкоза', formula:'C₆H₁₂O₆', x:400, y:60, role:'substrate', desc:'Исходный субстрат гликолиза. 6-углеродный сахар, главный источник энергии клетки.', props:[] },
|
||
{ id:'g6p', label:'Глюкозо-6-Ф', formula:'C₆H₁₃O₉P', x:400, y:145, role:'inter', desc:'Глюкозо-6-фосфат. Образуется при фосфорилировании глюкозы за счёт АТФ. Удерживает молекулу в клетке.', props:['−1 АТФ'] },
|
||
{ id:'f6p', label:'Фруктозо-6-Ф',formula:'C₆H₁₃O₉P', x:400, y:225, role:'inter', desc:'Изомер глюкозо-6-фосфата. Образуется при изомеризации ферментом фосфоглюкоизомеразой.', props:[] },
|
||
{ id:'f16bp', label:'Фруктозо-1,6-бФ',formula:'C₆H₁₄O₁₂P₂', x:400, y:310, role:'key', desc:'Фруктозо-1,6-бисфосфат — ключевой регуляторный метаболит. Образование катализирует фосфофруктокиназа-1 (ФФК-1).', props:['−1 АТФ', 'Контроль скорости'] },
|
||
{ id:'dhap', label:'ДГАФ', formula:'C₃H₇O₆P', x:260, y:395, role:'inter', desc:'Дигидроксиацетонфосфат — один из двух триозофосфатов при расщеплении фруктозо-1,6-бисфосфата. Быстро конвертируется в ГАФ.', props:[] },
|
||
{ id:'gap', label:'ГАФ', formula:'C₃H₇O₆P', x:540, y:395, role:'inter', desc:'Глицеральдегид-3-фосфат (ГАФ) — непосредственный субстрат следующих реакций. Оба триозофосфата канализируются через ГАФ.', props:[] },
|
||
{ id:'bpg', label:'1,3-бФГ', formula:'C₃H₈O₁₀P₂',x:540, y:480, role:'inter', desc:'1,3-бисфосфоглицерат. Образуется при окислении ГАФ, сопряжённом с восстановлением НАД⁺ в НАДН.', props:['2 НАДН'] },
|
||
{ id:'pg3', label:'3-ФГК', formula:'C₃H₇O₇P', x:540, y:560, role:'inter', desc:'3-фосфоглицерат. Образуется при субстратном фосфорилировании АДФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> АТФ ферментом фосфоглицераткиназой.', props:['+2 АТФ'] },
|
||
{ id:'pg2', label:'2-ФГК', formula:'C₃H₇O₇P', x:540, y:635, role:'inter', desc:'2-фосфоглицерат. Образуется при перемещении фосфатной группы с 3 на 2 положение.', props:[] },
|
||
{ id:'pep', label:'ФЕП', formula:'C₃H₅O₆P', x:540, y:710, role:'inter', desc:'Фосфоенолпируват (ФЕП) — высокоэнергетический промежуточный продукт. Образуется при дегидратации 2-ФГК.', props:[] },
|
||
{ id:'pyr', label:'Пируват', formula:'C₃H₄O₃', x:400, y:795, role:'product', desc:'Конечный продукт гликолиза. В аэробных условиях переходит в ацетил-КоА (цикл Кребса). В анаэробных <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> лактат или этанол.', props:['+2 АТФ', '2 молекулы'] },
|
||
],
|
||
edges: [
|
||
{ from:'glc', to:'g6p', enzyme:'Гексокиназа', co:'-АТФ', curveX:0 },
|
||
{ from:'g6p', to:'f6p', enzyme:'ФГИ', curveX:0 },
|
||
{ from:'f6p', to:'f16bp', enzyme:'ФФК-1', co:'-АТФ', curveX:0 },
|
||
{ from:'f16bp',to:'dhap', enzyme:'Альдолаза', curveX:0 },
|
||
{ from:'f16bp',to:'gap', enzyme:'Альдолаза', curveX:0 },
|
||
{ from:'dhap', to:'gap', enzyme:'ТФИ', curveX:0 },
|
||
{ from:'gap', to:'bpg', enzyme:'ГАФДГ', co:'+НАДН', curveX:0 },
|
||
{ from:'bpg', to:'pg3', enzyme:'ФГК', co:'+АТФ', curveX:0 },
|
||
{ from:'pg3', to:'pg2', enzyme:'Фосфоглицератмутаза', curveX:0 },
|
||
{ from:'pg2', to:'pep', enzyme:'Енолаза', curveX:0 },
|
||
{ from:'pep', to:'pyr', enzyme:'Пируваткиназа', co:'+АТФ', curveX:0 },
|
||
],
|
||
steps: [
|
||
{
|
||
title:'Фосфорилирование глюкозы',
|
||
mol:'g6p',
|
||
desc:'Гексокиназа катализирует перенос фосфатной группы с АТФ на глюкозу, образуя глюкозо-6-фосфат (Г6Ф). Реакция необратима и «ловит» глюкозу в клетке.',
|
||
energy:[{label:'-1 АТФ', cls:'atp-used'}],
|
||
quiz:{ q:'Зачем глюкозу фосфорилируют в первой реакции?', opts:['Для выхода из клетки','Чтобы удержать глюкозу в клетке','Для образования НАДН','Для расщепления кольца'], ans:1 }
|
||
},
|
||
{
|
||
title:'Изомеризация',
|
||
mol:'f6p',
|
||
desc:'Фосфоглюкоизомераза превращает Г6Ф в фруктозо-6-фосфат (Ф6Ф). Реакция обратима и перестраивает альдозный сахар в кетозный.',
|
||
energy:[],
|
||
quiz:{ q:'Какой фермент катализирует изомеризацию Г6Ф <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Ф6Ф?', opts:['Гексокиназа','Альдолаза','Фосфоглюкоизомераза','Пируваткиназа'], ans:2 }
|
||
},
|
||
{
|
||
title:'Ключевой контрольный шаг',
|
||
mol:'f16bp',
|
||
desc:'Фосфофруктокиназа-1 (ФФК-1) фосфорилирует Ф6Ф <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> фруктозо-1,6-бисфосфат. Это необратимая реакция — главный регуляторный пункт гликолиза. АТФ ингибирует, АМФ/АДФ активирует.',
|
||
energy:[{label:'-1 АТФ', cls:'atp-used'}],
|
||
quiz:{ q:'Что является главным аллостерическим активатором ФФК-1?', opts:['АТФ','АМФ','НАДН','Пируват'], ans:1 }
|
||
},
|
||
{
|
||
title:'Расщепление на триозы',
|
||
mol:'gap',
|
||
desc:'Альдолаза расщепляет фруктозо-1,6-бисфосфат на два триозофосфата: ДГАФ и ГАФ (глицеральдегид-3-фосфат). Триозофосфатизомераза быстро конвертирует ДГАФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ГАФ.',
|
||
energy:[],
|
||
quiz:{ q:'Сколько молекул ГАФ образуется из одной глюкозы?', opts:['1','2','3','4'], ans:1 }
|
||
},
|
||
{
|
||
title:'Окислительное фосфорилирование',
|
||
mol:'bpg',
|
||
desc:'ГАФДГ окисляет ГАФ и присоединяет неорганический фосфат <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> 1,3-бисфосфоглицерат. Сопряжено с восстановлением НАД⁺ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> НАДН. Реакция субстратного фосфорилирования.',
|
||
energy:[{label:'+2 НАДН', cls:'nadh'}],
|
||
quiz:{ q:'Чем восстанавливается НАД⁺ в этой реакции?', opts:['ГАФ','Пируват','ДГАФ','АТФ'], ans:0 }
|
||
},
|
||
{
|
||
title:'Первая выработка АТФ',
|
||
mol:'pg3',
|
||
desc:'Фосфоглицераткиназа переносит фосфат с 1,3-бФГ на АДФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> АТФ. Это субстратное фосфорилирование — первый синтез АТФ в гликолизе. С каждой глюкозы получаем 2 АТФ.',
|
||
energy:[{label:'+2 АТФ', cls:'atp-prod'}],
|
||
quiz:{ q:'Как называется тип синтеза АТФ в этой реакции?', opts:['Окислительное фосфорилирование','Субстратное фосфорилирование','Фотофосфорилирование','Трансфосфорилирование'], ans:1 }
|
||
},
|
||
{
|
||
title:'Мутация фосфатной группы',
|
||
mol:'pg2',
|
||
desc:'Фосфоглицератмутаза перемещает фосфатную группу с 3-го на 2-е углеродное положение, подготавливая молекулу к дегидратации.',
|
||
energy:[],
|
||
quiz:{ q:'Какой продукт образуется из 3-ФГК под действием мутазы?', opts:['ФЕП','2-ФГК','Пируват','1,3-бФГ'], ans:1 }
|
||
},
|
||
{
|
||
title:'Образование ФЕП',
|
||
mol:'pep',
|
||
desc:'Енолаза катализирует дегидратацию 2-фосфоглицерата <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> фосфоенолпируват (ФЕП). ФЕП — высокоэнергетический соединение с большой отрицательной ΔG° гидролиза фосфата.',
|
||
energy:[],
|
||
quiz:{ q:'Почему ФЕП называют «высокоэнергетическим»?', opts:['Содержит много атомов С','Большая ΔG° гидролиза фосфатной связи','Растворяется в жирах','Содержит двойную связь'], ans:1 }
|
||
},
|
||
{
|
||
title:'Финальная реакция — пируват',
|
||
mol:'pyr',
|
||
desc:'Пируваткиназа переносит фосфат с ФЕП на АДФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> АТФ + пируват. Необратимая реакция. Итог: из 1 глюкозы 2 пирувата, 2 НАДН, +2 АТФ нетто.',
|
||
energy:[{label:'+2 АТФ', cls:'atp-prod'},{label:'2 пируват', cls:'co2'}],
|
||
quiz:{ q:'Каков нетто-выход АТФ на 1 молекулу глюкозы в гликолизе?', opts:['1','2','4','36'], ans:1 }
|
||
},
|
||
]
|
||
},
|
||
|
||
krebs: {
|
||
name: 'Цикл Кребса',
|
||
color: '#06b6d4',
|
||
colorRgb: '6,182,212',
|
||
desc: '8 реакций окисления ацетил-КоА. Происходит в матриксе митохондрий. Выход на 1 оборот: 3 НАДН, 1 ФАДН₂, 1 ГТФ, 2 СО₂.',
|
||
stats: [
|
||
{ label: '3 НАДН / оборот', cls: 'nadh' },
|
||
{ label: '2 CO₂', cls: 'co2' },
|
||
{ label: '1 ГТФ', cls: 'atp' },
|
||
],
|
||
legend: [
|
||
{ color: '#06b6d4', type: 'circle', label: 'Промежуточный метаболит' },
|
||
{ color: '#06b6d4', type: 'line', label: 'Реакция цикла' },
|
||
],
|
||
nodes: [
|
||
{ id:'acetcoa', label:'Ацетил-КоА', formula:'CH₃CO-SCoA',x:440, y:80, role:'substrate', desc:'Активированный ацетат. Образуется из пирувата (гликолиз), жирных кислот (β-окисление) и аминокислот.', props:['Входит в цикл'] },
|
||
{ id:'oaa', label:'ОАА', formula:'C₄H₄O₅', x:260, y:140, role:'key', desc:'Оксалоацетат — акцептор ацетил-КоА. Регенерируется в каждом обороте цикла. Ключевой анаплеротический метаболит.', props:['Акцептор'] },
|
||
{ id:'cit', label:'Цитрат', formula:'C₆H₈O₇', x:160, y:260, role:'inter', desc:'Цитрат — первый продукт цикла. Синтезируется цитратсинтазой из ацетил-КоА и ОАА.', props:[] },
|
||
{ id:'isocit', label:'Изоцитрат', formula:'C₆H₈O₇', x:120, y:390, role:'inter', desc:'Изоцитрат — изомер цитрата. Субстрат изоцитратдегидрогеназы — ключевого регуляторного фермента.', props:[] },
|
||
{ id:'akg', label:'α-КГ', formula:'C₅H₆O₅', x:160, y:520, role:'inter', desc:'α-кетоглутарат (α-КГ). Образуется при окислительном декарбоксилировании изоцитрата. Выделяется CO₂.', props:['−CO₂','+НАДН'] },
|
||
{ id:'succoa', label:'Сукцинил-КоА',formula:'C₅H₆O₃S', x:300, y:620, role:'inter', desc:'Сукцинил-КоА — высокоэнергетический тиоэфир. Образуется при окислительном декарбоксилировании α-КГ.', props:['+НАДН','+ГТФ','-CO₂'] },
|
||
{ id:'succ', label:'Сукцинат', formula:'C₄H₆O₄', x:480, y:620, role:'inter', desc:'Сукцинат. Окисляется сукцинатдегидрогеназой (СДГ) — единственным мембранным ферментом цикла.', props:['+ФАДН₂'] },
|
||
{ id:'fum', label:'Фумарат', formula:'C₄H₄O₄', x:620, y:520, role:'inter', desc:'Фумарат — транс-изомер. Образуется при окислении сукцината. Гидратируется фумаразой.', props:[] },
|
||
{ id:'mal', label:'Малат', formula:'C₄H₆O₅', x:660, y:390, role:'inter', desc:'Малат (яблочная кислота). Образуется при гидратации фумарата. Окисляется малатдегидрогеназой.', props:['+НАДН'] },
|
||
],
|
||
edges: [
|
||
{ from:'acetcoa',to:'cit', enzyme:'Цитратсинтаза', co:'+ОАА', curveX:0 },
|
||
{ from:'oaa', to:'cit', enzyme:'', curveX:0 },
|
||
{ from:'cit', to:'isocit', enzyme:'Аконитаза', curveX:0 },
|
||
{ from:'isocit', to:'akg', enzyme:'ИзоцитратДГ', co:'+НАДН,-CO₂', curveX:0 },
|
||
{ from:'akg', to:'succoa', enzyme:'α-КГДГ-комплекс', co:'+НАДН,-CO₂', curveX:0 },
|
||
{ from:'succoa', to:'succ', enzyme:'Сукцинил-КоА-синтетаза', co:'+ГТФ', curveX:0 },
|
||
{ from:'succ', to:'fum', enzyme:'Сукцинатдегидрогеназа', co:'+ФАДН₂', curveX:0 },
|
||
{ from:'fum', to:'mal', enzyme:'Фумараза', curveX:0 },
|
||
{ from:'mal', to:'oaa', enzyme:'МалатДГ', co:'+НАДН', curveX:0 },
|
||
],
|
||
steps: [
|
||
{ title:'Конденсация с ОАА', mol:'cit', desc:'Цитратсинтаза присоединяет ацетил-КоА (2C) к оксалоацетату (4C) <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> цитрат (6C). Это необратимая реакция, запускающая цикл.', energy:[], quiz:{q:'Сколько углеродов в цитрате?', opts:['2','4','6','8'], ans:2} },
|
||
{ title:'Изомеризация цитрата', mol:'isocit', desc:'Аконитаза через промежуточный цис-аконитат превращает цитрат в изоцитрат. Реакция обратима, равновесие сдвинуто в сторону цитрата.', energy:[], quiz:{q:'Какой фермент изомеризует цитрат?', opts:['Фумараза','Аконитаза','Малатдегидрогеназа','Цитратсинтаза'], ans:1} },
|
||
{ title:'Первое окислительное декарбоксилирование', mol:'akg', desc:'Изоцитратдегидрогеназа окисляет изоцитрат <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> α-кетоглутарат с выделением CO₂ и НАДН. Ключевой регуляторный шаг — активируется изоцитратом, ингибируется НАДН.', energy:[{label:'+НАДН',cls:'nadh'},{label:'-CO₂',cls:'co2'}], quiz:{q:'Сколько углеродов в α-кетоглутарате?', opts:['2','4','5','6'], ans:2} },
|
||
{ title:'Второе окислительное декарбоксилирование', mol:'succoa', desc:'α-кетоглутаратдегидрогеназный комплекс (аналог ПДК) окисляет α-КГ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> сукцинил-КоА. Выделяется ещё одна CO₂ и НАДН.', energy:[{label:'+НАДН',cls:'nadh'},{label:'-CO₂',cls:'co2'}], quiz:{q:'Чем структурно похож α-КГДК на пируватдегидрогеназный комплекс?', opts:['Использует ФАДН₂','Механизм окислительного декарбоксилирования с КоА','Находится в цитоплазме','Требует витамин К'], ans:1} },
|
||
{ title:'Субстратное фосфорилирование', mol:'succ', desc:'Сукцинил-КоА-синтетаза расщепляет тиоэфирную связь сукцинил-КоА, сопрягая это с синтезом ГТФ (или АТФ). Единственная реакция субстратного фосфорилирования в цикле.', energy:[{label:'+ГТФ',cls:'atp-prod'}], quiz:{q:'Что синтезируется при реакции сукцинил-КоА-синтетазы?', opts:['НАДН','ФАДН₂','ГТФ','CO₂'], ans:2} },
|
||
{ title:'Окисление сукцината', mol:'fum', desc:'Сукцинатдегидрогеназа (комплекс II дыхательной цепи) окисляет сукцинат <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> фумарат, восстанавливая ФАД <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ФАДН₂.', energy:[{label:'+ФАДН₂',cls:'fadh2'}], quiz:{q:'К какому комплексу дыхательной цепи относится СДГ?', opts:['Комплекс I','Комплекс II','Комплекс III','АТФ-синтаза'], ans:1} },
|
||
{ title:'Гидратация фумарата', mol:'mal', desc:'Фумараза присоединяет воду к фумарату <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> L-малат. Реакция стереоспецифична — образуется только L-изомер.', energy:[], quiz:{q:'Что присоединяется к фумарату в этой реакции?', opts:['CO₂','АТФ','H₂O','НАДН'], ans:2} },
|
||
{ title:'Регенерация ОАА', mol:'oaa', desc:'Малатдегидрогеназа окисляет малат <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> оксалоацетат с образованием НАДН. Регенерируется акцептор для следующего оборота цикла.', energy:[{label:'+НАДН',cls:'nadh'}], quiz:{q:'Сколько оборотов цикла Кребса нужно на 1 молекулу глюкозы?', opts:['1','2','4','10'], ans:1} },
|
||
]
|
||
},
|
||
|
||
oxidation: {
|
||
name: 'β-Окисление',
|
||
color: '#fb923c',
|
||
colorRgb: '251,146,60',
|
||
desc: 'Повторяющиеся циклы окисления жирных кислот в митохондриях. Каждый цикл отщепляет 2C в виде ацетил-КоА и выделяет 1 НАДН + 1 ФАДН₂.',
|
||
stats: [
|
||
{ label: '+НАДН / цикл', cls: 'nadh' },
|
||
{ label: '+ФАДН₂', cls: 'nadh' },
|
||
{ label: '+Ацетил-КоА', cls: 'atp' },
|
||
],
|
||
legend: [
|
||
{ color: '#fb923c', type: 'circle', label: 'Промежуточный продукт' },
|
||
{ color: '#fb923c', type: 'line', label: 'Реакция β-окисления' },
|
||
],
|
||
nodes: [
|
||
{ id:'fac', label:'Жирная к-та',formula:'R-COOH', x:400, y:60, role:'substrate', desc:'Свободная жирная кислота (напр. пальмитиновая C₁₆). Активируется в ацил-КоА перед входом в митохондрии.', props:[] },
|
||
{ id:'acylcoa', label:'Ацил-КоА', formula:'R-CO-SCoA', x:400, y:150, role:'key', desc:'Активированная жирная кислота. Образуется при участии ацил-КоА-синтетазы за счёт АТФ (<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>АМФ+PPi). Не проходит через мембрану — транспортируется как карнитиновый эфир.', props:['-АТФ (<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>АМФ)'] },
|
||
{ id:'enoylcoa',label:'Транс-еноил-КоА',formula:'R-CH=CH-CO-SCoA',x:400,y:250,role:'inter',desc:'Транс-Δ²-еноил-КоА. Образуется при ФАД-зависимом окислении ацил-КоА ацил-КоА-дегидрогеназой.', props:['+ФАДН₂'] },
|
||
{ id:'hydroxy', label:'L-β-гидрокси-КоА',formula:'R-CHOH-CH₂-CO-SCoA',x:400,y:345,role:'inter',desc:'L-β-гидроксиацил-КоА. Образуется при гидратации двойной связи еноил-КоА гидратазой.', props:[] },
|
||
{ id:'ketoacoa',label:'β-кето-КоА', formula:'R-CO-CH₂-CO-SCoA',x:400,y:440,role:'inter',desc:'β-кетоацил-КоА. Образуется при НАД⁺-зависимом окислении L-β-гидроксиацил-КоА.', props:['+НАДН'] },
|
||
{ id:'newacyl', label:'Ацил-КоА (−2C)',formula:'R\'—CO-SCoA', x:240, y:540, role:'inter', desc:'Укороченный на 2 углерода ацил-КоА. Возвращается на начало цикла β-окисления.', props:['Следующий цикл'] },
|
||
{ id:'acetcoa2',label:'Ацетил-КоА', formula:'CH₃CO-SCoA', x:560, y:540, role:'product', desc:'Ацетил-КоА — входит в цикл Кребса. Из пальмитиновой кислоты (C₁₆) образуется 8 ацетил-КоА за 7 циклов β-окисления.', props:['<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Цикл Кребса'] },
|
||
],
|
||
edges: [
|
||
{ from:'fac', to:'acylcoa', enzyme:'Ацил-КоА-синтетаза', co:'-АТФ', curveX:0 },
|
||
{ from:'acylcoa', to:'enoylcoa', enzyme:'Ацил-КоА-ДГ', co:'+ФАДН₂', curveX:0 },
|
||
{ from:'enoylcoa',to:'hydroxy', enzyme:'Еноил-КоА-гидратаза', curveX:0 },
|
||
{ from:'hydroxy', to:'ketoacoa', enzyme:'L-3-гидроксиацил-КоА-ДГ', co:'+НАДН', curveX:0 },
|
||
{ from:'ketoacoa',to:'newacyl', enzyme:'Тиолаза', curveX:0 },
|
||
{ from:'ketoacoa',to:'acetcoa2', enzyme:'Тиолаза', curveX:0 },
|
||
{ from:'newacyl', to:'acylcoa', enzyme:'Повтор цикла', curveX:-60 },
|
||
],
|
||
steps: [
|
||
{ title:'Активация жирной кислоты', mol:'acylcoa', desc:'Ацил-КоА-синтетаза присоединяет КоА к жирной кислоте, образуя ацил-КоА. Расходуется АТФ (<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>АМФ+PPi, что эквивалентно 2 АТФ). Это происходит в цитоплазме.', energy:[{label:'-2 АТФ',cls:'atp-used'}], quiz:{q:'Где происходит активация жирной кислоты в ацил-КоА?', opts:['В митохондриях','В ядре','В цитоплазме','В рибосомах'], ans:2} },
|
||
{ title:'ФАД-зависимое окисление', mol:'enoylcoa', desc:'Ацил-КоА-дегидрогеназа окисляет ацил-КоА, вводя двойную связь между α и β углеродами <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> транс-Δ²-еноил-КоА. ФАД восстанавливается до ФАДН₂.', energy:[{label:'+ФАДН₂',cls:'fadh2'}], quiz:{q:'Какой кофактор восстанавливается в первой реакции β-окисления?', opts:['НАД⁺','ФАД','ГТФ','КоА'], ans:1} },
|
||
{ title:'Гидратация двойной связи', mol:'hydroxy', desc:'Еноил-КоА-гидратаза присоединяет воду по двойной связи <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> L-β-гидроксиацил-КоА. Реакция стереоспецифична.', energy:[], quiz:{q:'Что присоединяется в реакции гидратации еноил-КоА?', opts:['CO₂','O₂','H₂O','НАД⁺'], ans:2} },
|
||
{ title:'НАД⁺-зависимое окисление', mol:'ketoacoa', desc:'L-3-гидроксиацил-КоА-дегидрогеназа окисляет гидроксильную группу <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> кетогруппу, восстанавливая НАД⁺ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> НАДН.', energy:[{label:'+НАДН',cls:'nadh'}], quiz:{q:'Какая группа окисляется в этой реакции?', opts:['Карбоксильная','Аминогруппа','Гидроксильная','Метильная'], ans:2} },
|
||
{ title:'Тиолитическое расщепление', mol:'acetcoa2', desc:'Тиолаза расщепляет β-кетоацил-КоА присоединением КоА <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ацетил-КоА (2C) + укороченный ацил-КоА. Цикл повторяется.', energy:[{label:'+Ацетил-КоА',cls:'atp-prod'}], quiz:{q:'Сколько ацетил-КоА образуется из пальмитиновой кислоты (C16)?', opts:['4','6','7','8'], ans:3} },
|
||
]
|
||
},
|
||
|
||
synthesis: {
|
||
name: 'Синтез белка',
|
||
color: '#a78bfa',
|
||
colorRgb: '167,139,250',
|
||
desc: 'Трансляция — считывание мРНК рибосомой и полимеризация аминокислот в полипептидную цепь.',
|
||
stats: [
|
||
{ label: '~2 ГТФ / аминокислота', cls: 'atp' },
|
||
{ label: 'мРНК <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> белок', cls: 'nadh' },
|
||
],
|
||
legend: [
|
||
{ color: '#a78bfa', type: 'circle', label: 'Участник трансляции' },
|
||
{ color: '#a78bfa', type: 'line', label: 'Этап синтеза' },
|
||
],
|
||
nodes: [
|
||
{ id:'mrna', label:'мРНК', formula:'5′-AUG…-3′', x:400, y:60, role:'substrate', desc:'Матричная РНК — несёт генетическую информацию от ДНК к рибосоме в виде кодонов (триплетов нуклеотидов).', props:['Матрица'] },
|
||
{ id:'ribosome',label:'Рибосома', formula:'60S+40S', x:400, y:160, role:'key', desc:'Эукариотическая рибосома (80S). Состоит из малой (40S) и большой (60S) субъединиц. Имеет 3 сайта: A (аминоацильный), P (пептидильный), E (выход).', props:['A-P-E сайты'] },
|
||
{ id:'trna', label:'аминоацил-тРНК', formula:'aa-tRNA', x:230, y:260, role:'inter', desc:'тРНК с присоединённой аминокислотой. Распознаёт кодон мРНК через антикодон. Доставляется в A-сайт в комплексе с EF-Tu·ГТФ.', props:['-2 ГТФ'] },
|
||
{ id:'peptide', label:'Растущая цепь', formula:'...aa-aa-aa', x:560, y:260, role:'inter', desc:'Нарастающая полипептидная цепь в P-сайте. Пептидилтрансфераза (23S rRNA) катализирует образование пептидной связи.', props:['P-сайт'] },
|
||
{ id:'peptbond',label:'Пептидная связь',formula:'—CO—NH—', x:400, y:360, role:'inter', desc:'Образование пептидной связи катализируется рибозимом (23S rRNA) — пептидилтрансферазой. Выделяется тРНК из P-сайта.', props:[] },
|
||
{ id:'translo', label:'Транслокация', formula:'EF-G·ГТФ', x:400, y:460, role:'inter', desc:'Фактор EF-G (с ГТФ) сдвигает рибосому на 1 кодон (3 нт) в направлении 5′<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>3′. Освобождается Е-сайт. Расходуется ГТФ.', props:['-1 ГТФ'] },
|
||
{ id:'protein', label:'Белок', formula:'[полипептид]', x:400, y:560, role:'product', desc:'Готовый полипептид. Освобождается при встрече со стоп-кодоном (UAA, UAG, UGA) при участии факторов высвобождения RF1/RF2.', props:['Готовый продукт'] },
|
||
],
|
||
edges: [
|
||
{ from:'mrna', to:'ribosome', enzyme:'Инициация (eIF)', curveX:0 },
|
||
{ from:'ribosome',to:'trna', enzyme:'Декодирование', curveX:0 },
|
||
{ from:'trna', to:'peptbond', enzyme:'Пептидилтрансфераза', curveX:0 },
|
||
{ from:'peptide', to:'peptbond', enzyme:'', curveX:0 },
|
||
{ from:'peptbond',to:'translo', enzyme:'EF-G·ГТФ', curveX:0 },
|
||
{ from:'translo', to:'protein', enzyme:'Терминация (RF)', curveX:0 },
|
||
{ from:'translo', to:'ribosome', enzyme:'Следующий кодон', curveX:-70 },
|
||
],
|
||
steps: [
|
||
{ title:'Инициация', mol:'ribosome', desc:'Малая субъединица рибосомы распознаёт 5′-кэп мРНК при помощи факторов инициации (eIF4E/4G). Инициаторная Met-тРНК занимает P-сайт. Присоединяется большая субъединица.', energy:[{label:'-3 ГТФ',cls:'atp-used'}], quiz:{q:'Какой сайт занимает инициаторная Met-тРНК?', opts:['A-сайт','P-сайт','E-сайт','Все три'], ans:1} },
|
||
{ title:'Элонгация — доставка аа-тРНК', mol:'trna', desc:'EF-Tu·ГТФ доставляет аминоацил-тРНК в A-сайт. При правильном спаривании кодон–антикодон ГТФ гидролизуется, EF-Tu·ГДФ уходит.', energy:[{label:'-1 ГТФ',cls:'atp-used'}], quiz:{q:'Какой фактор доставляет аа-тРНК в А-сайт?', opts:['EF-G','EF-Tu','eIF2','RF1'], ans:1} },
|
||
{ title:'Пептидная связь', mol:'peptbond', desc:'Пептидилтрансфераза переносит пептидильную группу с P-сайта на аминогруппу в A-сайте, образуя пептидную связь. Энергия — из гидролиза аминоацильной связи тРНК.', energy:[], quiz:{q:'Что катализирует образование пептидной связи?', opts:['Белковый фермент','23S rRNA (рибозим)','ДНК-полимераза','АТФ-синтаза'], ans:1} },
|
||
{ title:'Транслокация', mol:'translo', desc:'EF-G·ГТФ сдвигает рибосому на 3 нуклеотида по мРНК. Цепь с тРНК перемещается из A <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> P, пустая тРНК из P <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> E и уходит. Расходуется ГТФ.', energy:[{label:'-1 ГТФ',cls:'atp-used'}], quiz:{q:'На сколько нуклеотидов сдвигается рибосома при транслокации?', opts:['1','2','3','4'], ans:2} },
|
||
{ title:'Терминация и высвобождение', mol:'protein', desc:'Стоп-кодон (UAA/UAG/UGA) распознаётся факторами высвобождения RF1/RF2. Пептидилтрансфераза гидролизует связь пептид-тРНК <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> белок освобождается. Рибосома диссоциирует.', energy:[], quiz:{q:'Сколько стоп-кодонов существует?', opts:['1','2','3','4'], ans:2} },
|
||
]
|
||
}
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// STATE
|
||
// ═══════════════════════════════════════════════════════
|
||
let currentPath = 'glycolysis';
|
||
let animOn = true;
|
||
let activeNode = null;
|
||
let learnStep = 0;
|
||
let learnMode = false;
|
||
let particles = [];
|
||
let animFrame = null;
|
||
let svgTranslate = { x: 0, y: 0 };
|
||
let svgScale = 1;
|
||
let dragState = null;
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// PATH SELECTION & RENDER
|
||
// ═══════════════════════════════════════════════════════
|
||
function selectPath(id) {
|
||
currentPath = id;
|
||
document.querySelectorAll('.path-chip').forEach(c => c.classList.toggle('active', c.dataset.path === id));
|
||
stopLearnMode();
|
||
renderPath();
|
||
renderPathInfo();
|
||
}
|
||
|
||
function renderPath() {
|
||
const pd = PATHWAYS[currentPath];
|
||
const area = document.getElementById('svg-area');
|
||
const W = area.clientWidth || 800;
|
||
const H = area.clientHeight || 600;
|
||
|
||
// auto-fit: find bounding box of nodes
|
||
const xs = pd.nodes.map(n => n.x), ys = pd.nodes.map(n => n.y);
|
||
const minX = Math.min(...xs) - 60, minY = Math.min(...ys) - 60;
|
||
const maxX = Math.max(...xs) + 60, maxY = Math.max(...ys) + 60;
|
||
const scaleX = W / (maxX - minX), scaleY = H / (maxY - minY);
|
||
svgScale = Math.min(scaleX, scaleY, 1.2) * 0.9;
|
||
svgTranslate.x = (W - (maxX - minX) * svgScale) / 2 - minX * svgScale;
|
||
svgTranslate.y = (H - (maxY - minY) * svgScale) / 2 - minY * svgScale;
|
||
applyTransform();
|
||
|
||
renderEdges(pd);
|
||
renderNodes(pd);
|
||
renderStats(pd);
|
||
renderLegend(pd);
|
||
|
||
stopParticles();
|
||
if (animOn) startParticles(pd);
|
||
}
|
||
|
||
function applyTransform() {
|
||
const g = document.getElementById('svg-root');
|
||
g.setAttribute('transform', `translate(${svgTranslate.x},${svgTranslate.y}) scale(${svgScale})`);
|
||
}
|
||
|
||
function renderEdges(pd) {
|
||
const layer = document.getElementById('edges-layer');
|
||
layer.innerHTML = '';
|
||
const color = pd.color;
|
||
const marker = `url(#arrowhead)`;
|
||
|
||
for (const e of pd.edges) {
|
||
const fn = pd.nodes.find(n => n.id === e.from);
|
||
const tn = pd.nodes.find(n => n.id === e.to);
|
||
if (!fn || !tn) continue;
|
||
|
||
const x1 = fn.x, y1 = fn.y, x2 = tn.x, y2 = tn.y;
|
||
const dx = x2 - x1, dy = y2 - y1;
|
||
const len = Math.hypot(dx, dy) || 1;
|
||
// shorten to edge of node circle (r=28)
|
||
const R = 28;
|
||
const sx = x1 + dx/len * R, sy = y1 + dy/len * R;
|
||
const ex = x2 - dx/len * R, ey = y2 - dy/len * R;
|
||
|
||
let pathD;
|
||
if (e.curveX && e.curveX !== 0) {
|
||
// quadratic curve
|
||
const mx = (x1+x2)/2 + e.curveX, my = (y1+y2)/2;
|
||
pathD = `M${sx},${sy} Q${mx},${my} ${ex},${ey}`;
|
||
} else {
|
||
pathD = `M${sx},${sy} L${ex},${ey}`;
|
||
}
|
||
|
||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||
path.setAttribute('d', pathD);
|
||
path.setAttribute('stroke', `rgba(${pd.colorRgb},0.45)`);
|
||
path.setAttribute('stroke-width', '1.8');
|
||
path.setAttribute('fill', 'none');
|
||
path.setAttribute('marker-end', marker);
|
||
path.setAttribute('data-edge-id', `${e.from}-${e.to}`);
|
||
layer.appendChild(path);
|
||
|
||
// enzyme label
|
||
if (e.enzyme) {
|
||
const midT = 0.5;
|
||
let lx, ly;
|
||
if (e.curveX) {
|
||
const mx = (x1+x2)/2 + e.curveX, my = (y1+y2)/2;
|
||
lx = (1-midT)*(1-midT)*sx + 2*(1-midT)*midT*mx + midT*midT*ex;
|
||
ly = (1-midT)*(1-midT)*sy + 2*(1-midT)*midT*my + midT*midT*ey - 12;
|
||
} else {
|
||
lx = (sx+ex)/2 + (-dy/len)*12;
|
||
ly = (sy+ey)/2 + (dx/len)*12;
|
||
}
|
||
const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||
txt.setAttribute('x', lx);
|
||
txt.setAttribute('y', ly);
|
||
txt.setAttribute('class', 'edge-enzyme');
|
||
txt.textContent = e.enzyme;
|
||
layer.appendChild(txt);
|
||
}
|
||
|
||
// co-factor label
|
||
if (e.co) {
|
||
const nx = -dy/len, ny = dx/len;
|
||
const lx = (sx+ex)/2 - nx*13, ly = (sy+ey)/2 - ny*13;
|
||
const txt2 = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||
txt2.setAttribute('x', lx);
|
||
txt2.setAttribute('y', ly);
|
||
txt2.setAttribute('class', 'edge-label');
|
||
txt2.setAttribute('fill', `rgba(${pd.colorRgb},0.7)`);
|
||
txt2.textContent = e.co;
|
||
layer.appendChild(txt2);
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderNodes(pd) {
|
||
const layer = document.getElementById('nodes-layer');
|
||
layer.innerHTML = '';
|
||
|
||
const roleColor = {
|
||
substrate: { fill: '#1a2e20', stroke: '#34d399' },
|
||
key: { fill: '#1e1a2e', stroke: '#a78bfa' },
|
||
inter: { fill: '#111128', stroke: `rgba(${pd.colorRgb},0.7)` },
|
||
product: { fill: '#1a2a20', stroke: '#4ade80' },
|
||
};
|
||
|
||
for (const n of pd.nodes) {
|
||
const rc = roleColor[n.role] || roleColor.inter;
|
||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||
g.setAttribute('class', 'mol-node' + (activeNode === n.id ? ' active' : ''));
|
||
g.setAttribute('data-node-id', n.id);
|
||
g.setAttribute('transform', `translate(${n.x},${n.y})`);
|
||
g.style.cursor = 'pointer';
|
||
g.addEventListener('click', () => clickNode(n.id));
|
||
|
||
// Outer glow circle
|
||
const glow = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||
glow.setAttribute('r', '34');
|
||
glow.setAttribute('fill', 'none');
|
||
glow.setAttribute('stroke', rc.stroke);
|
||
glow.setAttribute('stroke-width', '0.5');
|
||
glow.setAttribute('opacity', '0.2');
|
||
g.appendChild(glow);
|
||
|
||
// Main circle
|
||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||
circle.setAttribute('class', 'node-circle');
|
||
circle.setAttribute('r', n.role === 'key' ? '30' : '27');
|
||
circle.setAttribute('fill', rc.fill);
|
||
circle.setAttribute('stroke', rc.stroke);
|
||
circle.setAttribute('stroke-width', activeNode === n.id ? '2.5' : '1.8');
|
||
g.appendChild(circle);
|
||
|
||
// Label
|
||
const lines = n.label.split(' ');
|
||
if (lines.length === 1) {
|
||
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||
t.setAttribute('class', 'node-label');
|
||
t.setAttribute('y', '0');
|
||
t.textContent = n.label;
|
||
g.appendChild(t);
|
||
// formula
|
||
const f = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||
f.setAttribute('class', 'node-formula');
|
||
f.setAttribute('y', '13');
|
||
f.textContent = n.formula;
|
||
g.appendChild(f);
|
||
} else {
|
||
lines.forEach((ln, i) => {
|
||
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||
t.setAttribute('class', 'node-label');
|
||
t.setAttribute('y', (i - (lines.length-1)/2) * 13 + '');
|
||
t.setAttribute('font-size', '9.5');
|
||
t.textContent = ln;
|
||
g.appendChild(t);
|
||
});
|
||
}
|
||
|
||
layer.appendChild(g);
|
||
}
|
||
}
|
||
|
||
function renderStats(pd) {
|
||
const el = document.getElementById('path-stats');
|
||
el.innerHTML = pd.stats.map(s => `<div class="pstat ${s.cls}">${s.label}</div>`).join('');
|
||
}
|
||
|
||
function renderLegend(pd) {
|
||
const el = document.getElementById('legend-items');
|
||
el.innerHTML = pd.legend.map(l => {
|
||
if (l.type === 'circle') return `<div class="legend-item"><div class="legend-dot" style="background:${l.color}"></div>${l.label}</div>`;
|
||
if (l.type === 'circle-sm') return `<div class="legend-item"><div class="legend-dot" style="background:${l.color};width:5px;height:5px;opacity:.6"></div>${l.label}</div>`;
|
||
return `<div class="legend-item"><div class="legend-line" style="background:${l.color}"></div>${l.label}</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderPathInfo() {
|
||
const pd = PATHWAYS[currentPath];
|
||
document.getElementById('path-info-content').innerHTML = `
|
||
<div style="font-family:'Unbounded',sans-serif;font-size:.85rem;font-weight:800;color:#e0e0f0;margin-bottom:8px">${pd.name}</div>
|
||
<div style="font-family:'Manrope',sans-serif;font-size:.8rem;color:#aaa;line-height:1.6;margin-bottom:14px">${pd.desc}</div>
|
||
<div style="font-family:'Manrope',sans-serif;font-size:.72rem;font-weight:700;color:#444;text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px">Выход энергии</div>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px">
|
||
${pd.stats.map(s=>`<span class="pstat ${s.cls}" style="position:static;font-size:.75rem">${s.label}</span>`).join('')}
|
||
</div>
|
||
<div style="font-family:'Manrope',sans-serif;font-size:.72rem;font-weight:700;color:#444;text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px">Ключевые молекулы (${pd.nodes.length})</div>
|
||
${pd.nodes.map(n=>`
|
||
<div style="display:flex;gap:8px;align-items:center;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.04);cursor:pointer" onclick="clickNode('${n.id}');switchTab('mol')">
|
||
<div style="width:8px;height:8px;border-radius:50%;background:${pd.color};flex-shrink:0"></div>
|
||
<div>
|
||
<div style="font-family:'Manrope',sans-serif;font-size:.78rem;font-weight:700;color:#ccc">${n.label}</div>
|
||
<div style="font-family:'Manrope',sans-serif;font-size:.72rem;color:#555">${n.formula}</div>
|
||
</div>
|
||
</div>`).join('')}
|
||
`;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// PARTICLE ANIMATION
|
||
// ═══════════════════════════════════════════════════════
|
||
function startParticles(pd) {
|
||
const layer = document.getElementById('particles-layer');
|
||
layer.innerHTML = '';
|
||
particles = [];
|
||
|
||
for (const e of pd.edges) {
|
||
const fn = pd.nodes.find(n => n.id === e.from);
|
||
const tn = pd.nodes.find(n => n.id === e.to);
|
||
if (!fn || !tn) continue;
|
||
// stagger start
|
||
const delay = Math.random() * 2000;
|
||
setTimeout(() => spawnParticle(pd, e, fn, tn), delay);
|
||
}
|
||
}
|
||
|
||
function spawnParticle(pd, edge, fn, tn) {
|
||
if (!animOn) return;
|
||
const layer = document.getElementById('particles-layer');
|
||
|
||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||
circle.setAttribute('r', '4');
|
||
circle.setAttribute('fill', pd.color);
|
||
circle.setAttribute('opacity', '0.9');
|
||
circle.setAttribute('class', 'flow-particle');
|
||
layer.appendChild(circle);
|
||
|
||
const duration = 1400 + Math.random() * 600;
|
||
const start = performance.now();
|
||
|
||
const R = 28;
|
||
const dx0 = tn.x - fn.x, dy0 = tn.y - fn.y;
|
||
const len0 = Math.hypot(dx0, dy0) || 1;
|
||
const sx = fn.x + dx0/len0 * R, sy = fn.y + dy0/len0 * R;
|
||
const ex = tn.x - dx0/len0 * R, ey = tn.y - dy0/len0 * R;
|
||
|
||
function frame(now) {
|
||
if (!animOn) { circle.remove(); return; }
|
||
const t = Math.min((now - start) / duration, 1);
|
||
const ease = t < 0.5 ? 2*t*t : -1+(4-2*t)*t;
|
||
|
||
let px, py;
|
||
if (edge.curveX) {
|
||
const mx = (fn.x+tn.x)/2 + edge.curveX, my = (fn.y+tn.y)/2;
|
||
px = (1-ease)*(1-ease)*sx + 2*(1-ease)*ease*mx + ease*ease*ex;
|
||
py = (1-ease)*(1-ease)*sy + 2*(1-ease)*ease*my + ease*ease*ey;
|
||
} else {
|
||
px = sx + (ex-sx)*ease;
|
||
py = sy + (ey-sy)*ease;
|
||
}
|
||
|
||
circle.setAttribute('cx', px);
|
||
circle.setAttribute('cy', py);
|
||
circle.setAttribute('opacity', t < 0.1 ? t*9 : t > 0.85 ? (1-t)/0.15 : 0.9);
|
||
|
||
if (t < 1) {
|
||
requestAnimationFrame(frame);
|
||
} else {
|
||
circle.remove();
|
||
// respawn after pause
|
||
if (animOn) setTimeout(() => spawnParticle(pd, edge, fn, tn), 800 + Math.random() * 1200);
|
||
}
|
||
}
|
||
requestAnimationFrame(frame);
|
||
}
|
||
|
||
function stopParticles() {
|
||
animOn = false;
|
||
document.getElementById('particles-layer').innerHTML = '';
|
||
}
|
||
|
||
function toggleAnimation() {
|
||
animOn = !animOn;
|
||
const btn = document.getElementById('anim-toggle');
|
||
btn.classList.toggle('on', animOn);
|
||
btn.innerHTML = `<i data-lucide="${animOn ? 'play-circle' : 'pause-circle'}" style="width:14px;height:14px"></i> Анимація`;
|
||
btn.innerHTML = `<i data-lucide="${animOn ? 'play-circle' : 'pause-circle'}" style="width:14px;height:14px"></i> Анимация`;
|
||
if (window.lucide) lucide.createIcons();
|
||
if (animOn) startParticles(PATHWAYS[currentPath]);
|
||
else document.getElementById('particles-layer').innerHTML = '';
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// NODE CLICK <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> SIDE PANEL
|
||
// ═══════════════════════════════════════════════════════
|
||
function clickNode(id) {
|
||
activeNode = id;
|
||
// re-render nodes to update active state
|
||
renderNodes(PATHWAYS[currentPath]);
|
||
|
||
const pd = PATHWAYS[currentPath];
|
||
const n = pd.nodes.find(x => x.id === id);
|
||
if (!n) return;
|
||
|
||
document.getElementById('mol-empty').style.display = 'none';
|
||
document.getElementById('mol-card').classList.add('show');
|
||
document.getElementById('mc-name').textContent = n.label;
|
||
document.getElementById('mc-formula').textContent = n.formula;
|
||
document.getElementById('mc-desc').textContent = n.desc;
|
||
document.getElementById('mc-props').innerHTML = (n.props||[]).map(p => {
|
||
let cls = 'key';
|
||
if (p.includes('АТФ') || p.includes('ГТФ')) cls = p.startsWith('+') ? 'atp' : 'atp';
|
||
if (p.includes('НАДН') || p.includes('ФАДН')) cls = 'nadh';
|
||
if (p.includes('CO₂')) cls = 'co2';
|
||
return `<span class="mol-prop ${cls}">${p}</span>`;
|
||
}).join('');
|
||
document.getElementById('mc-link').href = '/biochem-library';
|
||
document.getElementById('mc-link').innerHTML = 'Найти в библиотеке <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>';
|
||
|
||
switchTab('mol');
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// LEARN MODE
|
||
// ═══════════════════════════════════════════════════════
|
||
function startLearn() {
|
||
learnMode = true;
|
||
learnStep = 0;
|
||
switchTab('learn');
|
||
document.getElementById('learn-inactive').style.display = 'none';
|
||
renderLearnStep();
|
||
}
|
||
|
||
function stopLearnMode() {
|
||
learnMode = false;
|
||
document.getElementById('learn-inactive').style.display = 'block';
|
||
document.getElementById('learn-active').innerHTML = '';
|
||
}
|
||
|
||
let quizAnswered = false;
|
||
|
||
function renderLearnStep() {
|
||
const pd = PATHWAYS[currentPath];
|
||
const steps = pd.steps;
|
||
if (learnStep >= steps.length) { renderLearnComplete(); return; }
|
||
|
||
const s = steps[learnStep];
|
||
const pct = Math.round((learnStep / steps.length) * 100);
|
||
quizAnswered = false;
|
||
|
||
// highlight node
|
||
activeNode = s.mol;
|
||
renderNodes(pd);
|
||
// scroll to node (pan)
|
||
const n = pd.nodes.find(x => x.id === s.mol);
|
||
if (n) {
|
||
const area = document.getElementById('svg-area');
|
||
const W = area.clientWidth, H = area.clientHeight;
|
||
svgTranslate.x = W/2 - n.x * svgScale;
|
||
svgTranslate.y = H/2 - n.y * svgScale;
|
||
applyTransform();
|
||
}
|
||
|
||
const html = `
|
||
<div class="learn-progress-bar"><div class="learn-progress-fill" style="width:${pct}%"></div></div>
|
||
<div class="step-counter">Шаг ${learnStep+1} из ${steps.length}</div>
|
||
<div class="step-title-row">
|
||
<span class="step-num">${learnStep+1}</span>
|
||
<span class="step-title">${s.title}</span>
|
||
</div>
|
||
<div class="step-desc">${s.desc}</div>
|
||
${s.energy.length ? `<div class="step-energy">${s.energy.map(e=>`<span class="energy-badge ${e.cls}">${e.label}</span>`).join('')}</div>` : ''}
|
||
<div class="mini-quiz">
|
||
<div class="quiz-q">${s.quiz.q}</div>
|
||
<div class="quiz-opts">
|
||
${s.quiz.opts.map((o,i)=>`<button class="quiz-opt" id="qopt-${i}" onclick="answerQuiz(${i},${s.quiz.ans})">${o}</button>`).join('')}
|
||
</div>
|
||
<div id="quiz-fb" style="display:none" class="quiz-feedback"></div>
|
||
</div>
|
||
<div class="step-nav">
|
||
<button class="step-btn prev" onclick="stepNav(-1)" ${learnStep===0?'disabled':''}><svg class="ic" viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> Назад</button>
|
||
<button class="step-btn next" id="step-next-btn" onclick="stepNav(1)" disabled>
|
||
${learnStep===steps.length-1 ? 'Завершить <svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>' : 'Далее <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>'}
|
||
</button>
|
||
</div>
|
||
`;
|
||
document.getElementById('learn-active').innerHTML = html;
|
||
}
|
||
|
||
function answerQuiz(chosen, correct) {
|
||
if (quizAnswered) return;
|
||
quizAnswered = true;
|
||
const ok = chosen === correct;
|
||
const fb = document.getElementById('quiz-fb');
|
||
fb.style.display = '';
|
||
fb.className = 'quiz-feedback ' + (ok ? 'ok' : 'err');
|
||
fb.innerHTML = ok ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg> Верно!' : `<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> Неверно. Правильный ответ: "${PATHWAYS[currentPath].steps[learnStep].quiz.opts[correct]}"`;
|
||
|
||
document.querySelectorAll('.quiz-opt').forEach((el, i) => {
|
||
el.disabled = true;
|
||
if (i === correct) el.classList.add('correct');
|
||
else if (i === chosen && !ok) el.classList.add('wrong');
|
||
});
|
||
document.getElementById('step-next-btn').disabled = false;
|
||
}
|
||
|
||
function stepNav(dir) {
|
||
learnStep += dir;
|
||
if (learnStep < 0) learnStep = 0;
|
||
renderLearnStep();
|
||
}
|
||
|
||
function renderLearnComplete() {
|
||
activeNode = null;
|
||
renderNodes(PATHWAYS[currentPath]);
|
||
document.getElementById('learn-active').innerHTML = `
|
||
<div class="learn-complete">
|
||
<div class="complete-icon"><svg class="ic" viewBox="0 0 24 24"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg></div>
|
||
<div class="complete-title">${PATHWAYS[currentPath].name} пройден!</div>
|
||
<div class="complete-text" style="margin-bottom:16px">Вы изучили все ${PATHWAYS[currentPath].steps.length} шагов пути.</div>
|
||
<button class="learn-btn" style="display:inline-flex;margin:0 auto" onclick="startLearn()">Повторить</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// TABS
|
||
// ═══════════════════════════════════════════════════════
|
||
function switchTab(tab) {
|
||
['mol','learn','info'].forEach(t => {
|
||
document.getElementById('tab-'+t).classList.toggle('active', t===tab);
|
||
document.getElementById('panel-'+t).style.display = t===tab ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// PAN / ZOOM
|
||
// ═══════════════════════════════════════════════════════
|
||
function zoom(factor) {
|
||
svgScale *= factor;
|
||
applyTransform();
|
||
}
|
||
function resetView() {
|
||
renderPath();
|
||
}
|
||
|
||
const svgArea = document.getElementById('svg-area');
|
||
svgArea.addEventListener('mousedown', e => {
|
||
if (e.target.closest('.mol-node')) return;
|
||
dragState = { sx: e.clientX, sy: e.clientY, tx: svgTranslate.x, ty: svgTranslate.y };
|
||
});
|
||
window.addEventListener('mousemove', e => {
|
||
if (!dragState) return;
|
||
svgTranslate.x = dragState.tx + (e.clientX - dragState.sx);
|
||
svgTranslate.y = dragState.ty + (e.clientY - dragState.sy);
|
||
applyTransform();
|
||
});
|
||
window.addEventListener('mouseup', () => { dragState = null; });
|
||
svgArea.addEventListener('wheel', e => {
|
||
e.preventDefault();
|
||
const delta = e.deltaY > 0 ? 0.88 : 1.14;
|
||
const rect = svgArea.getBoundingClientRect();
|
||
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
|
||
svgTranslate.x = mx - (mx - svgTranslate.x) * delta;
|
||
svgTranslate.y = my - (my - svgTranslate.y) * delta;
|
||
svgScale *= delta;
|
||
applyTransform();
|
||
}, { passive: false });
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// BOOT
|
||
// ═══════════════════════════════════════════════════════
|
||
async function init() {
|
||
try {
|
||
const user = await LS.getMe();
|
||
const initials = (user?.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||
document.getElementById('nav-avatar').textContent = initials;
|
||
const nav2 = document.getElementById('nav-avatar2');
|
||
if (nav2) nav2.textContent = initials;
|
||
if (user?.role === 'admin') document.getElementById('btn-admin').style.display = '';
|
||
LS.applyRoleSidebar(user);
|
||
LS.showBoardIfAllowed();
|
||
if (user?.role !== 'student') {
|
||
document.getElementById('btn-classes').style.display = '';
|
||
}
|
||
document.getElementById('nav-user').textContent = user?.name || '—';
|
||
} catch(e) {}
|
||
|
||
// wait for layout
|
||
await new Promise(r => setTimeout(r, 60));
|
||
renderPath();
|
||
renderPathInfo();
|
||
if (window.lucide) lucide.createIcons();
|
||
LS.notif?.init();
|
||
LS.hideDisabledFeatures?.();
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
<script src="/js/notifications.js"></script>
|
||
<script src="/js/mobile.js"></script>
|
||
</body>
|
||
</html>
|