From 000e42f9b399e62246948530406621123c2e4fdb Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 24 Jun 2026 18:56:31 +0300 Subject: [PATCH] =?UTF-8?q?feat(lab-graph):=20KaTeX-=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D1=83=D0=BB=D1=8B=20+=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20=D0=B2=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=BA=D0=B0=D0=BA=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=BE=D1=80=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D1=83=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit «График функции»: - примеры (чипы) и живой предпросмотр каждой функции рендерятся в KaTeX (data-tex на чипах, _initGraphPanel рендерит при открытии) - предпросмотр теперь всегда виден, крупный и в цвет функции; пустое поле показывает плейсхолдер-формулу приглушённо - НОВОЕ: keypad вставки структур (x², xⁿ, √, a/b, |x|, π, sin/cos/tg/ln/eˣ, ()) — клик вставляет в активное поле по каретке (как редактор формул в PowerPoint) - graphInsert(token) с маркером каретки |; активное поле отслеживается по focus Только фронт. Проверено: node --check, логика вставки 5/5. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/css/lab.css | 24 +++++++++++++---- frontend/js/labs/graph.js | 46 +++++++++++++++++++++++++++++--- frontend/labs-bodies.html | 56 +++++++++++++++++++++++++-------------- 3 files changed, 97 insertions(+), 29 deletions(-) diff --git a/frontend/css/lab.css b/frontend/css/lab.css index dfebcbb..c0e94e7 100644 --- a/frontend/css/lab.css +++ b/frontend/css/lab.css @@ -188,15 +188,29 @@ } .fn-input::placeholder { color: var(--text-3); font-weight: 500; } - /* KaTeX live preview */ + /* KaTeX live preview — крупная «отрисованная» формула под вводом */ .fn-preview { - min-height: 20px; padding: 3px 4px 3px 36px; + min-height: 22px; padding: 5px 8px 5px 36px; margin-top: 3px; font-size: 0.82rem; line-height: 1.5; - color: rgba(255,255,255,.65); - overflow: hidden; display: none; + overflow-x: auto; overflow-y: hidden; display: none; } .fn-preview.has-content { display: block; } - .fn-preview .katex { color: rgba(255,255,255,.8); font-size: 1em; } + .fn-preview .katex { color: var(--fn-color, rgba(255,255,255,.85)); font-size: 1.18em; } + .fn-preview.is-ph { opacity: .4; } /* плейсхолдер-формула, пока поле пустое */ + + /* keypad вставки структур (как редактор формул) */ + .gp-keypad { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; margin-bottom: 6px; } + .kp-btn { + display: flex; align-items: center; justify-content: center; min-height: 32px; + padding: 5px 2px; border-radius: 9px; border: 1.5px solid var(--border-h); + background: rgba(255,255,255,.02); color: var(--text-2); + font-family: 'Manrope', monospace; font-size: 0.78rem; font-weight: 700; + cursor: pointer; transition: all .14s; + } + .kp-btn:hover { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,.08); transform: translateY(-1px); } + .kp-btn:active { transform: translateY(0); } + .kp-btn .katex { font-size: 1em; } + .preset-btn .katex { font-size: 0.96em; } .fn-err { font-size: 0.68rem; color: var(--pink); font-weight: 600; padding: 2px 0 0 22px; display: none; diff --git a/frontend/js/labs/graph.js b/frontend/js/labs/graph.js index 185aa9d..b921567 100644 --- a/frontend/js/labs/graph.js +++ b/frontend/js/labs/graph.js @@ -507,6 +507,7 @@ class GraphSim { document.getElementById('sim-topbar-title').textContent = 'График функции'; _simShow('sim-graph'); _simShow('ctrl-graph'); + _initGraphPanel(); _registerSimState('graph', () => ({ @@ -524,6 +525,7 @@ class GraphSim { if (_embedMode) _startStateEmit('graph'); requestAnimationFrame(() => requestAnimationFrame(() => { + _initGraphPanel(); // KaTeX к этому моменту точно загружен if (!gSim) { gSim = new GraphSim(document.getElementById('graph-canvas')); gSim.onHover = updateInfoBar; @@ -575,16 +577,52 @@ class GraphSim { function renderPreview(idx) { const inp = document.getElementById('fn' + idx); const prev = document.getElementById('fn' + idx + '-prev'); + if (!prev) return; const raw = inp?.value?.trim() || ''; - if (!raw || typeof katex === 'undefined') { - prev.innerHTML = ''; prev.classList.remove('has-content'); return; + const src = raw || (inp?.getAttribute('placeholder') || ''); // пусто → показываем плейсхолдер-формулу + if (!src || typeof katex === 'undefined') { + prev.innerHTML = ''; prev.classList.remove('has-content', 'is-ph'); return; } try { - prev.innerHTML = katex.renderToString(toLatex(raw), { + prev.innerHTML = katex.renderToString(toLatex(src), { throwOnError: false, strict: false, displayMode: false, }); prev.classList.add('has-content'); - } catch { prev.innerHTML = ''; prev.classList.remove('has-content'); } + prev.classList.toggle('is-ph', !raw); // плейсхолдер — приглушённо + } catch { prev.innerHTML = ''; prev.classList.remove('has-content', 'is-ph'); } + } + + /* Вставка структуры формулы в активное поле (как редактор формул). + В токене символ | помечает позицию каретки, напр. 'sin(|)'. */ + let _activeFnIdx = 0, _graphPanelInit = false; + function graphInsert(token) { + const el = document.getElementById('fn' + _activeFnIdx) || document.getElementById('fn0'); + if (!el) return; + el.focus(); + let ins = String(token || ''); let caretInTok = -1; + const bar = ins.indexOf('|'); + if (bar >= 0) { caretInTok = bar; ins = ins.slice(0, bar) + ins.slice(bar + 1); } + const s = (el.selectionStart != null) ? el.selectionStart : el.value.length; + const e = (el.selectionEnd != null) ? el.selectionEnd : el.value.length; + el.value = el.value.slice(0, s) + ins + el.value.slice(e); + const pos = s + (caretInTok >= 0 ? caretInTok : ins.length); + try { el.setSelectionRange(pos, pos); } catch (_) {} + updateFn(_activeFnIdx); + } + + /* Отрисовать KaTeX на чипах/клавиатуре + следить за активным полем (идемпотентно). */ + function _initGraphPanel() { + const root = document.getElementById('sim-graph'); + if (!root || typeof katex === 'undefined') return; + root.querySelectorAll('.preset-btn[data-tex], .kp-btn[data-tex]').forEach(b => { + if (b.dataset.rendered) return; + try { b.innerHTML = katex.renderToString(b.dataset.tex, { throwOnError: false, strict: false, displayMode: false }); b.dataset.rendered = '1'; } catch (_) {} + }); + if (!_graphPanelInit) { + _graphPanelInit = true; + [0, 1, 2].forEach(i => { const el = document.getElementById('fn' + i); if (el) el.addEventListener('focus', () => { _activeFnIdx = i; }); }); + } + [0, 1, 2].forEach(renderPreview); } /* debounced formula update */ diff --git a/frontend/labs-bodies.html b/frontend/labs-bodies.html index 11b485f..21f064b 100644 --- a/frontend/labs-bodies.html +++ b/frontend/labs-bodies.html @@ -39,49 +39,65 @@
+
Вставить
+
+ + + + + + + + + + + + +
+
Примеры
Линейные / степенные
- - - - - + + + + +
Тригонометрия
- - - - - - + + + + + +
Показательные / логарифмы
- - - - + + + +
Прочие
- - - - - + + + + +