From 0640efc82ca0e8c907a19b446cd52ea45c89705e Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 24 Jun 2026 13:35:28 +0300 Subject: [PATCH] =?UTF-8?q?feat(trigcircle):=20=D1=80=D0=B0=D0=B7=D0=B2?= =?UTF-8?q?=D1=91=D1=80=D1=82=D0=BA=D0=B0=20=D1=83=D0=B3=D0=BB=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=B5=20+?= =?UTF-8?q?=20KaTeX-=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=B8=20=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=84-=D0=B2=D0=B8=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - развёртка: участок кривой [0, α] выделяется ярче (с свечением) — видно, как угол на окружности «разворачивается» в график - подпись текущего угла (π/3 и т.п.) на вертикальном маркере, KaTeX - подписи делений оси X (π/2, π, 3π/2, 2π) — теперь KaTeX-оверлеем - название функции (y = sin x / cos x / tg x / ctg x) — KaTeX-оверлеем - _ovLabel: любая LaTeX-команда (\pi, \sin…) теперь рендерится через KaTeX Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/labs/trigcircle.js | 59 ++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/frontend/js/labs/trigcircle.js b/frontend/js/labs/trigcircle.js index be151f2..913583c 100644 --- a/frontend/js/labs/trigcircle.js +++ b/frontend/js/labs/trigcircle.js @@ -192,7 +192,8 @@ class TrigCircleSim { 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); + // Любая LaTeX-команда (\pi, \tfrac, \sin…) → KaTeX; простой текст/число — быстро текстом. + const useK = /\\/.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; @@ -651,7 +652,6 @@ class TrigCircleSim { const fn = this.graphFn; const col = _TC[fn] || _TC.sin; - const lbl = fn==='sin'?'y = sin x':fn==='cos'?'y = cos x':fn==='tan'?'y = tg x':'y = ctg x'; const evFn = fn==='sin'?Math.sin:fn==='cos'?Math.cos:fn==='tan'?Math.tan:(x=>1/Math.tan(x)); const yR = (fn==='tan'||fn==='cot') ? 4 : 1.5; const xMin = -0.25*Math.PI, xMax = 2.25*Math.PI; @@ -698,19 +698,16 @@ class TrigCircleSim { c.fillText('1', gx-5, sy(1)); c.fillText('−1', gx-5, sy(-1)); } - /* x ticks */ - const ticks = [[0,'0'],[Math.PI/2,'π/2'],[Math.PI,'π'],[3*Math.PI/2,'3π/2'],[2*Math.PI,'2π']]; - c.font='500 10px Manrope,sans-serif'; c.fillStyle='rgba(255,255,255,0.20)'; - c.textAlign='center'; c.textBaseline='top'; - for (const [v,l] of ticks) { + /* x ticks — линии на canvas, подписи KaTeX-оверлеем */ + const ticks = [[0, '0'], [Math.PI/2, '\\tfrac{\\pi}{2}'], [Math.PI, '\\pi'], + [3*Math.PI/2, '\\tfrac{3\\pi}{2}'], [2*Math.PI, '2\\pi']]; + ticks.forEach(([v, lx], i) => { const xx = sx(v); - if (xx < gx+6 || xx > gx+gw-6) continue; - c.strokeStyle='rgba(255,255,255,0.05)'; c.lineWidth=1; - c.setLineDash([3,3]); - c.beginPath(); c.moveTo(xx, gy); c.lineTo(xx, gy+gh); c.stroke(); - c.setLineDash([]); - c.fillText(l, xx, gy+gh+6); - } + if (xx < gx+6 || xx > gx+gw-6) return; + c.strokeStyle='rgba(255,255,255,0.05)'; c.lineWidth=1; c.setLineDash([3,3]); + c.beginPath(); c.moveTo(xx, gy); c.lineTo(xx, gy+gh); c.stroke(); c.setLineDash([]); + this._ovLabel('gtick' + i, lx, xx, gy + gh + 9, 'rgba(255,255,255,0.55)', 't'); + }); /* ── ghost curves (other functions, dimmed) ── */ c.save(); @@ -782,6 +779,21 @@ class TrigCircleSim { } c.stroke(); + /* ── развёртка: ярче выделяем кривую на [0, α] — как угол «разворачивается» в график ── */ + { + const aMax = Math.min(Math.max(this.angle, 0), xMax); + c.strokeStyle = col; c.lineWidth = 4.5; c.lineCap = 'round'; c.lineJoin = 'round'; + c.shadowColor = col; c.shadowBlur = 6; + c.beginPath(); let onS = false; + for (let x = 0; x <= aMax + 1e-9; x += step) { + const yv = evFn(x); + if (!isFinite(yv) || Math.abs(yv) > yR * 2) { onS = false; continue; } + const spx = sx(x), spy = sy(yv); + if (!onS) { c.moveTo(spx, spy); onS = true; } else c.lineTo(spx, spy); + } + c.stroke(); c.shadowBlur = 0; + } + /* ── current angle marker ── */ const curY = evFn(this.angle); if (isFinite(curY) && Math.abs(curY) <= yR*2) { @@ -802,20 +814,19 @@ class TrigCircleSim { c.beginPath(); c.arc(mx, my, 2, 0, Math.PI*2); c.fill(); /* value badge (KaTeX overlay) */ this._ovLabel('gval', _latexVal(curY), mx + 12, my - 20, col, 'l', true); + /* подпись угла на оси X (развёртка: где текущий угол на графике) */ + this._ovLabel('gangle', _angleLatex(this.angle) || this._radLbl(this.angle), + mx, gy + 5, _TC.violet, 't', true); } c.restore(); - /* fn name badge */ - c.font='bold 13px Manrope,sans-serif'; - const tm2 = c.measureText(lbl); - const bw3 = tm2.width+18, bh3 = 26; - c.fillStyle='rgba(12,12,22,0.7)'; - c.beginPath(); c.roundRect(gx+8, gy+8, bw3, bh3, 8); c.fill(); - c.strokeStyle = _tcRgba(col, 0.25); c.lineWidth = 1; - c.beginPath(); c.roundRect(gx+8, gy+8, bw3, bh3, 8); c.stroke(); - c.fillStyle = col; c.textAlign='left'; c.textBaseline='middle'; - c.fillText(lbl, gx+17, gy+21); + /* fn name badge (KaTeX-оверлей) */ + const _glblTex = fn === 'sin' ? 'y = \\sin x' + : fn === 'cos' ? 'y = \\cos x' + : fn === 'tan' ? 'y = \\operatorname{tg} x' + : 'y = \\operatorname{ctg} x'; + this._ovLabel('glabel', _glblTex, gx + 16, gy + 21, col, 'l', true); } /* ═══ Snap particles ═══════════════════════════════════════════════ */