/* 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, '>'); } /* Обёртка с фоном */ M.box = function (w, h, inner, opts) { opts = opts || {}; return '
' + '' + (inner || '') + '
'; }; /* === ЧИСЛОВАЯ ПРЯМАЯ / КООРДИНАТНЫЙ ЛУЧ === * 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 += ''; }); /* ось со стрелками */ s += ''; s += ''; if (!opts.ray) s += ''; /* деления и подписи */ 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 += ''; if (isMajor && opts.labels !== false) { s += '' + esc(M.fmt(v)) + ''; } } /* точки-маркеры */ (opts.marks || []).forEach(function (mk) { var x = X(mk.v); var c = mk.color || '#e11d48'; if (mk.open) s += ''; else s += ''; if (mk.label) { var ly = mk.above === false ? axisY + 38 : axisY - 14; s += '' + esc(mk.label) + ''; } }); 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 += ''; s += ''; } /* сетка */ for (var gx = Math.ceil(xmin); gx <= Math.floor(xmax); gx++) { s += ''; } for (var gy = Math.ceil(ymin); gy <= Math.floor(ymax); gy++) { s += ''; } /* оси */ s += ''; s += ''; s += ''; s += ''; s += 'x'; s += 'y'; s += '0'; /* график функции */ 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 += ''; } /* ломаная (график реального процесса по точкам) */ if (opts.polyline && opts.polyline.length) { var pl2 = opts.polyline, pts2 = pl2.map(function (p) { return X(p.x) + ',' + Y(p.y); }).join(' '); s += ''; if (opts.polylineDots) pl2.forEach(function (p) { s += ''; }); } /* отрезки */ (opts.segments || []).forEach(function (sg) { s += ''; }); /* точки */ (opts.points || []).forEach(function (p) { var c = p.color || '#e11d48'; if (p.open) s += ''; else s += ''; if (p.label) s += '' + esc(p.label) + ''; }); 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 += ''; } 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 += ''; } 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 += '' + Math.round(frac * 100) + '%'; 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 = ''; var cir = function (c, fill, op) { return ''; }; if (opts.shade === 'union') { s += cir(cA, sc, 0.22) + cir(cB, sc, 0.22); } else if (opts.shade === 'inter') { s += '' + cir(cB, sc, 0.4) + ''; } else if (opts.shade === 'a') { s += cir(cA, sc, 0.3) + '' + cir(cB, 'var(--card,#fff)', 1) + ''; } else if (opts.shade === 'b') { s += cir(cB, sc, 0.3) + '' + cir(cB, 'var(--card,#fff)', 1) + '' + '' + cir(cB, sc, 0.3) + ''; } /* контуры */ s += ''; s += ''; /* подписи множеств */ s += '' + (opts.a || 'A') + ''; s += '' + (opts.b || 'B') + ''; /* числа/текст в областях */ var rg = opts.regions; if (rg) { function lab(x, y, t, col) { if (t == null || t === '') return ''; return '' + t + ''; } 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 }); }; })();