Files
Maxim Dolgolyov 203807ada8 feat(math6): Глава 3 — Множество (§1–§5 + финал)
§1 множество/элементы/∅ (∈ или ∉ + счёт элементов);
§2 способы задания (свойство→множество + проверка по свойству);
§3 операции ∩/∪ (наглядно через Math6.venn + счёт результата);
§4 круги Эйлера (задача с числами в областях + формула |A∪B|=|A|+|B|−|A∩B|);
финал — 5 боссов. Добавлен Math6.venn (две окружности с заливкой
областей и числами). Тесты math6: 16/16.

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

213 lines
14 KiB
JavaScript
Raw Permalink 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 });
};
/* === КРУГИ ЭЙЛЕРА / ДИАГРАММА ВЕННА (два множества) ===
* opts: {a,b (подписи), shade:'inter'|'union'|'a'|'b'|'none', shadeColor,
* regions:{aOnly,inter,bOnly,outside}} — числа/текст в областях.
*/
M.venn = function (opts) {
opts = opts || {};
var W = 300, H = 190, cA = { x: 115, y: 100 }, cB = { x: 185, y: 100 }, r = 66;
var sc = opts.shadeColor || '#7c3aed';
var s = '<defs><clipPath id="m6vA"><circle cx="' + cA.x + '" cy="' + cA.y + '" r="' + r + '"/></clipPath></defs>';
var cir = function (c, fill, op) { return '<circle cx="' + c.x + '" cy="' + c.y + '" r="' + r + '" fill="' + fill + '"' + (op != null ? ' opacity="' + op + '"' : '') + '/>'; };
if (opts.shade === 'union') { s += cir(cA, sc, 0.22) + cir(cB, sc, 0.22); }
else if (opts.shade === 'inter') { s += '<g clip-path="url(#m6vA)">' + cir(cB, sc, 0.4) + '</g>'; }
else if (opts.shade === 'a') { s += cir(cA, sc, 0.3) + '<g clip-path="url(#m6vA)">' + cir(cB, 'var(--card,#fff)', 1) + '</g>'; }
else if (opts.shade === 'b') { s += cir(cB, sc, 0.3) + '<g clip-path="url(#m6vA)">' + cir(cB, 'var(--card,#fff)', 1) + '</g>' + '<g clip-path="url(#m6vA)">' + cir(cB, sc, 0.3) + '</g>'; }
/* контуры */
s += '<circle cx="' + cA.x + '" cy="' + cA.y + '" r="' + r + '" fill="none" stroke="#4f46e5" stroke-width="2.5"/>';
s += '<circle cx="' + cB.x + '" cy="' + cB.y + '" r="' + r + '" fill="none" stroke="#e11d48" stroke-width="2.5"/>';
/* подписи множеств */
s += '<text x="' + (cA.x - 40) + '" y="' + (cA.y - r + 4) + '" font-size="16" font-weight="800" font-family="Unbounded,Inter,sans-serif" fill="#4f46e5">' + (opts.a || 'A') + '</text>';
s += '<text x="' + (cB.x + 30) + '" y="' + (cB.y - r + 4) + '" font-size="16" font-weight="800" font-family="Unbounded,Inter,sans-serif" fill="#e11d48">' + (opts.b || 'B') + '</text>';
/* числа/текст в областях */
var rg = opts.regions;
if (rg) {
function lab(x, y, t, col) { if (t == null || t === '') return ''; return '<text x="' + x + '" y="' + (y + 5) + '" text-anchor="middle" font-size="15" font-weight="700" font-family="JetBrains Mono,monospace" fill="' + (col || 'var(--text,#0f172a)') + '">' + t + '</text>'; }
s += lab(cA.x - 34, cA.y, rg.aOnly);
s += lab((cA.x + cB.x) / 2, cA.y, rg.inter);
s += lab(cB.x + 34, cB.y, rg.bOnly);
if (rg.outside != null) s += lab(W - 22, H - 14, rg.outside, 'var(--muted,#64748b)');
}
return M.box(W, H, s, { maxw: opts.maxw || W });
};
})();