203807ada8
§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>
213 lines
14 KiB
JavaScript
213 lines
14 KiB
JavaScript
/* math6_svg.js — SVG-хелперы учебника «Математика 6». window.Math6.
|
||
* Самодостаточно (без зависимостей). Все функции возвращают строку SVG,
|
||
* готовую к вставке в innerHTML. Координаты SVG: y растёт вниз — учтено.
|
||
*
|
||
* Готово (Phase 0 / Глава 1): fmt, box, numberLine (прямая/луч с метками и точками),
|
||
* plane (декартова плоскость — фундамент для Главы 5).
|
||
* Будет добавлено при наполнении глав 5–6: 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, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
||
|
||
/* Обёртка <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 });
|
||
};
|
||
|
||
})();
|