/* 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 '';
}
};
/* ============================================================
МОДУЛЬ 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: '',
/* Конвертеры мат. координат → 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: '',
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){}
};
})();