/* alg10_svg.js — библиотека SVG-хелперов для Алгебры 10 * * Главные модули: * ALG10.tri — тригонометрическая (единичная) окружность * ALG10.func — графики функций (sin, cos, tg, ctg, многочлены, корни) * ALG10.nthRoot — графики y = ⁿ√x * * Без зависимостей. Все функции возвращают строку SVG. * * Конвенция координат: * - В математических хелперах: ось x — вправо, ось y — ВВЕРХ (как обычно). * - Внутри SVG: ось y инвертируется (через `pxY = cy - y*scale`). * * Подключение: * */ (function(){ 'use strict'; if (window.ALG10 && window.ALG10.__installed) return; const A = window.ALG10 = window.ALG10 || {}; A.__installed = true; A.version = '1.0.0'; /* ============================================================ УТИЛИТЫ ============================================================ */ A.util = { /* Округление с заданной точностью (для подписей) */ round: (v, n) => Math.round(v * Math.pow(10, n||3)) / Math.pow(10, n||3), /* Форматирование числа: 0.866 → '0.87', 1.0 → '1' */ fmt: (v, n) => { n = n || 2; if (Math.abs(v) < 1e-9) return '0'; const s = A.util.round(v, n).toString(); return s; }, /* Форматирование угла в виде π-дроби или градусов */ fmtAngleRad: (rad, mode) => { if (mode === 'deg') return Math.round(rad * 180 / Math.PI) + '°'; /* Попытка распознать π/n */ const r = rad / Math.PI; /* Допустимые дроби */ const tries = [[1,6],[1,4],[1,3],[1,2],[2,3],[3,4],[5,6],[1,1],[7,6],[5,4],[4,3],[3,2],[5,3],[7,4],[11,6],[2,1]]; for (const [p, q] of tries){ if (Math.abs(r - p/q) < 0.01) { if (p === 1 && q === 1) return 'π'; if (q === 1) return p + 'π'; if (p === 1) return 'π/' + q; return p + 'π/' + q; } if (Math.abs(r + p/q) < 0.01) { if (p === 1 && q === 1) return '-π'; if (q === 1) return '-' + p + 'π'; if (p === 1) return '-π/' + q; return '-' + p + 'π/' + q; } } return A.util.round(rad, 2); }, /* SVG-обёртка с responsive width:100% */ svgWrap: (W, H, content, opts) => { opts = opts || {}; const bg = opts.bg || '#fff'; const border = opts.border !== false ? '1px solid #e2e8f0' : 'none'; const margin = opts.margin || '0 auto'; return '' + content + ''; } }; /* ============================================================ МОДУЛЬ TRI — тригонометрическая (единичная) окружность ============================================================ */ A.tri = {}; /* Создать canvas для тригонометрической окружности. * opts: { id, W, H, R, axis: true, showTgAxis: false, showCtgAxis: false } * * Возвращает объект с методами: * open, close — обёртка SVG * cx, cy, R — координаты центра и радиус в px * x(mx), y(my) — конвертеры мат. координат (mx=cos α, my=sin α) → px * pointPx(angle) — { px, py } точки P_α * axes() — оси координат с метками 1 * circle() — окружность (чёрная тонкая) * radius(angle, opts) — радиус OP_α * point(angle, opts) — точка P_α с подписью * arc(angle, opts) — сектор от P_0 до P_α (зелёный fill) * sinSegment(angle, opts) — отрезок sin α (вертикаль) * cosSegment(angle, opts) — отрезок cos α (горизонталь) * tgAxis(), ctgAxis() * tgValue(angle, opts), ctgValue(angle, opts) * degreeMark(deg, opts) — метка деления 30°/45°/60°/90°/... * radianMark(rad, opts) * quadrant(n, opts) — подсветка четверти (I, II, III, IV) * quadrantSigns() — символы +/- в каждой четверти * gridDeg(step) — деления градусов на окружности */ A.tri.canvas = function(opts){ opts = opts || {}; const W = opts.W || 320; const H = opts.H || 320; const margin = opts.margin || 32; const R = opts.R || Math.min(W, H)/2 - margin; const cx = W/2; const cy = H/2; const id = opts.id || ('tri-' + Math.floor(Math.random()*100000)); /* Сетка-фон (опционально) */ let gridSvg = ''; if (opts.gridStep) { const step = opts.gridStep; const lines = []; for (let x = step; x < W; x += step) lines.push(''); for (let y = step; y < H; y += step) lines.push(''); gridSvg = lines.join(''); } const C = { W, H, cx, cy, R, id, open: '' + gridSvg, close: '', /* Конвертеры мат. координат → px (R = 1 в мат. ед.) */ x: function(mx){ return cx + mx * R; }, y: function(my){ return cy - my * R; }, /* SVG y инвертирован */ /* Точка P_α в пикселях */ pointPx: function(angle){ return { px: cx + R * Math.cos(angle), py: cy - R * Math.sin(angle) }; } }; /* === Оси координат === */ C.axes = function(opts){ opts = opts || {}; const color = opts.color || '#475569'; const xExt = opts.xExt || R + 18; const yExt = opts.yExt || R + 18; let s = ''; /* Ось X */ s += ''; /* Ось Y */ s += ''; /* Стрелки */ s += '' + '' + '' + ''; /* Подписи x, y */ s += 'x'; s += 'y'; /* Метка O */ s += 'O'; /* Деления 1 */ s += ''; s += '1'; s += ''; s += '1'; return s; }; /* === Окружность === */ C.circle = function(opts){ opts = opts || {}; const color = opts.color || '#1e293b'; const w = opts.width || 2; return ''; }; /* === Радиус OP_α === */ C.radius = function(angle, opts){ opts = opts || {}; const color = opts.color || '#dc2626'; const w = opts.width || 2.2; const p = C.pointPx(angle); return ''; }; /* === Точка P_α === */ C.point = function(angle, opts){ opts = opts || {}; const p = C.pointPx(angle); const r = opts.r || 4; const color = opts.color || '#dc2626'; const label = opts.label; let s = ''; if (label !== undefined) { const lOff = opts.labelOffset || 14; const lx = p.px + lOff * Math.cos(angle); const ly = p.py - lOff * Math.sin(angle); const fs = opts.fontSize || 13; const lColor = opts.labelColor || color; s += ''+label+''; } return s; }; /* === Дуга сектора от P_0 до P_α === */ C.arc = function(angle, opts){ opts = opts || {}; const r = opts.r || R * 0.25; const color = opts.color || '#10b981'; const fill = opts.fill || 'rgba(16,185,129,.20)'; /* SVG-арка: углы в SVG-конвенции (y вниз). Наш angle — в мат. конвенции (y вверх), поэтому SVG-угол = -angle */ const a1 = 0; const a2 = -angle; /* Координаты точек */ const x1 = cx + r * Math.cos(a1); const y1 = cy + r * Math.sin(a1); const x2 = cx + r * Math.cos(a2); const y2 = cy + r * Math.sin(a2); let delta = a2 - a1; /* Нормализация */ while (delta > Math.PI) delta -= 2 * Math.PI; while (delta < -Math.PI) delta += 2 * Math.PI; const large = Math.abs(angle) > Math.PI ? 1 : 0; const sweep = angle > 0 ? 0 : 1; /* CCW в SVG y-inv = sweep=0 для +angle */ /* Заполненный сектор */ let s = ''; return s; }; /* === Отрезок sin α (вертикаль от точки до оси x) === */ C.sinSegment = function(angle, opts){ opts = opts || {}; const color = opts.color || '#dc2626'; const w = opts.width || 2; const p = C.pointPx(angle); let s = ''; if (opts.label !== false){ const labelY = (p.py + cy) / 2; const labelX = p.px + (p.px > cx ? 6 : -6); const anchor = p.px > cx ? 'start' : 'end'; s += ''+(opts.label || 'sin α')+''; } return s; }; /* === Отрезок cos α (горизонталь от точки до оси y) === */ C.cosSegment = function(angle, opts){ opts = opts || {}; const color = opts.color || '#2563eb'; const w = opts.width || 2; const p = C.pointPx(angle); let s = ''; /* Wait — корректно: cos-отрезок от центра до проекции точки на ось x */ s = ''; if (opts.label !== false){ const labelX = (cx + p.px) / 2; const labelY = cy + (p.py < cy ? 14 : -6); s += ''+(opts.label || 'cos α')+''; } return s; }; /* === Ось тангенсов (вертикальная касательная x=1) === */ C.tgAxis = function(opts){ opts = opts || {}; const color = opts.color || '#16a34a'; const xAx = cx + R; const ext = opts.ext || R * 0.85; let s = ''; s += 'ось tg'; return s; }; /* === Ось котангенсов (горизонтальная касательная y=1) === */ C.ctgAxis = function(opts){ opts = opts || {}; const color = opts.color || '#7c3aed'; const yAx = cy - R; const ext = opts.ext || R * 0.85; let s = ''; s += 'ось ctg'; return s; }; /* === Значение tg α на оси тангенсов === * Продлевает OP_α до пересечения с x=1, отмечает точку A_α */ C.tgValue = function(angle, opts){ opts = opts || {}; const color = opts.color || '#16a34a'; const t = Math.tan(angle); if (!isFinite(t)) return ''; /* нет тангенса */ const xAx = cx + R; const yA = cy - t * R; /* Линия от центра через P_α до A_α */ let s = ''; /* Точка A_α */ s += ''; if (opts.label !== false){ s += 'tg α ≈ '+A.util.fmt(t, 2)+''; } return s; }; /* === Значение ctg α на оси котангенсов === */ C.ctgValue = function(angle, opts){ opts = opts || {}; const color = opts.color || '#7c3aed'; const c = 1 / Math.tan(angle); if (!isFinite(c)) return ''; const yAx = cy - R; const xA = cx + c * R; let s = ''; s += ''; if (opts.label !== false){ s += 'ctg α ≈ '+A.util.fmt(c, 2)+''; } return s; }; /* === Подсветка четверти === */ C.quadrant = function(n, opts){ opts = opts || {}; const color = opts.color || '#10b981'; const fill = opts.fill || 'rgba(16,185,129,.10)'; /* Углы для секторов: I — 0..π/2, II — π/2..π, III — π..3π/2 (-π..-π/2), IV — 3π/2..2π (-π/2..0) */ const ranges = {1:[0, Math.PI/2], 2:[Math.PI/2, Math.PI], 3:[-Math.PI, -Math.PI/2], 4:[-Math.PI/2, 0]}; const [a1, a2] = ranges[n]; const r = R; const x1 = cx + r * Math.cos(a1); const y1 = cy - r * Math.sin(a1); const x2 = cx + r * Math.cos(a2); const y2 = cy - r * Math.sin(a2); /* Сектор */ return ''; }; /* === Метки четвертей === */ C.quadrantLabels = function(opts){ opts = opts || {}; const color = opts.color || '#64748b'; const off = R * 0.55; let s = ''; s += 'I'; s += 'II'; s += 'III'; s += 'IV'; return s; }; /* === Метка деления градуса (рисочка снаружи окружности + подпись) === */ C.degreeMark = function(deg, opts){ opts = opts || {}; const angle = deg * Math.PI / 180; const color = opts.color || '#64748b'; const tickLen = opts.tickLen || 6; const lOff = opts.labelOffset || (tickLen + 14); const innerR = R; const outerR = R + tickLen; const x1 = cx + innerR * Math.cos(angle); const y1 = cy - innerR * Math.sin(angle); const x2 = cx + outerR * Math.cos(angle); const y2 = cy - outerR * Math.sin(angle); let s = ''; if (opts.label !== false){ const lx = cx + (R + lOff) * Math.cos(angle); const ly = cy - (R + lOff) * Math.sin(angle); const lab = opts.label || (deg + '°'); s += ''+lab+''; } return s; }; /* === Метка радиана (π/n) === */ C.radianMark = function(rad, opts){ opts = opts || {}; return C.degreeMark(rad * 180 / Math.PI, Object.assign({}, opts, {label: opts.label || A.util.fmtAngleRad(rad)})); }; /* === Сетка делений по 30° === */ C.gridDeg = function(step, opts){ opts = opts || {}; step = step || 30; const color = opts.color || '#cbd5e1'; let s = ''; for (let d = 0; d < 360; d += step){ const a = d * Math.PI / 180; const x1 = cx + (R - 3) * Math.cos(a); const y1 = cy - (R - 3) * Math.sin(a); const x2 = cx + (R + 3) * Math.cos(a); const y2 = cy - (R + 3) * Math.sin(a); s += ''; } return s; }; /* === Дуга-сектор для угла (со стрелкой направления вращения) === */ C.rotationArrow = function(angle, opts){ opts = opts || {}; const color = opts.color || (angle > 0 ? '#10b981' : '#dc2626'); const r = opts.r || R * 0.18; const p1 = { x: cx + r * Math.cos(0), y: cy }; const p2 = { x: cx + r * Math.cos(-angle), y: cy + r * Math.sin(-angle) }; const large = Math.abs(angle) > Math.PI ? 1 : 0; const sweep = angle > 0 ? 0 : 1; let s = ''; s += ''; return s; }; return C; }; /* ============================================================ МОДУЛЬ FUNC — графики функций ============================================================ */ A.func = {}; /* Создать canvas для графика. * opts: { id, W, H, xRange:[xMin,xMax], yRange:[yMin,yMax], gridStep, bg } */ A.func.canvas = function(opts){ opts = opts || {}; const W = opts.W || 560; const H = opts.H || 240; const xRange = opts.xRange || [-5, 5]; const yRange = opts.yRange || [-3, 3]; const margin = opts.margin || 24; const id = opts.id || ('func-' + Math.floor(Math.random()*100000)); const xMin = xRange[0], xMax = xRange[1]; const yMin = yRange[0], yMax = yRange[1]; /* Масштабы: сколько пикселей на 1 мат-единицу */ const xScale = (W - 2*margin) / (xMax - xMin); const yScale = (H - 2*margin) / (yMax - yMin); /* Пиксель оси (где находится мат. 0) */ const px0 = margin - xMin * xScale; const py0 = H - margin + yMin * yScale; const C = { W, H, xMin, xMax, yMin, yMax, xScale, yScale, px0, py0, id, open: '', close: '', pxX: function(x){ return px0 + x * xScale; }, pxY: function(y){ return py0 - y * yScale; } }; /* === Сетка === */ C.grid = function(opts){ opts = opts || {}; const xStep = opts.xStep || 1; const yStep = opts.yStep || 1; const color = opts.color || '#f1f5f9'; let s = ''; /* Вертикальные линии */ for (let x = Math.ceil(xMin); x <= Math.floor(xMax); x += xStep){ const px = C.pxX(x); s += ''; } /* Горизонтальные */ for (let y = Math.ceil(yMin); y <= Math.floor(yMax); y += yStep){ const py = C.pxY(y); s += ''; } return s; }; /* === Оси === */ C.axes = function(opts){ opts = opts || {}; const color = opts.color || '#475569'; const xTicks = opts.xTicks; /* массив {val, label} */ const yTicks = opts.yTicks; let s = ''; /* Ось X */ s += ''; /* Ось Y */ s += ''; /* Стрелка X справа */ s += ''; /* Стрелка Y сверху */ s += ''; /* Подписи x, y */ s += 'x'; s += 'y'; /* Метка O */ s += 'O'; /* Тики */ if (xTicks){ xTicks.forEach(t => { const px = C.pxX(t.val); s += ''; s += ''+(t.label || t.val)+''; }); } if (yTicks){ yTicks.forEach(t => { const py = C.pxY(t.val); s += ''; s += ''+(t.label || t.val)+''; }); } return s; }; /* === График функции y=fn(x) на xRange === */ C.plot = function(fn, opts){ opts = opts || {}; const color = opts.color || '#0d9488'; const w = opts.width || 2.5; const step = opts.step || ((xMax - xMin) / 400); const breakOnNaN = opts.breakOnNaN !== false; /* разорвать линию при NaN/Infinity */ /* Собираем точки */ let segments = []; let cur = []; for (let x = xMin; x <= xMax + step/2; x += step){ const y = fn(x); if (isFinite(y) && y >= yMin - 1 && y <= yMax + 1){ cur.push([C.pxX(x), C.pxY(y)]); } else if (cur.length) { segments.push(cur); cur = []; } } if (cur.length) segments.push(cur); /* Рисуем path-ы */ let s = ''; for (const seg of segments){ if (seg.length < 2) continue; let d = 'M ' + seg[0][0] + ' ' + seg[0][1]; for (let i = 1; i < seg.length; i++) d += ' L ' + seg[i][0] + ' ' + seg[i][1]; s += ''; } return s; }; /* === Точка с координатами === */ C.pointXY = function(x, y, opts){ opts = opts || {}; const color = opts.color || '#dc2626'; const r = opts.r || 4; const px = C.pxX(x), py = C.pxY(y); let s = ''; if (opts.label){ const lx = px + (opts.dx || 8); const ly = py + (opts.dy || -8); s += ''+opts.label+''; } return s; }; /* === Касательная к графику fn в точке x0 === */ C.tangentLine = function(fn, x0, opts){ opts = opts || {}; const color = opts.color || '#dc2626'; const w = opts.width || 2; /* Численная производная */ const h = 0.0001; const k = (fn(x0 + h) - fn(x0 - h)) / (2 * h); const y0 = fn(x0); /* y = k(x - x0) + y0 */ const x1 = xMin, y1 = k * (x1 - x0) + y0; const x2 = xMax, y2 = k * (x2 - x0) + y0; let s = ''; return s; }; /* === Вертикальная асимптота === */ C.asymptoteV = function(x, opts){ opts = opts || {}; const color = opts.color || '#dc2626'; const px = C.pxX(x); return ''; }; /* === Горизонтальная асимптота === */ C.asymptoteH = function(y, opts){ opts = opts || {}; const color = opts.color || '#dc2626'; const py = C.pxY(y); return ''; }; /* === Закрашенная область под графиком === */ C.areaUnder = function(fn, a, b, opts){ opts = opts || {}; const fill = opts.fill || 'rgba(13,148,136,.18)'; const step = (b - a) / 200; let d = 'M ' + C.pxX(a) + ' ' + C.pxY(0); for (let x = a; x <= b; x += step){ d += ' L ' + C.pxX(x) + ' ' + C.pxY(fn(x)); } d += ' L ' + C.pxX(b) + ' ' + C.pxY(0) + ' Z'; return ''; }; return C; }; /* ============================================================ МОДУЛЬ NTHROOT — графики y = ⁿ√x ============================================================ */ A.nthRoot = {}; A.nthRoot.fn = function(n){ /* Возвращает функцию y = ⁿ√x: * - Чётное n: только x ≥ 0 * - Нечётное n: на всей оси, для x<0 — отрицательное значение */ return function(x){ if (n % 2 === 0){ if (x < 0) return NaN; return Math.pow(x, 1/n); } else { if (x < 0) return -Math.pow(-x, 1/n); return Math.pow(x, 1/n); } }; }; /* ============================================================ KaTeX render (как в geom7_svg.js) ============================================================ */ A.renderMath = function(root){ if (!root || !window.renderMathInElement) return; try { window.renderMathInElement(root, { delimiters: [ { left: '$$', right: '$$', display: true }, { left: '$', right: '$', display: false }, { left: '\\[', right: '\\]', display: true }, { left: '\\(', right: '\\)', display: false } ], throwOnError: false }); } catch(e){} }; })();