diff --git a/frontend/js/labs/trigcircle.js b/frontend/js/labs/trigcircle.js index 8d7db74..01da2e4 100644 --- a/frontend/js/labs/trigcircle.js +++ b/frontend/js/labs/trigcircle.js @@ -105,6 +105,7 @@ class TrigCircleSim { if (window.LabFX) LabFX.particles.draw(c); c.restore(); + this._ovClearUnused(); this._fireUpdate(); } @@ -171,6 +172,51 @@ class TrigCircleSim { c.restore(); } + /* ═══ KaTeX-оверлей: HTML-подписи поверх canvas (на canvas KaTeX не рисуется) ══════ */ + _ov() { + if (this._ovEl === undefined) this._ovEl = (typeof document !== 'undefined' && document.getElementById) ? document.getElementById('trig-overlay') : null; + return this._ovEl; + } + /* key — стабильный id подписи; latex — LaTeX (дробь/корень → KaTeX, иначе текст); + x,y — CSS-px над canvas; anchor: c|l|r|t|b; boxed — тёмная плашка (для координат). */ + _ovLabel(key, latex, x, y, color, anchor, boxed) { + const ov = this._ov(); if (!ov) return; + this._ovMap = this._ovMap || {}; + this._ovUsed = this._ovUsed || {}; + let rec = this._ovMap[key]; + if (!rec) { + const el = document.createElement('div'); + el.style.position = 'absolute'; el.style.whiteSpace = 'nowrap'; el.style.pointerEvents = 'none'; + el.style.willChange = 'transform'; + ov.appendChild(el); + rec = this._ovMap[key] = { el, last: null, boxed: null }; + } + if (rec.last !== latex) { + const useK = /\\tfrac|\\sqrt|\\left|\\frac/.test(latex) && (typeof window !== 'undefined' && window.katex); + if (useK) rec.el.innerHTML = window.katex.renderToString(latex, { throwOnError: false, strict: false, displayMode: false }); + else rec.el.textContent = latex; + rec.last = latex; + } + if (rec.boxed !== !!boxed) { + rec.el.style.cssText += boxed + ? ';background:rgba(12,12,22,0.82);border:1px solid rgba(155,93,229,0.3);border-radius:8px;padding:3px 9px' + : ';background:none;border:none;padding:0'; + rec.boxed = !!boxed; + } + rec.el.style.color = color || '#fff'; + const a = anchor || 'c'; + const tr = a === 'r' ? 'translate(-100%,-50%)' : a === 'l' ? 'translate(0,-50%)' + : a === 't' ? 'translate(-50%,0)' : a === 'b' ? 'translate(-50%,-100%)' : 'translate(-50%,-50%)'; + rec.el.style.transform = `translate(${x}px,${y}px) ${tr}`; + rec.el.style.display = ''; + this._ovUsed[key] = true; + } + _ovClearUnused() { + if (!this._ovMap) return; + for (const k in this._ovMap) if (!(this._ovUsed && this._ovUsed[k])) this._ovMap[k].el.style.display = 'none'; + this._ovUsed = {}; + } + goToAngle(rad) { this._animTarget = this._norm(rad); if (!this.animating) this._startAnim(); @@ -354,11 +400,10 @@ class TrigCircleSim { ag.addColorStop(1, _tcRgba(_TC.violet, 0.0)); c.strokeStyle = ag; c.lineWidth = 2.5; c.beginPath(); c.arc(cx, cy, ar, 0, -a, true); c.stroke(); - /* label */ - const mid = a / 2, lr = ar + 18; - c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.violet; - c.textAlign = 'center'; c.textBaseline = 'middle'; - c.fillText(this._radLbl(a), cx + lr * Math.cos(-mid), cy + lr * Math.sin(-mid)); + /* label (KaTeX overlay: π-доля для табличных, иначе текст) */ + const mid = a / 2, lr = ar + 20; + this._ovLabel('angle', _angleLatex(a) || this._radLbl(a), + cx + lr * Math.cos(-mid), cy + lr * Math.sin(-mid), _TC.violet, 'c'); } /* ── radius ── */ @@ -452,9 +497,9 @@ class TrigCircleSim { /* ── axis value badges ── */ if (this.showSin && Math.abs(sinA) > 0.04) - this._badge(c, cx - 12, py, this._fmt(sinA), _TC.sin, 'right', 'middle'); + this._ovLabel('vsin', _latexVal(sinA), cx - 14, py, _TC.sin, 'r'); if (this.showCos && Math.abs(cosA) > 0.04) - this._badge(c, projX, cy + 17, this._fmt(cosA), _TC.cos, 'center', 'top'); + this._ovLabel('vcos', _latexVal(cosA), projX, cy + 20, _TC.cos, 't'); /* ── main point ── */ const ps = this._hover || this._drag ? 10 : 8; @@ -470,8 +515,9 @@ class TrigCircleSim { c.strokeStyle = 'rgba(255,255,255,0.50)'; c.lineWidth = 2; c.beginPath(); c.arc(px, py, ps, 0, Math.PI*2); c.stroke(); - /* ── coordinate tooltip ── */ - this._tooltip(c, px, py, cosA, sinA); + /* ── coordinate tooltip (KaTeX overlay) ── */ + this._ovLabel('coord', `\\left(${_latexVal(cosA)};\\ ${_latexVal(sinA)}\\right)`, + px + (cosA >= 0 ? 18 : -18), py + (sinA >= 0 ? -20 : 20), '#fff', cosA >= 0 ? 'l' : 'r', true); /* ── quadrant roman numeral ── */ const qOff = r * 0.46; @@ -751,17 +797,8 @@ class TrigCircleSim { c.shadowBlur = 0; c.fillStyle = 'rgba(255,255,255,0.7)'; c.beginPath(); c.arc(mx, my, 2, 0, Math.PI*2); c.fill(); - /* value badge */ - const txt = this._fmt(curY); - c.font = 'bold 11px Manrope,sans-serif'; - const tm = c.measureText(txt); - const bx2 = mx+10, by2 = my-22, bw2 = tm.width+14, bh2 = 20; - c.fillStyle='rgba(12,12,22,0.85)'; - c.beginPath(); c.roundRect(bx2, by2-bh2/2, bw2, bh2, 6); c.fill(); - c.strokeStyle = _tcRgba(col, 0.4); c.lineWidth = 1; - c.beginPath(); c.roundRect(bx2, by2-bh2/2, bw2, bh2, 6); c.stroke(); - c.fillStyle = col; c.textAlign='left'; c.textBaseline='middle'; - c.fillText(txt, bx2+7, by2); + /* value badge (KaTeX overlay) */ + this._ovLabel('gval', _latexVal(curY), mx + 12, my - 20, col, 'l', true); } c.restore(); diff --git a/frontend/labs-bodies.html b/frontend/labs-bodies.html index ed2a4a4..11b485f 100644 --- a/frontend/labs-bodies.html +++ b/frontend/labs-bodies.html @@ -655,8 +655,10 @@ -