From 1d95f72d456b9f956175846762897f76dbc70485 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 14:47:21 +0300 Subject: [PATCH] =?UTF-8?q?feat(math6):=20Phase=200=20=E2=80=94=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=84=D1=80=D0=B0=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D1=80=D0=B0=20=D1=83=D1=87=D0=B5=D0=B1=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=C2=AB=D0=9C=D0=B0=D1=82=D0=B5=D0=BC=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=206=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Хаб + 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) --- backend/src/db/migrations/049_math6_hub.sql | 51 +++ backend/tests/math6-page.test.js | 88 +++++ frontend/css/math6.css | 243 ++++++++++++ frontend/js/math6_engine.js | 389 ++++++++++++++++++++ frontend/js/math6_svg.js | 150 ++++++++ frontend/textbooks/math_6_ch1.html | 106 ++++++ frontend/textbooks/math_6_ch2.html | 102 +++++ frontend/textbooks/math_6_ch3.html | 96 +++++ frontend/textbooks/math_6_ch4.html | 104 ++++++ frontend/textbooks/math_6_ch5.html | 96 +++++ frontend/textbooks/math_6_ch6.html | 98 +++++ frontend/textbooks/math_6_hub.html | 290 +++++++++++++++ 12 files changed, 1813 insertions(+) create mode 100644 backend/src/db/migrations/049_math6_hub.sql create mode 100644 backend/tests/math6-page.test.js create mode 100644 frontend/css/math6.css create mode 100644 frontend/js/math6_engine.js create mode 100644 frontend/js/math6_svg.js create mode 100644 frontend/textbooks/math_6_ch1.html create mode 100644 frontend/textbooks/math_6_ch2.html create mode 100644 frontend/textbooks/math_6_ch3.html create mode 100644 frontend/textbooks/math_6_ch4.html create mode 100644 frontend/textbooks/math_6_ch5.html create mode 100644 frontend/textbooks/math_6_ch6.html create mode 100644 frontend/textbooks/math_6_hub.html diff --git a/backend/src/db/migrations/049_math6_hub.sql b/backend/src/db/migrations/049_math6_hub.sql new file mode 100644 index 0000000..b976cb6 --- /dev/null +++ b/backend/src/db/migrations/049_math6_hub.sql @@ -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'); diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js new file mode 100644 index 0000000..34d200e --- /dev/null +++ b/backend/tests/math6-page.test.js @@ -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(/') + .replace(/'); + }); + 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(' | ')); +}); diff --git a/frontend/css/math6.css b/frontend/css/math6.css new file mode 100644 index 0000000..74eade5 --- /dev/null +++ b/frontend/css/math6.css @@ -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} diff --git a/frontend/js/math6_engine.js b/frontend/js/math6_engine.js new file mode 100644 index 0000000..1d82f37 --- /dev/null +++ b/frontend/js/math6_engine.js @@ -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 = ' Ур. ' + 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 += '
' + + '
' + p.num + '

' + p.name + '

' + + '
'; + }); + 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 = '
' + p.num + '
' + p.name + '
'; + 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 = '
Содержание этого параграфа готовится.
' + + 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 += '
XP-прогрессУр. ' + STATE.level + '
' + STATE.xp + ' XP' + xpNext + ' XP
'; + html += '

' + sb.title + '

'; + (sb.rows || []).forEach(function (r) { html += '
' + r[0] + '' + (r[1] ? ' — ' + r[1] : '') + '
'; }); + html += '
'; + var tips = M6.tips || []; + var tip = tips.filter(function (t) { return t.sec === id; })[0] || tips[0]; + if (tip) html += '

Подсказка

' + tip.html + '
'; + if (STATE.achievements.size > 0) { + html += '

Достижения ' + STATE.achievements.size + '

'; + Array.from(STATE.achievements.values()).slice(-5).forEach(function (text) { html += '
✓ ' + text + '
'; }); + html += '
'; + } + 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: '', + theory: '', + algo: '', + rule: '', + example: '', + oral: '' +}; +function M6icon(k) { return ICONS[k] || ''; } +function makeCard(kind, title, n, body) { + var labels = { repeat: 'Повторение', theory: 'Теория', algo: 'Алгоритм', rule: 'Правило', example: 'Пример', oral: 'Устно' }; + return '
' + (ICONS[kind] || '') + '
' + (labels[kind] || '') + (title && title !== labels[kind] ? ' · ' + title : '') + '
' + (n ? '
' + n + '
' : '') + '
' + body + '
'; +} +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 = '
'; + h += prev ? '' : ''; + h += next ? '' : ''; + h += '
'; return h; +} +function readBtn(id, label) { + return '
'; +} +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 = '' + it.html + '×'; 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('(? 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 = '' + g.term[0].toUpperCase() + g.term.slice(1) + '
' + g.def + '
' + (g.sec ? '
См. ' + shortName(g.sec) + '
' : ''); 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 = '
Ничего не найдено
'; return; } out.innerHTML = rows.map(function (r, i) { return ''; }).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 }; +})(); diff --git a/frontend/js/math6_svg.js b/frontend/js/math6_svg.js new file mode 100644 index 0000000..5a99965 --- /dev/null +++ b/frontend/js/math6_svg.js @@ -0,0 +1,150 @@ +/* math6_svg.js — SVG-хелперы учебника «Математика 6». window.Math6. + * Самодостаточно (без зависимостей). Все функции возвращают строку SVG, + * готовую к вставке в innerHTML. Координаты SVG: y растёт вниз — учтено. + * + * Готово (Phase 0 / Глава 1): fmt, box, numberLine (прямая/луч с метками и точками), + * plane (декартова плоскость — фундамент для Главы 5). + * Будет добавлено при наполнении глав 5–6: plotFn, circleFig, triangleFig, solid, net, + * reflectPoint/reflectLine (симметрия). + */ +(function () { +'use strict'; +if (window.Math6 && window.Math6.__installed) return; +var M = window.Math6 = window.Math6 || {}; +M.__installed = true; + +/* Русская запись числа: десятичная запятая, без хвостовых нулей */ +M.fmt = function (n) { + if (n == null || !isFinite(n)) return '' + n; + var s = (Math.round(n * 1e9) / 1e9).toString(); + return s.replace('.', ','); +}; +function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } + +/* Обёртка с фоном */ +M.box = function (w, h, inner, opts) { + opts = opts || {}; + return '
' + + '' + + (inner || '') + '
'; +}; + +/* === ЧИСЛОВАЯ ПРЯМАЯ / КООРДИНАТНЫЙ ЛУЧ === + * opts: {min,max, minor, major, ray(bool), marks:[{v,label,color,open,above}], + * segments:[{from,to,color}], width,height, title} + */ +M.numberLine = function (opts) { + opts = opts || {}; + var min = opts.min != null ? opts.min : 0; + var max = opts.max != null ? opts.max : 10; + var minor = opts.minor || 1; + var major = opts.major || minor; + var W = opts.width || 540, H = opts.height || 92; + var pad = 34; + var axisY = Math.round(H * 0.58); + var x0 = pad, x1 = W - pad; + function X(v) { return x0 + (v - min) / (max - min) * (x1 - x0); } + var col = opts.color || 'var(--pri,#4f46e5)'; + var s = ''; + + /* выделенные отрезки/интервалы (рисуем под осью) */ + (opts.segments || []).forEach(function (seg) { + var a = X(seg.from), b = X(seg.to); + s += ''; + }); + + /* ось со стрелками */ + s += ''; + s += ''; + if (!opts.ray) s += ''; + + /* деления и подписи */ + var nMinor = Math.round((max - min) / minor); + for (var i = 0; i <= nMinor; i++) { + var v = min + i * minor; v = Math.round(v * 1e6) / 1e6; + var x = X(v); + var isMajor = Math.abs(Math.round((v - min) / major) - (v - min) / major) < 1e-6; + var len = isMajor ? 9 : 5; + s += ''; + if (isMajor && opts.labels !== false) { + s += '' + esc(M.fmt(v)) + ''; + } + } + + /* точки-маркеры */ + (opts.marks || []).forEach(function (mk) { + var x = X(mk.v); var c = mk.color || '#e11d48'; + if (mk.open) s += ''; + else s += ''; + if (mk.label) { + var ly = mk.above === false ? axisY + 38 : axisY - 14; + s += '' + esc(mk.label) + ''; + } + }); + + return M.box(W, H, s, { maxw: opts.maxw || W, bg: opts.bg }); +}; + +/* === ДЕКАРТОВА ПЛОСКОСТЬ (фундамент для Главы 5) === + * opts: {xmin,xmax,ymin,ymax, points:[{x,y,label,color,open}], + * segments:[{from:{x,y},to:{x,y},color,dash}], quadrants(bool), + * plot:{fn,color,samples,from,to}, size, unitLabels(bool)} + */ +M.plane = function (opts) { + opts = opts || {}; + var xmin = opts.xmin != null ? opts.xmin : -5, xmax = opts.xmax != null ? opts.xmax : 5; + var ymin = opts.ymin != null ? opts.ymin : -5, ymax = opts.ymax != null ? opts.ymax : 5; + var S = opts.size || 360, pad = 22; + function X(x) { return pad + (x - xmin) / (xmax - xmin) * (S - 2 * pad); } + function Y(y) { return S - pad - (y - ymin) / (ymax - ymin) * (S - 2 * pad); } + var axc = 'var(--text,#0f172a)'; + var s = ''; + + if (opts.quadrants) { + var cx = X(0), cy = Y(0); + s += ''; + s += ''; + } + /* сетка */ + for (var gx = Math.ceil(xmin); gx <= Math.floor(xmax); gx++) { + s += ''; + } + for (var gy = Math.ceil(ymin); gy <= Math.floor(ymax); gy++) { + s += ''; + } + /* оси */ + s += ''; + s += ''; + s += ''; + s += ''; + s += 'x'; + s += 'y'; + s += '0'; + + /* график функции */ + if (opts.plot && typeof opts.plot.fn === 'function') { + var pl = opts.plot, from = pl.from != null ? pl.from : xmin, to = pl.to != null ? pl.to : xmax; + var N = pl.samples || 120, pts = [], started = false, d = ''; + for (var k = 0; k <= N; k++) { + var xv = from + (to - from) * k / N, yv = pl.fn(xv); + if (yv == null || !isFinite(yv) || yv < ymin - 1 || yv > ymax + 1) { started = false; continue; } + d += (started ? ' L ' : ' M ') + X(xv) + ' ' + Y(yv); started = true; + } + s += ''; + } + /* отрезки */ + (opts.segments || []).forEach(function (sg) { + s += ''; + }); + /* точки */ + (opts.points || []).forEach(function (p) { + var c = p.color || '#e11d48'; + if (p.open) s += ''; + else s += ''; + if (p.label) s += '' + esc(p.label) + ''; + }); + return M.box(S, S, s, { maxw: opts.maxw || S, bg: opts.bg }); +}; + +})(); diff --git a/frontend/textbooks/math_6_ch1.html b/frontend/textbooks/math_6_ch1.html new file mode 100644 index 0000000..12401b1 --- /dev/null +++ b/frontend/textbooks/math_6_ch1.html @@ -0,0 +1,106 @@ + + + + + + + +Математика 6 · Глава 1 · Десятичные дроби + + + + + + + + + + + + + + + +
+
+
+

Математика 6 · Глава 1

+
Десятичные дроби: запись и разряды · сравнение и округление · действия · бесконечные дроби
+
+
+ К математике 6 + + + +
+
+
+ +
+
+
+

Десятичные дроби

+

Десятичная дробь — это привычная запись чисел с запятой: цена, рост, масса, показания приборов. В этой главе мы научимся читать и записывать десятичные дроби по разрядам, сравнивать и округлять их, отмечать на координатном луче и выполнять все четыре действия — вплоть до деления на десятичную дробь и перевода обыкновенных дробей в десятичные.

+
+ +
Прогресс по главе
0%
+
+
+
+ +
Параграфы главы
+ +
+
+ + +
+
+ +
Интерактивный учебник «Математика 6» · Глава 1 · Десятичные дроби · LearnSpace
+ +
Достижение!
+
+ + + + + + diff --git a/frontend/textbooks/math_6_ch2.html b/frontend/textbooks/math_6_ch2.html new file mode 100644 index 0000000..1b0d50d --- /dev/null +++ b/frontend/textbooks/math_6_ch2.html @@ -0,0 +1,102 @@ + + + + + + + +Математика 6 · Глава 2 · Проценты и пропорции + + + + + + + + + + + + + + + +
+
+
+

Математика 6 · Глава 2

+
Проценты · задачи на проценты · пропорция · прямая и обратная зависимости · масштаб · диаграммы
+
+
+ К математике 6 + + + +
+
+
+ +
+
+
+

Проценты и пропорции

+

Процент — это сотая доля. Скидки, налоги, банковские вклады, состав сплавов и растворов — всё это язык процентов и пропорций. В этой главе мы разберём три основные задачи на проценты, освоим пропорцию и её главное свойство, научимся отличать прямую и обратную пропорциональные зависимости, поработаем с масштабом и построим круговые диаграммы.

+
+ +
Прогресс по главе
0%
+
+
+
+ +
Параграфы главы
+ +
+
+ + +
+
+ +
Интерактивный учебник «Математика 6» · Глава 2 · Проценты и пропорции · LearnSpace
+ +
Достижение!
+
+ + + + + + diff --git a/frontend/textbooks/math_6_ch3.html b/frontend/textbooks/math_6_ch3.html new file mode 100644 index 0000000..62208cc --- /dev/null +++ b/frontend/textbooks/math_6_ch3.html @@ -0,0 +1,96 @@ + + + + + + + +Математика 6 · Глава 3 · Множество + + + + + + + + + + + + + + + +
+
+
+

Математика 6 · Глава 3

+
Множество и его элементы · способы задания · пересечение и объединение · круги Эйлера
+
+
+ К математике 6 + + + +
+
+
+ +
+
+
+

Множество

+

Множество — одно из самых простых и самых важных понятий математики: это набор объектов, объединённых общим признаком. Мы научимся задавать множества перечислением и свойством, находить их пересечение и объединение и решать жизненные задачи с помощью наглядных кругов Эйлера.

+
+ +
Прогресс по главе
0%
+
+
+
+ +
Параграфы главы
+ +
+
+ + +
+
+ +
Интерактивный учебник «Математика 6» · Глава 3 · Множество · LearnSpace
+ +
Достижение!
+
+ + + + + + diff --git a/frontend/textbooks/math_6_ch4.html b/frontend/textbooks/math_6_ch4.html new file mode 100644 index 0000000..d334ce2 --- /dev/null +++ b/frontend/textbooks/math_6_ch4.html @@ -0,0 +1,104 @@ + + + + + + + +Математика 6 · Глава 4 · Рациональные числа + + + + + + + + + + + + + + + +
+
+
+

Математика 6 · Глава 4

+
Положительные и отрицательные числа · модуль · множества Z и Q · действия и законы
+
+
+ К математике 6 + + + +
+
+
+ +
+
+
+

Рациональные числа

+

Температура ниже нуля, долги, координаты влево от нуля — так в математику приходят отрицательные числа. В этой главе мы построим координатную прямую, познакомимся с модулем и противоположными числами, множествами целых $\mathbb{Z}$ и рациональных $\mathbb{Q}$ чисел, и научимся выполнять все действия с рациональными числами по правилам знаков.

+
+ +
Прогресс по главе
0%
+
+
+
+ +
Параграфы главы
+ +
+
+ + +
+
+ +
Интерактивный учебник «Математика 6» · Глава 4 · Рациональные числа · LearnSpace
+ +
Достижение!
+
+ + + + + + diff --git a/frontend/textbooks/math_6_ch5.html b/frontend/textbooks/math_6_ch5.html new file mode 100644 index 0000000..1445031 --- /dev/null +++ b/frontend/textbooks/math_6_ch5.html @@ -0,0 +1,96 @@ + + + + + + + +Математика 6 · Глава 5 · Координатная плоскость + + + + + + + + + + + + + + + +
+
+
+

Математика 6 · Глава 5

+
Декартова система координат · графики реальных процессов · графики зависимостей
+
+
+ К математике 6 + + + +
+
+
+ +
+
+
+

Координатная плоскость

+

Две пересекающиеся под прямым углом числовые прямые превращают плоскость в карту, где у каждой точки есть свой «адрес» — пара координат $(x;\,y)$. Мы научимся ставить и читать точки, понимать графики реальных процессов (движение, температура) и строить графики прямой и обратной пропорциональной зависимости.

+
+ +
Прогресс по главе
0%
+
+
+
+ +
Параграфы главы
+ +
+
+ + +
+
+ +
Интерактивный учебник «Математика 6» · Глава 5 · Координатная плоскость · LearnSpace
+ +
Достижение!
+
+ + + + + + diff --git a/frontend/textbooks/math_6_ch6.html b/frontend/textbooks/math_6_ch6.html new file mode 100644 index 0000000..c887969 --- /dev/null +++ b/frontend/textbooks/math_6_ch6.html @@ -0,0 +1,98 @@ + + + + + + + +Математика 6 · Глава 6 · Наглядная геометрия + + + + + + + + + + + + + + + +
+
+
+

Математика 6 · Глава 6

+
Тела и развёртки · окружность и круг · виды треугольников · симметрия
+
+
+ К математике 6 + + + +
+
+
+ +
+
+
+

Наглядная геометрия

+

Геометрия начинается с наблюдения. Мы рассмотрим пространственные тела и их развёртки, выведем формулы длины окружности $C=2\pi r$ и площади круга $S=\pi r^2$, научимся различать виды треугольников по сторонам и углам и построим фигуры, симметричные относительно точки и прямой.

+
+ +
Прогресс по главе
0%
+
+
+
+ +
Параграфы главы
+ +
+
+ + +
+
+ +
Интерактивный учебник «Математика 6» · Глава 6 · Наглядная геометрия · LearnSpace
+ +
Достижение!
+
+ + + + + + diff --git a/frontend/textbooks/math_6_hub.html b/frontend/textbooks/math_6_hub.html new file mode 100644 index 0000000..8c65af6 --- /dev/null +++ b/frontend/textbooks/math_6_hub.html @@ -0,0 +1,290 @@ + + + + + + + + +Математика 6 класс — учебник + + + + + + + +
+
+ +
+

Математика — 6 класс

+
Десятичные дроби · проценты · множества · рациональные числа · координаты · геометрия
+
+
+ +
+
+
+ +
+ +
+
+
+
Общий прогресс по курсу
+
Загрузка...
+
+
+ +
+ +
+ + +
0,5
+
Глава 1
+
Десятичные дроби
+
§1–§12
+
+
+
Запись и разряды, сравнение и округление, координатный луч, все действия, бесконечные дроби, преобразования выражений.
+
Прогресс0%
+
Открыть главу
+
+
+ + +
%
+
Глава 2
+
Проценты и пропорции
+
§1–§9
+
+
+
Проценты, три основные задачи, пропорция и её свойство, прямая и обратная зависимости, масштаб, круговые диаграммы.
+
Прогресс0%
+
Открыть главу
+
+
+ + +
+
Глава 3
+
Множество
+
§1–§5
+
+
+
Множество и его элементы, пустое множество, способы задания, пересечение и объединение, круги Эйлера.
+
Прогресс0%
+
Открыть главу
+
+
+ + +
−5
+
Глава 4
+
Рациональные числа
+
§1–§11
+
+
+
Отрицательные числа, координатная прямая, модуль, множества Z и Q, сравнение и все действия, законы сложения.
+
Прогресс0%
+
Открыть главу
+
+
+ + +
xy
+
Глава 5
+
Координатная плоскость
+
§1–§5
+
+
+
Декартова система координат, графики реальных процессов, графики прямой и обратной пропорциональности.
+
Прогресс0%
+
Открыть главу
+
+
+ + +
+
Глава 6
+
Наглядная геометрия
+
§1–§5
+
+
+
Тела и их развёртки, окружность и круг (длина и площадь), виды треугольников, центральная и осевая симметрия.
+
Прогресс0%
+
Открыть главу
+
+
+ +
+ +
+
+ +
+
+
Математик 6 класса
+
Пройдите все 38 параграфов шести глав, чтобы получить достижение
+
+
+ +
+ +
Интерактивный учебник «Математика — 6 класс» · LearnSpace
+ + + + +