4c1ce8394c
- y-ось графика теперь подписана значениями (KaTeX): sin/cos — 1, ½, 0, −½, −1; tg/ctg — 3, 2, 1, 0, −1, −2, −3 - пунктирные линии уровней + подписи слева от панели - подписи прячутся при смене функции (лишние уровни tg/ctg) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1420 lines
61 KiB
JavaScript
1420 lines
61 KiB
JavaScript
'use strict';
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════
|
||
TrigCircleSim — premium interactive unit-circle + graph visualisation
|
||
v3 — maximum polish
|
||
═══════════════════════════════════════════════════════════════════════ */
|
||
|
||
const _TC_NOTABLE = [
|
||
{ a: 0, l: '0', d: '0°' },
|
||
{ a: Math.PI / 6, l: 'π/6', d: '30°' },
|
||
{ a: Math.PI / 4, l: 'π/4', d: '45°' },
|
||
{ a: Math.PI / 3, l: 'π/3', d: '60°' },
|
||
{ a: Math.PI / 2, l: 'π/2', d: '90°' },
|
||
{ a: 2*Math.PI / 3, l: '2π/3', d: '120°' },
|
||
{ a: 3*Math.PI / 4, l: '3π/4', d: '135°' },
|
||
{ a: 5*Math.PI / 6, l: '5π/6', d: '150°' },
|
||
{ a: Math.PI, l: 'π', d: '180°' },
|
||
{ a: 7*Math.PI / 6, l: '7π/6', d: '210°' },
|
||
{ a: 5*Math.PI / 4, l: '5π/4', d: '225°' },
|
||
{ a: 4*Math.PI / 3, l: '4π/3', d: '240°' },
|
||
{ a: 3*Math.PI / 2, l: '3π/2', d: '270°' },
|
||
{ a: 5*Math.PI / 3, l: '5π/3', d: '300°' },
|
||
{ a: 7*Math.PI / 4, l: '7π/4', d: '315°' },
|
||
{ a: 11*Math.PI / 6, l: '11π/6', d: '330°' },
|
||
];
|
||
|
||
const _TC = {
|
||
sin: '#EF476F', cos: '#06D6E0', tan: '#FFD166', cot: '#7BF5A4',
|
||
point: '#9B5DE5', violet: '#9B5DE5',
|
||
};
|
||
|
||
function _tcRgb(hex) {
|
||
const n = parseInt(hex.slice(1), 16);
|
||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||
}
|
||
function _tcRgba(hex, a) {
|
||
const [r, g, b] = _tcRgb(hex);
|
||
return `rgba(${r},${g},${b},${a})`;
|
||
}
|
||
|
||
class TrigCircleSim {
|
||
constructor(canvas) {
|
||
this.canvas = canvas;
|
||
this.ctx = canvas.getContext('2d');
|
||
this.W = 0; this.H = 0; this.dpr = 1;
|
||
|
||
this.angle = Math.PI / 4;
|
||
this.showSin = true;
|
||
this.showCos = true;
|
||
this.showTan = false;
|
||
this.showCot = false;
|
||
this.showGraph = true;
|
||
this.graphFn = 'sin';
|
||
this.snapToNotable = true;
|
||
this.animating = false;
|
||
this.eq = null; // режим уравнения: { fn:'sin'|'cos'|'tg', a:Number, sols:[рад] } | null
|
||
this.showParity = false; // показать зеркальную точку −α (чётность/нечётность)
|
||
|
||
this._cx = 0; this._cy = 0; this._r = 0;
|
||
this._gx = 0; this._gw = 0; this._gh = 0; this._gy = 0;
|
||
|
||
this._drag = false;
|
||
this._hover = false;
|
||
this._raf = null;
|
||
this._animTarget = null;
|
||
this._animSpeed = 3;
|
||
this._idlePulse = 0;
|
||
this._idleRaf = null;
|
||
|
||
/* snap particles */
|
||
this._particles = [];
|
||
this._lastSnap = -1;
|
||
|
||
this.onUpdate = null;
|
||
|
||
this._bindEvents();
|
||
this._ro = new ResizeObserver(() => { this.fit(); this.draw(); });
|
||
this._ro.observe(canvas.parentElement);
|
||
}
|
||
|
||
/* ═══ Public ═══════════════════════════════════════════════════════ */
|
||
|
||
fit() {
|
||
const p = this.canvas.parentElement.getBoundingClientRect();
|
||
this.dpr = window.devicePixelRatio || 1;
|
||
this.W = p.width || 800; this.H = p.height || 500;
|
||
this.canvas.width = this.W * this.dpr;
|
||
this.canvas.height = this.H * this.dpr;
|
||
this.canvas.style.width = this.W + 'px';
|
||
this.canvas.style.height = this.H + 'px';
|
||
this._layout();
|
||
}
|
||
|
||
draw() {
|
||
const c = this.ctx;
|
||
c.save(); c.scale(this.dpr, this.dpr);
|
||
c.clearRect(0, 0, this.W, this.H);
|
||
|
||
this._drawBg(c);
|
||
this._drawCircle(c);
|
||
if (this.eq) this._drawEquation(c);
|
||
if (this.showParity) this._drawParity(c);
|
||
if (this.showGraph) { this._drawDivider(c); this._drawGraph(c); }
|
||
this._drawParticles(c);
|
||
if (window.LabFX) LabFX.particles.draw(c);
|
||
|
||
c.restore();
|
||
this._ovClearUnused();
|
||
this._fireUpdate();
|
||
}
|
||
|
||
setAngle(a) { this.angle = this._norm(a); this.draw(); }
|
||
setGraphFn(f){ this.graphFn = f; this.draw(); }
|
||
|
||
toggleLayer(n, v) {
|
||
if (n === 'sin') this.showSin = v;
|
||
if (n === 'cos') this.showCos = v;
|
||
if (n === 'tan') this.showTan = v;
|
||
if (n === 'cot') this.showCot = v;
|
||
if (n === 'graph') this.showGraph = v;
|
||
this._layout(); this.draw();
|
||
}
|
||
|
||
/* Режим уравнения: подсветить на окружности все решения fn(x)=a. */
|
||
setEquation(fn, a, sols) {
|
||
this.eq = { fn, a, sols: sols || [] };
|
||
if (this.eq.sols.length) this.angle = this.eq.sols[0]; // встать на первое решение
|
||
this.draw();
|
||
}
|
||
clearEquation() { this.eq = null; this.draw(); }
|
||
|
||
_drawEquation(c) {
|
||
const cx = this._cx, cy = this._cy, r = this._r;
|
||
const { fn, a, sols } = this.eq;
|
||
const accent = fn === 'sin' ? _TC.sin : fn === 'cos' ? _TC.cos : _TC.tan;
|
||
c.save();
|
||
/* направляющая линия значения */
|
||
c.strokeStyle = _tcRgba(accent, 0.55); c.lineWidth = 1.5; c.setLineDash([6, 5]);
|
||
c.beginPath();
|
||
if (fn === 'sin') { const y = cy - r * a; c.moveTo(cx - r - 22, y); c.lineTo(cx + r + 22, y); }
|
||
else if (fn === 'cos') { const x = cx + r * a; c.moveTo(x, cy - r - 22); c.lineTo(x, cy + r + 22); }
|
||
else { const ang = sols.length ? sols[0] : Math.atan(a); const dx = Math.cos(ang), dy = Math.sin(ang), L = r + 24;
|
||
c.moveTo(cx - L * dx, cy + L * dy); c.lineTo(cx + L * dx, cy - L * dy); }
|
||
c.stroke(); c.setLineDash([]);
|
||
/* точки-решения + подписи градусов */
|
||
c.font = 'bold 11px Manrope,sans-serif';
|
||
sols.forEach(ang => {
|
||
const x = cx + r * Math.cos(ang), y = cy - r * Math.sin(ang);
|
||
c.fillStyle = accent; c.shadowColor = accent; c.shadowBlur = 12;
|
||
c.beginPath(); c.arc(x, y, 6, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0;
|
||
c.fillStyle = 'rgba(255,255,255,0.92)'; c.beginPath(); c.arc(x, y, 2.2, 0, Math.PI * 2); c.fill();
|
||
const lr = r + 18, lx = cx + lr * Math.cos(ang), ly = cy - lr * Math.sin(ang);
|
||
c.fillStyle = accent; c.textAlign = 'center'; c.textBaseline = 'middle';
|
||
c.fillText(Math.round(ang * 180 / Math.PI) + '°', lx, ly);
|
||
});
|
||
c.restore();
|
||
}
|
||
|
||
/* Зеркальная точка −α (отражение через ось Ox): наглядно чётность cos и нечётность sin. */
|
||
_drawParity(c) {
|
||
const cx = this._cx, cy = this._cy, r = this._r, a = this.angle;
|
||
const px = cx + r * Math.cos(a), py = cy - r * Math.sin(a);
|
||
const mx = cx + r * Math.cos(-a), my = cy - r * Math.sin(-a);
|
||
c.save();
|
||
c.strokeStyle = _tcRgba(_TC.violet, 0.4); c.setLineDash([4, 4]); c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(px, py); c.lineTo(mx, my); c.stroke(); c.setLineDash([]);
|
||
c.strokeStyle = _TC.violet; c.lineWidth = 2; c.fillStyle = 'rgba(155,93,229,0.15)';
|
||
c.beginPath(); c.arc(mx, my, 6, 0, Math.PI * 2); c.fill(); c.stroke();
|
||
c.font = 'bold 11px Manrope,sans-serif'; c.fillStyle = _TC.violet;
|
||
c.textAlign = 'center'; c.textBaseline = 'middle';
|
||
c.fillText('-α', mx + (Math.cos(-a) >= 0 ? 14 : -14), my);
|
||
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) {
|
||
// Любая 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;
|
||
}
|
||
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();
|
||
}
|
||
|
||
start() { this._startIdle(); }
|
||
stop() { this._stopAnim(); this._stopIdle(); }
|
||
|
||
stats() {
|
||
const a = this.angle, s = Math.sin(a), co = Math.cos(a);
|
||
const t = Math.abs(co) > 1e-9 ? s / co : undefined;
|
||
const ct = Math.abs(s) > 1e-9 ? co / s : undefined;
|
||
const deg = a * 180 / Math.PI;
|
||
const q = a < Math.PI/2 ? 1 : a < Math.PI ? 2 : a < 3*Math.PI/2 ? 3 : 4;
|
||
// Опорный (острый) угол — к ближайшей оси Ox: основа формул приведения.
|
||
let ref;
|
||
if (a <= Math.PI / 2) ref = a;
|
||
else if (a <= Math.PI) ref = Math.PI - a;
|
||
else if (a <= 3 * Math.PI/2) ref = a - Math.PI;
|
||
else ref = 2 * Math.PI - a;
|
||
return {
|
||
angle: a, deg, radLabel: this._radLbl(a), sin: s, cos: co, tan: t, cot: ct, quadrant: q,
|
||
refAngle: ref, refDeg: ref * 180 / Math.PI,
|
||
};
|
||
}
|
||
|
||
/* ═══ Layout ═══════════════════════════════════════════════════════ */
|
||
|
||
_layout() {
|
||
const m = 44;
|
||
if (this.showGraph) {
|
||
const cW = this.W * 0.50;
|
||
this._r = Math.min(cW - m * 2, this.H - m * 2) / 2 * 0.76;
|
||
this._cx = cW / 2;
|
||
this._cy = this.H / 2;
|
||
this._gx = cW + 24;
|
||
this._gw = this.W - this._gx - m;
|
||
this._gh = this.H - m * 2;
|
||
this._gy = m;
|
||
} else {
|
||
this._r = Math.min(this.W - m * 2, this.H - m * 2) / 2 * 0.76;
|
||
this._cx = this.W / 2;
|
||
this._cy = this.H / 2;
|
||
}
|
||
this._r = Math.max(55, this._r);
|
||
}
|
||
|
||
/* ═══ Background ═══════════════════════════════════════════════════ */
|
||
|
||
_drawBg(c) {
|
||
const g = c.createRadialGradient(this._cx, this._cy, 0, this._cx, this._cy, this._r * 2.4);
|
||
g.addColorStop(0, 'rgba(155,93,229,0.055)');
|
||
g.addColorStop(0.5,'rgba(155,93,229,0.02)');
|
||
g.addColorStop(1, 'rgba(0,0,0,0)');
|
||
c.fillStyle = g; c.fillRect(0, 0, this.W, this.H);
|
||
|
||
/* decorative rings */
|
||
c.strokeStyle = 'rgba(255,255,255,0.016)'; c.lineWidth = 1;
|
||
for (let i = 1; i <= 3; i++) {
|
||
c.beginPath(); c.arc(this._cx, this._cy, this._r * (0.5 + i * 0.35), 0, Math.PI * 2);
|
||
c.stroke();
|
||
}
|
||
}
|
||
|
||
/* ═══ Unit Circle ══════════════════════════════════════════════════ */
|
||
|
||
_drawCircle(c) {
|
||
const cx = this._cx, cy = this._cy, r = this._r;
|
||
const a = this.angle;
|
||
const cosA = Math.cos(a), sinA = Math.sin(a);
|
||
const px = cx + r * cosA, py = cy - r * sinA;
|
||
const ext = Math.min(55, r * 0.35);
|
||
|
||
/* ── quadrant soft fill ── */
|
||
const q = this.stats().quadrant;
|
||
const qS = [0, Math.PI/2, Math.PI, 3*Math.PI/2][q-1];
|
||
c.fillStyle = 'rgba(155,93,229,0.022)';
|
||
c.beginPath(); c.moveTo(cx, cy);
|
||
c.arc(cx, cy, r, -(qS + Math.PI/2), -qS);
|
||
c.closePath(); c.fill();
|
||
|
||
/* ── degree tick marks (every 10°, bigger every 30°) ── */
|
||
for (let deg = 0; deg < 360; deg += 10) {
|
||
const rad = deg * Math.PI / 180;
|
||
const big = deg % 30 === 0;
|
||
const len = big ? 8 : 4;
|
||
const x1 = cx + (r - len) * Math.cos(rad);
|
||
const y1 = cy - (r - len) * Math.sin(rad);
|
||
const x2 = cx + r * Math.cos(rad);
|
||
const y2 = cy - r * Math.sin(rad);
|
||
c.strokeStyle = big ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.05)';
|
||
c.lineWidth = big ? 1.5 : 1;
|
||
c.beginPath(); c.moveTo(x1, y1); c.lineTo(x2, y2); c.stroke();
|
||
}
|
||
|
||
/* ── axes (gradient fade) ── */
|
||
const axGrad = (x1,y1,x2,y2) => {
|
||
const g = c.createLinearGradient(x1,y1,x2,y2);
|
||
g.addColorStop(0, 'rgba(255,255,255,0.0)');
|
||
g.addColorStop(0.08,'rgba(255,255,255,0.30)');
|
||
g.addColorStop(0.5, 'rgba(255,255,255,0.50)');
|
||
g.addColorStop(0.92,'rgba(255,255,255,0.30)');
|
||
g.addColorStop(1, 'rgba(255,255,255,0.0)');
|
||
return g;
|
||
};
|
||
c.lineWidth = 1.5;
|
||
c.strokeStyle = axGrad(cx - r - ext, cy, cx + r + ext, cy);
|
||
c.beginPath(); c.moveTo(cx - r - ext, cy); c.lineTo(cx + r + ext, cy); c.stroke();
|
||
c.strokeStyle = axGrad(cx, cy + r + ext, cx, cy - r - ext);
|
||
c.beginPath(); c.moveTo(cx, cy + r + ext); c.lineTo(cx, cy - r - ext); c.stroke();
|
||
|
||
/* arrows */
|
||
this._arrowH(c, cx + r + ext, cy, 0, 'rgba(255,255,255,0.5)');
|
||
this._arrowH(c, cx, cy - r - ext, -Math.PI/2, 'rgba(255,255,255,0.5)');
|
||
|
||
/* axis labels */
|
||
c.font = '700 13px Manrope,sans-serif'; c.fillStyle = 'rgba(255,255,255,0.45)';
|
||
c.textAlign = 'left'; c.textBaseline = 'top';
|
||
c.fillText('x', cx + r + ext - 12, cy + 8);
|
||
c.textAlign = 'right'; c.textBaseline = 'bottom';
|
||
c.fillText('y', cx - 10, cy - r - ext + 16);
|
||
|
||
/* ±1 ticks & labels */
|
||
c.strokeStyle = 'rgba(255,255,255,0.30)'; c.lineWidth = 1.5;
|
||
c.font = '600 11px Manrope,sans-serif'; c.fillStyle = 'rgba(255,255,255,0.45)';
|
||
const tk = 6;
|
||
c.beginPath(); c.moveTo(cx+r, cy-tk); c.lineTo(cx+r, cy+tk); c.stroke();
|
||
c.textAlign='center'; c.textBaseline='top'; c.fillText('1', cx+r, cy+9);
|
||
c.beginPath(); c.moveTo(cx-r, cy-tk); c.lineTo(cx-r, cy+tk); c.stroke();
|
||
c.fillText('−1', cx-r, cy+9);
|
||
c.beginPath(); c.moveTo(cx-tk, cy-r); c.lineTo(cx+tk, cy-r); c.stroke();
|
||
c.textAlign='right'; c.textBaseline='middle'; c.fillText('1', cx-10, cy-r);
|
||
c.beginPath(); c.moveTo(cx-tk, cy+r); c.lineTo(cx+tk, cy+r); c.stroke();
|
||
c.fillText('−1', cx-10, cy+r);
|
||
|
||
/* origin dot */
|
||
c.fillStyle = 'rgba(255,255,255,0.35)';
|
||
c.beginPath(); c.arc(cx, cy, 2.5, 0, Math.PI*2); c.fill();
|
||
|
||
/* ── unit circle (multi-layer) ── */
|
||
c.strokeStyle = _tcRgba(_TC.violet, 0.05 + Math.sin(this._idlePulse) * 0.02);
|
||
c.lineWidth = 14;
|
||
c.beginPath(); c.arc(cx, cy, r, 0, Math.PI*2); c.stroke();
|
||
c.strokeStyle = 'rgba(255,255,255,0.13)'; c.lineWidth = 2;
|
||
c.beginPath(); c.arc(cx, cy, r, 0, Math.PI*2); c.stroke();
|
||
|
||
/* ── notable angle dots + labels ── */
|
||
for (const n of _TC_NOTABLE) {
|
||
const nx = cx + r * Math.cos(n.a), ny = cy - r * Math.sin(n.a);
|
||
const act = Math.abs(a - n.a) < 0.03;
|
||
if (act) {
|
||
c.fillStyle = _tcRgba(_TC.violet, 0.5);
|
||
c.shadowColor = _TC.violet; c.shadowBlur = 10;
|
||
c.beginPath(); c.arc(nx, ny, 5, 0, Math.PI*2); c.fill();
|
||
c.shadowBlur = 0;
|
||
c.strokeStyle = _TC.violet; c.lineWidth = 1.5;
|
||
c.beginPath(); c.arc(nx, ny, 5, 0, Math.PI*2); c.stroke();
|
||
} else {
|
||
c.fillStyle = 'rgba(255,255,255,0.12)';
|
||
c.beginPath(); c.arc(nx, ny, 2.5, 0, Math.PI*2); c.fill();
|
||
}
|
||
if (n.l && n.l !== '0') {
|
||
const d = act ? 24 : 20;
|
||
const lx = cx + (r + d) * Math.cos(n.a);
|
||
const ly = cy - (r + d) * Math.sin(n.a);
|
||
c.font = act ? '700 11px Manrope,sans-serif' : '400 9px Manrope,sans-serif';
|
||
c.fillStyle = act ? _tcRgba(_TC.violet, 0.95) : 'rgba(255,255,255,0.18)';
|
||
c.textAlign = 'center'; c.textBaseline = 'middle';
|
||
c.fillText(n.l, lx, ly);
|
||
}
|
||
}
|
||
|
||
/* ── angle arc ── */
|
||
if (a > 0.015) {
|
||
const ar = Math.min(r * 0.22, 44);
|
||
c.fillStyle = _tcRgba(_TC.violet, 0.06);
|
||
c.beginPath(); c.moveTo(cx, cy); c.arc(cx, cy, ar, 0, -a, true); c.closePath(); c.fill();
|
||
const ag = c.createConicGradient(0, cx, cy);
|
||
ag.addColorStop(0, _tcRgba(_TC.violet, 0.7));
|
||
ag.addColorStop(Math.min(a / (Math.PI*2), 0.99), _tcRgba(_TC.violet, 0.25));
|
||
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 (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 ── */
|
||
const rg = c.createLinearGradient(cx, cy, px, py);
|
||
rg.addColorStop(0, 'rgba(255,255,255,0.12)'); rg.addColorStop(1, 'rgba(255,255,255,0.40)');
|
||
c.strokeStyle = rg; c.lineWidth = 1.5;
|
||
c.beginPath(); c.moveTo(cx, cy); c.lineTo(px, py); c.stroke();
|
||
|
||
/* ── projection dashes ── */
|
||
c.strokeStyle = 'rgba(255,255,255,0.08)'; c.lineWidth = 1;
|
||
c.setLineDash([4, 4]);
|
||
c.beginPath(); c.moveTo(px, py); c.lineTo(px, cy); c.stroke();
|
||
c.beginPath(); c.moveTo(px, py); c.lineTo(cx, py); c.stroke();
|
||
c.setLineDash([]);
|
||
|
||
const projX = cx + r * cosA;
|
||
|
||
/* ── triangle fill (sin+cos) ── */
|
||
if (this.showSin && this.showCos && Math.abs(cosA) > 0.04 && Math.abs(sinA) > 0.04) {
|
||
c.fillStyle = 'rgba(155,93,229,0.035)';
|
||
c.beginPath(); c.moveTo(cx, cy); c.lineTo(projX, cy); c.lineTo(px, py); c.closePath(); c.fill();
|
||
}
|
||
|
||
/* ═══ trig segments ═══ */
|
||
|
||
if (this.showCos) {
|
||
if (window.LabFX) {
|
||
LabFX.glow.drawGlow(c, () => { this._glowLine(c, cx, cy, projX, cy, _TC.cos, 4); }, { color: '#06D6E0', intensity: 4 });
|
||
} else {
|
||
this._glowLine(c, cx, cy, projX, cy, _TC.cos, 4);
|
||
}
|
||
c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.cos;
|
||
c.textAlign = 'center'; c.textBaseline = sinA >= 0 ? 'top' : 'bottom';
|
||
c.fillText('cos', (cx + projX) / 2, cy + (sinA >= 0 ? 12 : -12));
|
||
}
|
||
|
||
if (this.showSin) {
|
||
if (window.LabFX) {
|
||
LabFX.glow.drawGlow(c, () => { this._glowLine(c, projX, cy, px, py, _TC.sin, 4); }, { color: '#06D6E0', intensity: 4 });
|
||
} else {
|
||
this._glowLine(c, projX, cy, px, py, _TC.sin, 4);
|
||
}
|
||
c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.sin;
|
||
c.textAlign = cosA >= 0 ? 'left' : 'right'; c.textBaseline = 'middle';
|
||
c.fillText('sin', projX + (cosA >= 0 ? 9 : -9), (cy + py) / 2);
|
||
}
|
||
|
||
if (this.showTan && Math.abs(cosA) > 0.025) {
|
||
const tanV = sinA / cosA;
|
||
if (Math.abs(tanV) < 10) {
|
||
const tX = cosA >= 0 ? cx + r : cx - r;
|
||
const tY = cosA >= 0 ? cy - r * tanV : cy + r * tanV;
|
||
/* faint tangent guide line */
|
||
c.strokeStyle = _tcRgba(_TC.tan, 0.06); c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(tX, cy - r - ext); c.lineTo(tX, cy + r + ext); c.stroke();
|
||
this._glowLine(c, tX, cy, tX, tY, _TC.tan, 3.5);
|
||
c.strokeStyle = _tcRgba(_TC.tan, 0.18); c.lineWidth = 1;
|
||
c.setLineDash([5, 4]); c.beginPath(); c.moveTo(px, py); c.lineTo(tX, tY); c.stroke(); c.setLineDash([]);
|
||
c.fillStyle = _TC.tan; c.shadowColor = _TC.tan; c.shadowBlur = 8;
|
||
c.beginPath(); c.arc(tX, tY, 4.5, 0, Math.PI*2); c.fill(); c.shadowBlur = 0;
|
||
c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.tan;
|
||
c.textAlign = cosA >= 0 ? 'left' : 'right'; c.textBaseline = 'middle';
|
||
c.fillText('tg', tX + (cosA >= 0 ? 8 : -8), (cy + tY) / 2);
|
||
}
|
||
}
|
||
|
||
if (this.showCot && Math.abs(sinA) > 0.025) {
|
||
const cotV = cosA / sinA;
|
||
if (Math.abs(cotV) < 10) {
|
||
const cX = sinA >= 0 ? cx + r * cotV : cx - r * cotV;
|
||
const cY = sinA >= 0 ? cy - r : cy + r;
|
||
c.strokeStyle = _tcRgba(_TC.cot, 0.06); c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(cx - r - ext, cY); c.lineTo(cx + r + ext, cY); c.stroke();
|
||
this._glowLine(c, cx, cY, cX, cY, _TC.cot, 3.5);
|
||
c.strokeStyle = _tcRgba(_TC.cot, 0.18); c.lineWidth = 1;
|
||
c.setLineDash([5, 4]); c.beginPath(); c.moveTo(px, py); c.lineTo(cX, cY); c.stroke(); c.setLineDash([]);
|
||
c.fillStyle = _TC.cot; c.shadowColor = _TC.cot; c.shadowBlur = 8;
|
||
c.beginPath(); c.arc(cX, cY, 4.5, 0, Math.PI*2); c.fill(); c.shadowBlur = 0;
|
||
c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.cot;
|
||
c.textAlign = 'center'; c.textBaseline = sinA >= 0 ? 'bottom' : 'top';
|
||
c.fillText('ctg', (cx + cX) / 2, cY + (sinA >= 0 ? -8 : 8));
|
||
}
|
||
}
|
||
|
||
/* ── right-angle marker ── */
|
||
if (this.showSin && this.showCos && Math.abs(cosA) > 0.06 && Math.abs(sinA) > 0.06) {
|
||
const sz = 8, dx = cosA > 0 ? -sz : sz, dy = sinA > 0 ? sz : -sz;
|
||
c.strokeStyle = 'rgba(255,255,255,0.18)'; c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(projX+dx, cy); c.lineTo(projX+dx, cy-dy); c.lineTo(projX, cy-dy); c.stroke();
|
||
}
|
||
|
||
/* ── axis value badges ── */
|
||
if (this.showSin && Math.abs(sinA) > 0.04)
|
||
this._ovLabel('vsin', _latexVal(sinA), cx - 14, py, _TC.sin, 'r');
|
||
if (this.showCos && Math.abs(cosA) > 0.04)
|
||
this._ovLabel('vcos', _latexVal(cosA), projX, cy + 20, _TC.cos, 't');
|
||
|
||
/* ── main point ── */
|
||
const ps = this._hover || this._drag ? 10 : 8;
|
||
const blur = this._hover || this._drag ? 22 : 16;
|
||
c.fillStyle = _tcRgba(_TC.point, 0.10);
|
||
c.beginPath(); c.arc(px, py, ps + 10, 0, Math.PI*2); c.fill();
|
||
c.shadowColor = _TC.point; c.shadowBlur = blur;
|
||
c.fillStyle = _TC.point;
|
||
c.beginPath(); c.arc(px, py, ps, 0, Math.PI*2); c.fill();
|
||
c.shadowBlur = 0;
|
||
c.fillStyle = 'rgba(255,255,255,0.85)';
|
||
c.beginPath(); c.arc(px, py, ps * 0.35, 0, Math.PI*2); c.fill();
|
||
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 (KaTeX overlay) — выносим РАДИАЛЬНО НАРУЖУ за точку,
|
||
чтобы не перекрывать центральную дугу угла и её подпись ── */
|
||
const _odx = Math.cos(a), _ody = -Math.sin(a);
|
||
this._ovLabel('coord', `\\left(${_latexVal(cosA)};\\ ${_latexVal(sinA)}\\right)`,
|
||
px + _odx * 20 + (cosA >= 0 ? 6 : -6), py + _ody * 20 + (sinA >= 0 ? -8 : 8),
|
||
'#fff', cosA >= 0 ? 'l' : 'r', true);
|
||
|
||
/* ── quadrant roman numeral ── */
|
||
const qOff = r * 0.46;
|
||
const qx = (q===1||q===4) ? cx+qOff : cx-qOff;
|
||
const qy = (q<=2) ? cy-qOff : cy+qOff;
|
||
c.font = 'bold 22px Manrope,sans-serif'; c.fillStyle = _tcRgba(_TC.violet, 0.07);
|
||
c.textAlign = 'center'; c.textBaseline = 'middle';
|
||
c.fillText(['I','II','III','IV'][q-1]||'', qx, qy);
|
||
|
||
/* ── sign pills per quadrant ── */
|
||
this._quadSigns(c, cx, cy, r);
|
||
|
||
/* ── Pythagorean identity bar ── */
|
||
this._pythBar(c);
|
||
|
||
/* ── connection line to graph ── */
|
||
if (this.showGraph) this._connLine(c, px, py, sinA, cosA);
|
||
}
|
||
|
||
/* ═══ Connection line: circle <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> graph ═══════════════════════════════ */
|
||
|
||
_connLine(c, px, py, sinA, cosA) {
|
||
const fn = this.graphFn;
|
||
const val = fn === 'sin' ? sinA : fn === 'cos' ? cosA :
|
||
fn === 'tan' ? (Math.abs(cosA)>0.02 ? sinA/cosA : null) :
|
||
(Math.abs(sinA)>0.02 ? cosA/sinA : null);
|
||
if (val === null || !isFinite(val)) return;
|
||
|
||
const yR = (fn === 'tan' || fn === 'cot') ? 4 : 1.5;
|
||
if (Math.abs(val) > yR * 2) return;
|
||
|
||
const gy = this._gy, gh = this._gh;
|
||
const targetY = gy + gh/2 - val / yR * (gh/2);
|
||
|
||
/* source Y = py for sin, cy for cos, depends on fn */
|
||
const srcY = (fn === 'sin') ? py : (fn === 'cos') ? this._cy : py;
|
||
const srcX = (fn === 'sin' || fn === 'tan') ? px : this._cx;
|
||
|
||
c.strokeStyle = _tcRgba(_TC[fn] || _TC.sin, 0.12);
|
||
c.lineWidth = 1;
|
||
c.setLineDash([3, 5]);
|
||
c.beginPath(); c.moveTo(srcX, srcY); c.lineTo(this._gx, targetY); c.stroke();
|
||
c.setLineDash([]);
|
||
}
|
||
|
||
/* ═══ Quadrant sign pills ═══════════════════════════════════════════ */
|
||
|
||
_quadSigns(c, cx, cy, r) {
|
||
const signs = [
|
||
{ q: 1, s:'+', co:'+', t:'+' }, { q: 2, s:'+', co:'−', t:'−' },
|
||
{ q: 3, s:'−', co:'−', t:'+' }, { q: 4, s:'−', co:'+', t:'−' },
|
||
];
|
||
const curr = this.stats().quadrant;
|
||
const off = r * 0.78;
|
||
for (const sg of signs) {
|
||
const sx = (sg.q===1||sg.q===4) ? cx+off : cx-off;
|
||
const sy = (sg.q<=2) ? cy-off : cy+off;
|
||
const isCurr = sg.q === curr;
|
||
c.font = '500 8px Manrope,sans-serif';
|
||
c.fillStyle = isCurr ? 'rgba(255,255,255,0.25)' : 'rgba(255,255,255,0.07)';
|
||
c.textAlign = 'center'; c.textBaseline = 'middle';
|
||
const txt = `s${sg.s} c${sg.co} t${sg.t}`;
|
||
c.fillText(txt, sx, sy);
|
||
}
|
||
}
|
||
|
||
/* ═══ Pythagorean identity bar ══════════════════════════════════════ */
|
||
|
||
_pythBar(c) {
|
||
const s = Math.sin(this.angle), co = Math.cos(this.angle);
|
||
const sin2 = s * s, cos2 = co * co;
|
||
const bw = Math.min(this._r * 1.4, 180);
|
||
const bh = 6;
|
||
const bx = this._cx - bw / 2;
|
||
const by = this._cy + this._r + 38;
|
||
if (by + bh + 16 > this.H) return;
|
||
|
||
/* background track */
|
||
c.fillStyle = 'rgba(255,255,255,0.04)';
|
||
c.beginPath(); c.roundRect(bx, by, bw, bh, 3); c.fill();
|
||
|
||
/* sin² portion */
|
||
const sw = bw * sin2;
|
||
if (sw > 0.5) {
|
||
c.fillStyle = _tcRgba(_TC.sin, 0.5);
|
||
c.beginPath(); c.roundRect(bx, by, sw, bh, [3,0,0,3]); c.fill();
|
||
}
|
||
|
||
/* cos² portion */
|
||
const cw = bw * cos2;
|
||
if (cw > 0.5) {
|
||
c.fillStyle = _tcRgba(_TC.cos, 0.5);
|
||
c.beginPath(); c.roundRect(bx + sw, by, cw, bh, [0,3,3,0]); c.fill();
|
||
}
|
||
|
||
/* label */
|
||
c.font = '500 9px Manrope,sans-serif'; c.fillStyle = 'rgba(255,255,255,0.25)';
|
||
c.textAlign = 'center'; c.textBaseline = 'top';
|
||
c.fillText(`sin² + cos² = 1`, this._cx, by + bh + 4);
|
||
}
|
||
|
||
/* ═══ Divider ══════════════════════════════════════════════════════ */
|
||
|
||
_drawDivider(c) {
|
||
const x = this._gx - 14;
|
||
const pad = 20;
|
||
const lg = c.createLinearGradient(x, pad, x, this.H - pad);
|
||
lg.addColorStop(0, 'rgba(155,93,229,0.0)');
|
||
lg.addColorStop(0.15,'rgba(155,93,229,0.18)');
|
||
lg.addColorStop(0.5, 'rgba(155,93,229,0.28)');
|
||
lg.addColorStop(0.85,'rgba(155,93,229,0.18)');
|
||
lg.addColorStop(1, 'rgba(155,93,229,0.0)');
|
||
c.strokeStyle = lg; c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(x, pad); c.lineTo(x, this.H - pad); c.stroke();
|
||
/* glow */
|
||
const gg = c.createLinearGradient(x - 16, 0, x + 16, 0);
|
||
gg.addColorStop(0, 'rgba(155,93,229,0.0)');
|
||
gg.addColorStop(0.5, 'rgba(155,93,229,0.035)');
|
||
gg.addColorStop(1, 'rgba(155,93,229,0.0)');
|
||
c.fillStyle = gg; c.fillRect(x - 16, pad, 32, this.H - pad*2);
|
||
}
|
||
|
||
/* ═══ Graph ════════════════════════════════════════════════════════ */
|
||
|
||
_drawGraph(c) {
|
||
const gx = this._gx, gy = this._gy, gw = this._gw, gh = this._gh;
|
||
if (gw < 50 || gh < 50) return;
|
||
|
||
const fn = this.graphFn;
|
||
const col = _TC[fn] || _TC.sin;
|
||
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;
|
||
const sx = x => gx + (x-xMin)/(xMax-xMin)*gw;
|
||
const sy = y => gy + gh/2 - y/yR*(gh/2);
|
||
|
||
/* ── glass panel ── */
|
||
const pp = 12;
|
||
const px1 = gx-pp, py1 = gy-pp, pw = gw+pp*2, ph = gh+pp*2;
|
||
c.fillStyle = 'rgba(10,10,20,0.50)';
|
||
c.beginPath(); c.roundRect(px1, py1, pw, ph, 20); c.fill();
|
||
/* gradient border */
|
||
const bg = c.createLinearGradient(px1, py1, px1+pw, py1+ph);
|
||
bg.addColorStop(0, 'rgba(155,93,229,0.18)');
|
||
bg.addColorStop(0.3,'rgba(255,255,255,0.06)');
|
||
bg.addColorStop(0.7,'rgba(255,255,255,0.06)');
|
||
bg.addColorStop(1, 'rgba(155,93,229,0.18)');
|
||
c.strokeStyle = bg; c.lineWidth = 1.5;
|
||
c.beginPath(); c.roundRect(px1, py1, pw, ph, 20); c.stroke();
|
||
/* top highlight */
|
||
const hg = c.createLinearGradient(px1, py1, px1, py1+50);
|
||
hg.addColorStop(0, 'rgba(255,255,255,0.025)'); hg.addColorStop(1, 'rgba(255,255,255,0.0)');
|
||
c.fillStyle = hg;
|
||
c.beginPath(); c.roundRect(px1+1, py1+1, pw-2, 50, [20,20,0,0]); c.fill();
|
||
|
||
/* ── zero axis ── */
|
||
const zy = sy(0);
|
||
c.strokeStyle = 'rgba(255,255,255,0.14)'; c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(gx, zy); c.lineTo(gx+gw, zy); c.stroke();
|
||
/* y-axis on graph */
|
||
const x0 = sx(0);
|
||
if (x0 > gx + 4 && x0 < gx + gw - 4) {
|
||
c.strokeStyle = 'rgba(255,255,255,0.08)'; c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(x0, gy); c.lineTo(x0, gy+gh); c.stroke();
|
||
}
|
||
|
||
/* ── шкала значений по оси Y (значения на координатной плоскости) ── */
|
||
const yVals = (fn==='tan'||fn==='cot')
|
||
? [[3,'3'],[2,'2'],[1,'1'],[0,'0'],[-1,'-1'],[-2,'-2'],[-3,'-3']]
|
||
: [[1,'1'],[0.5,'\\tfrac{1}{2}'],[0,'0'],[-0.5,'-\\tfrac{1}{2}'],[-1,'-1']];
|
||
yVals.forEach(([v, lx], i) => {
|
||
const yy = sy(v);
|
||
if (yy < gy + 6 || yy > gy + gh - 6) return;
|
||
if (v !== 0) {
|
||
c.strokeStyle = 'rgba(255,255,255,0.05)'; c.lineWidth = 1; c.setLineDash([4, 4]);
|
||
c.beginPath(); c.moveTo(gx, yy); c.lineTo(gx+gw, yy); c.stroke(); c.setLineDash([]);
|
||
}
|
||
this._ovLabel('gy' + i, lx, gx - 6, yy, 'rgba(255,255,255,0.55)', 'r');
|
||
});
|
||
|
||
/* 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) 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();
|
||
c.beginPath(); c.rect(gx, gy, gw, gh); c.clip();
|
||
|
||
const allFns = [
|
||
{ id: 'sin', ev: Math.sin, c: _TC.sin },
|
||
{ id: 'cos', ev: Math.cos, c: _TC.cos },
|
||
{ id: 'tan', ev: Math.tan, c: _TC.tan },
|
||
{ id: 'cot', ev: x => 1/Math.tan(x), c: _TC.cot },
|
||
];
|
||
const step = (xMax - xMin) / (gw * 1.5);
|
||
|
||
for (const f of allFns) {
|
||
if (f.id === fn) continue; /* skip active — draw it last */
|
||
const show = (f.id==='sin'&&this.showSin) || (f.id==='cos'&&this.showCos) ||
|
||
(f.id==='tan'&&this.showTan) || (f.id==='cot'&&this.showCot);
|
||
if (!show) continue;
|
||
const yRg = (f.id==='tan'||f.id==='cot') ? 4 : 1.5;
|
||
const syG = y => gy + gh/2 - y/yRg*(gh/2);
|
||
c.strokeStyle = _tcRgba(f.c, 0.18); c.lineWidth = 1.5;
|
||
c.beginPath(); let on = false;
|
||
for (let x = xMin; x <= xMax; x += step) {
|
||
const yv = f.ev(x);
|
||
if (!isFinite(yv) || Math.abs(yv) > yRg*2) { on = false; continue; }
|
||
const spx = sx(x), spy = syG(yv);
|
||
if (!on) { c.moveTo(spx, spy); on = true; } else c.lineTo(spx, spy);
|
||
}
|
||
c.stroke();
|
||
}
|
||
|
||
/* gradient fill under active curve (sin/cos) */
|
||
if (fn==='sin'||fn==='cos') {
|
||
const pts = [];
|
||
for (let x = xMin; x <= xMax; x += step) {
|
||
const yv = evFn(x);
|
||
if (isFinite(yv)) pts.push({ sx: sx(x), sy: sy(yv) });
|
||
}
|
||
if (pts.length > 2) {
|
||
const fg = c.createLinearGradient(0, gy, 0, gy+gh);
|
||
fg.addColorStop(0, _tcRgba(col, 0.10));
|
||
fg.addColorStop(0.5, _tcRgba(col, 0.0));
|
||
fg.addColorStop(1, _tcRgba(col, 0.10));
|
||
c.fillStyle = fg; c.beginPath();
|
||
c.moveTo(pts[0].sx, zy);
|
||
pts.forEach(p => c.lineTo(p.sx, p.sy));
|
||
c.lineTo(pts[pts.length-1].sx, zy);
|
||
c.closePath(); c.fill();
|
||
}
|
||
}
|
||
|
||
/* active curve: glow + main */
|
||
c.strokeStyle = _tcRgba(col, 0.12); c.lineWidth = 10; c.lineCap='round'; c.lineJoin='round';
|
||
c.beginPath(); let on2 = false;
|
||
for (let x = xMin; x <= xMax; x += step) {
|
||
const yv = evFn(x);
|
||
if (!isFinite(yv)||Math.abs(yv)>yR*2) { on2 = false; continue; }
|
||
const spx = sx(x), spy = sy(yv);
|
||
if (!on2) { c.moveTo(spx, spy); on2 = true; } else c.lineTo(spx, spy);
|
||
}
|
||
c.stroke();
|
||
c.strokeStyle = col; c.lineWidth = 2.5;
|
||
c.beginPath(); on2 = false;
|
||
for (let x = xMin; x <= xMax; x += step) {
|
||
const yv = evFn(x);
|
||
if (!isFinite(yv)||Math.abs(yv)>yR*2) { on2 = false; continue; }
|
||
const spx = sx(x), spy = sy(yv);
|
||
if (!on2) { c.moveTo(spx, spy); on2 = true; } else c.lineTo(spx, spy);
|
||
}
|
||
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) {
|
||
const mx = sx(this.angle), my = sy(curY);
|
||
c.strokeStyle = _tcRgba(_TC.violet, 0.18); c.lineWidth = 1;
|
||
c.setLineDash([4, 4]);
|
||
c.beginPath(); c.moveTo(mx, gy); c.lineTo(mx, gy+gh); c.stroke();
|
||
c.strokeStyle = _tcRgba(col, 0.18);
|
||
c.beginPath(); c.moveTo(gx, my); c.lineTo(mx, my); c.stroke();
|
||
c.setLineDash([]);
|
||
/* dot */
|
||
c.fillStyle = _tcRgba(_TC.point, 0.12);
|
||
c.beginPath(); c.arc(mx, my, 13, 0, Math.PI*2); c.fill();
|
||
c.fillStyle = _TC.point; c.shadowColor = _TC.point; c.shadowBlur = 12;
|
||
c.beginPath(); c.arc(mx, my, 5.5, 0, Math.PI*2); c.fill();
|
||
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 (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 (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 ═══════════════════════════════════════════════ */
|
||
|
||
_spawnSnap(px, py) {
|
||
for (let i = 0; i < 8; i++) {
|
||
const ang = Math.random() * Math.PI * 2;
|
||
const speed = 30 + Math.random() * 50;
|
||
this._particles.push({
|
||
x: px, y: py,
|
||
vx: Math.cos(ang) * speed,
|
||
vy: Math.sin(ang) * speed,
|
||
life: 1,
|
||
col: _TC.violet,
|
||
});
|
||
}
|
||
}
|
||
|
||
_drawParticles(c) {
|
||
const dt = 0.016;
|
||
for (let i = this._particles.length - 1; i >= 0; i--) {
|
||
const p = this._particles[i];
|
||
p.x += p.vx * dt; p.y += p.vy * dt;
|
||
p.life -= dt * 1.8;
|
||
if (p.life <= 0) { this._particles.splice(i, 1); continue; }
|
||
c.fillStyle = _tcRgba(p.col, p.life * 0.6);
|
||
c.shadowColor = p.col; c.shadowBlur = 6;
|
||
c.beginPath(); c.arc(p.x, p.y, 2 * p.life, 0, Math.PI*2); c.fill();
|
||
c.shadowBlur = 0;
|
||
}
|
||
}
|
||
|
||
/* ═══ Drawing helpers ══════════════════════════════════════════════ */
|
||
|
||
_glowLine(c, x1, y1, x2, y2, col, w) {
|
||
c.lineCap = 'round';
|
||
c.strokeStyle = _tcRgba(col, 0.14); c.lineWidth = w + 8;
|
||
c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
|
||
c.strokeStyle = col; c.lineWidth = w;
|
||
c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
|
||
c.strokeStyle = _tcRgba(col, 0.45); c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
|
||
}
|
||
|
||
_arrowH(c, x, y, angle, col) {
|
||
c.save(); c.translate(x, y); c.rotate(angle);
|
||
c.fillStyle = col;
|
||
c.beginPath(); c.moveTo(0,0); c.lineTo(-9,-4.5); c.lineTo(-9,4.5); c.closePath(); c.fill();
|
||
c.restore();
|
||
}
|
||
|
||
_badge(c, x, y, txt, col, tA, tB) {
|
||
c.font='600 10px Manrope,sans-serif';
|
||
const m = c.measureText(txt);
|
||
const pw = m.width+10, ph = 17;
|
||
let bx = x, by = y;
|
||
if (tA==='right') bx = x - pw;
|
||
else if (tA==='center') bx = x - pw/2;
|
||
if (tB==='middle') by = y - ph/2;
|
||
c.fillStyle='rgba(12,12,22,0.75)';
|
||
c.beginPath(); c.roundRect(bx, by, pw, ph, 4); c.fill();
|
||
c.strokeStyle = _tcRgba(col, 0.35); c.lineWidth = 1;
|
||
c.beginPath(); c.roundRect(bx, by, pw, ph, 4); c.stroke();
|
||
c.fillStyle = col; c.textAlign='center'; c.textBaseline='middle';
|
||
c.fillText(txt, bx + pw/2, by + ph/2);
|
||
}
|
||
|
||
_tooltip(c, px, py, cosA, sinA) {
|
||
const txt = `(${this._fmt(cosA)}; ${this._fmt(sinA)})`;
|
||
c.font='600 11px Manrope,sans-serif';
|
||
const m = c.measureText(txt);
|
||
const pw = m.width+16, ph = 24;
|
||
const offX = cosA >= 0 ? 16 : -pw-16;
|
||
const offY = sinA >= 0 ? -ph-12 : 12;
|
||
const bx = px+offX, by = py+offY;
|
||
c.fillStyle='rgba(12,12,22,0.80)';
|
||
c.beginPath(); c.roundRect(bx, by, pw, ph, 8); c.fill();
|
||
c.strokeStyle = _tcRgba(_TC.violet, 0.3); c.lineWidth = 1;
|
||
c.beginPath(); c.roundRect(bx, by, pw, ph, 8); c.stroke();
|
||
c.fillStyle='rgba(255,255,255,0.82)';
|
||
c.textAlign='center'; c.textBaseline='middle';
|
||
c.fillText(txt, bx+pw/2, by+ph/2);
|
||
}
|
||
|
||
/* ═══ Formatting ═══════════════════════════════════════════════════ */
|
||
|
||
_fmt(v) {
|
||
const a = Math.abs(v), s = v < 0 ? '−' : '';
|
||
if (a < 5e-4) return '0';
|
||
if (Math.abs(a-0.5)<1e-3) return s+'½';
|
||
if (Math.abs(a-1)<1e-3) return s+'1';
|
||
if (Math.abs(a-Math.SQRT2/2)<1e-3) return s+'√2/2';
|
||
if (Math.abs(a-Math.sqrt(3)/2)<1e-3) return s+'√3/2';
|
||
if (Math.abs(a-Math.sqrt(3)/3)<1e-3) return s+'√3/3';
|
||
if (Math.abs(a-Math.sqrt(3))<1e-3) return s+'√3';
|
||
if (Math.abs(a-2)<1e-3) return s+'2';
|
||
if (Math.abs(a-2*Math.sqrt(3)/3)<1e-3)return s+'2√3/3';
|
||
return v.toFixed(3);
|
||
}
|
||
|
||
_radLbl(a) {
|
||
for (const n of _TC_NOTABLE) { if (Math.abs(a-n.a)<0.02) return n.l; }
|
||
return (a*180/Math.PI).toFixed(1)+'°';
|
||
}
|
||
|
||
_norm(a) { return ((a%(2*Math.PI))+2*Math.PI)%(2*Math.PI); }
|
||
_fireUpdate() { if (this.onUpdate) this.onUpdate(this.stats()); }
|
||
|
||
/* ═══ Events ═══════════════════════════════════════════════════════ */
|
||
|
||
_bindEvents() {
|
||
const cv = this.canvas;
|
||
|
||
const mp = e => {
|
||
const r = cv.getBoundingClientRect();
|
||
return { x:(e.clientX??e.touches?.[0]?.clientX??0)-r.left, y:(e.clientY??e.touches?.[0]?.clientY??0)-r.top };
|
||
};
|
||
|
||
const snapAngle = e => {
|
||
const m = mp(e);
|
||
let a = Math.atan2(-(m.y-this._cy), m.x-this._cx);
|
||
if (a<0) a += 2*Math.PI;
|
||
if (this.snapToNotable) {
|
||
for (const n of _TC_NOTABLE) {
|
||
if (Math.abs(a-n.a)<0.09) { a = n.a; break; }
|
||
}
|
||
}
|
||
return a;
|
||
};
|
||
|
||
const hit = e => {
|
||
const m = mp(e);
|
||
const px = this._cx + this._r*Math.cos(this.angle);
|
||
const py = this._cy - this._r*Math.sin(this.angle);
|
||
if (Math.hypot(m.x-px, m.y-py) < 30) return true;
|
||
return Math.abs(Math.hypot(m.x-this._cx, m.y-this._cy) - this._r) < 22;
|
||
};
|
||
|
||
const checkSnap = (newA) => {
|
||
for (const n of _TC_NOTABLE) {
|
||
if (Math.abs(newA-n.a)<0.02 && this._lastSnap !== n.a) {
|
||
this._lastSnap = n.a;
|
||
const nx = this._cx + this._r*Math.cos(n.a);
|
||
const ny = this._cy - this._r*Math.sin(n.a);
|
||
this._spawnSnap(nx, ny);
|
||
return;
|
||
}
|
||
}
|
||
};
|
||
|
||
const end = () => {
|
||
if (this._drag) { this._drag = false; this.draw(); }
|
||
cv.style.cursor = 'default';
|
||
};
|
||
|
||
cv.addEventListener('mousedown', e => {
|
||
if (!hit(e)) return;
|
||
this._drag = true; this._stopAnim();
|
||
const na = snapAngle(e); checkSnap(na);
|
||
this.angle = na; this.draw();
|
||
cv.style.cursor = 'grabbing';
|
||
});
|
||
|
||
this._lastDragSoundTs = 0;
|
||
cv.addEventListener('mousemove', e => {
|
||
if (this._drag) {
|
||
const na = snapAngle(e); checkSnap(na);
|
||
this.angle = na; this.draw();
|
||
const now = performance.now();
|
||
if (window.LabFX && now - this._lastDragSoundTs > 100) {
|
||
this._lastDragSoundTs = now;
|
||
const pitch = 0.8 + (this.angle / (2 * Math.PI)) * 0.8;
|
||
LabFX.sound.play('tick', { pitch, volume: 0.05 });
|
||
LabFX.haptic(5);
|
||
}
|
||
} else {
|
||
const h = hit(e);
|
||
if (h !== this._hover) { this._hover = h; this.draw(); }
|
||
cv.style.cursor = h ? 'grab' : 'default';
|
||
}
|
||
});
|
||
cv.addEventListener('mouseup', end);
|
||
cv.addEventListener('mouseleave', () => { if (this._hover){this._hover=false;this.draw();} end(); });
|
||
|
||
/* scroll wheel fine-tune */
|
||
cv.addEventListener('wheel', e => {
|
||
e.preventDefault();
|
||
const step = e.shiftKey ? 0.01 : (Math.PI / 180);
|
||
this.angle = this._norm(this.angle - Math.sign(e.deltaY) * step);
|
||
this._lastSnap = -1;
|
||
this.draw();
|
||
}, { passive: false });
|
||
|
||
/* keyboard arrows */
|
||
cv.setAttribute('tabindex', '0');
|
||
cv.style.outline = 'none';
|
||
cv.addEventListener('keydown', e => {
|
||
const step = e.shiftKey ? (Math.PI/180) : (Math.PI/36); /* 1° or 5° */
|
||
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
||
e.preventDefault(); this.angle = this._norm(this.angle + step); this.draw();
|
||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
||
e.preventDefault(); this.angle = this._norm(this.angle - step); this.draw();
|
||
}
|
||
});
|
||
|
||
/* touch */
|
||
cv.addEventListener('touchstart', e => {
|
||
e.preventDefault();
|
||
if (!hit(e)) return;
|
||
this._drag = true; this._stopAnim();
|
||
const na = snapAngle(e); checkSnap(na); this.angle = na; this.draw();
|
||
}, { passive: false });
|
||
cv.addEventListener('touchmove', e => {
|
||
e.preventDefault();
|
||
if (this._drag) { const na = snapAngle(e); checkSnap(na); this.angle = na; this.draw(); }
|
||
}, { passive: false });
|
||
cv.addEventListener('touchend', end);
|
||
}
|
||
|
||
/* ═══ Animation ════════════════════════════════════════════════════ */
|
||
|
||
_startAnim() {
|
||
this.animating = true;
|
||
let last = performance.now();
|
||
const loop = now => {
|
||
if (!this.animating) return;
|
||
const dt = (now-last)/1000; last = now;
|
||
let d = this._animTarget - this.angle;
|
||
if (d > Math.PI) d -= 2*Math.PI;
|
||
if (d < -Math.PI) d += 2*Math.PI;
|
||
if (Math.abs(d) < 0.012) {
|
||
this.angle = this._animTarget;
|
||
this.animating = false;
|
||
/* snap particle at end */
|
||
const nx = this._cx + this._r*Math.cos(this.angle);
|
||
const ny = this._cy - this._r*Math.sin(this.angle);
|
||
this._spawnSnap(nx, ny);
|
||
this.draw(); return;
|
||
}
|
||
const speed = this._animSpeed * Math.max(0.3, Math.min(1, Math.abs(d)/0.5));
|
||
this.angle += Math.sign(d) * Math.min(Math.abs(d), speed * dt);
|
||
this.angle = this._norm(this.angle);
|
||
this.draw();
|
||
this._raf = requestAnimationFrame(loop);
|
||
};
|
||
this._raf = requestAnimationFrame(loop);
|
||
}
|
||
|
||
_stopAnim() {
|
||
this.animating = false;
|
||
if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }
|
||
}
|
||
|
||
_startIdle() {
|
||
if (this._idleRaf) return;
|
||
let last = performance.now();
|
||
const loop = now => {
|
||
const dt = (now-last)/1000; last = now;
|
||
this._idlePulse += dt * 1.5;
|
||
if (window.LabFX) LabFX.particles.update(dt);
|
||
/* update particles */
|
||
if (this._particles.length > 0 || (!this._drag && !this.animating)) this.draw();
|
||
this._idleRaf = requestAnimationFrame(loop);
|
||
};
|
||
this._idleRaf = requestAnimationFrame(loop);
|
||
}
|
||
|
||
_stopIdle() {
|
||
if (this._idleRaf) { cancelAnimationFrame(this._idleRaf); this._idleRaf = null; }
|
||
}
|
||
}
|
||
|
||
if (typeof window !== 'undefined') window.TrigCircleSim = TrigCircleSim;
|
||
|
||
/* ─── lab UI init ─────────────────────────────────── */
|
||
var trigSim = null;
|
||
|
||
function _openTrigCircle() {
|
||
document.getElementById('sim-topbar-title').textContent = 'Тригонометрическая окружность';
|
||
_simShow('sim-trigcircle');
|
||
_simShow('ctrl-trigcircle');
|
||
|
||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||
if (!trigSim) {
|
||
trigSim = new TrigCircleSim(document.getElementById('trigcircle-canvas'));
|
||
trigSim.onUpdate = _trigUpdateUI;
|
||
}
|
||
trigSim.fit();
|
||
trigSim.start();
|
||
_trigUpdateUI(trigSim.stats());
|
||
}));
|
||
}
|
||
|
||
function trigToggle(layer, rowEl) {
|
||
if (!trigSim) return;
|
||
const isActive = rowEl.classList.toggle('active');
|
||
trigSim.toggleLayer(layer, isActive);
|
||
}
|
||
|
||
function trigSetGraphFn(fn, el) {
|
||
if (!trigSim) return;
|
||
document.querySelectorAll('.trig-fn-btn').forEach(b => b.classList.remove('active'));
|
||
el.classList.add('active');
|
||
trigSim.setGraphFn(fn);
|
||
}
|
||
|
||
function trigGoTo(rad) {
|
||
if (!trigSim) return;
|
||
trigSim.goToAngle(rad);
|
||
}
|
||
|
||
function trigReset() {
|
||
if (!trigSim) return;
|
||
trigSim.setAngle(Math.PI / 4);
|
||
if (window.LabFX) LabFX.sound.play('click');
|
||
}
|
||
|
||
/* Ввод угла в градусах (поле + Enter/кнопка). Принимает любое число (включая <0 и >360),
|
||
goToAngle нормализует — заодно демонстрирует котерминальность. */
|
||
function trigSetAngleDeg(inp) {
|
||
if (!trigSim || !inp) return;
|
||
const v = parseFloat(String(inp.value || '').replace(',', '.'));
|
||
if (!isFinite(v)) return;
|
||
trigSim.goToAngle(v * Math.PI / 180);
|
||
}
|
||
function trigAngleKey(e, inp) { if (e && (e.key === 'Enter' || e.keyCode === 13)) trigSetAngleDeg(inp); }
|
||
|
||
/* Показать/скрыть график функций (тема «функции» — по умолчанию можно убрать,
|
||
круг займёт всю ширину). Переиспользует существующий слой 'graph'. */
|
||
function trigToggleGraph(rowEl) {
|
||
if (!trigSim) return;
|
||
const on = rowEl.classList.toggle('active');
|
||
trigSim.toggleLayer('graph', on);
|
||
const fns = document.getElementById('trig-graph-fns');
|
||
if (fns) fns.style.display = on ? '' : 'none';
|
||
}
|
||
|
||
/* ── Уравнения: решения fn(x)=a на [0,2π) ── */
|
||
function _trigSolveAngles(fn, a) {
|
||
const TAU = 2 * Math.PI, norm = x => ((x % TAU) + TAU) % TAU;
|
||
let raw;
|
||
if (fn === 'sin') { if (Math.abs(a) > 1) return []; const b = Math.asin(a); raw = [b, Math.PI - b]; }
|
||
else if (fn === 'cos') { if (Math.abs(a) > 1) return []; const b = Math.acos(a); raw = [b, -b]; }
|
||
else { const b = Math.atan(a); raw = [b, b + Math.PI]; } // tg — всегда есть решения
|
||
const out = [];
|
||
raw.map(norm).forEach(x => { if (!out.some(y => Math.abs(y - x) < 1e-6 || Math.abs(y - x - TAU) < 1e-6)) out.push(x); });
|
||
return out.sort((p, q) => p - q);
|
||
}
|
||
/* Радиан → LaTeX красивой π-доли (или null). Покрывает главные значения arcsin/arccos/arctg. */
|
||
function _radLatex(rad) {
|
||
const P = Math.PI;
|
||
const T = [[0, '0'], [P/6, '\\tfrac{\\pi}{6}'], [P/4, '\\tfrac{\\pi}{4}'], [P/3, '\\tfrac{\\pi}{3}'],
|
||
[P/2, '\\tfrac{\\pi}{2}'], [2*P/3, '\\tfrac{2\\pi}{3}'], [3*P/4, '\\tfrac{3\\pi}{4}'],
|
||
[5*P/6, '\\tfrac{5\\pi}{6}'], [P, '\\pi']];
|
||
for (const [v, l] of T) {
|
||
if (Math.abs(rad - v) < 1e-6) return l;
|
||
if (v > 0 && Math.abs(rad + v) < 1e-6) return '-' + l;
|
||
}
|
||
return null;
|
||
}
|
||
/* Общая формула решения (LaTeX) или {none:true}. */
|
||
function _trigEqFormulaLatex(fn, a) {
|
||
if ((fn === 'sin' || fn === 'cos') && Math.abs(a) > 1) return { none: true };
|
||
if (fn === 'sin') {
|
||
const p = _radLatex(Math.asin(a)) || ('\\arcsin ' + _latexVal(a));
|
||
return { latex: `x = (-1)^{n}\\,${p} + \\pi n,\\ n\\in\\mathbb{Z}` };
|
||
}
|
||
if (fn === 'cos') {
|
||
const p = _radLatex(Math.acos(a)) || ('\\arccos ' + _latexVal(a));
|
||
return { latex: `x = \\pm ${p} + 2\\pi n,\\ n\\in\\mathbb{Z}` };
|
||
}
|
||
const p = _radLatex(Math.atan(a)) || ('\\operatorname{arctg} ' + _latexVal(a));
|
||
return { latex: `x = ${p} + \\pi n,\\ n\\in\\mathbb{Z}` };
|
||
}
|
||
|
||
var trigEqFn = 'sin';
|
||
function trigSetEqFn(fn, btn) {
|
||
trigEqFn = fn;
|
||
document.querySelectorAll('.trig-eq-fn').forEach(b => b.classList.remove('active'));
|
||
if (btn) btn.classList.add('active');
|
||
}
|
||
function trigSolve() {
|
||
if (!trigSim) return;
|
||
const inp = document.getElementById('trig-eq-input');
|
||
const a = parseFloat(String(inp && inp.value || '').replace(',', '.'));
|
||
const fnTex = { sin: '\\sin', cos: '\\cos', tg: '\\operatorname{tg}' }[trigEqFn];
|
||
const fEl = document.getElementById('trig-eq-formula');
|
||
const sEl = document.getElementById('trig-eq-sols');
|
||
if (!isFinite(a)) { if (fEl) fEl.innerHTML = '<span style="color:var(--text-3)">Введите значение a</span>'; if (sEl) sEl.textContent = ''; return; }
|
||
const sols = _trigSolveAngles(trigEqFn, a);
|
||
trigSim.setEquation(trigEqFn, a, sols);
|
||
const K = window.katex;
|
||
const tex = l => (K ? K.renderToString(l, { throwOnError: false, strict: false, displayMode: false }) : l);
|
||
const eqHead = tex(`${fnTex} x = ${_latexVal(a)}`);
|
||
const f = _trigEqFormulaLatex(trigEqFn, a);
|
||
if (fEl) {
|
||
fEl.innerHTML = `<div style="margin-bottom:5px;color:var(--violet)">${eqHead}</div>` +
|
||
(f.none ? '<div style="color:#EF476F">Нет решений (|a| > 1)</div>' : `<div>${tex(f.latex)}</div>`);
|
||
}
|
||
if (sEl) sEl.textContent = sols.length
|
||
? 'На [0, 2π): ' + sols.map(x => Math.round(x * 180 / Math.PI) + '°').join(', ')
|
||
: '';
|
||
}
|
||
function trigClearEq() {
|
||
if (!trigSim) return;
|
||
trigSim.clearEquation();
|
||
const fEl = document.getElementById('trig-eq-formula'); if (fEl) fEl.innerHTML = '';
|
||
const sEl = document.getElementById('trig-eq-sols'); if (sEl) sEl.textContent = '';
|
||
}
|
||
function trigEqKey(e) { if (e && (e.key === 'Enter' || e.keyCode === 13)) trigSolve(); }
|
||
|
||
/* ── Таблица значений (первая четверть) — строится один раз, KaTeX ── */
|
||
function _trigBuildValueTable() {
|
||
const el = document.getElementById('trig-table');
|
||
if (!el || el.dataset.built) return;
|
||
const cols = [['sin', '#EF476F'], ['cos', '#06D6E0'], ['tg', '#FFD166'], ['ctg', '#7BF5A4']];
|
||
const head = '<tr><th style="text-align:left;padding:2px 4px;color:var(--text-3);font-weight:700">α</th>' +
|
||
cols.map(([n, c]) => `<th style="padding:2px 4px;color:${c};font-weight:700">${n}</th>`).join('') + '</tr>';
|
||
const body = [0, 30, 45, 60, 90].map(deg => {
|
||
const a = deg * Math.PI / 180, sn = Math.sin(a), cs = Math.cos(a);
|
||
const tn = Math.abs(cs) > 1e-9 ? sn / cs : undefined;
|
||
const ct = Math.abs(sn) > 1e-9 ? cs / sn : undefined;
|
||
const cell = v => `<td style="padding:3px 4px;text-align:center">${_tex(_latexVal(v))}</td>`;
|
||
return `<tr data-deg="${deg}"><td style="padding:3px 4px;font-weight:700">${deg}°</td>${cell(sn)}${cell(cs)}${cell(tn)}${cell(ct)}</tr>`;
|
||
}).join('');
|
||
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:0.74rem">${head}${body}</table>`;
|
||
el.dataset.built = '1';
|
||
}
|
||
function trigToggleTable(rowEl) {
|
||
const on = rowEl.classList.toggle('active');
|
||
const el = document.getElementById('trig-table');
|
||
if (!el) return;
|
||
if (on) { _trigBuildValueTable(); el.style.display = ''; if (trigSim) _trigUpdateUI(trigSim.stats()); }
|
||
else el.style.display = 'none';
|
||
}
|
||
|
||
/* ── Чётность/нечётность + периоды (статический KaTeX-блок, строится один раз) ── */
|
||
function trigToggleParity(rowEl) {
|
||
if (!trigSim) return;
|
||
const on = rowEl.classList.toggle('active');
|
||
trigSim.showParity = on;
|
||
trigSim.draw();
|
||
const pEl = document.getElementById('trig-parity');
|
||
if (!pEl) return;
|
||
pEl.style.display = on ? '' : 'none';
|
||
if (on && !pEl.dataset.built) {
|
||
pEl.innerHTML =
|
||
`<div>${_tex('\\sin(-\\alpha) = -\\sin\\alpha')}</div>` +
|
||
`<div>${_tex('\\cos(-\\alpha) = \\cos\\alpha')}</div>` +
|
||
`<div>${_tex('\\operatorname{tg}(-\\alpha) = -\\operatorname{tg}\\alpha')}</div>` +
|
||
`<div style="margin-top:6px;color:var(--text-3);font-size:0.7rem">${_tex('T_{\\sin}=T_{\\cos}=2\\pi,\\quad T_{\\operatorname{tg}}=T_{\\operatorname{ctg}}=\\pi')}</div>`;
|
||
pEl.dataset.built = '1';
|
||
}
|
||
}
|
||
|
||
function _trigUpdateUI(s) {
|
||
const _f = v => {
|
||
if (v === undefined) return '—';
|
||
const a = Math.abs(v), sg = v < 0 ? '−' : '';
|
||
if (a < 5e-4) return '0';
|
||
if (Math.abs(a - 0.5) < 1e-3) return sg + '½';
|
||
if (Math.abs(a - 1) < 1e-3) return sg + '1';
|
||
if (Math.abs(a - Math.SQRT2/2) < 1e-3) return sg + '√2/2';
|
||
if (Math.abs(a - Math.sqrt(3)/2) < 1e-3) return sg + '√3/2';
|
||
if (Math.abs(a - Math.sqrt(3)/3) < 1e-3) return sg + '√3/3';
|
||
if (Math.abs(a - Math.sqrt(3)) < 1e-3) return sg + '√3';
|
||
return v.toFixed(4);
|
||
};
|
||
const degStr = s.deg.toFixed(1) + '°';
|
||
|
||
// Значения — KaTeX для дробей/корней, текст для простых чисел (быстро при перетаскивании).
|
||
const setMathVal = (id, v) => {
|
||
const el = document.getElementById(id); if (!el) return;
|
||
const lx = _latexVal(v);
|
||
if (/\\tfrac|\\sqrt|\\text/.test(lx)) el.innerHTML = _tex(lx);
|
||
else el.textContent = lx;
|
||
};
|
||
setMathVal('trig-v-sin', s.sin);
|
||
setMathVal('trig-v-cos', s.cos);
|
||
setMathVal('trig-v-tan', s.tan);
|
||
setMathVal('trig-v-cot', s.cot);
|
||
|
||
// Угол: KaTeX (град = π-доля) + радианы + котерминальные (+360°·k)
|
||
const al = _angleLatex(s.angle);
|
||
const head = al ? `${Math.round(s.deg)}^\\circ = ${al}` : `${degStr}`;
|
||
document.getElementById('trig-angle-badge').innerHTML =
|
||
`<div>${_tex(head)}</div>` +
|
||
`<span style="font-size:0.72rem;opacity:0.6">${s.angle.toFixed(4)} рад</span>` +
|
||
`<br><span style="font-size:0.68rem;opacity:0.5">+ 360°·k (котерминальные)</span>`;
|
||
|
||
// Опорный (острый) угол — guarded (панель может не иметь элемента)
|
||
const refEl = document.getElementById('trig-ref');
|
||
if (refEl) refEl.textContent = (Math.round(s.refDeg * 10) / 10) + '°';
|
||
// Знаки функций в текущей четверти
|
||
const signsEl = document.getElementById('trig-signs');
|
||
if (signsEl) {
|
||
const sg = v => (v > 1e-9 ? '+' : v < -1e-9 ? '−' : '0');
|
||
signsEl.innerHTML =
|
||
`<b style="color:#EF476F">sin ${sg(s.sin)}</b> · <b style="color:#06D6E0">cos ${sg(s.cos)}</b> · ` +
|
||
`<b style="color:#FFD166">tg ${s.tan === undefined ? '—' : sg(s.tan)}</b>`;
|
||
}
|
||
|
||
// Точные значения + формула приведения (только для табличных углов)
|
||
const fEl = document.getElementById('trig-formula');
|
||
if (fEl) {
|
||
const beta = Math.round(s.refDeg);
|
||
const degR = Math.round(s.deg);
|
||
const isTable = [0, 30, 45, 60, 90].some(b => Math.abs(s.refDeg - b) < 0.5);
|
||
if (!isTable) {
|
||
fEl.innerHTML = '<span style="color:var(--text-3);font-size:0.72rem;line-height:1.5">Нетабличный угол — точных значений нет, см. приближённые выше.</span>';
|
||
} else {
|
||
const reduce = (s.quadrant !== 1) && (beta === 30 || beta === 45 || beta === 60);
|
||
const K = window.katex;
|
||
const tex = latex => (K ? K.renderToString(latex, { throwOnError: false, strict: false, displayMode: false }) : latex);
|
||
const FN = { sin: '\\sin', cos: '\\cos', tg: '\\operatorname{tg}', ctg: '\\operatorname{ctg}' };
|
||
let html = '';
|
||
if (reduce) {
|
||
const wrap = s.quadrant === 2 ? `180^\\circ - ${beta}^\\circ`
|
||
: s.quadrant === 3 ? `180^\\circ + ${beta}^\\circ`
|
||
: `360^\\circ - ${beta}^\\circ`;
|
||
html += `<div style="color:var(--violet);margin-bottom:6px">${tex(`${degR}^\\circ = ${wrap}`)}</div>`;
|
||
}
|
||
const line = (nm, color, val) => {
|
||
const sgn = (val !== undefined && val < -1e-9) ? '-' : '';
|
||
const mid = reduce ? ` = ${sgn}${FN[nm]}\\,${beta}^\\circ` : '';
|
||
// KaTeX наследует CSS-цвет родителя → красим div, формулу не трогаем.
|
||
return `<div style="color:${color};line-height:1.95">${tex(`${FN[nm]}\\,${degR}^\\circ${mid} = ${_latexVal(val)}`)}</div>`;
|
||
};
|
||
fEl.innerHTML = html + line('sin', '#EF476F', s.sin) + line('cos', '#06D6E0', s.cos) +
|
||
line('tg', '#FFD166', s.tan) + line('ctg', '#7BF5A4', s.cot);
|
||
}
|
||
}
|
||
|
||
// Подсветка строки таблицы значений (по опорному острому углу)
|
||
const tbl = document.getElementById('trig-table');
|
||
if (tbl && tbl.dataset.built && typeof tbl.querySelectorAll === 'function') {
|
||
const beta = Math.round(s.refDeg);
|
||
tbl.querySelectorAll('tr[data-deg]').forEach(tr => {
|
||
tr.style.background = (Number(tr.dataset.deg) === beta) ? 'rgba(155,93,229,0.18)' : '';
|
||
});
|
||
}
|
||
|
||
// Stats bar — значения тоже KaTeX (дроби/корни)
|
||
document.getElementById('trigbar-angle').textContent = degStr;
|
||
setMathVal('trigbar-sin', s.sin);
|
||
setMathVal('trigbar-cos', s.cos);
|
||
setMathVal('trigbar-tan', s.tan);
|
||
setMathVal('trigbar-cot', s.cot);
|
||
document.getElementById('trigbar-quad').textContent = ['I', 'II', 'III', 'IV'][s.quadrant - 1];
|
||
}
|
||
|
||
/* Точное значение → LaTeX (зеркалит _f, но для KaTeX). undefined → «—». */
|
||
function _latexVal(v) {
|
||
if (v === undefined) return '\\text{не опр.}';
|
||
const a = Math.abs(v), sg = v < -1e-9 ? '-' : '';
|
||
if (a < 5e-4) return '0';
|
||
if (Math.abs(a - 0.5) < 1e-3) return sg + '\\tfrac{1}{2}';
|
||
if (Math.abs(a - Math.SQRT2 / 2) < 1e-3) return sg + '\\tfrac{\\sqrt{2}}{2}';
|
||
if (Math.abs(a - Math.sqrt(3) / 2) < 1e-3) return sg + '\\tfrac{\\sqrt{3}}{2}';
|
||
if (Math.abs(a - Math.sqrt(3) / 3) < 1e-3) return sg + '\\tfrac{\\sqrt{3}}{3}';
|
||
if (Math.abs(a - 1) < 1e-3) return sg + '1';
|
||
if (Math.abs(a - Math.sqrt(3)) < 1e-3) return sg + '\\sqrt{3}';
|
||
return v.toFixed(3);
|
||
}
|
||
|
||
/* Рендер LaTeX → HTML через KaTeX (с фолбэком на сырой LaTeX, если katex ещё не готов). */
|
||
function _tex(latex) {
|
||
const K = window.katex;
|
||
return K ? K.renderToString(latex, { throwOnError: false, strict: false, displayMode: false }) : latex;
|
||
}
|
||
/* Юникод-метка π-доли ('7π/6','π/4','π','0') → LaTeX. */
|
||
function _piLabelToLatex(l) {
|
||
if (l === '0') return '0';
|
||
const conv = s => s.replace('π', '\\pi');
|
||
if (l.indexOf('/') >= 0) { const p = l.split('/'); return `\\tfrac{${conv(p[0])}}{${p[1]}}`; }
|
||
return conv(l);
|
||
}
|
||
/* Радиан текущего угла → LaTeX красивой π-доли по таблице 16 углов (или null). */
|
||
function _angleLatex(rad) {
|
||
for (const n of _TC_NOTABLE) if (Math.abs(rad - n.a) < 1e-6) return _piLabelToLatex(n.l);
|
||
return null;
|
||
}
|
||
|
||
/* ── KaTeX live preview ── */
|
||
|
||
/** Convert user ascii expression <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> LaTeX string for KaTeX preview */
|