Files
Learn_System/frontend/js/math6_svg.js
T
Maxim Dolgolyov 1d95f72d45 feat(math6): Phase 0 — инфраструктура учебника «Математика 6»
Хаб + 6 каркасов глав на общем движке math6_engine.js (плумбинг:
прогресс/XP/ачивки/навигация/сайдбар/поиск/глоссарий + хелперы),
math6_svg.js (window.Math6: numberLine, plane), math6.css (фреймворк
по образцу Алгебры 7). Миграция 049: хаб math-6 + math-6-ch1..ch6.
Секции глав генерируются движком из M6.paras; § без билдера → заглушка.
Тест math6-page.test.js: 8/8 (хаб 6 карточек, 6 глав, навигация, прогресс).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:47:21 +03:00

151 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* math6_svg.js — SVG-хелперы учебника «Математика 6». window.Math6.
* Самодостаточно (без зависимостей). Все функции возвращают строку SVG,
* готовую к вставке в innerHTML. Координаты SVG: y растёт вниз — учтено.
*
* Готово (Phase 0 / Глава 1): fmt, box, numberLine (прямая/луч с метками и точками),
* plane (декартова плоскость — фундамент для Главы 5).
* Будет добавлено при наполнении глав 56: plotFn, circleFig, triangleFig, solid, net,
* reflectPoint/reflectLine (симметрия).
*/
(function () {
'use strict';
if (window.Math6 && window.Math6.__installed) return;
var M = window.Math6 = window.Math6 || {};
M.__installed = true;
/* Русская запись числа: десятичная запятая, без хвостовых нулей */
M.fmt = function (n) {
if (n == null || !isFinite(n)) return '' + n;
var s = (Math.round(n * 1e9) / 1e9).toString();
return s.replace('.', ',');
};
function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
/* Обёртка <svg> с фоном */
M.box = function (w, h, inner, opts) {
opts = opts || {};
return '<div class="m6-fig" style="max-width:' + (opts.maxw || w) + 'px">'
+ '<svg viewBox="0 0 ' + w + ' ' + h + '" preserveAspectRatio="xMidYMid meet" '
+ 'style="width:100%;height:auto;display:block;background:' + (opts.bg || 'var(--card,#fff)') + ';border-radius:10px;border:1px solid var(--border,#e2e8f0)">'
+ (inner || '') + '</svg></div>';
};
/* === ЧИСЛОВАЯ ПРЯМАЯ / КООРДИНАТНЫЙ ЛУЧ ===
* opts: {min,max, minor, major, ray(bool), marks:[{v,label,color,open,above}],
* segments:[{from,to,color}], width,height, title}
*/
M.numberLine = function (opts) {
opts = opts || {};
var min = opts.min != null ? opts.min : 0;
var max = opts.max != null ? opts.max : 10;
var minor = opts.minor || 1;
var major = opts.major || minor;
var W = opts.width || 540, H = opts.height || 92;
var pad = 34;
var axisY = Math.round(H * 0.58);
var x0 = pad, x1 = W - pad;
function X(v) { return x0 + (v - min) / (max - min) * (x1 - x0); }
var col = opts.color || 'var(--pri,#4f46e5)';
var s = '';
/* выделенные отрезки/интервалы (рисуем под осью) */
(opts.segments || []).forEach(function (seg) {
var a = X(seg.from), b = X(seg.to);
s += '<line x1="' + a + '" y1="' + axisY + '" x2="' + b + '" y2="' + axisY + '" stroke="' + (seg.color || '#10b981') + '" stroke-width="6" stroke-linecap="round" opacity="0.5"/>';
});
/* ось со стрелками */
s += '<line x1="' + (x0 - 8) + '" y1="' + axisY + '" x2="' + (x1 + 8) + '" y2="' + axisY + '" stroke="' + col + '" stroke-width="2"/>';
s += '<polygon points="' + (x1 + 8) + ',' + axisY + ' ' + (x1 - 2) + ',' + (axisY - 5) + ' ' + (x1 - 2) + ',' + (axisY + 5) + '" fill="' + col + '"/>';
if (!opts.ray) s += '<polygon points="' + (x0 - 8) + ',' + axisY + ' ' + (x0 + 2) + ',' + (axisY - 5) + ' ' + (x0 + 2) + ',' + (axisY + 5) + '" fill="' + col + '"/>';
/* деления и подписи */
var nMinor = Math.round((max - min) / minor);
for (var i = 0; i <= nMinor; i++) {
var v = min + i * minor; v = Math.round(v * 1e6) / 1e6;
var x = X(v);
var isMajor = Math.abs(Math.round((v - min) / major) - (v - min) / major) < 1e-6;
var len = isMajor ? 9 : 5;
s += '<line x1="' + x + '" y1="' + (axisY - len) + '" x2="' + x + '" y2="' + (axisY + len) + '" stroke="' + col + '" stroke-width="' + (isMajor ? 1.6 : 1) + '"/>';
if (isMajor && opts.labels !== false) {
s += '<text x="' + x + '" y="' + (axisY + 24) + '" text-anchor="middle" font-size="12" font-family="JetBrains Mono,monospace" fill="var(--muted,#64748b)">' + esc(M.fmt(v)) + '</text>';
}
}
/* точки-маркеры */
(opts.marks || []).forEach(function (mk) {
var x = X(mk.v); var c = mk.color || '#e11d48';
if (mk.open) s += '<circle cx="' + x + '" cy="' + axisY + '" r="6" fill="var(--card,#fff)" stroke="' + c + '" stroke-width="2.5"/>';
else s += '<circle cx="' + x + '" cy="' + axisY + '" r="6" fill="' + c + '" stroke="#fff" stroke-width="1.5"/>';
if (mk.label) {
var ly = mk.above === false ? axisY + 38 : axisY - 14;
s += '<text x="' + x + '" y="' + ly + '" text-anchor="middle" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="' + c + '">' + esc(mk.label) + '</text>';
}
});
return M.box(W, H, s, { maxw: opts.maxw || W, bg: opts.bg });
};
/* === ДЕКАРТОВА ПЛОСКОСТЬ (фундамент для Главы 5) ===
* opts: {xmin,xmax,ymin,ymax, points:[{x,y,label,color,open}],
* segments:[{from:{x,y},to:{x,y},color,dash}], quadrants(bool),
* plot:{fn,color,samples,from,to}, size, unitLabels(bool)}
*/
M.plane = function (opts) {
opts = opts || {};
var xmin = opts.xmin != null ? opts.xmin : -5, xmax = opts.xmax != null ? opts.xmax : 5;
var ymin = opts.ymin != null ? opts.ymin : -5, ymax = opts.ymax != null ? opts.ymax : 5;
var S = opts.size || 360, pad = 22;
function X(x) { return pad + (x - xmin) / (xmax - xmin) * (S - 2 * pad); }
function Y(y) { return S - pad - (y - ymin) / (ymax - ymin) * (S - 2 * pad); }
var axc = 'var(--text,#0f172a)';
var s = '';
if (opts.quadrants) {
var cx = X(0), cy = Y(0);
s += '<rect x="' + cx + '" y="' + pad + '" width="' + (S - pad - cx) + '" height="' + (cy - pad) + '" fill="rgba(79,70,229,.05)"/>';
s += '<rect x="' + pad + '" y="' + cy + '" width="' + (cx - pad) + '" height="' + (S - pad - cy) + '" fill="rgba(225,29,72,.05)"/>';
}
/* сетка */
for (var gx = Math.ceil(xmin); gx <= Math.floor(xmax); gx++) {
s += '<line x1="' + X(gx) + '" y1="' + pad + '" x2="' + X(gx) + '" y2="' + (S - pad) + '" stroke="var(--border,#e2e8f0)" stroke-width="' + (gx === 0 ? 0 : 0.8) + '"/>';
}
for (var gy = Math.ceil(ymin); gy <= Math.floor(ymax); gy++) {
s += '<line x1="' + pad + '" y1="' + Y(gy) + '" x2="' + (S - pad) + '" y2="' + Y(gy) + '" stroke="var(--border,#e2e8f0)" stroke-width="' + (gy === 0 ? 0 : 0.8) + '"/>';
}
/* оси */
s += '<line x1="' + pad + '" y1="' + Y(0) + '" x2="' + (S - pad + 6) + '" y2="' + Y(0) + '" stroke="' + axc + '" stroke-width="1.6"/>';
s += '<polygon points="' + (S - pad + 6) + ',' + Y(0) + ' ' + (S - pad - 2) + ',' + (Y(0) - 4) + ' ' + (S - pad - 2) + ',' + (Y(0) + 4) + '" fill="' + axc + '"/>';
s += '<line x1="' + X(0) + '" y1="' + (S - pad) + '" x2="' + X(0) + '" y2="' + (pad - 6) + '" stroke="' + axc + '" stroke-width="1.6"/>';
s += '<polygon points="' + X(0) + ',' + (pad - 6) + ' ' + (X(0) - 4) + ',' + (pad + 2) + ' ' + (X(0) + 4) + ',' + (pad + 2) + '" fill="' + axc + '"/>';
s += '<text x="' + (S - pad + 2) + '" y="' + (Y(0) + 16) + '" font-size="13" font-style="italic" font-family="serif" fill="' + axc + '">x</text>';
s += '<text x="' + (X(0) + 8) + '" y="' + (pad + 2) + '" font-size="13" font-style="italic" font-family="serif" fill="' + axc + '">y</text>';
s += '<text x="' + (X(0) - 12) + '" y="' + (Y(0) + 16) + '" font-size="11" font-family="JetBrains Mono,monospace" fill="var(--muted,#64748b)">0</text>';
/* график функции */
if (opts.plot && typeof opts.plot.fn === 'function') {
var pl = opts.plot, from = pl.from != null ? pl.from : xmin, to = pl.to != null ? pl.to : xmax;
var N = pl.samples || 120, pts = [], started = false, d = '';
for (var k = 0; k <= N; k++) {
var xv = from + (to - from) * k / N, yv = pl.fn(xv);
if (yv == null || !isFinite(yv) || yv < ymin - 1 || yv > ymax + 1) { started = false; continue; }
d += (started ? ' L ' : ' M ') + X(xv) + ' ' + Y(yv); started = true;
}
s += '<path d="' + d + '" fill="none" stroke="' + (pl.color || 'var(--pri,#4f46e5)') + '" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>';
}
/* отрезки */
(opts.segments || []).forEach(function (sg) {
s += '<line x1="' + X(sg.from.x) + '" y1="' + Y(sg.from.y) + '" x2="' + X(sg.to.x) + '" y2="' + Y(sg.to.y) + '" stroke="' + (sg.color || 'var(--pri,#4f46e5)') + '" stroke-width="2"' + (sg.dash ? ' stroke-dasharray="' + sg.dash + '"' : '') + '/>';
});
/* точки */
(opts.points || []).forEach(function (p) {
var c = p.color || '#e11d48';
if (p.open) s += '<circle cx="' + X(p.x) + '" cy="' + Y(p.y) + '" r="5" fill="var(--card,#fff)" stroke="' + c + '" stroke-width="2.5"/>';
else s += '<circle cx="' + X(p.x) + '" cy="' + Y(p.y) + '" r="5" fill="' + c + '" stroke="#fff" stroke-width="1.5"/>';
if (p.label) s += '<text x="' + (X(p.x) + 8) + '" y="' + (Y(p.y) - 7) + '" font-size="12" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="' + c + '">' + esc(p.label) + '</text>';
});
return M.box(S, S, s, { maxw: opts.maxw || S, bg: opts.bg });
};
})();