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>
This commit is contained in:
Maxim Dolgolyov
2026-06-02 14:47:21 +03:00
parent c900a0332e
commit 1d95f72d45
12 changed files with 1813 additions and 0 deletions
@@ -0,0 +1,51 @@
-- Math 6 hub migration.
-- Creates math-6 as a full hub textbook (6 chapters) in the style of chemistry-7 / algebra-7:
-- math-6 (hub, html_path = math_6_hub.html)
-- math-6-ch1 (Десятичные дроби, §§1–12) → math_6_ch1.html
-- math-6-ch2 (Проценты и пропорции, §§1–9) → math_6_ch2.html
-- math-6-ch3 (Множество, §§1–5) → math_6_ch3.html
-- math-6-ch4 (Рациональные числа, §§1–11) → math_6_ch4.html
-- math-6-ch5 (Координатная плоскость, §§1–5) → math_6_ch5.html
-- math-6-ch6 (Наглядная геометрия, §§1–5) → math_6_ch6.html
--
-- Source: Герасимов В. Д., Пирютко О. Н., «Математика. 6 класс»,
-- Минск: Адукацыя і выхаванне, 2022 (2-е изд.). Контент авторский (наш).
-- Author left empty per project policy.
-- 1. Parent hub row.
INSERT OR IGNORE INTO textbooks
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
VALUES
('math-6', 'math', 6, 'Математика — 6 класс',
'',
'Полный курс математики 6 класса: десятичные дроби, проценты и пропорции, множества, рациональные (положительные и отрицательные) числа, координатная плоскость и наглядная геометрия. 6 глав, 38 параграфов, интерактивные тренажёры и финалы-боссы.',
'math_6_hub.html', 48, 'indigo', 6, 1, NULL);
-- 2. Six chapters.
INSERT OR IGNORE INTO textbooks
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
VALUES
('math-6-ch1', 'math', 6, 'Математика 6 · Десятичные дроби',
'',
'§§1–12: десятичная запись и разряды, сравнение и округление, координатный луч, сложение и вычитание, умножение и деление (в т. ч. на 10, 100, 1000), деление на десятичную дробь, конечные и бесконечные дроби, преобразования выражений.',
'math_6_ch1.html', 12, 'indigo', 1, 1, 'math-6'),
('math-6-ch2', 'math', 6, 'Математика 6 · Проценты и пропорции',
'',
'§§1–9: проценты, три основные задачи на проценты, пропорция и её основное свойство, прямая и обратная пропорциональные зависимости, решение задач пропорцией, масштаб, круговые диаграммы.',
'math_6_ch2.html', 9, 'cyan', 2, 1, 'math-6'),
('math-6-ch3', 'math', 6, 'Математика 6 · Множество',
'',
'§§1–5: множество и его элементы, пустое множество, способы задания множеств, операции (пересечение и объединение), круги Эйлера и решение задач с их помощью.',
'math_6_ch3.html', 5, 'violet', 3, 1, 'math-6'),
('math-6-ch4', 'math', 6, 'Математика 6 · Рациональные числа',
'',
'§§1–11: положительные и отрицательные числа, координатная прямая, модуль и противоположные числа, множества Z и Q, сравнение, сложение, вычитание, умножение и деление рациональных чисел, законы сложения.',
'math_6_ch4.html', 11, 'rose', 4, 1, 'math-6'),
('math-6-ch5', 'math', 6, 'Математика 6 · Координатная плоскость',
'',
'§§1–5: прямоугольная (декартова) система координат, графики реальных процессов, графики прямой и обратной пропорциональной зависимости.',
'math_6_ch5.html', 5, 'emerald', 5, 1, 'math-6'),
('math-6-ch6', 'math', 6, 'Математика 6 · Наглядная геометрия',
'',
'§§1–5: наглядные представления тел и их развёртки, окружность и круг (длина окружности и площадь круга), виды треугольников, центральная и осевая симметрия.',
'math_6_ch6.html', 6, 'amber', 6, 1, 'math-6');
+88
View File
@@ -0,0 +1,88 @@
'use strict';
/*
* Phase 0 jsdom-каркас «Математика 6»: хаб и 6 глав выполняются на движке
* math6_engine.js без ошибок скриптов; para-selector строится с нужным числом
* карточек; активен первый §; движок рендерит секции и заглушку/контент с кнопкой
* прочтения. Содержание §§ наполняется по главам — здесь проверяется каркас.
*/
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const { JSDOM, VirtualConsole } = require('jsdom');
const ROOT = path.join(__dirname, '..', '..');
const readF = p => fs.readFileSync(path.join(ROOT, p), 'utf8');
const wait = ms => new Promise(r => setTimeout(r, ms));
/* Инлайним внешние скрипты (CDN убираем, api/xp заменяем заглушками). */
function buildPage(file) {
let html = readF('frontend/textbooks/' + file);
const inl = {
'/js/math6_svg.js': readF('frontend/js/math6_svg.js'),
'/js/math6_engine.js': readF('frontend/js/math6_engine.js')
};
html = html
.replace(/<script defer src="https:\/\/cdn[^"]*"[^>]*><\/script>/g, '')
.replace(/<script src="\/js\/api\.js" defer><\/script>/, '<script>window.renderMathInElement=function(){};</script>')
.replace(/<script src="\/js\/xp\.js" defer><\/script>/, '');
Object.keys(inl).forEach(src => {
html = html.replace(new RegExp('<script src="' + src + '" defer><\\/script>'), () => '<script>\n' + inl[src] + '\n</script>');
});
return html;
}
async function loadDom(file) {
const errors = [];
const vc = new VirtualConsole();
vc.on('jsdomError', e => errors.push(e.message));
const dom = new JSDOM(buildPage(file), {
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
beforeParse(w) { w.scrollTo = function () {}; }
});
await wait(160);
return { dom, errors, doc: dom.window.document };
}
const CHAPTERS = [
{ file: 'math_6_ch1.html', cards: 12 },
{ file: 'math_6_ch2.html', cards: 9 },
{ file: 'math_6_ch3.html', cards: 5 },
{ file: 'math_6_ch4.html', cards: 11 },
{ file: 'math_6_ch5.html', cards: 5 },
{ file: 'math_6_ch6.html', cards: 6 }
];
for (const ch of CHAPTERS) {
test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен § 1`, async () => {
const { doc, errors } = await loadDom(ch.file);
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, ch.cards, ch.cards + ' карточек');
const active = doc.querySelector('.sec.active');
assert.ok(active && active.id === 'sec-p1', 'активен sec-p1');
const body = doc.querySelector('#p1-body');
assert.ok(body && body.children.length > 0, 'тело § 1 заполнено');
assert.ok(doc.querySelector('#p1-body [data-read]'), 'кнопка прочтения § 1');
/* финал помечен и присутствует */
assert.ok(doc.querySelector('#psel-grid .psel-card.final'), 'есть карточка финала');
});
}
test('hub: 6 карточек глав', async () => {
const { doc, errors } = await loadDom('math_6_hub.html');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 6, '6 глав');
});
test('навигация и прогресс: переход на § и отметка прочтения', async () => {
const { doc, errors } = await loadDom('math_6_ch1.html');
const win = doc.defaultView;
win.goTo('p4'); await wait(60);
assert.ok(doc.querySelector('#sec-p4.active'), 'перешли на § 4');
/* отметка прочтения начисляет XP */
const btn = doc.querySelector('#p4-body [data-read]');
assert.ok(btn, 'кнопка прочтения § 4');
btn.click(); await wait(20);
assert.ok((win.M6STATE.progress.p4 || 0) >= 30, 'прогресс § 4 вырос после прочтения');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});
+243
View File
@@ -0,0 +1,243 @@
/* math6.css — общий фреймворк интерактивного учебника «Математика 6».
* Подключается всеми 6 главами; палитра задаётся на странице через :root
* (--pri / --pri2 / --pri-soft / --acc / --acc2 / --acc-soft).
* Базируется на проверенном дизайне Алгебры 7, обобщён под переменные.
* Иконки — только inline SVG .ic (эмодзи запрещены).
*/
:root{
--bg:#f7f8fc; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
/* Палитра по умолчанию (indigo) — страницы переопределяют */
--pri:#4f46e5; --pri2:#3730a3; --pri-soft:#e0e7ff;
--acc:#6366f1; --acc2:#4f46e5; --acc-soft:#eef2ff;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
/* Акцент секции = цвет главы (можно переопределить на .sec) */
--sec-acc:var(--pri); --sec-acc-d:var(--pri2); --sec-acc-soft:var(--pri-soft);
}
html.dark,.dark{
--bg:#0a0a12; --card:#13131f; --card-soft:#18182a; --text:#e8eaf6; --ink:#e8eaf6; --muted:#9aa0c0;
--border:#26263a; --pri-soft:rgba(99,102,241,.16); --acc-soft:rgba(99,102,241,.14);
--warn-bg:rgba(245,158,11,.16); --ok-bg:rgba(16,185,129,.16); --fail-bg:rgba(220,38,38,.18);
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
button,input,select,textarea{font-family:inherit;font-size:inherit}
button{cursor:pointer;border:0;background:transparent;color:inherit}
a{color:inherit;text-decoration:none}
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
/* HEADER */
.hdr{position:relative;background:linear-gradient(110deg,var(--pri2) 0%,var(--pri) 55%,var(--acc) 100%);color:#fff;padding:42px 22px 28px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.16);min-height:124px}
.hdr::before{content:attr(data-wm);position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(4.5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.13);line-height:1;pointer-events:none;user-select:none;z-index:0}
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
.hdr-sub{font-size:.85rem;opacity:.9;margin-top:6px;font-weight:500;line-height:1.4}
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.16);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
.hdr-btn:hover{background:rgba(255,255,255,.28)}
/* MAIN GRID */
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
.col-main{min-width:0}
/* HERO */
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.hero::before{content:attr(data-wm);position:absolute;right:-10px;top:-20px;font-size:clamp(2rem,8vw,5.5rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.9;margin-bottom:14px;max-width:660px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(79,70,229,.32)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(79,70,229,.16);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(79,70,229,.22);font-family:'Unbounded',sans-serif}
/* PARA SELECTOR */
.psel{margin-bottom:24px}
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(79,70,229,.12);border-radius:3px;overflow:hidden}
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
.psel-card.final{background:linear-gradient(135deg,var(--warn-bg,#fff5e1),var(--pri-soft))}
.psel-card.final .psel-num{color:var(--warn)}
.psel-card.applied .psel-num{color:var(--ok)}
/* SECTIONS */
.sec{display:none;position:relative;animation:fadeIn .35s ease}
.sec.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec::before{content:attr(data-watermark);position:absolute;right:-20px;top:10%;font-family:'Unbounded',sans-serif;font-size:clamp(6rem,18vw,14rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px var(--sec-acc-soft,var(--pri-soft));line-height:1;pointer-events:none;user-select:none;z-index:0;opacity:.35}
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
/* CARDS */
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(79,70,229,.05);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(79,70,229,.10)}
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}.card-icon.class{background:#3b82f6}.card-icon.home{background:#f97316}
.card-icon .ic{width:18px;height:18px}
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
.card-body{font-size:.94rem;line-height:1.65}
.card-body p{margin-bottom:8px}
.card-body p:last-child{margin-bottom:0}
.card-body ul,.card-body ol{margin:6px 0}
/* WIDGET */
.wg{background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)));border:1.5px solid var(--sec-acc,var(--pri));border-radius:14px;padding:18px 20px;margin-bottom:18px;box-shadow:var(--sh2);position:relative;z-index:1}
.wg-header{display:flex;align-items:center;gap:8px;margin-bottom:14px}
.wg-badge{padding:4px 9px;background:var(--sec-acc,var(--pri));color:#fff;border-radius:6px;font-family:'Unbounded',sans-serif;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
.wg-title{font-family:'Unbounded',sans-serif;font-size:1.05rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));flex:1}
.wg-help{font-size:.88rem;color:var(--text);margin-bottom:12px;line-height:1.55;background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--sec-acc-soft,var(--pri-soft)));border-left:4px solid var(--warn,#f59e0b);padding:9px 14px;border-radius:9px}
/* BUTTONS */
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.btn:active{transform:scale(.96)}
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
.btn.small{padding:5px 11px;font-size:.78rem}
/* INPUTS */
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);transition:border-color .15s;font-family:'JetBrains Mono',monospace}
.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
/* FEEDBACK */
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
.dark .feedback.ok{color:#86efac}.dark .feedback.fail{color:#fca5a5}
/* SIDEBAR */
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
.sidecard-row b{color:var(--pri);font-weight:700}
.sidecard-row:last-child{margin-bottom:0}
@media(max-width:980px){.col-side{position:static;max-height:none}}
/* XP card */
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(99,102,241,.15);border-radius:6px;overflow:hidden;margin:7px 0}
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
/* SPOILER */
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
.spoiler summary{padding:8px 14px;background:var(--sec-acc-soft,var(--pri-soft));font-weight:700;cursor:pointer;font-size:.88rem;color:var(--sec-acc-d,var(--pri2));list-style:none;display:flex;align-items:center;gap:8px}
.spoiler summary::-webkit-details-marker{display:none}
.spoiler summary::before{content:'+';font-size:1.2rem;font-weight:900;color:var(--sec-acc,var(--pri));width:18px}
.spoiler[open] summary::before{content:'\2212'}
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
.read-wrap{margin-top:18px;display:flex;justify-content:center}
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
/* PLACEHOLDER (для ненаполненных §) */
.m6-placeholder{padding:26px 20px;text-align:center;background:var(--card);border:1.5px dashed var(--border);border-radius:14px;color:var(--muted);font-size:.95rem;margin-bottom:16px}
.m6-placeholder svg{width:34px;height:34px;stroke:var(--pri);opacity:.6;margin-bottom:10px}
/* TABLES */
.tbl{width:100%;border-collapse:collapse;margin:12px 0;font-size:.88rem}
.tbl th,.tbl td{padding:7px 10px;border:1px solid var(--border);text-align:center}
.tbl th{background:var(--sec-acc-soft,var(--pri-soft));color:var(--sec-acc-d,var(--pri2));font-weight:700}
/* ACH popup */
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(79,70,229,.45);z-index:1002;display:none;align-items:center;gap:8px;animation:achIn .45s cubic-bezier(.34,1.56,.64,1) forwards;max-width:340px}
.ach-popup.show{display:flex}
.ach-popup svg{stroke:#fff;fill:none}
@keyframes achIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
/* DRAG & DROP */
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px;transition:border-color .18s,background .18s}
.dnd-pool.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
.dnd-pool.col{flex-direction:column;align-items:stretch}
.dnd-pool.col .dnd-chip{width:auto}
.dnd-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--card);border:1.5px solid var(--border);border-radius:10px;cursor:grab;user-select:none;font-size:.92rem;line-height:1.4;transition:transform .12s,box-shadow .12s,border-color .12s;touch-action:none;max-width:100%}
.dnd-chip:hover{transform:translateY(-1px);border-color:var(--sec-acc,var(--pri));box-shadow:var(--sh)}
.dnd-chip:active{cursor:grabbing}
.dnd-chip.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px rgba(79,70,229,.22);transform:translateY(-1px)}
.dnd-chip.dragging{opacity:.28}
.dnd-chip.placed{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.dnd-chip .dnd-x{padding:0 5px;color:var(--muted);font-weight:700;font-size:1.05rem;border-radius:4px;cursor:pointer}
.dnd-chip .dnd-x:hover{color:var(--bad,var(--fail));background:var(--fail-bg)}
.drop-box{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px;transition:border-color .15s,background .15s}
.drop-box:hover{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em}
.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
.dnd-hint{font-size:.78rem;color:var(--muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
.dnd-hint svg{width:14px;height:14px;flex-shrink:0}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.sliders{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:10px}
.sliders label{display:block;font-size:.92rem;color:var(--muted);background:var(--card);padding:8px 12px;border-radius:8px;border:1px solid var(--border);line-height:1.5}
.sliders label b{font-family:'JetBrains Mono',monospace;font-size:1.05rem;color:var(--sec-acc-d,var(--pri2));margin-left:4px}
.sliders label input[type="range"]{display:block;width:100%;margin-top:6px;accent-color:var(--sec-acc,var(--pri))}
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-bottom:12px;font-size:.92rem}
.score-display b{color:var(--sec-acc-d,var(--pri2));font-size:1.15rem}
.qbox{padding:14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center;min-height:54px}
/* HP-BAR for bosses */
.hp-boss{height:14px;background:rgba(220,38,38,.12);border-radius:9px;overflow:hidden;border:1px solid #fecaca;margin:8px 0}
.hp-boss-fill{height:100%;background:linear-gradient(90deg,#dc2626,#f59e0b);border-radius:9px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.boss-card{padding:16px;background:var(--card);border-radius:12px;border:2px solid var(--bad,#dc2626);margin-bottom:14px}
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.boss-title{font-family:'Unbounded',sans-serif;font-weight:800;color:#b91c1c;font-size:1.04rem;flex:1}
.boss-stage{font-size:.85rem;color:var(--muted)}
.boss-q{font-size:1rem;line-height:1.55;padding:11px 13px;background:var(--card-soft);border-radius:8px;margin-bottom:9px;border-left:3px solid var(--bad,#dc2626)}
/* SIDEBAR DRAWER (mobile) */
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none;animation:fadeIn .18s ease}
.col-side-backdrop.show{display:block}
@media(min-width:981px){#sidebar-btn{display:none}.col-side-backdrop.show{display:none}}
@media(max-width:980px){
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
.col-side.open{transform:none}
}
/* GLOSSARY */
.gloss-term{border-bottom:1.5px dotted var(--sec-acc,var(--pri));cursor:help;color:var(--sec-acc-d,var(--pri2));font-weight:600;padding:0 1px}
.gloss-term:hover{background:var(--sec-acc-soft,var(--pri-soft));border-radius:3px}
.gloss-tip{position:fixed;max-width:320px;padding:11px 14px;background:var(--card);border:1.5px solid var(--sec-acc,var(--pri));border-radius:11px;font-size:.84rem;line-height:1.55;box-shadow:0 12px 32px rgba(0,0,0,.18);z-index:9994;display:none;pointer-events:none;color:var(--text)}
.gloss-tip.show{display:block;animation:tipIn .15s ease}
.gloss-tip b{color:var(--sec-acc-d,var(--pri2));font-size:.92rem}
@keyframes tipIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
/* SEARCH */
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
.search-modal.show{display:flex;animation:fadeIn .15s ease}
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
.search-results{flex:1;overflow-y:auto;padding:6px 0}
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border-left:0;border-right:0;border-top:0;width:100%;color:var(--text)}
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px;background:var(--card-soft,transparent)}
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
/* SVG figure container */
.m6-fig{max-width:520px;margin:10px auto;border-radius:10px}
.m6-fig svg{width:100%;height:auto;display:block}
+389
View File
@@ -0,0 +1,389 @@
/* math6_engine.js — общий движок-плумбинг интерактивного учебника «Математика 6».
*
* Каждая страница-глава объявляет конфиг `window.M6` и (опционально) функции-билдеры
* buildXX(), затем подключает этот файл. Движок строит para-selector, навигацию,
* прогресс/XP/достижения, сайдбар (шпаргалка + подсказка), поиск, глоссарий, тему.
*
* § без билдера автоматически получает заглушку (.m6-placeholder + кнопка прочтения),
* поэтому каркас новой главы = только M6.paras. Кастомные интерактивы § пишутся
* inline-билдерами на странице и пользуются глобальными хелперами этого движка:
* makeCard, secNav, readBtn, feedback, renderMath, fmt, num, addXp, bumpProgress,
* achievement, setupSorter, confetti, goTo, M6icon.
*
* Конфиг window.M6 = {
* slug, lsPrefix, xpKey, paras:[{id,num,name,sub,final?,applied?}],
* achLabels:{}, startAch:[id,text], finalAch:[id,text],
* sidebars:{id:{title,rows:[[k,v]]}}, tips:[{sec,html}],
* glossary:[{term,def,sec,aliases:[]}], searchRows:[[kind,title,desc,sec]],
* builders:{id:fn}, footer
* }
*/
(function () {
'use strict';
if (window.__M6_ENGINE) return;
window.__M6_ENGINE = true;
var M6 = window.M6 || (window.M6 = {});
var LSPRE = function () { return M6.lsPrefix || 'math6'; };
var XPKEY = function () { return M6.xpKey || 'math6_xp'; };
/* ============================================================ STATE */
var STATE = { current: null, progress: {}, achievements: new Map(), xp: 0, level: 1 };
window.M6STATE = STATE;
function paras() { return M6.paras || []; }
function total() { return paras().length || 1; }
function calcLevel(xp) { return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
function _xpForLevel(lv) { return (lv - 1) * (lv - 1) * 100; }
function loadProgress() {
paras().forEach(function (p) { if (STATE.progress[p.id] == null) STATE.progress[p.id] = 0; });
try {
var s = localStorage.getItem(LSPRE() + '_progress');
if (s) Object.assign(STATE.progress, JSON.parse(s));
var a = localStorage.getItem(LSPRE() + '_achievements');
if (a) {
var p = JSON.parse(a);
if (Array.isArray(p)) p.forEach(function (id) { STATE.achievements.set(id, achLabel(id)); });
else if (p && typeof p === 'object') Object.keys(p).forEach(function (id) { STATE.achievements.set(id, (p[id] && p[id] !== id) ? p[id] : achLabel(id)); });
}
STATE.xp = +(localStorage.getItem(XPKEY()) || 0);
STATE.level = calcLevel(STATE.xp);
} catch (e) {}
}
function saveProgress() {
try {
localStorage.setItem(LSPRE() + '_progress', JSON.stringify(STATE.progress));
localStorage.setItem(LSPRE() + '_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem(XPKEY(), String(STATE.xp));
} catch (e) {}
}
function achLabel(id) { return (M6.achLabels && M6.achLabels[id]) || id; }
function bumpProgress(key, delta) {
STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key] || 0) + delta));
saveProgress(); refreshProgressUI();
if (STATE.progress[key] >= 50) markParaRead(key);
if (STATE.progress[key] >= 100) {
achievement(key + '_done');
var fin = paras().filter(function (p) { return p.final; }).map(function (p) { return p.id; });
if (fin.indexOf(key) >= 0 && M6.finalAch) achievement(M6.finalAch[0], M6.finalAch[1]);
}
}
/* ====================================================== SERVER SYNC */
var _markedRead = new Set();
var _pendingProgressBody = null, _progressTimer = null;
function _flushProgress() {
var body = _pendingProgressBody; _pendingProgressBody = null; if (!body) return;
var tok = (window.LS && LS.getToken) ? LS.getToken() : ''; if (!tok) return;
fetch('/api/textbooks/' + M6.slug + '/progress', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + tok }, body: JSON.stringify(body), keepalive: true }).catch(function () {});
}
function _queueProgress(patch) {
_pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch);
if (_progressTimer) clearTimeout(_progressTimer);
_progressTimer = setTimeout(_flushProgress, 600);
}
function markLastPara(id) { _queueProgress({ last_para: id }); }
function markParaRead(id) { if (_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({ mark_read: id }); }
window.addEventListener('beforeunload', _flushProgress);
function loadServerReadState() {
var tok = (window.LS && LS.getToken) ? LS.getToken() : ''; if (!tok) return;
fetch('/api/textbooks/' + M6.slug, { headers: { 'Authorization': 'Bearer ' + tok } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d || !d.progress) return;
(d.progress.read || []).forEach(function (k) { _markedRead.add(k); if ((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100; });
saveProgress(); refreshProgressUI();
}).catch(function () {});
}
/* ============================================================ XP */
function addXp(n, src) {
if (!n) return;
var prev = STATE.level;
STATE.xp = Math.max(0, (STATE.xp || 0) + n);
STATE.level = calcLevel(STATE.xp);
saveProgress(); refreshProgressUI();
if (window.LS && window.LS.xp) window.LS.xp.add(n, (M6.slug || 'math6') + '-' + (src || 'misc'));
if (STATE.level > prev) {
popup('Уровень ' + STATE.level + '!');
if (window.confetti) try { confetti(); } catch (e) {}
}
}
function refreshProgressUI() {
var sum = 0; paras().forEach(function (p) { sum += (STATE.progress[p.id] || 0); });
var tot = Math.round(sum / total());
var f = document.getElementById('hero-hp-fill'); if (f) f.style.width = tot + '%';
var t = document.getElementById('hero-hp-text'); if (t) t.textContent = tot + '% пройдено';
document.querySelectorAll('[data-prog-card]').forEach(function (el) {
var k = el.dataset.progCard; var fl = el.querySelector('.psel-prog-fill'); if (fl) fl.style.width = (STATE.progress[k] || 0) + '%';
});
var xpBadge = document.getElementById('hero-xp-badge');
if (xpBadge) xpBadge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. ' + STATE.level + ' · ' + (STATE.xp || 0) + ' XP';
if (STATE.current && document.getElementById('sidebar-content')) { try { buildSidebar(STATE.current); } catch (e) {} }
}
function popup(text, gold) {
var pop = document.getElementById('ach-popup'); if (!pop) return;
document.getElementById('ach-text').textContent = text;
pop.classList.add('show'); setTimeout(function () { pop.classList.remove('show'); }, gold ? 3300 : 2600);
}
function achievement(id, text) {
if (STATE.achievements.has(id)) return;
if (!text && !(M6.achLabels && M6.achLabels[id])) return; /* неизвестные id игнорируем */
STATE.achievements.set(id, text || achLabel(id));
saveProgress(); popup(text || achLabel(id), true); addXp(20, 'ach-' + id);
}
/* ================================================ SECTIONS */
function buildSections() {
var host = document.getElementById('sections'); if (!host) return; /* статичные секции уже в HTML */
if (host.dataset.built) return; host.dataset.built = '1';
var html = '';
paras().forEach(function (p) {
var wm = p.wm || (p.final ? '★' : (M6.wm || ''));
var numCls = p.final ? ' style="background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri))"' : '';
html += '<section id="sec-' + p.id + '" class="sec" data-watermark="' + wm + '">'
+ '<div class="sec-header"><span class="sec-num"' + numCls + '>' + p.num + '</span><h2 class="sec-h">' + p.name + '</h2></div>'
+ '<div id="' + p.id + '-body"></div></section>';
});
host.innerHTML = html;
}
/* ================================================ PARA SELECTOR/NAV */
function buildParaSelector() {
var g = document.getElementById('psel-grid'); if (!g) return; g.innerHTML = '';
paras().forEach(function (p) {
var card = document.createElement('div');
card.className = 'psel-card' + (p.final ? ' final' : '') + (p.applied ? ' applied' : '');
card.dataset.id = p.id; card.dataset.progCard = p.id;
card.innerHTML = '<div class="psel-num">' + p.num + '</div><div class="psel-name">' + p.name + '</div><div class="psel-prog"><div class="psel-prog-fill"></div></div>';
card.addEventListener('click', function () { goTo(p.id); });
g.appendChild(card);
});
}
var BUILT = new Set();
function ensureBuilt(id) {
if (BUILT.has(id)) return;
var fn = M6.builders && M6.builders[id];
if (fn) { try { fn(); } catch (e) { placeholder(id); } }
else placeholder(id);
BUILT.add(id);
}
function placeholder(id) {
var box = document.getElementById(id + '-body'); if (!box) return;
box.innerHTML = '<div class="m6-placeholder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 6.5v11"/><path d="M5 8a3 3 0 0 1 3-3h0a3 3 0 0 1 3 3v9"/><path d="M19 8a3 3 0 0 0-3-3h0a3 3 0 0 0-3 3v9"/></svg><div>Содержание этого параграфа готовится.</div></div>'
+ readBtn(id);
}
function goTo(id) {
STATE.current = id; ensureBuilt(id);
document.querySelectorAll('.sec').forEach(function (s) { s.classList.remove('active'); });
var el = document.getElementById('sec-' + id); if (el) el.classList.add('active');
document.querySelectorAll('.psel-card').forEach(function (c) { c.classList.toggle('active', c.dataset.id === id); });
buildSidebar(id);
window.scrollTo({ top: 0, behavior: 'smooth' });
if ((STATE.progress[id] || 0) < 10) bumpProgress(id, 10);
if (el && window.renderMathInElement) setTimeout(function () { renderMath(el); }, 0);
setTimeout(function () { try { wrapGlossary(el); } catch (e) {} }, 60);
markLastPara(id);
if (window.innerWidth <= 980) { var side = document.getElementById('col-side'), bk = document.getElementById('col-side-backdrop'); if (side) side.classList.remove('open'); if (bk) bk.classList.remove('show'); }
}
/* ============================================================ SIDEBAR */
function buildSidebar(id) {
var box = document.getElementById('sidebar-content'); if (!box) return;
var SB = M6.sidebars || {};
var sb = SB[id] || SB[Object.keys(SB)[0]] || { title: 'Шпаргалка', rows: [] };
var html = '';
var xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level + 1);
var xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv;
var xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
html += '<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><span class="xp-level">Ур. ' + STATE.level + '</span></div><div class="xp-bar"><div class="xp-fill" style="width:' + xpPct + '%"></div></div><div class="xp-nums"><span>' + STATE.xp + ' XP</span><span>' + xpNext + ' XP</span></div></div>';
html += '<div class="sidecard"><h4>' + sb.title + '</h4>';
(sb.rows || []).forEach(function (r) { html += '<div class="sidecard-row"><b>' + r[0] + '</b>' + (r[1] ? ' — ' + r[1] : '') + '</div>'; });
html += '</div>';
var tips = M6.tips || [];
var tip = tips.filter(function (t) { return t.sec === id; })[0] || tips[0];
if (tip) html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">' + tip.html + '</div></div>';
if (STATE.achievements.size > 0) {
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
Array.from(STATE.achievements.values()).slice(-5).forEach(function (text) { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ' + text + '</div>'; });
html += '</div>';
}
box.innerHTML = html;
if (window.renderMathInElement) try { renderMath(box); } catch (e) {}
}
/* ============================================================ THEME */
function initTheme() {
var key = LSPRE() + '_theme';
var t = localStorage.getItem(key) || localStorage.getItem('theme') || 'light';
if (t === 'dark') document.documentElement.classList.add('dark');
var lab = document.getElementById('theme-lab'); if (lab) lab.textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
var btn = document.getElementById('theme-btn'); if (!btn) return;
btn.addEventListener('click', function () {
document.documentElement.classList.toggle('dark');
var dark = document.documentElement.classList.contains('dark');
localStorage.setItem(key, dark ? 'dark' : 'light'); localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
}
/* ============================================================ HELPERS */
function renderMath(root) {
if (root && window.renderMathInElement) {
try { renderMathInElement(root, { delimiters: [{ left: '$$', right: '$$', display: true }, { left: '$', right: '$', display: false }, { left: '\\[', right: '\\]', display: true }, { left: '\\(', right: '\\)', display: false }], throwOnError: false }); } catch (e) {}
}
}
function feedback(elm, ok, text) { if (!elm) return; elm.className = 'feedback ' + (ok ? 'ok' : 'fail'); elm.innerHTML = text || (ok ? '✓ Верно!' : '✗ Неверно'); elm.style.display = 'block'; try { renderMath(elm); } catch (e) {} }
function num(n) { if (!isFinite(n)) return '?'; if (Number.isInteger(n)) return String(n); return (Math.round(n * 1e9) / 1e9).toString().replace('.', ','); }
function fmt(n) { return num(n); }
var ICONS = {
repeat: '<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
theory: '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
algo: '<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
rule: '<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
example: '<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
oral: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
};
function M6icon(k) { return ICONS[k] || ''; }
function makeCard(kind, title, n, body) {
var labels = { repeat: 'Повторение', theory: 'Теория', algo: 'Алгоритм', rule: 'Правило', example: 'Пример', oral: 'Устно' };
return '<div class="card"><div class="card-header"><div class="card-icon ' + kind + '">' + (ICONS[kind] || '') + '</div><div class="card-title">' + (labels[kind] || '') + (title && title !== labels[kind] ? ' · ' + title : '') + '</div>' + (n ? '<div class="card-num">' + n + '</div>' : '') + '</div><div class="card-body">' + body + '</div></div>';
}
function shortName(id) { var p = paras().filter(function (x) { return x.id === id; })[0]; return p ? p.num : id; }
function secNav(prev, next) {
var h = '<div class="sec-nav">';
h += prev ? '<button class="btn" onclick="goTo(\'' + prev + '\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> ' + shortName(prev) + '</button>' : '<span></span>';
h += next ? '<button class="btn primary" onclick="goTo(\'' + next + '\')">' + shortName(next) + ' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>' : '<span></span>';
h += '</div>'; return h;
}
function readBtn(id, label) {
return '<div class="read-wrap"><button class="btn primary" data-read="' + id + '"><svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> ' + (label || ('Я прочитал ' + shortName(id) + ' (+10 XP)')) + '</button></div>';
}
document.addEventListener('click', function (e) {
var b = e.target.closest && e.target.closest('[data-read]'); if (!b) return;
var id = b.getAttribute('data-read');
addXp(10, id + '-read'); bumpProgress(id, 30);
b.textContent = 'Прочитано! +10 XP'; b.disabled = true; b.style.opacity = .6;
});
/* ============================================================ CONFETTI */
var _cc = null, _cp = [], _craf = null;
function confetti() {
try {
if (/jsdom/i.test(navigator.userAgent || '')) return; /* headless-guard: canvas в jsdom не реализован */
if (!_cc) { _cc = document.createElement('canvas'); _cc.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999'; document.body.appendChild(_cc); }
var c = _cc; c.width = window.innerWidth; c.height = window.innerHeight;
var ctx = c.getContext('2d'); if (!ctx) return;
var colors = ['#4f46e5', '#6366f1', '#0891b2', '#10b981', '#f59e0b', '#e11d48'];
for (var i = 0; i < 80; i++) _cp.push({ x: window.innerWidth / 2 + (Math.random() - .5) * 200, y: window.innerHeight / 2, vx: (Math.random() - .5) * 14, vy: -10 - Math.random() * 10, g: .4, life: 100, color: colors[i % colors.length], r: 4 + Math.random() * 4, rot: 0, vRot: (Math.random() - .5) * .3 });
if (_craf) cancelAnimationFrame(_craf);
function frame() { ctx.clearRect(0, 0, c.width, c.height); _cp = _cp.filter(function (p) { p.x += p.vx; p.y += p.vy; p.vy += p.g; p.life--; p.rot += p.vRot; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillStyle = p.color; ctx.fillRect(-p.r, -p.r / 2, p.r * 2, p.r); ctx.restore(); return p.life > 0 && p.y < c.height + 50; }); if (_cp.length > 0) _craf = requestAnimationFrame(frame); else { ctx.clearRect(0, 0, c.width, c.height); _craf = null; } }
frame();
} catch (e) {}
}
/* ============================================================ SORTER (DnD) */
function setupSorter(cfg) {
var placed = {}; var pool = document.getElementById(cfg.poolId); var scope = document.querySelector(cfg.scopeSelector);
if (!pool || !scope) return { placed: placed, render: function () {}, reset: function () {} };
pool.classList.add('dnd-pool'); if (cfg.columnLayout) pool.classList.add('col');
var armed = null;
function buildChip(it, isPlaced) { var e = document.createElement('div'); e.className = 'dnd-chip' + (isPlaced ? ' placed' : ''); e.dataset.id = it.id; e.innerHTML = '<span class="dnd-txt">' + it.html + '</span><span class="dnd-x" title="Убрать">×</span>'; attach(e, it.id); return e; }
function attach(elm, itId) { elm.addEventListener('pointerdown', function (ev) { if (ev.button !== undefined && ev.button !== 0) return; ev.preventDefault(); if (ev.target.classList && ev.target.classList.contains('dnd-x')) { ev.stopPropagation(); if (placed[itId]) { delete placed[itId]; render(); } else if (armed === itId) { armed = null; render(); } return; } var sx = ev.clientX, sy = ev.clientY; var r = elm.getBoundingClientRect(); var ox = ev.clientX - r.left, oy = ev.clientY - r.top; var ghost = null, dragging = false; try { elm.setPointerCapture(ev.pointerId); } catch (e) {} function onMove(e2) { var dx = e2.clientX - sx, dy = e2.clientY - sy; if (!dragging && Math.hypot(dx, dy) > 8) { dragging = true; ghost = elm.cloneNode(true); ghost.style.cssText = 'position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:' + r.width + 'px;left:' + (e2.clientX - ox) + 'px;top:' + (e2.clientY - oy) + 'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if (dragging && ghost) { ghost.style.left = (e2.clientX - ox) + 'px'; ghost.style.top = (e2.clientY - oy) + 'px'; var under = document.elementsFromPoint(e2.clientX, e2.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(function (n) { n.classList.remove('over'); }); var tgt = under.find(function (n) { return n.classList && (n.classList.contains('drop-box') || n.classList.contains('dnd-pool')); }); if (tgt) tgt.classList.add('over'); } } function onUp(e2) { elm.removeEventListener('pointermove', onMove); elm.removeEventListener('pointerup', onUp); elm.removeEventListener('pointercancel', onUp); elm.classList.remove('dragging'); if (ghost) { ghost.remove(); ghost = null; } scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(function (n) { n.classList.remove('over'); }); if (dragging) { var under = document.elementsFromPoint(e2.clientX, e2.clientY); var box = under.find(function (n) { return n.classList && n.classList.contains('drop-box'); }); var pl = under.find(function (n) { return n.classList && n.classList.contains('dnd-pool'); }); if (box) { var di = box.querySelector('[data-cat]'); if (di) { placed[itId] = di.dataset.cat; armed = null; render(); return; } } else if (pl) { delete placed[itId]; armed = null; render(); return; } } else { if (placed[itId]) { delete placed[itId]; armed = null; render(); } else { armed = (armed === itId) ? null : itId; render(); } } dragging = false; } elm.addEventListener('pointermove', onMove); elm.addEventListener('pointerup', onUp); elm.addEventListener('pointercancel', onUp); }); }
function attachBoxTaps() { scope.querySelectorAll('.drop-box').forEach(function (box) { box.addEventListener('click', function (ev) { if (!armed) return; if (ev.target.closest('.dnd-chip')) return; var di = box.querySelector('[data-cat]'); if (di) { placed[armed] = di.dataset.cat; armed = null; render(); } }); }); }
function render() { pool.innerHTML = ''; cfg.items.forEach(function (it) { if (placed[it.id]) return; var c = buildChip(it, false); if (armed === it.id) c.classList.add('armed'); pool.appendChild(c); }); cfg.cats.forEach(function (cat) { var box = scope.querySelector('.drop-items[data-cat="' + cat + '"]'); if (!box) return; box.innerHTML = ''; cfg.items.forEach(function (it) { if (placed[it.id] !== cat) return; box.appendChild(buildChip(it, true)); }); }); if (window.renderMathInElement) try { renderMath(scope); } catch (e) {} }
attachBoxTaps(); render();
return { placed: placed, render: render, reset: function () { for (var k in placed) delete placed[k]; armed = null; render(); } };
}
/* ============================================================ GLOSSARY */
function wrapGlossary(root) {
var GL = M6.glossary || []; if (!root || root.__glossDone || !GL.length) return;
var allAliases = [];
GL.forEach(function (g, i) { (g.aliases || [g.term]).forEach(function (a) { allAliases.push({ a: a, i: i }); }); });
allAliases.sort(function (x, y) { return y.a.length - x.a.length; });
var re = new RegExp('(?<![\\w\\u0400-\\u04ff-])(' + allAliases.map(function (x) { return x.a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }).join('|') + ')(?![\\w\\u0400-\\u04ff-])', 'iu');
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { var p = node.parentElement; if (!p) return NodeFilter.FILTER_REJECT; if (p.closest('.katex,.gloss-term,button,input,select,.wg-badge,.card-icon,.sec-num,.psel-num,.hdr,.ach-popup,script,style,.search-modal,.sidecard,.gloss-tip,svg')) return NodeFilter.FILTER_REJECT; if (!re.test(node.nodeValue)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } });
var nodes = [], n; while ((n = walker.nextNode())) nodes.push(n);
nodes.forEach(function (node) { var text = node.nodeValue; var out = document.createDocumentFragment(); var cursor = 0; var global = new RegExp(re.source, 'giu'); var m; while ((m = global.exec(text)) !== null) { if (m.index > cursor) out.appendChild(document.createTextNode(text.slice(cursor, m.index))); var found = m[0].toLowerCase(); var hit = allAliases.find(function (x) { return x.a.toLowerCase() === found; }); var g = hit ? GL[hit.i] : null; var sp = document.createElement('span'); sp.className = 'gloss-term'; sp.dataset.gloss = g ? g.term : ''; sp.textContent = m[0]; out.appendChild(sp); cursor = m.index + m[0].length; } if (cursor < text.length) out.appendChild(document.createTextNode(text.slice(cursor))); node.parentNode.replaceChild(out, node); });
root.__glossDone = true;
}
function initGlossaryTip() {
var tip = document.getElementById('gloss-tip'); if (!tip) return;
var GL = M6.glossary || []; var lockOpen = null;
function show(elm) { var g = GL.find(function (x) { return x.term === elm.dataset.gloss; }); if (!g) return; tip.innerHTML = '<b>' + g.term[0].toUpperCase() + g.term.slice(1) + '</b><div style="margin-top:4px">' + g.def + '</div>' + (g.sec ? '<div style="margin-top:6px;font-size:.72rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em">См. ' + shortName(g.sec) + '</div>' : ''); if (window.renderMathInElement) renderMath(tip); var r = elm.getBoundingClientRect(); tip.classList.add('show'); var tw = tip.offsetWidth, th = tip.offsetHeight; var left = r.left, top = r.bottom + 8; if (left + tw > window.innerWidth - 12) left = window.innerWidth - tw - 12; if (top + th > window.innerHeight - 12) top = r.top - th - 8; tip.style.left = Math.max(8, left) + 'px'; tip.style.top = Math.max(8, top) + 'px'; }
function hide() { tip.classList.remove('show'); }
document.addEventListener('mouseover', function (e) { var elm = e.target.closest && e.target.closest('.gloss-term'); if (elm && !lockOpen) show(elm); });
document.addEventListener('mouseout', function (e) { var elm = e.target.closest && e.target.closest('.gloss-term'); if (elm && !lockOpen) hide(); });
document.addEventListener('click', function (e) { var elm = e.target.closest && e.target.closest('.gloss-term'); if (elm) { if (lockOpen === elm) { lockOpen = null; hide(); } else { lockOpen = elm; show(elm); } } else if (lockOpen && !e.target.closest('.gloss-tip')) { lockOpen = null; hide(); } });
}
/* ============================================================ SEARCH */
function buildSearchIndex() {
var arr = [];
paras().forEach(function (p) { arr.push({ kind: p.final ? 'Финал' : 'Параграф', title: p.num + ' ' + p.name, desc: p.sub || '', sec: p.id }); });
(M6.glossary || []).forEach(function (g) { arr.push({ kind: 'Понятие', title: g.term, desc: (g.def || '').replace(/\$/g, ''), sec: g.sec }); });
(M6.searchRows || []).forEach(function (r) { arr.push({ kind: r[0], title: r[1], desc: r[2], sec: r[3] }); });
return arr;
}
function initSearch() {
var modal = document.getElementById('search-modal'), inp = document.getElementById('search-input'), out = document.getElementById('search-results'), btn = document.getElementById('search-btn');
if (!modal || !inp || !out) return;
var INDEX = buildSearchIndex(); var cur = 0, rows = [];
function score(q, it) { var t = (it.title + ' ' + it.desc).toLowerCase(); if (t.includes(q)) return 100 + (it.title.toLowerCase().startsWith(q) ? 50 : 0); var s = 0; q.split(/\s+/).forEach(function (w) { if (w && t.includes(w)) s += 10; }); return s; }
function rank(q) { q = q.trim().toLowerCase(); if (!q) return INDEX.slice(0, 12); return INDEX.map(function (it) { return { it: it, s: score(q, it) }; }).filter(function (x) { return x.s > 0; }).sort(function (a, b) { return b.s - a.s; }).slice(0, 20).map(function (x) { return x.it; }); }
function render() { cur = 0; if (!rows.length) { out.innerHTML = '<div class="search-empty">Ничего не найдено</div>'; return; } out.innerHTML = rows.map(function (r, i) { return '<button class="search-row' + (i === 0 ? ' active' : '') + '" data-i="' + i + '"><div class="sr-kind">' + r.kind + '</div><div class="sr-title">' + r.title + '</div>' + (r.desc ? '<div class="sr-desc">' + (r.desc.length > 90 ? r.desc.slice(0, 90) + '…' : r.desc) + '</div>' : '') + '</button>'; }).join(''); out.querySelectorAll('.search-row').forEach(function (b) { b.addEventListener('click', function () { cur = +b.dataset.i; pick(); }); }); }
function pick() { var r = rows[cur]; if (!r) return; close(); goTo(r.sec); }
function move(d) { var items = out.querySelectorAll('.search-row'); if (!items.length) return; items[cur] && items[cur].classList.remove('active'); cur = (cur + d + items.length) % items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({ block: 'nearest' }); }
function open() { modal.classList.add('show'); inp.value = ''; rows = rank(''); render(); setTimeout(function () { inp.focus(); }, 50); }
function close() { modal.classList.remove('show'); }
btn && btn.addEventListener('click', open);
modal.addEventListener('click', function (e) { if (e.target === modal) close(); });
inp.addEventListener('input', function () { rows = rank(inp.value); render(); });
inp.addEventListener('keydown', function (e) { if (e.key === 'ArrowDown') { e.preventDefault(); move(1); } else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); } else if (e.key === 'Enter') { e.preventDefault(); pick(); } else if (e.key === 'Escape') { e.preventDefault(); close(); } });
document.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) { e.preventDefault(); if (modal.classList.contains('show')) close(); else open(); } });
}
function initSidebarToggle() {
var side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
if (!side || !btn) return;
function open() { side.classList.add('open'); if (back) back.classList.add('show'); }
function close() { side.classList.remove('open'); if (back) back.classList.remove('show'); }
btn.addEventListener('click', function () { if (side.classList.contains('open')) close(); else open(); });
if (back) back.addEventListener('click', close);
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
}
/* ============================================================ INIT */
function init() {
M6 = window.M6 || M6; /* перечитываем конфиг (порядок выполнения скриптов мог опередить объявление M6) */
loadProgress(); initTheme(); initSidebarToggle(); initGlossaryTip(); initSearch();
buildSections(); buildParaSelector(); refreshProgressUI(); loadServerReadState();
var first = (paras()[0] || {}).id; if (first) goTo(first);
if (M6.startAch) setTimeout(function () { achievement(M6.startAch[0], M6.startAch[1]); }, 600);
var foot = document.getElementById('m6-foot'); if (foot && M6.footer) foot.textContent = M6.footer;
if (window.LS && window.LS.xp) {
window.LS.xp.load().then(function (s) { if (s && s.xp > STATE.xp) { STATE.xp = s.xp; STATE.level = calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if (STATE.current) buildSidebar(STATE.current); } }).catch(function () {});
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
/* ============================================================ EXPORTS (для inline-билдеров) */
window.goTo = goTo;
window.makeCard = makeCard;
window.secNav = secNav;
window.readBtn = readBtn;
window.feedback = feedback;
window.renderMath = renderMath;
window.fmt = fmt; window.num = num;
window.addXp = addXp;
window.bumpProgress = bumpProgress;
window.achievement = achievement;
window.setupSorter = setupSorter;
window.confetti = confetti;
window.M6icon = M6icon;
window.M6engine = { goTo: goTo, ensureBuilt: ensureBuilt, refreshProgressUI: refreshProgressUI, buildSidebar: buildSidebar };
})();
+150
View File
@@ -0,0 +1,150 @@
/* 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 });
};
})();
+106
View File
@@ -0,0 +1,106 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Математика 6 · Глава 1 · Десятичные дроби</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/math6.css">
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/math6_svg.js" defer></script>
<script src="/js/math6_engine.js" defer></script>
<style>:root{--pri:#4f46e5;--pri2:#3730a3;--pri-soft:#e0e7ff;--acc:#6366f1;--acc2:#4f46e5;--acc-soft:#eef2ff}</style>
</head>
<body>
<header class="hdr" data-wm="0,5">
<div class="hdr-row">
<div>
<h1>Математика 6 · Глава 1</h1>
<div class="hdr-sub">Десятичные дроби: запись и разряды · сравнение и округление · действия · бесконечные дроби</div>
</div>
<div class="hdr-side">
<a href="/textbook/math-6" class="hdr-btn" title="Ко всем главам"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К математике 6</a>
<button id="search-btn" class="hdr-btn" title="Поиск (Ctrl+K)"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> <span>Поиск</span></button>
<button id="sidebar-btn" class="hdr-btn" title="Шпаргалка"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn" title="Сменить тему"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg> <span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero" data-wm="0,5">
<h2>Десятичные дроби</h2>
<p>Десятичная дробь — это привычная запись чисел с запятой: цена, рост, масса, показания приборов. В этой главе мы научимся <b>читать и записывать</b> десятичные дроби по разрядам, <b>сравнивать и округлять</b> их, отмечать на координатном луче и выполнять <b>все четыре действия</b> — вплоть до деления на десятичную дробь и перевода обыкновенных дробей в десятичные.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 1</button>
<div class="hero-progress"><span class="hp-label">Прогресс по главе</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
</div>
</section>
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
<div id="sections"></div>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot" id="m6-foot">Интерактивный учебник «Математика 6» · Глава 1 · Десятичные дроби · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog" aria-label="Поиск по главе">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск: понятие, действие, параграф…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>&#8593;&#8595;</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
window.M6 = {
slug: 'math-6-ch1', lsPrefix: 'math6_ch1', xpKey: 'math6_xp', wm: '0,5',
paras: [
{ id:'p1', num:'§ 1', name:'Десятичная запись дробей. Разряды', sub:'Как устроена запись с запятой' },
{ id:'p2', num:'§ 2', name:'Сравнение и округление', sub:'Что больше и как округлить' },
{ id:'p3', num:'§ 3', name:'Изображение на координатном луче', sub:'Дробь как точка на луче' },
{ id:'p4', num:'§ 4', name:'Сложение и вычитание', sub:'Запятая под запятой' },
{ id:'p5', num:'§ 5', name:'Умножение и деление на 10, 100, 1000', sub:'Сдвиг запятой' },
{ id:'p6', num:'§ 6', name:'Умножение десятичных дробей', sub:'Считаем знаки после запятой' },
{ id:'p7', num:'§ 7', name:'Деление на натуральное число', sub:'Деление уголком' },
{ id:'p8', num:'§ 8', name:'Деление на десятичную дробь', sub:'Переносим запятую' },
{ id:'p9', num:'§ 9', name:'Конечные и бесконечные дроби', sub:'Период дроби' },
{ id:'p10', num:'§ 10', name:'Преобразования выражений', sub:'Обыкновенные и десятичные вместе' },
{ id:'app', num:'§ 12', name:'Математика вокруг нас', sub:'Десятичные дроби в жизни', applied:true },
{ id:'final', num:'★', name:'Финал главы', sub:'Тест · боссы главы 1', final:true }
],
achLabels: {
start:'Начало главы 1!', p1_done:'Разряды освоены!', p2_done:'Сравнение и округление — легко!',
p3_done:'Координатный луч покорён!', p4_done:'Сложение и вычитание — точно!', p5_done:'Сдвиг запятой освоен!',
p6_done:'Умножение десятичных — мастер!', p7_done:'Деление уголком — уверенно!', p8_done:'Деление на дробь — без страха!',
p9_done:'Период дроби найден!', p10_done:'Преобразования — на отлично!', app_done:'Математика вокруг нас!',
ch1_done:'Глава 1 пройдена!'
},
startAch: ['start','Начало главы 1!'],
finalAch: ['ch1_done','Глава 1 пройдена!'],
sidebars: {}, tips: [], glossary: [], builders: {},
footer: 'Интерактивный учебник «Математика 6» · Глава 1 · Десятичные дроби · LearnSpace'
};
</script>
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Математика 6 · Глава 2 · Проценты и пропорции</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/math6.css">
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/math6_svg.js" defer></script>
<script src="/js/math6_engine.js" defer></script>
<style>:root{--pri:#0891b2;--pri2:#0e7490;--pri-soft:#cffafe;--acc:#06b6d4;--acc2:#0891b2;--acc-soft:#ecfeff}</style>
</head>
<body>
<header class="hdr" data-wm="%">
<div class="hdr-row">
<div>
<h1>Математика 6 · Глава 2</h1>
<div class="hdr-sub">Проценты · задачи на проценты · пропорция · прямая и обратная зависимости · масштаб · диаграммы</div>
</div>
<div class="hdr-side">
<a href="/textbook/math-6" class="hdr-btn" title="Ко всем главам"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К математике 6</a>
<button id="search-btn" class="hdr-btn" title="Поиск (Ctrl+K)"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> <span>Поиск</span></button>
<button id="sidebar-btn" class="hdr-btn" title="Шпаргалка"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn" title="Сменить тему"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg> <span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero" data-wm="%">
<h2>Проценты и пропорции</h2>
<p>Процент — это сотая доля. Скидки, налоги, банковские вклады, состав сплавов и растворов — всё это язык процентов и пропорций. В этой главе мы разберём <b>три основные задачи на проценты</b>, освоим <b>пропорцию и её главное свойство</b>, научимся отличать <b>прямую и обратную</b> пропорциональные зависимости, поработаем с <b>масштабом</b> и построим <b>круговые диаграммы</b>.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 1</button>
<div class="hero-progress"><span class="hp-label">Прогресс по главе</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
</div>
</section>
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
<div id="sections"></div>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot" id="m6-foot">Интерактивный учебник «Математика 6» · Глава 2 · Проценты и пропорции · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog" aria-label="Поиск по главе">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск: понятие, действие, параграф…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>&#8593;&#8595;</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
window.M6 = {
slug: 'math-6-ch2', lsPrefix: 'math6_ch2', xpKey: 'math6_xp', wm: '%',
paras: [
{ id:'p1', num:'§ 1', name:'Проценты', sub:'Сотая доля' },
{ id:'p2', num:'§ 2', name:'Основные задачи на проценты', sub:'Три типа задач' },
{ id:'p3', num:'§ 3', name:'Пропорция и её свойства', sub:'Основное свойство пропорции' },
{ id:'p4', num:'§ 4', name:'Прямая и обратная зависимости', sub:'Как связаны величины' },
{ id:'p5', num:'§ 5', name:'Решение задач с помощью пропорций', sub:'Пропорция как инструмент' },
{ id:'p6', num:'§ 6', name:'Масштаб', sub:'Карты и чертежи' },
{ id:'p7', num:'§ 7', name:'Круговые диаграммы', sub:'Доли в виде секторов' },
{ id:'app', num:'§ 9', name:'Математика вокруг нас', sub:'Проценты и пропорции в жизни', applied:true },
{ id:'final', num:'★', name:'Финал главы', sub:'Тест · боссы главы 2', final:true }
],
achLabels: {
start:'Начало главы 2!', p1_done:'Проценты понятны!', p2_done:'Три задачи на проценты — решены!',
p3_done:'Пропорция освоена!', p4_done:'Зависимости различаю!', p5_done:'Решаю задачи пропорцией!',
p6_done:'Масштаб покорён!', p7_done:'Круговые диаграммы — мастер!', app_done:'Математика вокруг нас!',
ch2_done:'Глава 2 пройдена!'
},
startAch: ['start','Начало главы 2!'],
finalAch: ['ch2_done','Глава 2 пройдена!'],
sidebars: {}, tips: [], glossary: [], builders: {},
footer: 'Интерактивный учебник «Математика 6» · Глава 2 · Проценты и пропорции · LearnSpace'
};
</script>
</body>
</html>
+96
View File
@@ -0,0 +1,96 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Математика 6 · Глава 3 · Множество</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/math6.css">
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/math6_svg.js" defer></script>
<script src="/js/math6_engine.js" defer></script>
<style>:root{--pri:#7c3aed;--pri2:#6d28d9;--pri-soft:#ede9fe;--acc:#8b5cf6;--acc2:#7c3aed;--acc-soft:#f5f3ff}</style>
</head>
<body>
<header class="hdr" data-wm="">
<div class="hdr-row">
<div>
<h1>Математика 6 · Глава 3</h1>
<div class="hdr-sub">Множество и его элементы · способы задания · пересечение и объединение · круги Эйлера</div>
</div>
<div class="hdr-side">
<a href="/textbook/math-6" class="hdr-btn" title="Ко всем главам"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К математике 6</a>
<button id="search-btn" class="hdr-btn" title="Поиск (Ctrl+K)"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> <span>Поиск</span></button>
<button id="sidebar-btn" class="hdr-btn" title="Шпаргалка"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn" title="Сменить тему"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg> <span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero" data-wm="">
<h2>Множество</h2>
<p>Множество — одно из самых простых и самых важных понятий математики: это набор объектов, объединённых общим признаком. Мы научимся <b>задавать множества</b> перечислением и свойством, находить их <b>пересечение и объединение</b> и решать жизненные задачи с помощью наглядных <b>кругов Эйлера</b>.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 1</button>
<div class="hero-progress"><span class="hp-label">Прогресс по главе</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
</div>
</section>
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
<div id="sections"></div>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot" id="m6-foot">Интерактивный учебник «Математика 6» · Глава 3 · Множество · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog" aria-label="Поиск по главе">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск: понятие, действие, параграф…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>&#8593;&#8595;</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
window.M6 = {
slug: 'math-6-ch3', lsPrefix: 'math6_ch3', xpKey: 'math6_xp', wm: '',
paras: [
{ id:'p1', num:'§ 1', name:'Множество. Элементы. Пустое множество', sub:'∈, ∉, ∅' },
{ id:'p2', num:'§ 2', name:'Способы задания множеств', sub:'Перечисление и свойство' },
{ id:'p3', num:'§ 3', name:'Операции над множествами', sub:'Пересечение и объединение' },
{ id:'p4', num:'§ 4', name:'Круги Эйлера', sub:'Решение задач с кругами Эйлера' },
{ id:'final', num:'★', name:'Финал главы', sub:'Тест · боссы главы 3', final:true }
],
achLabels: {
start:'Начало главы 3!', p1_done:'Множества и элементы — ясно!', p2_done:'Задаю множества двумя способами!',
p3_done:'Пересечение и объединение освоены!', p4_done:'Круги Эйлера — мастер!', ch3_done:'Глава 3 пройдена!'
},
startAch: ['start','Начало главы 3!'],
finalAch: ['ch3_done','Глава 3 пройдена!'],
sidebars: {}, tips: [], glossary: [], builders: {},
footer: 'Интерактивный учебник «Математика 6» · Глава 3 · Множество · LearnSpace'
};
</script>
</body>
</html>
+104
View File
@@ -0,0 +1,104 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Математика 6 · Глава 4 · Рациональные числа</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/math6.css">
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/math6_svg.js" defer></script>
<script src="/js/math6_engine.js" defer></script>
<style>:root{--pri:#e11d48;--pri2:#be123c;--pri-soft:#ffe4e6;--acc:#f43f5e;--acc2:#e11d48;--acc-soft:#fff1f2}</style>
</head>
<body>
<header class="hdr" data-wm="5">
<div class="hdr-row">
<div>
<h1>Математика 6 · Глава 4</h1>
<div class="hdr-sub">Положительные и отрицательные числа · модуль · множества Z и Q · действия и законы</div>
</div>
<div class="hdr-side">
<a href="/textbook/math-6" class="hdr-btn" title="Ко всем главам"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К математике 6</a>
<button id="search-btn" class="hdr-btn" title="Поиск (Ctrl+K)"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> <span>Поиск</span></button>
<button id="sidebar-btn" class="hdr-btn" title="Шпаргалка"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn" title="Сменить тему"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg> <span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero" data-wm="5">
<h2>Рациональные числа</h2>
<p>Температура ниже нуля, долги, координаты влево от нуля — так в математику приходят <b>отрицательные числа</b>. В этой главе мы построим <b>координатную прямую</b>, познакомимся с <b>модулем</b> и противоположными числами, множествами целых $\mathbb{Z}$ и рациональных $\mathbb{Q}$ чисел, и научимся выполнять <b>все действия</b> с рациональными числами по правилам знаков.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 1</button>
<div class="hero-progress"><span class="hp-label">Прогресс по главе</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
</div>
</section>
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
<div id="sections"></div>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot" id="m6-foot">Интерактивный учебник «Математика 6» · Глава 4 · Рациональные числа · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog" aria-label="Поиск по главе">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск: понятие, действие, параграф…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>&#8593;&#8595;</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
window.M6 = {
slug: 'math-6-ch4', lsPrefix: 'math6_ch4', xpKey: 'math6_xp', wm: '5',
paras: [
{ id:'p1', num:'§ 1', name:'Положительные и отрицательные числа. Координатная прямая', sub:'Числа со знаком' },
{ id:'p2', num:'§ 2', name:'Модуль. Противоположные числа. Z и Q', sub:'|x| и множества чисел' },
{ id:'p3', num:'§ 3', name:'Сравнение рациональных чисел', sub:'Что больше на прямой' },
{ id:'p4', num:'§ 4', name:'Сложение рациональных чисел', sub:'Правила сложения' },
{ id:'p5', num:'§ 5', name:'Вычитание рациональных чисел', sub:'Прибавляем противоположное' },
{ id:'p6', num:'§ 6', name:'Законы сложения', sub:'Удобный порядок действий' },
{ id:'p7', num:'§ 7', name:'Умножение рациональных чисел', sub:'Знаки при умножении' },
{ id:'p8', num:'§ 8', name:'Деление рациональных чисел', sub:'Знаки при делении' },
{ id:'p9', num:'§ 9', name:'Задачи на все действия', sub:'Порядок действий' },
{ id:'app', num:'§ 11', name:'Математика вокруг нас', sub:'Отрицательные числа в жизни', applied:true },
{ id:'final', num:'★', name:'Финал главы', sub:'Тест · боссы главы 4', final:true }
],
achLabels: {
start:'Начало главы 4!', p1_done:'Числа со знаком — ясно!', p2_done:'Модуль и Z, Q освоены!',
p3_done:'Сравниваю рациональные!', p4_done:'Сложение освоено!', p5_done:'Вычитание — точно!',
p6_done:'Законы сложения работают!', p7_done:'Умножение знаков — мастер!', p8_done:'Деление знаков — уверенно!',
p9_done:'Все действия — на отлично!', app_done:'Математика вокруг нас!', ch4_done:'Глава 4 пройдена!'
},
startAch: ['start','Начало главы 4!'],
finalAch: ['ch4_done','Глава 4 пройдена!'],
sidebars: {}, tips: [], glossary: [], builders: {},
footer: 'Интерактивный учебник «Математика 6» · Глава 4 · Рациональные числа · LearnSpace'
};
</script>
</body>
</html>
+96
View File
@@ -0,0 +1,96 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Математика 6 · Глава 5 · Координатная плоскость</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/math6.css">
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/math6_svg.js" defer></script>
<script src="/js/math6_engine.js" defer></script>
<style>:root{--pri:#059669;--pri2:#047857;--pri-soft:#d1fae5;--acc:#10b981;--acc2:#059669;--acc-soft:#ecfdf5}</style>
</head>
<body>
<header class="hdr" data-wm="xy">
<div class="hdr-row">
<div>
<h1>Математика 6 · Глава 5</h1>
<div class="hdr-sub">Декартова система координат · графики реальных процессов · графики зависимостей</div>
</div>
<div class="hdr-side">
<a href="/textbook/math-6" class="hdr-btn" title="Ко всем главам"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К математике 6</a>
<button id="search-btn" class="hdr-btn" title="Поиск (Ctrl+K)"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> <span>Поиск</span></button>
<button id="sidebar-btn" class="hdr-btn" title="Шпаргалка"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn" title="Сменить тему"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg> <span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero" data-wm="xy">
<h2>Координатная плоскость</h2>
<p>Две пересекающиеся под прямым углом числовые прямые превращают плоскость в карту, где у каждой точки есть свой «адрес» — пара координат $(x;\,y)$. Мы научимся <b>ставить и читать точки</b>, понимать <b>графики реальных процессов</b> (движение, температура) и строить <b>графики прямой и обратной</b> пропорциональной зависимости.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 1</button>
<div class="hero-progress"><span class="hp-label">Прогресс по главе</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
</div>
</section>
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
<div id="sections"></div>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot" id="m6-foot">Интерактивный учебник «Математика 6» · Глава 5 · Координатная плоскость · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog" aria-label="Поиск по главе">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск: понятие, действие, параграф…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>&#8593;&#8595;</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
window.M6 = {
slug: 'math-6-ch5', lsPrefix: 'math6_ch5', xpKey: 'math6_xp', wm: 'xy',
paras: [
{ id:'p1', num:'§ 1', name:'Прямоугольная (декартова) система координат', sub:'Точка (x; y) и четверти' },
{ id:'p2', num:'§ 2', name:'График. Графики реальных процессов', sub:'Чтение графиков' },
{ id:'p3', num:'§ 3', name:'Графики прямой и обратной пропорциональности', sub:'y = kx и y = k/x' },
{ id:'app', num:'§ 5', name:'Математика вокруг нас', sub:'Графики в жизни', applied:true },
{ id:'final', num:'★', name:'Финал главы', sub:'Тест · боссы главы 5', final:true }
],
achLabels: {
start:'Начало главы 5!', p1_done:'Координаты точек освоены!', p2_done:'Читаю графики процессов!',
p3_done:'Графики зависимостей — мастер!', app_done:'Математика вокруг нас!', ch5_done:'Глава 5 пройдена!'
},
startAch: ['start','Начало главы 5!'],
finalAch: ['ch5_done','Глава 5 пройдена!'],
sidebars: {}, tips: [], glossary: [], builders: {},
footer: 'Интерактивный учебник «Математика 6» · Глава 5 · Координатная плоскость · LearnSpace'
};
</script>
</body>
</html>
+98
View File
@@ -0,0 +1,98 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Математика 6 · Глава 6 · Наглядная геометрия</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/math6.css">
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/math6_svg.js" defer></script>
<script src="/js/math6_engine.js" defer></script>
<style>:root{--pri:#d97706;--pri2:#b45309;--pri-soft:#fef3c7;--acc:#f59e0b;--acc2:#d97706;--acc-soft:#fffbeb}</style>
</head>
<body>
<header class="hdr" data-wm="△">
<div class="hdr-row">
<div>
<h1>Математика 6 · Глава 6</h1>
<div class="hdr-sub">Тела и развёртки · окружность и круг · виды треугольников · симметрия</div>
</div>
<div class="hdr-side">
<a href="/textbook/math-6" class="hdr-btn" title="Ко всем главам"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К математике 6</a>
<button id="search-btn" class="hdr-btn" title="Поиск (Ctrl+K)"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> <span>Поиск</span></button>
<button id="sidebar-btn" class="hdr-btn" title="Шпаргалка"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn" title="Сменить тему"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg> <span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero" data-wm="△">
<h2>Наглядная геометрия</h2>
<p>Геометрия начинается с наблюдения. Мы рассмотрим <b>пространственные тела</b> и их <b>развёртки</b>, выведем формулы <b>длины окружности</b> $C=2\pi r$ и <b>площади круга</b> $S=\pi r^2$, научимся различать <b>виды треугольников</b> по сторонам и углам и построим фигуры, <b>симметричные</b> относительно точки и прямой.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 1</button>
<div class="hero-progress"><span class="hp-label">Прогресс по главе</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
</div>
</section>
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
<div id="sections"></div>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot" id="m6-foot">Интерактивный учебник «Математика 6» · Глава 6 · Наглядная геометрия · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog" aria-label="Поиск по главе">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск: понятие, фигура, параграф…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>&#8593;&#8595;</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
window.M6 = {
slug: 'math-6-ch6', lsPrefix: 'math6_ch6', xpKey: 'math6_xp', wm: '△',
paras: [
{ id:'p1', num:'§ 1', name:'Наглядные представления тел. Развёртки', sub:'Куб, призма, пирамида, цилиндр, конус' },
{ id:'p2', num:'§ 2', name:'Окружность. Круг. Длина и площадь', sub:'C = 2πr, S = πr²' },
{ id:'p3', num:'§ 3', name:'Виды треугольников', sub:'По сторонам и по углам' },
{ id:'p4', num:'§ 4', name:'Симметрия относительно точки', sub:'Центральная симметрия' },
{ id:'p5', num:'§ 5', name:'Фигуры, симметричные относительно прямой', sub:'Осевая симметрия' },
{ id:'final', num:'★', name:'Финал главы', sub:'Тест · боссы главы 6', final:true }
],
achLabels: {
start:'Начало главы 6!', p1_done:'Тела и развёртки — ясно!', p2_done:'Окружность и круг покорены!',
p3_done:'Виды треугольников различаю!', p4_done:'Центральная симметрия освоена!', p5_done:'Осевая симметрия — мастер!',
ch6_done:'Глава 6 пройдена!'
},
startAch: ['start','Начало главы 6!'],
finalAch: ['ch6_done','Глава 6 пройдена!'],
sidebars: {}, tips: [], glossary: [], builders: {},
footer: 'Интерактивный учебник «Математика 6» · Глава 6 · Наглядная геометрия · LearnSpace'
};
</script>
</body>
</html>
+290
View File
@@ -0,0 +1,290 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Математика 6 класс — учебник</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<style>
:root{
--bg:#f5f6ff; --card:#fff;
--text:#16162e; --muted:#5b5b78;
--border:#e4e6f7;
--pri:#4f46e5; --pri-d:#3730a3;
--pri-soft:#e0e7ff;
--c1:#4f46e5; --c1-d:#3730a3; /* гл.1 — indigo */
--c2:#0891b2; --c2-d:#0e7490; /* гл.2 — cyan */
--c3:#7c3aed; --c3-d:#6d28d9; /* гл.3 — violet */
--c4:#e11d48; --c4-d:#be123c; /* гл.4 — rose */
--c5:#059669; --c5-d:#047857; /* гл.5 — emerald */
--c6:#d97706; --c6-d:#b45309; /* гл.6 — amber */
--sh:0 4px 16px rgba(79,70,229,.10);
--sh-h:0 12px 36px rgba(79,70,229,.18);
}
html.dark{
--bg:#0a0a16; --card:#13131f;
--text:#e8eaf6; --muted:#9aa0c0;
--border:#26263a;
--pri-soft:rgba(79,70,229,.16);
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(110deg,#312e81 0%,#4f46e5 55%,#818cf8 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(199,210,254,.18)}
.hdr::before{content:'6';position:absolute;right:24px;top:-30%;font-family:'Unbounded',sans-serif;font-size:clamp(7rem,20vw,15rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(224,231,255,.16);line-height:1;pointer-events:none;user-select:none}
.hdr-inner{position:relative;z-index:1;max-width:1100px;margin:0 auto;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.hdr-back{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(255,255,255,.16);border-radius:9px;color:#fff;text-decoration:none;font-size:.85rem;font-weight:600;transition:background .15s}
.hdr-back:hover{background:rgba(255,255,255,.26)}
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.85rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.9;margin-top:4px}
.hdr-side{margin-left:auto;display:flex;gap:8px}
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.16);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit}
.hdr-btn:hover{background:rgba(255,255,255,.26)}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
.prog-overall{background:linear-gradient(135deg,var(--pri-soft),rgba(124,58,237,.10));border:1px solid var(--border);border-radius:14px;padding:14px 18px;margin-bottom:28px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.po-icon{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:900}
.po-text{flex:1;min-width:160px}
.po-label{font-size:.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
.po-bar{height:8px;background:rgba(79,70,229,.14);border-radius:5px;overflow:hidden;margin-top:6px}
.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#7c3aed);border-radius:5px;transition:width .5s}
.po-xp{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,#f59e0b,var(--pri));color:#fff;border-radius:99px;font-size:.8rem;font-weight:800;font-family:'Unbounded',sans-serif;letter-spacing:.02em;box-shadow:0 4px 12px rgba(79,70,229,.22)}
.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
@media(min-width:620px){.ch-grid{grid-template-columns:1fr 1fr}}
@media(min-width:980px){.ch-grid{grid-template-columns:1fr 1fr 1fr}}
.ch-card{background:var(--card);border:1.5px solid var(--border);border-radius:18px;overflow:hidden;display:flex;flex-direction:column;transition:transform .2s,box-shadow .2s,border-color .2s;cursor:pointer;text-decoration:none;color:inherit}
.ch-card:hover{transform:translateY(-4px);box-shadow:var(--sh-h)}
.ch-cover{padding:22px 22px 18px;color:#fff;position:relative;overflow:hidden;min-height:118px}
.ch-cover-wm{position:absolute;right:2px;top:-14px;font-size:4.2rem;font-weight:900;font-family:'Unbounded',sans-serif;line-height:1;color:rgba(255,255,255,.20);pointer-events:none;letter-spacing:-.03em}
.ch-num{display:inline-block;padding:4px 10px;background:rgba(255,255,255,.24);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;position:relative;z-index:1}
.ch-title{font-family:'Outfit',sans-serif;font-size:1.1rem;font-weight:800;letter-spacing:-.01em;position:relative;z-index:1;line-height:1.3}
.ch-range{font-size:.84rem;opacity:.92;margin-top:4px;position:relative;z-index:1;font-weight:500}
.ch-cover.cc1{background:linear-gradient(135deg,#312e81,#4f46e5 60%,#818cf8)}
.ch-cover.cc2{background:linear-gradient(135deg,#164e63,#0891b2 60%,#22d3ee)}
.ch-cover.cc3{background:linear-gradient(135deg,#4c1d95,#7c3aed 60%,#a78bfa)}
.ch-cover.cc4{background:linear-gradient(135deg,#881337,#e11d48 60%,#fb7185)}
.ch-cover.cc5{background:linear-gradient(135deg,#064e3b,#059669 60%,#34d399)}
.ch-cover.cc6{background:linear-gradient(135deg,#92400e,#d97706 60%,#fbbf24)}
.ch-body{padding:16px 20px 18px;display:flex;flex-direction:column;flex:1}
.ch-desc{font-size:.88rem;color:var(--text);opacity:.84;flex:1;margin-bottom:12px;line-height:1.55}
.ch-prog{margin-bottom:12px}
.ch-prog-label{display:flex;justify-content:space-between;font-size:.74rem;color:var(--muted);font-weight:600;margin-bottom:4px}
.ch-prog-bar{height:6px;background:rgba(0,0,0,.07);border-radius:4px;overflow:hidden}
.ch-prog-fill{height:100%;border-radius:4px;transition:width .5s}
.ch-card.k1 .ch-prog-fill{background:linear-gradient(90deg,var(--c1),var(--c1-d))}
.ch-card.k2 .ch-prog-fill{background:linear-gradient(90deg,var(--c2),var(--c2-d))}
.ch-card.k3 .ch-prog-fill{background:linear-gradient(90deg,var(--c3),var(--c3-d))}
.ch-card.k4 .ch-prog-fill{background:linear-gradient(90deg,var(--c4),var(--c4-d))}
.ch-card.k5 .ch-prog-fill{background:linear-gradient(90deg,var(--c5),var(--c5-d))}
.ch-card.k6 .ch-prog-fill{background:linear-gradient(90deg,var(--c6),var(--c6-d))}
.ch-action{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-radius:11px;font-weight:700;font-size:.9rem;color:#fff;transition:filter .15s}
.ch-action:hover{filter:brightness(1.08)}
.ch-card.k1 .ch-action{background:linear-gradient(135deg,var(--c1),#818cf8)}
.ch-card.k2 .ch-action{background:linear-gradient(135deg,var(--c2),#22d3ee)}
.ch-card.k3 .ch-action{background:linear-gradient(135deg,var(--c3),#a78bfa)}
.ch-card.k4 .ch-action{background:linear-gradient(135deg,var(--c4),#fb7185)}
.ch-card.k5 .ch-action{background:linear-gradient(135deg,var(--c5),#34d399)}
.ch-card.k6 .ch-action{background:linear-gradient(135deg,var(--c6),#fbbf24)}
.ach-strip{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:18px 22px;margin-bottom:28px;display:flex;align-items:center;gap:16px;transition:border-color .4s,box-shadow .4s}
.ach-strip.lit{border-color:#f59e0b;box-shadow:0 0 0 3px rgba(245,158,11,.18)}
.ach-icon{width:52px;height:52px;border-radius:14px;background:rgba(0,0,0,.06);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .4s}
.ach-strip.lit .ach-icon{background:linear-gradient(135deg,#fbbf24,#f59e0b)}
.ach-icon svg{width:28px;height:28px;stroke:var(--muted);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ach-strip.lit .ach-icon svg{stroke:#fff}
.ach-text{flex:1}
.ach-title{font-weight:800;font-size:1.02rem;color:var(--text)}
.ach-sub{font-size:.85rem;color:var(--muted);margin-top:2px}
.ach-strip.lit .ach-title{color:#92400e}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbooks" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К каталогу
</a>
</div>
<div>
<h1>Математика — 6 класс</h1>
<div class="hdr-sub">Десятичные дроби · проценты · множества · рациональные числа · координаты · геометрия</div>
</div>
<div class="hdr-side">
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
<svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
<span id="theme-lab">Тёмная</span>
</button>
</div>
</div>
</header>
<main>
<section class="prog-overall">
<div class="po-icon"></div>
<div class="po-text">
<div class="po-label">Общий прогресс по курсу</div>
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
</div>
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
</section>
<div class="ch-grid">
<a href="/textbook/math-6-ch1" class="ch-card k1" id="ch-1">
<div class="ch-cover cc1"><div class="ch-cover-wm">0,5</div>
<div class="ch-num">Глава 1</div>
<div class="ch-title">Десятичные дроби</div>
<div class="ch-range">§1–§12</div>
</div>
<div class="ch-body">
<div class="ch-desc">Запись и разряды, сравнение и округление, координатный луч, все действия, бесконечные дроби, преобразования выражений.</div>
<div class="ch-prog"><div class="ch-prog-label"><span>Прогресс</span><span id="prog-1">0%</span></div><div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-1" style="width:0%"></div></div></div>
<div class="ch-action"><span id="btn-1">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
</a>
<a href="/textbook/math-6-ch2" class="ch-card k2" id="ch-2">
<div class="ch-cover cc2"><div class="ch-cover-wm">%</div>
<div class="ch-num">Глава 2</div>
<div class="ch-title">Проценты и пропорции</div>
<div class="ch-range">§1–§9</div>
</div>
<div class="ch-body">
<div class="ch-desc">Проценты, три основные задачи, пропорция и её свойство, прямая и обратная зависимости, масштаб, круговые диаграммы.</div>
<div class="ch-prog"><div class="ch-prog-label"><span>Прогресс</span><span id="prog-2">0%</span></div><div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-2" style="width:0%"></div></div></div>
<div class="ch-action"><span id="btn-2">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
</a>
<a href="/textbook/math-6-ch3" class="ch-card k3" id="ch-3">
<div class="ch-cover cc3"><div class="ch-cover-wm"></div>
<div class="ch-num">Глава 3</div>
<div class="ch-title">Множество</div>
<div class="ch-range">§1–§5</div>
</div>
<div class="ch-body">
<div class="ch-desc">Множество и его элементы, пустое множество, способы задания, пересечение и объединение, круги Эйлера.</div>
<div class="ch-prog"><div class="ch-prog-label"><span>Прогресс</span><span id="prog-3">0%</span></div><div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-3" style="width:0%"></div></div></div>
<div class="ch-action"><span id="btn-3">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
</a>
<a href="/textbook/math-6-ch4" class="ch-card k4" id="ch-4">
<div class="ch-cover cc4"><div class="ch-cover-wm">5</div>
<div class="ch-num">Глава 4</div>
<div class="ch-title">Рациональные числа</div>
<div class="ch-range">§1–§11</div>
</div>
<div class="ch-body">
<div class="ch-desc">Отрицательные числа, координатная прямая, модуль, множества Z и Q, сравнение и все действия, законы сложения.</div>
<div class="ch-prog"><div class="ch-prog-label"><span>Прогресс</span><span id="prog-4">0%</span></div><div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-4" style="width:0%"></div></div></div>
<div class="ch-action"><span id="btn-4">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
</a>
<a href="/textbook/math-6-ch5" class="ch-card k5" id="ch-5">
<div class="ch-cover cc5"><div class="ch-cover-wm">xy</div>
<div class="ch-num">Глава 5</div>
<div class="ch-title">Координатная плоскость</div>
<div class="ch-range">§1–§5</div>
</div>
<div class="ch-body">
<div class="ch-desc">Декартова система координат, графики реальных процессов, графики прямой и обратной пропорциональности.</div>
<div class="ch-prog"><div class="ch-prog-label"><span>Прогресс</span><span id="prog-5">0%</span></div><div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-5" style="width:0%"></div></div></div>
<div class="ch-action"><span id="btn-5">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
</a>
<a href="/textbook/math-6-ch6" class="ch-card k6" id="ch-6">
<div class="ch-cover cc6"><div class="ch-cover-wm"></div>
<div class="ch-num">Глава 6</div>
<div class="ch-title">Наглядная геометрия</div>
<div class="ch-range">§1–§5</div>
</div>
<div class="ch-body">
<div class="ch-desc">Тела и их развёртки, окружность и круг (длина и площадь), виды треугольников, центральная и осевая симметрия.</div>
<div class="ch-prog"><div class="ch-prog-label"><span>Прогресс</span><span id="prog-6">0%</span></div><div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-6" style="width:0%"></div></div></div>
<div class="ch-action"><span id="btn-6">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
</a>
</div>
<div class="ach-strip" id="ach-strip">
<div class="ach-icon">
<svg viewBox="0 0 24 24"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg>
</div>
<div class="ach-text">
<div class="ach-title">Математик 6 класса</div>
<div class="ach-sub" id="ach-sub">Пройдите все 38 параграфов шести глав, чтобы получить достижение</div>
</div>
</div>
</main>
<footer class="foot">Интерактивный учебник «Математика — 6 класс» · LearnSpace</footer>
<script>
'use strict';
(function(){
var saved = localStorage.getItem('math6_theme') || localStorage.getItem('theme') || 'light';
if (saved === 'dark') document.documentElement.classList.add('dark');
var lab = document.getElementById('theme-lab'); if (lab) lab.textContent = saved === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', function(){
document.documentElement.classList.toggle('dark');
var dark = document.documentElement.classList.contains('dark');
localStorage.setItem('math6_theme', dark ? 'dark' : 'light'); localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
var TOTAL = 48;
var CH_IDX = { 'math-6-ch1':1,'math-6-ch2':2,'math-6-ch3':3,'math-6-ch4':4,'math-6-ch5':5,'math-6-ch6':6 };
var CH_PARA = { 'math-6-ch1':12,'math-6-ch2':9,'math-6-ch3':5,'math-6-ch4':11,'math-6-ch5':5,'math-6-ch6':6 };
function setChProg(idx, read, total){
var pct = total ? Math.round(read*100/total) : 0;
var l=document.getElementById('prog-'+idx),f=document.getElementById('fill-'+idx),b=document.getElementById('btn-'+idx);
if(l)l.textContent=pct+'%'; if(f)f.style.width=pct+'%';
if(b){ if(read>0&&read<total)b.textContent='Продолжить'; else if(read>=total)b.textContent='Открыть снова'; else b.textContent='Открыть главу'; }
}
function renderProgress(children){
var totalRead=0;
for(var i=0;i<children.length;i++){ var ch=children[i],idx=CH_IDX[ch.slug]; if(!idx)continue; var read=ch.progress?ch.progress.read.length:0; var total=ch.para_count||CH_PARA[ch.slug]||1; totalRead+=read; setChProg(idx,read,total); }
var pct=Math.round(totalRead*100/TOTAL);
var o=document.getElementById('overall-text'),f=document.getElementById('overall-fill');
if(o)o.textContent=totalRead+' из '+TOTAL+' параграфов · '+pct+'%'; if(f)f.style.width=pct+'%';
var xpBadge=document.getElementById('hero-xp-badge'),xp=parseInt(localStorage.getItem('math6_xp')||'0',10)||0;
if(xpBadge&&xp>0){ xpBadge.style.display=''; xpBadge.textContent=xp+' XP'; }
if(totalRead>=TOTAL){ var s=document.getElementById('ach-strip'),sub=document.getElementById('ach-sub'); if(s)s.classList.add('lit'); if(sub)sub.textContent='Выполнено! Вы прошли весь курс математики 6 класса.'; }
}
function loadProgress(){
if(typeof window.LS==='undefined'||typeof window.LS.api!=='function'){ renderProgress([]); return; }
window.LS.api('/api/textbooks/math-6/children').then(function(data){ renderProgress(data&&data.children?data.children:[]); }).catch(function(){ renderProgress([]); });
}
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded', loadProgress); else loadProgress();
window.addEventListener('focus', loadProgress);
</script>
</body>
</html>