Files
Learn_System/frontend/js/math6_svg.js
T
Maxim Dolgolyov a7835659d5 feat(math6): Глава 2 — Проценты и пропорции (§1–§9 + финал)
§1 процент наглядно (сетка 100) + конвертер %↔дробь↔десятичная;
§2 три типа задач (классификатор + тренажёр % от числа);
§3 пропорция (найди член крест-накрест + проверка свойства);
§4 прямая/обратная зависимость (классификатор + таблица);
§5 решение пропорцией (прямые и обратные задачи);
§6 масштаб (карта↔местность); §7 круговые диаграммы (Math6.pie +
%↔градусы); §9 прикладной; финал — 5 боссов. Тесты math6: 15/15.

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

181 lines
11 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"/>';
}
/* ломаная (график реального процесса по точкам) */
if (opts.polyline && opts.polyline.length) {
var pl2 = opts.polyline, pts2 = pl2.map(function (p) { return X(p.x) + ',' + Y(p.y); }).join(' ');
s += '<polyline points="' + pts2 + '" fill="none" stroke="' + (opts.polylineColor || 'var(--pri,#4f46e5)') + '" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>';
if (opts.polylineDots) pl2.forEach(function (p) { s += '<circle cx="' + X(p.x) + '" cy="' + Y(p.y) + '" r="3.5" fill="' + (opts.polylineColor || 'var(--pri,#4f46e5)') + '"/>'; });
}
/* отрезки */
(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 });
};
/* === КРУГОВАЯ ДИАГРАММА ===
* segs: [{value,label,color}]. opts: {size, r}. Возвращает boxed SVG (проценты в секторах).
*/
M.pie = function (segs, opts) {
opts = opts || {};
var S = opts.size || 220, cx = S / 2, cy = S / 2, r = opts.r || (S / 2 - 8);
var total = segs.reduce(function (a, s) { return a + s.value; }, 0) || 1;
var palette = ['#4f46e5', '#0891b2', '#e11d48', '#059669', '#d97706', '#7c3aed', '#db2777'];
var ang = -Math.PI / 2, s = '';
segs.forEach(function (seg, i) {
var frac = seg.value / total, a2 = ang + frac * 2 * Math.PI;
var col = seg.color || palette[i % palette.length];
if (frac >= 0.999) { s += '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="' + col + '"/>'; }
else {
var x1 = cx + r * Math.cos(ang), y1 = cy + r * Math.sin(ang), x2 = cx + r * Math.cos(a2), y2 = cy + r * Math.sin(a2), large = frac > 0.5 ? 1 : 0;
s += '<path d="M ' + cx + ' ' + cy + ' L ' + x1 + ' ' + y1 + ' A ' + r + ' ' + r + ' 0 ' + large + ' 1 ' + x2 + ' ' + y2 + ' Z" fill="' + col + '" stroke="var(--card,#fff)" stroke-width="1.5"/>';
}
var mid = ang + frac * Math.PI, lx = cx + r * 0.62 * Math.cos(mid), ly = cy + r * 0.62 * Math.sin(mid);
if (frac > 0.05) s += '<text x="' + lx + '" y="' + (ly + 4) + '" text-anchor="middle" font-size="12" font-weight="700" fill="#fff">' + Math.round(frac * 100) + '%</text>';
ang = a2;
});
return M.box(S, S, s, { maxw: opts.maxw || S });
};
})();