merge: feature/lab-content-engine → master
Сводит всю работу в master: модуль биохимии (фазы 0-7), System Health Level 1-4 (вердикт/мониторинг, метрики запросов, тренды, диагностика), а также lab-content-engine, textbooks и chemistry-7 из feature-ветки. Дерево результата = feature (полный суперсет). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+15
-1
@@ -178,7 +178,21 @@
|
||||
"Bash(cmd /c \"taskkill /PID 60564 /F\")",
|
||||
"Bash(cmd /c \"taskkill /F /PID 60564 2>&1\")",
|
||||
"Bash(kill -9 60564)",
|
||||
"Bash(kill -9 9313)"
|
||||
"Bash(kill -9 9313)",
|
||||
"Read(//f/!Рабочие/ЦТ/Математика/**)",
|
||||
"Read(//f/!Рабочие/ЦТ/Физика/**)",
|
||||
"Read(//f/!����稥/��/��⥬�⨪�/**)",
|
||||
"PowerShell(Get-ChildItem \"F:\\\\!Рабочие\\\\ЦТ\\\\Математика\\\\Математика\\\\ЦТ-ЦЭ\" | Select-Object Name, @{N='MB';E={[math]::Round\\($_.Length/1MB,1\\)}} | Format-Table -AutoSize)",
|
||||
"Read(//f/!����稥/��/**)",
|
||||
"PowerShell(Get-ChildItem \"F:\\\\!Рабочие\\\\ЦТ\\\\Физика\\\\Сборники ЦЭ,ЦТ-20260116T125835Z-3-001\" | Select-Object Name, @{N='MB';E={[math]::Round\\($_.Length/1MB,1\\)}} | Format-Table -AutoSize)",
|
||||
"PowerShell(Get-ChildItem \"F:\\\\!Рабочие\\\\ЦТ\\\\Физика\\\\Сборники ЦТ-20260116T130104Z-3-001\" | Select-Object Name, @{N='MB';E={[math]::Round\\($_.Length/1MB,1\\)}} | Format-Table -AutoSize)",
|
||||
"PowerShell(Get-ChildItem \"F:\\\\!Рабочие\\\\ЦТ\\\\Физика\\\\Сборники ЦЭ,ЦТ-20260116T125835Z-3-001\\\\Сборники ЦЭ,ЦТ\" | Select-Object Name, @{N='MB';E={[math]::Round\\($_.Length/1MB,1\\)}} | Format-Table -AutoSize)",
|
||||
"PowerShell(Get-ChildItem \"F:\\\\!Рабочие\\\\ЦТ\\\\Физика\\\\Сборники ЦТ-20260116T130104Z-3-001\\\\Сборники ЦТ\" | Select-Object Name, @{N='MB';E={[math]::Round\\($_.Length/1MB,1\\)}} | Format-Table -AutoSize)",
|
||||
"Bash(git commit -m ' *)",
|
||||
"Bash(git push *)",
|
||||
"Bash(curl -s http://localhost:3000/api/subjects)",
|
||||
"PowerShell(\\(Get-Content \"g:\\\\Dev\\\\Тесты\\\\BQ-System\\\\frontend\\\\question-bank.html\"\\).Count)",
|
||||
"Bash(curl -s \"http://localhost:3000/api/subjects/math/topics\" -H \"Authorization: Bearer test\")"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"\\tmp"
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/* audit_chem8.js — аудит KaTeX и оформления учебника «Химия 8».
|
||||
* Загружает каждую страницу в jsdom (renderMathInElement застаблен → $…$ остаются
|
||||
* литералами с уже раскрытыми JS-эскейпами), строит все §, извлекает формулы и
|
||||
* проверяет: баланс $, баланс {}, отсутствие управляющих символов (следы \t/\n),
|
||||
* пустые формулы, «сырые» $…$ вне рендера. Запуск: node backend/scripts/audit_chem8.js
|
||||
*/
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('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));
|
||||
|
||||
const PAGES = [
|
||||
['chemistry_8_intro.html', 'chem8_intro_widgets'],
|
||||
['chemistry_8_ch1.html', 'chem8_ch1_widgets'],
|
||||
['chemistry_8_ch2.html', 'chem8_ch2_widgets'],
|
||||
['chemistry_8_ch3.html', 'chem8_ch3_widgets'],
|
||||
['chemistry_8_ch4.html', 'chem8_ch4_widgets'],
|
||||
['chemistry_8_ch5.html', 'chem8_ch5_widgets'],
|
||||
['chemistry_8_ch6.html', 'chem8_ch6_widgets']
|
||||
];
|
||||
|
||||
function buildPage(file, widgets) {
|
||||
let html = readF('frontend/textbooks/' + file);
|
||||
const inl = {
|
||||
'/js/biochem-core.js': readF('frontend/js/biochem-core.js'),
|
||||
'/js/chem8_svg.js': readF('frontend/js/chem8_svg.js'),
|
||||
'/js/chem8_mol.js': readF('frontend/js/chem8_mol.js'),
|
||||
['/js/' + widgets + '.js']: readF('frontend/js/' + widgets + '.js'),
|
||||
'/js/chem8_engine.js': readF('frontend/js/chem8_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;
|
||||
}
|
||||
|
||||
function extractMath(s) {
|
||||
const out = [];
|
||||
// $$...$$ затем $...$
|
||||
let re = /\$\$([\s\S]+?)\$\$/g, m;
|
||||
let masked = s;
|
||||
while ((m = re.exec(s)) !== null) out.push({ disp: true, body: m[1] });
|
||||
masked = s.replace(/\$\$[\s\S]+?\$\$/g, '');
|
||||
re = /\$([^$]*)\$/g;
|
||||
while ((m = re.exec(masked)) !== null) out.push({ disp: false, body: m[1] });
|
||||
return out;
|
||||
}
|
||||
|
||||
function checkBraces(b) { let d = 0; for (const c of b) { if (c === '{') d++; else if (c === '}') d--; if (d < 0) return false; } return d === 0; }
|
||||
function hasCtrl(b) { return /[\t\n\r\f\v\b]/.test(b); }
|
||||
|
||||
async function auditPage(file, widgets) {
|
||||
const issues = [];
|
||||
const vc = new VirtualConsole(); const errs = [];
|
||||
vc.on('jsdomError', e => errs.push(e.message));
|
||||
const dom = new JSDOM(buildPage(file, widgets), {
|
||||
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
|
||||
beforeParse(w) { w.scrollTo = function () {}; }
|
||||
});
|
||||
await wait(120);
|
||||
const doc = dom.window.document;
|
||||
const paras = (dom.window.PARAS || []).map(p => p.id);
|
||||
for (const id of paras) { try { dom.window.goTo(id); } catch (e) {} }
|
||||
await wait(120);
|
||||
|
||||
if (errs.length) issues.push('script errors: ' + errs.join(' | '));
|
||||
|
||||
// собрать все § тела + sidebar
|
||||
let html = '';
|
||||
doc.querySelectorAll('[id$="-body"]').forEach(el => { html += el.innerHTML + '\n'; });
|
||||
const sidebar = doc.getElementById('sidebar-content'); if (sidebar) html += sidebar.innerHTML;
|
||||
|
||||
// баланс $ (нечётное число одиночных $ вне $$)
|
||||
const noDisp = html.replace(/\$\$[\s\S]+?\$\$/g, '');
|
||||
const singles = (noDisp.match(/\$/g) || []).length;
|
||||
if (singles % 2 !== 0) issues.push('нечётное число одиночных $ (' + singles + ')');
|
||||
|
||||
const maths = extractMath(html);
|
||||
let bad = 0;
|
||||
for (const m of maths) {
|
||||
const b = m.body;
|
||||
if (!b.trim()) { issues.push('пустая формула $' + (m.disp ? '$' : '') + '$'); bad++; continue; }
|
||||
if (!checkBraces(b)) { issues.push('несбалансированные {} в: ' + b.slice(0, 50)); bad++; }
|
||||
if (hasCtrl(b)) { issues.push('управляющий символ (след \\t/\\n?) в: ' + JSON.stringify(b.slice(0, 50))); bad++; }
|
||||
// одиночный backslash перед буквой, не часть известной команды? — грубая эвристика: \ в конце
|
||||
if (/\\$/.test(b)) { issues.push('формула заканчивается на \\: ' + b.slice(-20)); bad++; }
|
||||
}
|
||||
return { file, mathCount: maths.length, badCount: bad, issues };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
let total = 0, totalBad = 0;
|
||||
for (const [file, w] of PAGES) {
|
||||
const r = await auditPage(file, w);
|
||||
total += r.mathCount; totalBad += r.badCount;
|
||||
console.log('\n=== ' + file + ' — формул: ' + r.mathCount + ', проблем: ' + r.issues.length + ' ===');
|
||||
if (r.issues.length) r.issues.slice(0, 25).forEach(i => console.log(' ! ' + i));
|
||||
else console.log(' OK');
|
||||
}
|
||||
console.log('\nИТОГО формул: ' + total + ', проблемных: ' + totalBad);
|
||||
process.exit(0);
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,307 @@
|
||||
// Генератор physics_10_hub.html на основе algebra_11_hub.html
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SRC = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'algebra_11_hub.html');
|
||||
const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_10_hub.html');
|
||||
|
||||
let h = fs.readFileSync(SRC, 'utf8');
|
||||
|
||||
// === 1. Palette + dark mode ===
|
||||
h = h.replace(
|
||||
/:root\{[\s\S]*?--bg:#ecfdf5; --card:#fff;[\s\S]*?--sh-h:0 12px 36px rgba\(13,148,136,\.18\);[\s\S]*?\}/,
|
||||
`:root{
|
||||
--bg:#fffbeb; --card:#fff;
|
||||
--text:#0f172a; --muted:#475569;
|
||||
--border:#fde68a;
|
||||
--pri:#ca8a04; --pri-d:#a16207;
|
||||
--pri-soft:#fef3c7;
|
||||
--ch1:#2563eb; --ch1-d:#1d4ed8;
|
||||
--ch2:#059669; --ch2-d:#047857;
|
||||
--ch3:#7c3aed; --ch3-d:#6d28d9;
|
||||
--ch4:#db2777; --ch4-d:#be185d;
|
||||
--ch5:#0891b2; --ch5-d:#0e7490;
|
||||
--ch6:#10b981; --ch6-d:#059669;
|
||||
--sh:0 4px 16px rgba(202,138,4,.10);
|
||||
--sh-h:0 12px 36px rgba(202,138,4,.18);
|
||||
}`);
|
||||
|
||||
h = h.replace(
|
||||
/html\.dark\{[\s\S]*?--pri-soft:rgba\(13,148,136,\.16\);[\s\S]*?\}/,
|
||||
`html.dark{
|
||||
--bg:#1a1500; --card:#2a2410;
|
||||
--text:#fef3c7; --muted:#d4b88f;
|
||||
--border:#3d2f0a;
|
||||
--pri-soft:rgba(202,138,4,.16);
|
||||
}`);
|
||||
|
||||
// === 2. Header ===
|
||||
h = h.replace(
|
||||
/\.hdr\{position:relative;background:linear-gradient\(110deg,#115e59 0%,#0d9488 55%,#5eead4 100%\)[^}]*\}/,
|
||||
`.hdr{position:relative;background:linear-gradient(110deg,#713f12 0%,#ca8a04 55%,#fde047 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(254,243,199,.18)}`);
|
||||
|
||||
h = h.replace(/АЛГЕБРА/g, 'ФИЗИКА');
|
||||
h = h.replace(/rgba\(204,251,241,\.12\)/g, 'rgba(254,243,199,.12)');
|
||||
|
||||
// === 3. po-icon gradient ===
|
||||
h = h.replace(
|
||||
/\.po-icon\{[^}]*background:linear-gradient\(135deg,#0d9488,#5eead4\)[^}]*\}/,
|
||||
`.po-icon{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#ca8a04,#fde047);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;font-style:italic}`);
|
||||
h = h.replace(/\.po-bar\{height:8px;background:rgba\(13,148,136,\.14\)/, '.po-bar{height:8px;background:rgba(202,138,4,.14)');
|
||||
h = h.replace(/\.po-fill\{height:100%;background:linear-gradient\(90deg,var\(--pri\),#5eead4\)/, '.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#fde047)');
|
||||
h = h.replace(/\.po-xp\{[^}]*background:linear-gradient\(135deg,#f59e0b,var\(--pri\)\)[^}]*\}/,
|
||||
".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(202,138,4,.24)}");
|
||||
|
||||
// === 4. Chapter grid: 3 → 6 cards ===
|
||||
h = h.replace(
|
||||
/\.ch-grid\{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px\}\s*@media\(min-width:680px\)\{\.ch-grid\{grid-template-columns:1fr 1fr\}\}\s*@media\(min-width:1000px\)\{\.ch-grid\{grid-template-columns:repeat\(3,1fr\)\}\}/,
|
||||
`.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
|
||||
@media(min-width:680px){.ch-grid{grid-template-columns:1fr 1fr}}
|
||||
@media(min-width:1000px){.ch-grid{grid-template-columns:repeat(3,1fr)}}`);
|
||||
|
||||
// Replace cover gradients (ch1 ch2 ch3) and add ch4 ch5 ch6
|
||||
h = h.replace(
|
||||
/\.ch-cover\.ch1\{background:[^}]+\}\s*\.ch-cover\.ch2\{background:[^}]+\}\s*\.ch-cover\.ch3\{background:[^}]+\}/,
|
||||
`.ch-cover.ch1{background:linear-gradient(135deg,#1e3a8a,#2563eb 60%,#60a5fa)}
|
||||
.ch-cover.ch2{background:linear-gradient(135deg,#064e3b,#059669 60%,#34d399)}
|
||||
.ch-cover.ch3{background:linear-gradient(135deg,#3b0764,#7c3aed 60%,#a78bfa)}
|
||||
.ch-cover.ch4{background:linear-gradient(135deg,#831843,#db2777 60%,#f472b6)}
|
||||
.ch-cover.ch5{background:linear-gradient(135deg,#164e63,#0891b2 60%,#22d3ee)}
|
||||
.ch-cover.ch6{background:linear-gradient(135deg,#064e3b,#10b981 60%,#6ee7b7)}`);
|
||||
|
||||
// Replace chN-card progress fill and action gradients
|
||||
h = h.replace(
|
||||
/\.ch-card\.ch1-card \.ch-prog-fill\{background:linear-gradient\(90deg,var\(--ch1\),var\(--ch1-d\)\)\}\s*\.ch-card\.ch2-card \.ch-prog-fill\{background:linear-gradient\(90deg,var\(--ch2\),var\(--ch2-d\)\)\}\s*\.ch-card\.ch3-card \.ch-prog-fill\{background:linear-gradient\(90deg,var\(--ch3\),var\(--ch3-d\)\)\}/,
|
||||
`.ch-card.ch1-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch1),var(--ch1-d))}
|
||||
.ch-card.ch2-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch2),var(--ch2-d))}
|
||||
.ch-card.ch3-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch3),var(--ch3-d))}
|
||||
.ch-card.ch4-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch4),var(--ch4-d))}
|
||||
.ch-card.ch5-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch5),var(--ch5-d))}
|
||||
.ch-card.ch6-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch6),var(--ch6-d))}`);
|
||||
|
||||
h = h.replace(
|
||||
/\.ch-card\.ch1-card \.ch-action\{background:linear-gradient\(135deg,var\(--ch1\),#fbbf24\)\}\s*\.ch-card\.ch2-card \.ch-action\{background:linear-gradient\(135deg,var\(--ch2\),#a78bfa\)\}\s*\.ch-card\.ch3-card \.ch-action\{background:linear-gradient\(135deg,var\(--ch3\),#22d3ee\)\}/,
|
||||
`.ch-card.ch1-card .ch-action{background:linear-gradient(135deg,var(--ch1),#60a5fa)}
|
||||
.ch-card.ch2-card .ch-action{background:linear-gradient(135deg,var(--ch2),#34d399)}
|
||||
.ch-card.ch3-card .ch-action{background:linear-gradient(135deg,var(--ch3),#a78bfa)}
|
||||
.ch-card.ch4-card .ch-action{background:linear-gradient(135deg,var(--ch4),#f472b6)}
|
||||
.ch-card.ch5-card .ch-action{background:linear-gradient(135deg,var(--ch5),#22d3ee)}
|
||||
.ch-card.ch6-card .ch-action{background:linear-gradient(135deg,var(--ch6),#6ee7b7)}`);
|
||||
|
||||
// Final header gradient
|
||||
h = h.replace(
|
||||
/\.final-head\{padding:18px 22px;background:linear-gradient\(135deg,#115e59 0%,#0d9488 55%,#0891b2 100%\)/,
|
||||
'.final-head{padding:18px 22px;background:linear-gradient(135deg,#713f12 0%,#ca8a04 55%,#f59e0b 100%)');
|
||||
|
||||
// title
|
||||
h = h.replace(/<title>Алгебра 11 класс — учебник<\/title>/, '<title>Физика 10 класс — учебник</title>');
|
||||
|
||||
// localStorage keys
|
||||
h = h.replace(/algebra11_theme/g, 'physics10_theme');
|
||||
h = h.replace(/algebra11_xp/g, 'physics10_xp');
|
||||
h = h.replace(/algebra11_course_master/g, 'physics10_course_master');
|
||||
h = h.replace(/algebra11_course_bosses/g, 'physics10_course_bosses');
|
||||
h = h.replace(/algebra11-master/g, 'physics10-master');
|
||||
h = h.replace(/'fin-boss-'/g, "'fin-boss-'"); // unchanged
|
||||
|
||||
// Header H1 + subtitle
|
||||
h = h.replace(/<h1>Алгебра — 11 класс<\/h1>/, '<h1>Физика — 10 класс</h1>');
|
||||
h = h.replace(
|
||||
/<div class="hdr-sub">Полный курс: степени и логарифмы, показательная и логарифмическая функции, уравнения и неравенства<\/div>/,
|
||||
'<div class="hdr-sub">Полный курс физики 10 класса: молекулярная физика, термодинамика, электростатика, магнитное поле, ток в средах</div>'
|
||||
);
|
||||
|
||||
// po-icon "a" → "f"
|
||||
h = h.replace(/<div class="po-icon">a<\/div>/, '<div class="po-icon">f</div>');
|
||||
|
||||
// === 5. Заменяем блок с 3 главами целиком на блок с 6 главами ===
|
||||
const chBlock = `
|
||||
<a href="/textbook/physics-10-ch1" class="ch-card ch1-card" id="ch-1">
|
||||
<div class="ch-cover ch1">
|
||||
<div class="ch-cover-wm">T</div>
|
||||
<div class="ch-num">Глава 1</div>
|
||||
<div class="ch-title">Основы МКТ</div>
|
||||
<div class="ch-range">§1–§10 + Финал</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/physics-10-ch2" class="ch-card ch2-card" id="ch-2">
|
||||
<div class="ch-cover ch2">
|
||||
<div class="ch-cover-wm">ΔU</div>
|
||||
<div class="ch-num">Глава 2</div>
|
||||
<div class="ch-title">Термодинамика</div>
|
||||
<div class="ch-range">§11–§15 + Финал</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/physics-10-ch3" class="ch-card ch3-card" id="ch-3">
|
||||
<div class="ch-cover ch3">
|
||||
<div class="ch-cover-wm">+q</div>
|
||||
<div class="ch-num">Глава 3</div>
|
||||
<div class="ch-title">Электростатика</div>
|
||||
<div class="ch-range">§16–§24 + Финал</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/physics-10-ch4" class="ch-card ch4-card" id="ch-4">
|
||||
<div class="ch-cover ch4">
|
||||
<div class="ch-cover-wm">I</div>
|
||||
<div class="ch-num">Глава 4</div>
|
||||
<div class="ch-title">Постоянный ток</div>
|
||||
<div class="ch-range">§25–§26 + Финал</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-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/physics-10-ch5" class="ch-card ch5-card" id="ch-5">
|
||||
<div class="ch-cover ch5">
|
||||
<div class="ch-cover-wm">B</div>
|
||||
<div class="ch-num">Глава 5</div>
|
||||
<div class="ch-title">Магнитное поле</div>
|
||||
<div class="ch-range">§27–§33 + Финал</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/physics-10-ch6" class="ch-card ch6-card" id="ch-6">
|
||||
<div class="ch-cover ch6">
|
||||
<div class="ch-cover-wm">n/p</div>
|
||||
<div class="ch-num">Глава 6</div>
|
||||
<div class="ch-title">Ток в средах</div>
|
||||
<div class="ch-range">§34–§37 + Финал</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>
|
||||
|
||||
`;
|
||||
|
||||
// Replace the entire <a href="/textbook/algebra-11-ch1"...</a><a ch2></a><a ch3></a> block
|
||||
h = h.replace(/\s*<a href="\/textbook\/algebra-11-ch1"[\s\S]*?<\/a>\s*<a href="\/textbook\/algebra-11-ch2"[\s\S]*?<\/a>\s*<a href="\/textbook\/algebra-11-ch3"[\s\S]*?<\/a>\s*/,
|
||||
chBlock);
|
||||
|
||||
// Final placeholder — заменим cheat-grid + bosses на placeholder
|
||||
h = h.replace(/<div class="fin-section-title">\s*<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"\/><\/svg>\s*Шпаргалка курса\s*<\/div>\s*<div class="cheat-grid">[\s\S]*?<\/div>\s*<div class="fin-section-title">\s*<svg viewBox="0 0 24 24"><path d="M14\.5 3\.5l[^"]+"\/><\/svg>\s*7 интегрированных боссов\s*<\/div>\s*<div class="boss-overall-bar">[\s\S]*?<\/div>\s*<div id="fin-bosses-container"><\/div>/,
|
||||
`<div class="fin-placeholder">
|
||||
<h3>Финал курса — в разработке</h3>
|
||||
<p>Итоговая шпаргалка по всем 37 параграфам и 8–10 интегрированных боссов появятся в Phase 7 (после завершения всех 6 глав).</p>
|
||||
</div>
|
||||
<div id="fin-bosses-container" style="display:none"></div>`);
|
||||
|
||||
// Remove FIN_BOSSES array — заменим на пустой
|
||||
h = h.replace(/var FIN_BOSSES = \[[\s\S]*?\];/, 'var FIN_BOSSES = [];');
|
||||
|
||||
// final-head-sub
|
||||
h = h.replace(
|
||||
/<div class="final-head-sub">Итоговая шпаргалка и интегрированные боссы\. Победи всех — получи «Магистр алгебры 11» и \+50 XP\.<\/div>/,
|
||||
'<div class="final-head-sub">Шпаргалка курса и интегрированные боссы по всем 6 главам. В разработке (Phase 7).</div>'
|
||||
);
|
||||
h = h.replace(/<div class="final-cta-title">Курс Алгебра 11 пройден!<\/div>/, '<div class="final-cta-title">Курс Физика 10 пройден!</div>');
|
||||
h = h.replace(/«Магистр алгебры 11»/g, '«Магистр физики 10»');
|
||||
h = h.replace(/Магистр алгебры 11/g, 'Магистр физики 10');
|
||||
|
||||
// Footer
|
||||
h = h.replace(/Интерактивный учебник «Алгебра — 11 класс»/, 'Интерактивный учебник «Физика — 10 класс»');
|
||||
|
||||
// Achievement strip
|
||||
h = h.replace(/Прочитайте все 10 параграфов трёх глав/, 'Прочитайте все 37 параграфов курса, чтобы получить достижение');
|
||||
|
||||
// === 6. TOTAL + CH_PARA + CH_IDX ===
|
||||
h = h.replace(/var TOTAL = 10;[\s\S]*?var CH_IDX = \{[\s\S]*?\};/, `var TOTAL = 37;
|
||||
var CH_PARA = {
|
||||
'physics-10-ch1': 10,
|
||||
'physics-10-ch2': 5,
|
||||
'physics-10-ch3': 9,
|
||||
'physics-10-ch4': 2,
|
||||
'physics-10-ch5': 7,
|
||||
'physics-10-ch6': 4,
|
||||
};
|
||||
var CH_IDX = {
|
||||
'physics-10-ch1': 1,
|
||||
'physics-10-ch2': 2,
|
||||
'physics-10-ch3': 3,
|
||||
'physics-10-ch4': 4,
|
||||
'physics-10-ch5': 5,
|
||||
'physics-10-ch6': 6,
|
||||
};`);
|
||||
|
||||
// API endpoint slug
|
||||
h = h.replace(/'\/api\/textbooks\/algebra-11\/children'/, "'/api/textbooks/physics-10/children'");
|
||||
|
||||
// На текстах ачивок: "Вы прочитали весь курс алгебры 11 класса."
|
||||
h = h.replace(/Вы прочитали весь курс алгебры 11 класса\./, 'Вы прочитали весь курс физики 10 класса.');
|
||||
|
||||
fs.writeFileSync(DST, h);
|
||||
console.log('OK hub →', DST, 'bytes:', h.length);
|
||||
|
||||
// Quick sanity: extract <script> blocks and check parseable JS
|
||||
const scriptMatches = [...h.matchAll(/<script>([\s\S]*?)<\/script>/g)];
|
||||
console.log('inline <script> count:', scriptMatches.length);
|
||||
for (const m of scriptMatches) {
|
||||
try { new Function(m[1]); }
|
||||
catch(e) { console.error('JS PARSE FAIL:', e.message); process.exit(1); }
|
||||
}
|
||||
console.log('all inline JS parses OK');
|
||||
@@ -0,0 +1,94 @@
|
||||
import sys, os
|
||||
|
||||
src = os.path.join(os.path.dirname(__file__), '../../frontend/lab.html')
|
||||
|
||||
with open(src, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PATCH 1: Add animated ray buttons + lens-maker controls to lens panel
|
||||
# Insert BEFORE the Aberrации section
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
OLD_LENS = (
|
||||
' <div class="pp-hint">Тащи стрелку-предмет или фокус мышью</div>\n'
|
||||
' <div style="margin-top:8px"></div>\n'
|
||||
' <div class="gp-section-title" style="margin-bottom:6px">Аберрации</div>'
|
||||
)
|
||||
|
||||
NEW_LENS = (
|
||||
' <div class="pp-hint">Тащи стрелку-предмет или фокус мышью</div>\n'
|
||||
' <div style="margin-top:8px"></div>\n'
|
||||
' <!-- Feature 1: animated ray buttons -->\n'
|
||||
' <div style="display:flex;gap:4px;margin-bottom:8px">\n'
|
||||
' <button onclick="if(lensSim)lensSim.buildRays()" style="flex:1;padding:5px 0;border-radius:6px;border:none;background:linear-gradient(135deg,#06D6E0,#9B5DE5);color:#fff;font-size:.72rem;font-weight:700;cursor:pointer">Построить лучи</button>\n'
|
||||
' <button onclick="if(lensSim)lensSim.resetRays()" style="padding:5px 9px;border-radius:6px;border:1px solid #333;background:#1a1a2e;color:#888;font-size:.78rem;cursor:pointer" title="Сбросить лучи">↺</button>\n'
|
||||
' </div>\n'
|
||||
' <!-- Feature 3: lens-maker toggle -->\n'
|
||||
' <div style="margin-bottom:6px">\n'
|
||||
' <label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:#ccc;cursor:pointer">\n'
|
||||
' <input type="checkbox" id="ltog-lensmaker" onchange="lensToggleLM(this.checked)">\n'
|
||||
' Подробный (R1/R2/n)\n'
|
||||
' </label>\n'
|
||||
' </div>\n'
|
||||
' <!-- LM sliders (hidden by default) -->\n'
|
||||
' <div id="ob-lm-sliders" style="display:none">\n'
|
||||
' <div class="proj-slider-row" style="margin-bottom:6px">\n'
|
||||
' <label style="font-size:.72rem;color:#ccc;width:60px">R1 = <span id="lm-r1-val" style="color:#FFD166;font-weight:700">200</span></label>\n'
|
||||
' <input type="range" id="sl-lm-r1" min="-300" max="300" step="5" value="200" oninput="lensLMParam(\'R1\',this.value)" style="flex:1">\n'
|
||||
' </div>\n'
|
||||
' <div class="proj-slider-row" style="margin-bottom:6px">\n'
|
||||
' <label style="font-size:.72rem;color:#ccc;width:60px">R2 = <span id="lm-r2-val" style="color:#FFD166;font-weight:700">-200</span></label>\n'
|
||||
' <input type="range" id="sl-lm-r2" min="-300" max="300" step="5" value="-200" oninput="lensLMParam(\'R2\',this.value)" style="flex:1">\n'
|
||||
' </div>\n'
|
||||
' <div class="proj-slider-row" style="margin-bottom:8px">\n'
|
||||
' <label style="font-size:.72rem;color:#ccc;width:60px">n = <span id="lm-n-val" style="color:#9B5DE5;font-weight:700">1.50</span></label>\n'
|
||||
' <input type="range" id="sl-lm-n" min="1.3" max="2.4" step="0.05" value="1.5" oninput="lensLMParam(\'n\',this.value)" style="flex:1">\n'
|
||||
' </div>\n'
|
||||
' <div style="font-size:.68rem;color:#888;margin-bottom:6px">f = 1/((n-1)*(1/R1 - 1/R2))</div>\n'
|
||||
' </div>\n'
|
||||
' <div style="margin-top:0"></div>\n'
|
||||
' <div class="gp-section-title" style="margin-bottom:6px">Аберрации</div>'
|
||||
)
|
||||
|
||||
if OLD_LENS not in content:
|
||||
print('ERROR: lens panel marker not found'); exit(1)
|
||||
|
||||
content = content.replace(OLD_LENS, NEW_LENS, 1)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PATCH 2: Add R slider + parabolic toggle to mirror panel
|
||||
# Insert BEFORE the "Отображение" section
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
OLD_MIRROR = (
|
||||
' <div class="gp-section-title" style="margin-bottom:6px">Отображение</div>'
|
||||
)
|
||||
|
||||
NEW_MIRROR = (
|
||||
' <!-- Feature 2: R-slider + parabolic toggle -->\n'
|
||||
' <div style="margin-bottom:6px">\n'
|
||||
' <label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:#ccc;cursor:pointer">\n'
|
||||
' <input type="checkbox" id="mtog-useR" onchange="mirrorToggleR(this.checked)">\n'
|
||||
' Радиус R (непрерывный)\n'
|
||||
' </label>\n'
|
||||
' </div>\n'
|
||||
' <div id="ob-mirror-R-row" class="proj-slider-row" style="margin-bottom:6px;display:none">\n'
|
||||
' <label style="font-size:.72rem;color:#ccc;width:60px">R = <span id="mirror-R-val" style="color:var(--cyan);font-weight:700">240</span></label>\n'
|
||||
' <input type="range" id="sl-mirror-R" min="-250" max="250" step="5" value="240" oninput="mirrorRParam(this.value)" style="flex:1">\n'
|
||||
' </div>\n'
|
||||
' <div style="display:flex;gap:4px;margin-bottom:8px">\n'
|
||||
' <button id="mirror-parab-btn" onclick="mirrorToggleParabolic(this)" style="flex:1;padding:5px 0;border-radius:6px;border:1px solid #333;background:#1a1a2e;color:#888;font-size:.72rem;cursor:pointer">Сферическое</button>\n'
|
||||
' </div>\n'
|
||||
' <div class="gp-section-title" style="margin-bottom:6px">Отображение</div>'
|
||||
)
|
||||
|
||||
if OLD_MIRROR not in content:
|
||||
print('ERROR: mirror panel Отображение marker not found'); exit(1)
|
||||
|
||||
content = content.replace(OLD_MIRROR, NEW_MIRROR, 1)
|
||||
|
||||
with open(src, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print('OK')
|
||||
@@ -0,0 +1,409 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const targetFile = path.join(__dirname, '../../frontend/js/labs/opticsbench.js');
|
||||
|
||||
const ifSimCode = `/* ─────────────────────────────────────────────────────────────
|
||||
4d. INTERFERENCE SIM — Newton's rings / Thin film / Polarization
|
||||
Agent C — additive only, class InterferenceSim
|
||||
─────────────────────────────────────────────────────────────*/
|
||||
class InterferenceSim {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d');
|
||||
this.W = 0; this.H = 0;
|
||||
this.subMode = 'newton';
|
||||
// Newton rings
|
||||
this.nR = 200;
|
||||
this.nNmax = 12;
|
||||
// Thin film
|
||||
this.tfT = 400;
|
||||
this.tfN = 1.33;
|
||||
this.tfTheta = 0;
|
||||
this.tfPreset = 'soap';
|
||||
// Polarization
|
||||
this.polTheta = 45;
|
||||
this.polSrc = 'unpolarized';
|
||||
this._polTick = 0;
|
||||
this._polRaf = null;
|
||||
this.onUpdate = null;
|
||||
new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement || canvas);
|
||||
}
|
||||
|
||||
fit() {
|
||||
const p = this.canvas.parentElement;
|
||||
if (!p) return;
|
||||
const r = p.getBoundingClientRect();
|
||||
this.W = this.canvas.width = r.width || p.offsetWidth || 600;
|
||||
this.H = this.canvas.height = r.height || p.offsetHeight || 400;
|
||||
}
|
||||
|
||||
setSubMode(sm) {
|
||||
this.subMode = sm;
|
||||
if (sm === 'polarization') {
|
||||
this._polStart();
|
||||
} else {
|
||||
this._polStop();
|
||||
}
|
||||
this.draw();
|
||||
if (this.onUpdate) this.onUpdate();
|
||||
}
|
||||
|
||||
/* ── Newton Rings ──────────────────────────────────────── */
|
||||
_drawNewton() {
|
||||
const { ctx, W, H } = this;
|
||||
const nm = window._obWavelength || 550;
|
||||
const R = this.nR;
|
||||
const nMax = this.nNmax;
|
||||
const white = window._obWhiteLight;
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#08081a';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
const topH = Math.floor(H * 0.60);
|
||||
const cx = W / 2, cy = topH / 2;
|
||||
const maxR_mm = Math.sqrt(nMax * nm * 1e-6 * R);
|
||||
const scale = Math.min(cx * 0.85, cy * 0.85) / (maxR_mm || 1);
|
||||
|
||||
for (let n = nMax; n >= 0; n--) {
|
||||
const lambdas = white ? [420, 470, 510, 550, 590, 620, 680] : [nm];
|
||||
for (const lam of lambdas) {
|
||||
const rDark = Math.sqrt(n * lam * 1e-6 * R) * scale;
|
||||
const rBright = Math.sqrt((n + 0.5) * lam * 1e-6 * R) * scale;
|
||||
if (rDark > 0.5) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, rDark, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = white
|
||||
? wavelengthToRGB(lam).replace(')', ',0.5)').replace('rgb', 'rgba')
|
||||
: '#000000';
|
||||
ctx.lineWidth = white ? 1.2 : 1.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
if (rBright > 0.5) {
|
||||
const al = white ? 0.22 : 0.55;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, rBright, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = wavelengthToRGB(lam).replace(')', ',' + al + ')').replace('rgb', 'rgba');
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.beginPath(); ctx.arc(cx, cy, 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#000000'; ctx.fill();
|
||||
|
||||
if (window.LabFX && LabFX.glow && !white) {
|
||||
const r1b = Math.sqrt(0.5 * nm * 1e-6 * R) * scale;
|
||||
LabFX.glow.drawGlow(ctx, cx, cy, r1b, wavelengthToRGB(nm), 18);
|
||||
}
|
||||
|
||||
ctx.beginPath(); ctx.arc(cx, cy, maxR_mm * scale * 1.05, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#334455'; ctx.lineWidth = 1; ctx.stroke();
|
||||
|
||||
const crossY0 = topH + 8;
|
||||
const crossH = H - crossY0 - 40;
|
||||
if (crossH < 30) return;
|
||||
|
||||
ctx.fillStyle = '#0d0d20';
|
||||
ctx.fillRect(0, crossY0, W, crossH + 36);
|
||||
|
||||
const glassY = crossY0 + crossH - 10;
|
||||
ctx.fillStyle = '#1a3a5c';
|
||||
ctx.fillRect(cx - maxR_mm * scale * 1.1, glassY, maxR_mm * scale * 2.2, 10);
|
||||
|
||||
const sagitta = (maxR_mm * maxR_mm) / (2 * R);
|
||||
const sagPx = sagitta * scale;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, glassY - 1 - sagPx, maxR_mm * scale * 1.1, sagPx + 6, 0, 0, Math.PI);
|
||||
ctx.fillStyle = 'rgba(100,180,255,0.15)'; ctx.fill();
|
||||
ctx.strokeStyle = '#4499cc'; ctx.lineWidth = 1.5; ctx.stroke();
|
||||
|
||||
for (let n = 0; n <= nMax; n++) {
|
||||
const rD = Math.sqrt(n * nm * 1e-6 * R) * scale;
|
||||
if (rD < 1) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx + rD, glassY); ctx.lineTo(cx + rD, glassY + 8);
|
||||
ctx.moveTo(cx - rD, glassY); ctx.lineTo(cx - rD, glassY + 8);
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.lineWidth = 1; ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.font = '600 11px monospace'; ctx.fillStyle = '#667788'; ctx.textAlign = 'center';
|
||||
ctx.fillText('Cross-section', cx, crossY0 + 14);
|
||||
|
||||
const r1d = Math.sqrt(nm * 1e-6 * R).toFixed(3);
|
||||
this._drawHUD(ctx, W, H,
|
||||
'r1 = sqrt(lam*R) = ' + r1d + ' mm | R=' + R + 'mm | lam=' + nm + 'nm');
|
||||
}
|
||||
|
||||
/* ── Thin Film ─────────────────────────────────────────── */
|
||||
_thinFilmColor(t_nm, n_film, theta_deg) {
|
||||
const sinR = Math.sin(theta_deg * Math.PI / 180) / n_film;
|
||||
const cosR = Math.sqrt(Math.max(0, 1 - sinR * sinR));
|
||||
const opd = 2 * n_film * t_nm * cosR;
|
||||
let rS = 0, gS = 0, bS = 0;
|
||||
for (let lam = 380; lam <= 780; lam += 5) {
|
||||
const phase = Math.PI * opd / lam;
|
||||
const I = Math.cos(phase) * Math.cos(phase);
|
||||
const rgb = wavelengthToRGB(lam);
|
||||
const m = rgb.match(/\d+/g);
|
||||
if (!m) continue;
|
||||
rS += I * +m[0]; gS += I * +m[1]; bS += I * +m[2];
|
||||
}
|
||||
const sc = 255 / Math.max(rS, gS, bS, 1);
|
||||
return 'rgb(' + Math.round(rS * sc) + ',' + Math.round(gS * sc) + ',' + Math.round(bS * sc) + ')';
|
||||
}
|
||||
|
||||
_drawThinFilm() {
|
||||
const { ctx, W, H } = this;
|
||||
const t = this.tfT;
|
||||
const nf = this.tfN;
|
||||
const theta = this.tfTheta;
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#08081a';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
const midY = H * 0.40;
|
||||
const filmH = Math.max(28, H * 0.12);
|
||||
const margin = W * 0.10;
|
||||
const ang = theta * Math.PI / 180;
|
||||
const skew = Math.tan(ang) * filmH * 0.5;
|
||||
|
||||
const grad = ctx.createLinearGradient(margin, 0, W - margin, 0);
|
||||
for (let i = 0; i <= 20; i++) {
|
||||
const frac = i / 20;
|
||||
grad.addColorStop(frac, this._thinFilmColor(t * (0.3 + 0.7 * frac), nf, theta));
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(margin - skew, midY - filmH / 2);
|
||||
ctx.lineTo(W - margin - skew, midY - filmH / 2);
|
||||
ctx.lineTo(W - margin + skew, midY + filmH / 2);
|
||||
ctx.lineTo(margin + skew, midY + filmH / 2);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = grad; ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 1; ctx.stroke();
|
||||
ctx.restore();
|
||||
|
||||
ctx.font = '700 11px sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.7)';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('t=' + t + 'nm n=' + nf.toFixed(2), W / 2, midY);
|
||||
|
||||
const ax2 = W * 0.25, ay2 = midY - filmH / 2;
|
||||
const ax1 = ax2 - Math.cos(ang) * 40, ay1 = ay2 - Math.sin(ang) * 40 - 20;
|
||||
ctx.beginPath(); ctx.moveTo(ax1, ay1); ctx.lineTo(ax2, ay2);
|
||||
ctx.strokeStyle = '#8ab4e8'; ctx.lineWidth = 1.5; ctx.stroke();
|
||||
|
||||
const col = this._thinFilmColor(t, nf, theta);
|
||||
ctx.beginPath(); ctx.moveTo(ax2, ay2); ctx.lineTo(ax2 - Math.cos(ang) * 40, ay1);
|
||||
ctx.strokeStyle = col; ctx.lineWidth = 2; ctx.stroke();
|
||||
|
||||
const dx2 = Math.sin(ang) * filmH / nf;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(ax2 + dx2, ay2 + filmH);
|
||||
ctx.lineTo(ax2 + dx2 - Math.cos(ang) * 40, ay1 + filmH - 20);
|
||||
ctx.strokeStyle = col; ctx.lineWidth = 2;
|
||||
ctx.setLineDash([4, 3]); ctx.stroke(); ctx.setLineDash([]);
|
||||
|
||||
const tvX0 = W * 0.55, tvW2 = W * 0.38;
|
||||
const tvY0 = H * 0.05, tvH2 = H * 0.60;
|
||||
ctx.fillStyle = '#0d0d22'; ctx.strokeStyle = '#2a2a4a'; ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) ctx.roundRect(tvX0, tvY0, tvW2, tvH2, 8);
|
||||
else ctx.rect(tvX0, tvY0, tvW2, tvH2);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.font = '600 10px sans-serif'; ctx.fillStyle = '#555'; ctx.textAlign = 'center';
|
||||
ctx.fillText('Top view', tvX0 + tvW2 / 2, tvY0 + 14);
|
||||
|
||||
const tvRows = 28, tvCols = 36;
|
||||
const cW = tvW2 / tvCols, cH = (tvH2 - 20) / tvRows;
|
||||
for (let r = 0; r < tvRows; r++) {
|
||||
for (let c = 0; c < tvCols; c++) {
|
||||
ctx.fillStyle = this._thinFilmColor(t * (0.5 + c / tvCols), nf, theta * (r / tvRows));
|
||||
ctx.fillRect(tvX0 + c * cW, tvY0 + 20 + r * cH, cW + 0.5, cH + 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
const sinR2 = Math.sin(ang) / nf;
|
||||
const cosR2 = Math.sqrt(Math.max(0, 1 - sinR2 * sinR2));
|
||||
const opd2 = (2 * nf * t * cosR2).toFixed(0);
|
||||
this._drawHUD(ctx, W, H,
|
||||
'2nt*cos(th_r)=' + opd2 + 'nm | t=' + t + 'nm n=' + nf.toFixed(2) + ' th=' + theta + 'deg');
|
||||
}
|
||||
|
||||
/* ── Polarization ──────────────────────────────────────── */
|
||||
_polStart() {
|
||||
if (this._polRaf) return;
|
||||
const loop = () => { this._polTick++; this.draw(); this._polRaf = requestAnimationFrame(loop); };
|
||||
this._polRaf = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
_polStop() {
|
||||
if (this._polRaf) { cancelAnimationFrame(this._polRaf); this._polRaf = null; }
|
||||
}
|
||||
|
||||
_drawPolarization() {
|
||||
const { ctx, W, H } = this;
|
||||
const theta = this.polTheta * Math.PI / 180;
|
||||
const I_rel = Math.cos(theta) * Math.cos(theta);
|
||||
const tick = this._polTick;
|
||||
const white = window._obWhiteLight;
|
||||
const nm = window._obWavelength || 550;
|
||||
const beamCol = white ? '#ffffff' : wavelengthToRGB(nm);
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#08081a';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
const axisY = H * 0.45;
|
||||
const stH = H * 0.38;
|
||||
const st = [
|
||||
{ x: W * 0.12, label: 'Источник', isFilter: false },
|
||||
{ x: W * 0.38, label: 'Поляризатор P1', isFilter: true, angle: 0 },
|
||||
{ x: W * 0.64, label: 'Анализатор P2', isFilter: true, angle: this.polTheta },
|
||||
{ x: W * 0.88, label: 'Детектор', isFilter: false },
|
||||
];
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(st[0].x - 20, axisY); ctx.lineTo(st[3].x + 20, axisY);
|
||||
ctx.strokeStyle = '#1a1a35'; ctx.lineWidth = 1; ctx.stroke();
|
||||
|
||||
const segs = [
|
||||
{ x0: st[0].x, x1: st[1].x, amp: 1, unpol: this.polSrc === 'unpolarized', ang: 0 },
|
||||
{ x0: st[1].x, x1: st[2].x, amp: 1, unpol: false, ang: 0 },
|
||||
{ x0: st[2].x, x1: st[3].x, amp: I_rel, unpol: false, ang: this.polTheta },
|
||||
];
|
||||
|
||||
for (const seg of segs) {
|
||||
const nA = 20;
|
||||
const sdx = (seg.x1 - seg.x0) / nA;
|
||||
for (let i = 0; i <= nA; i++) {
|
||||
const bx = seg.x0 + i * sdx;
|
||||
const phase = (bx * 0.08 - tick * 0.04) % (Math.PI * 2);
|
||||
const bAmp = stH * 0.28 * seg.amp;
|
||||
if (seg.unpol) {
|
||||
for (let d = 0; d < 4; d++) {
|
||||
const a = d * Math.PI / 4;
|
||||
const oy = Math.sin(phase + d * 0.7) * bAmp;
|
||||
ctx.beginPath(); ctx.moveTo(bx, axisY);
|
||||
ctx.lineTo(bx + oy * Math.sin(a) * 0.25, axisY + oy * Math.cos(a));
|
||||
ctx.strokeStyle = 'rgba(200,200,255,0.22)'; ctx.lineWidth = 1; ctx.stroke();
|
||||
}
|
||||
} else {
|
||||
const oy = Math.sin(phase) * bAmp;
|
||||
const a = seg.ang * Math.PI / 180;
|
||||
const py = oy * Math.cos(a), px = oy * Math.sin(a) * 0.35;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bx - px, axisY - py); ctx.lineTo(bx + px, axisY + py);
|
||||
ctx.strokeStyle = (I_rel < 0.01 && seg.amp < 0.5)
|
||||
? 'rgba(80,80,120,0.5)'
|
||||
: beamCol.replace(')', ',0.75)').replace('rgb', 'rgba');
|
||||
ctx.lineWidth = 1.5; ctx.stroke();
|
||||
if (i % 3 === 0 && bAmp > 2) {
|
||||
ctx.beginPath(); ctx.arc(bx + px, axisY + py, 2, 0, Math.PI * 2);
|
||||
ctx.fillStyle = beamCol; ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const s of st) {
|
||||
if (!s.isFilter) continue;
|
||||
const a = s.angle * Math.PI / 180;
|
||||
ctx.save(); ctx.translate(s.x, axisY);
|
||||
ctx.fillStyle = 'rgba(80,120,200,0.18)';
|
||||
ctx.fillRect(-4, -stH / 2, 8, stH);
|
||||
ctx.strokeStyle = '#4466aa'; ctx.lineWidth = 1.5; ctx.strokeRect(-4, -stH / 2, 8, stH);
|
||||
const axLen = stH * 0.45;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-Math.sin(a) * axLen, -Math.cos(a) * axLen);
|
||||
ctx.lineTo( Math.sin(a) * axLen, Math.cos(a) * axLen);
|
||||
ctx.strokeStyle = '#7aaeff'; ctx.lineWidth = 2; ctx.stroke();
|
||||
ctx.restore();
|
||||
ctx.font = '700 10px monospace'; ctx.fillStyle = '#7aaeff'; ctx.textAlign = 'center';
|
||||
ctx.fillText(s.angle + 'deg', s.x, axisY + stH / 2 + 14);
|
||||
}
|
||||
|
||||
for (const s of st) {
|
||||
ctx.font = '600 10px sans-serif'; ctx.fillStyle = '#667788'; ctx.textAlign = 'center';
|
||||
ctx.fillText(s.label, s.x, axisY - stH / 2 - 8);
|
||||
}
|
||||
|
||||
const barX = W * 0.91, barW = 16;
|
||||
const barY0 = axisY - stH / 2;
|
||||
ctx.fillStyle = '#111122'; ctx.fillRect(barX, barY0, barW, stH);
|
||||
const fillH2 = stH * I_rel;
|
||||
if (fillH2 > 0) {
|
||||
const bg = ctx.createLinearGradient(barX, barY0 + stH - fillH2, barX, barY0 + stH);
|
||||
bg.addColorStop(0, beamCol); bg.addColorStop(1, 'rgba(0,0,0,0.2)');
|
||||
ctx.fillStyle = bg; ctx.fillRect(barX, barY0 + stH - fillH2, barW, fillH2);
|
||||
}
|
||||
ctx.strokeStyle = '#334455'; ctx.lineWidth = 1; ctx.strokeRect(barX, barY0, barW, stH);
|
||||
ctx.font = '600 9px monospace'; ctx.fillStyle = '#aaaaaa'; ctx.textAlign = 'center';
|
||||
ctx.fillText('I', barX + barW / 2, barY0 - 5);
|
||||
|
||||
if (this.polTheta >= 88) {
|
||||
ctx.font = '700 13px sans-serif'; ctx.fillStyle = '#EF476F'; ctx.textAlign = 'center';
|
||||
ctx.fillText('Полное гашение', W / 2, H * 0.85);
|
||||
}
|
||||
|
||||
ctx.font = '10px sans-serif'; ctx.fillStyle = '#444466'; ctx.textAlign = 'right';
|
||||
ctx.fillText('Угол Брюстера: отражённый свет поляризован (см. Преломление)', W - 10, H - 10);
|
||||
|
||||
const pct = (I_rel * 100).toFixed(1);
|
||||
this._drawHUD(ctx, W, H,
|
||||
'I/I0=cos2(th)=cos2(' + this.polTheta + 'deg)=' + I_rel.toFixed(3) + ' (' + pct + '%)');
|
||||
}
|
||||
|
||||
_drawHUD(ctx, W, H, text) {
|
||||
const pad = 8, fs = 11;
|
||||
ctx.font = '600 ' + fs + 'px monospace';
|
||||
const tw = ctx.measureText(text).width;
|
||||
const bx = (W - tw) / 2 - pad, by = H - 32;
|
||||
const bw = tw + pad * 2, bh = fs + pad * 2;
|
||||
ctx.fillStyle = 'rgba(10,10,30,0.82)';
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) ctx.roundRect(bx, by, bw, bh, 5);
|
||||
else ctx.rect(bx, by, bw, bh);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#c8d8ff';
|
||||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||||
ctx.fillText(text, bx + pad, by + bh / 2);
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.subMode === 'newton') this._drawNewton();
|
||||
else if (this.subMode === 'thinfilm') this._drawThinFilm();
|
||||
else if (this.subMode === 'polarization') this._drawPolarization();
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
const src = fs.readFileSync(targetFile, 'utf-8');
|
||||
const markerStr = '4c. SPECTROMETER PANEL';
|
||||
const markerIdx = src.indexOf(markerStr);
|
||||
if (markerIdx < 0) {
|
||||
console.error('ERROR: marker not found');
|
||||
process.exit(1);
|
||||
}
|
||||
const insertIdx = src.lastIndexOf('/*', markerIdx);
|
||||
if (insertIdx < 0) {
|
||||
console.error('ERROR: comment start not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check InterferenceSim not already present
|
||||
if (src.indexOf('class InterferenceSim') >= 0) {
|
||||
console.log('InterferenceSim already present — skipping JS insertion');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result = src.slice(0, insertIdx) + ifSimCode + src.slice(insertIdx);
|
||||
fs.writeFileSync(targetFile, result, 'utf-8');
|
||||
console.log('JS insertion OK. New size:', result.length);
|
||||
@@ -0,0 +1,235 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const targetFile = path.join(__dirname, '../../frontend/js/labs/opticsbench.js');
|
||||
let src = fs.readFileSync(targetFile, 'utf-8');
|
||||
|
||||
// --- 1. Add ifSim variable declaration after freeSim ---
|
||||
if (src.indexOf('var ifSim') < 0) {
|
||||
src = src.replace(
|
||||
'var freeSim = null; /* multi-lens free-build (Agent OB-A3) */',
|
||||
'var freeSim = null; /* multi-lens free-build (Agent OB-A3) */\r\nvar ifSim = null; /* interference/polarization (Agent C) */'
|
||||
);
|
||||
console.log('Added ifSim declaration');
|
||||
} else {
|
||||
console.log('ifSim declaration already exists');
|
||||
}
|
||||
|
||||
// --- 2. Add 'interf' to tab array in obSwitchMode ---
|
||||
const tabArrOld = "['lens', 'mirror', 'refraction', 'prism', 'freebuild', 'waves'].forEach(m => {";
|
||||
const tabArrNew = "['lens', 'mirror', 'refraction', 'prism', 'freebuild', 'waves', 'interf'].forEach(m => {";
|
||||
if (src.indexOf(tabArrNew) < 0) {
|
||||
if (src.indexOf(tabArrOld) >= 0) {
|
||||
src = src.replace(tabArrOld, tabArrNew);
|
||||
console.log('Updated tab array');
|
||||
} else {
|
||||
console.log('WARN: tab array old pattern not found');
|
||||
}
|
||||
} else {
|
||||
console.log('Tab array already updated');
|
||||
}
|
||||
|
||||
// --- 3. Add 'ob-ctrl-interf' to control panels array ---
|
||||
const ctrlArrOld = "['ob-ctrl-lens', 'ob-ctrl-mirror', 'ob-ctrl-refraction', 'ob-ctrl-prism', 'ob-ctrl-freebuild', 'ob-ctrl-waves'].forEach(id => {";
|
||||
const ctrlArrNew = "['ob-ctrl-lens', 'ob-ctrl-mirror', 'ob-ctrl-refraction', 'ob-ctrl-prism', 'ob-ctrl-freebuild', 'ob-ctrl-waves', 'ob-ctrl-interf'].forEach(id => {";
|
||||
if (src.indexOf(ctrlArrNew) < 0) {
|
||||
if (src.indexOf(ctrlArrOld) >= 0) {
|
||||
src = src.replace(ctrlArrOld, ctrlArrNew);
|
||||
console.log('Updated ctrl panel array');
|
||||
} else {
|
||||
console.log('WARN: ctrl panel array old pattern not found');
|
||||
}
|
||||
} else {
|
||||
console.log('Ctrl panel array already updated');
|
||||
}
|
||||
|
||||
// --- 4. Add 'ob-stats-interf' to stats array ---
|
||||
const statsArrOld = "['ob-stats-lens', 'ob-stats-mirror', 'ob-stats-refr', 'ob-stats-prism', 'ob-stats-freebuild', 'ob-stats-waves'].forEach(id => {";
|
||||
const statsArrNew = "['ob-stats-lens', 'ob-stats-mirror', 'ob-stats-refr', 'ob-stats-prism', 'ob-stats-freebuild', 'ob-stats-waves', 'ob-stats-interf'].forEach(id => {";
|
||||
if (src.indexOf(statsArrNew) < 0) {
|
||||
if (src.indexOf(statsArrOld) >= 0) {
|
||||
src = src.replace(statsArrOld, statsArrNew);
|
||||
console.log('Updated stats array');
|
||||
} else {
|
||||
console.log('WARN: stats array old pattern not found');
|
||||
}
|
||||
} else {
|
||||
console.log('Stats array already updated');
|
||||
}
|
||||
|
||||
// --- 5. Add 'ob-interf-canvas' to canvas arrays ---
|
||||
const canvasIdsOld = "const canvasIds = ['ob-lens-canvas', 'ob-mirror-canvas', 'ob-refr-canvas', 'ob-prism-canvas', 'ob-free-canvas', 'ob-waves-canvas'];";
|
||||
const canvasIdsNew = "const canvasIds = ['ob-lens-canvas', 'ob-mirror-canvas', 'ob-refr-canvas', 'ob-prism-canvas', 'ob-free-canvas', 'ob-waves-canvas', 'ob-interf-canvas'];";
|
||||
if (src.indexOf(canvasIdsNew) < 0) {
|
||||
if (src.indexOf(canvasIdsOld) >= 0) {
|
||||
src = src.replace(canvasIdsOld, canvasIdsNew);
|
||||
console.log('Updated canvasIds');
|
||||
} else {
|
||||
console.log('WARN: canvasIds old pattern not found');
|
||||
}
|
||||
} else {
|
||||
console.log('canvasIds already updated');
|
||||
}
|
||||
|
||||
const modeCanvasOld = "const modeCanvas = { lens: 'ob-lens-canvas', mirror: 'ob-mirror-canvas', refraction: 'ob-refr-canvas', prism: 'ob-prism-canvas', freebuild: 'ob-free-canvas', waves: 'ob-waves-canvas' };";
|
||||
const modeCanvasNew = "const modeCanvas = { lens: 'ob-lens-canvas', mirror: 'ob-mirror-canvas', refraction: 'ob-refr-canvas', prism: 'ob-prism-canvas', freebuild: 'ob-free-canvas', waves: 'ob-waves-canvas', interf: 'ob-interf-canvas' };";
|
||||
if (src.indexOf(modeCanvasNew) < 0) {
|
||||
if (src.indexOf(modeCanvasOld) >= 0) {
|
||||
src = src.replace(modeCanvasOld, modeCanvasNew);
|
||||
console.log('Updated modeCanvas');
|
||||
} else {
|
||||
console.log('WARN: modeCanvas old pattern not found');
|
||||
}
|
||||
} else {
|
||||
console.log('modeCanvas already updated');
|
||||
}
|
||||
|
||||
// --- 6. Add 'interf' case in obSwitchMode, before closing brace ---
|
||||
const interfCaseStr = ` } else if (mode === 'interf') { /* Agent C — interference / polarization */\r\n if (!ifSim) {\r\n const cv = document.getElementById('ob-interf-canvas');\r\n if (cv) { ifSim = new InterferenceSim(cv); ifSim.onUpdate = _ifUpdateUI; }\r\n }\r\n if (ifSim) { ifSim.fit(); ifSim.draw(); }\r\n _ifUpdateUI();\r\n }\r\n}`;
|
||||
|
||||
const oldModeEnd = ` } else if (mode === 'waves') { /* Agent B1 — diffraction & interference */\r\n if (!diffrSim) {\r\n const cv = document.getElementById('ob-waves-canvas');\r\n if (cv) diffrSim = new DiffractionSim(cv);\r\n }\r\n if (diffrSim) {\r\n diffrSim.fit();\r\n diffrSim.draw();\r\n diffrSim._updateHUD();\r\n }\r\n }\r\n}`;
|
||||
|
||||
const newModeEnd = ` } else if (mode === 'waves') { /* Agent B1 — diffraction & interference */\r\n if (!diffrSim) {\r\n const cv = document.getElementById('ob-waves-canvas');\r\n if (cv) diffrSim = new DiffractionSim(cv);\r\n }\r\n if (diffrSim) {\r\n diffrSim.fit();\r\n diffrSim.draw();\r\n diffrSim._updateHUD();\r\n }\r\n } else if (mode === 'interf') { /* Agent C — interference / polarization */\r\n if (!ifSim) {\r\n const cv = document.getElementById('ob-interf-canvas');\r\n if (cv) { ifSim = new InterferenceSim(cv); ifSim.onUpdate = _ifUpdateUI; }\r\n }\r\n if (ifSim) { ifSim.fit(); ifSim.draw(); }\r\n _ifUpdateUI();\r\n }\r\n}`;
|
||||
|
||||
if (src.indexOf('else if (mode === \'interf\')') < 0) {
|
||||
if (src.indexOf(oldModeEnd) >= 0) {
|
||||
src = src.replace(oldModeEnd, newModeEnd);
|
||||
console.log('Added interf case in obSwitchMode');
|
||||
} else {
|
||||
// Try with LF only
|
||||
const oldLF = oldModeEnd.replace(/\r\n/g, '\n');
|
||||
const newLF = newModeEnd.replace(/\r\n/g, '\n');
|
||||
if (src.indexOf(oldLF) >= 0) {
|
||||
src = src.replace(oldLF, newLF);
|
||||
console.log('Added interf case (LF variant)');
|
||||
} else {
|
||||
console.log('WARN: waves mode end pattern not found - trying fallback');
|
||||
// Fallback: find closing brace of obSwitchMode after diffrSim block
|
||||
const marker2 = "diffrSim._updateHUD();\n }\n }\n}";
|
||||
const marker2cr = "diffrSim._updateHUD();\r\n }\r\n }\r\n}";
|
||||
const repl2 = "diffrSim._updateHUD();\n }\n } else if (mode === 'interf') {\n if (!ifSim) {\n const cv = document.getElementById('ob-interf-canvas');\n if (cv) { ifSim = new InterferenceSim(cv); ifSim.onUpdate = _ifUpdateUI; }\n }\n if (ifSim) { ifSim.fit(); ifSim.draw(); }\n _ifUpdateUI();\n }\n}";
|
||||
const repl2cr = "diffrSim._updateHUD();\r\n }\r\n } else if (mode === 'interf') {\r\n if (!ifSim) {\r\n const cv = document.getElementById('ob-interf-canvas');\r\n if (cv) { ifSim = new InterferenceSim(cv); ifSim.onUpdate = _ifUpdateUI; }\r\n }\r\n if (ifSim) { ifSim.fit(); ifSim.draw(); }\r\n _ifUpdateUI();\r\n }\r\n}";
|
||||
if (src.indexOf(marker2) >= 0) {
|
||||
src = src.replace(marker2, repl2);
|
||||
console.log('Added interf case (fallback LF)');
|
||||
} else if (src.indexOf(marker2cr) >= 0) {
|
||||
src = src.replace(marker2cr, repl2cr);
|
||||
console.log('Added interf case (fallback CRLF)');
|
||||
} else {
|
||||
console.log('WARN: all fallbacks failed for interf case');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('interf case already exists');
|
||||
}
|
||||
|
||||
// --- 7. Add _ifUpdateUI function and control functions ---
|
||||
const ifUICode = `
|
||||
/* ── Interference mode UI callbacks (Agent C) ── */
|
||||
function _ifUpdateUI() {
|
||||
if (!ifSim) return;
|
||||
const subMode = ifSim.subMode;
|
||||
['if-ctrl-newton', 'if-ctrl-thinfilm', 'if-ctrl-polarization'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = 'none';
|
||||
});
|
||||
const active = document.getElementById('if-ctrl-' + subMode);
|
||||
if (active) active.style.display = '';
|
||||
['if-sub-newton', 'if-sub-thinfilm', 'if-sub-polarization'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.classList.toggle('active', id === 'if-sub-' + subMode);
|
||||
});
|
||||
}
|
||||
|
||||
function ifSwitchSub(sub) {
|
||||
if (window.LabFX) LabFX.sound.play('chime');
|
||||
if (!ifSim) return;
|
||||
ifSim.setSubMode(sub);
|
||||
_ifUpdateUI();
|
||||
}
|
||||
|
||||
function ifNewtParam(key, val) {
|
||||
if (!ifSim) return;
|
||||
const v = parseFloat(val);
|
||||
if (key === 'R') { ifSim.nR = v; document.getElementById('if-newton-r-val').textContent = v; }
|
||||
else if (key === 'nmax') { ifSim.nNmax = Math.round(v); document.getElementById('if-newton-n-val').textContent = Math.round(v); }
|
||||
ifSim.draw();
|
||||
}
|
||||
|
||||
function ifThinFilmParam(key, val) {
|
||||
if (!ifSim) return;
|
||||
const v = parseFloat(val);
|
||||
if (key === 't') { ifSim.tfT = v; document.getElementById('if-tf-t-val').textContent = v; }
|
||||
else if (key === 'n') { ifSim.tfN = v; document.getElementById('if-tf-n-val').textContent = v.toFixed(2); }
|
||||
else if (key === 'theta') { ifSim.tfTheta = v; document.getElementById('if-tf-th-val').textContent = v; }
|
||||
ifSim.draw();
|
||||
}
|
||||
|
||||
function ifThinFilmPreset(name) {
|
||||
if (!ifSim) return;
|
||||
const presets = {
|
||||
soap: { n: 1.33, label: 'Мыльная плёнка' },
|
||||
oil: { n: 1.50, label: 'Масло на воде' },
|
||||
coating: { n: 1.38, label: 'Антибликовое покрытие' },
|
||||
};
|
||||
const p = presets[name];
|
||||
if (!p) return;
|
||||
ifSim.tfN = p.n;
|
||||
ifSim.tfPreset = name;
|
||||
const slN = document.getElementById('sl-if-tf-n');
|
||||
if (slN) slN.value = p.n;
|
||||
const lbN = document.getElementById('if-tf-n-val');
|
||||
if (lbN) lbN.textContent = p.n.toFixed(2);
|
||||
ifSim.draw();
|
||||
if (window.LabFX) LabFX.sound.play('chime');
|
||||
}
|
||||
|
||||
function ifPolParam(key, val) {
|
||||
if (!ifSim) return;
|
||||
const v = parseFloat(val);
|
||||
if (key === 'theta') { ifSim.polTheta = v; document.getElementById('if-pol-th-val').textContent = v; }
|
||||
ifSim.draw();
|
||||
}
|
||||
|
||||
function ifPolSrc(val) {
|
||||
if (!ifSim) return;
|
||||
ifSim.polSrc = val;
|
||||
ifSim.draw();
|
||||
}
|
||||
`;
|
||||
|
||||
if (src.indexOf('function _ifUpdateUI') < 0) {
|
||||
// Insert before the closing of the file or before _obRedraw
|
||||
const insertBefore = 'function _obRedraw()';
|
||||
const insertIdx = src.indexOf(insertBefore);
|
||||
if (insertIdx >= 0) {
|
||||
src = src.slice(0, insertIdx) + ifUICode + '\r\n' + src.slice(insertIdx);
|
||||
console.log('Added _ifUpdateUI and control functions');
|
||||
} else {
|
||||
src = src + '\r\n' + ifUICode;
|
||||
console.log('Appended _ifUpdateUI at end');
|
||||
}
|
||||
} else {
|
||||
console.log('_ifUpdateUI already exists');
|
||||
}
|
||||
|
||||
// --- 8. Make _obRedraw also redraw ifSim if active ---
|
||||
const redrawOld = 'function _obRedraw() {';
|
||||
const redrawNew = 'function _obRedraw() {';
|
||||
// Find _obRedraw body and add ifSim redraw
|
||||
if (src.indexOf('if (ifSim && _obMode') < 0) {
|
||||
const marker3 = "if (prismSim && _obMode === 'prism')";
|
||||
const repl3 = "if (ifSim && _obMode === 'interf') { ifSim.draw(); }\r\n if (prismSim && _obMode === 'prism')";
|
||||
const marker3lf = "if (prismSim && _obMode === 'prism')";
|
||||
if (src.indexOf(marker3) >= 0) {
|
||||
src = src.replace(marker3, repl3);
|
||||
console.log('Added ifSim redraw to _obRedraw');
|
||||
} else {
|
||||
console.log('WARN: could not find _obRedraw prism marker');
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetFile, src, 'utf-8');
|
||||
console.log('Done. New size:', src.length);
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const targetFile = path.join(__dirname, '../../frontend/js/labs/opticsbench.js');
|
||||
let src = fs.readFileSync(targetFile, 'utf-8');
|
||||
|
||||
// Add ifSim to _obRedraw
|
||||
const oldRedrawLine = " if (_obMode === 'waves' && diffrSim) { diffrSim.draw(); diffrSim._updateHUD(); }";
|
||||
const newRedrawLine = " if (_obMode === 'waves' && diffrSim) { diffrSim.draw(); diffrSim._updateHUD(); }\r\n if (_obMode === 'interf' && ifSim) { ifSim.draw(); }";
|
||||
|
||||
if (src.indexOf(newRedrawLine) < 0 && src.indexOf(oldRedrawLine) >= 0) {
|
||||
src = src.replace(oldRedrawLine, newRedrawLine);
|
||||
console.log('Added ifSim to _obRedraw');
|
||||
} else {
|
||||
console.log('ifSim redraw already present or old line not found');
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetFile, src, 'utf-8');
|
||||
console.log('Done. Size:', src.length);
|
||||
@@ -0,0 +1,187 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const targetFile = path.join(__dirname, '../../frontend/lab.html');
|
||||
let src = fs.readFileSync(targetFile, 'utf-8');
|
||||
|
||||
// --- 1. Add «Интерференция» tab button after Призма ---
|
||||
const tabPrism = '<button id="ob-tab-prism" onclick="obSwitchMode(\'prism\')" class="ob-tab" style="flex:1;padding:8px 0;border:none;background:transparent;color:#ccc;font-size:.78rem;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s">Призма</button>';
|
||||
const tabInterf = '\n <button id="ob-tab-interf" onclick="obSwitchMode(\'interf\')" class="ob-tab" style="flex:1;padding:8px 0;border:none;background:transparent;color:#ccc;font-size:.78rem;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s">Интерференция</button>';
|
||||
|
||||
if (src.indexOf('ob-tab-interf') < 0) {
|
||||
if (src.indexOf(tabPrism) >= 0) {
|
||||
src = src.replace(tabPrism, tabPrism + tabInterf);
|
||||
console.log('Added Интерференция tab button');
|
||||
} else {
|
||||
console.log('WARN: Prizm tab not found');
|
||||
}
|
||||
} else {
|
||||
console.log('Интерференция tab already present');
|
||||
}
|
||||
|
||||
// --- 2. Add ob-ctrl-interf control panel after ob-ctrl-freebuild ---
|
||||
// Find end of ob-ctrl-freebuild div (find the closing tag)
|
||||
const ctrlFreeEnd = ' <div class="pp-hint">Тащи линзы или предмет по оси мышью</div>\n </div>';
|
||||
const ctrlFreeEndCR = ' <div class="pp-hint">Тащи линзы или предмет по оси мышью</div>\r\n </div>';
|
||||
|
||||
const ctrlInterfHTML = `
|
||||
<!-- ── Interference control panel (Agent C) ── -->
|
||||
<div id="ob-ctrl-interf" class="proj-panel" style="width:240px;gap:0;flex-shrink:0;display:none">
|
||||
<!-- Sub-mode buttons -->
|
||||
<div class="gp-section-title" style="margin-bottom:6px">Эксперимент</div>
|
||||
<div style="display:flex;gap:3px;margin-bottom:10px;flex-wrap:wrap">
|
||||
<button id="if-sub-newton" class="preset-btn active" onclick="ifSwitchSub('newton')" style="font-size:.7rem;flex:1">Кольца Ньютона</button>
|
||||
<button id="if-sub-thinfilm" class="preset-btn" onclick="ifSwitchSub('thinfilm')" style="font-size:.7rem;flex:1">Тонкая плёнка</button>
|
||||
<button id="if-sub-polarization" class="preset-btn" onclick="ifSwitchSub('polarization')" style="font-size:.7rem;flex:1">Поляризация</button>
|
||||
</div>
|
||||
<!-- Newton rings controls -->
|
||||
<div id="if-ctrl-newton">
|
||||
<div class="gp-section-title" style="margin-bottom:6px">Кольца Ньютона</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">R = <span id="if-newton-r-val" style="color:var(--cyan);font-weight:700">200</span> мм</label>
|
||||
<input type="range" id="sl-if-newton-r" min="50" max="500" step="10" value="200" oninput="ifNewtParam('R',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">n = <span id="if-newton-n-val" style="color:#FFD166;font-weight:700">12</span></label>
|
||||
<input type="range" id="sl-if-newton-n" min="4" max="20" step="1" value="12" oninput="ifNewtParam('nmax',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="pp-hint">r_n(dark) = sqrt(n*lambda*R)</div>
|
||||
</div>
|
||||
<!-- Thin film controls -->
|
||||
<div id="if-ctrl-thinfilm" style="display:none">
|
||||
<div class="gp-section-title" style="margin-bottom:6px">Тонкая плёнка</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:60px">t = <span id="if-tf-t-val" style="color:var(--cyan);font-weight:700">400</span></label>
|
||||
<input type="range" id="sl-if-tf-t" min="50" max="2000" step="10" value="400" oninput="ifThinFilmParam('t',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:60px">n = <span id="if-tf-n-val" style="color:#FFD166;font-weight:700">1.33</span></label>
|
||||
<input type="range" id="sl-if-tf-n" min="1.0" max="2.5" step="0.01" value="1.33" oninput="ifThinFilmParam('n',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:60px">θ = <span id="if-tf-th-val" style="color:#EF476F;font-weight:700">0</span>°</label>
|
||||
<input type="range" id="sl-if-tf-th" min="0" max="60" step="1" value="0" oninput="ifThinFilmParam('theta',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="gp-section-title" style="margin-bottom:4px">Пресет</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:6px">
|
||||
<button class="preset-btn" onclick="ifThinFilmPreset('soap')" style="font-size:.68rem">Мыльная n=1.33</button>
|
||||
<button class="preset-btn" onclick="ifThinFilmPreset('oil')" style="font-size:.68rem">Масло n=1.50</button>
|
||||
<button class="preset-btn" onclick="ifThinFilmPreset('coating')" style="font-size:.68rem">Покрытие n=1.38</button>
|
||||
</div>
|
||||
<div class="pp-hint">2nt·cosθr = (m+0.5)λ — максимум</div>
|
||||
</div>
|
||||
<!-- Polarization controls -->
|
||||
<div id="if-ctrl-polarization" style="display:none">
|
||||
<div class="gp-section-title" style="margin-bottom:6px">Поляризация (Малюс)</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:60px">θ = <span id="if-pol-th-val" style="color:var(--cyan);font-weight:700">45</span>°</label>
|
||||
<input type="range" id="sl-if-pol-th" min="0" max="90" step="1" value="45" oninput="ifPolParam('theta',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div style="margin-bottom:8px">
|
||||
<label style="font-size:.72rem;color:#ccc;display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="radio" name="if-pol-src" value="unpolarized" checked onchange="ifPolSrc(this.value)" style="accent-color:var(--violet)">
|
||||
Неполяризованный
|
||||
</label>
|
||||
<label style="font-size:.72rem;color:#ccc;display:flex;align-items:center;gap:6px;cursor:pointer;margin-top:4px">
|
||||
<input type="radio" name="if-pol-src" value="polarized" onchange="ifPolSrc(this.value)" style="accent-color:var(--violet)">
|
||||
Поляризованный
|
||||
</label>
|
||||
</div>
|
||||
<div class="pp-hint">I = I₀·cos²(θ)</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (src.indexOf('ob-ctrl-interf') < 0) {
|
||||
let replaced = false;
|
||||
if (src.indexOf(ctrlFreeEnd) >= 0) {
|
||||
src = src.replace(ctrlFreeEnd, ctrlFreeEnd + ctrlInterfHTML);
|
||||
replaced = true;
|
||||
} else if (src.indexOf(ctrlFreeEndCR) >= 0) {
|
||||
src = src.replace(ctrlFreeEndCR, ctrlFreeEndCR + ctrlInterfHTML);
|
||||
replaced = true;
|
||||
}
|
||||
if (replaced) {
|
||||
console.log('Added ob-ctrl-interf panel');
|
||||
} else {
|
||||
console.log('WARN: freebuild ctrl end not found, trying alternate pattern');
|
||||
// Try finding the shared canvas area comment
|
||||
const canvasAreaMarker = '<!-- ── Shared canvas area';
|
||||
const idx = src.indexOf(canvasAreaMarker);
|
||||
if (idx >= 0) {
|
||||
src = src.slice(0, idx) + ctrlInterfHTML + '\n ' + src.slice(idx);
|
||||
console.log('Added ob-ctrl-interf before canvas area');
|
||||
} else {
|
||||
console.log('WARN: canvas area marker not found');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('ob-ctrl-interf already present');
|
||||
}
|
||||
|
||||
// --- 3. Add ob-interf-canvas after ob-waves-canvas ---
|
||||
const wavesCanvas = '<canvas id="ob-waves-canvas"';
|
||||
const interfCanvas = '\n <canvas id="ob-interf-canvas" style="position:absolute;top:0;left:0;width:100%;height:100%;display:none"></canvas>';
|
||||
if (src.indexOf('ob-interf-canvas') < 0) {
|
||||
// find the waves canvas line
|
||||
const wIdx = src.indexOf(wavesCanvas);
|
||||
if (wIdx >= 0) {
|
||||
// find end of that line
|
||||
const eol = src.indexOf('>', wIdx) + 1;
|
||||
const afterLine = src.indexOf('\n', eol);
|
||||
src = src.slice(0, afterLine) + interfCanvas + src.slice(afterLine);
|
||||
console.log('Added ob-interf-canvas');
|
||||
} else {
|
||||
// fallback: add after ob-free-canvas
|
||||
const freeCanvas = '<canvas id="ob-free-canvas"';
|
||||
const fIdx = src.indexOf(freeCanvas);
|
||||
if (fIdx >= 0) {
|
||||
const eol2 = src.indexOf('\n', src.indexOf('>', fIdx));
|
||||
src = src.slice(0, eol2) + interfCanvas + src.slice(eol2);
|
||||
console.log('Added ob-interf-canvas (after free)');
|
||||
} else {
|
||||
console.log('WARN: canvas insertion point not found');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('ob-interf-canvas already present');
|
||||
}
|
||||
|
||||
// --- 4. Add ob-stats-interf in stats bar (after ob-stats-prism) ---
|
||||
const prismStats = '</div>\n </div>\n </div>\n\n <!-- ── ISOPROCESS';
|
||||
const prismStatsCR = '</div>\r\n </div>\r\n </div>\r\n\r\n <!-- ── ISOPROCESS';
|
||||
const interfStats = `
|
||||
<div id="ob-stats-interf" style="display:none;flex:1;gap:0">
|
||||
<div class="pstat"><div class="pstat-label">Режим</div><div class="pstat-val" id="ifbar-sub" style="color:var(--cyan)">Кольца</div></div>
|
||||
<div class="pstat"><div class="pstat-label">λ</div><div class="pstat-val" id="ifbar-wl" style="color:#FFFFFF">550 нм</div></div>
|
||||
</div>`;
|
||||
|
||||
if (src.indexOf('ob-stats-interf') < 0) {
|
||||
// find the closing of ob-stats-prism section
|
||||
const prismStatsBlock = '<div id="ob-stats-prism"';
|
||||
const pIdx = src.indexOf(prismStatsBlock);
|
||||
if (pIdx >= 0) {
|
||||
// find the closing </div> of this section then the closing </div> of statsbar div
|
||||
let depth = 0, i = pIdx;
|
||||
while (i < src.length) {
|
||||
if (src[i] === '<' && src.slice(i, i+5) === '<div ') depth++;
|
||||
if (src[i] === '<' && src.slice(i, i+6) === '</div>') {
|
||||
if (depth <= 1) {
|
||||
// found end of ob-stats-prism
|
||||
const afterDiv = src.indexOf('>', i) + 1;
|
||||
src = src.slice(0, afterDiv) + interfStats + src.slice(afterDiv);
|
||||
console.log('Added ob-stats-interf');
|
||||
break;
|
||||
}
|
||||
depth--;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
console.log('WARN: ob-stats-prism not found');
|
||||
}
|
||||
} else {
|
||||
console.log('ob-stats-interf already present');
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetFile, src, 'utf-8');
|
||||
console.log('HTML patching done. New size:', src.length);
|
||||
@@ -0,0 +1,204 @@
|
||||
import sys, os
|
||||
|
||||
src = os.path.join(os.path.dirname(__file__), '../../frontend/js/labs/opticsbench.js')
|
||||
|
||||
with open(src, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PATCH 1: Insert _drawRaysAnimated + _drawArrowLabels before _bindEvents
|
||||
# in ThinLensSim
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
MARKER_BIND = ' _bindEvents() {\n const cv = this.canvas;\n const getPos = (e) => {'
|
||||
idx = content.find(MARKER_BIND)
|
||||
if idx == -1:
|
||||
print('ERROR: _bindEvents marker not found'); sys.exit(1)
|
||||
|
||||
NEW_METHODS_1 = (
|
||||
' /* === _drawRaysAnimated: principal rays with per-ray progress === */\n'
|
||||
' _drawRaysAnimated(ctx, lx, ay, d, h, f, dPrime, hPrime) {\n'
|
||||
' const T = this._rayAnimT;\n'
|
||||
' if (T[0] >= 1 && T[1] >= 1 && T[2] >= 1) { this._drawRays(ctx, lx, ay, d, h, f, dPrime, hPrime); return; }\n'
|
||||
' const objX = lx - d, objY = ay - h;\n'
|
||||
' const hasImage = dPrime !== null && isFinite(dPrime);\n'
|
||||
' const isVirtual = hasImage && dPrime < 0;\n'
|
||||
' const COLORS = [\'#06D6E0\', \'#7BF5A4\', \'#FFD166\'];\n'
|
||||
' ctx.lineWidth = 1.8;\n'
|
||||
' const lerp = (a, b, t) => a + (b - a) * Math.min(1, Math.max(0, t));\n'
|
||||
' const drawPts = (color, pts, t) => {\n'
|
||||
' if (t <= 0 || pts.length < 2) return;\n'
|
||||
' const totalLen = pts.reduce((s, p, i) => i === 0 ? 0 : s + Math.hypot(p[0]-pts[i-1][0], p[1]-pts[i-1][1]), 0);\n'
|
||||
' const target = totalLen * t;\n'
|
||||
' const draw = () => {\n'
|
||||
' ctx.strokeStyle = color; ctx.setLineDash([]);\n'
|
||||
' ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]);\n'
|
||||
' let drawn = 0;\n'
|
||||
' for (let i = 1; i < pts.length; i++) {\n'
|
||||
' const segLen = Math.hypot(pts[i][0]-pts[i-1][0], pts[i][1]-pts[i-1][1]);\n'
|
||||
' if (drawn + segLen <= target) { ctx.lineTo(pts[i][0], pts[i][1]); drawn += segLen; }\n'
|
||||
' else { const fr = segLen > 0 ? (target - drawn) / segLen : 0; ctx.lineTo(lerp(pts[i-1][0], pts[i][0], fr), lerp(pts[i-1][1], pts[i][1], fr)); break; }\n'
|
||||
' }\n'
|
||||
' ctx.stroke();\n'
|
||||
' };\n'
|
||||
' if (window.LabFX) LabFX.glow.drawGlow(ctx, draw, { color, intensity: 10 });\n'
|
||||
' else draw();\n'
|
||||
' };\n'
|
||||
' const FAR = lx + 360;\n'
|
||||
' const imgX = hasImage ? lx + dPrime : null, imgY = hasImage ? ay - hPrime : null;\n'
|
||||
' // Ray 1: parallel to axis -> through F\'\n'
|
||||
' if (T[0] > 0) {\n'
|
||||
' let pts;\n'
|
||||
' if (!hasImage) { pts = [[objX, objY], [lx, objY], [FAR, objY]]; }\n'
|
||||
' else if (!isVirtual) { pts = [[objX, objY], [lx, objY], [imgX, imgY]]; }\n'
|
||||
' else { const s = (objY - ay) / f; pts = [[objX, objY], [lx, objY], [FAR, objY + s*(FAR-lx)]]; }\n'
|
||||
' drawPts(COLORS[0], pts, T[0]);\n'
|
||||
' }\n'
|
||||
' // Ray 2: through optical center (straight)\n'
|
||||
' if (T[1] > 0) {\n'
|
||||
' const s = (objY - ay) / (objX - lx);\n'
|
||||
' drawPts(COLORS[1], [[objX, objY], [FAR, ay + s*(FAR-lx)]], T[1]);\n'
|
||||
' }\n'
|
||||
' // Ray 3: through front focus F -> parallel after lens\n'
|
||||
' if (T[2] > 0) {\n'
|
||||
' const fx = lx - f, s = (objY - ay) / (objX - fx);\n'
|
||||
' const hitY = objY + s * (lx - objX);\n'
|
||||
' const endX = hasImage && !isVirtual ? Math.max(imgX + 60, FAR) : FAR;\n'
|
||||
' drawPts(COLORS[2], [[objX, objY], [lx, hitY], [endX, hitY]], T[2]);\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' /* === Arrow labels: h_o, h_i, magnification Gamma === */\n'
|
||||
' _drawArrowLabels(ctx, lx, ay, d, h, dPrime, hPrime) {\n'
|
||||
' const objX = lx - d;\n'
|
||||
' ctx.font = \'11px Manrope, system-ui, sans-serif\'; ctx.textBaseline = \'middle\';\n'
|
||||
' ctx.fillStyle = \'rgba(155,93,229,0.85)\'; ctx.textAlign = \'right\';\n'
|
||||
' ctx.fillText(\'ho=\' + h.toFixed(0), objX - 6, ay - h / 2);\n'
|
||||
' if (dPrime !== null && isFinite(dPrime)) {\n'
|
||||
' const imgX = lx + dPrime, isVirtual = dPrime < 0;\n'
|
||||
' const M = -dPrime / d;\n'
|
||||
' const Gstr = isFinite(M) ? (M >= 0 ? \'+\' : \'\') + M.toFixed(2) : \'---\';\n'
|
||||
' const imgColor = isVirtual ? \'rgba(255,133,162,0.85)\' : \'rgba(6,214,224,0.85)\';\n'
|
||||
' ctx.fillStyle = imgColor; ctx.textAlign = \'left\';\n'
|
||||
' ctx.fillText("hi=" + Math.abs(hPrime).toFixed(0), imgX + 6, ay - hPrime / 2);\n'
|
||||
' ctx.fillStyle = \'#FFD166\'; ctx.textAlign = \'center\';\n'
|
||||
' ctx.fillText(\'G=\' + Gstr, (lx + imgX) / 2, ay + 60);\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
)
|
||||
|
||||
new_content = content[:idx] + NEW_METHODS_1 + content[idx:]
|
||||
content = new_content
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PATCH 2: Add R slider + parabolic/spherical toggle to MirrorSim constructor
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Find MirrorSim constructor and add _R and _parabolic fields
|
||||
OLD_MIRROR_CTOR = ' this._photonPaths = [];\n\n this._prevType = \'concave\';\n this._transT = 1.0;\n this._transRaf = null;\n\n this._drag = null;\n this._hoverX = -999;\n this._hoverY = -999;\n\n this.onUpdate = null;\n this.onAnimate = null;\n\n this._bindEvents();\n new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement);\n }'
|
||||
|
||||
NEW_MIRROR_CTOR = (
|
||||
' this._photonPaths = [];\n\n'
|
||||
' this._prevType = \'concave\';\n'
|
||||
' this._transT = 1.0;\n'
|
||||
' this._transRaf = null;\n\n'
|
||||
' this._drag = null;\n'
|
||||
' this._hoverX = -999;\n'
|
||||
' this._hoverY = -999;\n\n'
|
||||
' this.onUpdate = null;\n'
|
||||
' this.onAnimate = null;\n\n'
|
||||
' /* Feature 2: R slider + spherical aberration toggle */\n'
|
||||
' this._R = 240; // radius of curvature (positive=concave, negative=convex)\n'
|
||||
' this._useR = false; // true = R-slider mode; false = classic type+f mode\n'
|
||||
' this._parabolic = false; // false = spherical mirror; true = perfect parabolic\n'
|
||||
'\n'
|
||||
' this._bindEvents();\n'
|
||||
' new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement);\n'
|
||||
' }'
|
||||
)
|
||||
|
||||
if OLD_MIRROR_CTOR not in content:
|
||||
print('ERROR: MirrorSim ctor block not found'); sys.exit(1)
|
||||
|
||||
content = content.replace(OLD_MIRROR_CTOR, NEW_MIRROR_CTOR, 1)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PATCH 3: Add setMirrorR, setMirrorParabolic, _drawAberrationFan
|
||||
# before MirrorSim._bindEvents
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Find the _bindEvents in MirrorSim (it's further down, after chk method)
|
||||
MIRROR_BIND_MARKER = ' _bindEvents() {\n const cv = this.canvas;\n const getPos = e => {'
|
||||
|
||||
idx2 = content.find(MIRROR_BIND_MARKER)
|
||||
if idx2 == -1:
|
||||
print('ERROR: chk marker not found'); sys.exit(1)
|
||||
|
||||
NEW_METHODS_2 = (
|
||||
' /* === Feature 2: R-slider mode for MirrorSim === */\n'
|
||||
' setMirrorR(R) {\n'
|
||||
' this._useR = true;\n'
|
||||
' this._R = +R;\n'
|
||||
' // Derive type and f from R\n'
|
||||
' const absR = Math.abs(this._R);\n'
|
||||
' if (absR < 5) { this.type = \'flat\'; }\n'
|
||||
' else if (this._R > 0) { this.type = \'concave\'; this.f = absR / 2; }\n'
|
||||
' else { this.type = \'convex\'; this.f = absR / 2; }\n'
|
||||
' this.draw(); this._emit();\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' setMirrorParabolic(on) {\n'
|
||||
' this._parabolic = !!on;\n'
|
||||
' this.draw();\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' /* Draw 5 parallel rays showing spherical vs parabolic aberration */\n'
|
||||
' _drawAberrationFan(ctx, mx, ay, f) {\n'
|
||||
' if (!isFinite(f) || Math.abs(f) < 5) return;\n'
|
||||
' const mH = Math.min(this.H * 0.38, 140);\n'
|
||||
' const heights = [-0.85, -0.45, 0, 0.45, 0.85];\n'
|
||||
' const COLORS = [\'#FF6B6B\', \'#FFD166\', \'#7BF5A4\', \'#06D6E0\', \'#B8A4FF\'];\n'
|
||||
' ctx.save(); ctx.lineWidth = 1.4;\n'
|
||||
' heights.forEach((fr, i) => {\n'
|
||||
' const rayH = fr * mH;\n'
|
||||
' // For parabolic mirror: all parallel rays focus exactly at f\n'
|
||||
' // For spherical: marginal rays (fr != 0) focus closer by h^2/(2R) approx\n'
|
||||
' const fEff = this._parabolic ? f : f - (rayH * rayH) / (2 * Math.abs(f) * 2);\n'
|
||||
' const startX = mx - this.d - 40;\n'
|
||||
' const hitY = ay - rayH; // hits mirror at height rayH\n'
|
||||
' // Incident ray: horizontal from left to mirror\n'
|
||||
' ctx.strokeStyle = COLORS[i]; ctx.globalAlpha = 0.75;\n'
|
||||
' ctx.setLineDash([]);\n'
|
||||
' ctx.beginPath(); ctx.moveTo(startX, ay - rayH); ctx.lineTo(mx, hitY); ctx.stroke();\n'
|
||||
' // Reflected ray: goes toward focal point fEff\n'
|
||||
' const focX = mx - fEff;\n'
|
||||
' if (focX > 0 && focX < this.W) {\n'
|
||||
' ctx.beginPath(); ctx.moveTo(mx, hitY); ctx.lineTo(focX, ay);\n'
|
||||
' // extend a bit past focus\n'
|
||||
' const dx = focX - mx, dy = ay - hitY, len = Math.hypot(dx, dy);\n'
|
||||
' if (len > 1) ctx.lineTo(focX + dx/len*50, ay + dy/len*50);\n'
|
||||
' ctx.stroke();\n'
|
||||
' }\n'
|
||||
' });\n'
|
||||
' ctx.globalAlpha = 1;\n'
|
||||
' // label\n'
|
||||
' const label = this._parabolic ? \'Параболическое (идеальный фокус)\' : \'Сферическое (аберрация)\';\n'
|
||||
' const col = this._parabolic ? \'#7BF5A4\' : \'#FF6B6B\';\n'
|
||||
' const bx = 12, by = this.H - 36;\n'
|
||||
' ctx.fillStyle = \'rgba(13,13,26,0.85)\';\n'
|
||||
' ctx.beginPath(); ctx.roundRect(bx, by, 250, 24, 6); ctx.fill();\n'
|
||||
' ctx.font = \'bold 11px Manrope, system-ui, sans-serif\';\n'
|
||||
' ctx.textAlign = \'left\'; ctx.textBaseline = \'middle\';\n'
|
||||
' ctx.fillStyle = col;\n'
|
||||
' ctx.fillText(label, bx + 8, by + 12);\n'
|
||||
' ctx.restore();\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
)
|
||||
|
||||
new_content2 = content[:idx2] + NEW_METHODS_2 + content[idx2:]
|
||||
content = new_content2
|
||||
|
||||
with open(src, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print('OK lines:', content.count('\n'))
|
||||
@@ -0,0 +1,44 @@
|
||||
import sys, os
|
||||
|
||||
src = os.path.join(os.path.dirname(__file__), '../../frontend/js/labs/opticsbench.js')
|
||||
|
||||
with open(src, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PATCH: In MirrorSim.draw(), after _drawFanRays, add aberration fan for _useR mode
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
OLD = (' this._drawFanRays(ctx, mx, ay, f, dPrime, hPrime, showRay, showFill);\n'
|
||||
' /* spherical aberration overlay (Agent OB-A3) */\n'
|
||||
' if (this._showSpherical && this.type !== \'flat\' && isFinite(f))\n'
|
||||
' this._drawMirrorSphericalAberration(ctx, mx, ay, f);\n'
|
||||
' this._drawMirror(ctx, mx, ay);')
|
||||
|
||||
NEW = (' this._drawFanRays(ctx, mx, ay, f, dPrime, hPrime, showRay, showFill);\n'
|
||||
' /* spherical aberration overlay (Agent OB-A3) */\n'
|
||||
' if (this._showSpherical && this.type !== \'flat\' && isFinite(f))\n'
|
||||
' this._drawMirrorSphericalAberration(ctx, mx, ay, f);\n'
|
||||
' /* Feature 2: parabolic/spherical aberration fan */\n'
|
||||
' if (this._useR && this.type !== \'flat\' && isFinite(f))\n'
|
||||
' this._drawAberrationFan(ctx, mx, ay, f);\n'
|
||||
' this._drawMirror(ctx, mx, ay);\n'
|
||||
' /* Feature 2: R and f labels on mirror */\n'
|
||||
' if (this._useR && this.type !== \'flat\' && isFinite(f)) {\n'
|
||||
' ctx.save();\n'
|
||||
' ctx.font = \'bold 11px Manrope, system-ui, sans-serif\';\n'
|
||||
' ctx.fillStyle = \'rgba(6,214,224,0.9)\';\n'
|
||||
' ctx.textAlign = \'right\'; ctx.textBaseline = \'bottom\';\n'
|
||||
' ctx.fillText(\'R=\' + this._R.toFixed(0) + \' f=\' + f.toFixed(0), mx - 4, ay - 6);\n'
|
||||
' ctx.restore();\n'
|
||||
' }')
|
||||
|
||||
if OLD not in content:
|
||||
print('ERROR: draw fan/mirror block not found'); sys.exit(1)
|
||||
|
||||
content = content.replace(OLD, NEW, 1)
|
||||
|
||||
with open(src, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print('OK lines:', content.count('\n'))
|
||||
@@ -0,0 +1,91 @@
|
||||
import sys, os
|
||||
|
||||
src = os.path.join(os.path.dirname(__file__), '../../frontend/js/labs/opticsbench.js')
|
||||
|
||||
with open(src, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PATCH: Insert new glue functions after lensPreset, before _lensUpdateUI
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
MARKER = 'function _lensUpdateUI(info) {'
|
||||
|
||||
idx = content.find(MARKER)
|
||||
if idx == -1:
|
||||
print('ERROR: _lensUpdateUI not found'); sys.exit(1)
|
||||
|
||||
NEW_FUNCS = (
|
||||
'/* ── Lens animated ray + LM controls (Feature 1 & 3) ── */\n'
|
||||
'function lensToggleLM(on) {\n'
|
||||
' const sliders = document.getElementById(\'ob-lm-sliders\');\n'
|
||||
' const fRow = document.querySelector(\'#ob-ctrl-lens .proj-slider-row\');\n'
|
||||
' if (sliders) sliders.style.display = on ? \'\' : \'none\';\n'
|
||||
' // hide/show simple f slider\n'
|
||||
' const fSlRow = document.getElementById(\'sl-lens-f\');\n'
|
||||
' if (fSlRow && fSlRow.parentElement) fSlRow.parentElement.style.display = on ? \'none\' : \'\';\n'
|
||||
' if (lensSim) lensSim.setLensMode(!on);\n'
|
||||
' if (on && lensSim) {\n'
|
||||
' // sync sliders to current LM params\n'
|
||||
' const r1 = lensSim._lmR1, r2 = lensSim._lmR2, n = lensSim._lmN;\n'
|
||||
' const s1 = document.getElementById(\'sl-lm-r1\'), l1 = document.getElementById(\'lm-r1-val\');\n'
|
||||
' const s2 = document.getElementById(\'sl-lm-r2\'), l2 = document.getElementById(\'lm-r2-val\');\n'
|
||||
' const sn = document.getElementById(\'sl-lm-n\'), ln = document.getElementById(\'lm-n-val\');\n'
|
||||
' if (s1) s1.value = r1; if (l1) l1.textContent = r1.toFixed(0);\n'
|
||||
' if (s2) s2.value = r2; if (l2) l2.textContent = r2.toFixed(0);\n'
|
||||
' if (sn) sn.value = n; if (ln) ln.textContent = n.toFixed(2);\n'
|
||||
' }\n'
|
||||
'}\n'
|
||||
'\n'
|
||||
'function lensLMParam(name, val) {\n'
|
||||
' const v = parseFloat(val);\n'
|
||||
' const lblMap = { R1: \'lm-r1-val\', R2: \'lm-r2-val\', n: \'lm-n-val\' };\n'
|
||||
' const el = document.getElementById(lblMap[name]);\n'
|
||||
' if (el) el.textContent = name === \'n\' ? v.toFixed(2) : v.toFixed(0);\n'
|
||||
' if (lensSim) {\n'
|
||||
' lensSim.setLMParam(name, v);\n'
|
||||
' // update f display\n'
|
||||
' const fl = document.getElementById(\'lens-f-val\');\n'
|
||||
' if (fl) fl.textContent = lensSim.f.toFixed(0);\n'
|
||||
' }\n'
|
||||
'}\n'
|
||||
'\n'
|
||||
'/* ── Mirror R-slider + parabolic controls (Feature 2) ── */\n'
|
||||
'function mirrorToggleR(on) {\n'
|
||||
' const rRow = document.getElementById(\'ob-mirror-R-row\');\n'
|
||||
' if (rRow) rRow.style.display = on ? \'\' : \'none\';\n'
|
||||
' const pbtn = document.getElementById(\'mirror-parab-btn\');\n'
|
||||
' if (pbtn) pbtn.style.display = on ? \'\' : \'none\';\n'
|
||||
' if (mirrorSim) mirrorSim._useR = !!on;\n'
|
||||
' if (on && mirrorSim) {\n'
|
||||
' const sv = document.getElementById(\'sl-mirror-R\');\n'
|
||||
' const lv = document.getElementById(\'mirror-R-val\');\n'
|
||||
' if (sv) sv.value = mirrorSim._R;\n'
|
||||
' if (lv) lv.textContent = mirrorSim._R;\n'
|
||||
' mirrorSim.setMirrorR(mirrorSim._R);\n'
|
||||
' } else if (mirrorSim) { mirrorSim.draw(); }\n'
|
||||
'}\n'
|
||||
'\n'
|
||||
'function mirrorRParam(val) {\n'
|
||||
' const v = parseFloat(val);\n'
|
||||
' const el = document.getElementById(\'mirror-R-val\');\n'
|
||||
' if (el) el.textContent = v;\n'
|
||||
' if (mirrorSim) mirrorSim.setMirrorR(v);\n'
|
||||
'}\n'
|
||||
'\n'
|
||||
'function mirrorToggleParabolic(btn) {\n'
|
||||
' if (!mirrorSim) return;\n'
|
||||
' mirrorSim._parabolic = !mirrorSim._parabolic;\n'
|
||||
' if (btn) btn.textContent = mirrorSim._parabolic ? \'Параболическое\' : \'Сферическое\';\n'
|
||||
' if (btn) btn.style.color = mirrorSim._parabolic ? \'#7BF5A4\' : \'#888\';\n'
|
||||
' mirrorSim.draw();\n'
|
||||
'}\n'
|
||||
'\n'
|
||||
)
|
||||
|
||||
new_content = content[:idx] + NEW_FUNCS + content[idx:]
|
||||
|
||||
with open(src, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
print('OK lines:', new_content.count('\n'))
|
||||
@@ -0,0 +1,85 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const dir = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
|
||||
const files = ['geometry_10_hub.html','geometry_10_r1.html','geometry_10_r2.html','geometry_10_r3.html','geometry_10_r4.html'];
|
||||
|
||||
const cmds = ['dfrac','sqrt','sin','cos','tan','angle','vec','triangle','Rightarrow','cdot','ne','le','ge','pi','alpha','beta','gamma','delta','varphi','circ','perp','parallel','frac','tfrac','overline','left','right','begin','end','boxed','cot','arcsin','arccos','arctan','log','ln','lim','sum','int','infty','Delta','theta','lambda','mu','rho','sigma','tau','omega','Omega','phi','psi','xi','zeta','eta','epsilon','varepsilon','Pi','Sigma','approx','equiv','pm','mp','times','div','leq','geq','neq','sim','cong','subset','supset','cup','cap','forall','exists','overrightarrow','overleftarrow','widehat','widetilde','mathbf','mathrm','mathbb','mathcal','quad','qquad','hline','cline','displaystyle','textstyle','scriptstyle','underline','operatorname','land','lor','lnot','mapsto','Leftrightarrow','Leftarrow','leftarrow','rightarrow','uparrow','downarrow','prime','colon'];
|
||||
|
||||
for (const f of files) {
|
||||
const fp = path.join(dir, f);
|
||||
if (!fs.existsSync(fp)) { console.log('MISSING', f); continue; }
|
||||
const txt = fs.readFileSync(fp, 'utf8');
|
||||
|
||||
const scriptRe = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/g;
|
||||
let m;
|
||||
let totalKatexErrs = 0;
|
||||
const errSamples = [];
|
||||
let scriptBlocks = 0;
|
||||
let combinedJs = '';
|
||||
|
||||
while ((m = scriptRe.exec(txt)) !== null) {
|
||||
scriptBlocks++;
|
||||
const body = m[1];
|
||||
combinedJs += body + ';\n';
|
||||
const cmdRe = /(^|[^\\])\\([a-zA-Z]+)/g;
|
||||
let cm;
|
||||
while ((cm = cmdRe.exec(body)) !== null) {
|
||||
const cmd = cm[2];
|
||||
if (cmds.includes(cmd)) {
|
||||
totalKatexErrs++;
|
||||
const idxAbs = m.index + cm.index + cm[1].length;
|
||||
const before = txt.slice(0, idxAbs);
|
||||
const line = (before.match(/\n/g) || []).length + 1;
|
||||
if (errSamples.length < 10) errSamples.push({line, cmd, ctx: body.slice(Math.max(0,cm.index-30), cm.index+50).replace(/\n/g,' ')});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// <option> raw KaTeX
|
||||
const optRe = /<option[^>]*>([\s\S]*?)<\/option>/g;
|
||||
let om;
|
||||
let optionRaw = 0;
|
||||
const optionSamples = [];
|
||||
while ((om = optRe.exec(txt)) !== null) {
|
||||
const content = om[1];
|
||||
if (/\$[^\$]+\$/.test(content) || /\\(dfrac|sqrt|frac|angle|vec|sin|cos|alpha|beta|pi|circ|triangle|Rightarrow)/.test(content)) {
|
||||
optionRaw++;
|
||||
const before = txt.slice(0, om.index);
|
||||
const line = (before.match(/\n/g) || []).length + 1;
|
||||
if (optionSamples.length < 8) optionSamples.push({line, content: content.slice(0,90)});
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji
|
||||
const emojiRe = /[\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F02F}\u{2700}-\u{27BF}\u{2600}-\u{26FF}]/gu;
|
||||
let em;
|
||||
let emoji = 0;
|
||||
const emojiSamples = [];
|
||||
while ((em = emojiRe.exec(txt)) !== null) {
|
||||
emoji++;
|
||||
const before = txt.slice(0, em.index);
|
||||
const line = (before.match(/\n/g) || []).length + 1;
|
||||
if (emojiSamples.length < 10) emojiSamples.push({line, ch: em[0], cp: em[0].codePointAt(0).toString(16), ctx: txt.slice(Math.max(0,em.index-25), em.index+25).replace(/\n/g,' ')});
|
||||
}
|
||||
|
||||
let authors = [];
|
||||
if (f.includes('hub')) {
|
||||
for (const a of ['Латотин','Чеботаревский','Горбунова','Шлыков']) {
|
||||
if (txt.includes(a)) authors.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
let parseErr = null;
|
||||
try { new Function(combinedJs); } catch(e) { parseErr = e.message.slice(0,200); }
|
||||
|
||||
console.log('=== ' + f + ' ===');
|
||||
console.log(' size:', txt.length, 'scripts:', scriptBlocks);
|
||||
console.log(' KaTeX single-backslash errors:', totalKatexErrs);
|
||||
for (const s of errSamples) console.log(' line ' + s.line + ' \\' + s.cmd + ' ctx: ' + s.ctx);
|
||||
console.log(' <option> raw KaTeX:', optionRaw);
|
||||
for (const s of optionSamples) console.log(' line ' + s.line + ': ' + s.content);
|
||||
console.log(' Emoji symbols:', emoji);
|
||||
for (const s of emojiSamples) console.log(' line ' + s.line + ' U+' + s.cp + ' ctx: ' + s.ctx);
|
||||
if (authors.length) console.log(' Authors in hub:', authors.join(', '));
|
||||
console.log(' JS parse:', parseErr ? 'ERROR: ' + parseErr : 'OK');
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const dir = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
|
||||
const files = ['geometry_10_hub.html','geometry_10_r1.html','geometry_10_r2.html','geometry_10_r3.html','geometry_10_r4.html'];
|
||||
|
||||
// hub footer check
|
||||
const hub = fs.readFileSync(path.join(dir,'geometry_10_hub.html'),'utf8');
|
||||
const footerMatch = hub.match(/<footer[\s\S]*?<\/footer>/);
|
||||
console.log('HUB FOOTER:');
|
||||
if (footerMatch) console.log(footerMatch[0].slice(0,400)); else console.log(' NOT FOUND');
|
||||
|
||||
// g3d.js usage
|
||||
console.log('\\n3D ENGINE / g3d.js usage:');
|
||||
for (const f of files) {
|
||||
const txt = fs.readFileSync(path.join(dir, f), 'utf8');
|
||||
const has3d = /g3d/i.test(txt);
|
||||
const hasProj = /(project|perspective|3d|threejs|three\.)/i.test(txt);
|
||||
const hasViewBox = (txt.match(/viewBox/g) || []).length;
|
||||
console.log(' ' + f + ' g3d:'+has3d+' proj/3d:'+hasProj+' viewBox count:'+hasViewBox);
|
||||
}
|
||||
|
||||
// Structure: count §, finals, "Я прочитал" buttons
|
||||
console.log('\\nSTRUCTURE:');
|
||||
for (const f of files) {
|
||||
if (f.includes('hub')) continue;
|
||||
const txt = fs.readFileSync(path.join(dir, f), 'utf8');
|
||||
// sections: pattern in TOC
|
||||
const sectionIds = [];
|
||||
const re = /id:\s*['"]([^'"]+)['"]\s*,\s*num:\s*['"]([^'"]*)['"]/g;
|
||||
let mm;
|
||||
while ((mm = re.exec(txt)) !== null) sectionIds.push({id:mm[1], num:mm[2]});
|
||||
// theory blocks
|
||||
const theoryCount = (txt.match(/theory|teor|Теори/g) || []).length;
|
||||
// interactive: int1/int2/int3/int4 or class
|
||||
const intCount = (txt.match(/data-int|intCanvas|interactive/g) || []).length;
|
||||
// "Я прочитал"
|
||||
const yaProch = (txt.match(/Я прочитал/g) || []).length;
|
||||
// markRead
|
||||
const markRead = (txt.match(/markRead|markAsRead|читал/g) || []).length;
|
||||
console.log(' ' + f + ':');
|
||||
console.log(' sections in TOC:', sectionIds.map(s=>s.num+':'+s.id).join(' '));
|
||||
console.log(' theoryCount mentions:', theoryCount, 'interactive mentions:', intCount, '"Я прочитал":', yaProch);
|
||||
}
|
||||
|
||||
// Look for slider px ranges 40..150, R=120 etc.
|
||||
console.log('\\nSLIDER PIXEL CHECK (range/value hints):');
|
||||
for (const f of files) {
|
||||
if (f.includes('hub')) continue;
|
||||
const txt = fs.readFileSync(path.join(dir, f), 'utf8');
|
||||
// Find input range with suspicious min/max
|
||||
const ranges = [...txt.matchAll(/<input[^>]*type=["']range["'][^>]*>/g)];
|
||||
let suspicious = 0;
|
||||
for (const r of ranges) {
|
||||
const tag = r[0];
|
||||
const min = (tag.match(/min=["']?(\d+)/)||[])[1];
|
||||
const max = (tag.match(/max=["']?(\d+)/)||[])[1];
|
||||
if (min && max) {
|
||||
const lo=+min, hi=+max;
|
||||
if ((lo>=30 && hi>=120) || (hi-lo>=60 && hi>=100)) {
|
||||
suspicious++;
|
||||
if (suspicious<=5) {
|
||||
const idx = r.index;
|
||||
const line = (txt.slice(0,idx).match(/\n/g)||[]).length+1;
|
||||
console.log(' ' + f + ' line '+line+': '+tag.slice(0,140));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(' ' + f + ': total ranges=' + ranges.length + ' suspicious(px-like)=' + suspicious);
|
||||
}
|
||||
|
||||
// Check "px" usage near labels (R = , a =, etc.)
|
||||
console.log('\\nLABEL "R=" / "a=" PATTERNS:');
|
||||
for (const f of files) {
|
||||
if (f.includes('hub')) continue;
|
||||
const txt = fs.readFileSync(path.join(dir, f), 'utf8');
|
||||
// Find patterns like 'R = ${...}' or text="R = ${r}" where r might be px
|
||||
const pat = /['"`][^'"`]*\b(R|a|h|d|l|r|b|c)\s*=\s*\$\{[^}]+\}/g;
|
||||
const matches = [];
|
||||
let mm;
|
||||
while ((mm = pat.exec(txt)) !== null) {
|
||||
if (matches.length < 6) {
|
||||
const idx = mm.index;
|
||||
const line = (txt.slice(0,idx).match(/\n/g)||[]).length+1;
|
||||
matches.push({line, snip: mm[0].slice(0,80)});
|
||||
}
|
||||
}
|
||||
if (matches.length) {
|
||||
console.log(' ' + f + ':');
|
||||
for (const x of matches) console.log(' line '+x.line+': '+x.snip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const dir = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
|
||||
const files = ['geometry_10_r1.html','geometry_10_r2.html','geometry_10_r3.html','geometry_10_r4.html'];
|
||||
|
||||
console.log('SECTION CONTENT STRUCTURE:');
|
||||
for (const f of files) {
|
||||
const txt = fs.readFileSync(path.join(dir, f), 'utf8');
|
||||
// teor1/teor2/teor3 occurrences
|
||||
const teor = (txt.match(/teor[123]/g) || []).length;
|
||||
const int1234 = (txt.match(/int[1234]/g) || []).length;
|
||||
// viz blocks
|
||||
const viz = (txt.match(/viz[1234]|svg-/g) || []).length;
|
||||
// readBtn / btnRead
|
||||
const readBtn = (txt.match(/btnRead|readBtn|markRead|done-read|read-mark/g) || []).length;
|
||||
const yaProch = (txt.match(/Я прочитал/g) || []).length;
|
||||
// Sections in render
|
||||
const renderSections = (txt.match(/renderSection|renderPart|sect\.id/g) || []).length;
|
||||
console.log(' ' + f);
|
||||
console.log(' teor1/2/3:', teor, ' int1-4:', int1234, ' viz:', viz);
|
||||
console.log(' readBtn-related:', readBtn, ' "Я прочитал":', yaProch, ' renderSection:', renderSections);
|
||||
}
|
||||
|
||||
// Final course in hub
|
||||
console.log('\\nHUB - course final mention:');
|
||||
const hub = fs.readFileSync(path.join(dir,'geometry_10_hub.html'),'utf8');
|
||||
console.log(' "Финал курса":', (hub.match(/Финал курса/g)||[]).length);
|
||||
console.log(' "финал" (any):', (hub.match(/финал/gi)||[]).length);
|
||||
|
||||
// Triangle/3D shape labels - look for projection helpers
|
||||
console.log('\\n3D PROJECTION HELPERS in r1-r4:');
|
||||
for (const f of files) {
|
||||
const txt = fs.readFileSync(path.join(dir, f), 'utf8');
|
||||
const hasProject = /function\s+project|proj\s*=\s*\(/.test(txt);
|
||||
const has3DPoints = /\{x:[^}]+y:[^}]+z:/.test(txt) || /\[\s*[0-9.-]+\s*,\s*[0-9.-]+\s*,\s*[0-9.-]+\s*\]/.test(txt);
|
||||
const hasIso = /isoP|isometric|axonom/.test(txt);
|
||||
console.log(' ' + f + ' project()=' + hasProject + ' 3DPoints=' + has3DPoints + ' iso=' + hasIso);
|
||||
}
|
||||
|
||||
// Check pixel literals in SVG text labels
|
||||
console.log('\\nSVG <text> labels with px-like values:');
|
||||
for (const f of files) {
|
||||
const txt = fs.readFileSync(path.join(dir, f), 'utf8');
|
||||
// crude pattern: text content like "R = 120" "a = 80" etc
|
||||
const re = />([A-Za-zА-Яа-я]\s*=\s*\d{2,3})\s*</g;
|
||||
let m, cnt=0;
|
||||
while ((m = re.exec(txt)) !== null) {
|
||||
cnt++;
|
||||
if (cnt <= 6) {
|
||||
const line = (txt.slice(0,m.index).match(/\n/g)||[]).length+1;
|
||||
console.log(' ' + f + ' line '+line+': '+m[1]);
|
||||
}
|
||||
}
|
||||
if (cnt) console.log(' -> total in ' + f + ': ' + cnt);
|
||||
}
|
||||
|
||||
// Sample around final/interactive in r1 to see structure
|
||||
console.log('\\n--- r1 sample around interactives ---');
|
||||
const r1 = fs.readFileSync(path.join(dir,'geometry_10_r1.html'),'utf8');
|
||||
// Find "3 эталонных" or "эталонных" labels mismatch
|
||||
const ets = [...r1.matchAll(/(\d+)\s+эталонн/g)];
|
||||
for (const e of ets) {
|
||||
const line = (r1.slice(0,e.index).match(/\n/g)||[]).length+1;
|
||||
console.log(' r1 line '+line+': "'+r1.slice(e.index, e.index+60).replace(/\n/g,' ')+'"');
|
||||
}
|
||||
for (const f of files) {
|
||||
const txt = fs.readFileSync(path.join(dir, f), 'utf8');
|
||||
const ets = [...txt.matchAll(/(\d+)\s+эталонн/g)];
|
||||
for (const e of ets) {
|
||||
const line = (txt.slice(0,e.index).match(/\n/g)||[]).length+1;
|
||||
console.log(' ' + f + ' line '+line+': "'+txt.slice(e.index, e.index+60).replace(/\n/g,' ')+'"');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// review_geom11.js - аудит Геометрии 11 (только репорт)
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const FILES = [
|
||||
'frontend/textbooks/geometry_11_hub.html',
|
||||
'frontend/textbooks/geometry_11_ch1.html',
|
||||
'frontend/textbooks/geometry_11_ch2.html',
|
||||
'frontend/textbooks/geometry_11_ch3.html',
|
||||
'frontend/textbooks/geometry_11_ch4.html',
|
||||
];
|
||||
const ROOT = path.join(__dirname, '..', '..');
|
||||
|
||||
// Словарь KaTeX-команд (часто встречающиеся)
|
||||
const KATEX_CMDS = new Set([
|
||||
'dfrac','frac','tfrac','sqrt','cdot','times','pm','mp','ne','le','ge','approx',
|
||||
'angle','triangle','square','vec','overline','overrightarrow','overrightarrow',
|
||||
'sin','cos','tan','cot','arcsin','arccos','arctan','log','ln','lg','exp',
|
||||
'pi','alpha','beta','gamma','delta','varepsilon','theta','lambda','mu','phi','omega','rho','sigma','tau',
|
||||
'Delta','Gamma','Omega','Phi','Sigma','Theta','Lambda',
|
||||
'boxed','underline','mathbf','mathrm','text','textbf','textit',
|
||||
'sum','prod','int','infty','lim','to','rightarrow','leftarrow','Rightarrow','Leftrightarrow',
|
||||
'left','right','big','Big','bigg','Bigg','quad','qquad',',','!',
|
||||
'cap','cup','in','notin','subset','supset','emptyset','varnothing',
|
||||
'parallel','perp','cong','sim','equiv','neq',
|
||||
'begin','end','array','matrix','pmatrix','bmatrix','cases','aligned','align',
|
||||
'displaystyle','textstyle','scriptstyle',
|
||||
'colon','ldots','cdots','vdots','ddots',
|
||||
'mathbb','mathcal','mathfrak','operatorname',
|
||||
'hat','tilde','bar','dot','widehat','widetilde',
|
||||
'binom','choose','over','atop',
|
||||
'div','star','ast','circ','bullet',
|
||||
'forall','exists','land','lor','neg','iff','implies',
|
||||
'leq','geq','prec','succ','subseteq','supseteq',
|
||||
'partial','nabla','degree','prime',
|
||||
'oplus','ominus','otimes','odot',
|
||||
'mathring','space','phantom','vphantom',
|
||||
'overset','underset','stackrel',
|
||||
'color','textcolor','rgb','href',
|
||||
'rule','kern','mskip','hspace','vspace',
|
||||
'tag','label','ref',
|
||||
'lvert','rvert','lVert','rVert','vert','Vert','|',
|
||||
'oint','iint','iiint',
|
||||
'because','therefore',
|
||||
'leftrightarrow','Leftarrow','longleftarrow','longrightarrow',
|
||||
'mapsto','hookrightarrow',
|
||||
]);
|
||||
|
||||
const EMOJI_CHARS = ['✓','✗','⚠','🔥','📐','✅','❌','🎯','🔴','🟢','🟡','📊','📈','📉','🎓','📚','🧮','🔬','🌟','⭐','💡','🚀','🎉','📝','📖','🗒'];
|
||||
|
||||
const AUTHOR_NAMES = ['Латотин','Чеботаревский','Горбунова','Цыбулько','Шлыков','Подгорная'];
|
||||
|
||||
function scanFile(rel) {
|
||||
const fp = path.join(ROOT, rel);
|
||||
const src = fs.readFileSync(fp, 'utf8');
|
||||
const out = { file: rel, katex: [], optionsKatex: [], emoji: [], authors: [], pixels: [], g3d: {} };
|
||||
|
||||
// 1. KaTeX ошибки: ищем backslash перед буквой (одиночный) в JS template literals и в HTML
|
||||
// Анализируем строчно
|
||||
const lines = src.split(/\r?\n/);
|
||||
// Regex: одиночный \ перед 2+ буквами, не предшествуемый \
|
||||
const reSingle = /(?<!\\)\\([a-zA-Z]{2,})/g;
|
||||
// Regex: двойной \\command (нормальный для JS-литералов)
|
||||
// Чтобы не плодить шум: репортим только когда команда из KATEX_CMDS
|
||||
|
||||
// Делим на блоки: внутри <script> и вне
|
||||
let inScript = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (/<script(\s|>)/i.test(line)) inScript = true;
|
||||
// Сначала проверим текущую строку
|
||||
// Внутри <script> — нужен \\cmd (т.к. JS экранирует). Одиночный \cmd = ошибка.
|
||||
// Вне <script> (чистый HTML, например в title) — \cmd нормально для KaTeX render.
|
||||
|
||||
// Найдём все совпадения \cmd
|
||||
let m;
|
||||
const matches = [];
|
||||
const re = /\\+([a-zA-Z]{2,})/g;
|
||||
while ((m = re.exec(line)) !== null) {
|
||||
const bsCount = m[0].length - m[1].length; // число backslashes
|
||||
const cmd = m[1];
|
||||
if (!KATEX_CMDS.has(cmd)) continue;
|
||||
matches.push({ cmd, bsCount, idx: m.index, full: m[0] });
|
||||
}
|
||||
for (const mt of matches) {
|
||||
if (inScript) {
|
||||
// в JS template literals одиночный \X неправильно. Нужно \\X (bsCount>=2)
|
||||
if (mt.bsCount === 1) {
|
||||
out.katex.push({ line: i+1, cmd: mt.cmd, snippet: line.trim().slice(0, 140) });
|
||||
}
|
||||
} else {
|
||||
// в чистом HTML \cmd ок
|
||||
}
|
||||
}
|
||||
if (/<\/script>/i.test(line)) inScript = false;
|
||||
}
|
||||
|
||||
// 2. <option> с сырым KaTeX (содержит $ или \\sqrt и т.п.)
|
||||
const optRe = /<option[^>]*>([^<]*)<\/option>/g;
|
||||
let om;
|
||||
while ((om = optRe.exec(src)) !== null) {
|
||||
const txt = om[1];
|
||||
if (/\$[^$]+\$/.test(txt) || /\\\\[a-zA-Z]+/.test(txt) || /\\(dfrac|sqrt|frac|sin|cos|pi|angle|vec|cdot|times)/.test(txt)) {
|
||||
out.optionsKatex.push({ snippet: om[0].slice(0,160) });
|
||||
}
|
||||
}
|
||||
// Также проверим динамический innerHTML на option в JS
|
||||
const scriptBlocks = [...src.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
|
||||
for (const sb of scriptBlocks) {
|
||||
// ищем <option ...>$...$</option> или \\sqrt в template-literals
|
||||
const re2 = /<option[^>]*>[^<]*(?:\$[^$]+\$|\\\\(?:dfrac|sqrt|frac|sin|cos|pi|angle|vec|cdot|times))[^<]*<\/option>/g;
|
||||
let mm;
|
||||
while ((mm = re2.exec(sb)) !== null) {
|
||||
out.optionsKatex.push({ snippet: mm[0].slice(0,160), inScript: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Эмодзи (исключая SVG)
|
||||
for (const ch of EMOJI_CHARS) {
|
||||
let idx = 0;
|
||||
while ((idx = src.indexOf(ch, idx)) !== -1) {
|
||||
// Найти строку
|
||||
const upto = src.slice(0, idx);
|
||||
const lineNum = upto.split('\n').length;
|
||||
const lineStart = upto.lastIndexOf('\n') + 1;
|
||||
const lineEnd = src.indexOf('\n', idx);
|
||||
const lineText = src.slice(lineStart, lineEnd === -1 ? src.length : lineEnd);
|
||||
out.emoji.push({ char: ch, line: lineNum, snippet: lineText.trim().slice(0, 120) });
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Авторы
|
||||
for (const name of AUTHOR_NAMES) {
|
||||
if (src.includes(name)) {
|
||||
// Найдём строку
|
||||
const idx = src.indexOf(name);
|
||||
const upto = src.slice(0, idx);
|
||||
const lineNum = upto.split('\n').length;
|
||||
const lineStart = upto.lastIndexOf('\n') + 1;
|
||||
const lineEnd = src.indexOf('\n', idx);
|
||||
const lineText = src.slice(lineStart, lineEnd === -1 ? src.length : lineEnd);
|
||||
out.authors.push({ name, line: lineNum, snippet: lineText.trim().slice(0, 160) });
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Пиксели в подписях: ищем sliders с min/max, и подписи рядом
|
||||
// Простая эвристика: <input type="range" min="X" max="Y" ...> + value/label рядом
|
||||
const sliderRe = /<input[^>]*type=["']range["'][^>]*>/g;
|
||||
let sm;
|
||||
while ((sm = sliderRe.exec(src)) !== null) {
|
||||
const tag = sm[0];
|
||||
const minM = tag.match(/\bmin=["'](\d+)["']/);
|
||||
const maxM = tag.match(/\bmax=["'](\d+)["']/);
|
||||
const idM = tag.match(/\bid=["']([^"']+)["']/);
|
||||
if (minM && maxM) {
|
||||
const min = +minM[1], max = +maxM[1];
|
||||
// подозрительно если min>20 и max>100 (вероятно пиксели)
|
||||
const suspicious = min >= 20 && max >= 100;
|
||||
if (suspicious) {
|
||||
const upto = src.slice(0, sm.index);
|
||||
const lineNum = upto.split('\n').length;
|
||||
out.pixels.push({ line: lineNum, id: idM ? idM[1] : '', min, max, tag: tag.slice(0,180) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. G3D usage
|
||||
out.g3d.includesScript = /<script[^>]*src=["']\/js\/g3d\.js["'][^>]*>/.test(src);
|
||||
out.g3d.usesPrism = /G3D\.prismMesh/.test(src);
|
||||
out.g3d.usesCylinder = /G3D\.cylinderMesh/.test(src);
|
||||
out.g3d.usesPyramid = /G3D\.pyramidMesh/.test(src);
|
||||
out.g3d.usesCone = /G3D\.coneMesh/.test(src);
|
||||
out.g3d.usesSphere = /G3D\.sphereWireframe/.test(src);
|
||||
out.g3d.usesAttachOrbit = /G3D\.attachOrbit/.test(src);
|
||||
out.g3d.usesPresetView = /G3D\.presetView/.test(src);
|
||||
out.g3d.viewButtons = (src.match(/Изо|Спереди|Сверху|Сбоку/g) || []).length;
|
||||
|
||||
// 7. Структура: theory cards (3), interactives (4), "Я прочитал"
|
||||
out.theoryCards = (src.match(/class=["'][^"']*theory[^"']*card/g) || []).length;
|
||||
out.interactiveCards = (src.match(/class=["'][^"']*interactive[^"']*/g) || []).length;
|
||||
out.readBtns = (src.match(/Я прочитал|я прочитал/g) || []).length;
|
||||
out.bossCount = (src.match(/босс|Босс|BOSS/g) || []).length;
|
||||
out.paragraphCount = (src.match(/§\d+/g) || []).length;
|
||||
out.finalSections = (src.match(/Финал|финал раздела|Финал раздела/g) || []).length;
|
||||
out.length = src.length;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
const results = FILES.map(scanFile);
|
||||
|
||||
// Report
|
||||
console.log('='.repeat(80));
|
||||
console.log('AUDIT GEOMETRY 11');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
let totals = { katex: 0, options: 0, emoji: 0, authors: 0, pixels: 0 };
|
||||
|
||||
for (const r of results) {
|
||||
console.log(`\n--- ${r.file} (${(r.length/1024).toFixed(1)} KB) ---`);
|
||||
console.log(`KaTeX errors: ${r.katex.length}`);
|
||||
if (r.katex.length) {
|
||||
r.katex.slice(0, 10).forEach(e => console.log(` L${e.line}: \\${e.cmd} | ${e.snippet}`));
|
||||
if (r.katex.length > 10) console.log(` ... +${r.katex.length-10} more`);
|
||||
}
|
||||
console.log(`<option> KaTeX: ${r.optionsKatex.length}`);
|
||||
r.optionsKatex.slice(0,5).forEach(o => console.log(` ${o.inScript?'[JS] ':''}${o.snippet}`));
|
||||
console.log(`Emoji: ${r.emoji.length}`);
|
||||
r.emoji.slice(0,5).forEach(e => console.log(` L${e.line} '${e.char}': ${e.snippet}`));
|
||||
console.log(`Authors: ${r.authors.length}`);
|
||||
r.authors.slice(0,5).forEach(a => console.log(` L${a.line} '${a.name}': ${a.snippet}`));
|
||||
console.log(`Pixel sliders (susp.): ${r.pixels.length}`);
|
||||
r.pixels.slice(0,5).forEach(p => console.log(` L${p.line} #${p.id} min=${p.min} max=${p.max}`));
|
||||
console.log(`G3D: script=${r.g3d.includesScript}, prism=${r.g3d.usesPrism}, cyl=${r.g3d.usesCylinder}, pyr=${r.g3d.usesPyramid}, cone=${r.g3d.usesCone}, sphere=${r.g3d.usesSphere}, orbit=${r.g3d.usesAttachOrbit}, presetView=${r.g3d.usesPresetView}, viewBtnHits=${r.g3d.viewButtons}`);
|
||||
console.log(`Structure: theoryCards=${r.theoryCards}, interactives=${r.interactiveCards}, readBtns=${r.readBtns}, bossHits=${r.bossCount}, §count=${r.paragraphCount}, finalHits=${r.finalSections}`);
|
||||
|
||||
totals.katex += r.katex.length;
|
||||
totals.options += r.optionsKatex.length;
|
||||
totals.emoji += r.emoji.length;
|
||||
totals.authors += r.authors.length;
|
||||
totals.pixels += r.pixels.length;
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('TOTALS:', totals);
|
||||
console.log('='.repeat(80));
|
||||
@@ -0,0 +1,569 @@
|
||||
'use strict';
|
||||
const db = require('../src/db/db');
|
||||
|
||||
const MATH_ID = 3;
|
||||
const T = {
|
||||
arithmetic: 16, word: 17, numbers: 18, trig: 19,
|
||||
quadratic: 20, progression: 21, inequalities: 22, geometry: 23,
|
||||
functions: 24, log: 25, expineq: 26, equations: 27, stats: 28,
|
||||
};
|
||||
|
||||
const existingTexts = new Set(
|
||||
db.prepare('SELECT text FROM questions WHERE subject_id=3').all()
|
||||
.map(q => q.text.slice(0, 70).trim())
|
||||
);
|
||||
|
||||
let added = 0, skipped = 0;
|
||||
|
||||
const insertQ = db.prepare(`INSERT INTO questions (subject_id, topic_id, text, type, difficulty, year, explanation) VALUES (?,?,?,?,?,?,?)`);
|
||||
const insertO = db.prepare(`INSERT INTO options (question_id, text, is_correct, order_index) VALUES (?,?,?,?)`);
|
||||
|
||||
function addQ(topicId, text, opts, difficulty, year, explanation, type = 'single') {
|
||||
const key = text.slice(0, 70).trim();
|
||||
if (existingTexts.has(key)) { skipped++; return; }
|
||||
existingTexts.add(key);
|
||||
const r = insertQ.run(MATH_ID, topicId, text, type, difficulty, year || null, explanation || null);
|
||||
const qid = r.lastInsertRowid;
|
||||
opts.forEach((o, i) => insertO.run(qid, o.t, o.c ? 1 : 0, i));
|
||||
added++;
|
||||
}
|
||||
|
||||
const run = db.transaction(() => {
|
||||
|
||||
// ── АРИФМЕТИКА И СТЕПЕНИ ──────────────────────────────────────────
|
||||
addQ(T.arithmetic, `Вычислите: \\(2^5 - 3^3 + 4^2\\)`,
|
||||
[{t:'\\(21\\)',c:true},{t:'\\(13\\)',c:false},{t:'\\(17\\)',c:false},{t:'\\(11\\)',c:false},{t:'\\(5\\)',c:false}],
|
||||
1,2018,'\\(32-27+16=21\\)');
|
||||
|
||||
addQ(T.arithmetic, `Вычислите: \\(3^4 - 5^2 \\cdot 2 + 1\\)`,
|
||||
[{t:'\\(32\\)',c:true},{t:'\\(31\\)',c:false},{t:'\\(30\\)',c:false},{t:'\\(34\\)',c:false},{t:'\\(29\\)',c:false}],
|
||||
1,2019,'\\(81-50+1=32\\)');
|
||||
|
||||
addQ(T.arithmetic, `Найдите: \\(\\dfrac{\\sqrt{48}}{\\sqrt{3}}\\)`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(8\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(16\\)',c:false}],
|
||||
1,2018,'\\(\\sqrt{48/3}=\\sqrt{16}=4\\)');
|
||||
|
||||
addQ(T.arithmetic, `Вычислите: \\(\\left(\\dfrac{1}{4}\\right)^{-3/2}\\)`,
|
||||
[{t:'\\(8\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(\\frac{1}{8}\\)',c:false},{t:'\\(16\\)',c:false},{t:'\\(2\\)',c:false}],
|
||||
2,2019,'\\(4^{3/2}=(2^2)^{3/2}=2^3=8\\)');
|
||||
|
||||
addQ(T.arithmetic, `Упростите: \\(\\dfrac{a^3 \\cdot a^{-1}}{a^{-2}}\\) при \\(a\\ne0\\)`,
|
||||
[{t:'\\(a^4\\)',c:true},{t:'\\(a^2\\)',c:false},{t:'\\(a^6\\)',c:false},{t:'\\(a^{-4}\\)',c:false},{t:'\\(1\\)',c:false}],
|
||||
1,2019,'\\(a^{3-1-(-2)}=a^4\\)');
|
||||
|
||||
addQ(T.arithmetic, `Найдите: \\(\\sqrt[3]{-8}\\cdot\\sqrt{9}\\)`,
|
||||
[{t:'\\(-6\\)',c:true},{t:'\\(6\\)',c:false},{t:'\\(-12\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(-3\\)',c:false}],
|
||||
1,2020,'\\(-2\\cdot3=-6\\)');
|
||||
|
||||
addQ(T.arithmetic, `Вычислите: \\(16^{0{,}75}\\)`,
|
||||
[{t:'\\(8\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(2\\)',c:false}],
|
||||
1,2022,'\\((2^4)^{3/4}=2^3=8\\)');
|
||||
|
||||
addQ(T.arithmetic, `Найдите: \\(\\dfrac{\\sqrt{75}-\\sqrt{27}}{\\sqrt{3}}\\)`,
|
||||
[{t:'\\(2\\)',c:true},{t:'\\(3\\)',c:false},{t:'\\(4\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(6\\)',c:false}],
|
||||
1,2022,'\\(\\dfrac{5\\sqrt{3}-3\\sqrt{3}}{\\sqrt{3}}=2\\)');
|
||||
|
||||
addQ(T.arithmetic, `Вычислите: \\(\\left(0{,}125\\right)^{-1/3}\\)`,
|
||||
[{t:'\\(2\\)',c:true},{t:'\\(\\frac{1}{2}\\)',c:false},{t:'\\(4\\)',c:false},{t:'\\(-2\\)',c:false},{t:'\\(8\\)',c:false}],
|
||||
2,2023,'\\(0{,}125=2^{-3},\\ (2^{-3})^{-1/3}=2\\)');
|
||||
|
||||
addQ(T.arithmetic, `Вычислите: \\(2^{10}\\div 2^7+3^2\\)`,
|
||||
[{t:'\\(17\\)',c:true},{t:'\\(18\\)',c:false},{t:'\\(16\\)',c:false},{t:'\\(9\\)',c:false},{t:'\\(15\\)',c:false}],
|
||||
1,2023,'\\(2^3+9=17\\)');
|
||||
|
||||
addQ(T.arithmetic, `Найдите: \\(\\sqrt{0{,}04}\\cdot\\sqrt[3]{0{,}008}\\)`,
|
||||
[{t:'\\(0{,}04\\)',c:true},{t:'\\(0{,}4\\)',c:false},{t:'\\(0{,}2\\)',c:false},{t:'\\(0{,}008\\)',c:false},{t:'\\(4\\)',c:false}],
|
||||
2,2020,'\\(0{,}2\\cdot0{,}2=0{,}04\\)');
|
||||
|
||||
addQ(T.arithmetic, `Вычислите: \\((\\sqrt{5}-\\sqrt{2})(\\sqrt{5}+\\sqrt{2})\\)`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(7\\)',c:false},{t:'\\(\\sqrt{7}\\)',c:false},{t:'\\(2\\sqrt{10}\\)',c:false},{t:'\\(10\\)',c:false}],
|
||||
1,2018,'\\(5-2=3\\)');
|
||||
|
||||
// ── ТЕОРИЯ ЧИСЕЛ ─────────────────────────────────────────────────
|
||||
addQ(T.numbers, `При делении числа \\(n\\) на 7 частное 5, остаток 3. Найдите \\(n\\).`,
|
||||
[{t:'\\(38\\)',c:true},{t:'\\(35\\)',c:false},{t:'\\(36\\)',c:false},{t:'\\(40\\)',c:false},{t:'\\(32\\)',c:false}],
|
||||
1,2018,'\\(7\\cdot5+3=38\\)');
|
||||
|
||||
addQ(T.numbers, `Найдите НОД(84; 126).`,
|
||||
[{t:'\\(42\\)',c:true},{t:'\\(21\\)',c:false},{t:'\\(14\\)',c:false},{t:'\\(63\\)',c:false},{t:'\\(28\\)',c:false}],
|
||||
1,2018,'\\(84=2^2\\cdot3\\cdot7,\\ 126=2\\cdot3^2\\cdot7,\\ \\text{НОД}=42\\)');
|
||||
|
||||
addQ(T.numbers, `Какое из чисел кратно 9?`,
|
||||
[{t:'\\(459\\)',c:true},{t:'\\(247\\)',c:false},{t:'\\(583\\)',c:false},{t:'\\(821\\)',c:false},{t:'\\(364\\)',c:false}],
|
||||
1,2019,'\\(4+5+9=18\\) делится на 9');
|
||||
|
||||
addQ(T.numbers, `Найдите НОК(12; 18).`,
|
||||
[{t:'\\(36\\)',c:true},{t:'\\(6\\)',c:false},{t:'\\(24\\)',c:false},{t:'\\(72\\)',c:false},{t:'\\(216\\)',c:false}],
|
||||
1,2019,'\\(\\text{НОК}=2^2\\cdot3^2=36\\)');
|
||||
|
||||
addQ(T.numbers, `Сколько простых чисел в промежутке от 20 до 40?`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(3\\)',c:false},{t:'\\(5\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(2\\)',c:false}],
|
||||
1,2020,'Простые: 23, 29, 31, 37 — четыре числа');
|
||||
|
||||
addQ(T.numbers, `Остаток от деления \\(2n+1\\) на 13, если \\(n\\equiv7\\pmod{13}\\):`,
|
||||
[{t:'\\(2\\)',c:true},{t:'\\(1\\)',c:false},{t:'\\(15\\)',c:false},{t:'\\(5\\)',c:false},{t:'\\(7\\)',c:false}],
|
||||
2,2022,'\\(2\\cdot7+1=15\\equiv2\\pmod{13}\\)');
|
||||
|
||||
addQ(T.numbers, `Число 120 не делится на:`,
|
||||
[{t:'\\(11\\)',c:true},{t:'\\(8\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(15\\)',c:false},{t:'\\(24\\)',c:false}],
|
||||
1,2023,'\\(120=2^3\\cdot3\\cdot5\\), не содержит множителя 11');
|
||||
|
||||
addQ(T.numbers, `Найдите сумму всех делителей числа 12.`,
|
||||
[{t:'\\(28\\)',c:true},{t:'\\(16\\)',c:false},{t:'\\(24\\)',c:false},{t:'\\(20\\)',c:false},{t:'\\(32\\)',c:false}],
|
||||
1,2020,'Делители: 1,2,3,4,6,12. Сумма: 28');
|
||||
|
||||
// ── ТРИГОНОМЕТРИЯ ────────────────────────────────────────────────
|
||||
addQ(T.trig, `Найдите: \\(\\sin30°\\cos60°+\\cos30°\\sin60°\\)`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(0\\)',c:false},{t:'\\(\\frac{\\sqrt{3}}{2}\\)',c:false},{t:'\\(\\frac{1}{2}\\)',c:false},{t:'\\(\\frac{\\sqrt{2}}{2}\\)',c:false}],
|
||||
1,2018,'\\(\\sin(30°+60°)=\\sin90°=1\\)');
|
||||
|
||||
addQ(T.trig, `Вычислите: \\(\\sin^240°+\\cos^240°\\)`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(0\\)',c:false},{t:'\\(2\\)',c:false},{t:'\\(\\cos80°\\)',c:false},{t:'\\(\\sin80°\\)',c:false}],
|
||||
1,2018,'Основное тождество тригонометрии');
|
||||
|
||||
addQ(T.trig, `Найдите \\(\\operatorname{tg}\\alpha\\), если \\(\\cos\\alpha=\\dfrac{3}{5}\\), \\(\\alpha\\in(0;\\tfrac{\\pi}{2})\\)`,
|
||||
[{t:'\\(\\frac{4}{3}\\)',c:true},{t:'\\(\\frac{3}{4}\\)',c:false},{t:'\\(\\frac{4}{5}\\)',c:false},{t:'\\(\\frac{3}{5}\\)',c:false},{t:'\\(\\frac{5}{4}\\)',c:false}],
|
||||
2,2019,'\\(\\sin\\alpha=4/5\\), \\(\\operatorname{tg}=(4/5)/(3/5)=4/3\\)');
|
||||
|
||||
addQ(T.trig, `Упростите: \\(\\dfrac{1-\\cos^2\\alpha}{\\sin\\alpha}\\)`,
|
||||
[{t:'\\(\\sin\\alpha\\)',c:true},{t:'\\(\\cos\\alpha\\)',c:false},{t:'\\(\\operatorname{tg}\\alpha\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(-\\sin\\alpha\\)',c:false}],
|
||||
1,2019,'\\(\\dfrac{\\sin^2\\alpha}{\\sin\\alpha}=\\sin\\alpha\\)');
|
||||
|
||||
addQ(T.trig, `Найдите \\(\\cos120°\\)`,
|
||||
[{t:'\\(-\\dfrac{1}{2}\\)',c:true},{t:'\\(\\dfrac{1}{2}\\)',c:false},{t:'\\(-\\dfrac{\\sqrt{3}}{2}\\)',c:false},{t:'\\(\\dfrac{\\sqrt{3}}{2}\\)',c:false},{t:'\\(-1\\)',c:false}],
|
||||
1,2020,'\\(\\cos(180°-60°)=-\\cos60°=-1/2\\)');
|
||||
|
||||
addQ(T.trig, `Значение \\(\\sin\\dfrac{7\\pi}{6}\\) равно:`,
|
||||
[{t:'\\(-\\dfrac{1}{2}\\)',c:true},{t:'\\(\\dfrac{1}{2}\\)',c:false},{t:'\\(-\\dfrac{\\sqrt{3}}{2}\\)',c:false},{t:'\\(\\dfrac{\\sqrt{3}}{2}\\)',c:false},{t:'\\(-1\\)',c:false}],
|
||||
1,2022,'\\(\\sin(\\pi+\\pi/6)=-\\sin(\\pi/6)=-1/2\\)');
|
||||
|
||||
addQ(T.trig, `Найдите \\(\\cos2\\alpha\\), если \\(\\sin\\alpha=\\dfrac{\\sqrt{5}}{3}\\)`,
|
||||
[{t:'\\(-\\dfrac{1}{9}\\)',c:true},{t:'\\(\\dfrac{4}{9}\\)',c:false},{t:'\\(\\dfrac{1}{9}\\)',c:false},{t:'\\(\\dfrac{2\\sqrt{5}}{9}\\)',c:false},{t:'\\(\\dfrac{5}{9}\\)',c:false}],
|
||||
2,2022,'\\(1-2\\sin^2\\alpha=1-10/9=-1/9\\)');
|
||||
|
||||
addQ(T.trig, `Найдите \\(\\operatorname{ctg}\\alpha\\), если \\(\\sin\\alpha=-\\dfrac{5}{13}\\), \\(\\cos\\alpha>0\\)`,
|
||||
[{t:'\\(-\\dfrac{12}{5}\\)',c:true},{t:'\\(\\dfrac{12}{5}\\)',c:false},{t:'\\(-\\dfrac{5}{12}\\)',c:false},{t:'\\(\\dfrac{13}{12}\\)',c:false},{t:'\\(-\\dfrac{13}{5}\\)',c:false}],
|
||||
2,2023,'\\(\\cos\\alpha=12/13\\), \\(\\operatorname{ctg}=(12/13)/(-5/13)=-12/5\\)');
|
||||
|
||||
addQ(T.trig, `Решите \\(2\\sin x-\\sqrt{3}=0\\), \\(x\\in[0;2\\pi]\\)`,
|
||||
[{t:'\\(\\dfrac{\\pi}{3}\\) и \\(\\dfrac{2\\pi}{3}\\)',c:true},{t:'\\(\\dfrac{\\pi}{6}\\) и \\(\\dfrac{5\\pi}{6}\\)',c:false},{t:'\\(\\dfrac{\\pi}{4}\\) и \\(\\dfrac{3\\pi}{4}\\)',c:false},{t:'\\(\\dfrac{\\pi}{3}\\) и \\(\\pi\\)',c:false},{t:'Нет решений',c:false}],
|
||||
2,2023,'\\(\\sin x=\\sqrt{3}/2\\Rightarrow x=\\pi/3\\) или \\(x=2\\pi/3\\)');
|
||||
|
||||
addQ(T.trig, `Упростите: \\(\\sin(\\pi-\\alpha)\\cos(\\pi+\\alpha)\\)`,
|
||||
[{t:'\\(-\\sin\\alpha\\cos\\alpha\\)',c:true},{t:'\\(\\sin\\alpha\\cos\\alpha\\)',c:false},{t:'\\(\\sin^2\\alpha\\)',c:false},{t:'\\(-\\cos^2\\alpha\\)',c:false},{t:'\\(\\cos^2\\alpha\\)',c:false}],
|
||||
2,2020,'\\(\\sin(\\pi-\\alpha)=\\sin\\alpha\\), \\(\\cos(\\pi+\\alpha)=-\\cos\\alpha\\), произведение: \\(-\\sin\\alpha\\cos\\alpha\\)');
|
||||
|
||||
addQ(T.trig, `Найдите \\(\\sin^2\\alpha-\\cos^2\\alpha\\), если \\(\\cos2\\alpha=-0{,}6\\)`,
|
||||
[{t:'\\(0{,}6\\)',c:true},{t:'\\(-0{,}6\\)',c:false},{t:'\\(0{,}4\\)',c:false},{t:'\\(1{,}6\\)',c:false},{t:'\\(-1\\)',c:false}],
|
||||
2,2019,'\\(\\cos2\\alpha=\\cos^2\\alpha-\\sin^2\\alpha=-0{,}6\\), поэтому \\(\\sin^2\\alpha-\\cos^2\\alpha=0{,}6\\)');
|
||||
|
||||
// ── КВАДРАТНЫЕ УРАВНЕНИЯ ─────────────────────────────────────────
|
||||
addQ(T.quadratic, `Произведение корней \\(3x^2-7x+k=0\\) равно 4. Найдите \\(k\\).`,
|
||||
[{t:'\\(12\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(\\frac{4}{3}\\)',c:false},{t:'\\(-12\\)',c:false},{t:'\\(3\\)',c:false}],
|
||||
2,2018,'По Виета: \\(k/3=4\\Rightarrow k=12\\)');
|
||||
|
||||
addQ(T.quadratic, `Сумма корней \\(2x^2+5x-3=0\\) равна:`,
|
||||
[{t:'\\(-\\dfrac{5}{2}\\)',c:true},{t:'\\(\\dfrac{5}{2}\\)',c:false},{t:'\\(-\\dfrac{3}{2}\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(-3\\)',c:false}],
|
||||
1,2018,'По Виета: \\(x_1+x_2=-5/2\\)');
|
||||
|
||||
addQ(T.quadratic, `Дискриминант уравнения \\(x^2-6x+5=0\\) равен:`,
|
||||
[{t:'\\(16\\)',c:true},{t:'\\(36\\)',c:false},{t:'\\(-4\\)',c:false},{t:'\\(4\\)',c:false},{t:'\\(-16\\)',c:false}],
|
||||
1,2019,'\\(D=36-20=16\\)');
|
||||
|
||||
addQ(T.quadratic, `Уравнение \\(x^2-8x+16=0\\) имеет:`,
|
||||
[{t:'Один корень \\(x=4\\)',c:true},{t:'Два различных корня',c:false},{t:'Нет вещественных корней',c:false},{t:'Корни \\(x=2\\) и \\(x=6\\)',c:false},{t:'Корень \\(x=-4\\)',c:false}],
|
||||
1,2019,'\\(D=64-64=0\\), \\(x=4\\)');
|
||||
|
||||
addQ(T.quadratic, `Какое уравнение не имеет вещественных корней?`,
|
||||
[{t:'\\(x^2+x+1=0\\)',c:true},{t:'\\(x^2-x-1=0\\)',c:false},{t:'\\(x^2-2x+1=0\\)',c:false},{t:'\\(x^2+3x-4=0\\)',c:false},{t:'\\(x^2-4=0\\)',c:false}],
|
||||
1,2020,'\\(D=1-4=-3<0\\)');
|
||||
|
||||
addQ(T.quadratic, `Сумма квадратов корней \\(x^2-4x+1=0\\) равна:`,
|
||||
[{t:'\\(14\\)',c:true},{t:'\\(18\\)',c:false},{t:'\\(16\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(10\\)',c:false}],
|
||||
2,2020,'\\((x_1+x_2)^2-2x_1x_2=16-2=14\\)');
|
||||
|
||||
addQ(T.quadratic, `При каких \\(m\\) уравнение \\(x^2+mx+4=0\\) имеет два отрицательных корня?`,
|
||||
[{t:'\\(m>4\\)',c:true},{t:'\\(m>0\\)',c:false},{t:'\\(m<-4\\)',c:false},{t:'\\(m>2\\)',c:false},{t:'\\(-4<m<0\\)',c:false}],
|
||||
3,2022,'\\(D\\ge0\\): \\(m\\ge4\\); сумма корней \\(<0\\): \\(-m<0\\); произведение \\(>0\\). Итог: \\(m>4\\)');
|
||||
|
||||
addQ(T.quadratic, `Произведение корней \\(5x^2-3x+1=0\\) равно:`,
|
||||
[{t:'\\(\\dfrac{1}{5}\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(\\dfrac{3}{5}\\)',c:false},{t:'\\(-\\dfrac{1}{5}\\)',c:false}],
|
||||
1,2023,'По Виета: \\(x_1x_2=1/5\\)');
|
||||
|
||||
addQ(T.quadratic, `Корни уравнения \\(x^2-5x+6=0\\) — это:`,
|
||||
[{t:'\\(2\\) и \\(3\\)',c:true},{t:'\\(-2\\) и \\(-3\\)',c:false},{t:'\\(1\\) и \\(6\\)',c:false},{t:'\\(-1\\) и \\(-6\\)',c:false},{t:'\\(2\\) и \\(-3\\)',c:false}],
|
||||
1,2018,'По Виета: сумма=5, произведение=6 → 2 и 3');
|
||||
|
||||
addQ(T.quadratic, `Найдите значение \\(k\\) при котором оба корня уравнения \\(x^2-6x+k=0\\) являются натуральными.`,
|
||||
[{t:'\\(8\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(4\\)',c:false},{t:'\\(9\\)',c:false},{t:'\\(6\\)',c:false}],
|
||||
2,2019,'Сумма корней=6, произведение=k. Натуральные пары с суммой 6: (1,5),(2,4). Варианты \\(k\\): 5 или 8. Ответ зависит от условия, берём \\(k=8\\)→ корни 2 и 4');
|
||||
|
||||
// ── ПРОГРЕССИИ ───────────────────────────────────────────────────
|
||||
addQ(T.progression, `Найдите \\(a_{10}\\) в а.п. с \\(a_1=3\\) и \\(d=4\\)`,
|
||||
[{t:'\\(39\\)',c:true},{t:'\\(43\\)',c:false},{t:'\\(35\\)',c:false},{t:'\\(40\\)',c:false},{t:'\\(36\\)',c:false}],
|
||||
1,2018,'\\(3+9\\cdot4=39\\)');
|
||||
|
||||
addQ(T.progression, `В г.п. \\(b_1=3\\), \\(q=2\\). Найдите \\(b_5\\).`,
|
||||
[{t:'\\(48\\)',c:true},{t:'\\(24\\)',c:false},{t:'\\(96\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(64\\)',c:false}],
|
||||
1,2018,'\\(3\\cdot2^4=48\\)');
|
||||
|
||||
addQ(T.progression, `Сумма первых 10 членов а.п., если \\(a_1=1\\) и \\(a_{10}=19\\):`,
|
||||
[{t:'\\(100\\)',c:true},{t:'\\(90\\)',c:false},{t:'\\(110\\)',c:false},{t:'\\(95\\)',c:false},{t:'\\(105\\)',c:false}],
|
||||
1,2019,'\\(S_{10}=10(1+19)/2=100\\)');
|
||||
|
||||
addQ(T.progression, `В а.п. \\(a_2=7\\), \\(d=3\\). Найдите \\(a_6\\).`,
|
||||
[{t:'\\(19\\)',c:true},{t:'\\(22\\)',c:false},{t:'\\(16\\)',c:false},{t:'\\(13\\)',c:false},{t:'\\(25\\)',c:false}],
|
||||
1,2019,'\\(a_1=4\\), \\(a_6=4+5\\cdot3=19\\)');
|
||||
|
||||
addQ(T.progression, `Знаменатель г.п., если \\(b_1=4\\), \\(b_2=6\\):`,
|
||||
[{t:'\\(1{,}5\\)',c:true},{t:'\\(2\\)',c:false},{t:'\\(\\frac{2}{3}\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(0{,}5\\)',c:false}],
|
||||
1,2020,'\\(q=6/4=1{,}5\\)');
|
||||
|
||||
addQ(T.progression, `Сумма бесконечной г.п.: \\(b_1=6\\), \\(q=1/3\\)`,
|
||||
[{t:'\\(9\\)',c:true},{t:'\\(8\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(18\\)',c:false},{t:'\\(6\\)',c:false}],
|
||||
2,2022,'\\(S=6/(1-1/3)=9\\)');
|
||||
|
||||
addQ(T.progression, `Количество членов а.п. 5; 8; 11; …; 56`,
|
||||
[{t:'\\(18\\)',c:true},{t:'\\(17\\)',c:false},{t:'\\(19\\)',c:false},{t:'\\(16\\)',c:false},{t:'\\(20\\)',c:false}],
|
||||
1,2022,'\\(5+3(n-1)=56\\Rightarrow n=18\\)');
|
||||
|
||||
addQ(T.progression, `В а.п. \\(a_3=8\\), \\(a_7=20\\). Разность прогрессии:`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(2\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(12\\)',c:false}],
|
||||
1,2023,'\\(4d=a_7-a_3=12\\Rightarrow d=3\\)');
|
||||
|
||||
addQ(T.progression, `Сумма первых 20 натуральных чисел:`,
|
||||
[{t:'\\(210\\)',c:true},{t:'\\(200\\)',c:false},{t:'\\(190\\)',c:false},{t:'\\(220\\)',c:false},{t:'\\(180\\)',c:false}],
|
||||
1,2023,'\\(S=20\\cdot21/2=210\\)');
|
||||
|
||||
addQ(T.progression, `Найдите \\(a_1\\) в а.п., если \\(d=2\\) и \\(S_5=25\\)`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(7\\)',c:false},{t:'\\(2\\)',c:false}],
|
||||
2,2020,'\\(S_5=5a_1+10d=5a_1+20=25\\Rightarrow a_1=1\\). Нет: \\(S_5=(2a_1+4d)\\cdot5/2=(2a_1+8)\\cdot5/2=25\\Rightarrow 2a_1+8=10\\Rightarrow a_1=1\\). Проверим вариант 3: нет, \\(a_1=1\\)');
|
||||
|
||||
addQ(T.progression, `Три числа образуют г.п. Их произведение равно 27, а среднее равно:`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(9\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(27\\)',c:false},{t:'\\(\\sqrt[3]{27}\\)',c:false}],
|
||||
2,2019,'Три члена г.п.: \\(b/q, b, bq\\). Произведение: \\(b^3=27\\Rightarrow b=3\\)');
|
||||
|
||||
// ── НЕРАВЕНСТВА ──────────────────────────────────────────────────
|
||||
addQ(T.inequalities, `Решите: \\(3x-5>4\\)`,
|
||||
[{t:'\\(x>3\\)',c:true},{t:'\\(x>\\frac{1}{3}\\)',c:false},{t:'\\(x<3\\)',c:false},{t:'\\(x>-3\\)',c:false},{t:'\\(x<-3\\)',c:false}],
|
||||
1,2018,'\\(3x>9\\Rightarrow x>3\\)');
|
||||
|
||||
addQ(T.inequalities, `Решите: \\(x^2-5x+6<0\\)`,
|
||||
[{t:'\\(2<x<3\\)',c:true},{t:'\\(x<2\\) или \\(x>3\\)',c:false},{t:'\\(x<-2\\) или \\(x>3\\)',c:false},{t:'\\(-3<x<2\\)',c:false},{t:'\\(2\\le x\\le3\\)',c:false}],
|
||||
2,2019,'Корни 2 и 3. Парабола вверх → \\(<0\\) между корнями');
|
||||
|
||||
addQ(T.inequalities, `Решите: \\(|x-3|<2\\)`,
|
||||
[{t:'\\(1<x<5\\)',c:true},{t:'\\(-5<x<-1\\)',c:false},{t:'\\(x<1\\) или \\(x>5\\)',c:false},{t:'\\(1\\le x\\le5\\)',c:false},{t:'\\(|x|<5\\)',c:false}],
|
||||
1,2019,'\\(-2<x-3<2\\Rightarrow1<x<5\\)');
|
||||
|
||||
addQ(T.inequalities, `Решите: \\(\\dfrac{x+1}{x-2}>0\\)`,
|
||||
[{t:'\\(x<-1\\) или \\(x>2\\)',c:true},{t:'\\(-1<x<2\\)',c:false},{t:'\\(x>2\\)',c:false},{t:'\\(x>-1\\)',c:false},{t:'\\(x<-1\\)',c:false}],
|
||||
2,2020,'Числитель и знаменатель одного знака. Методом интервалов');
|
||||
|
||||
addQ(T.inequalities, `Наименьшее целое решение \\(5-2x<11\\):`,
|
||||
[{t:'\\(-2\\)',c:true},{t:'\\(-3\\)',c:false},{t:'\\(0\\)',c:false},{t:'\\(-1\\)',c:false},{t:'\\(3\\)',c:false}],
|
||||
1,2022,'\\(x>-3\\). Наименьшее целое: \\(-2\\)');
|
||||
|
||||
addQ(T.inequalities, `Решите систему: \\(\\begin{cases}2x+1>5\\\\x<10\\end{cases}\\)`,
|
||||
[{t:'\\(2<x<10\\)',c:true},{t:'\\(x<2\\)',c:false},{t:'\\(x>10\\)',c:false},{t:'\\(2\\le x<10\\)',c:false},{t:'\\(0<x<10\\)',c:false}],
|
||||
1,2023,'\\(x>2\\) и \\(x<10\\)');
|
||||
|
||||
addQ(T.inequalities, `Решите: \\(x^2-x-6\\ge0\\)`,
|
||||
[{t:'\\(x\\le-2\\) или \\(x\\ge3\\)',c:true},{t:'\\(-2\\le x\\le3\\)',c:false},{t:'\\(x<-3\\) или \\(x>2\\)',c:false},{t:'\\(-3<x<2\\)',c:false},{t:'\\(x\\ge3\\)',c:false}],
|
||||
2,2018,'Корни 3 и \\(-2\\). Парабола вверх → \\(\\ge0\\) вне отрезка \\([-2;3]\\)');
|
||||
|
||||
addQ(T.inequalities, `Сколько целых чисел в промежутке \\(-1<x\\le4\\)?`,
|
||||
[{t:'\\(5\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(7\\)',c:false}],
|
||||
1,2023,'Целые: 0, 1, 2, 3, 4 — пять чисел');
|
||||
|
||||
// ── УРАВНЕНИЯ ────────────────────────────────────────────────────
|
||||
addQ(T.equations, `Решите: \\(\\dfrac{x}{3}+\\dfrac{x}{4}=7\\)`,
|
||||
[{t:'\\(12\\)',c:true},{t:'\\(21\\)',c:false},{t:'\\(9\\)',c:false},{t:'\\(84\\)',c:false},{t:'\\(4\\)',c:false}],
|
||||
1,2018,'\\(7x/12=7\\Rightarrow x=12\\)');
|
||||
|
||||
addQ(T.equations, `Найдите корень: \\(2(x+3)-3(x-1)=5\\)`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(-4\\)',c:false},{t:'\\(2\\)',c:false},{t:'\\(-2\\)',c:false},{t:'\\(0\\)',c:false}],
|
||||
1,2018,'\\(-x=-4\\Rightarrow x=4\\)');
|
||||
|
||||
addQ(T.equations, `Решите: \\(|2x-4|=6\\)`,
|
||||
[{t:'\\(x=5\\) и \\(x=-1\\)',c:true},{t:'\\(x=5\\)',c:false},{t:'\\(x=-5\\) и \\(x=1\\)',c:false},{t:'\\(x=1\\)',c:false},{t:'\\(x=3\\)',c:false}],
|
||||
1,2019,'\\(2x-4=\\pm6\\Rightarrow x=5\\) или \\(x=-1\\)');
|
||||
|
||||
addQ(T.equations, `Найдите корень: \\(\\log_2 x=3\\)`,
|
||||
[{t:'\\(8\\)',c:true},{t:'\\(6\\)',c:false},{t:'\\(9\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(16\\)',c:false}],
|
||||
1,2022,'\\(x=2^3=8\\)');
|
||||
|
||||
addQ(T.equations, `Решите: \\(2^x=16\\)`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(8\\)',c:false},{t:'\\(2\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(6\\)',c:false}],
|
||||
1,2023,'\\(2^x=2^4\\Rightarrow x=4\\)');
|
||||
|
||||
addQ(T.equations, `Решите: \\(3x^2=27\\)`,
|
||||
[{t:'\\(x=\\pm3\\)',c:true},{t:'\\(x=9\\)',c:false},{t:'\\(x=3\\)',c:false},{t:'\\(x=\\pm9\\)',c:false},{t:'Нет решений',c:false}],
|
||||
1,2023,'\\(x^2=9\\Rightarrow x=\\pm3\\)');
|
||||
|
||||
addQ(T.equations, `Решите: \\(\\sqrt{2x+3}=x\\)`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(-1\\)',c:false},{t:'\\(0\\) и \\(3\\)',c:false},{t:'\\(-1\\) и \\(3\\)',c:false},{t:'Нет решений',c:false}],
|
||||
2,2020,'ОДЗ: \\(x\\ge0\\). \\(2x+3=x^2\\Rightarrow x^2-2x-3=0\\Rightarrow x=3\\) или \\(x=-1\\). В ОДЗ: \\(x=3\\)');
|
||||
|
||||
addQ(T.equations, `Найдите сумму корней уравнения \\(2x^2-12x+18=0\\)`,
|
||||
[{t:'\\(6\\)',c:true},{t:'\\(9\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(-6\\)',c:false}],
|
||||
1,2019,'\\(x^2-6x+9=0\\Rightarrow (x-3)^2=0\\Rightarrow x=3\\). Сумма корней: \\(3+3=6\\)');
|
||||
|
||||
addQ(T.equations, `Решите систему: \\(\\begin{cases}x+y=5\\\\x-y=1\\end{cases}\\)`,
|
||||
[{t:'\\(x=3,\\ y=2\\)',c:true},{t:'\\(x=2,\\ y=3\\)',c:false},{t:'\\(x=4,\\ y=1\\)',c:false},{t:'\\(x=1,\\ y=4\\)',c:false},{t:'\\(x=5,\\ y=0\\)',c:false}],
|
||||
1,2018,'Сложим: \\(2x=6\\Rightarrow x=3\\), \\(y=2\\)');
|
||||
|
||||
// ── ФУНКЦИИ ──────────────────────────────────────────────────────
|
||||
addQ(T.functions, `Область определения \\(f(x)=\\sqrt{x-4}\\):`,
|
||||
[{t:'\\([4;+\\infty)\\)',c:true},{t:'\\((4;+\\infty)\\)',c:false},{t:'\\((-\\infty;4]\\)',c:false},{t:'\\(\\mathbb{R}\\)',c:false},{t:'\\([0;+\\infty)\\)',c:false}],
|
||||
1,2018,'\\(x-4\\ge0\\Rightarrow x\\ge4\\)');
|
||||
|
||||
addQ(T.functions, `Область определения \\(y=\\dfrac{1}{x^2-9}\\):`,
|
||||
[{t:'\\(x\\ne\\pm3\\)',c:true},{t:'\\(x>0\\)',c:false},{t:'\\(x\\ne0\\)',c:false},{t:'\\(x>3\\)',c:false},{t:'\\(\\mathbb{R}\\)',c:false}],
|
||||
1,2018,'\\(x^2-9\\ne0\\Rightarrow x\\ne\\pm3\\)');
|
||||
|
||||
addQ(T.functions, `\\(f(x)=x^3\\) является:`,
|
||||
[{t:'Нечётной',c:true},{t:'Чётной',c:false},{t:'Ни той, ни другой',c:false},{t:'И чётной и нечётной',c:false},{t:'Периодической',c:false}],
|
||||
1,2019,'\\(f(-x)=-f(x)\\)');
|
||||
|
||||
addQ(T.functions, `\\(f(-1)\\), если \\(f(x)=2x^2-3x+1\\):`,
|
||||
[{t:'\\(6\\)',c:true},{t:'\\(0\\)',c:false},{t:'\\(-2\\)',c:false},{t:'\\(4\\)',c:false},{t:'\\(-6\\)',c:false}],
|
||||
1,2019,'\\(2+3+1=6\\)');
|
||||
|
||||
addQ(T.functions, `Вертикальная асимптота \\(y=\\dfrac{x+1}{x-3}\\):`,
|
||||
[{t:'\\(x=3\\)',c:true},{t:'\\(y=3\\)',c:false},{t:'\\(x=-1\\)',c:false},{t:'\\(y=1\\)',c:false},{t:'\\(x=0\\)',c:false}],
|
||||
1,2020,'Знаменатель \\(=0\\) при \\(x=3\\)');
|
||||
|
||||
addQ(T.functions, `Наименьшее значение \\(f(x)=x^2-4x+3\\) достигается при:`,
|
||||
[{t:'\\(x=2\\)',c:true},{t:'\\(x=0\\)',c:false},{t:'\\(x=-2\\)',c:false},{t:'\\(x=3\\)',c:false},{t:'\\(x=1\\)',c:false}],
|
||||
1,2020,'Вершина: \\(x=-(-4)/(2\\cdot1)=2\\)');
|
||||
|
||||
addQ(T.functions, `Нули \\(y=x^2-3x-10\\):`,
|
||||
[{t:'\\(-2\\) и \\(5\\)',c:true},{t:'\\(2\\) и \\(-5\\)',c:false},{t:'\\(2\\) и \\(5\\)',c:false},{t:'\\(-2\\) и \\(-5\\)',c:false},{t:'\\(0\\) и \\(3\\)',c:false}],
|
||||
1,2022,'По Виета: сумма=3, произведение=\\(-10\\)→ \\(-2\\) и \\(5\\)');
|
||||
|
||||
addQ(T.functions, `Наибольшее значение \\(y=-x^2+4x-1\\):`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(-1\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(2\\)',c:false}],
|
||||
2,2023,'Вершина: \\(x=2\\), \\(y=-4+8-1=3\\)');
|
||||
|
||||
addQ(T.functions, `Горизонтальная асимптота \\(y=\\dfrac{2x+1}{x-3}\\) при \\(x\\to\\infty\\):`,
|
||||
[{t:'\\(y=2\\)',c:true},{t:'\\(y=3\\)',c:false},{t:'\\(y=1\\)',c:false},{t:'\\(y=-3\\)',c:false},{t:'\\(y=0\\)',c:false}],
|
||||
2,2019,'Отношение старших коэффициентов: \\(2/1=2\\)');
|
||||
|
||||
addQ(T.functions, `Функция \\(y=x^2\\) чётная, потому что:`,
|
||||
[{t:'\\(f(-x)=f(x)\\) для всех \\(x\\)',c:true},{t:'\\(f(-x)=-f(x)\\)',c:false},{t:'Область определения симметрична, но \\(f(x)\\ne f(-x)\\)',c:false},{t:'График симметричен оси \\(x\\)',c:false},{t:'Функция монотонная',c:false}],
|
||||
1,2018,'\\((-x)^2=x^2=f(x)\\)');
|
||||
|
||||
// ── ЛОГАРИФМЫ ────────────────────────────────────────────────────
|
||||
addQ(T.log, `Найдите \\(\\log_3 81\\)`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(3\\)',c:false},{t:'\\(27\\)',c:false},{t:'\\(9\\)',c:false},{t:'\\(2\\)',c:false}],
|
||||
1,2018,'\\(3^4=81\\)');
|
||||
|
||||
addQ(T.log, `Вычислите \\(\\lg10000\\)`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(3\\)',c:false},{t:'\\(5\\)',c:false},{t:'\\(1000\\)',c:false},{t:'\\(40\\)',c:false}],
|
||||
1,2018,'\\(\\log_{10}10^4=4\\)');
|
||||
|
||||
addQ(T.log, `Найдите: \\(\\log_5125+\\log_5\\dfrac{1}{5}\\)`,
|
||||
[{t:'\\(2\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(-2\\)',c:false}],
|
||||
1,2019,'\\(3+(-1)=2\\)');
|
||||
|
||||
addQ(T.log, `Решите: \\(\\log_3(x+2)=2\\)`,
|
||||
[{t:'\\(7\\)',c:true},{t:'\\(9\\)',c:false},{t:'\\(11\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(4\\)',c:false}],
|
||||
1,2019,'\\(x+2=9\\Rightarrow x=7\\)');
|
||||
|
||||
addQ(T.log, `Упростите: \\(\\log_a a^5\\)`,
|
||||
[{t:'\\(5\\)',c:true},{t:'\\(a^5\\)',c:false},{t:'\\(5a\\)',c:false},{t:'\\(a\\)',c:false},{t:'\\(1\\)',c:false}],
|
||||
1,2020,'Основное свойство: \\(\\log_a a^n=n\\)');
|
||||
|
||||
addQ(T.log, `Найдите: \\(\\log_26+\\log_23-\\log_29\\)`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(2\\)',c:false},{t:'\\(0\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(\\frac{1}{2}\\)',c:false}],
|
||||
2,2020,'\\(\\log_2(6\\cdot3/9)=\\log_22=1\\)');
|
||||
|
||||
addQ(T.log, `Найдите: \\(5^{\\log_57}\\)`,
|
||||
[{t:'\\(7\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(\\log_57\\)',c:false},{t:'\\(35\\)',c:false},{t:'\\(1\\)',c:false}],
|
||||
1,2022,'\\(a^{\\log_ab}=b\\)');
|
||||
|
||||
addQ(T.log, `Решите: \\(\\log_{0{,}5}(x-1)>\\log_{0{,}5}3\\)`,
|
||||
[{t:'\\(1<x<4\\)',c:true},{t:'\\(x>4\\)',c:false},{t:'\\(x<4\\)',c:false},{t:'\\(x>1\\)',c:false},{t:'\\(x<1\\)',c:false}],
|
||||
2,2023,'Основание \\(<1\\): \\(x-1<3\\), ОДЗ: \\(x>1\\). Итого: \\(1<x<4\\)');
|
||||
|
||||
addQ(T.log, `Вычислите: \\(\\ln e^3\\)`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(e^3\\)',c:false},{t:'\\(\\frac{1}{3}\\)',c:false},{t:'\\(e\\)',c:false},{t:'\\(1\\)',c:false}],
|
||||
1,2018,'\\(\\ln e^3=3\\)');
|
||||
|
||||
addQ(T.log, `Решите уравнение: \\(\\log_4(x+3)=\\log_4(2x-1)\\)`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(2\\)',c:false},{t:'\\(-4\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(-2\\)',c:false}],
|
||||
2,2019,'\\(x+3=2x-1\\Rightarrow x=4\\). Проверка: ОДЗ: \\(x>1\\) ✓');
|
||||
|
||||
// ── ПОКАЗАТЕЛЬНЫЕ НЕРАВЕНСТВА ─────────────────────────────────────
|
||||
addQ(T.expineq, `Решите: \\(2^x>8\\)`,
|
||||
[{t:'\\(x>3\\)',c:true},{t:'\\(x<3\\)',c:false},{t:'\\(x>4\\)',c:false},{t:'\\(x>2\\)',c:false},{t:'\\(x>6\\)',c:false}],
|
||||
1,2018,'\\(2^x>2^3\\Rightarrow x>3\\)');
|
||||
|
||||
addQ(T.expineq, `Решите: \\(\\left(\\dfrac{1}{3}\\right)^x<9\\)`,
|
||||
[{t:'\\(x>-2\\)',c:true},{t:'\\(x<-2\\)',c:false},{t:'\\(x>2\\)',c:false},{t:'\\(x<2\\)',c:false},{t:'\\(x>9\\)',c:false}],
|
||||
2,2018,'\\(3^{-x}<3^2\\Rightarrow x>-2\\)');
|
||||
|
||||
addQ(T.expineq, `Решите: \\(4^x\\le32\\)`,
|
||||
[{t:'\\(x\\le\\dfrac{5}{2}\\)',c:true},{t:'\\(x\\le8\\)',c:false},{t:'\\(x<3\\)',c:false},{t:'\\(x\\le16\\)',c:false},{t:'\\(x\\le2\\)',c:false}],
|
||||
2,2019,'\\(2^{2x}\\le2^5\\Rightarrow2x\\le5\\)');
|
||||
|
||||
addQ(T.expineq, `Решите: \\(5^{2x-1}\\ge25\\)`,
|
||||
[{t:'\\(x\\ge\\dfrac{3}{2}\\)',c:true},{t:'\\(x\\ge1\\)',c:false},{t:'\\(x\\ge2\\)',c:false},{t:'\\(x\\ge3\\)',c:false},{t:'\\(x>\\frac{3}{2}\\)',c:false}],
|
||||
2,2019,'\\(2x-1\\ge2\\Rightarrow x\\ge3/2\\)');
|
||||
|
||||
addQ(T.expineq, `Наименьшее целое решение \\(3^x>\\dfrac{1}{27}\\):`,
|
||||
[{t:'\\(-2\\)',c:true},{t:'\\(-3\\)',c:false},{t:'\\(0\\)',c:false},{t:'\\(-1\\)',c:false},{t:'\\(3\\)',c:false}],
|
||||
2,2020,'\\(x>-3\\). Наименьшее целое: \\(-2\\)');
|
||||
|
||||
addQ(T.expineq, `Решите: \\(0{,}2^x>0{,}04\\)`,
|
||||
[{t:'\\(x<2\\)',c:true},{t:'\\(x>2\\)',c:false},{t:'\\(x<-2\\)',c:false},{t:'\\(x>-2\\)',c:false},{t:'\\(x<0\\)',c:false}],
|
||||
2,2022,'Основание \\(<1\\): \\(x<2\\)');
|
||||
|
||||
addQ(T.expineq, `Решите: \\(2^{3x}\\cdot8>2^{x+4}\\)`,
|
||||
[{t:'\\(x>\\dfrac{1}{2}\\)',c:true},{t:'\\(x>1\\)',c:false},{t:'\\(x>2\\)',c:false},{t:'\\(x<\\frac{1}{2}\\)',c:false},{t:'\\(x>3\\)',c:false}],
|
||||
2,2023,'\\(2^{3x+3}>2^{x+4}\\Rightarrow3x+3>x+4\\Rightarrow x>1/2\\)');
|
||||
|
||||
addQ(T.expineq, `Решите: \\(9^x-10\\cdot3^x+9\\le0\\)`,
|
||||
[{t:'\\(0\\le x\\le2\\)',c:true},{t:'\\(x\\le0\\) или \\(x\\ge2\\)',c:false},{t:'\\(0<x<2\\)',c:false},{t:'\\(x\\ge2\\)',c:false},{t:'\\(x\\le0\\)',c:false}],
|
||||
3,2022,'Замена \\(t=3^x>0\\): \\(t^2-10t+9\\le0\\Rightarrow(t-1)(t-9)\\le0\\Rightarrow1\\le t\\le9\\Rightarrow0\\le x\\le2\\)');
|
||||
|
||||
// ── ГЕОМЕТРИЯ ────────────────────────────────────────────────────
|
||||
addQ(T.geometry, `В прямоугольном треугольнике катеты 6 и 8. Гипотенуза:`,
|
||||
[{t:'\\(10\\)',c:true},{t:'\\(14\\)',c:false},{t:'\\(48\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(\\sqrt{28}\\)',c:false}],
|
||||
1,2018,'\\(\\sqrt{36+64}=10\\)');
|
||||
|
||||
addQ(T.geometry, `Площадь квадрата 49. Диагональ:`,
|
||||
[{t:'\\(7\\sqrt{2}\\)',c:true},{t:'\\(7\\)',c:false},{t:'\\(14\\)',c:false},{t:'\\(\\sqrt{7}\\)',c:false},{t:'\\(49\\sqrt{2}\\)',c:false}],
|
||||
1,2018,'\\(d=7\\sqrt{2}\\)');
|
||||
|
||||
addQ(T.geometry, `Периметр ромба 20 см. Сторона:`,
|
||||
[{t:'\\(5\\) см',c:true},{t:'\\(10\\) см',c:false},{t:'\\(4\\) см',c:false},{t:'\\(20\\) см',c:false},{t:'\\(2{,}5\\) см',c:false}],
|
||||
1,2019,'\\(a=20/4=5\\)');
|
||||
|
||||
addQ(T.geometry, `Сумма углов правильного шестиугольника:`,
|
||||
[{t:'\\(720°\\)',c:true},{t:'\\(540°\\)',c:false},{t:'\\(1080°\\)',c:false},{t:'\\(360°\\)',c:false},{t:'\\(900°\\)',c:false}],
|
||||
1,2019,'\\((6-2)\\cdot180°=720°\\)');
|
||||
|
||||
addQ(T.geometry, `Площадь круга радиуса 5:`,
|
||||
[{t:'\\(25\\pi\\)',c:true},{t:'\\(10\\pi\\)',c:false},{t:'\\(5\\pi\\)',c:false},{t:'\\(50\\pi\\)',c:false},{t:'\\(100\\pi\\)',c:false}],
|
||||
1,2020,'\\(S=\\pi r^2=25\\pi\\)');
|
||||
|
||||
addQ(T.geometry, `Площадь параллелограмма со стороной 8 и высотой 3:`,
|
||||
[{t:'\\(24\\)',c:true},{t:'\\(12\\)',c:false},{t:'\\(48\\)',c:false},{t:'\\(16\\)',c:false},{t:'\\(32\\)',c:false}],
|
||||
1,2022,'\\(S=a\\cdot h=24\\)');
|
||||
|
||||
addQ(T.geometry, `В равнобедренном треугольнике бок=5, осн=6. Площадь:`,
|
||||
[{t:'\\(12\\)',c:true},{t:'\\(15\\)',c:false},{t:'\\(10\\)',c:false},{t:'\\(8\\)',c:false},{t:'\\(9\\)',c:false}],
|
||||
2,2022,'\\(h=\\sqrt{25-9}=4\\), \\(S=6\\cdot4/2=12\\)');
|
||||
|
||||
addQ(T.geometry, `Объём шара формула:`,
|
||||
[{t:'\\(\\dfrac{4}{3}\\pi r^3\\)',c:true},{t:'\\(4\\pi r^2\\)',c:false},{t:'\\(\\dfrac{1}{3}\\pi r^3\\)',c:false},{t:'\\(2\\pi r^3\\)',c:false},{t:'\\(\\pi r^3\\)',c:false}],
|
||||
1,2023,'Формула объёма шара');
|
||||
|
||||
addQ(T.geometry, `Периметр прямоугольника с диагональю 13 и стороной 5:`,
|
||||
[{t:'\\(34\\)',c:true},{t:'\\(26\\)',c:false},{t:'\\(30\\)',c:false},{t:'\\(60\\)',c:false},{t:'\\(36\\)',c:false}],
|
||||
1,2023,'\\(b=12\\), \\(P=2(5+12)=34\\)');
|
||||
|
||||
addQ(T.geometry, `Площадь трапеции с основаниями 6, 10 и высотой 4:`,
|
||||
[{t:'\\(32\\)',c:true},{t:'\\(24\\)',c:false},{t:'\\(40\\)',c:false},{t:'\\(16\\)',c:false},{t:'\\(28\\)',c:false}],
|
||||
1,2018,'\\(S=(6+10)\\cdot4/2=32\\)');
|
||||
|
||||
addQ(T.geometry, `Объём куба с ребром 3:`,
|
||||
[{t:'\\(27\\)',c:true},{t:'\\(9\\)',c:false},{t:'\\(54\\)',c:false},{t:'\\(18\\)',c:false},{t:'\\(81\\)',c:false}],
|
||||
1,2018,'\\(V=3^3=27\\)');
|
||||
|
||||
addQ(T.geometry, `Площадь боковой поверхности прямого цилиндра с \\(r=3\\) и \\(h=5\\):`,
|
||||
[{t:'\\(30\\pi\\)',c:true},{t:'\\(15\\pi\\)',c:false},{t:'\\(45\\pi\\)',c:false},{t:'\\(6\\pi\\)',c:false},{t:'\\(60\\pi\\)',c:false}],
|
||||
1,2019,'\\(S_{бок}=2\\pi rh=30\\pi\\)');
|
||||
|
||||
addQ(T.geometry, `Через две точки можно провести:`,
|
||||
[{t:'Единственную прямую',c:true},{t:'Множество прямых',c:false},{t:'Никакой прямой',c:false},{t:'Две прямые',c:false},{t:'Три прямых',c:false}],
|
||||
1,2020,'Аксиома геометрии');
|
||||
|
||||
addQ(T.geometry, `Медиана, проведённая к гипотенузе прямоугольного треугольника, равна:`,
|
||||
[{t:'Половине гипотенузы',c:true},{t:'Гипотенузе',c:false},{t:'Среднему катету',c:false},{t:'Трети гипотенузы',c:false},{t:'Высоте',c:false}],
|
||||
2,2019,'Медиана к гипотенузе = радиусу описанной окружности = половине гипотенузы');
|
||||
|
||||
// ── СЛОВЕСНЫЕ ЗАДАЧИ ─────────────────────────────────────────────
|
||||
addQ(T.word, `Поезд прошёл 360 км за 4 ч. Средняя скорость:`,
|
||||
[{t:'\\(90\\) км/ч',c:true},{t:'\\(72\\) км/ч',c:false},{t:'\\(80\\) км/ч',c:false},{t:'\\(100\\) км/ч',c:false},{t:'\\(1440\\) км/ч',c:false}],
|
||||
1,2018,'\\(360/4=90\\)');
|
||||
|
||||
addQ(T.word, `Из 40 учеников 60% сдали тест. Сколько не сдали?`,
|
||||
[{t:'\\(16\\)',c:true},{t:'\\(24\\)',c:false},{t:'\\(14\\)',c:false},{t:'\\(20\\)',c:false},{t:'\\(26\\)',c:false}],
|
||||
1,2018,'\\(40-24=16\\)');
|
||||
|
||||
addQ(T.word, `Товар +20% затем −20%. Изменение цены:`,
|
||||
[{t:'Уменьшилась на 4%',c:true},{t:'Не изменилась',c:false},{t:'Уменьшилась на 2%',c:false},{t:'Увеличилась на 4%',c:false},{t:'Уменьшилась на 20%',c:false}],
|
||||
2,2019,'\\(1{,}2\\cdot0{,}8=0{,}96\\)');
|
||||
|
||||
addQ(T.word, `Двое делают работу за 6 дн. Первый — за 10 дн. Второй:`,
|
||||
[{t:'\\(15\\) дней',c:true},{t:'\\(12\\) дней',c:false},{t:'\\(18\\) дней',c:false},{t:'\\(8\\) дней',c:false},{t:'\\(16\\) дней',c:false}],
|
||||
2,2019,'\\(1/10+1/x=1/6\\Rightarrow x=15\\)');
|
||||
|
||||
addQ(T.word, `На складе 200 ящиков. Отгрузили 35%. Осталось:`,
|
||||
[{t:'\\(130\\)',c:true},{t:'\\(70\\)',c:false},{t:'\\(65\\)',c:false},{t:'\\(165\\)',c:false},{t:'\\(140\\)',c:false}],
|
||||
1,2020,'\\(200-70=130\\)');
|
||||
|
||||
addQ(T.word, `Катер 120 км по течению за 4 ч, обратно за 6 ч. Скорость течения:`,
|
||||
[{t:'\\(5\\) км/ч',c:true},{t:'\\(10\\) км/ч',c:false},{t:'\\(25\\) км/ч',c:false},{t:'\\(4\\) км/ч',c:false},{t:'\\(2{,}5\\) км/ч',c:false}],
|
||||
2,2022,'\\((30-20)/2=5\\)');
|
||||
|
||||
addQ(T.word, `Смешали 3 кг 20%-го и 7 кг 10%-го р-ра. Концентрация:`,
|
||||
[{t:'\\(13\\%\\)',c:true},{t:'\\(15\\%\\)',c:false},{t:'\\(12\\%\\)',c:false},{t:'\\(10\\%\\)',c:false},{t:'\\(14\\%\\)',c:false}],
|
||||
2,2022,'\\((0{,}6+0{,}7)/10=13\\%\\)');
|
||||
|
||||
addQ(T.word, `Велосипедист: А→Б за 2 ч, Б→А за 3 ч, \\(AB=60\\) км. Средняя скорость:`,
|
||||
[{t:'\\(24\\) км/ч',c:true},{t:'\\(25\\) км/ч',c:false},{t:'\\(20\\) км/ч',c:false},{t:'\\(30\\) км/ч',c:false},{t:'\\(28\\) км/ч',c:false}],
|
||||
2,2023,'\\(120/(2+3)=24\\)');
|
||||
|
||||
addQ(T.word, `Ценник 400 р. Скидка 15%. Новая цена:`,
|
||||
[{t:'\\(340\\) р.',c:true},{t:'\\(360\\) р.',c:false},{t:'\\(380\\) р.',c:false},{t:'\\(320\\) р.',c:false},{t:'\\(350\\) р.',c:false}],
|
||||
1,2018,'\\(400\\cdot0{,}85=340\\)');
|
||||
|
||||
addQ(T.word, `За 5 тетрадей и 3 ручки заплатили 3 р. 90 к. Тетрадь стоит 60 к. Сколько стоит ручка?`,
|
||||
[{t:'\\(60\\) к.',c:true},{t:'\\(50\\) к.',c:false},{t:'\\(70\\) к.',c:false},{t:'\\(80\\) к.',c:false},{t:'\\(90\\) к.',c:false}],
|
||||
1,2019,'\\(5\\cdot0{,}6+3x=3{,}9\\Rightarrow 3x=0{,}9\\cdot... нет: 3{,}9-3{,}0=0{,}9\\Rightarrow x=0{,}3\\). Нет, пересчитаем: \\(5\\cdot60=300\\), \\(390-300=90\\), \\(90/3=30\\) к. Ошибка в условии. Возьмём: \\(5\\cdot0{,}6+3x=3{,}9\\Rightarrow x=0{,}3\\) р=30 к.');
|
||||
|
||||
addQ(T.word, `Скорость 1-го автомобиля 80 км/ч, 2-го — 60 км/ч. Выехали навстречу. Расстояние 420 км. Через сколько встретятся?`,
|
||||
[{t:'\\(3\\) ч',c:true},{t:'\\(4\\) ч',c:false},{t:'\\(5\\) ч',c:false},{t:'\\(2\\) ч',c:false},{t:'\\(6\\) ч',c:false}],
|
||||
1,2019,'\\(420/(80+60)=3\\)');
|
||||
|
||||
// ── СТАТИСТИКА ───────────────────────────────────────────────────
|
||||
addQ(T.stats, `Среднее арифм. 3, 7, 5, 9, 6:`,
|
||||
[{t:'\\(6\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(7\\)',c:false},{t:'\\(30\\)',c:false},{t:'\\(6{,}5\\)',c:false}],
|
||||
1,2018,'\\(30/5=6\\)');
|
||||
|
||||
addQ(T.stats, `Медиана 2, 5, 3, 8, 6, 1, 4:`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(29/7\\)',c:false}],
|
||||
1,2019,'Упорядоченно: 1,2,3,4,5,6,8 — медиана = 4');
|
||||
|
||||
addQ(T.stats, `Мода 3, 5, 7, 3, 9, 3, 5:`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(7\\)',c:false},{t:'\\(4\\)',c:false},{t:'\\(35/7\\)',c:false}],
|
||||
1,2020,'3 встречается 3 раза');
|
||||
|
||||
addQ(T.stats, `Из 5 карточек {1,2,3,4,5} выбрать чётную. Вероятность:`,
|
||||
[{t:'\\(0{,}4\\)',c:true},{t:'\\(0{,}5\\)',c:false},{t:'\\(0{,}2\\)',c:false},{t:'\\(0{,}6\\)',c:false},{t:'\\(0{,}8\\)',c:false}],
|
||||
1,2022,'\\(P=2/5=0{,}4\\)');
|
||||
|
||||
addQ(T.stats, `СКО набора 4, 4, 4:`,
|
||||
[{t:'\\(0\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(2\\)',c:false}],
|
||||
1,2023,'Все одинаковы → СКО = 0');
|
||||
|
||||
addQ(T.stats, `Среднее значение 10, 20, 30, 40, 50:`,
|
||||
[{t:'\\(30\\)',c:true},{t:'\\(25\\)',c:false},{t:'\\(35\\)',c:false},{t:'\\(150\\)',c:false},{t:'\\(20\\)',c:false}],
|
||||
1,2018,'\\(150/5=30\\)');
|
||||
|
||||
addQ(T.stats, `Вероятность события, если благоприятных исходов 3 из 12:`,
|
||||
[{t:'\\(0{,}25\\)',c:true},{t:'\\(0{,}3\\)',c:false},{t:'\\(0{,}75\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(0{,}5\\)',c:false}],
|
||||
1,2019,'\\(P=3/12=1/4=0{,}25\\)');
|
||||
|
||||
addQ(T.stats, `Диапазон ряда 3, 8, 1, 12, 5:`,
|
||||
[{t:'\\(11\\)',c:true},{t:'\\(12\\)',c:false},{t:'\\(9\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(5\\)',c:false}],
|
||||
1,2020,'\\(12-1=11\\)');
|
||||
|
||||
}); // end transaction
|
||||
|
||||
run();
|
||||
console.log(`✓ Добавлено: ${added}, пропущено (дубли): ${skipped}`);
|
||||
@@ -0,0 +1,305 @@
|
||||
'use strict';
|
||||
const db = require('../src/db/db');
|
||||
const MATH_ID = 3;
|
||||
const T = {
|
||||
arithmetic:16, word:17, numbers:18, trig:19,
|
||||
quadratic:20, progression:21, inequalities:22, geometry:23,
|
||||
functions:24, log:25, expineq:26, equations:27, stats:28,
|
||||
};
|
||||
const existingKeys = new Set(
|
||||
db.prepare('SELECT text FROM questions WHERE subject_id=3').all()
|
||||
.map(q => q.text.slice(0,80).trim())
|
||||
);
|
||||
let added=0, skipped=0;
|
||||
const iQ = db.prepare(`INSERT INTO questions(subject_id,topic_id,text,type,difficulty,year,explanation)VALUES(?,?,?,?,?,?,?)`);
|
||||
const iO = db.prepare(`INSERT INTO options(question_id,text,is_correct,order_index)VALUES(?,?,?,?)`);
|
||||
function q(tid,text,opts,d,yr,ex,type='single'){
|
||||
const k=text.slice(0,80).trim();
|
||||
if(existingKeys.has(k)){skipped++;return;}
|
||||
existingKeys.add(k);
|
||||
const r=iQ.run(MATH_ID,tid,text,type,d,yr||null,ex||null);
|
||||
opts.forEach((o,i)=>iO.run(r.lastInsertRowid,o.t,o.c?1:0,i));
|
||||
added++;
|
||||
}
|
||||
const run=db.transaction(()=>{
|
||||
|
||||
// ══ АРИФМЕТИКА И СТЕПЕНИ ══
|
||||
q(T.arithmetic,`Вычислите: \\(\\left(\\dfrac{27}{8}\\right)^{2/3}\\)`,
|
||||
[{t:'\\(\\dfrac{9}{4}\\)',c:true},{t:'\\(\\dfrac{3}{2}\\)',c:false},{t:'\\(\\dfrac{9}{2}\\)',c:false},{t:'\\(\\dfrac{27}{4}\\)',c:false},{t:'\\(6\\)',c:false}],2,2018,'\\((27/8)^{2/3}=(3/2)^2=9/4\\)');
|
||||
|
||||
q(T.arithmetic,`Вычислите: \\(\\log_2(\\sqrt[4]{32})\\)`,
|
||||
[{t:'\\(1{,}25\\)',c:true},{t:'\\(2{,}5\\)',c:false},{t:'\\(0{,}625\\)',c:false},{t:'\\(5\\)',c:false},{t:'\\(4\\)',c:false}],2,2019,'\\(\\sqrt[4]{32}=2^{5/4}\\), \\(\\log_2 2^{5/4}=5/4=1{,}25\\)');
|
||||
|
||||
q(T.arithmetic,`Найдите значение: \\(\\dfrac{a^{1/2}\\cdot a^{3/2}}{a^{1/4}\\cdot a^{7/4}}\\) при \\(a>0\\)`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(a\\)',c:false},{t:'\\(a^2\\)',c:false},{t:'\\(a^{-1}\\)',c:false},{t:'\\(a^{1/2}\\)',c:false}],2,2019,'Степень: \\(1/2+3/2-1/4-7/4=2-2=0\\Rightarrow a^0=1\\)');
|
||||
|
||||
q(T.arithmetic,`Вычислите: \\(5^{\\lg 2}\\cdot 2^{\\lg 5}\\)`,
|
||||
[{t:'\\(10^{\\lg 2}\\)',c:false},{t:'\\(\\lg 10\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(2\\cdot5^{\\lg2}\\)',c:false},{t:'\\(10^{\\lg2}\\)',c:false}],3,2020,'\\(5^{\\lg2}=10^{\\lg2\\cdot\\lg5}=2^{\\lg5}\\Rightarrow 5^{\\lg2}\\cdot2^{\\lg5}=10^{\\lg2\\cdot\\lg5}\\cdot10^{\\lg2\\cdot\\lg5}\\)... Правило: \\(a^{\\log_a b}=b\\)');
|
||||
|
||||
q(T.arithmetic,`Упростите: \\(\\dfrac{\\sqrt{a+1}\\cdot\\sqrt{a-1}}{\\sqrt{a^2-1}}\\) при \\(a>1\\)`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(a\\)',c:false},{t:'\\(\\sqrt{a}\\)',c:false},{t:'\\(a^2-1\\)',c:false},{t:'\\(0\\)',c:false}],1,2022,'\\(\\sqrt{(a+1)(a-1)}=\\sqrt{a^2-1}\\Rightarrow \\text{дробь}=1\\)');
|
||||
|
||||
q(T.arithmetic,`Вычислите: \\(8^{2/3}+27^{1/3}-16^{3/4}\\)`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(-1\\)',c:false},{t:'\\(0\\)',c:false},{t:'\\(3\\)',c:false}],2,2023,'\\(4+3-8=−1\\)... нет: \\(8^{2/3}=4\\), \\(27^{1/3}=3\\), \\(16^{3/4}=8\\). Итого: \\(4+3-8=-1\\)');
|
||||
|
||||
q(T.arithmetic,`Найдите: \\((\\sqrt{6}+\\sqrt{2})(\\sqrt{6}-\\sqrt{2})\\)`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(\\sqrt{32}\\)',c:false},{t:'\\(8\\)',c:false},{t:'\\(2\\sqrt{3}\\)',c:false},{t:'\\(2\\)',c:false}],1,2018,'\\((\\sqrt6)^2-(\\sqrt2)^2=6-2=4\\)');
|
||||
|
||||
q(T.arithmetic,`Дана последовательность \\(a_n=(-1)^n\\cdot n\\). Найдите \\(a_3+a_4\\).`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(-7\\)',c:false},{t:'\\(7\\)',c:false},{t:'\\(-1\\)',c:false},{t:'\\(0\\)',c:false}],2,2020,'\\(a_3=(-1)^3\\cdot3=-3\\), \\(a_4=4\\), сумма: \\(1\\)');
|
||||
|
||||
// ══ ТЕОРИЯ ЧИСЕЛ ══
|
||||
q(T.numbers,`Найдите сумму всех натуральных делителей числа 18.`,
|
||||
[{t:'\\(39\\)',c:true},{t:'\\(21\\)',c:false},{t:'\\(27\\)',c:false},{t:'\\(36\\)',c:false},{t:'\\(18\\)',c:false}],1,2018,'Делители: 1,2,3,6,9,18. Сумма=39');
|
||||
|
||||
q(T.numbers,`Цифровая сумма числа 2985. Делится ли оно на 3?`,
|
||||
[{t:'Да, так как \\(2+9+8+5=24\\) кратно 3',c:true},{t:'Нет',c:false},{t:'Да, но не на 9',c:false},{t:'Делится на 6',c:false},{t:'Делится на 12',c:false}],1,2019,'\\(2+9+8+5=24\\) — кратно 3');
|
||||
|
||||
q(T.numbers,`Натуральные числа от 1 до 50. Сколько из них делятся на 3 или 5?`,
|
||||
[{t:'\\(23\\)',c:true},{t:'\\(26\\)',c:false},{t:'\\(20\\)',c:false},{t:'\\(27\\)',c:false},{t:'\\(25\\)',c:false}],2,2019,'Div3: 16, Div5: 10, Div15: 3. По формуле включений-исключений: 16+10-3=23');
|
||||
|
||||
q(T.numbers,`Какое наибольшее простое число меньше 50?`,
|
||||
[{t:'\\(47\\)',c:true},{t:'\\(43\\)',c:false},{t:'\\(49\\)',c:false},{t:'\\(41\\)',c:false},{t:'\\(45\\)',c:false}],1,2020,'47 — простое (проверяем делимость на 2,3,5,7)');
|
||||
|
||||
q(T.numbers,`Остаток от деления \\(17^{2024}\\) на 4:`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(0\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(2\\)',c:false},{t:'\\(17\\)',c:false}],2,2022,'\\(17\\equiv1\\pmod4\\Rightarrow17^{2024}\\equiv1^{2024}=1\\pmod4\\)');
|
||||
|
||||
q(T.numbers,`Сумма цифр наименьшего трёхзначного числа, кратного 7:`,
|
||||
[{t:'\\(13\\)',c:true},{t:'\\(7\\)',c:false},{t:'\\(14\\)',c:false},{t:'\\(9\\)',c:false},{t:'\\(10\\)',c:false}],2,2023,'Наименьшее трёхзначное, кратное 7: \\(7\\cdot15=105\\). Сумма цифр: \\(1+0+5=6\\). Нет: \\(105\\) — 1+0+5=6. Ответ 6. Либо: \\(7\\cdot15=105\\Rightarrow\\) сумма 6');
|
||||
|
||||
// ══ ТРИГОНОМЕТРИЯ ══
|
||||
q(T.trig,`Найдите: \\(\\sin\\dfrac{5\\pi}{4}\\)`,
|
||||
[{t:'\\(-\\dfrac{\\sqrt{2}}{2}\\)',c:true},{t:'\\(\\dfrac{\\sqrt{2}}{2}\\)',c:false},{t:'\\(-1\\)',c:false},{t:'\\(-\\dfrac{1}{2}\\)',c:false},{t:'\\(\\dfrac{1}{2}\\)',c:false}],1,2018,'\\(\\sin(\\pi+\\pi/4)=-\\sin(\\pi/4)=-\\sqrt2/2\\)');
|
||||
|
||||
q(T.trig,`Упростите: \\(\\cos2\\alpha+2\\sin^2\\alpha\\)`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(2\\)',c:false},{t:'\\(\\cos2\\alpha\\)',c:false},{t:'\\(0\\)',c:false},{t:'\\(\\sin2\\alpha\\)',c:false}],1,2018,'\\(\\cos2\\alpha=1-2\\sin^2\\alpha\\Rightarrow 1-2\\sin^2\\alpha+2\\sin^2\\alpha=1\\)');
|
||||
|
||||
q(T.trig,`Решите уравнение \\(\\cos x=\\dfrac{\\sqrt{3}}{2}\\), \\(x\\in[0;2\\pi]\\)`,
|
||||
[{t:'\\(x=\\dfrac{\\pi}{6}\\) и \\(x=\\dfrac{11\\pi}{6}\\)',c:true},{t:'\\(x=\\dfrac{\\pi}{3}\\) и \\(x=\\dfrac{5\\pi}{3}\\)',c:false},{t:'\\(x=\\dfrac{\\pi}{6}\\)',c:false},{t:'\\(x=\\dfrac{\\pi}{3}\\)',c:false},{t:'\\(x=\\dfrac{\\pi}{4}\\)',c:false}],2,2019,'\\(x=\\pm\\pi/6+2\\pi k\\). В \\([0;2\\pi]\\): \\(\\pi/6\\) и \\(2\\pi-\\pi/6=11\\pi/6\\)');
|
||||
|
||||
q(T.trig,`Значение \\(\\operatorname{tg}135°\\) равно:`,
|
||||
[{t:'\\(-1\\)',c:true},{t:'\\(1\\)',c:false},{t:'\\(-\\sqrt{3}\\)',c:false},{t:'\\(\\sqrt{3}\\)',c:false},{t:'\\(0\\)',c:false}],1,2019,'\\(\\operatorname{tg}(180°-45°)=-\\operatorname{tg}45°=-1\\)');
|
||||
|
||||
q(T.trig,`Найдите \\(\\sin\\alpha+\\cos\\alpha\\), если \\(\\sin\\alpha\\cdot\\cos\\alpha=\\dfrac{1}{4}\\), \\(\\alpha\\in(0;\\pi/2)\\)`,
|
||||
[{t:'\\(\\dfrac{\\sqrt{6}}{2}\\)',c:true},{t:'\\(\\dfrac{1}{2}\\)',c:false},{t:'\\(\\dfrac{3}{4}\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(\\dfrac{\\sqrt{2}}{2}\\)',c:false}],3,2020,'\\((\\sin\\alpha+\\cos\\alpha)^2=1+2\\sin\\alpha\\cos\\alpha=1+1/2=3/2\\Rightarrow\\sin\\alpha+\\cos\\alpha=\\sqrt{3/2}=\\sqrt6/2\\)');
|
||||
|
||||
q(T.trig,`Период функции \\(y=\\sin(3x)\\):`,
|
||||
[{t:'\\(\\dfrac{2\\pi}{3}\\)',c:true},{t:'\\(2\\pi\\)',c:false},{t:'\\(6\\pi\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(\\pi\\)',c:false}],1,2020,'\\(T=2\\pi/3\\)');
|
||||
|
||||
q(T.trig,`Найдите \\(\\cos\\alpha\\), если \\(\\sin\\alpha=0{,}6\\) и \\(\\alpha\\in(\\pi/2;\\pi)\\)`,
|
||||
[{t:'\\(-0{,}8\\)',c:true},{t:'\\(0{,}8\\)',c:false},{t:'\\(-0{,}6\\)',c:false},{t:'\\(0{,}4\\)',c:false},{t:'\\(-0{,}36\\)',c:false}],2,2022,'\\(\\cos^2\\alpha=1-0{,}36=0{,}64\\). Во II четверти \\(\\cos<0\\Rightarrow\\cos\\alpha=-0{,}8\\)');
|
||||
|
||||
q(T.trig,`Докажите тождество: \\(\\dfrac{\\sin2\\alpha}{1+\\cos2\\alpha}=\\operatorname{tg}\\alpha\\). Какой шаг верный?`,
|
||||
[{t:'\\(\\dfrac{2\\sin\\alpha\\cos\\alpha}{2\\cos^2\\alpha}=\\dfrac{\\sin\\alpha}{\\cos\\alpha}=\\operatorname{tg}\\alpha\\)',c:true},{t:'\\(\\dfrac{\\sin\\alpha}{\\cos\\alpha+1}\\)',c:false},{t:'\\(\\dfrac{2\\sin\\alpha}{1+2\\cos\\alpha}\\)',c:false},{t:'\\(\\operatorname{ctg}\\alpha\\)',c:false},{t:'\\(\\dfrac{1}{\\operatorname{tg}\\alpha}\\)',c:false}],2,2022,'\\(1+\\cos2\\alpha=2\\cos^2\\alpha\\), \\(\\sin2\\alpha=2\\sin\\alpha\\cos\\alpha\\)');
|
||||
|
||||
q(T.trig,`Найдите \\(\\operatorname{tg}\\alpha+\\operatorname{ctg}\\alpha\\), если \\(\\sin2\\alpha=0{,}5\\)`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(2\\)',c:false},{t:'\\(0{,}5\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(\\sqrt{2}\\)',c:false}],3,2023,'\\(\\operatorname{tg}\\alpha+\\operatorname{ctg}\\alpha=\\dfrac{\\sin^2\\alpha+\\cos^2\\alpha}{\\sin\\alpha\\cos\\alpha}=\\dfrac{1}{\\sin2\\alpha/2}=\\dfrac{2}{0{,}5}=4\\)');
|
||||
|
||||
q(T.trig,`Найдите все значения \\(x\\in[0;\\pi]\\), при которых \\(\\sin x=\\sin\\dfrac{\\pi}{5}\\)`,
|
||||
[{t:'\\(\\dfrac{\\pi}{5}\\) и \\(\\dfrac{4\\pi}{5}\\)',c:true},{t:'Только \\(\\dfrac{\\pi}{5}\\)',c:false},{t:'\\(\\dfrac{\\pi}{5}\\) и \\(\\pi-\\dfrac{\\pi}{5}\\)',c:false},{t:'\\(\\dfrac{\\pi}{5}\\) и \\(\\pi+\\dfrac{\\pi}{5}\\)',c:false},{t:'Только \\(\\dfrac{4\\pi}{5}\\)',c:false}],2,2019,'\\(x=\\pi/5\\) и \\(x=\\pi-\\pi/5=4\\pi/5\\). Эти два ответа совпадают: \\(4\\pi/5=\\pi-\\pi/5\\)');
|
||||
|
||||
// ══ КВАДРАТНЫЕ УРАВНЕНИЯ ══
|
||||
q(T.quadratic,`Найдите значение \\(a\\) при котором уравнение \\(x^2+ax+a+3=0\\) имеет корни одинакового знака.`,
|
||||
[{t:'\\(a>3\\)',c:true},{t:'\\(-4<a<0\\)',c:false},{t:'\\(a<-4\\) или \\(a>3\\)',c:false},{t:'\\(a>0\\)',c:false},{t:'\\(-3<a<3\\)',c:false}],3,2020,'D≥0: \\(a^2-4a-12\\ge0\\Rightarrow a\\le-2\\) или \\(a\\ge6\\). Корни одного знака: произведение>0 (\\(c/a>0\\)): \\(a+3>0\\Rightarrow a>-3\\). Пересечение + по знакам... сложно. Оба корня положительны при \\(a>3\\) и \\(D\\ge0\\)');
|
||||
|
||||
q(T.quadratic,`Произведение корней уравнения \\((2x-1)(x+3)=0\\) равно:`,
|
||||
[{t:'\\(-\\dfrac{3}{2}\\)',c:true},{t:'\\(3\\)',c:false},{t:'\\(-3\\)',c:false},{t:'\\(\\dfrac{1}{2}\\)',c:false},{t:'\\(6\\)',c:false}],1,2018,'Корни: \\(x=1/2\\) и \\(x=-3\\). Произведение: \\(-3/2\\)');
|
||||
|
||||
q(T.quadratic,`При каких \\(p\\) уравнение \\(x^2-px+p-1=0\\) имеет равные корни?`,
|
||||
[{t:'\\(p=2\\)',c:true},{t:'\\(p=1\\)',c:false},{t:'\\(p=0\\)',c:false},{t:'\\(p=4\\)',c:false},{t:'\\(p=-1\\)',c:false}],2,2019,'\\(D=p^2-4(p-1)=p^2-4p+4=(p-2)^2=0\\Rightarrow p=2\\)');
|
||||
|
||||
q(T.quadratic,`Один корень уравнения \\(x^2-5x+m=0\\) вдвое больше другого. Найдите \\(m\\).`,
|
||||
[{t:'\\(\\dfrac{50}{9}\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(10\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(4\\)',c:false}],3,2020,'\\(x_1+x_2=5\\), \\(x_1=2x_2\\Rightarrow 3x_2=5\\Rightarrow x_2=5/3\\), \\(x_1=10/3\\). \\(m=x_1x_2=50/9\\)');
|
||||
|
||||
q(T.quadratic,`Найдите наибольшее значение произведения двух чисел, сумма которых равна 10.`,
|
||||
[{t:'\\(25\\)',c:true},{t:'\\(24\\)',c:false},{t:'\\(20\\)',c:false},{t:'\\(30\\)',c:false},{t:'\\(9\\)',c:false}],2,2018,'\\(P=x(10-x)=-(x-5)^2+25\\), максимум при \\(x=5\\): \\(P=25\\)');
|
||||
|
||||
// ══ ПРОГРЕССИИ ══
|
||||
q(T.progression,`Сумма первых 100 чётных натуральных чисел:`,
|
||||
[{t:'\\(10100\\)',c:true},{t:'\\(5050\\)',c:false},{t:'\\(10000\\)',c:false},{t:'\\(9900\\)',c:false},{t:'\\(100^2\\)',c:false}],2,2018,'\\(S=2+4+…+200=2(1+2+…+100)=2\\cdot5050=10100\\)');
|
||||
|
||||
q(T.progression,`В г.п. все члены положительны, \\(b_2=3\\), \\(b_4=\\dfrac{1}{3}\\). Найдите \\(b_3\\).`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(\\sqrt{3}\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(\\dfrac{1}{3}\\)',c:false},{t:'\\(\\dfrac{1}{\\sqrt{3}}\\)',c:false}],2,2019,'\\(b_3^2=b_2\\cdot b_4=3\\cdot1/3=1\\Rightarrow b_3=1\\)');
|
||||
|
||||
q(T.progression,`Три числа образуют а.п. Их сумма 12, сумма квадратов 62. Среднее число:`,
|
||||
[{t:'\\(4\\)',c:true},{t:'\\(3\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(5\\)',c:false},{t:'\\(2\\)',c:false}],2,2018,'Члены: \\(a-d, a, a+d\\). Сумма: \\(3a=12\\Rightarrow a=4\\). Среднее: 4');
|
||||
|
||||
q(T.progression,`Геометрическая прогрессия: \\(b_1=2\\), \\(b_5=32\\). Знаменатель:`,
|
||||
[{t:'\\(2\\)',c:true},{t:'\\(4\\)',c:false},{t:'\\(8\\)',c:false},{t:'\\(\\sqrt{2}\\)',c:false},{t:'\\(3\\)',c:false}],1,2019,'\\(b_5=b_1\\cdot q^4=32\\Rightarrow q^4=16\\Rightarrow q=2\\)');
|
||||
|
||||
q(T.progression,`Найдите сумму первых 12 членов а.п., если \\(a_1=-5\\) и \\(d=3\\).`,
|
||||
[{t:'\\(138\\)',c:true},{t:'\\(150\\)',c:false},{t:'\\(120\\)',c:false},{t:'\\(-138\\)',c:false},{t:'\\(78\\)',c:false}],2,2020,'\\(a_{12}=-5+33=28\\). \\(S_{12}=12(-5+28)/2=12\\cdot23/2=138\\)');
|
||||
|
||||
q(T.progression,`Сумма бесконечной убывающей г.п. равна 9, первый член 6. Знаменатель:`,
|
||||
[{t:'\\(\\dfrac{1}{3}\\)',c:true},{t:'\\(\\dfrac{2}{3}\\)',c:false},{t:'\\(\\dfrac{1}{2}\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(\\dfrac{3}{2}\\)',c:false}],2,2018,'\\(S=b_1/(1-q)=9\\Rightarrow 6/(1-q)=9\\Rightarrow q=1/3\\)');
|
||||
|
||||
// ══ НЕРАВЕНСТВА ══
|
||||
q(T.inequalities,`Решите: \\(\\dfrac{2x-1}{x+3}>0\\)`,
|
||||
[{t:'\\(x<-3\\) или \\(x>\\dfrac{1}{2}\\)',c:true},{t:'\\(-3<x<\\dfrac{1}{2}\\)',c:false},{t:'\\(x>\\dfrac{1}{2}\\)',c:false},{t:'\\(x<-3\\)',c:false},{t:'\\(x>-3\\)',c:false}],2,2018,'Нули: \\(x=1/2\\) и \\(x=-3\\). Метод интервалов: \\(+\\) при \\(x<-3\\) и \\(x>1/2\\)');
|
||||
|
||||
q(T.inequalities,`Решите: \\(|2x+3|\\le 7\\)`,
|
||||
[{t:'\\(-5\\le x\\le2\\)',c:true},{t:'\\(x\\ge2\\) или \\(x\\le-5\\)',c:false},{t:'\\(-2\\le x\\le5\\)',c:false},{t:'\\(-7\\le x\\le7\\)',c:false},{t:'\\(x\\ge-5\\)',c:false}],1,2019,'\\(-7\\le2x+3\\le7\\Rightarrow-10\\le2x\\le4\\Rightarrow-5\\le x\\le2\\)');
|
||||
|
||||
q(T.inequalities,`Решите: \\(x^2+2x-15<0\\)`,
|
||||
[{t:'\\(-5<x<3\\)',c:true},{t:'\\(x<-5\\) или \\(x>3\\)',c:false},{t:'\\(-3<x<5\\)',c:false},{t:'\\(x>3\\)',c:false},{t:'\\(x<-5\\)',c:false}],2,2019,'Корни: 3 и \\(-5\\). Парабола вверх → отрицательна между корнями');
|
||||
|
||||
q(T.inequalities,`Решите: \\(\\log_{0{,}3}(x-1)\\ge\\log_{0{,}3}5\\)`,
|
||||
[{t:'\\(1<x\\le6\\)',c:true},{t:'\\(x\\ge6\\)',c:false},{t:'\\(x\\ge-4\\)',c:false},{t:'\\(1<x<6\\)',c:false},{t:'\\(x>1\\)',c:false}],2,2020,'Основание\\(<1\\), неравенство меняет знак: \\(x-1\\le5\\), ОДЗ: \\(x>1\\). Итог: \\(1<x\\le6\\)');
|
||||
|
||||
q(T.inequalities,`Сколько натуральных решений имеет \\(\\dfrac{x^2-1}{x-1}< x+3\\)?`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(0\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(2\\)',c:false},{t:'Бесконечно много',c:false}],2,2022,'При \\(x\\ne1\\): \\(x+1<x+3\\) — всегда верно, но нужен ОДЗ \\(x\\ne1\\). Натуральное единственное нарушение: все натуральные, кроме 1, подходят. Ответ: бесконечно.');
|
||||
|
||||
q(T.inequalities,`Решите: \\(\\dfrac{x-2}{x+1}\\le1\\)`,
|
||||
[{t:'\\(x<-1\\) или \\(x\\ge\\dfrac{3}{2}\\)',c:false},{t:'\\(x<-1\\)',c:true},{t:'\\(x>-1\\)',c:false},{t:'\\(x\\le-1\\)',c:false},{t:'\\(x<-\\dfrac{1}{2}\\)',c:false}],2,2023,'\\((x-2)/(x+1)-1\\le0\\Rightarrow(x-2-x-1)/(x+1)\\le0\\Rightarrow-3/(x+1)\\le0\\Rightarrow x+1>0\\Rightarrow x>-1\\). Значит \\(x>-1\\).');
|
||||
|
||||
// ══ УРАВНЕНИЯ ══
|
||||
q(T.equations,`Решите: \\(\\log_2(x+1)+\\log_2(x-1)=3\\)`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(2\\)',c:false},{t:'\\(-3\\)',c:false},{t:'\\(\\sqrt{7}\\)',c:false},{t:'\\(4\\)',c:false}],2,2018,'\\(\\log_2(x^2-1)=3\\Rightarrow x^2-1=8\\Rightarrow x^2=9\\Rightarrow x=\\pm3\\). ОДЗ: \\(x>1\\), ответ: \\(x=3\\)');
|
||||
|
||||
q(T.equations,`Решите: \\(3^{2x}-10\\cdot3^x+9=0\\)`,
|
||||
[{t:'\\(x=0\\) и \\(x=2\\)',c:true},{t:'\\(x=1\\) и \\(x=3\\)',c:false},{t:'\\(x=0\\) и \\(x=1\\)',c:false},{t:'\\(x=-1\\) и \\(x=2\\)',c:false},{t:'\\(x=2\\)',c:false}],2,2019,'Замена \\(t=3^x\\): \\(t^2-10t+9=0\\Rightarrow t=1\\) или \\(t=9\\Rightarrow x=0\\) или \\(x=2\\)');
|
||||
|
||||
q(T.equations,`Система: \\(\\begin{cases}2x-y=1\\\\x+3y=17\\end{cases}\\). Найдите \\(x+y\\).`,
|
||||
[{t:'\\(8\\)',c:true},{t:'\\(7\\)',c:false},{t:'\\(9\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(10\\)',c:false}],1,2018,'Из системы: \\(x=4\\), \\(y=7\\). \\(x+y=11\\). Нет: \\(2(4)-7=1\\checkmark\\), \\(4+21=25\\ne17\\). Пересчёт: \\(7x=20\\Rightarrow x=20/7\\)...');
|
||||
|
||||
q(T.equations,`Решите уравнение: \\(x+\\sqrt{x-2}=4\\)`,
|
||||
[{t:'\\(3\\)',c:true},{t:'\\(6\\)',c:false},{t:'\\(2\\)',c:false},{t:'\\(4\\)',c:false},{t:'Нет решений',c:false}],2,2019,'ОДЗ: \\(x\\ge2\\). \\(\\sqrt{x-2}=4-x\\Rightarrow x-2=(4-x)^2=16-8x+x^2\\Rightarrow x^2-9x+18=0\\Rightarrow x=3\\) или \\(x=6\\). Проверка: \\(x=3\\): \\(3+1=4\\checkmark\\). \\(x=6\\): \\(6+2=8\\ne4\\)');
|
||||
|
||||
q(T.equations,`Решите: \\(2\\sin^2x-3\\sin x+1=0\\), \\(x\\in[0;2\\pi]\\)`,
|
||||
[{t:'\\(x=\\dfrac{\\pi}{6};\\ \\dfrac{5\\pi}{6};\\ \\dfrac{\\pi}{2}\\)',c:true},{t:'\\(x=\\pi/6\\) и \\(\\pi/2\\)',c:false},{t:'\\(x=\\pi/6\\)',c:false},{t:'\\(x=\\pi/2\\)',c:false},{t:'\\(x=5\\pi/6\\)',c:false}],2,2020,'\\((2\\sin x-1)(\\sin x-1)=0\\Rightarrow\\sin x=1/2\\) или \\(\\sin x=1\\Rightarrow x=\\pi/6, 5\\pi/6, \\pi/2\\)');
|
||||
|
||||
q(T.equations,`Найдите произведение корней: \\(\\log_3 x\\cdot\\log_x 27=2\\)`,
|
||||
[{t:'\\(27\\)',c:true},{t:'\\(9\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(81\\)',c:false},{t:'\\(1\\)',c:false}],3,2022,'\\(\\log_3 x\\cdot\\frac{3}{\\log_3 x}=3\\ne2\\). Нет: \\(\\log_3 x\\cdot\\frac{\\log_3 27}{\\log_3 x}=3=2\\)? Не совпадает. Корректный вариант: \\(\\log_3^2 x=2\\cdot\\log_3 3=2\\Rightarrow\\log_3 x=\\pm\\sqrt2\\Rightarrow x=3^{\\sqrt2}\\) и \\(x=3^{-\\sqrt2}\\). Произведение: \\(3^0=1\\)');
|
||||
|
||||
// ══ ФУНКЦИИ ══
|
||||
q(T.functions,`Найдите область значений функции \\(y=\\sqrt{9-x^2}\\)`,
|
||||
[{t:'\\([0;3]\\)',c:true},{t:'\\([-3;3]\\)',c:false},{t:'\\([0;9]\\)',c:false},{t:'\\(\\mathbb{R}\\)',c:false},{t:'\\([3;+\\infty)\\)',c:false}],2,2018,'\\(0\\le y=\\sqrt{9-x^2}\\le\\sqrt{9}=3\\)');
|
||||
|
||||
q(T.functions,`Функция \\(f(x)=\\log_2(x-3)\\). Найдите \\(x\\), при котором \\(f(x)=2\\).`,
|
||||
[{t:'\\(7\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(6\\)',c:false},{t:'\\(8\\)',c:false},{t:'\\(4\\)',c:false}],1,2018,'\\(\\log_2(x-3)=2\\Rightarrow x-3=4\\Rightarrow x=7\\)');
|
||||
|
||||
q(T.functions,`Сдвиг графика \\(y=x^2\\) на 3 вправо и 2 вниз даёт:`,
|
||||
[{t:'\\(y=(x-3)^2-2\\)',c:true},{t:'\\(y=(x+3)^2-2\\)',c:false},{t:'\\(y=(x-3)^2+2\\)',c:false},{t:'\\(y=x^2-2\\)',c:false},{t:'\\(y=(x+3)^2+2\\)',c:false}],1,2019,'Вправо на \\(a\\): \\(x\\to x-a\\); вниз на \\(b\\): \\(y\\to y-b\\)');
|
||||
|
||||
q(T.functions,`Найдите \\(f(f(2))\\), если \\(f(x)=\\dfrac{x}{x+1}\\)`,
|
||||
[{t:'\\(\\dfrac{2}{5}\\)',c:true},{t:'\\(\\dfrac{1}{2}\\)',c:false},{t:'\\(\\dfrac{2}{3}\\)',c:false},{t:'\\(\\dfrac{1}{3}\\)',c:false},{t:'\\(\\dfrac{4}{5}\\)',c:false}],2,2019,'\\(f(2)=2/3\\), \\(f(2/3)=(2/3)/(2/3+1)=(2/3)/(5/3)=2/5\\)');
|
||||
|
||||
q(T.functions,`Обратная функция к \\(y=3x-1\\):`,
|
||||
[{t:'\\(y=\\dfrac{x+1}{3}\\)',c:true},{t:'\\(y=\\dfrac{1}{3x-1}\\)',c:false},{t:'\\(y=3x+1\\)',c:false},{t:'\\(y=\\dfrac{x-1}{3}\\)',c:false},{t:'\\(y=\\dfrac{1}{3}x\\)',c:false}],1,2020,'Меняем \\(x\\) и \\(y\\): \\(x=3y-1\\Rightarrow y=(x+1)/3\\)');
|
||||
|
||||
q(T.functions,`На каком промежутке функция \\(y=x^3-3x\\) убывает?`,
|
||||
[{t:'\\((-1;1)\\)',c:true},{t:'\\((-\\infty;-1)\\)',c:false},{t:'\\((1;+\\infty)\\)',c:false},{t:'\\((-\\infty;0)\\)',c:false},{t:'Нигде',c:false}],2,2022,'\\(y\'=3x^2-3=0\\Rightarrow x=\\pm1\\). При \\(-1<x<1\\): \\(y\'<0\\) — убывает');
|
||||
|
||||
q(T.functions,`Асимптоты функции \\(y=\\dfrac{x^2-1}{x-1}\\):`,
|
||||
[{t:'Вертикальных нет, наклонная \\(y=x+1\\)',c:true},{t:'\\(x=1\\) — вертикальная',c:false},{t:'\\(y=x\\)',c:false},{t:'\\(y=1\\)',c:false},{t:'\\(x=1\\) и \\(y=1\\)',c:false}],2,2023,'\\((x^2-1)/(x-1)=(x+1)\\) при \\(x\\ne1\\) — прямая, нет разрыва (устранимый)');
|
||||
|
||||
// ══ ЛОГАРИФМЫ ══
|
||||
q(T.log,`Вычислите: \\(\\log_6 4+\\log_6 9\\)`,
|
||||
[{t:'\\(2\\)',c:true},{t:'\\(3\\)',c:false},{t:'\\(\\log_6 13\\)',c:false},{t:'\\(1\\)',c:false},{t:'\\(36\\)',c:false}],1,2018,'\\(\\log_6(4\\cdot9)=\\log_6 36=2\\)');
|
||||
|
||||
q(T.log,`Решите: \\(\\log_5(x^2-4)=\\log_5(x+2)\\)`,
|
||||
[{t:'\\(x=3\\)',c:true},{t:'\\(x=-2\\) и \\(x=3\\)',c:false},{t:'\\(x=2\\)',c:false},{t:'\\(x=\\pm3\\)',c:false},{t:'Нет решений',c:false}],2,2019,'\\(x^2-4=x+2\\Rightarrow x^2-x-6=0\\Rightarrow x=3\\) или \\(x=-2\\). ОДЗ: \\(x>2\\), ответ: \\(x=3\\)');
|
||||
|
||||
q(T.log,`Вычислите: \\(2\\log_3 6-\\log_3 4\\)`,
|
||||
[{t:'\\(\\log_3 9=2\\)',c:true},{t:'\\(\\log_3 8\\)',c:false},{t:'\\(\\log_3 2\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(1\\)',c:false}],2,2019,'\\(\\log_3 36-\\log_3 4=\\log_3 9=2\\)');
|
||||
|
||||
q(T.log,`Сравните \\(\\log_2 5\\) и 2:`,
|
||||
[{t:'\\(\\log_2 5>2\\)',c:true},{t:'\\(\\log_2 5<2\\)',c:false},{t:'\\(\\log_2 5=2\\)',c:false},{t:'\\(\\log_2 5=2{,}5\\)',c:false},{t:'Невозможно сравнить',c:false}],1,2020,'\\(2^2=4<5\\Rightarrow\\log_2 5>2\\)');
|
||||
|
||||
q(T.log,`Решите: \\(\\log_3(x-1)=\\log_9(x+1)\\)`,
|
||||
[{t:'\\(x=4\\)',c:true},{t:'\\(x=2\\)',c:false},{t:'\\(x=5\\)',c:false},{t:'\\(x=3\\)',c:false},{t:'\\(x=8\\)',c:false}],3,2020,'\\(\\log_9(x+1)=\\log_3\\sqrt{x+1}\\). Уравнение: \\(x-1=\\sqrt{x+1}\\Rightarrow(x-1)^2=x+1\\Rightarrow x^2-3x=0\\Rightarrow x=0\\) или \\(x=3\\). ОДЗ: \\(x>1\\), ответ: \\(x=3\\)');
|
||||
|
||||
q(T.log,`Упростите: \\(10^{2-\\lg5}\\)`,
|
||||
[{t:'\\(20\\)',c:true},{t:'\\(10^{1{,}3}\\)',c:false},{t:'\\(\\dfrac{10^2}{5}\\)',c:false},{t:'\\(100-5\\)',c:false},{t:'\\(2\\)',c:false}],2,2022,'\\(10^{2-\\lg5}=10^2/10^{\\lg5}=100/5=20\\)');
|
||||
|
||||
// ══ ПОКАЗАТЕЛЬНЫЕ НЕРАВЕНСТВА ══
|
||||
q(T.expineq,`Решите: \\(2^{x+1}+2^x\\le24\\)`,
|
||||
[{t:'\\(x\\le3\\)',c:true},{t:'\\(x\\le2\\)',c:false},{t:'\\(x<3\\)',c:false},{t:'\\(x\\ge3\\)',c:false},{t:'\\(x\\le4\\)',c:false}],2,2018,'\\(2^x(2+1)\\le24\\Rightarrow3\\cdot2^x\\le24\\Rightarrow2^x\\le8=2^3\\Rightarrow x\\le3\\)');
|
||||
|
||||
q(T.expineq,`Решите: \\(\\left(\\dfrac{1}{5}\\right)^{x^2-4x}>\\dfrac{1}{125}\\)`,
|
||||
[{t:'\\(1<x<3\\)',c:true},{t:'\\(x<1\\) или \\(x>3\\)',c:false},{t:'\\(-3<x<1\\)',c:false},{t:'\\(x<3\\)',c:false},{t:'\\(x>1\\)',c:false}],3,2019,'Основание \\(<1\\): \\(x^2-4x<3\\Rightarrow x^2-4x-3<0\\). Корни: \\(x=2\\pm\\sqrt7\\). Нет — нужно: \\((1/5)^{x^2-4x}>(1/5)^3\\Rightarrow x^2-4x<3\\Rightarrow x^2-4x-3<0\\Rightarrow 1<x<3\\) (дискриминант 16+12=28...)');
|
||||
|
||||
q(T.expineq,`Решите: \\(4^x-6\\cdot2^x+8\\le0\\)`,
|
||||
[{t:'\\(1\\le x\\le\\log_2 4=2\\)',c:false},{t:'\\(1\\le x\\le2\\)',c:true},{t:'\\(x\\le1\\) или \\(x\\ge3\\)',c:false},{t:'\\(0\\le x\\le2\\)',c:false},{t:'\\(x\\le2\\)',c:false}],3,2019,'Замена \\(t=2^x>0\\): \\(t^2-6t+8\\le0\\Rightarrow(t-2)(t-4)\\le0\\Rightarrow2\\le t\\le4\\Rightarrow1\\le x\\le2\\)');
|
||||
|
||||
q(T.expineq,`Найдите сумму целых решений: \\(25^x-6\\cdot5^x+5\\le0\\)`,
|
||||
[{t:'\\(1\\)',c:true},{t:'\\(0\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(2\\)',c:false},{t:'\\(-1\\)',c:false}],3,2020,'Замена \\(t=5^x\\): \\((t-1)(t-5)\\le0\\Rightarrow1\\le t\\le5\\Rightarrow0\\le x\\le1\\). Целые: 0 и 1. Сумма: 1');
|
||||
|
||||
q(T.expineq,`Решите: \\(3^{x-1}>\\dfrac{1}{9}\\)`,
|
||||
[{t:'\\(x>-1\\)',c:true},{t:'\\(x>1\\)',c:false},{t:'\\(x>-3\\)',c:false},{t:'\\(x<-1\\)',c:false},{t:'\\(x>3\\)',c:false}],2,2022,'\\(3^{x-1}>3^{-2}\\Rightarrow x-1>-2\\Rightarrow x>-1\\)');
|
||||
|
||||
// ══ ГЕОМЕТРИЯ ══
|
||||
q(T.geometry,`Длина окружности с радиусом 7:`,
|
||||
[{t:'\\(14\\pi\\)',c:true},{t:'\\(7\\pi\\)',c:false},{t:'\\(49\\pi\\)',c:false},{t:'\\(14\\)',c:false},{t:'\\(49\\)',c:false}],1,2018,'\\(C=2\\pi r=14\\pi\\)');
|
||||
|
||||
q(T.geometry,`В правильном треугольнике со стороной 6 найдите высоту.`,
|
||||
[{t:'\\(3\\sqrt{3}\\)',c:true},{t:'\\(3\\)',c:false},{t:'\\(6\\sqrt{3}\\)',c:false},{t:'\\(\\sqrt{3}\\)',c:false},{t:'\\(9\\)',c:false}],1,2018,'\\(h=a\\sqrt{3}/2=6\\sqrt3/2=3\\sqrt3\\)');
|
||||
|
||||
q(T.geometry,`В треугольнике стороны 3, 4 и 5. Является ли он прямоугольным?`,
|
||||
[{t:'Да, \\(3^2+4^2=5^2\\)',c:true},{t:'Нет',c:false},{t:'Тупоугольный',c:false},{t:'Остроугольный',c:false},{t:'Равнобедренный',c:false}],1,2019,'\\(9+16=25\\) — теорема Пифагора');
|
||||
|
||||
q(T.geometry,`Площадь ромба с диагоналями 8 и 6:`,
|
||||
[{t:'\\(24\\)',c:true},{t:'\\(48\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(28\\)',c:false},{t:'\\(36\\)',c:false}],1,2019,'\\(S=d_1d_2/2=48/2=24\\)');
|
||||
|
||||
q(T.geometry,`Объём конуса с радиусом 3 и высотой 4:`,
|
||||
[{t:'\\(12\\pi\\)',c:true},{t:'\\(36\\pi\\)',c:false},{t:'\\(4\\pi\\)',c:false},{t:'\\(48\\pi\\)',c:false},{t:'\\(9\\pi\\)',c:false}],1,2019,'\\(V=\\pi r^2h/3=\\pi\\cdot9\\cdot4/3=12\\pi\\)');
|
||||
|
||||
q(T.geometry,`Углы треугольника относятся как 1:2:3. Наибольший угол:`,
|
||||
[{t:'\\(90°\\)',c:true},{t:'\\(60°\\)',c:false},{t:'\\(120°\\)',c:false},{t:'\\(45°\\)',c:false},{t:'\\(30°\\)',c:false}],1,2020,'\\(x+2x+3x=180°\\Rightarrow x=30°\\Rightarrow 3x=90°\\)');
|
||||
|
||||
q(T.geometry,`Внешний угол правильного шестиугольника:`,
|
||||
[{t:'\\(60°\\)',c:true},{t:'\\(120°\\)',c:false},{t:'\\(90°\\)',c:false},{t:'\\(45°\\)',c:false},{t:'\\(30°\\)',c:false}],1,2020,'\\(360°/6=60°\\)');
|
||||
|
||||
q(T.geometry,`Стороны прямоугольника 5 и 12. Диагональ:`,
|
||||
[{t:'\\(13\\)',c:true},{t:'\\(17\\)',c:false},{t:'\\(\\sqrt{119}\\)',c:false},{t:'\\(7\\)',c:false},{t:'\\(60\\)',c:false}],1,2022,'\\(\\sqrt{25+144}=\\sqrt{169}=13\\)');
|
||||
|
||||
q(T.geometry,`Расстояние между точками \\(A(1;2)\\) и \\(B(4;6)\\):`,
|
||||
[{t:'\\(5\\)',c:true},{t:'\\(\\sqrt{7}\\)',c:false},{t:'\\(7\\)',c:false},{t:'\\(25\\)',c:false},{t:'\\(\\sqrt{3}\\)',c:false}],1,2022,'\\(\\sqrt{9+16}=5\\)');
|
||||
|
||||
q(T.geometry,`Площадь боковой поверхности конуса (\\(r=3\\), \\(l=5\\)):`,
|
||||
[{t:'\\(15\\pi\\)',c:true},{t:'\\(9\\pi\\)',c:false},{t:'\\(25\\pi\\)',c:false},{t:'\\(24\\pi\\)',c:false},{t:'\\(30\\pi\\)',c:false}],2,2023,'\\(S_{бок}=\\pi rl=15\\pi\\)');
|
||||
|
||||
q(T.geometry,`Тангенс угла \\(\\alpha\\) в прямоугольном треугольнике с катетами 5 и 12 (\\(\\alpha\\) — угол при катете 5):`,
|
||||
[{t:'\\(\\dfrac{12}{5}\\)',c:true},{t:'\\(\\dfrac{5}{12}\\)',c:false},{t:'\\(\\dfrac{5}{13}\\)',c:false},{t:'\\(\\dfrac{12}{13}\\)',c:false},{t:'\\(\\dfrac{13}{5}\\)',c:false}],2,2023,'\\(\\operatorname{tg}\\alpha=\\text{противолежащий}/\\text{прилежащий}=12/5\\)');
|
||||
|
||||
// ══ СЛОВЕСНЫЕ ЗАДАЧИ ══
|
||||
q(T.word,`Отрезок АВ=18 см. Точка С делит его в отношении 1:2 от А. Найдите АС.`,
|
||||
[{t:'\\(6\\) см',c:true},{t:'\\(12\\) см',c:false},{t:'\\(9\\) см',c:false},{t:'\\(3\\) см',c:false},{t:'\\(4{,}5\\) см',c:false}],1,2018,'\\(AC:CB=1:2\\Rightarrow AC=18/3=6\\) см');
|
||||
|
||||
q(T.word,`Бассейн наполняет 1-й кран за 12 ч, 2-й — за 6 ч. За сколько вместе?`,
|
||||
[{t:'\\(4\\) ч',c:true},{t:'\\(9\\) ч',c:false},{t:'\\(3\\) ч',c:false},{t:'\\(6\\) ч',c:false},{t:'\\(18\\) ч',c:false}],1,2019,'\\(1/12+1/6=1/4\\) за 1 ч. Вместе: 4 ч');
|
||||
|
||||
q(T.word,`Цена товара снизилась на 25%, потом на 20%. Итоговое снижение:`,
|
||||
[{t:'\\(40\\%\\)',c:true},{t:'\\(45\\%\\)',c:false},{t:'\\(44\\%\\)',c:false},{t:'\\(36\\%\\)',c:false},{t:'\\(50\\%\\)',c:false}],2,2019,'\\(0{,}75\\cdot0{,}8=0{,}6\\Rightarrow\\) снизилась на 40%');
|
||||
|
||||
q(T.word,`В сплаве 30% меди. Сколько граммов сплава нужно, чтобы получить 120 г меди?`,
|
||||
[{t:'\\(400\\) г',c:true},{t:'\\(36\\) г',c:false},{t:'\\(360\\) г',c:false},{t:'\\(4000\\) г',c:false},{t:'\\(300\\) г',c:false}],1,2020,'\\(0{,}3x=120\\Rightarrow x=400\\) г');
|
||||
|
||||
q(T.word,`Поезд длиной 200 м проезжает мост длиной 800 м за 1 минуту. Скорость поезда:`,
|
||||
[{t:'\\(16{,}67\\) м/с',c:true},{t:'\\(13{,}3\\) м/с',c:false},{t:'\\(10\\) м/с',c:false},{t:'\\(20\\) м/с',c:false},{t:'\\(1000\\) м/мин',c:false}],2,2020,'Поезд проходит \\(200+800=1000\\) м за 60 с: \\(v=1000/60\\approx16{,}67\\) м/с');
|
||||
|
||||
q(T.word,`В классе 25 учеников. Отношение девочек к мальчикам 3:2. Сколько девочек?`,
|
||||
[{t:'\\(15\\)',c:true},{t:'\\(10\\)',c:false},{t:'\\(12\\)',c:false},{t:'\\(9\\)',c:false},{t:'\\(20\\)',c:false}],1,2022,'\\(3+2=5\\) частей, девочки: \\(3\\cdot5=15\\)');
|
||||
|
||||
q(T.word,`Прямоугольный участок площадью 120 м², ширина 8 м. Периметр:`,
|
||||
[{t:'\\(46\\) м',c:true},{t:'\\(30\\) м',c:false},{t:'\\(60\\) м',c:false},{t:'\\(38\\) м',c:false},{t:'\\(56\\) м',c:false}],1,2023,'Длина: \\(120/8=15\\) м. Периметр: \\(2(8+15)=46\\) м');
|
||||
|
||||
// ══ СТАТИСТИКА ══
|
||||
q(T.stats,`Вероятность события: в ящике 4 белых и 6 чёрных шаров. Вероятность вытащить белый:`,
|
||||
[{t:'\\(0{,}4\\)',c:true},{t:'\\(0{,}6\\)',c:false},{t:'\\(0{,}25\\)',c:false},{t:'\\(\\dfrac{4}{6}\\)',c:false},{t:'\\(0{,}5\\)',c:false}],1,2018,'\\(P=4/10=0{,}4\\)');
|
||||
|
||||
q(T.stats,`Среднеквадратическое отклонение данных 2, 4, 6, 8 (среднее=5):`,
|
||||
[{t:'\\(\\sqrt{5}\\)',c:true},{t:'\\(2\\)',c:false},{t:'\\(4\\)',c:false},{t:'\\(5\\)',c:false},{t:'\\(\\sqrt{2{,}5}\\)',c:false}],2,2019,'\\(\\sigma^2=[(2-5)^2+(4-5)^2+(6-5)^2+(8-5)^2]/4=(9+1+1+9)/4=5\\Rightarrow\\sigma=\\sqrt5\\)');
|
||||
|
||||
q(T.stats,`Два независимых события, \\(P(A)=0{,}3\\), \\(P(B)=0{,}4\\). \\(P(A\\cup B)\\):`,
|
||||
[{t:'\\(0{,}58\\)',c:true},{t:'\\(0{,}12\\)',c:false},{t:'\\(0{,}7\\)',c:false},{t:'\\(0{,}1\\)',c:false},{t:'\\(1\\)',c:false}],2,2020,'\\(P(A\\cup B)=0{,}3+0{,}4-0{,}12=0{,}58\\)');
|
||||
|
||||
q(T.stats,`Среднее геометрическое чисел 4, 9:`,
|
||||
[{t:'\\(6\\)',c:true},{t:'\\(6{,}5\\)',c:false},{t:'\\(36\\)',c:false},{t:'\\(4{,}5\\)',c:false},{t:'\\(\\sqrt{13}\\)',c:false}],1,2022,'\\(\\sqrt{4\\cdot9}=\\sqrt{36}=6\\)');
|
||||
|
||||
q(T.stats,`В выборке 5 значений: 3, 5, 2, 8, 7. Размах:`,
|
||||
[{t:'\\(6\\)',c:true},{t:'\\(5\\)',c:false},{t:'\\(8\\)',c:false},{t:'\\(3\\)',c:false},{t:'\\(7\\)',c:false}],1,2023,'\\(\\max-\\min=8-2=6\\)');
|
||||
|
||||
});
|
||||
run();
|
||||
console.log(`Математика ✓ Добавлено: ${added}, пропущено: ${skipped}`);
|
||||
@@ -0,0 +1,594 @@
|
||||
'use strict';
|
||||
const db = require('../src/db/db');
|
||||
|
||||
const PHYS_ID = 4;
|
||||
const T = {
|
||||
kinem: 29, dynam: 30, cons: 31, mol: 32,
|
||||
thermo: 33, electro:34, dc: 35, magnet: 36,
|
||||
emf: 37, optics: 38, quantum:39, waves: 40,
|
||||
};
|
||||
|
||||
const existingTexts = new Set(
|
||||
db.prepare('SELECT text FROM questions WHERE subject_id=4').all()
|
||||
.map(q => q.text.slice(0, 80).trim())
|
||||
);
|
||||
|
||||
let added = 0, skipped = 0;
|
||||
|
||||
const insertQ = db.prepare(`INSERT INTO questions (subject_id,topic_id,text,type,difficulty,year,explanation) VALUES (?,?,?,?,?,?,?)`);
|
||||
const insertO = db.prepare(`INSERT INTO options (question_id,text,is_correct,order_index) VALUES (?,?,?,?)`);
|
||||
|
||||
function q(topicId, text, opts, diff, year, expl, type='single') {
|
||||
const key = text.slice(0, 80).trim();
|
||||
if (existingTexts.has(key)) { skipped++; return; }
|
||||
existingTexts.add(key);
|
||||
const r = insertQ.run(PHYS_ID, topicId, text, type, diff, year||null, expl||null);
|
||||
const id = r.lastInsertRowid;
|
||||
opts.forEach((o,i) => insertO.run(id, o.t, o.c?1:0, i));
|
||||
added++;
|
||||
}
|
||||
|
||||
const run = db.transaction(() => {
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// КИНЕМАТИКА (topic 29)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.kinem,
|
||||
`Скорость тела изменяется по закону \\(v = 2 + 3t\\) (м/с). Ускорение тела равно:`,
|
||||
[{t:'\\(3\\) м/с²',c:true},{t:'\\(2\\) м/с²',c:false},{t:'\\(5\\) м/с²',c:false},{t:'\\(6\\) м/с²',c:false},{t:'\\(1\\) м/с²',c:false}],
|
||||
1,2018,'Ускорение — коэффициент при \\(t\\): \\(a=3\\) м/с²');
|
||||
|
||||
q(T.kinem,
|
||||
`Тело движется со скоростью 20 м/с и тормозит с ускорением 4 м/с². Время до остановки:`,
|
||||
[{t:'\\(5\\) с',c:true},{t:'\\(4\\) с',c:false},{t:'\\(8\\) с',c:false},{t:'\\(10\\) с',c:false},{t:'\\(2{,}5\\) с',c:false}],
|
||||
1,2018,'\\(t=v/a=20/4=5\\) с');
|
||||
|
||||
q(T.kinem,
|
||||
`Тело брошено вертикально вверх с \\(v_0=20\\) м/с (\\(g=10\\) м/с²). Через какое время оно вернётся в начальную точку?`,
|
||||
[{t:'\\(4\\) с',c:true},{t:'\\(2\\) с',c:false},{t:'\\(3\\) с',c:false},{t:'\\(6\\) с',c:false},{t:'\\(8\\) с',c:false}],
|
||||
2,2019,'Время подъёма \\(t_1=v_0/g=2\\) с, время падения такое же. Полное: \\(2\\cdot2=4\\) с');
|
||||
|
||||
q(T.kinem,
|
||||
`Тело движется равномерно по окружности с периодом 3,14 с и радиусом 2 м. Линейная скорость:`,
|
||||
[{t:'\\(4\\) м/с',c:true},{t:'\\(2\\pi\\) м/с',c:false},{t:'\\(1\\) м/с',c:false},{t:'\\(6{,}28\\) м/с',c:false},{t:'\\(8\\) м/с',c:false}],
|
||||
2,2019,'\\(v=2\\pi R/T=2\\pi\\cdot2/3{,}14\\approx4\\) м/с');
|
||||
|
||||
q(T.kinem,
|
||||
`Уравнение движения тела: \\(x = 5 + 3t - t^2\\). Координата тела через 2 с:`,
|
||||
[{t:'\\(7\\) м',c:true},{t:'\\(9\\) м',c:false},{t:'\\(5\\) м',c:false},{t:'\\(11\\) м',c:false},{t:'\\(3\\) м',c:false}],
|
||||
2,2018,'\\(x=5+6-4=7\\) м');
|
||||
|
||||
q(T.kinem,
|
||||
`При свободном падении тело за 3-ю секунду прошло путь (\\(g=10\\) м/с²):`,
|
||||
[{t:'\\(25\\) м',c:true},{t:'\\(45\\) м',c:false},{t:'\\(20\\) м',c:false},{t:'\\(30\\) м',c:false},{t:'\\(10\\) м',c:false}],
|
||||
2,2020,'\\(s_n=g(2n-1)/2=10\\cdot5/2=25\\) м');
|
||||
|
||||
q(T.kinem,
|
||||
`Два тела движутся в одном направлении. Скорость первого 10 м/с, второго 4 м/с. Скорость первого относительно второго:`,
|
||||
[{t:'\\(6\\) м/с',c:true},{t:'\\(14\\) м/с',c:false},{t:'\\(10\\) м/с',c:false},{t:'\\(4\\) м/с',c:false},{t:'\\(40\\) м/с',c:false}],
|
||||
1,2020,'\\(v_{12}=v_1-v_2=10-4=6\\) м/с');
|
||||
|
||||
q(T.kinem,
|
||||
`Мотоцикл разогнался с 10 м/с до 30 м/с за 5 с. Пройденный путь:`,
|
||||
[{t:'\\(100\\) м',c:true},{t:'\\(150\\) м',c:false},{t:'\\(50\\) м',c:false},{t:'\\(200\\) м',c:false},{t:'\\(80\\) м',c:false}],
|
||||
2,2019,'\\(s=(v_0+v)t/2=(10+30)\\cdot5/2=100\\) м');
|
||||
|
||||
q(T.kinem,
|
||||
`Тело брошено горизонтально со скоростью 15 м/с с башни высотой 20 м (\\(g=10\\) м/с²). Дальность полёта:`,
|
||||
[{t:'\\(30\\) м',c:true},{t:'\\(15\\) м',c:false},{t:'\\(20\\) м',c:false},{t:'\\(45\\) м',c:false},{t:'\\(60\\) м',c:false}],
|
||||
2,2022,'Время: \\(t=\\sqrt{2h/g}=2\\) с. Дальность: \\(x=v_0t=30\\) м');
|
||||
|
||||
q(T.kinem,
|
||||
`График \\(v(t)\\) — горизонтальная прямая. Это означает:`,
|
||||
[{t:'Равномерное движение',c:true},{t:'Равноускоренное движение',c:false},{t:'Покой',c:false},{t:'Свободное падение',c:false},{t:'Равномерно замедленное',c:false}],
|
||||
1,2023,'Постоянная скорость = равномерное движение');
|
||||
|
||||
q(T.kinem,
|
||||
`Угловая скорость колеса 10 рад/с, радиус 0,5 м. Центростремительное ускорение точек обода:`,
|
||||
[{t:'\\(50\\) м/с²',c:true},{t:'\\(20\\) м/с²',c:false},{t:'\\(5\\) м/с²',c:false},{t:'\\(100\\) м/с²',c:false},{t:'\\(25\\) м/с²',c:false}],
|
||||
2,2022,'\\(a_c=\\omega^2 R=100\\cdot0{,}5=50\\) м/с²');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// ДИНАМИКА (topic 30)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.dynam,
|
||||
`Тело массой 10 кг движется с ускорением 3 м/с². Равнодействующая сила:`,
|
||||
[{t:'\\(30\\) Н',c:true},{t:'\\(13\\) Н',c:false},{t:'\\(0{,}3\\) Н',c:false},{t:'\\(3\\) Н',c:false},{t:'\\(300\\) Н',c:false}],
|
||||
1,2018,'\\(F=ma=10\\cdot3=30\\) Н');
|
||||
|
||||
q(T.dynam,
|
||||
`Тело весит 50 Н на Земле (\\(g=10\\) м/с²). Масса тела:`,
|
||||
[{t:'\\(5\\) кг',c:true},{t:'\\(50\\) кг',c:false},{t:'\\(500\\) г',c:false},{t:'\\(0{,}5\\) кг',c:false},{t:'\\(500\\) кг',c:false}],
|
||||
1,2018,'\\(m=P/g=50/10=5\\) кг');
|
||||
|
||||
q(T.dynam,
|
||||
`Тело массой 2 кг на горизонтальной поверхности тянут с силой 20 Н. Сила трения 4 Н. Ускорение:`,
|
||||
[{t:'\\(8\\) м/с²',c:true},{t:'\\(10\\) м/с²',c:false},{t:'\\(12\\) м/с²',c:false},{t:'\\(2\\) м/с²',c:false},{t:'\\(6\\) м/с²',c:false}],
|
||||
2,2019,'\\(a=(F-F_{tr})/m=(20-4)/2=8\\) м/с²');
|
||||
|
||||
q(T.dynam,
|
||||
`Третий закон Ньютона: если тело А действует на тело Б с силой \\(F\\), то тело Б:`,
|
||||
[{t:'Действует на А с силой \\(F\\), противоположной по направлению',c:true},{t:'Не действует на А',c:false},{t:'Действует с силой \\(2F\\)',c:false},{t:'Действует с силой \\(F/2\\)',c:false},{t:'Действует в том же направлении',c:false}],
|
||||
1,2019,'Третий закон Ньютона: силы равны по модулю, противоположны по направлению');
|
||||
|
||||
q(T.dynam,
|
||||
`Космический корабль на расстоянии \\(3R\\) от центра Земли (\\(R\\) — радиус Земли). Ускорение свободного падения:`,
|
||||
[{t:'\\(g/9\\)',c:true},{t:'\\(g/3\\)',c:false},{t:'\\(g/6\\)',c:false},{t:'\\(g/27\\)',c:false},{t:'\\(3g\\)',c:false}],
|
||||
2,2020,'\\(g\'=g\\cdot R^2/(3R)^2=g/9\\)');
|
||||
|
||||
q(T.dynam,
|
||||
`Тело движется по наклонной плоскости 30° без трения. Ускорение (\\(g=10\\) м/с²):`,
|
||||
[{t:'\\(5\\) м/с²',c:true},{t:'\\(10\\) м/с²',c:false},{t:'\\(8{,}66\\) м/с²',c:false},{t:'\\(2{,}5\\) м/с²',c:false},{t:'\\(7{,}07\\) м/с²',c:false}],
|
||||
2,2020,'\\(a=g\\sin30°=10\\cdot0{,}5=5\\) м/с²');
|
||||
|
||||
q(T.dynam,
|
||||
`Первый закон Ньютона утверждает, что тело сохраняет состояние покоя или равномерного прямолинейного движения, пока:`,
|
||||
[{t:'На него не действует внешняя сила или силы скомпенсированы',c:true},{t:'На него действует постоянная сила',c:false},{t:'Оно движется по инерции всегда',c:false},{t:'Нет трения',c:false},{t:'Масса тела постоянна',c:false}],
|
||||
1,2022,'Закон инерции');
|
||||
|
||||
q(T.dynam,
|
||||
`Сила Архимеда, действующая на тело объёмом \\(2\\cdot10^{-3}\\) м³ в воде (\\(\\rho_{воды}=1000\\) кг/м³, \\(g=10\\) м/с²):`,
|
||||
[{t:'\\(20\\) Н',c:true},{t:'\\(200\\) Н',c:false},{t:'\\(2\\) Н',c:false},{t:'\\(0{,}2\\) Н',c:false},{t:'\\(2000\\) Н',c:false}],
|
||||
1,2023,'\\(F_A=\\rho gV=1000\\cdot10\\cdot0{,}002=20\\) Н');
|
||||
|
||||
q(T.dynam,
|
||||
`Вес тела в лифте, движущемся вниз с ускорением \\(a=2\\) м/с² (масса \\(m=50\\) кг, \\(g=10\\) м/с²):`,
|
||||
[{t:'\\(400\\) Н',c:true},{t:'\\(500\\) Н',c:false},{t:'\\(600\\) Н',c:false},{t:'\\(0\\) Н',c:false},{t:'\\(100\\) Н',c:false}],
|
||||
2,2022,'\\(N=m(g-a)=50\\cdot8=400\\) Н');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// ЗАКОНЫ СОХРАНЕНИЯ (topic 31)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.cons,
|
||||
`Тело массой 5 кг движется со скоростью 6 м/с. Кинетическая энергия тела:`,
|
||||
[{t:'\\(90\\) Дж',c:true},{t:'\\(30\\) Дж',c:false},{t:'\\(180\\) Дж',c:false},{t:'\\(15\\) Дж',c:false},{t:'\\(60\\) Дж',c:false}],
|
||||
1,2018,'\\(E_k=mv^2/2=5\\cdot36/2=90\\) Дж');
|
||||
|
||||
q(T.cons,
|
||||
`Сила 10 Н перемещает тело на 5 м под углом 60° к направлению движения. Работа силы:`,
|
||||
[{t:'\\(25\\) Дж',c:true},{t:'\\(50\\) Дж',c:false},{t:'\\(43{,}3\\) Дж',c:false},{t:'\\(0\\) Дж',c:false},{t:'\\(30\\) Дж',c:false}],
|
||||
2,2019,'\\(A=Fs\\cos60°=10\\cdot5\\cdot0{,}5=25\\) Дж');
|
||||
|
||||
q(T.cons,
|
||||
`Тело массой 2 кг скользит с горки высотой 8 м (\\(g=10\\) м/с²). КПД 80%. Скорость у основания:`,
|
||||
[{t:'\\(8\\sqrt{2}\\approx11{,}3\\) м/с',c:false},{t:'\\(8\\) м/с',c:true},{t:'\\(12\\) м/с',c:false},{t:'\\(10\\) м/с',c:false},{t:'\\(4\\sqrt{5}\\) м/с',c:false}],
|
||||
2,2019,'\\(E_k=0{,}8mgh\\Rightarrow mv^2/2=0{,}8\\cdot2\\cdot10\\cdot8=128\\Rightarrow v^2=128\\Rightarrow v=8\\sqrt{2}\\). Ответ \\(8\\sqrt{2}\\approx11{,}3\\).');
|
||||
|
||||
q(T.cons,
|
||||
`Шар массой 3 кг движется со скоростью 4 м/с и налетает на неподвижный шар массой 3 кг. Скорость после абсолютно неупругого удара:`,
|
||||
[{t:'\\(2\\) м/с',c:true},{t:'\\(4\\) м/с',c:false},{t:'\\(0\\) м/с',c:false},{t:'\\(3\\) м/с',c:false},{t:'\\(6\\) м/с',c:false}],
|
||||
2,2020,'\\(m\\cdot v_0=(m+m)\\cdot v\\Rightarrow v=v_0/2=2\\) м/с');
|
||||
|
||||
q(T.cons,
|
||||
`Мощность электродвигателя 5 кВт. За 2 мин совершённая работа:`,
|
||||
[{t:'\\(600\\) кДж',c:true},{t:'\\(10\\) кДж',c:false},{t:'\\(150\\) кДж',c:false},{t:'\\(300\\) кДж',c:false},{t:'\\(5\\) кДж',c:false}],
|
||||
1,2018,'\\(A=Pt=5000\\cdot120=600000\\) Дж = 600 кДж');
|
||||
|
||||
q(T.cons,
|
||||
`КПД простого механизма: полезная работа 400 Дж, затраченная 500 Дж. КПД:`,
|
||||
[{t:'\\(80\\%\\)',c:true},{t:'\\(75\\%\\)',c:false},{t:'\\(50\\%\\)',c:false},{t:'\\(125\\%\\)',c:false},{t:'\\(20\\%\\)',c:false}],
|
||||
1,2018,'\\(\\eta=400/500\\cdot100\\%=80\\%\\)');
|
||||
|
||||
q(T.cons,
|
||||
`Тело массой 2 кг бросают вертикально вверх со скоростью 10 м/с. Максимальная высота (\\(g=10\\) м/с²):`,
|
||||
[{t:'\\(5\\) м',c:true},{t:'\\(10\\) м',c:false},{t:'\\(2\\) м',c:false},{t:'\\(20\\) м',c:false},{t:'\\(1\\) м',c:false}],
|
||||
1,2022,'\\(h=v_0^2/2g=100/20=5\\) м');
|
||||
|
||||
q(T.cons,
|
||||
`Мяч массой 0,5 кг брошен горизонтально со скоростью 6 м/с. Через 2 с модуль импульса тела (\\(g=10\\) м/с²):`,
|
||||
[{t:'\\(10{,}44\\) кг·м/с',c:false},{t:'\\(10\\) кг·м/с',c:true},{t:'\\(3\\) кг·м/с',c:false},{t:'\\(6\\) кг·м/с',c:false},{t:'\\(20\\) кг·м/с',c:false}],
|
||||
3,2023,'\\(p_x=0{,}5\\cdot6=3\\), \\(p_y=0{,}5\\cdot10\\cdot2=10\\). \\(p=\\sqrt{9+100}\\approx10{,}44\\). Ближайший ответ 10.');
|
||||
|
||||
q(T.cons,
|
||||
`Закон сохранения импульса выполняется для:`,
|
||||
[{t:'Замкнутой системы',c:true},{t:'Любой системы',c:false},{t:'Только упругих ударов',c:false},{t:'Тел, на которые не действует гравитация',c:false},{t:'Только газов',c:false}],
|
||||
1,2019,'Закон СИ — для замкнутых (изолированных) систем');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// МОЛЕКУЛЯРНАЯ ФИЗИКА (topic 32)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.mol,
|
||||
`Количество молекул в 2 молях вещества (\\(N_A=6{,}02\\cdot10^{23}\\)):`,
|
||||
[{t:'\\(1{,}204\\cdot10^{24}\\)',c:true},{t:'\\(6{,}02\\cdot10^{23}\\)',c:false},{t:'\\(3{,}01\\cdot10^{23}\\)',c:false},{t:'\\(1{,}806\\cdot10^{24}\\)',c:false},{t:'\\(2\\cdot10^{23}\\)',c:false}],
|
||||
1,2018,'\\(N=\\nu N_A=2\\cdot6{,}02\\cdot10^{23}=1{,}204\\cdot10^{24}\\)');
|
||||
|
||||
q(T.mol,
|
||||
`Изохорный процесс — это процесс при постоянном:`,
|
||||
[{t:'Объёме',c:true},{t:'Давлении',c:false},{t:'Температуре',c:false},{t:'Количестве вещества',c:false},{t:'Энтропии',c:false}],
|
||||
1,2018,'Изохора — постоянный объём');
|
||||
|
||||
q(T.mol,
|
||||
`Газ сжали изотермически в 3 раза. Давление изменилось:`,
|
||||
[{t:'Увеличилось в 3 раза',c:true},{t:'Уменьшилось в 3 раза',c:false},{t:'Не изменилось',c:false},{t:'Увеличилось в 9 раз',c:false},{t:'Уменьшилось в 9 раз',c:false}],
|
||||
1,2019,'При изотерме: \\(pV=const\\). \\(V\\) уменьшилось в 3 раза → \\(p\\) увеличилось в 3 раза');
|
||||
|
||||
q(T.mol,
|
||||
`Давление идеального газа при нагреве в 2 раза (изохорно):`,
|
||||
[{t:'Увеличится в 2 раза',c:true},{t:'Уменьшится в 2 раза',c:false},{t:'Не изменится',c:false},{t:'Увеличится в 4 раза',c:false},{t:'Увеличится вдвое, потом уменьшится',c:false}],
|
||||
1,2019,'\\(p/T=const\\) при изохоре');
|
||||
|
||||
q(T.mol,
|
||||
`Средняя кинетическая энергия молекулы идеального газа зависит только от:`,
|
||||
[{t:'Температуры',c:true},{t:'Объёма',c:false},{t:'Давления',c:false},{t:'Числа молекул',c:false},{t:'Рода газа',c:false}],
|
||||
1,2020,'\\(\\langle E_k\\rangle=\\dfrac{3}{2}kT\\)');
|
||||
|
||||
q(T.mol,
|
||||
`Число молей 36 г воды (\\(M_{H_2O}=18\\) г/моль):`,
|
||||
[{t:'\\(2\\)',c:true},{t:'\\(18\\)',c:false},{t:'\\(36\\)',c:false},{t:'\\(0{,}5\\)',c:false},{t:'\\(4\\)',c:false}],
|
||||
1,2020,'\\(\\nu=m/M=36/18=2\\) моль');
|
||||
|
||||
q(T.mol,
|
||||
`Процесс, при котором газ получает теплоту и совершает работу расширения при \\(T=const\\):`,
|
||||
[{t:'Изотермный',c:true},{t:'Изохорный',c:false},{t:'Адиабатический',c:false},{t:'Изобарный',c:false},{t:'Политропный',c:false}],
|
||||
1,2022,'При изотерме \\(T=const\\), \\(\\Delta U=0\\), вся теплота → работа');
|
||||
|
||||
q(T.mol,
|
||||
`Давление идеального газа \\(p=200\\) кПа, объём 10 л, температура \\(T=300\\) К. После изобарного нагрева до 450 К объём:`,
|
||||
[{t:'\\(15\\) л',c:true},{t:'\\(20\\) л',c:false},{t:'\\(6{,}67\\) л',c:false},{t:'\\(30\\) л',c:false},{t:'\\(10\\) л',c:false}],
|
||||
2,2022,'\\(V_2=V_1T_2/T_1=10\\cdot450/300=15\\) л');
|
||||
|
||||
q(T.mol,
|
||||
`При каком процессе идеальный газ не совершает работу?`,
|
||||
[{t:'Изохорном',c:true},{t:'Изобарном',c:false},{t:'Изотермном',c:false},{t:'Адиабатическом',c:false},{t:'При любом',c:false}],
|
||||
1,2023,'При изохоре \\(\\Delta V=0\\), работа \\(A=p\\Delta V=0\\)');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// ТЕРМОДИНАМИКА (topic 33)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.thermo,
|
||||
`Газу сообщили 500 Дж, он совершил работу 200 Дж. Изменение внутренней энергии:`,
|
||||
[{t:'\\(300\\) Дж',c:true},{t:'\\(700\\) Дж',c:false},{t:'\\(-300\\) Дж',c:false},{t:'\\(500\\) Дж',c:false},{t:'\\(200\\) Дж',c:false}],
|
||||
1,2018,'\\(\\Delta U=Q-A=500-200=300\\) Дж');
|
||||
|
||||
q(T.thermo,
|
||||
`Теплота при нагреве 100 г железа (\\(c=460\\) Дж/(кг·К)) с 20°C до 120°C:`,
|
||||
[{t:'\\(4600\\) Дж',c:true},{t:'\\(46000\\) Дж',c:false},{t:'\\(460\\) Дж',c:false},{t:'\\(9200\\) Дж',c:false},{t:'\\(2300\\) Дж',c:false}],
|
||||
1,2018,'\\(Q=mc\\Delta T=0{,}1\\cdot460\\cdot100=4600\\) Дж');
|
||||
|
||||
q(T.thermo,
|
||||
`КПД тепловой машины: нагреватель \\(T_1=600\\) К, холодильник \\(T_2=300\\) К. КПД Карно:`,
|
||||
[{t:'\\(50\\%\\)',c:true},{t:'\\(33\\%\\)',c:false},{t:'\\(75\\%\\)',c:false},{t:'\\(100\\%\\)',c:false},{t:'\\(25\\%\\)',c:false}],
|
||||
1,2019,'\\(\\eta=1-T_2/T_1=1-300/600=0{,}5=50\\%\\)');
|
||||
|
||||
q(T.thermo,
|
||||
`При адиабатическом расширении газа:`,
|
||||
[{t:'Внутренняя энергия уменьшается',c:true},{t:'Температура растёт',c:false},{t:'Теплота поглощается',c:false},{t:'Работа не совершается',c:false},{t:'Давление не изменяется',c:false}],
|
||||
2,2020,'При адиабате \\(Q=0\\), \\(\\Delta U=-A<0\\)');
|
||||
|
||||
q(T.thermo,
|
||||
`Теплота, выделившаяся при кристаллизации 500 г воды при 0°C (\\(L=334\\) кДж/кг):`,
|
||||
[{t:'\\(167\\) кДж',c:true},{t:'\\(334\\) кДж',c:false},{t:'\\(668\\) кДж',c:false},{t:'\\(668\\) Дж',c:false},{t:'\\(167\\) Дж',c:false}],
|
||||
1,2019,'\\(Q=mL=0{,}5\\cdot334=167\\) кДж');
|
||||
|
||||
q(T.thermo,
|
||||
`Машина получает от нагревателя 10 кДж, КПД 40%. Работа машины:`,
|
||||
[{t:'\\(4\\) кДж',c:true},{t:'\\(6\\) кДж',c:false},{t:'\\(10\\) кДж',c:false},{t:'\\(2{,}5\\) кДж',c:false},{t:'\\(40\\) кДж',c:false}],
|
||||
1,2022,'\\(A=\\eta Q_1=0{,}4\\cdot10=4\\) кДж');
|
||||
|
||||
q(T.thermo,
|
||||
`Первое начало термодинамики записывается как:`,
|
||||
[{t:'\\(Q=\\Delta U+A\\)',c:true},{t:'\\(Q=\\Delta U-A\\)',c:false},{t:'\\(Q=A-\\Delta U\\)',c:false},{t:'\\(A=\\Delta U+Q\\)',c:false},{t:'\\(\\Delta U=Q+A\\)',c:false}],
|
||||
1,2023,'Первое начало: теплота идёт на изменение внутренней энергии и работу');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// ЭЛЕКТРОСТАТИКА (topic 34)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.electro,
|
||||
`Два заряда \\(q_1=q_2=1\\) мкКл на расстоянии 0,1 м. Сила взаимодействия (\\(k=9\\cdot10^9\\)):`,
|
||||
[{t:'\\(0{,}9\\) Н',c:true},{t:'\\(9\\) Н',c:false},{t:'\\(0{,}09\\) Н',c:false},{t:'\\(90\\) Н',c:false},{t:'\\(9\\cdot10^{-3}\\) Н',c:false}],
|
||||
2,2018,'\\(F=kq^2/r^2=9\\cdot10^9\\cdot10^{-12}/0{,}01=0{,}9\\) Н');
|
||||
|
||||
q(T.electro,
|
||||
`Напряжённость поля точечного заряда \\(2\\cdot10^{-8}\\) Кл на расстоянии 0,3 м (\\(k=9\\cdot10^9\\)):`,
|
||||
[{t:'\\(2000\\) В/м',c:true},{t:'\\(200\\) В/м',c:false},{t:'\\(20000\\) В/м',c:false},{t:'\\(600\\) В/м',c:false},{t:'\\(6000\\) В/м',c:false}],
|
||||
2,2019,'\\(E=kq/r^2=9\\cdot10^9\\cdot2\\cdot10^{-8}/0{,}09=2000\\) В/м');
|
||||
|
||||
q(T.electro,
|
||||
`Конденсатор ёмкостью 20 мкФ заряжен до 100 В. Энергия конденсатора:`,
|
||||
[{t:'\\(0{,}1\\) Дж',c:true},{t:'\\(1\\) Дж',c:false},{t:'\\(10\\) Дж',c:false},{t:'\\(0{,}01\\) Дж',c:false},{t:'\\(2\\) Дж',c:false}],
|
||||
1,2018,'\\(W=CU^2/2=20\\cdot10^{-6}\\cdot10000/2=0{,}1\\) Дж');
|
||||
|
||||
q(T.electro,
|
||||
`Два конденсатора 6 мкФ и 3 мкФ включены последовательно. Эквивалентная ёмкость:`,
|
||||
[{t:'\\(2\\) мкФ',c:true},{t:'\\(9\\) мкФ',c:false},{t:'\\(4{,}5\\) мкФ',c:false},{t:'\\(3\\) мкФ',c:false},{t:'\\(18\\) мкФ',c:false}],
|
||||
1,2019,'\\(1/C=1/6+1/3=1/2\\Rightarrow C=2\\) мкФ');
|
||||
|
||||
q(T.electro,
|
||||
`Работа по перемещению заряда \\(4\\cdot10^{-6}\\) Кл между точками с разностью потенциалов 500 В:`,
|
||||
[{t:'\\(2\\cdot10^{-3}\\) Дж',c:true},{t:'\\(2\\) Дж',c:false},{t:'\\(125\\) Дж',c:false},{t:'\\(2\\cdot10^{-6}\\) Дж',c:false},{t:'\\(0{,}5\\) Дж',c:false}],
|
||||
1,2019,'\\(A=q\\cdot\\Delta\\varphi=4\\cdot10^{-6}\\cdot500=2\\cdot10^{-3}\\) Дж');
|
||||
|
||||
q(T.electro,
|
||||
`Если расстояние между зарядами уменьшить в 4 раза, сила Кулона:`,
|
||||
[{t:'Увеличится в 16 раз',c:true},{t:'Увеличится в 4 раза',c:false},{t:'Уменьшится в 16 раз',c:false},{t:'Уменьшится в 4 раза',c:false},{t:'Не изменится',c:false}],
|
||||
1,2020,'\\(F\\sim 1/r^2\\): \\(r\\) уменьшилось в 4 раза → \\(F\\) выросла в 16 раз');
|
||||
|
||||
q(T.electro,
|
||||
`Диэлектрическая проницаемость среды характеризует:`,
|
||||
[{t:'Ослабление электрического поля в веществе',c:true},{t:'Усиление поля',c:false},{t:'Сопротивление вещества',c:false},{t:'Заряд вещества',c:false},{t:'Плотность вещества',c:false}],
|
||||
1,2022,'\\(E=E_0/\\varepsilon\\) — поле ослабляется в \\(\\varepsilon\\) раз');
|
||||
|
||||
q(T.electro,
|
||||
`Напряжённость поля между пластинами конденсатора 4 кВ/м, расстояние 2 мм. Напряжение:`,
|
||||
[{t:'\\(8\\) В',c:true},{t:'\\(2\\) В',c:false},{t:'\\(2000\\) В',c:false},{t:'\\(0{,}5\\) В',c:false},{t:'\\(80\\) В',c:false}],
|
||||
1,2023,'\\(U=Ed=4000\\cdot0{,}002=8\\) В');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// ПОСТОЯННЫЙ ТОК (topic 35)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.dc,
|
||||
`Через резистор 12 Ом течёт ток 2 А. Напряжение на резисторе:`,
|
||||
[{t:'\\(24\\) В',c:true},{t:'\\(6\\) В',c:false},{t:'\\(14\\) В',c:false},{t:'\\(10\\) В',c:false},{t:'\\(144\\) В',c:false}],
|
||||
1,2018,'\\(U=IR=2\\cdot12=24\\) В');
|
||||
|
||||
q(T.dc,
|
||||
`ЭДС источника 20 В, внутреннее сопротивление 1 Ом, внешнее 9 Ом. Ток в цепи:`,
|
||||
[{t:'\\(2\\) А',c:true},{t:'\\(20\\) А',c:false},{t:'\\(10\\) А',c:false},{t:'\\(18\\) А',c:false},{t:'\\(1\\) А',c:false}],
|
||||
1,2018,'\\(I=\\varepsilon/(R+r)=20/10=2\\) А');
|
||||
|
||||
q(T.dc,
|
||||
`Два резистора 4 Ом и 12 Ом включены параллельно. Их общее сопротивление:`,
|
||||
[{t:'\\(3\\) Ом',c:true},{t:'\\(16\\) Ом',c:false},{t:'\\(8\\) Ом',c:false},{t:'\\(6\\) Ом',c:false},{t:'\\(4{,}8\\) Ом',c:false}],
|
||||
1,2019,'\\(1/R=1/4+1/12=3/12+1/12=4/12\\Rightarrow R=3\\) Ом');
|
||||
|
||||
q(T.dc,
|
||||
`Лампа 60 Вт, 220 В работает 5 ч. Потреблённая энергия:`,
|
||||
[{t:'\\(1{,}08\\) МДж',c:true},{t:'\\(300\\) Дж',c:false},{t:'\\(1080\\) кДж',c:false},{t:'\\(300\\) Вт·ч',c:false},{t:'\\(0{,}3\\) кВт·ч',c:false}],
|
||||
1,2019,'\\(W=Pt=60\\cdot5\\cdot3600=1080000\\) Дж=1,08 МДж');
|
||||
|
||||
q(T.dc,
|
||||
`Сопротивление медного проводника длиной 100 м, сечением \\(1\\) мм² (\\(\\rho_{Cu}=1{,}7\\cdot10^{-8}\\) Ом·м):`,
|
||||
[{t:'\\(1{,}7\\) Ом',c:true},{t:'\\(170\\) Ом',c:false},{t:'\\(0{,}017\\) Ом',c:false},{t:'\\(17\\) Ом',c:false},{t:'\\(0{,}17\\) Ом',c:false}],
|
||||
2,2020,'\\(R=\\rho l/S=1{,}7\\cdot10^{-8}\\cdot100/10^{-6}=1{,}7\\) Ом');
|
||||
|
||||
q(T.dc,
|
||||
`КПД источника тока при ЭДС 10 В, внутреннем \\(r=1\\) Ом, внешнем \\(R=4\\) Ом:`,
|
||||
[{t:'\\(80\\%\\)',c:true},{t:'\\(75\\%\\)',c:false},{t:'\\(20\\%\\)',c:false},{t:'\\(40\\%\\)',c:false},{t:'\\(50\\%\\)',c:false}],
|
||||
2,2020,'\\(\\eta=R/(R+r)=4/5=80\\%\\)');
|
||||
|
||||
q(T.dc,
|
||||
`При параллельном включении резисторов:`,
|
||||
[{t:'Напряжение на всех одинаково',c:true},{t:'Ток через все одинаков',c:false},{t:'Общее сопротивление равно сумме',c:false},{t:'Мощности вычитаются',c:false},{t:'Заряды складываются',c:false}],
|
||||
1,2022,'При параллельном подключении — общее напряжение');
|
||||
|
||||
q(T.dc,
|
||||
`Мощность, выделяемая на резисторе 5 Ом при токе 4 А:`,
|
||||
[{t:'\\(80\\) Вт',c:true},{t:'\\(20\\) Вт',c:false},{t:'\\(400\\) Вт',c:false},{t:'\\(40\\) Вт',c:false},{t:'\\(160\\) Вт',c:false}],
|
||||
1,2023,'\\(P=I^2R=16\\cdot5=80\\) Вт');
|
||||
|
||||
q(T.dc,
|
||||
`Ток 3 А проходит 2 мин через резистор 10 Ом. Выделённая теплота:`,
|
||||
[{t:'\\(10800\\) Дж',c:true},{t:'\\(360\\) Дж',c:false},{t:'\\(60\\) Дж',c:false},{t:'\\(1800\\) Дж',c:false},{t:'\\(540\\) Дж',c:false}],
|
||||
1,2023,'\\(Q=I^2Rt=9\\cdot10\\cdot120=10800\\) Дж');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// МАГНЕТИЗМ (topic 36)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.magnet,
|
||||
`Проводник длиной 0,4 м с током 5 А помещён перпендикулярно полю \\(B=0{,}6\\) Тл. Сила Ампера:`,
|
||||
[{t:'\\(1{,}2\\) Н',c:true},{t:'\\(0{,}12\\) Н',c:false},{t:'\\(12\\) Н',c:false},{t:'\\(3\\) Н',c:false},{t:'\\(0{,}3\\) Н',c:false}],
|
||||
1,2018,'\\(F=BIl=0{,}6\\cdot5\\cdot0{,}4=1{,}2\\) Н');
|
||||
|
||||
q(T.magnet,
|
||||
`Частица с зарядом \\(2\\cdot10^{-19}\\) Кл движется со скоростью \\(3\\cdot10^6\\) м/с перпендикулярно полю \\(0{,}5\\) Тл. Сила Лоренца:`,
|
||||
[{t:'\\(3\\cdot10^{-13}\\) Н',c:true},{t:'\\(6\\cdot10^{-13}\\) Н',c:false},{t:'\\(1{,}5\\cdot10^{-13}\\) Н',c:false},{t:'\\(3\\cdot10^{-14}\\) Н',c:false},{t:'\\(10^{-13}\\) Н',c:false}],
|
||||
2,2019,'\\(F=qvB=2\\cdot10^{-19}\\cdot3\\cdot10^6\\cdot0{,}5=3\\cdot10^{-13}\\) Н');
|
||||
|
||||
q(T.magnet,
|
||||
`Индукция поля прямого проводника с током 10 А на расстоянии 0,1 м (\\(\\mu_0=4\\pi\\cdot10^{-7}\\) Гн/м):`,
|
||||
[{t:'\\(2\\cdot10^{-5}\\) Тл',c:true},{t:'\\(4\\cdot10^{-5}\\) Тл',c:false},{t:'\\(10^{-4}\\) Тл',c:false},{t:'\\(2\\pi\\cdot10^{-5}\\) Тл',c:false},{t:'\\(10^{-5}\\) Тл',c:false}],
|
||||
2,2020,'\\(B=\\mu_0 I/(2\\pi r)=4\\pi\\cdot10^{-7}\\cdot10/(2\\pi\\cdot0{,}1)=2\\cdot10^{-5}\\) Тл');
|
||||
|
||||
q(T.magnet,
|
||||
`Протон (\\ m=1{,}67\\cdot10^{-27}\\) кг, \\(q=1{,}6\\cdot10^{-19}\\) Кл) движется со скоростью \\(10^7\\) м/с перпендикулярно \\(B=0{,}1\\) Тл. Радиус траектории:`,
|
||||
[{t:'\\(1{,}044\\) м',c:true},{t:'\\(0{,}1\\) м',c:false},{t:'\\(10\\) м',c:false},{t:'\\(0{,}5\\) м',c:false},{t:'\\(5\\) м',c:false}],
|
||||
3,2021,'\\(r=mv/(qB)=1{,}67\\cdot10^{-27}\\cdot10^7/(1{,}6\\cdot10^{-19}\\cdot0{,}1)\\approx1{,}044\\) м');
|
||||
|
||||
q(T.magnet,
|
||||
`Правило правой руки для проводника с током:`,
|
||||
[{t:'Направление линий поля вокруг проводника',c:true},{t:'Направление силы Ампера',c:false},{t:'Направление тока индукции',c:false},{t:'Направление скорости заряда',c:false},{t:'Направление ЭДС',c:false}],
|
||||
1,2022,'Правило правой руки (буравчика) — направление силовых линий поля тока');
|
||||
|
||||
q(T.magnet,
|
||||
`Магнитный поток через контур площадью \\(0{,}05\\) м² в поле \\(B=2\\) Тл при угле 30° к нормали:`,
|
||||
[{t:'\\(0{,}05\\sqrt{3}/2\\approx0{,}0866\\) Вб',c:false},{t:'\\(0{,}05\\) Вб',c:false},{t:'\\(\\sqrt{3}/20\\approx0{,}0866\\) Вб',c:true},{t:'\\(0{,}1\\) Вб',c:false},{t:'\\(0{,}025\\) Вб',c:false}],
|
||||
2,2022,'\\(\\Phi=BS\\cos30°=2\\cdot0{,}05\\cdot(\\sqrt{3}/2)=0{,}05\\sqrt{3}\\approx0{,}0866\\) Вб');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// ЭЛЕКТРОМАГНИТНАЯ ИНДУКЦИЯ (topic 37)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.emf,
|
||||
`Поток через контур изменился с 2 Вб до 0 Вб за 0,4 с. ЭДС индукции:`,
|
||||
[{t:'\\(5\\) В',c:true},{t:'\\(0{,}8\\) В',c:false},{t:'\\(50\\) В',c:false},{t:'\\(2{,}5\\) В',c:false},{t:'\\(0{,}2\\) В',c:false}],
|
||||
1,2018,'\\(\\varepsilon=|\\Delta\\Phi/\\Delta t|=2/0{,}4=5\\) В');
|
||||
|
||||
q(T.emf,
|
||||
`Катушка \\(L=0{,}2\\) Гн. Ток изменился с 3 А до 1 А за 0,1 с. ЭДС самоиндукции:`,
|
||||
[{t:'\\(4\\) В',c:true},{t:'\\(0{,}4\\) В',c:false},{t:'\\(40\\) В',c:false},{t:'\\(2\\) В',c:false},{t:'\\(0{,}1\\) В',c:false}],
|
||||
2,2018,'\\(\\varepsilon_L=L|\\Delta I/\\Delta t|=0{,}2\\cdot2/0{,}1=4\\) В');
|
||||
|
||||
q(T.emf,
|
||||
`Трансформатор: первичная обмотка 200 витков, вторичная 400 витков, напряжение первичной 100 В. Напряжение вторичной:`,
|
||||
[{t:'\\(200\\) В',c:true},{t:'\\(50\\) В',c:false},{t:'\\(400\\) В',c:false},{t:'\\(100\\) В',c:false},{t:'\\(800\\) В',c:false}],
|
||||
1,2019,'\\(U_2/U_1=n_2/n_1=2\\Rightarrow U_2=200\\) В');
|
||||
|
||||
q(T.emf,
|
||||
`Период колебаний контура \\(L=25\\cdot10^{-3}\\) Гн, \\(C=100\\cdot10^{-6}\\) Ф:`,
|
||||
[{t:'\\(\\pi\\cdot10^{-2}\\approx0{,}031\\) с',c:true},{t:'\\(0{,}1\\) с',c:false},{t:'\\(0{,}01\\) с',c:false},{t:'\\(0{,}314\\) с',c:false},{t:'\\(0{,}001\\) с',c:false}],
|
||||
3,2020,'\\(T=2\\pi\\sqrt{LC}=2\\pi\\cdot\\sqrt{25\\cdot10^{-3}\\cdot10^{-4}}=2\\pi\\cdot0{,}005=\\pi/100\\approx0{,}0314\\) с');
|
||||
|
||||
q(T.emf,
|
||||
`ЭДС переменного тока 311 В (амплитуда). Действующее значение:`,
|
||||
[{t:'\\(220\\) В',c:true},{t:'\\(311\\) В',c:false},{t:'\\(155{,}5\\) В',c:false},{t:'\\(440\\) В',c:false},{t:'\\(100\\) В',c:false}],
|
||||
1,2019,'\\(U=U_m/\\sqrt{2}\\approx311/1{,}414\\approx220\\) В');
|
||||
|
||||
q(T.emf,
|
||||
`Скорость распространения электромагнитных волн в вакууме:`,
|
||||
[{t:'\\(3\\cdot10^8\\) м/с',c:true},{t:'\\(3\\cdot10^5\\) м/с',c:false},{t:'\\(340\\) м/с',c:false},{t:'\\(1{,}5\\cdot10^8\\) м/с',c:false},{t:'\\(6\\cdot10^8\\) м/с',c:false}],
|
||||
1,2022,'Скорость света в вакууме \\(c=3\\cdot10^8\\) м/с');
|
||||
|
||||
q(T.emf,
|
||||
`Закон Ленца: ток индукции направлен так, чтобы:`,
|
||||
[{t:'Противодействовать изменению потока',c:true},{t:'Усиливать изменение потока',c:false},{t:'Создавать наибольший нагрев',c:false},{t:'Совпадать с направлением основного тока',c:false},{t:'Останавливать контур',c:false}],
|
||||
1,2018,'Закон Ленца: противодействие изменению потока');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// ОПТИКА (topic 38)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.optics,
|
||||
`Угол падения луча на зеркало 40°. Угол отражения:`,
|
||||
[{t:'\\(40°\\)',c:true},{t:'\\(80°\\)',c:false},{t:'\\(50°\\)',c:false},{t:'\\(20°\\)',c:false},{t:'\\(140°\\)',c:false}],
|
||||
1,2018,'Закон отражения: угол падения = угол отражения');
|
||||
|
||||
q(T.optics,
|
||||
`Предмет перед рассеивающей линзой с фокусом \\(F=30\\) см. Мнимое изображение находится:`,
|
||||
[{t:'Между фокусом и линзой с той же стороны',c:true},{t:'За фокусом с другой стороны',c:false},{t:'На расстоянии фокуса с другой стороны',c:false},{t:'В бесконечности',c:false},{t:'На расстоянии предмета',c:false}],
|
||||
2,2019,'Рассеивающая линза всегда даёт мнимое, уменьшенное, прямое изображение');
|
||||
|
||||
q(T.optics,
|
||||
`Показатель преломления стекла 1,5. Скорость света в стекле (\\(c=3\\cdot10^8\\) м/с):`,
|
||||
[{t:'\\(2\\cdot10^8\\) м/с',c:true},{t:'\\(4{,}5\\cdot10^8\\) м/с',c:false},{t:'\\(3\\cdot10^8\\) м/с',c:false},{t:'\\(1{,}5\\cdot10^8\\) м/с',c:false},{t:'\\(10^8\\) м/с',c:false}],
|
||||
1,2018,'\\(v=c/n=3\\cdot10^8/1{,}5=2\\cdot10^8\\) м/с');
|
||||
|
||||
q(T.optics,
|
||||
`Тонкая линза с \\(F=15\\) см. Предмет на расстоянии 45 см. Расстояние до изображения:`,
|
||||
[{t:'\\(22{,}5\\) см',c:true},{t:'\\(30\\) см',c:false},{t:'\\(15\\) см',c:false},{t:'\\(60\\) см',c:false},{t:'\\(90\\) см',c:false}],
|
||||
2,2019,'\\(1/v=1/F-1/u=1/15-1/(-45)\\)... Исправление: \\(1/v=1/F-1/d\\) при \\(d=45\\): \\(1/v=1/15-1/45=3/45-1/45=2/45\\Rightarrow v=22{,}5\\) см');
|
||||
|
||||
q(T.optics,
|
||||
`Дифракционная решётка: 2 мм содержит 500 штрихов. Период решётки:`,
|
||||
[{t:'\\(4\\cdot10^{-6}\\) м',c:true},{t:'\\(2\\cdot10^{-3}\\) м',c:false},{t:'\\(10^{-3}\\) м',c:false},{t:'\\(4\\cdot10^{-3}\\) м',c:false},{t:'\\(2\\cdot10^{-6}\\) м',c:false}],
|
||||
2,2020,'\\(d=2\\cdot10^{-3}/500=4\\cdot10^{-6}\\) м');
|
||||
|
||||
q(T.optics,
|
||||
`Интерференция наблюдается, если источники:`,
|
||||
[{t:'Когерентные',c:true},{t:'Мощные',c:false},{t:'Одинакового цвета',c:false},{t:'Близко расположенные',c:false},{t:'Движущиеся',c:false}],
|
||||
1,2022,'Условие интерференции — когерентность источников');
|
||||
|
||||
q(T.optics,
|
||||
`Явление, при котором свет, отражённый от поверхности, частично или полностью поляризован:`,
|
||||
[{t:'Поляризация',c:true},{t:'Дифракция',c:false},{t:'Интерференция',c:false},{t:'Дисперсия',c:false},{t:'Рефракция',c:false}],
|
||||
1,2019,'Отражение вызывает поляризацию — закон Брюстера');
|
||||
|
||||
q(T.optics,
|
||||
`Мнимое, увеличенное, прямое изображение даёт:`,
|
||||
[{t:'Собирающая линза при расположении предмета между \\(F\\) и линзой',c:true},{t:'Рассеивающая линза при любом расположении',c:false},{t:'Собирающая линза при \\(d>2F\\)',c:false},{t:'Плоское зеркало',c:false},{t:'Призма',c:false}],
|
||||
2,2023,'При \\(d<F\\) собирающая линза: лупа');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// КВАНТОВАЯ И ЯДЕРНАЯ ФИЗИКА (topic 39)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.quantum,
|
||||
`Фотон с длиной волны \\(500\\) нм. Энергия (\\(h=6{,}63\\cdot10^{-34}\\) Дж·с, \\(c=3\\cdot10^8\\) м/с):`,
|
||||
[{t:'\\(3{,}98\\cdot10^{-19}\\) Дж',c:true},{t:'\\(6{,}63\\cdot10^{-34}\\) Дж',c:false},{t:'\\(3\\cdot10^{8}\\) Дж',c:false},{t:'\\(10^{-19}\\) Дж',c:false},{t:'\\(2\\cdot10^{-19}\\) Дж',c:false}],
|
||||
2,2018,'\\(E=hc/\\lambda=6{,}63\\cdot10^{-34}\\cdot3\\cdot10^8/5\\cdot10^{-7}\\approx3{,}98\\cdot10^{-19}\\) Дж');
|
||||
|
||||
q(T.quantum,
|
||||
`При \\(\\beta^-\\)-распаде из ядра вылетает:`,
|
||||
[{t:'Электрон и антинейтрино',c:true},{t:'\\(\\alpha\\)-частица',c:false},{t:'Гамма-квант',c:false},{t:'Позитрон и нейтрино',c:false},{t:'Протон',c:false}],
|
||||
1,2018,'\\(\\beta^-\\): нейтрон → протон + электрон + антинейтрино');
|
||||
|
||||
q(T.quantum,
|
||||
`Ядро \\(^{24}_{11}\\)Na испытывает бета-минус-распад. Дочернее ядро:`,
|
||||
[{t:'\\(^{24}_{12}\\)Mg',c:true},{t:'\\(^{20}_{9}\\)F',c:false},{t:'\\(^{24}_{10}\\)Ne',c:false},{t:'\\(^{24}_{11}\\)Na',c:false},{t:'\\(^{23}_{11}\\)Na',c:false}],
|
||||
2,2019,'\\(\\beta^-\\): \\(Z+1=12\\) (Mg), \\(A=24\\)');
|
||||
|
||||
q(T.quantum,
|
||||
`Период полураспада 10 лет. Через 30 лет от \\(N_0\\) останется:`,
|
||||
[{t:'\\(N_0/8\\)',c:true},{t:'\\(N_0/4\\)',c:false},{t:'\\(N_0/16\\)',c:false},{t:'\\(N_0/3\\)',c:false},{t:'\\(N_0/6\\)',c:false}],
|
||||
1,2019,'3 периода: \\(N=N_0\\cdot(1/2)^3=N_0/8\\)');
|
||||
|
||||
q(T.quantum,
|
||||
`Ядерная реакция: \\(^{14}_7N + ^4_2He \\to X + ^1_1H\\). Ядро \\(X\\):`,
|
||||
[{t:'\\(^{17}_8O\\)',c:true},{t:'\\(^{18}_9F\\)',c:false},{t:'\\(^{15}_7N\\)',c:false},{t:'\\(^{13}_6C\\)',c:false},{t:'\\(^{17}_7N\\)',c:false}],
|
||||
2,2020,'\\(A=14+4-1=17\\), \\(Z=7+2-1=8\\) — кислород');
|
||||
|
||||
q(T.quantum,
|
||||
`Фотоэффект: красная граница — это:`,
|
||||
[{t:'Минимальная частота, при которой возможен фотоэффект',c:true},{t:'Максимальная длина волны, не вызывающая фотоэффект',c:false},{t:'Максимальная скорость фотоэлектронов',c:false},{t:'Цвет излучения металла',c:false},{t:'Порог энергии кванта',c:false}],
|
||||
1,2019,'Красная граница: \\(\\nu_0=A/h\\)');
|
||||
|
||||
q(T.quantum,
|
||||
`Энергия связи нуклонов в ядре — это:`,
|
||||
[{t:'Работа, необходимая для полного расщепления ядра на нуклоны',c:true},{t:'Энергия, выделяемая при слиянии ядер',c:false},{t:'Кинетическая энергия нуклонов',c:false},{t:'Энергия электронной оболочки',c:false},{t:'Энергия гамма-излучения',c:false}],
|
||||
1,2022,'Энергия связи = минимальная работа против ядерных сил');
|
||||
|
||||
q(T.quantum,
|
||||
`\\(\\alpha\\)-излучение отклоняется в магнитном поле. Это объясняется:`,
|
||||
[{t:'\\(\\alpha\\)-частица имеет заряд \\(+2e\\)',c:true},{t:'Большой массой',c:false},{t:'Высокой скоростью',c:false},{t:'Нейтральностью',c:false},{t:'Волновыми свойствами',c:false}],
|
||||
1,2020,'Сила Лоренца действует на движущийся заряд');
|
||||
|
||||
q(T.quantum,
|
||||
`В реакции деления тяжёлого ядра выделяется энергия вследствие:`,
|
||||
[{t:'Дефекта масс',c:true},{t:'Выброса фотонов',c:false},{t:'Нагрева нейтронов',c:false},{t:'Химических реакций',c:false},{t:'Давления радиации',c:false}],
|
||||
2,2023,'\\(E=\\Delta m c^2\\) — дефект масс');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// КОЛЕБАНИЯ И ВОЛНЫ (topic 40)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
q(T.waves,
|
||||
`Длина волны, если скорость 340 м/с, частота 85 Гц:`,
|
||||
[{t:'\\(4\\) м',c:true},{t:'\\(0{,}25\\) м',c:false},{t:'\\(28900\\) м',c:false},{t:'\\(2\\) м',c:false},{t:'\\(8\\) м',c:false}],
|
||||
1,2018,'\\(\\lambda=v/f=340/85=4\\) м');
|
||||
|
||||
q(T.waves,
|
||||
`Частота колебаний маятника: 5 колебаний за 10 с:`,
|
||||
[{t:'\\(0{,}5\\) Гц',c:true},{t:'\\(5\\) Гц',c:false},{t:'\\(2\\) Гц',c:false},{t:'\\(50\\) Гц',c:false},{t:'\\(0{,}2\\) Гц',c:false}],
|
||||
1,2018,'\\(f=N/t=5/10=0{,}5\\) Гц');
|
||||
|
||||
q(T.waves,
|
||||
`Период математического маятника зависит от:`,
|
||||
[{t:'Длины нити и \\(g\\)',c:true},{t:'Массы груза',c:false},{t:'Амплитуды колебаний',c:false},{t:'Материала нити',c:false},{t:'Плотности воздуха',c:false}],
|
||||
1,2019,'\\(T=2\\pi\\sqrt{L/g}\\)');
|
||||
|
||||
q(T.waves,
|
||||
`Скорость поперечных волн на струне зависит от:`,
|
||||
[{t:'Натяжения и линейной плотности струны',c:true},{t:'Только амплитуды',c:false},{t:'Частоты колебаний',c:false},{t:'Длины волны',c:false},{t:'Температуры',c:false}],
|
||||
2,2019,'\\(v=\\sqrt{T/\\mu}\\), где \\(T\\) — натяжение, \\(\\mu\\) — линейная масса');
|
||||
|
||||
q(T.waves,
|
||||
`При увеличении длины математического маятника в 4 раза период:`,
|
||||
[{t:'Увеличится в 2 раза',c:true},{t:'Уменьшится в 2 раза',c:false},{t:'Увеличится в 4 раза',c:false},{t:'Не изменится',c:false},{t:'Уменьшится в 4 раза',c:false}],
|
||||
1,2020,'\\(T\\sim\\sqrt{L}\\): \\(L\\) ×4 → \\(T\\) ×2');
|
||||
|
||||
q(T.waves,
|
||||
`Резонанс — это явление:`,
|
||||
[{t:'Резкое возрастание амплитуды вынужденных колебаний при совпадении частот',c:true},{t:'Затухание собственных колебаний',c:false},{t:'Отражение волн',c:false},{t:'Интерференция звука',c:false},{t:'Дифракция звука',c:false}],
|
||||
1,2020,'Резонанс: \\(\\omega_{вынужд}=\\omega_0\\)');
|
||||
|
||||
q(T.waves,
|
||||
`Звуковая волна — это волна:`,
|
||||
[{t:'Продольная',c:true},{t:'Поперечная',c:false},{t:'Электромагнитная',c:false},{t:'Световая',c:false},{t:'Стоячая',c:false}],
|
||||
1,2022,'В звуке колебания частиц вдоль направления распространения — продольная');
|
||||
|
||||
q(T.waves,
|
||||
`Эффект Доплера: источник звука удаляется от наблюдателя. Наблюдаемая частота:`,
|
||||
[{t:'Меньше, чем у источника',c:true},{t:'Больше',c:false},{t:'Равна частоте источника',c:false},{t:'Зависит от громкости',c:false},{t:'Равна нулю',c:false}],
|
||||
1,2022,'При удалении источника: \\(f<f_0\\)');
|
||||
|
||||
q(T.waves,
|
||||
`Стоячая волна образуется при:`,
|
||||
[{t:'Наложении двух одинаковых волн, распространяющихся навстречу',c:true},{t:'Отражении волны от свободного конца',c:false},{t:'Дифракции волны',c:false},{t:'Поляризации',c:false},{t:'Преломлении',c:false}],
|
||||
2,2023,'Суперпозиция двух бегущих волн, идущих навстречу');
|
||||
|
||||
q(T.waves,
|
||||
`Маятник с периодом 2 с. При какой длине нити (\\(g=10\\) м/с²)?`,
|
||||
[{t:'\\(1\\) м',c:true},{t:'\\(2\\) м',c:false},{t:'\\(0{,}5\\) м',c:false},{t:'\\(4\\) м',c:false},{t:'\\(0{,}25\\) м',c:false}],
|
||||
2,2019,'\\(L=g(T/2\\pi)^2=10\\cdot(2/2\\pi)^2=10/(\\pi^2)\\approx1\\) м');
|
||||
|
||||
}); // end transaction
|
||||
|
||||
run();
|
||||
console.log(`✓ Добавлено: ${added}, пропущено (дубли): ${skipped}`);
|
||||
@@ -0,0 +1,288 @@
|
||||
'use strict';
|
||||
const db = require('../src/db/db');
|
||||
const PHYS_ID = 4;
|
||||
const T = {
|
||||
kinem:29, dynam:30, cons:31, mol:32, thermo:33,
|
||||
electro:34, dc:35, magnet:36, emf:37, optics:38, quantum:39, waves:40,
|
||||
};
|
||||
const existingKeys = new Set(
|
||||
db.prepare('SELECT text FROM questions WHERE subject_id=4').all()
|
||||
.map(q=>q.text.slice(0,80).trim())
|
||||
);
|
||||
let added=0, skipped=0;
|
||||
const iQ=db.prepare(`INSERT INTO questions(subject_id,topic_id,text,type,difficulty,year,explanation)VALUES(?,?,?,?,?,?,?)`);
|
||||
const iO=db.prepare(`INSERT INTO options(question_id,text,is_correct,order_index)VALUES(?,?,?,?)`);
|
||||
function q(tid,text,opts,d,yr,ex,type='single'){
|
||||
const k=text.slice(0,80).trim();
|
||||
if(existingKeys.has(k)){skipped++;return;}
|
||||
existingKeys.add(k);
|
||||
const r=iQ.run(PHYS_ID,tid,text,type,d,yr||null,ex||null);
|
||||
opts.forEach((o,i)=>iO.run(r.lastInsertRowid,o.t,o.c?1:0,i));
|
||||
added++;
|
||||
}
|
||||
const run=db.transaction(()=>{
|
||||
|
||||
// ══ КИНЕМАТИКА ══
|
||||
q(T.kinem,`Тело брошено вертикально вверх с \\(v_0=25\\) м/с (\\(g=10\\) м/с²). Высота через 3 с:`,
|
||||
[{t:'\\(30\\) м',c:true},{t:'\\(25\\) м',c:false},{t:'\\(45\\) м',c:false},{t:'\\(15\\) м',c:false},{t:'\\(10\\) м',c:false}],2,2018,'\\(h=v_0t-gt^2/2=75-45=30\\) м');
|
||||
|
||||
q(T.kinem,`Скорость тела при равноускоренном движении: \\(a=5\\) м/с², \\(v_0=0\\), \\(t=6\\) с:`,
|
||||
[{t:'\\(30\\) м/с',c:true},{t:'\\(11\\) м/с',c:false},{t:'\\(5\\) м/с',c:false},{t:'\\(90\\) м/с',c:false},{t:'\\(1\\) м/с',c:false}],1,2018,'\\(v=at=5\\cdot6=30\\) м/с');
|
||||
|
||||
q(T.kinem,`Тело движется равномерно: за 2 мин прошло 600 м. Скорость в км/ч:`,
|
||||
[{t:'\\(18\\) км/ч',c:true},{t:'\\(300\\) км/ч',c:false},{t:'\\(5\\) км/ч',c:false},{t:'\\(5{,}4\\) км/ч',c:false},{t:'\\(10\\) км/ч',c:false}],1,2019,'\\(v=600/120=5\\) м/с \\(=18\\) км/ч');
|
||||
|
||||
q(T.kinem,`Камень бросают горизонтально со скоростью 20 м/с с высоты 20 м. Скорость при ударе (\\(g=10\\) м/с²):`,
|
||||
[{t:'\\(20\\sqrt{2}\\approx28{,}3\\) м/с',c:true},{t:'\\(20\\) м/с',c:false},{t:'\\(40\\) м/с',c:false},{t:'\\(20+20=40\\) м/с',c:false},{t:'\\(10\\) м/с',c:false}],3,2019,'\\(t=\\sqrt{2h/g}=2\\) с. \\(v_y=gt=20\\) м/с. \\(v=\\sqrt{20^2+20^2}=20\\sqrt2\\)');
|
||||
|
||||
q(T.kinem,`Ускорение при равномерном движении по окружности (\\(v=6\\) м/с, \\(R=9\\) м):`,
|
||||
[{t:'\\(4\\) м/с²',c:true},{t:'\\(54\\) м/с²',c:false},{t:'\\(1{,}5\\) м/с²',c:false},{t:'\\(3\\) м/с²',c:false},{t:'\\(2\\) м/с²',c:false}],1,2020,'\\(a_c=v^2/R=36/9=4\\) м/с²');
|
||||
|
||||
q(T.kinem,`Автомобиль тормозит с \\(a=5\\) м/с² от \\(v_0=30\\) м/с. Тормозной путь:`,
|
||||
[{t:'\\(90\\) м',c:true},{t:'\\(6\\) м',c:false},{t:'\\(150\\) м',c:false},{t:'\\(300\\) м',c:false},{t:'\\(45\\) м',c:false}],2,2020,'\\(s=v_0^2/(2a)=900/10=90\\) м');
|
||||
|
||||
q(T.kinem,`Тело падает вертикально. Через 4 с скорость (\\(g=10\\) м/с²):`,
|
||||
[{t:'\\(40\\) м/с',c:true},{t:'\\(80\\) м/с',c:false},{t:'\\(160\\) м/с',c:false},{t:'\\(20\\) м/с',c:false},{t:'\\(10\\) м/с',c:false}],1,2022,'\\(v=gt=40\\) м/с');
|
||||
|
||||
q(T.kinem,`Уравнение движения \\(x=10+4t-t^2\\). В какой момент тело остановится?`,
|
||||
[{t:'\\(t=2\\) с',c:true},{t:'\\(t=4\\) с',c:false},{t:'\\(t=10\\) с',c:false},{t:'\\(t=1\\) с',c:false},{t:'\\(t=5\\) с',c:false}],2,2019,'\\(v=4-2t=0\\Rightarrow t=2\\) с');
|
||||
|
||||
q(T.kinem,`Точка на краю колеса радиуса 0,3 м, вращающегося с \\(n=10\\) об/с. Линейная скорость:`,
|
||||
[{t:'\\(6\\pi\\) м/с',c:true},{t:'\\(3\\) м/с',c:false},{t:'\\(\\pi\\) м/с',c:false},{t:'\\(60\\pi\\) м/с',c:false},{t:'\\(20\\pi\\) м/с',c:false}],2,2023,'\\(v=2\\pi nR=2\\pi\\cdot10\\cdot0{,}3=6\\pi\\) м/с');
|
||||
|
||||
// ══ ДИНАМИКА ══
|
||||
q(T.dynam,`Если скорость тела равномерна, то:`,
|
||||
[{t:'Суммарная сила равна нулю',c:true},{t:'Масса тела нулевая',c:false},{t:'Силы трения нет',c:false},{t:'Тело не движется',c:false},{t:'Ускорение велико',c:false}],1,2018,'Первый закон Ньютона: равномерное движение ⟹ F⃗_рез=0');
|
||||
|
||||
q(T.dynam,`Тело на горизонтальной поверхности с \\(\\mu=0{,}4\\), \\(m=5\\) кг, \\(g=10\\) м/с². Сила трения покоя (максимальная):`,
|
||||
[{t:'\\(20\\) Н',c:true},{t:'\\(50\\) Н',c:false},{t:'\\(4\\) Н',c:false},{t:'\\(2\\) Н',c:false},{t:'\\(40\\) Н',c:false}],1,2018,'\\(f=\\mu mg=0{,}4\\cdot5\\cdot10=20\\) Н');
|
||||
|
||||
q(T.dynam,`Два тела (2 кг и 3 кг) связаны нитью через блок. Ускорение системы (\\(g=10\\) м/с²):`,
|
||||
[{t:'\\(2\\) м/с²',c:true},{t:'\\(5\\) м/с²',c:false},{t:'\\(1\\) м/с²',c:false},{t:'\\(10\\) м/с²',c:false},{t:'\\(3\\) м/с²',c:false}],2,2019,'\\(a=(m_2-m_1)g/(m_1+m_2)=(3-2)\\cdot10/5=2\\) м/с²');
|
||||
|
||||
q(T.dynam,`Сила гравитации Земли на спутник: Земля \\(M=6\\cdot10^{24}\\) кг, спутник \\(m=100\\) кг, \\(r=7\\cdot10^6\\) м, \\(G=6{,}67\\cdot10^{-11}\\):`,
|
||||
[{t:'\\(\\approx820\\) Н',c:true},{t:'\\(1000\\) Н',c:false},{t:'\\(500\\) Н',c:false},{t:'\\(1500\\) Н',c:false},{t:'\\(9800\\) Н',c:false}],2,2020,'\\(F=GMm/r^2=6{,}67\\cdot10^{-11}\\cdot6\\cdot10^{24}\\cdot100/(49\\cdot10^{12})\\approx820\\) Н');
|
||||
|
||||
q(T.dynam,`Какая сила удерживает автомобиль (\\(m=1500\\) кг) на повороте \\(R=50\\) м при \\(v=10\\) м/с?`,
|
||||
[{t:'\\(3000\\) Н',c:true},{t:'\\(300\\) Н',c:false},{t:'\\(30000\\) Н',c:false},{t:'\\(750\\) Н',c:false},{t:'\\(1500\\) Н',c:false}],2,2019,'\\(F=mv^2/R=1500\\cdot100/50=3000\\) Н');
|
||||
|
||||
q(T.dynam,`Вес человека (70 кг) в лифте, движущемся вверх с \\(a=2\\) м/с², \\(g=10\\) м/с²:`,
|
||||
[{t:'\\(840\\) Н',c:true},{t:'\\(700\\) Н',c:false},{t:'\\(560\\) Н',c:false},{t:'\\(140\\) Н',c:false},{t:'\\(980\\) Н',c:false}],2,2020,'\\(N=m(g+a)=70\\cdot12=840\\) Н');
|
||||
|
||||
q(T.dynam,`Тело на горизонтальной плоскости: сила 30 Н под углом 60° к горизонту. Горизонтальная составляющая:`,
|
||||
[{t:'\\(15\\) Н',c:true},{t:'\\(15\\sqrt3\\) Н',c:false},{t:'\\(26\\) Н',c:false},{t:'\\(30\\) Н',c:false},{t:'\\(10\\) Н',c:false}],2,2023,'\\(F_x=F\\cos60°=30\\cdot0{,}5=15\\) Н');
|
||||
|
||||
// ══ ЗАКОНЫ СОХРАНЕНИЯ ══
|
||||
q(T.cons,`Тело массой 3 кг движется со скоростью 4 м/с. Кинетическая энергия:`,
|
||||
[{t:'\\(24\\) Дж',c:true},{t:'\\(12\\) Дж',c:false},{t:'\\(48\\) Дж',c:false},{t:'\\(6\\) Дж',c:false},{t:'\\(36\\) Дж',c:false}],1,2018,'\\(E_k=mv^2/2=3\\cdot16/2=24\\) Дж');
|
||||
|
||||
q(T.cons,`При абсолютно упругом ударе сохраняются:`,
|
||||
[{t:'И импульс, и кинетическая энергия',c:true},{t:'Только импульс',c:false},{t:'Только энергия',c:false},{t:'Ни то, ни другое',c:false},{t:'Только скорости',c:false}],1,2018,'Определение упругого удара');
|
||||
|
||||
q(T.cons,`Тело массой 2 кг поднимают на высоту 5 м с постоянной скоростью (\\(g=10\\) м/с²). Работа силы тяги:`,
|
||||
[{t:'\\(100\\) Дж',c:true},{t:'\\(-100\\) Дж',c:false},{t:'\\(0\\) Дж',c:false},{t:'\\(50\\) Дж',c:false},{t:'\\(200\\) Дж',c:false}],1,2019,'\\(A=mgh=2\\cdot10\\cdot5=100\\) Дж');
|
||||
|
||||
q(T.cons,`Шар (1 кг, 3 м/с) и шар (2 кг, покой). Абсолютно неупругий удар. Скорость:`,
|
||||
[{t:'\\(1\\) м/с',c:true},{t:'\\(3\\) м/с',c:false},{t:'\\(1{,}5\\) м/с',c:false},{t:'\\(0{,}5\\) м/с',c:false},{t:'\\(2\\) м/с',c:false}],2,2019,'\\(mv_0=(m+M)v\\Rightarrow v=3/3=1\\) м/с');
|
||||
|
||||
q(T.cons,`Пружина с жёсткостью 200 Н/м сжата на 5 см. Потенциальная энергия:`,
|
||||
[{t:'\\(0{,}25\\) Дж',c:true},{t:'\\(10\\) Дж',c:false},{t:'\\(25\\) Дж',c:false},{t:'\\(5\\) Дж',c:false},{t:'\\(0{,}05\\) Дж',c:false}],1,2020,'\\(E=kx^2/2=200\\cdot(0{,}05)^2/2=0{,}25\\) Дж');
|
||||
|
||||
q(T.cons,`Машина мощностью 40 кВт движется равномерно. Сила тяги при скорости 20 м/с:`,
|
||||
[{t:'\\(2000\\) Н',c:true},{t:'\\(800000\\) Н',c:false},{t:'\\(200\\) Н',c:false},{t:'\\(20\\) Н',c:false},{t:'\\(8000\\) Н',c:false}],1,2020,'\\(F=P/v=40000/20=2000\\) Н');
|
||||
|
||||
q(T.cons,`Снаряд 10 кг летит горизонтально 300 м/с и разрывается. Первый осколок (6 кг) улетает со скоростью 500 м/с в том же направлении. Скорость второго (4 кг):`,
|
||||
[{t:'\\(0\\) м/с',c:true},{t:'\\(150\\) м/с',c:false},{t:'\\(-100\\) м/с',c:false},{t:'\\(-75\\) м/с',c:false},{t:'\\(100\\) м/с',c:false}],3,2022,'\\(10\\cdot300=6\\cdot500+4v_2\\Rightarrow3000=3000+4v_2\\Rightarrow v_2=0\\)');
|
||||
|
||||
// ══ МОЛЕКУЛЯРНАЯ ФИЗИКА ══
|
||||
q(T.mol,`Газ при 0°C и \\(2\\cdot10^5\\) Па занимает 3 л. Если его нагреть до 273°C при том же давлении, объём:`,
|
||||
[{t:'\\(6\\) л',c:true},{t:'\\(9\\) л',c:false},{t:'\\(4{,}5\\) л',c:false},{t:'\\(1{,}5\\) л',c:false},{t:'\\(3\\) л',c:false}],2,2018,'\\(V_2=V_1T_2/T_1=3\\cdot546/273=6\\) л');
|
||||
|
||||
q(T.mol,`Среднеквадратичная скорость молекул кислорода при 0°C (\\(M=0{,}032\\) кг/моль, \\(R=8{,}31\\)):`,
|
||||
[{t:'\\(461\\) м/с',c:true},{t:'\\(920\\) м/с',c:false},{t:'\\(230\\) м/с',c:false},{t:'\\(1500\\) м/с',c:false},{t:'\\(100\\) м/с',c:false}],3,2019,'\\(v_{кв}=\\sqrt{3RT/M}=\\sqrt{3\\cdot8{,}31\\cdot273/0{,}032}\\approx461\\) м/с');
|
||||
|
||||
q(T.mol,`Уравнение состояния идеального газа: \\(pV=\\nu RT\\). Что такое \\(R\\)?`,
|
||||
[{t:'Универсальная газовая постоянная (8,31 Дж/(моль·К))',c:true},{t:'Сопротивление',c:false},{t:'Радиус молекулы',c:false},{t:'Постоянная Авогадро',c:false},{t:'Постоянная Больцмана',c:false}],1,2018,'\\(R=8{,}31\\) Дж/(моль·К)');
|
||||
|
||||
q(T.mol,`При изотермическом сжатии газ объёма 4 л до 1 л при давлении 100 кПа. Новое давление:`,
|
||||
[{t:'\\(400\\) кПа',c:true},{t:'\\(25\\) кПа',c:false},{t:'\\(200\\) кПа',c:false},{t:'\\(100\\) кПа',c:false},{t:'\\(4000\\) кПа',c:false}],1,2019,'\\(p_1V_1=p_2V_2\\Rightarrow p_2=100\\cdot4/1=400\\) кПа');
|
||||
|
||||
q(T.mol,`Какой процесс изображён прямой линией на графике \\(p-T\\) при \\(V=const\\)?`,
|
||||
[{t:'Изохорный',c:true},{t:'Изобарный',c:false},{t:'Изотермный',c:false},{t:'Адиабатический',c:false},{t:'Политропный',c:false}],1,2020,'При \\(V=const\\): \\(p=\\nu R/V\\cdot T\\) — прямая через начало координат');
|
||||
|
||||
q(T.mol,`Концентрация молекул идеального газа связана с давлением и температурой:`,
|
||||
[{t:'\\(n=p/(kT)\\)',c:true},{t:'\\(n=kT/p\\)',c:false},{t:'\\(n=p\\cdot T/k\\)',c:false},{t:'\\(n=pk/T\\)',c:false},{t:'\\(n=RT/p\\)',c:false}],2,2022,'\\(p=nkT\\Rightarrow n=p/(kT)\\)');
|
||||
|
||||
q(T.mol,`Масса молекулы азота \\(N_2\\) (\\(M=28\\) г/моль):`,
|
||||
[{t:'\\(4{,}65\\cdot10^{-26}\\) кг',c:true},{t:'\\(28\\cdot10^{-3}\\) кг',c:false},{t:'\\(14\\cdot10^{-27}\\) кг',c:false},{t:'\\(4{,}65\\cdot10^{-23}\\) кг',c:false},{t:'\\(10^{-26}\\) кг',c:false}],2,2023,'\\(m=M/N_A=28\\cdot10^{-3}/(6{,}02\\cdot10^{23})\\approx4{,}65\\cdot10^{-26}\\) кг');
|
||||
|
||||
// ══ ТЕРМОДИНАМИКА ══
|
||||
q(T.thermo,`При изобарном расширении газ получает 500 Дж и совершает работу 300 Дж. Изменение внутренней энергии:`,
|
||||
[{t:'\\(200\\) Дж',c:true},{t:'\\(800\\) Дж',c:false},{t:'\\(-200\\) Дж',c:false},{t:'\\(300\\) Дж',c:false},{t:'\\(500\\) Дж',c:false}],1,2018,'\\(\\Delta U=Q-A=500-300=200\\) Дж');
|
||||
|
||||
q(T.thermo,`Теплота плавления льда: \\(m=200\\) г, \\(L=334\\) кДж/кг:`,
|
||||
[{t:'\\(66{,}8\\) кДж',c:true},{t:'\\(334\\) кДж',c:false},{t:'\\(167\\) кДж',c:false},{t:'\\(668\\) Дж',c:false},{t:'\\(6{,}68\\) кДж',c:false}],1,2019,'\\(Q=mL=0{,}2\\cdot334=66{,}8\\) кДж');
|
||||
|
||||
q(T.thermo,`КПД тепловой машины 0,25. При получении \\(Q_1=1000\\) Дж работа:`,
|
||||
[{t:'\\(250\\) Дж',c:true},{t:'\\(750\\) Дж',c:false},{t:'\\(4000\\) Дж',c:false},{t:'\\(25\\) Дж',c:false},{t:'\\(500\\) Дж',c:false}],1,2018,'\\(A=\\eta Q_1=0{,}25\\cdot1000=250\\) Дж');
|
||||
|
||||
q(T.thermo,`Нагреватель даёт 5000 Дж, холодильник получает 3500 Дж. КПД:`,
|
||||
[{t:'\\(30\\%\\)',c:true},{t:'\\(70\\%\\)',c:false},{t:'\\(50\\%\\)',c:false},{t:'\\(43\\%\\)',c:false},{t:'\\(0\\%\\)',c:false}],2,2019,'\\(A=5000-3500=1500\\). \\(\\eta=1500/5000=0{,}3=30\\%\\)');
|
||||
|
||||
q(T.thermo,`При адиабатическом сжатии газа температура:`,
|
||||
[{t:'Увеличивается',c:true},{t:'Уменьшается',c:false},{t:'Не изменяется',c:false},{t:'Сначала растёт, потом падает',c:false},{t:'Зависит от давления',c:false}],2,2022,'\\(Q=0\\), работа внешних сил → рост внутренней энергии → рост \\(T\\)');
|
||||
|
||||
q(T.thermo,`Второе начало термодинамики запрещает:`,
|
||||
[{t:'Самопроизвольный переход теплоты от холодного тела к горячему',c:true},{t:'Тепловой двигатель',c:false},{t:'Охлаждение газа',c:false},{t:'Нагрев тела',c:false},{t:'Работу при адиабате',c:false}],1,2023,'Второе начало: теплота сама по себе не переходит от холодного к горячему');
|
||||
|
||||
// ══ ЭЛЕКТРОСТАТИКА ══
|
||||
q(T.electro,`Напряжённость поля в точке между двумя равными и противоположными зарядами (направление):`,
|
||||
[{t:'От положительного к отрицательному',c:true},{t:'От отрицательного к положительному',c:false},{t:'Перпендикулярно оси',c:false},{t:'Равна нулю',c:false},{t:'Хаотична',c:false}],1,2018,'Линии поля выходят из \\(+\\) и входят в \\(-\\)');
|
||||
|
||||
q(T.electro,`Заряд \\(q=5\\) мкКл в однородном поле \\(E=4000\\) В/м. Сила на заряд:`,
|
||||
[{t:'\\(0{,}02\\) Н',c:true},{t:'\\(0{,}2\\) Н',c:false},{t:'\\(20\\) Н',c:false},{t:'\\(4005\\) Н',c:false},{t:'\\(0{,}8\\) Н',c:false}],1,2019,'\\(F=qE=5\\cdot10^{-6}\\cdot4000=0{,}02\\) Н');
|
||||
|
||||
q(T.electro,`Ёмкость плоского конденсатора увеличится, если:`,
|
||||
[{t:'Уменьшить расстояние между пластинами',c:true},{t:'Увеличить расстояние',c:false},{t:'Уменьшить площадь пластин',c:false},{t:'Удалить диэлектрик (воздух)',c:false},{t:'Уменьшить напряжение',c:false}],1,2019,'\\(C=\\varepsilon\\varepsilon_0 S/d\\): при уменьшении \\(d\\) ёмкость растёт');
|
||||
|
||||
q(T.electro,`Электрическое поле не существует:`,
|
||||
[{t:'Внутри проводника в статике',c:true},{t:'Между пластинами конденсатора',c:false},{t:'Вокруг точечного заряда',c:false},{t:'В диэлектрике',c:false},{t:'В вакууме',c:false}],1,2020,'В равновесии внутри проводника \\(E=0\\)');
|
||||
|
||||
q(T.electro,`Потенциал точки, если заряд \\(q=4\\cdot10^{-9}\\) Кл, \\(r=0{,}2\\) м (\\(k=9\\cdot10^9\\)):`,
|
||||
[{t:'\\(180\\) В',c:true},{t:'\\(1800\\) В',c:false},{t:'\\(18\\) В',c:false},{t:'\\(9\\) В',c:false},{t:'\\(90\\) В',c:false}],2,2022,'\\(\\varphi=kq/r=9\\cdot10^9\\cdot4\\cdot10^{-9}/0{,}2=180\\) В');
|
||||
|
||||
q(T.electro,`Энергия, запасённая в конденсаторе \\(C=8\\) мкФ при \\(U=500\\) В:`,
|
||||
[{t:'\\(1\\) Дж',c:true},{t:'\\(2\\) Дж',c:false},{t:'\\(0{,}5\\) Дж',c:false},{t:'\\(4000\\) Дж',c:false},{t:'\\(0{,}002\\) Дж',c:false}],2,2023,'\\(W=CU^2/2=8\\cdot10^{-6}\\cdot250000/2=1\\) Дж');
|
||||
|
||||
q(T.electro,`Заряд конденсатора 30 мкКл при напряжении 150 В. Ёмкость:`,
|
||||
[{t:'\\(0{,}2\\) мкФ',c:true},{t:'\\(4500\\) мкФ',c:false},{t:'\\(180\\) мкФ',c:false},{t:'\\(5\\) мкФ',c:false},{t:'\\(2\\) мкФ',c:false}],1,2018,'\\(C=q/U=30\\cdot10^{-6}/150=0{,}2\\cdot10^{-6}\\) Ф = 0,2 мкФ');
|
||||
|
||||
// ══ ПОСТОЯННЫЙ ТОК ══
|
||||
q(T.dc,`Напряжение 6 В, сопротивление 3 Ом. Ток:`,
|
||||
[{t:'\\(2\\) А',c:true},{t:'\\(18\\) А',c:false},{t:'\\(0{,}5\\) А',c:false},{t:'\\(3\\) А',c:false},{t:'\\(9\\) А',c:false}],1,2018,'\\(I=U/R=6/3=2\\) А');
|
||||
|
||||
q(T.dc,`Три резистора 2 Ом, 4 Ом, 6 Ом включены последовательно. Общее сопротивление:`,
|
||||
[{t:'\\(12\\) Ом',c:true},{t:'\\(4\\) Ом',c:false},{t:'\\(\\frac{4}{3}\\) Ом',c:false},{t:'\\(24\\) Ом',c:false},{t:'\\(1\\) Ом',c:false}],1,2018,'\\(R=2+4+6=12\\) Ом');
|
||||
|
||||
q(T.dc,`Три резистора 6 Ом параллельно. Общее сопротивление:`,
|
||||
[{t:'\\(2\\) Ом',c:true},{t:'\\(18\\) Ом',c:false},{t:'\\(6\\) Ом',c:false},{t:'\\(3\\) Ом',c:false},{t:'\\(0{,}5\\) Ом',c:false}],1,2019,'\\(R=6/3=2\\) Ом');
|
||||
|
||||
q(T.dc,`Источник: ЭДС=9 В, \\(r=0{,}5\\) Ом, \\(R=4{,}5\\) Ом. Напряжение на внешнем резисторе:`,
|
||||
[{t:'\\(8{,}1\\) В',c:true},{t:'\\(9\\) В',c:false},{t:'\\(7{,}2\\) В',c:false},{t:'\\(4{,}5\\) В',c:false},{t:'\\(0{,}9\\) В',c:false}],2,2019,'\\(I=9/5=1{,}8\\) А, \\(U=IR=1{,}8\\cdot4{,}5=8{,}1\\) В');
|
||||
|
||||
q(T.dc,`Проводник медный, длина 50 м, сечение 2 мм² (\\(\\rho=1{,}7\\cdot10^{-8}\\)):`,
|
||||
[{t:'\\(0{,}425\\) Ом',c:true},{t:'\\(4{,}25\\) Ом',c:false},{t:'\\(42{,}5\\) Ом',c:false},{t:'\\(0{,}0425\\) Ом',c:false},{t:'\\(17\\) Ом',c:false}],2,2020,'\\(R=\\rho l/S=1{,}7\\cdot10^{-8}\\cdot50/(2\\cdot10^{-6})=0{,}425\\) Ом');
|
||||
|
||||
q(T.dc,`Ток через нить лампы 0,5 А, сопротивление 800 Ом. Мощность лампы:`,
|
||||
[{t:'\\(200\\) Вт',c:true},{t:'\\(400\\) Вт',c:false},{t:'\\(100\\) Вт',c:false},{t:'\\(1600\\) Вт',c:false},{t:'\\(1{,}6\\) кВт',c:false}],1,2020,'\\(P=I^2R=0{,}25\\cdot800=200\\) Вт');
|
||||
|
||||
q(T.dc,`ЭДС источника 12 В, внутреннее сопротивление 2 Ом. При коротком замыкании ток:`,
|
||||
[{t:'\\(6\\) А',c:true},{t:'\\(12\\) А',c:false},{t:'\\(24\\) А',c:false},{t:'\\(0\\) А',c:false},{t:'\\(14\\) А',c:false}],2,2022,'\\(I_{кз}=\\varepsilon/r=12/2=6\\) А');
|
||||
|
||||
q(T.dc,`Мощность источника, отдаваемая во внешнюю цепь, максимальна при:`,
|
||||
[{t:'\\(R=r\\)',c:true},{t:'\\(R=0\\)',c:false},{t:'\\(R\\to\\infty\\)',c:false},{t:'\\(R=2r\\)',c:false},{t:'\\(R=r/2\\)',c:false}],3,2023,'Теорема о максимальной мощности: \\(R_{внеш}=r\\)');
|
||||
|
||||
// ══ МАГНЕТИЗМ ══
|
||||
q(T.magnet,`Электрон (\\(q=1{,}6\\cdot10^{-19}\\) Кл) влетает со скоростью \\(2\\cdot10^6\\) м/с в поле \\(B=0{,}1\\) Тл. Сила Лоренца:`,
|
||||
[{t:'\\(3{,}2\\cdot10^{-14}\\) Н',c:true},{t:'\\(3{,}2\\cdot10^{-13}\\) Н',c:false},{t:'\\(1{,}6\\cdot10^{-14}\\) Н',c:false},{t:'\\(0{,}32\\) Н',c:false},{t:'\\(10^{-14}\\) Н',c:false}],2,2018,'\\(F=qvB=1{,}6\\cdot10^{-19}\\cdot2\\cdot10^6\\cdot0{,}1=3{,}2\\cdot10^{-14}\\) Н');
|
||||
|
||||
q(T.magnet,`Если сила Ампера максимальна, угол между током и полем:`,
|
||||
[{t:'\\(90°\\)',c:true},{t:'\\(0°\\)',c:false},{t:'\\(45°\\)',c:false},{t:'\\(180°\\)',c:false},{t:'\\(60°\\)',c:false}],1,2018,'\\(F=BIl\\sin\\alpha\\), максимум при \\(\\alpha=90°\\)');
|
||||
|
||||
q(T.magnet,`Катушка с 100 витками, площадь \\(0{,}01\\) м², в поле \\(B=2\\) Тл. Магнитный поток через катушку:`,
|
||||
[{t:'\\(2\\) Вб',c:true},{t:'\\(0{,}02\\) Вб',c:false},{t:'\\(200\\) Вб',c:false},{t:'\\(0{,}2\\) Вб',c:false},{t:'\\(100\\) Вб',c:false}],2,2019,'\\(\\Phi=NBS=100\\cdot2\\cdot0{,}01=2\\) Вб');
|
||||
|
||||
q(T.magnet,`Проводник 2 м с током 5 А в поле 0,4 Тл, угол 30°. Сила Ампера:`,
|
||||
[{t:'\\(2\\) Н',c:true},{t:'\\(4\\) Н',c:false},{t:'\\(1\\) Н',c:false},{t:'\\(3{,}46\\) Н',c:false},{t:'\\(8\\) Н',c:false}],2,2019,'\\(F=BIl\\sin30°=0{,}4\\cdot5\\cdot2\\cdot0{,}5=2\\) Н');
|
||||
|
||||
q(T.magnet,`В каком случае сила Лоренца не действует на заряженную частицу?`,
|
||||
[{t:'Частица движется вдоль поля',c:true},{t:'Частица движется перпендикулярно полю',c:false},{t:'Частица движется под углом 45°',c:false},{t:'Частица в покое в поле',c:false},{t:'При высокой скорости',c:false}],1,2020,'\\(F=qvB\\sin\\alpha=0\\) при \\(\\alpha=0°\\) (вдоль поля)');
|
||||
|
||||
q(T.magnet,`Катушка с \\(L=0{,}1\\) Гн несёт ток 3 А. Энергия магнитного поля катушки:`,
|
||||
[{t:'\\(0{,}45\\) Дж',c:true},{t:'\\(0{,}3\\) Дж',c:false},{t:'\\(0{,}9\\) Дж',c:false},{t:'\\(3\\) Дж',c:false},{t:'\\(0{,}15\\) Дж',c:false}],2,2022,'\\(W=LI^2/2=0{,}1\\cdot9/2=0{,}45\\) Дж');
|
||||
|
||||
// ══ ЭЛЕКТРОМАГНИТНАЯ ИНДУКЦИЯ ══
|
||||
q(T.emf,`Правило Ленца: при усилении внешнего магнитного потока через контур, ток индукции:`,
|
||||
[{t:'Противодействует усилению потока (создаёт поле против внешнего)',c:true},{t:'Усиливает внешний поток',c:false},{t:'Не возникает',c:false},{t:'Зависит от материала контура',c:false},{t:'Направлен по полю',c:false}],1,2018,'Закон Ленца — электромагнитная инерция');
|
||||
|
||||
q(T.emf,`Прямолинейный проводник длиной 0,5 м движется со скоростью 4 м/с перпендикулярно полю 2 Тл. ЭДС:`,
|
||||
[{t:'\\(4\\) В',c:true},{t:'\\(0{,}25\\) В',c:false},{t:'\\(16\\) В',c:false},{t:'\\(1\\) В',c:false},{t:'\\(8\\) В',c:false}],2,2019,'\\(\\varepsilon=Blv=2\\cdot0{,}5\\cdot4=4\\) В');
|
||||
|
||||
q(T.emf,`Трансформатор 500/50 витков подключён к 220 В. Вторичное напряжение:`,
|
||||
[{t:'\\(22\\) В',c:true},{t:'\\(2200\\) В',c:false},{t:'\\(44\\) В',c:false},{t:'\\(11\\) В',c:false},{t:'\\(110\\) В',c:false}],1,2019,'\\(U_2=U_1\\cdot n_2/n_1=220\\cdot50/500=22\\) В');
|
||||
|
||||
q(T.emf,`Частота переменного тока 50 Гц. Период:`,
|
||||
[{t:'\\(0{,}02\\) с',c:true},{t:'\\(50\\) с',c:false},{t:'\\(2\\) с',c:false},{t:'\\(0{,}05\\) с',c:false},{t:'\\(100\\) с',c:false}],1,2020,'\\(T=1/f=1/50=0{,}02\\) с');
|
||||
|
||||
q(T.emf,`Длина радиоволны при частоте 90 МГц:`,
|
||||
[{t:'\\(3{,}33\\) м',c:true},{t:'\\(0{,}3\\) м',c:false},{t:'\\(33{,}3\\) м',c:false},{t:'\\(300\\) м',c:false},{t:'\\(3000\\) м',c:false}],2,2022,'\\(\\lambda=c/f=3\\cdot10^8/(90\\cdot10^6)=10/3\\approx3{,}33\\) м');
|
||||
|
||||
q(T.emf,`Действующее значение тока \\(i=10\\sin(314t)\\) А:`,
|
||||
[{t:'\\(\\frac{10}{\\sqrt{2}}\\approx7{,}07\\) А',c:true},{t:'\\(10\\) А',c:false},{t:'\\(5\\) А',c:false},{t:'\\(20\\) А',c:false},{t:'\\(314\\) А',c:false}],2,2023,'\\(I=I_m/\\sqrt2=10/\\sqrt2\\approx7{,}07\\) А');
|
||||
|
||||
// ══ ОПТИКА ══
|
||||
q(T.optics,`Угол падения луча на границу стекло-воздух равен критическому (\\(n=1{,}5\\)). Угол преломления:`,
|
||||
[{t:'\\(90°\\)',c:true},{t:'\\(45°\\)',c:false},{t:'\\(0°\\)',c:false},{t:'\\(60°\\)',c:false},{t:'\\(30°\\)',c:false}],2,2018,'При полном внутреннем отражении преломлённый луч идёт вдоль границы: \\(\\theta_2=90°\\)');
|
||||
|
||||
q(T.optics,`Рассеивающая линза с \\(f=-20\\) см. Оптическая сила:`,
|
||||
[{t:'\\(-5\\) дптр',c:true},{t:'\\(5\\) дптр',c:false},{t:'\\(-0{,}2\\) дптр',c:false},{t:'\\(20\\) дптр',c:false},{t:'\\(0{,}5\\) дптр',c:false}],1,2018,'\\(D=1/f=1/(-0{,}2)=-5\\) дптр (фокус в метрах)');
|
||||
|
||||
q(T.optics,`Предмет на расстоянии \\(2F\\) от собирающей линзы. Изображение:`,
|
||||
[{t:'Действительное, равное, перевёрнутое на \\(2F\\)',c:true},{t:'Мнимое, увеличенное',c:false},{t:'В фокусе',c:false},{t:'В бесконечности',c:false},{t:'Перед линзой',c:false}],2,2019,'При \\(d=2F\\): \\(v=2F\\), увеличение 1, перевёрнутое');
|
||||
|
||||
q(T.optics,`Фотон видимого света имеет наибольшую энергию при:`,
|
||||
[{t:'Фиолетовом цвете',c:true},{t:'Красном',c:false},{t:'Жёлтом',c:false},{t:'Зелёном',c:false},{t:'Синем',c:false}],1,2019,'\\(E=h\\nu\\): наибольшая \\(\\nu\\) у фиолетового (наименьшая длина волны)');
|
||||
|
||||
q(T.optics,`Луч из воды (\\(n=1{,}33\\)) выходит в воздух под углом 30° к нормали. Угол преломления:`,
|
||||
[{t:'\\(\\approx41{,}7°\\)',c:true},{t:'\\(30°\\)',c:false},{t:'\\(60°\\)',c:false},{t:'\\(90°\\)',c:false},{t:'\\(20°\\)',c:false}],2,2020,'\\(\\sin\\theta_2=n\\sin30°=1{,}33\\cdot0{,}5=0{,}665\\Rightarrow\\theta_2\\approx41{,}7°\\)');
|
||||
|
||||
q(T.optics,`Дифракционная решётка: период 2 мкм, длина волны 500 нм, 1-й максимум:`,
|
||||
[{t:'\\(\\sin\\theta=0{,}25\\)',c:true},{t:'\\(\\theta=30°\\)',c:false},{t:'\\(\\sin\\theta=0{,}5\\)',c:false},{t:'\\(\\theta=15°\\)',c:false},{t:'\\(\\theta=45°\\)',c:false}],2,2023,'\\(d\\sin\\theta=m\\lambda\\Rightarrow\\sin\\theta=500\\cdot10^{-9}/(2\\cdot10^{-6})=0{,}25\\)');
|
||||
|
||||
// ══ КВАНТОВАЯ И ЯДЕРНАЯ ФИЗИКА ══
|
||||
q(T.quantum,`Постулат Бора: атом излучает при переходе электрона:`,
|
||||
[{t:'С более высокого уровня на более низкий',c:true},{t:'С нижнего на верхний',c:false},{t:'При любом движении',c:false},{t:'При ускорении',c:false},{t:'В магнитном поле',c:false}],1,2018,'Второй постулат Бора: излучение при переходе вниз');
|
||||
|
||||
q(T.quantum,`Закон радиоактивного распада: \\(N=N_0\\cdot2^{-t/T_{1/2}}\\). Через \\(T_{1/2}\\) останется:`,
|
||||
[{t:'\\(N_0/2\\)',c:true},{t:'\\(N_0\\)',c:false},{t:'\\(N_0/4\\)',c:false},{t:'\\(0\\)',c:false},{t:'\\(2N_0\\)',c:false}],1,2018,'По определению периода полураспада');
|
||||
|
||||
q(T.quantum,`\\(\\gamma\\)-излучение — это:`,
|
||||
[{t:'Коротковолновое электромагнитное излучение',c:true},{t:'Поток электронов',c:false},{t:'Поток протонов',c:false},{t:'Поток нейтронов',c:false},{t:'Поток \\(\\alpha\\)-частиц',c:false}],1,2019,'\\(\\gamma\\)-кванты — фотоны высокой энергии');
|
||||
|
||||
q(T.quantum,`Реакция синтеза: \\(^2_1H+^3_1H\\to X+^1_0n\\). Ядро \\(X\\):`,
|
||||
[{t:'\\(^4_2He\\)',c:true},{t:'\\(^3_1H\\)',c:false},{t:'\\(^4_1H\\)',c:false},{t:'\\(^5_2He\\)',c:false},{t:'\\(^3_2He\\)',c:false}],2,2019,'\\(A=2+3-1=4\\), \\(Z=1+1-0=2\\) — гелий');
|
||||
|
||||
q(T.quantum,`Работа выхода электрона 3,36 эВ (\\(h=6{,}63\\cdot10^{-34}\\)). Красная граница:`,
|
||||
[{t:'\\(\\approx370\\) нм',c:true},{t:'\\(600\\) нм',c:false},{t:'\\(700\\) нм',c:false},{t:'\\(250\\) нм',c:false},{t:'\\(500\\) нм',c:false}],3,2020,'\\(\\nu_0=A/h=3{,}36\\cdot1{,}6\\cdot10^{-19}/(6{,}63\\cdot10^{-34})\\approx8{,}1\\cdot10^{14}\\) Гц. \\(\\lambda=c/\\nu\\approx370\\) нм');
|
||||
|
||||
q(T.quantum,`Число нейтронов в ядре \\(^{235}_{92}U\\):`,
|
||||
[{t:'\\(143\\)',c:true},{t:'\\(92\\)',c:false},{t:'\\(235\\)',c:false},{t:'\\(327\\)',c:false},{t:'\\(147\\)',c:false}],1,2022,'\\(N=A-Z=235-92=143\\)');
|
||||
|
||||
q(T.quantum,`Линейчатый спектр испускания атома водорода объясняет:`,
|
||||
[{t:'Теория Бора: квантованные переходы',c:true},{t:'Тепловое излучение',c:false},{t:'Рассеяние фотонов',c:false},{t:'Эффект Комптона',c:false},{t:'Радиоактивность',c:false}],1,2023,'Постулаты Бора объясняют дискретный спектр');
|
||||
|
||||
// ══ КОЛЕБАНИЯ И ВОЛНЫ ══
|
||||
q(T.waves,`Уравнение колебания: \\(x=5\\cos(2\\pi t)\\) см. Период:`,
|
||||
[{t:'\\(1\\) с',c:true},{t:'\\(2\\pi\\) с',c:false},{t:'\\(0{,}5\\) с',c:false},{t:'\\(5\\) с',c:false},{t:'\\(10\\pi\\) с',c:false}],1,2018,'\\(\\omega=2\\pi/T\\Rightarrow T=1\\) с');
|
||||
|
||||
q(T.waves,`Звуковая волна 340 м/с, длина 0,85 м. Частота:`,
|
||||
[{t:'\\(400\\) Гц',c:true},{t:'\\(289\\) Гц',c:false},{t:'\\(4000\\) Гц',c:false},{t:'\\(85\\) Гц',c:false},{t:'\\(850\\) Гц',c:false}],1,2018,'\\(f=v/\\lambda=340/0{,}85=400\\) Гц');
|
||||
|
||||
q(T.waves,`Колебательный контур: \\(L=4\\) мГн, \\(C=4\\) нФ. Резонансная частота:`,
|
||||
[{t:'\\(\\frac{1}{4\\pi}\\cdot10^6\\approx79{,}6\\) кГц',c:true},{t:'\\(1\\) МГц',c:false},{t:'\\(250\\) кГц',c:false},{t:'\\(4\\) МГц',c:false},{t:'\\(0{,}25\\) МГц',c:false}],3,2019,'\\(f=1/(2\\pi\\sqrt{LC})=1/(2\\pi\\sqrt{16\\cdot10^{-12}})=1/(2\\pi\\cdot4\\cdot10^{-6})\\approx39{,}8\\) кГц');
|
||||
|
||||
q(T.waves,`Маятник длиной 0,25 м (\\(g=10\\) м/с²). Частота колебаний:`,
|
||||
[{t:'\\(1\\) Гц',c:true},{t:'\\(2\\) Гц',c:false},{t:'\\(\\pi\\) Гц',c:false},{t:'\\(0{,}5\\) Гц',c:false},{t:'\\(4\\) Гц',c:false}],2,2019,'\\(T=2\\pi\\sqrt{0{,}25/10}=\\pi/\\sqrt{10}\\approx1\\) с, \\(f\\approx1\\) Гц');
|
||||
|
||||
q(T.waves,`Вынужденные колебания: амплитуда максимальна при:`,
|
||||
[{t:'Резонансе (частота внешней силы = собственной)',c:true},{t:'Максимальном затухании',c:false},{t:'Нулевой скорости',c:false},{t:'Увеличении массы',c:false},{t:'Любой частоте',c:false}],1,2020,'Определение резонанса');
|
||||
|
||||
q(T.waves,`Скорость распространения волн не зависит от:`,
|
||||
[{t:'Амплитуды',c:true},{t:'Среды',c:false},{t:'Температуры среды',c:false},{t:'Давления (для газов)',c:false},{t:'Типа волны',c:false}],1,2022,'Скорость волны — свойство среды, не зависит от амплитуды');
|
||||
|
||||
q(T.waves,`Интенсивность звука пропорциональна квадрату:`,
|
||||
[{t:'Амплитуды',c:true},{t:'Длины волны',c:false},{t:'Частоты',c:false},{t:'Скорости звука',c:false},{t:'Плотности среды',c:false}],2,2023,'\\(I\\sim A^2\\)');
|
||||
|
||||
});
|
||||
run();
|
||||
console.log(`Физика ✓ Добавлено: ${added}, пропущено: ${skipped}`);
|
||||
@@ -648,6 +648,21 @@ function _dbTables() {
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
// Активные проверки: отклик БД (мс) и тест записи на диск рядом с БД.
|
||||
function _runChecks() {
|
||||
let dbPingMs = null, dbOk = false;
|
||||
try { const t = process.hrtime.bigint(); db.prepare('SELECT 1 AS ok').get(); dbPingMs = Number(process.hrtime.bigint() - t) / 1e6; dbOk = true; } catch {}
|
||||
let diskWritable = false;
|
||||
try {
|
||||
const f = path.join(path.dirname(path.resolve(DB_PATH)), '.health-write-test');
|
||||
fs.writeFileSync(f, 'ok'); fs.unlinkSync(f); diskWritable = true;
|
||||
} catch {}
|
||||
return { dbPingMs, dbOk, diskWritable };
|
||||
}
|
||||
const _recentErrStmt = db.prepare(
|
||||
"SELECT id, level, message, route, method, created_at FROM error_log ORDER BY id DESC LIMIT 8"
|
||||
);
|
||||
|
||||
function getHealth(_req, res) {
|
||||
const uptimeSec = process.uptime();
|
||||
const mem = process.memoryUsage();
|
||||
@@ -661,7 +676,7 @@ function getHealth(_req, res) {
|
||||
|
||||
const totalMem = os.totalmem(), freeMem = os.freemem();
|
||||
const memPercent = totalMem ? (totalMem - freeMem) / totalMem : 0;
|
||||
const eventLoopLagMs = Number.isFinite(_eluMonitor.mean) ? _eluMonitor.mean / 1e6 : 0;
|
||||
const eventLoopLagMs = _eluMonitor.mean / 1e6;
|
||||
|
||||
const totalUsers = db.prepare('SELECT COUNT(*) AS n FROM users').get().n;
|
||||
const todaySessions = db.prepare("SELECT COUNT(*) AS n FROM test_sessions WHERE started_at >= date('now')").get().n;
|
||||
@@ -672,6 +687,10 @@ function getHealth(_req, res) {
|
||||
let sseStats = { users: 0, guests: 0, connections: 0 };
|
||||
try { sseStats = sse.stats(); } catch {}
|
||||
|
||||
// Активные health-проверки (Level 4): отклик БД и запись на диск.
|
||||
const checks = _runChecks();
|
||||
const recentErrorList = (() => { try { return _recentErrStmt.all(); } catch { return []; } })();
|
||||
|
||||
// Вердикт здоровья по порогам.
|
||||
const reasons = [];
|
||||
let status = 'ok';
|
||||
@@ -688,9 +707,13 @@ function getHealth(_req, res) {
|
||||
if (eventLoopLagMs > 200) crit(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`);
|
||||
else if (eventLoopLagMs > 70) warn(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`);
|
||||
if (dbSizeBytes > 1.5e9) warn('БД >1.5 ГБ');
|
||||
if (!checks.dbOk) crit('БД недоступна');
|
||||
if (!checks.diskWritable) crit('Диск недоступен для записи');
|
||||
if (checks.dbPingMs != null && checks.dbPingMs > 100) warn(`Медленный отклик БД ${checks.dbPingMs.toFixed(0)} мс`);
|
||||
|
||||
res.json({
|
||||
status, reasons,
|
||||
checks, recentErrorList,
|
||||
uptime: uptimeSec,
|
||||
startedAt: new Date(Date.now() - uptimeSec * 1000).toISOString(),
|
||||
memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal },
|
||||
@@ -713,6 +736,12 @@ function getHealth(_req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ── GET /api/admin/metrics — метрики HTTP-запросов (Level 2) ──────────── */
|
||||
const metrics = require('../utils/metrics');
|
||||
function getMetrics(_req, res) {
|
||||
res.json(metrics.snapshot());
|
||||
}
|
||||
|
||||
/* ── Topics CRUD ─────────────────────────────────────────────────────── */
|
||||
function getTopics(req, res) {
|
||||
const { subject_id } = req.query;
|
||||
@@ -802,7 +831,7 @@ module.exports = {
|
||||
getUsers, updateRole, getUserSessions, getAllSessions, getSessionDetail,
|
||||
clearUserSessions, deleteSession, updateUser, banUser, deleteUser,
|
||||
getFeatures, updateFeatures, getFreeStudentFeatures, updateFreeStudentFeatures,
|
||||
getAuditLog, clearAuditLog, getErrorLog, clearErrorLog, getHealth,
|
||||
getAuditLog, clearAuditLog, getErrorLog, clearErrorLog, getHealth, getMetrics,
|
||||
getTopics, createTopic, updateTopic, deleteTopic,
|
||||
broadcast,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
-- Chemistry 7 hub migration.
|
||||
-- Creates chemistry-7 as a full hub textbook (4 chapters) in the style of chemistry-8:
|
||||
-- chemistry-7 (hub, html_path = chemistry_7_hub.html)
|
||||
-- chemistry-7-ch1 (Первоначальные химические понятия, §§1–12) → chemistry_7_ch1.html
|
||||
-- chemistry-7-ch2 (Кислород, §§13–17) → chemistry_7_ch2.html
|
||||
-- chemistry-7-ch3 (Водород, §§18–22) → chemistry_7_ch3.html
|
||||
-- chemistry-7-ch4 (Вода, §§23–26) → chemistry_7_ch4.html
|
||||
--
|
||||
-- Source: Шиманович И. Е., Красицкий В. А., Сечко О. И., Хвалюк В. Н.,
|
||||
-- «Химия 7», Народная асвета, 2023 (2-е изд.). Контент авторский (наш).
|
||||
-- Author left empty per project policy.
|
||||
|
||||
-- 1. Insert the parent chemistry-7 hub row (does not exist yet in the catalog).
|
||||
INSERT OR IGNORE INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||
VALUES
|
||||
('chemistry-7', 'chemistry', 7, 'Химия — 7 класс',
|
||||
'',
|
||||
'Первый курс химии: первоначальные химические понятия (вещество, атом, элемент, молекула, формула, валентность, химическая реакция и уравнение), кислород и оксиды, водород, кислоты и соли, вода и основания, реакция нейтрализации. 4 главы, 26 параграфов, 5 лабораторных опытов, 4 практические работы.',
|
||||
'chemistry_7_hub.html', 26, 'emerald', 7, 1, NULL);
|
||||
|
||||
-- 2. Insert the 4 children (chapters).
|
||||
INSERT OR IGNORE INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||
VALUES
|
||||
('chemistry-7-ch1', 'chemistry', 7, 'Химия 7 · Первоначальные химические понятия',
|
||||
'',
|
||||
'§§1–12: химия как наука о веществах, чистые вещества и смеси, атомы и химические элементы, относительная атомная масса, молекулы, простые и сложные вещества, химическая формула и относительная молекулярная масса, валентность, физические и химические явления, закон сохранения массы и химические уравнения. Лабораторный опыт 1, практическая работа 1.',
|
||||
'chemistry_7_ch1.html', 12, 'emerald', 1, 1, 'chemistry-7'),
|
||||
('chemistry-7-ch2', 'chemistry', 7, 'Химия 7 · Кислород',
|
||||
'',
|
||||
'§§13–17: воздух как смесь газов, кислород как химический элемент и простое вещество, химические свойства кислорода и горение, оксиды, получение кислорода и катализаторы. Лабораторный опыт 2, практическая работа 2.',
|
||||
'chemistry_7_ch2.html', 5, 'cyan', 2, 1, 'chemistry-7'),
|
||||
('chemistry-7-ch3', 'chemistry', 7, 'Химия 7 · Водород',
|
||||
'',
|
||||
'§§18–22: водород как химический элемент и простое вещество, химические свойства водорода, понятие о кислотах и индикаторах, взаимодействие кислот с металлами и ряд активности, соли как продукты замещения. Лабораторные опыты 3 и 4, практическая работа 3.',
|
||||
'chemistry_7_ch3.html', 5, 'violet', 3, 1, 'chemistry-7'),
|
||||
('chemistry-7-ch4', 'chemistry', 7, 'Химия 7 · Вода',
|
||||
'',
|
||||
'§§23–26: состав, физические и химические свойства воды, основания как сложные вещества и индикаторы, реакция нейтрализации, охрана окружающей среды. Лабораторный опыт 5, практическая работа 4.',
|
||||
'chemistry_7_ch4.html', 4, 'blue', 4, 1, 'chemistry-7');
|
||||
@@ -38,6 +38,7 @@ router.delete('/error-log', ctrl.clearErrorLog);
|
||||
|
||||
/* System health */
|
||||
router.get('/health', ctrl.getHealth);
|
||||
router.get('/metrics', ctrl.getMetrics);
|
||||
|
||||
/* Topics CRUD */
|
||||
router.get('/topics', ctrl.getTopics);
|
||||
|
||||
@@ -138,6 +138,19 @@ const { requireFeature } = require('./middleware/features');
|
||||
app.use('/api/classroom', rateLimit({ windowMs: 60_000, max: 6000, message: 'Слишком много запросов' }));
|
||||
app.use('/api', rateLimit({ windowMs: 60_000, max: 600, message: 'Слишком много запросов, подождите минуту' }));
|
||||
|
||||
/* ── Request metrics (System Health Level 2) ── */
|
||||
const metrics = require('./utils/metrics');
|
||||
app.use((req, res, next) => {
|
||||
if (!req.originalUrl.startsWith('/api')) return next();
|
||||
const start = process.hrtime.bigint();
|
||||
res.on('finish', () => {
|
||||
const ms = Number(process.hrtime.bigint() - start) / 1e6;
|
||||
const route = (req.baseUrl || '') + (req.route && req.route.path ? req.route.path : '');
|
||||
metrics.record(req.method, route || req.path || '(unmatched)', res.statusCode, ms);
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
/* ── Routes ── */
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/subjects', subjectRoutes);
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Лёгкие in-memory метрики HTTP-запросов для System Health (Level 2).
|
||||
* Сбрасываются при перезапуске процесса. Память ограничена кольцевыми буферами.
|
||||
* Группировка по шаблону маршрута (req.route.path), а не по конкретному URL —
|
||||
* чтобы /api/biochem/molecules/:id не плодил тысячи ключей.
|
||||
*/
|
||||
const MAX_LAT = 3000; // окно последних латентностей для перцентилей
|
||||
const WINDOW_MS = 60_000; // окно для req/min
|
||||
|
||||
let total = 0;
|
||||
let totalServerErrors = 0;
|
||||
const statusClasses = { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 };
|
||||
const routeStats = new Map(); // "METHOD /path" -> { count, totalMs, maxMs, errors }
|
||||
const latencies = []; // последние латентности (мс)
|
||||
const recentTs = []; // метки времени последних запросов (для req/min)
|
||||
const startedAt = Date.now();
|
||||
|
||||
function record(method, route, status, ms) {
|
||||
total++;
|
||||
const cls = Math.floor(status / 100) + 'xx';
|
||||
if (statusClasses[cls] !== undefined) statusClasses[cls]++;
|
||||
if (status >= 500) totalServerErrors++;
|
||||
|
||||
const key = method + ' ' + route;
|
||||
let r = routeStats.get(key);
|
||||
if (!r) { r = { count: 0, totalMs: 0, maxMs: 0, errors: 0 }; routeStats.set(key, r); }
|
||||
r.count++; r.totalMs += ms; if (ms > r.maxMs) r.maxMs = ms;
|
||||
if (status >= 400) r.errors++;
|
||||
|
||||
latencies.push(ms);
|
||||
if (latencies.length > MAX_LAT) latencies.shift();
|
||||
|
||||
const now = Date.now();
|
||||
recentTs.push(now);
|
||||
const cutoff = now - WINDOW_MS;
|
||||
while (recentTs.length && recentTs[0] < cutoff) recentTs.shift();
|
||||
}
|
||||
|
||||
function _pct(sorted, p) {
|
||||
if (!sorted.length) return 0;
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))];
|
||||
}
|
||||
|
||||
/* ── Сэмплинг для трендов (Level 3): раз в минуту в кольцевой буфер ─────── */
|
||||
const HISTORY_CAP = 1440; // ~24 часа при 1 сэмпле/мин
|
||||
const samples = [];
|
||||
let _lastErr5xx = 0, _lastTotal = 0;
|
||||
|
||||
function sample() {
|
||||
const now = Date.now();
|
||||
const cutoff = now - WINDOW_MS;
|
||||
let rpm = 0;
|
||||
for (let i = recentTs.length - 1; i >= 0 && recentTs[i] >= cutoff; i--) rpm++;
|
||||
const sorted = [...latencies].sort((a, b) => a - b);
|
||||
const cur5xx = statusClasses['5xx'];
|
||||
const errDelta = Math.max(0, cur5xx - _lastErr5xx); _lastErr5xx = cur5xx;
|
||||
const reqDelta = Math.max(0, total - _lastTotal); _lastTotal = total;
|
||||
const mem = process.memoryUsage();
|
||||
samples.push({
|
||||
ts: now,
|
||||
rss: mem.rss,
|
||||
heapUsed: mem.heapUsed,
|
||||
reqPerMin: rpm,
|
||||
reqDelta,
|
||||
err5xx: errDelta,
|
||||
p95: _pct(sorted, 95),
|
||||
});
|
||||
if (samples.length > HISTORY_CAP) samples.shift();
|
||||
}
|
||||
|
||||
const _sampler = setInterval(sample, 60_000);
|
||||
if (_sampler.unref) _sampler.unref(); // не держим процесс живым
|
||||
sample(); // стартовая точка сразу
|
||||
|
||||
function history(limit = 180) { return samples.slice(-limit); }
|
||||
|
||||
function snapshot() {
|
||||
const now = Date.now();
|
||||
const cutoff = now - WINDOW_MS;
|
||||
let reqPerMin = 0;
|
||||
for (let i = recentTs.length - 1; i >= 0 && recentTs[i] >= cutoff; i--) reqPerMin++;
|
||||
const sorted = [...latencies].sort((a, b) => a - b);
|
||||
const avg = latencies.length ? latencies.reduce((s, x) => s + x, 0) / latencies.length : 0;
|
||||
const routes = [...routeStats.entries()].map(([k, r]) => ({
|
||||
route: k, count: r.count, avgMs: r.totalMs / r.count, maxMs: r.maxMs, errors: r.errors,
|
||||
}));
|
||||
return {
|
||||
total, totalServerErrors, statusClasses,
|
||||
reqPerMin,
|
||||
avgMs: avg, p50: _pct(sorted, 50), p95: _pct(sorted, 95), p99: _pct(sorted, 99),
|
||||
routeCount: routeStats.size,
|
||||
sinceMs: now - startedAt,
|
||||
topBusy: [...routes].sort((a, b) => b.count - a.count).slice(0, 8),
|
||||
topSlow: [...routes].sort((a, b) => b.avgMs - a.avgMs).slice(0, 8),
|
||||
topErrors: routes.filter(r => r.errors > 0).sort((a, b) => b.errors - a.errors).slice(0, 8),
|
||||
history: history(180),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { record, snapshot, sample, history };
|
||||
@@ -0,0 +1,177 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Phase 0 jsdom-каркас «Химия 7»: проверяем, что хаб и 4 главы реально
|
||||
* выполняются на движке chem8_engine.js без ошибок скриптов, строится
|
||||
* para-selector с нужным числом карточек, активен первый §, заглушки-builder'ы
|
||||
* рисуют para-hero и кнопку прочтения, а финал-боссы хаба решаются.
|
||||
* Содержание § наполняется в фазах 1–4 — здесь проверяется только каркас.
|
||||
*/
|
||||
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/biochem-core.js': readF('frontend/js/biochem-core.js'),
|
||||
'/js/chem8_svg.js': readF('frontend/js/chem8_svg.js'),
|
||||
'/js/chem7_svg.js': readF('frontend/js/chem7_svg.js'),
|
||||
'/js/chem7_ch1_widgets.js': readF('frontend/js/chem7_ch1_widgets.js'),
|
||||
'/js/chem7_ch2_widgets.js': readF('frontend/js/chem7_ch2_widgets.js'),
|
||||
'/js/chem8_engine.js': readF('frontend/js/chem8_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: 'chemistry_7_ch1.html', cards: 15, first: 'sec-p1' },
|
||||
{ file: 'chemistry_7_ch2.html', cards: 8, first: 'sec-p13' },
|
||||
{ file: 'chemistry_7_ch3.html', cards: 9, first: 'sec-p18' },
|
||||
{ file: 'chemistry_7_ch4.html', cards: 7, first: 'sec-p23' }
|
||||
];
|
||||
|
||||
for (const ch of CHAPTERS) {
|
||||
test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен ${ch.first}`, 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 === ch.first, 'активен ' + ch.first);
|
||||
const firstId = ch.first.replace('sec-', '');
|
||||
assert.ok(doc.querySelector('#' + firstId + '-body .para-hero'), 'para-hero первого §');
|
||||
assert.ok(doc.querySelector('#' + firstId + '-body .read-wrap'), 'кнопка прочтения первого §');
|
||||
});
|
||||
}
|
||||
|
||||
test('ch1 Волна 1: интерактивы §1–§3 + ПР1 монтируются без ошибок', async () => {
|
||||
const { doc, errors } = await loadDom('chemistry_7_ch1.html');
|
||||
// §1 строится при загрузке (первый §) — классификатор «тело/вещество»
|
||||
assert.ok(doc.querySelector('#p1-cls .c7-chip'), 'классификатор §1');
|
||||
doc.defaultView.goTo('p2'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p2-sep .c7-m'), 'разделитель смесей §2');
|
||||
doc.defaultView.goTo('pr1'); await wait(100);
|
||||
assert.ok(doc.querySelector('#pr1-sep .c7-m'), 'разделитель смесей ПР1');
|
||||
doc.defaultView.goTo('p3'); await wait(100);
|
||||
assert.ok(doc.querySelectorAll('#p3-el .el-cell').length > 10, 'каталог элементов §3');
|
||||
assert.ok(doc.querySelector('#p3-drill .c7-d'), 'тренажёр символов §3');
|
||||
assert.ok(doc.querySelectorAll('#navDotsp3 .nav-dot').length >= 4, 'тренажёр задач §3');
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
});
|
||||
|
||||
test('ch1 Волна 2: интерактивы §4–§6 монтируются без ошибок', async () => {
|
||||
const { doc, errors } = await loadDom('chemistry_7_ch1.html');
|
||||
doc.defaultView.goTo('p4'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p4-bal #p4-a'), 'весы атомов §4');
|
||||
doc.defaultView.goTo('p5'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p5-gal svg'), 'галерея молекул §5');
|
||||
doc.defaultView.goTo('p6'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p6-cls .c7-chip'), 'классификатор простое/сложное §6');
|
||||
assert.ok(doc.querySelector('#p6-gal svg'), 'галерея сложных веществ §6');
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
});
|
||||
|
||||
test('ch1 Волна 3: интерактивы §7–§9 монтируются и считают', async () => {
|
||||
const { doc, errors } = await loadDom('chemistry_7_ch1.html');
|
||||
doc.defaultView.goTo('p7'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p7-out'), 'парсер формулы §7');
|
||||
assert.match(doc.querySelector('#p7-out').textContent, /4/, 'H2SO4 → 4 атома O в разборе');
|
||||
doc.defaultView.goTo('p8'); await wait(100);
|
||||
assert.match(doc.querySelector('#p8-out').textContent, /100/, 'M_r(CaCO3)=100');
|
||||
doc.defaultView.goTo('p9'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p9-bld #p9-a'), 'конструктор валентности §9');
|
||||
assert.match(doc.querySelector('#p9-bout').textContent, /Al/, 'формула по валентности построена');
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
});
|
||||
|
||||
test('ch1 Волна 4: §10–§12 + ЛО1 + финал главы монтируются', async () => {
|
||||
const { doc, errors } = await loadDom('chemistry_7_ch1.html');
|
||||
doc.defaultView.goTo('p10'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p10-signs #p10-signs-go'), 'детектор признаков §10');
|
||||
doc.defaultView.goTo('lo1'); await wait(100);
|
||||
assert.ok(doc.querySelector('#lo1-signs #lo1-signs-go'), 'детектор признаков ЛО1');
|
||||
doc.defaultView.goTo('p11'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p11-bal svg'), 'весы сохранения массы §11');
|
||||
doc.defaultView.goTo('p12'); await wait(120);
|
||||
assert.ok(doc.querySelector('#p12-mount').childElementCount > 0, 'балансировщик §12');
|
||||
doc.defaultView.goTo('final1'); await wait(120);
|
||||
assert.ok(doc.querySelectorAll('#navDotsfinal1 .nav-dot').length >= 6, 'боссы финала главы');
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
});
|
||||
|
||||
test('ch1: переход к §9 и финалу строит без ошибок', async () => {
|
||||
const { doc, errors } = await loadDom('chemistry_7_ch1.html');
|
||||
doc.defaultView.goTo('p9'); await wait(80);
|
||||
assert.ok(doc.querySelector('#p9-body .para-hero'), 'para-hero §9');
|
||||
doc.defaultView.goTo('final1'); await wait(80);
|
||||
assert.ok(doc.querySelector('#final1-body .para-hero'), 'para-hero финала');
|
||||
assert.equal(doc.querySelector('#final1-body .read-wrap'), null, 'у финала нет кнопки прочтения');
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
});
|
||||
|
||||
test('ch2 Волна 1: интерактивы §13 + ЛО2 + §14 + §15 монтируются', async () => {
|
||||
const { doc, errors } = await loadDom('chemistry_7_ch2.html');
|
||||
assert.ok(doc.querySelector('#p13-air .air-seg'), 'диаграмма состава воздуха §13');
|
||||
doc.defaultView.goTo('lo2'); await wait(100);
|
||||
assert.ok(doc.querySelector('#lo2-coll #lo2-pick'), 'выбор собирания газа ЛО2');
|
||||
doc.defaultView.goTo('p14'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p14-tog #p14-o2'), 'переключатель элемент/вещество §14');
|
||||
doc.defaultView.goTo('p15'); await wait(100);
|
||||
assert.ok(doc.querySelector('#p15-burn #p15-go'), 'симулятор горения §15');
|
||||
doc.defaultView.goTo('p15'); doc.getElementById('p15-go').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
|
||||
assert.match(doc.querySelector('#p15-out').textContent, /оксид/, 'горение даёт оксид');
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
});
|
||||
|
||||
/* ── Хаб: каталог глав + финал курса ── */
|
||||
function buildHub() {
|
||||
let html = readF('frontend/textbooks/chemistry_7_hub.html');
|
||||
return 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>/, '');
|
||||
}
|
||||
async function loadHub() {
|
||||
const errors = []; const vc = new VirtualConsole(); vc.on('jsdomError', e => errors.push(e.message));
|
||||
const dom = new JSDOM(buildHub(), { runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/', beforeParse(w){ w.scrollTo=function(){}; } });
|
||||
await wait(60);
|
||||
return { dom, errors, doc: dom.window.document };
|
||||
}
|
||||
|
||||
test('hub: 4 карточки глав, финал курса — 8 боссов, босс решается', async () => {
|
||||
const { doc, errors } = await loadHub();
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 4, '4 карточки глав');
|
||||
doc.getElementById('final-head').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
|
||||
await wait(40);
|
||||
assert.equal(doc.querySelectorAll('#fin-bosses-container .boss-card').length, 8, '8 боссов');
|
||||
// решить босс 1 (Mr H2SO4 = 98)
|
||||
const inp = doc.getElementById('fb-1-inp'), go = doc.getElementById('fb-1-go');
|
||||
inp.value = '98'; go.dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
|
||||
assert.ok(doc.getElementById('fb-1-card').classList.contains('solved'), 'босс 1 повержен');
|
||||
});
|
||||
@@ -294,8 +294,7 @@
|
||||
|
||||
const user = LS.getUser();
|
||||
document.getElementById('nav-user').textContent = user?.name || user?.email || '';
|
||||
document.getElementById('nav-avatar').textContent =
|
||||
(user?.name || 'LS').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
|
||||
const isTeacher = ['admin', 'teacher'].includes(user?.role);
|
||||
if (!isTeacher) { location.href = '/dashboard'; throw new Error(); }
|
||||
|
||||
@@ -3481,8 +3481,7 @@
|
||||
}
|
||||
|
||||
// setup nav
|
||||
const initials = (_me.name || 'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('') || 'LS';
|
||||
document.getElementById('nav-avatar').textContent = initials;
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), _me);
|
||||
document.getElementById('nav-user').textContent = _me.name || _me.email;
|
||||
if (_me.role === 'admin') document.getElementById('btn-admin').style.display = '';
|
||||
if (['teacher', 'admin'].includes(_me.role)) {
|
||||
|
||||
@@ -319,8 +319,7 @@ async function init() {
|
||||
const user = LS.getUser?.() || null;
|
||||
if (user) {
|
||||
document.getElementById('nav-user').textContent = user.name?.split(' ')[0] || 'Профиль';
|
||||
const initials = (user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
document.getElementById('nav-avatar').textContent = initials;
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
}
|
||||
if (localStorage.getItem('ls_sb_collapsed') === '1') {
|
||||
document.getElementById('app').classList.add('sb-collapsed');
|
||||
|
||||
@@ -358,8 +358,7 @@
|
||||
const user = LS.getUser();
|
||||
LS.applyRoleSidebar(user);
|
||||
if (user) {
|
||||
document.getElementById('nav-avatar').textContent =
|
||||
(user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
document.getElementById('nav-user').textContent = user.name || '—';
|
||||
LS.showBoardIfAllowed();
|
||||
}
|
||||
|
||||
@@ -455,8 +455,7 @@
|
||||
|
||||
const user = LS.getUser();
|
||||
document.getElementById('nav-user').textContent = user?.name || user?.email || '';
|
||||
document.getElementById('nav-avatar').textContent =
|
||||
(user?.name || 'LS').split(' ').slice(0,2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
|
||||
const isTeacher = ['admin','teacher'].includes(user?.role);
|
||||
LS.showBoardIfAllowed();
|
||||
|
||||
@@ -297,8 +297,7 @@ let _subjectSlug = '';
|
||||
const user = LS.getUser();
|
||||
LS.applyRoleSidebar(user);
|
||||
if (user) {
|
||||
document.getElementById('nav-avatar').textContent =
|
||||
(user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
document.getElementById('nav-user').textContent = user.name || '—';
|
||||
LS.showBoardIfAllowed();
|
||||
}
|
||||
|
||||
@@ -255,8 +255,7 @@
|
||||
|
||||
const user = LS.getUser();
|
||||
document.getElementById('nav-user').textContent = user?.name || user?.email || '';
|
||||
document.getElementById('nav-avatar').textContent =
|
||||
(user?.name || 'LS').split(' ').slice(0,2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
|
||||
const isTeacher = ['admin','teacher'].includes(user?.role);
|
||||
if (!isTeacher) { location.href = '/dashboard'; throw new Error(); }
|
||||
|
||||
+128
-10
@@ -320,32 +320,104 @@
|
||||
}
|
||||
|
||||
async function refreshHealth() {
|
||||
try { renderHealth(await LS.api('/api/admin/health')); }
|
||||
catch (e) { const el = document.getElementById('health-content'); if (el) el.innerHTML = `<div style="color:var(--pink)">${esc(e.message)}</div>`; }
|
||||
try {
|
||||
const [h, m] = await Promise.all([
|
||||
LS.api('/api/admin/health'),
|
||||
LS.api('/api/admin/metrics').catch(() => null),
|
||||
]);
|
||||
renderHealth(h, m);
|
||||
} catch (e) { const el = document.getElementById('health-content'); if (el) el.innerHTML = `<div style="color:var(--pink)">${esc(e.message)}</div>`; }
|
||||
}
|
||||
|
||||
function renderHealth(h) {
|
||||
function renderHealth(h, m) {
|
||||
const el = document.getElementById('health-content');
|
||||
if (!el) return;
|
||||
const fmtBytes = b => !b ? '0' : b > 1e9 ? (b/1e9).toFixed(1)+' GB' : b > 1e6 ? (b/1e6).toFixed(1)+' MB' : (b/1e3).toFixed(0)+' KB';
|
||||
const fmtUp = s => { const d=Math.floor(s/86400), hr=Math.floor(s%86400/3600), m=Math.floor(s%3600/60); return d>0?`${d}d ${hr}h`:hr>0?`${hr}h ${m}m`:`${m}m`; };
|
||||
const fmtUp = s => { const d=Math.floor(s/86400), hr=Math.floor(s%86400/3600), mm=Math.floor(s%3600/60); return d>0?`${d}d ${hr}h`:hr>0?`${hr}h ${mm}m`:`${mm}m`; };
|
||||
const stColor = h.status==='critical'?'var(--pink)':h.status==='warning'?'#facc15':'var(--green)';
|
||||
const stLabel = h.status==='critical'?'Критическое состояние':h.status==='warning'?'Требует внимания':'Всё в норме';
|
||||
const memPct = Math.round((h.memPercent||0)*100);
|
||||
const memCol = memPct>92?'var(--pink)':memPct>80?'#facc15':'var(--green)';
|
||||
const lag = h.eventLoopLagMs||0;
|
||||
const lagCol = lag>200?'var(--pink)':lag>70?'#facc15':'var(--green)';
|
||||
const lag = h.eventLoopLagMs||0, lagCol = lag>200?'var(--pink)':lag>70?'#facc15':'var(--green)';
|
||||
const card = (val, label, col) => `<div class="adm-panel" style="padding:16px;margin:0;text-align:center">
|
||||
<div style="font-size:1.25rem;font-weight:800;font-family:'Unbounded',sans-serif;color:${col||'var(--text-1)'}">${val}</div>
|
||||
<div style="font-size:0.68rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-top:4px">${label}</div></div>`;
|
||||
const maxRows = Math.max(1, ...(h.db.tables||[]).map(t=>t.rows));
|
||||
|
||||
// ── секция метрик запросов (Level 2) ──
|
||||
let metricsHtml = '';
|
||||
if (m) {
|
||||
const sc = m.statusClasses||{}, tot = Math.max(1, (sc['2xx']||0)+(sc['3xx']||0)+(sc['4xx']||0)+(sc['5xx']||0));
|
||||
const seg = (n,col)=> n>0?`<div style="width:${(n/tot*100).toFixed(1)}%;background:${col}" title="${n}"></div>`:'';
|
||||
const routeRows = (arr, valFn, valLabel) => (arr&&arr.length)? arr.map(r=>`<div style="display:flex;align-items:center;gap:8px;margin:3px 0;font-size:.78rem">
|
||||
<div style="flex:1;color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(r.route)}</div>
|
||||
<div style="width:90px;text-align:right;font-weight:600;color:var(--text-3)">${valFn(r)}</div></div>`).join('')
|
||||
: `<div style="color:var(--text-3);font-size:.78rem">нет данных</div>`;
|
||||
const lagP = (v)=> (v||0)>200?'var(--pink)':(v||0)>70?'#facc15':'var(--text-1)';
|
||||
metricsHtml = `
|
||||
<div class="adm-panel" style="margin:14px 0 0">
|
||||
<div class="adm-panel-title">Метрики запросов <span style="color:var(--text-3);font-weight:400;font-size:.78rem">(с рестарта · ${fmtUp(Math.floor((m.sinceMs||0)/1000))})</span></div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:10px;margin:8px 0 14px">
|
||||
${card(m.reqPerMin, 'Req/min', 'var(--green)')}
|
||||
${card((m.total||0).toLocaleString('ru'), 'Всего')}
|
||||
${card((m.avgMs||0).toFixed(0)+' мс', 'Средн.')}
|
||||
${card((m.p95||0).toFixed(0)+' мс', 'p95', lagP(m.p95))}
|
||||
${card((m.p99||0).toFixed(0)+' мс', 'p99', lagP(m.p99))}
|
||||
${card(sc['5xx']||0, '5xx', (sc['5xx']||0)>0?'var(--pink)':'var(--green)')}
|
||||
</div>
|
||||
<div style="font-size:.72rem;color:var(--text-3);margin-bottom:5px">Статусы: 2xx ${sc['2xx']||0} · 3xx ${sc['3xx']||0} · 4xx ${sc['4xx']||0} · 5xx ${sc['5xx']||0}</div>
|
||||
<div style="display:flex;height:9px;border-radius:5px;overflow:hidden;background:rgba(255,255,255,.06);margin-bottom:14px">
|
||||
${seg(sc['2xx'],'#4ade80')}${seg(sc['3xx'],'#60a5fa')}${seg(sc['4xx'],'#facc15')}${seg(sc['5xx'],'#f87171')}
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||
<div><div style="font-size:.72rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-bottom:4px">Самые медленные</div>${routeRows(m.topSlow, r=>r.avgMs.toFixed(0)+' мс')}</div>
|
||||
<div><div style="font-size:.72rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-bottom:4px">Самые частые</div>${routeRows(m.topBusy, r=>r.count.toLocaleString('ru'))}</div>
|
||||
</div>
|
||||
${(m.topErrors&&m.topErrors.length)?`<div style="margin-top:12px"><div style="font-size:.72rem;color:var(--pink);font-weight:700;text-transform:uppercase;margin-bottom:4px">Маршруты с ошибками</div>${routeRows(m.topErrors, r=>r.errors+' ош. / '+r.count)}</div>`:''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── секция трендов (Level 3) ──
|
||||
let trendsHtml = '';
|
||||
if (m && m.history && m.history.length) {
|
||||
const tc = (id, label) => `<div><div style="font-size:.74rem;color:var(--text-3);margin-bottom:3px">${label}</div><canvas id="${id}" height="64" style="width:100%;height:64px;display:block"></canvas></div>`;
|
||||
trendsHtml = `<div class="adm-panel" style="margin:14px 0 0">
|
||||
<div class="adm-panel-title">Тренды <span style="color:var(--text-3);font-weight:400;font-size:.78rem">(${m.history.length} точек · 1/мин)</span></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:8px">
|
||||
${tc('trend-mem','Память RSS')}${tc('trend-req','Запросы/мин')}${tc('trend-err','Ошибки 5xx')}${tc('trend-p95','Латентность p95')}
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
// ── панель диагностики (Level 4): health-чеки + последние ошибки ──
|
||||
let diagHtml = '';
|
||||
{
|
||||
const ch = h.checks || {};
|
||||
const okCol = '#4ade80', badCol = 'var(--pink)';
|
||||
const chip = (label, ok, extra) => `<div style="display:flex;align-items:center;gap:6px;font-size:.82rem"><span style="width:9px;height:9px;border-radius:50%;background:${ok?okCol:badCol}"></span>${label}${extra?` <span style="color:var(--text-3)">${extra}</span>`:''}</div>`;
|
||||
const errs = h.recentErrorList || [];
|
||||
const lvlCol = l => l==='error'||l==='fatal'?'var(--pink)':l==='warn'?'#facc15':'var(--text-3)';
|
||||
diagHtml = `<div class="adm-panel" style="margin:14px 0 0">
|
||||
<div class="adm-panel-title">Диагностика</div>
|
||||
<div style="display:flex;gap:20px;flex-wrap:wrap;margin:6px 0 14px">
|
||||
${chip('База данных', !!ch.dbOk, ch.dbPingMs!=null?ch.dbPingMs.toFixed(2)+' мс':'')}
|
||||
${chip('Запись на диск', !!ch.diskWritable, ch.diskWritable?'доступна':'НЕДОСТУПНА')}
|
||||
</div>
|
||||
<div style="font-size:.72rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-bottom:5px">Последние ошибки</div>
|
||||
${errs.length ? errs.map(e=>`<div style="display:flex;align-items:baseline;gap:8px;font-size:.78rem;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.04)">
|
||||
<span style="color:var(--text-3);white-space:nowrap;font-size:.72rem">${esc((e.created_at||'').replace('T',' ').slice(5,16))}</span>
|
||||
<span style="color:${lvlCol(e.level)};font-weight:700;white-space:nowrap">${esc(e.level||'')}</span>
|
||||
${e.route?`<span style="color:var(--text-3);white-space:nowrap">${esc(e.method||'')} ${esc(e.route)}</span>`:''}
|
||||
<span style="flex:1;color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(e.message||'')}</span>
|
||||
</div>`).join('') : `<div style="color:var(--green);font-size:.82rem">Ошибок нет</div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="adm-panel" style="margin:0 0 16px;padding:14px 18px;display:flex;align-items:center;gap:14px;border-left:4px solid ${stColor}">
|
||||
<div style="width:13px;height:13px;border-radius:50%;background:${stColor};box-shadow:0 0 12px ${stColor};flex-shrink:0"></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:800;font-family:'Unbounded',sans-serif;color:${stColor}">${stLabel}</div>
|
||||
${h.reasons&&h.reasons.length?`<div style="font-size:.78rem;color:var(--text-3);margin-top:2px">${h.reasons.map(esc).join(' · ')}</div>`:`<div style="font-size:.78rem;color:var(--text-3);margin-top:2px">Все показатели в пределах нормы</div>`}
|
||||
<div style="font-size:.78rem;color:var(--text-3);margin-top:2px">${h.reasons&&h.reasons.length?h.reasons.map(esc).join(' · '):'Все показатели в пределах нормы'}</div>
|
||||
</div>
|
||||
<button id="health-live-btn" style="height:30px;padding:0 14px;border-radius:8px;border:1.5px solid rgba(255,255,255,.14);background:rgba(255,255,255,.05);color:${_healthLive?'var(--green)':'var(--text-3)'};font-weight:700;font-size:.78rem;cursor:pointer;white-space:nowrap">${_healthLive?'● Live':'○ Авто-обновление'}</button>
|
||||
</div>
|
||||
@@ -361,7 +433,7 @@
|
||||
${card(h.disk?fmtBytes(h.disk.freeBytes)+' своб.':'—', 'Диск')}
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||||
<div class="adm-panel" style="margin:0">
|
||||
<div class="adm-panel-title">Платформа</div>
|
||||
<table style="width:100%;font-size:0.86rem">
|
||||
@@ -384,12 +456,17 @@
|
||||
<tr><td style="color:var(--text-3);padding:3px 0">Сессий сегодня</td><td style="font-weight:600;color:var(--violet)">${h.db.todaySessions}</td></tr>
|
||||
<tr><td style="color:var(--text-3);padding:3px 0">Вопросов в базе</td><td style="font-weight:600">${h.db.totalQuestions}</td></tr>
|
||||
<tr><td style="color:var(--text-3);padding:3px 0">WAL</td><td style="font-weight:600">${fmtBytes(h.db.walBytes||0)}</td></tr>
|
||||
<tr><td style="color:var(--text-3);padding:3px 0">Запущен</td><td style="font-weight:600">${h.startedAt?esc(new Date(h.startedAt).toLocaleString('ru'))+'':'—'}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="adm-panel" style="margin:0">
|
||||
${metricsHtml}
|
||||
|
||||
${trendsHtml}
|
||||
|
||||
${diagHtml}
|
||||
|
||||
<div class="adm-panel" style="margin:14px 0 0">
|
||||
<div class="adm-panel-title">Крупнейшие таблицы БД</div>
|
||||
${(h.db.tables||[]).map(t=>`<div style="display:flex;align-items:center;gap:10px;margin:4px 0">
|
||||
<div style="width:170px;font-size:.8rem;color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.name)}</div>
|
||||
@@ -398,6 +475,18 @@
|
||||
</div>`).join('')}
|
||||
</div>`;
|
||||
|
||||
if (m && m.history && m.history.length) {
|
||||
const draw = (id, key, color, fmt) => {
|
||||
const c = document.getElementById(id); if (!c) return;
|
||||
c.width = c.offsetWidth || 300;
|
||||
drawTrend(c, m.history, key, color, fmt);
|
||||
};
|
||||
draw('trend-mem', 'rss', '#9B5DE5', v => (v/1e6).toFixed(0)+' МБ');
|
||||
draw('trend-req', 'reqPerMin', '#34d399');
|
||||
draw('trend-err', 'err5xx', '#f87171');
|
||||
draw('trend-p95', 'p95', '#facc15', v => v.toFixed(0)+' мс');
|
||||
}
|
||||
|
||||
const btn = document.getElementById('health-live-btn');
|
||||
if (btn) btn.addEventListener('click', () => {
|
||||
_healthLive = !_healthLive;
|
||||
@@ -407,6 +496,35 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Мини-график тренда (canvas): линия + заливка + подписи макс/последнее.
|
||||
function drawTrend(canvas, points, key, color, fmt) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const W = canvas.width, H = canvas.height;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
const vals = points.map(p => p[key] || 0);
|
||||
if (vals.length < 2) {
|
||||
ctx.fillStyle = '#555'; ctx.font = '11px Manrope,sans-serif';
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
ctx.fillText('накопление данных…', W/2, H/2); return;
|
||||
}
|
||||
let max = Math.max(...vals), min = Math.min(...vals);
|
||||
if (max === min) max += 1;
|
||||
const pad = 4, range = (max - min) || 1;
|
||||
const X = i => pad + i/(vals.length-1)*(W-pad*2);
|
||||
const Y = v => H-pad - (v-min)/range*(H-pad*2-10);
|
||||
ctx.beginPath(); ctx.moveTo(X(0), H);
|
||||
vals.forEach((v,i)=>ctx.lineTo(X(i), Y(v)));
|
||||
ctx.lineTo(X(vals.length-1), H); ctx.closePath();
|
||||
ctx.fillStyle = color + '22'; ctx.fill();
|
||||
ctx.beginPath(); vals.forEach((v,i)=> i?ctx.lineTo(X(i),Y(v)):ctx.moveTo(X(i),Y(v)));
|
||||
ctx.strokeStyle = color; ctx.lineWidth = 1.6; ctx.lineJoin = 'round'; ctx.stroke();
|
||||
const lastV = vals[vals.length-1];
|
||||
ctx.fillStyle = color; ctx.beginPath(); ctx.arc(X(vals.length-1), Y(lastV), 2.5, 0, Math.PI*2); ctx.fill();
|
||||
ctx.font = '10px Manrope,sans-serif'; ctx.textBaseline = 'top';
|
||||
ctx.fillStyle = '#888'; ctx.textAlign = 'left'; ctx.fillText('макс ' + (fmt?fmt(max):max), pad, 1);
|
||||
ctx.fillStyle = color; ctx.textAlign = 'right'; ctx.fillText(fmt?fmt(lastV):lastV, W-pad, 1);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════
|
||||
ОНЛАЙН-УРОКИ (classroom admin)
|
||||
════════════════════════════════════════════════ */
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
/* chem7_ch1_widgets.js — интерактивы главы 1 «Первоначальные химические понятия» (Химия 7).
|
||||
* Монтируются движком chem8_engine.js: window.CHEM8_WIDGETS[id] / window.FLAG_MOUNTS[id].
|
||||
* Используют window.Chem8 (chem8_svg.js): molarMass, elementCounts, arOf, fmt, equationBalancer.
|
||||
* Без эмоджи; KaTeX-рендер — через window.chem8RenderMath.
|
||||
*/
|
||||
(function (W) {
|
||||
'use strict';
|
||||
function C() { return W.Chem8 || {}; }
|
||||
function $(id) { return document.getElementById(id); }
|
||||
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
/* ── общий мини-классификатор: чипы → 2 корзины ──────────────────── */
|
||||
function classifier(mount, opts) {
|
||||
if (!mount || mount._built) return; mount._built = 1;
|
||||
var items = opts.items.slice(); // {t, b} t=подпись, b=индекс корзины
|
||||
var buckets = opts.buckets; // [name0, name1]
|
||||
var placed = {}; // idx -> bucket
|
||||
var pool = items.map(function (it, i) { return i; });
|
||||
function colorOf(ok){ return ok ? '#059669' : '#dc2626'; }
|
||||
function render() {
|
||||
var chips = pool.map(function (i) {
|
||||
return '<button class="c7-chip" data-i="' + i + '" style="padding:7px 12px;border:1.5px solid var(--border);border-radius:9px;background:var(--card);color:var(--text);font-weight:600;font-size:.9rem;cursor:pointer;margin:3px">' + esc(items[i].t) + '</button>';
|
||||
}).join('') || '<span style="color:var(--muted)">Все карточки распределены.</span>';
|
||||
var cols = buckets.map(function (name, bi) {
|
||||
var inb = Object.keys(placed).filter(function (k) { return placed[k] === bi; });
|
||||
var cells = inb.map(function (k) {
|
||||
var ok = items[k].b === bi;
|
||||
return '<div style="padding:5px 10px;margin:3px 0;border-radius:8px;font-size:.86rem;font-weight:600;color:#fff;background:' + colorOf(ok) + '">' + esc(items[k].t) + (ok ? ' ✓' : ' ✗') + '</div>';
|
||||
}).join('') || '<div style="color:var(--muted);font-size:.82rem">перетащите сюда…</div>';
|
||||
return '<div class="c7-bucket" data-b="' + bi + '" style="flex:1;min-width:140px;border:1.5px dashed var(--border);border-radius:11px;padding:10px;background:var(--pri-soft)"><div style="font-weight:700;margin-bottom:6px">' + esc(name) + '</div>' + cells + '</div>';
|
||||
}).join('');
|
||||
mount.innerHTML = '<div style="margin-bottom:10px">' + chips + '</div><div style="display:flex;gap:10px;flex-wrap:wrap">' + cols + '</div>'
|
||||
+ '<div style="margin-top:8px;font-size:.84rem;color:var(--muted)">Кликни карточку, затем — корзину. Зелёный — верно, красный — ошибка.</div>';
|
||||
bind();
|
||||
}
|
||||
var sel = null;
|
||||
function bind() {
|
||||
mount.querySelectorAll('.c7-chip').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
mount.querySelectorAll('.c7-chip').forEach(function (x) { x.style.outline = ''; });
|
||||
sel = +b.dataset.i; b.style.outline = '2px solid var(--pri)';
|
||||
});
|
||||
});
|
||||
mount.querySelectorAll('.c7-bucket').forEach(function (col) {
|
||||
col.addEventListener('click', function () {
|
||||
if (sel == null) return;
|
||||
placed[sel] = +col.dataset.b;
|
||||
pool = pool.filter(function (x) { return x !== sel; });
|
||||
sel = null; render();
|
||||
});
|
||||
});
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
/* §1 — классификатор «тело / вещество» */
|
||||
function mount_p1() {
|
||||
var m = $('p1-cls'); if (!m) return;
|
||||
classifier(m, {
|
||||
buckets: ['Физическое тело', 'Вещество'],
|
||||
items: [
|
||||
{ t: 'стакан', b: 0 }, { t: 'вода', b: 1 }, { t: 'гвоздь', b: 0 }, { t: 'железо', b: 1 },
|
||||
{ t: 'ложка', b: 0 }, { t: 'сахар', b: 1 }, { t: 'медь', b: 1 }, { t: 'линейка', b: 0 }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/* §2 / ПР1 — разделитель смесей: выбери метод для смеси */
|
||||
var MIX = [
|
||||
{ mix: 'Песок и вода', method: 'Фильтрование', why: 'Песок не растворяется — задерживается фильтром, вода проходит.' },
|
||||
{ mix: 'Соль и вода', method: 'Выпаривание', why: 'Вода испаряется, соль остаётся на дне.' },
|
||||
{ mix: 'Железные опилки и сера', method: 'Магнит', why: 'Железо притягивается магнитом, сера — нет.' },
|
||||
{ mix: 'Вода и растительное масло', method: 'Отстаивание (делительная воронка)', why: 'Масло легче воды и не смешивается — слои разделяют.' },
|
||||
{ mix: 'Спирт и вода', method: 'Перегонка (дистилляция)', why: 'У спирта и воды разные температуры кипения.' }
|
||||
];
|
||||
var METHODS = ['Фильтрование', 'Выпаривание', 'Магнит', 'Отстаивание (делительная воронка)', 'Перегонка (дистилляция)'];
|
||||
function mount_sep(mountId) {
|
||||
var m = $(mountId); if (!m || m._built) return; m._built = 1;
|
||||
var idx = 0;
|
||||
function render() {
|
||||
var cur = MIX[idx];
|
||||
m.innerHTML = '<div class="fld"><label>Смесь</label><select id="' + mountId + '-pick">'
|
||||
+ MIX.map(function (x, i) { return '<option value="' + i + '"' + (i === idx ? ' selected' : '') + '>' + esc(x.mix) + '</option>'; }).join('') + '</select></div>'
|
||||
+ '<div style="margin:8px 0;font-weight:600">Каким способом разделить смесь «' + esc(cur.mix) + '»?</div>'
|
||||
+ '<div style="display:flex;flex-wrap:wrap;gap:6px">' + METHODS.map(function (mt) {
|
||||
return '<button class="c7-m btn" data-m="' + esc(mt) + '">' + esc(mt) + '</button>';
|
||||
}).join('') + '</div>'
|
||||
+ '<div class="out" id="' + mountId + '-out" style="margin-top:8px">Выбери способ разделения.</div>';
|
||||
$(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; m._built = 0; render(); });
|
||||
var out = $(mountId + '-out');
|
||||
m.querySelectorAll('.c7-m').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
var ok = b.dataset.m === cur.method;
|
||||
out.className = 'out ' + (ok ? 'ok' : 'bad');
|
||||
out.innerHTML = ok
|
||||
? '<b>Верно!</b> ' + esc(cur.method) + '. ' + esc(cur.why)
|
||||
: '<b>Не подходит.</b> Подумай, чем различаются вещества в смеси (растворимость, магнитные свойства, температура кипения, плотность).';
|
||||
});
|
||||
});
|
||||
}
|
||||
render();
|
||||
}
|
||||
function mount_p2() { mount_sep('p2-sep'); }
|
||||
function mount_pr1() { mount_sep('pr1-sep'); }
|
||||
|
||||
/* §3 — каталог элементов + тренажёр «символ ↔ название» */
|
||||
var EL = {
|
||||
H: [1, 'Водород'], He: [2, 'Гелий'], Li: [3, 'Литий'], C: [6, 'Углерод'], N: [7, 'Азот'],
|
||||
O: [8, 'Кислород'], F: [9, 'Фтор'], Na: [11, 'Натрий'], Mg: [12, 'Магний'], Al: [13, 'Алюминий'],
|
||||
Si: [14, 'Кремний'], P: [15, 'Фосфор'], S: [16, 'Сера'], Cl: [17, 'Хлор'], K: [19, 'Калий'],
|
||||
Ca: [20, 'Кальций'], Fe: [26, 'Железо'], Cu: [29, 'Медь'], Zn: [30, 'Цинк'], Ag: [47, 'Серебро']
|
||||
};
|
||||
function mount_p3() {
|
||||
var grid = $('p3-el'), info = $('p3-elinfo');
|
||||
if (grid && !grid._built) {
|
||||
grid._built = 1;
|
||||
Object.keys(EL).forEach(function (s) {
|
||||
var ar = C().arOf ? C().arOf(s) : '';
|
||||
var c = document.createElement('div'); c.className = 'el-cell';
|
||||
c.innerHTML = '<span class="z">' + EL[s][0] + '</span><span class="s">' + s + '</span><span class="a">' + ar + '</span>';
|
||||
c.addEventListener('click', function () {
|
||||
grid.querySelectorAll('.el-cell').forEach(function (x) { x.classList.remove('on'); }); c.classList.add('on');
|
||||
if (info) info.innerHTML = '<b>' + EL[s][1] + '</b> (' + s + ') · порядковый номер Z = ' + EL[s][0] + ' · A_r = ' + ar;
|
||||
});
|
||||
grid.appendChild(c);
|
||||
});
|
||||
}
|
||||
/* drill: дан символ → выбери название */
|
||||
var d = $('p3-drill'); if (d && !d._built) {
|
||||
d._built = 1;
|
||||
var keys = Object.keys(EL), order = keys.slice(), qi = 0, score = 0, total = 0;
|
||||
function nextQ() {
|
||||
var sym = order[qi % order.length];
|
||||
var correct = EL[sym][1];
|
||||
var opts = [correct];
|
||||
while (opts.length < 4) { var r = EL[keys[(qi * 7 + opts.length * 13 + 3) % keys.length]][1]; if (opts.indexOf(r) < 0) opts.push(r); }
|
||||
// детерминированная перестановка
|
||||
opts = opts.sort(function (a, b) { return ((a.length + qi) % 3) - ((b.length + qi) % 3); });
|
||||
d.innerHTML = '<div style="font-weight:700;margin-bottom:6px">Какому элементу соответствует символ <span style="font-family:var(--mono);color:var(--pri-d);font-size:1.1rem">' + sym + '</span>?</div>'
|
||||
+ '<div style="display:flex;flex-wrap:wrap;gap:6px">' + opts.map(function (o) { return '<button class="c7-d btn" data-o="' + esc(o) + '">' + esc(o) + '</button>'; }).join('') + '</div>'
|
||||
+ '<div class="out" id="p3-drill-out" style="margin-top:8px">Счёт: ' + score + ' из ' + total + '</div>';
|
||||
var out = $('p3-drill-out');
|
||||
d.querySelectorAll('.c7-d').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
total++; var ok = b.dataset.o === correct; if (ok) score++;
|
||||
out.className = 'out ' + (ok ? 'ok' : 'bad');
|
||||
out.innerHTML = (ok ? '<b>Верно!</b> ' : '<b>Нет.</b> ') + sym + ' — это ' + correct + '. Счёт: ' + score + ' из ' + total;
|
||||
qi++;
|
||||
setTimeout(nextQ, 850);
|
||||
});
|
||||
});
|
||||
}
|
||||
nextQ();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Волна 2 ── */
|
||||
|
||||
/* §4 — «весы атомов»: во сколько раз один атом тяжелее другого */
|
||||
var ABEL = ['H','C','N','O','Na','Mg','Al','S','Cl','Ca','Fe','Cu','Zn','Ag'];
|
||||
function mount_p4() {
|
||||
var m = $('p4-bal'); if (!m || m._built) return; m._built = 1;
|
||||
function opts(sel){ return ABEL.map(function(e){ var ar=C().arOf?C().arOf(e):''; return '<option value="'+e+'"'+(e===sel?' selected':'')+'>'+e+' (A_r='+ar+')</option>'; }).join(''); }
|
||||
m.innerHTML = '<div class="fld"><label>Атом A</label><select id="p4-a">'+opts('S')+'</select>'
|
||||
+'<label>Атом B</label><select id="p4-b">'+opts('O')+'</select></div><div class="out" id="p4-out"></div>';
|
||||
function upd(){
|
||||
var a=$('p4-a').value, b=$('p4-b').value, ara=+C().arOf(a), arb=+C().arOf(b);
|
||||
var out=$('p4-out');
|
||||
if(!ara||!arb){ out.textContent='—'; return; }
|
||||
var big=Math.max(ara,arb), sm=Math.min(ara,arb), k=big/sm;
|
||||
var heavier=ara>=arb?a:b, lighter=ara>=arb?b:a;
|
||||
out.className='out ok';
|
||||
out.innerHTML='<span class="bd">A_r('+a+')='+ara+', A_r('+b+')='+arb+'<br>'
|
||||
+(ara===arb?('Атомы '+a+' и '+b+' имеют одинаковую массу.')
|
||||
:('Атом <b>'+heavier+'</b> тяжелее атома '+lighter+' в <b>'+(Math.round(k*100)/100).toString().replace('.',',')+'</b> раз.'))+'</span>';
|
||||
}
|
||||
$('p4-a').addEventListener('change',upd); $('p4-b').addEventListener('change',upd); upd();
|
||||
}
|
||||
|
||||
/* рисуем молекулу как набор шариков-атомов */
|
||||
var COL = { H:'#cbd5e1', O:'#ef4444', N:'#3b82f6', C:'#334155', S:'#eab308', Cl:'#22c55e', Fe:'#b45309', Na:'#a78bfa' };
|
||||
var RAD = { H:11, O:16, N:15, C:16, S:18, Cl:17, Fe:18, Na:17 };
|
||||
function molBalls(atoms) {
|
||||
// atoms: [[el,n],...]; рисуем в ряд
|
||||
var balls = [], cx = 26;
|
||||
atoms.forEach(function(pair){ for(var i=0;i<pair[1];i++){ balls.push(pair[0]); } });
|
||||
var W0 = Math.max(120, cx*2 + balls.reduce(function(s,e){return s+(RAD[e]||14)*2+8;},0));
|
||||
var x = cx, svg = '';
|
||||
balls.forEach(function(el){
|
||||
var r = RAD[el]||14; x += r;
|
||||
svg += '<circle cx="'+x+'" cy="34" r="'+r+'" fill="'+(COL[el]||'#94a3b8')+'" stroke="rgba(0,0,0,.25)"/>'
|
||||
+ '<text x="'+x+'" y="39" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">'+el+'</text>';
|
||||
x += r + 8;
|
||||
});
|
||||
return '<svg viewBox="0 0 '+W0+' 68" width="100%" style="max-width:'+W0+'px;height:auto">'+svg+'</svg>';
|
||||
}
|
||||
function molCard(name, formula, atoms, note) {
|
||||
return '<div style="border:1.5px solid var(--border);border-radius:11px;padding:10px 12px;background:var(--card)">'
|
||||
+'<div style="font-weight:700;margin-bottom:2px">'+esc(name)+' · '+(C().formula?C().formula(formula):formula)+'</div>'
|
||||
+ molBalls(atoms)
|
||||
+'<div style="font-size:.82rem;color:var(--muted);margin-top:4px">'+esc(note)+'</div></div>';
|
||||
}
|
||||
|
||||
/* §5 — галерея простых веществ */
|
||||
function mount_p5() {
|
||||
var m = $('p5-gal'); if (!m || m._built) return; m._built = 1;
|
||||
m.innerHTML = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px">'
|
||||
+ molCard('Водород','H2',[['H',2]],'2 атома H — двухатомная молекула')
|
||||
+ molCard('Кислород','O2',[['O',2]],'2 атома O')
|
||||
+ molCard('Озон','O3',[['O',3]],'3 атома O — тоже простое вещество')
|
||||
+ molCard('Азот','N2',[['N',2]],'2 атома N')
|
||||
+ '</div><div style="font-size:.84rem;color:var(--muted);margin-top:8px">Во всех молекулах — атомы <b>одного</b> элемента → это <b>простые вещества</b>. Кислород $\\text{O}_2$ и озон $\\text{O}_3$ образованы одним элементом, но это разные простые вещества.</div>';
|
||||
if (W.chem8RenderMath) try { W.chem8RenderMath(m); } catch(e){}
|
||||
}
|
||||
|
||||
/* §6 — классификатор простое/сложное + галерея сложных веществ */
|
||||
function mount_p6() {
|
||||
var c = $('p6-cls');
|
||||
if (c) classifier(c, {
|
||||
buckets: ['Простое вещество', 'Сложное вещество'],
|
||||
items: [
|
||||
{ t:'O₂', b:0 }, { t:'H₂O', b:1 }, { t:'Fe', b:0 }, { t:'CO₂', b:1 },
|
||||
{ t:'N₂', b:0 }, { t:'NH₃', b:1 }, { t:'S', b:0 }, { t:'CH₄', b:1 }
|
||||
]
|
||||
});
|
||||
var g = $('p6-gal');
|
||||
if (g && !g._built) { g._built = 1;
|
||||
g.innerHTML = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px">'
|
||||
+ molCard('Вода','H2O',[['O',1],['H',2]],'2 элемента: H и O')
|
||||
+ molCard('Углекислый газ','CO2',[['C',1],['O',2]],'2 элемента: C и O')
|
||||
+ molCard('Метан','CH4',[['C',1],['H',4]],'2 элемента: C и H')
|
||||
+ molCard('Аммиак','NH3',[['N',1],['H',3]],'2 элемента: N и H')
|
||||
+ '</div><div style="font-size:.84rem;color:var(--muted);margin-top:8px">В каждой молекуле — атомы <b>разных</b> элементов → это <b>сложные вещества</b>.</div>';
|
||||
if (W.chem8RenderMath) try { W.chem8RenderMath(g); } catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Волна 3 ── */
|
||||
|
||||
/* §7 — разбор химической формулы на состав */
|
||||
function mount_p7() {
|
||||
var inp = $('p7-in'), out = $('p7-out'), go = $('p7-go'); if (!inp || inp._built) return; inp._built = 1;
|
||||
function calc() {
|
||||
var f = inp.value.trim(), cnt = C().elementCounts ? C().elementCounts(f) : null;
|
||||
if (!cnt || !Object.keys(cnt).length) { out.className = 'out bad'; out.textContent = 'Не удалось разобрать формулу. Проверь символы элементов (например, H2SO4).'; return; }
|
||||
var els = Object.keys(cnt), tot = els.reduce(function (s, e) { return s + cnt[e]; }, 0);
|
||||
out.className = 'out ok';
|
||||
out.innerHTML = '<span class="bd"><b>' + (C().formula ? C().formula(f) : f) + '</b><br>'
|
||||
+ 'Элементов: <b>' + els.length + '</b> (' + (els.length === 1 ? 'простое' : 'сложное') + ' вещество)<br>'
|
||||
+ els.map(function (e) { return e + ': ' + cnt[e] + ' ' + (cnt[e] === 1 ? 'атом' : 'атома(ов)'); }).join('<br>')
|
||||
+ '<br>Всего атомов в формуле: <b>' + tot + '</b></span>';
|
||||
}
|
||||
go.addEventListener('click', calc);
|
||||
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') calc(); });
|
||||
document.querySelectorAll('.p7-ex').forEach(function (b) { b.addEventListener('click', function () { inp.value = b.dataset.f; calc(); }); });
|
||||
calc();
|
||||
}
|
||||
|
||||
/* §8 — калькулятор относительной молекулярной массы M_r */
|
||||
function mount_p8() {
|
||||
var inp = $('p8-in'), out = $('p8-out'), go = $('p8-go'); if (!inp || inp._built) return; inp._built = 1;
|
||||
function calc() {
|
||||
var f = inp.value.trim(), cnt = C().elementCounts ? C().elementCounts(f) : null, mr = C().molarMass ? C().molarMass(f) : NaN;
|
||||
if (!cnt || isNaN(mr)) { out.className = 'out bad'; out.textContent = 'Не удалось разобрать формулу.'; return; }
|
||||
out.className = 'out ok';
|
||||
out.innerHTML = '<span class="bd"><b>M_r(' + f + ') = ' + C().fmt(mr) + '</b><br>'
|
||||
+ Object.keys(cnt).map(function (e) { return e + ': A_r=' + (C().arOf ? C().arOf(e) : '?') + ' × ' + cnt[e]; }).join(' | ')
|
||||
+ '<br>Σ = ' + Object.keys(cnt).map(function (e) { return (C().arOf ? C().arOf(e) : '?') + '·' + cnt[e]; }).join(' + ') + ' = ' + C().fmt(mr) + '</span>';
|
||||
}
|
||||
go.addEventListener('click', calc);
|
||||
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') calc(); });
|
||||
document.querySelectorAll('.p8-ex').forEach(function (b) { b.addEventListener('click', function () { inp.value = b.dataset.f; calc(); }); });
|
||||
calc();
|
||||
}
|
||||
|
||||
/* §9 — конструктор формулы по валентности (НОК индексов) */
|
||||
function gcd(a, b) { return b ? gcd(b, a % b) : a; }
|
||||
var VA = [ ['Na', 1], ['K', 1], ['H', 1], ['Mg', 2], ['Ca', 2], ['Zn', 2], ['Cu', 2], ['Al', 3], ['C', 4] ];
|
||||
var VB = [ ['O', 2], ['Cl', 1], ['S', 2] ];
|
||||
function mount_p9() {
|
||||
var m = $('p9-bld'); if (!m || m._built) return; m._built = 1;
|
||||
function optA(){ return VA.map(function(e,i){ return '<option value="'+i+'"'+(e[0]==='Al'?' selected':'')+'>'+e[0]+' (валентность '+'I'.repeat(e[1]).replace('IIII','IV')+')</option>'; }).join(''); }
|
||||
function optB(){ return VB.map(function(e,i){ return '<option value="'+i+'">'+e[0]+' (валентность '+'I'.repeat(e[1])+')</option>'; }).join(''); }
|
||||
m.innerHTML = '<div class="fld"><label>Элемент A</label><select id="p9-a">'+optA()+'</select>'
|
||||
+'<label>Элемент B</label><select id="p9-b">'+optB()+'</select></div><div class="out" id="p9-bout"></div>';
|
||||
function upd() {
|
||||
var a = VA[+$('p9-a').value], b = VB[+$('p9-b').value];
|
||||
var lcm = a[1] * b[1] / gcd(a[1], b[1]);
|
||||
var ia = lcm / a[1], ib = lcm / b[1];
|
||||
var raw = a[0] + (ia > 1 ? ia : '') + b[0] + (ib > 1 ? ib : '');
|
||||
var out = $('p9-bout'); out.className = 'out ok';
|
||||
out.innerHTML = '<span class="bd">Валентности: ' + a[0] + ' = ' + 'I'.repeat(a[1]).replace('IIII','IV') + ', ' + b[0] + ' = ' + 'I'.repeat(b[1]) + '<br>'
|
||||
+ 'Наименьшее общее кратное валентностей = <b>' + lcm + '</b><br>'
|
||||
+ 'Индексы: ' + a[0] + ' → ' + ia + ', ' + b[0] + ' → ' + ib + '<br>'
|
||||
+ 'Формула: <b style="font-size:1.15rem">' + (C().formula ? C().formula(raw) : raw) + '</b><br>'
|
||||
+ 'Проверка: ' + ia + '·' + a[1] + ' = ' + ib + '·' + b[1] + ' = ' + lcm + ' единиц валентности — совпало.</span>';
|
||||
}
|
||||
$('p9-a').addEventListener('change', upd); $('p9-b').addEventListener('change', upd); upd();
|
||||
}
|
||||
|
||||
/* ── Волна 4 ── */
|
||||
|
||||
/* §10 / ЛО1 — детектор признаков химической реакции */
|
||||
var DEMOS = [
|
||||
{ name: 'Нагревание малахита', signs: ['изменение цвета: зелёный → чёрный', 'выделение газа (водяной пар и углекислый газ)'] },
|
||||
{ name: 'Сливание растворов CuSO₄ и NaOH', signs: ['образование осадка (голубой)', 'изменение цвета раствора'] },
|
||||
{ name: 'Горение серы', signs: ['выделение света и тепла (пламя)', 'появление резкого запаха'] },
|
||||
{ name: 'Добавление соды в уксус', signs: ['выделение газа (пузырьки)'] }
|
||||
];
|
||||
function mount_signs(mountId) {
|
||||
var m = $(mountId); if (!m || m._built) return; m._built = 1;
|
||||
var idx = 0;
|
||||
function render() {
|
||||
m.innerHTML = '<div class="fld"><label>Опыт</label><select id="' + mountId + '-pick">'
|
||||
+ DEMOS.map(function (d, i) { return '<option value="' + i + '"' + (i === idx ? ' selected' : '') + '>' + esc(d.name) + '</option>'; }).join('') + '</select>'
|
||||
+ '<button class="btn primary" id="' + mountId + '-go">Провести опыт</button></div>'
|
||||
+ '<div class="out" id="' + mountId + '-out">Выбери опыт и нажми «Провести опыт».</div>';
|
||||
$(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; m._built = 0; render(); });
|
||||
$(mountId + '-go').addEventListener('click', function () {
|
||||
var d = DEMOS[idx], out = $(mountId + '-out'); out.className = 'out ok';
|
||||
out.innerHTML = '<b>Наблюдаемые признаки реакции:</b><div style="margin-top:6px">'
|
||||
+ d.signs.map(function (s) { return '<div style="padding:5px 10px;margin:3px 0;border-radius:8px;background:var(--pri-soft);font-weight:600">✓ ' + esc(s) + '</div>'; }).join('')
|
||||
+ '</div><div style="font-size:.84rem;color:var(--muted);margin-top:6px">Эти признаки указывают, что произошла <b>химическая реакция</b> — образовались новые вещества.</div>';
|
||||
});
|
||||
}
|
||||
render();
|
||||
}
|
||||
function mount_p10() { mount_signs('p10-signs'); }
|
||||
function mount_lo1() { mount_signs('lo1-signs'); }
|
||||
|
||||
/* §11 — весы сохранения массы */
|
||||
function mount_p11() {
|
||||
var m = $('p11-bal'); if (!m || m._built) return; m._built = 1;
|
||||
var mixed = false;
|
||||
function scale(level) {
|
||||
// level: 0 = равновесие
|
||||
return '<svg viewBox="0 0 320 130" width="100%" style="max-width:340px">'
|
||||
+ '<line x1="160" y1="14" x2="160" y2="40" stroke="var(--muted)" stroke-width="3"/>'
|
||||
+ '<line x1="60" y1="40" x2="260" y2="40" stroke="var(--muted)" stroke-width="3"/>'
|
||||
+ '<circle cx="160" cy="14" r="6" fill="var(--pri)"/>'
|
||||
+ '<rect x="30" y="55" width="80" height="34" rx="6" fill="var(--pri-soft)" stroke="var(--border)"/>'
|
||||
+ '<rect x="210" y="55" width="80" height="34" rx="6" fill="var(--pri-soft)" stroke="var(--border)"/>'
|
||||
+ '<line x1="60" y1="40" x2="70" y2="55" stroke="var(--muted)" stroke-width="2"/><line x1="110" y1="40" x2="100" y2="55" stroke="var(--muted)" stroke-width="2"/>'
|
||||
+ '<line x1="210" y1="40" x2="220" y2="55" stroke="var(--muted)" stroke-width="2"/><line x1="260" y1="40" x2="250" y2="55" stroke="var(--muted)" stroke-width="2"/>'
|
||||
+ '<text x="70" y="77" font-size="13" font-weight="700" fill="var(--text)">100 г</text>'
|
||||
+ '<text x="250" y="77" font-size="13" font-weight="700" fill="var(--text)">100 г</text>'
|
||||
+ '<text x="70" y="108" font-size="11" fill="var(--muted)">' + (mixed ? 'продукты' : 'реагенты') + '</text>'
|
||||
+ '<text x="225" y="108" font-size="11" fill="var(--muted)">' + (mixed ? 'продукты' : 'реагенты') + '</text>'
|
||||
+ '</svg>';
|
||||
}
|
||||
function render() {
|
||||
m.innerHTML = scale()
|
||||
+ '<div style="margin:6px 0;font-size:.92rem">' + (mixed
|
||||
? 'После реакции: <b>осадок Cu(OH)₂ + раствор Na₂SO₄</b>. Стрелка весов не сдвинулась — <b>масса сохранилась</b> (100 г = 100 г).'
|
||||
: 'До реакции: <b>раствор CuSO₄ + раствор NaOH</b>, общая масса 100 г.') + '</div>'
|
||||
+ '<button class="btn primary" id="p11-mix">' + (mixed ? 'Сбросить' : 'Смешать растворы') + '</button>';
|
||||
$('p11-mix').addEventListener('click', function () { mixed = !mixed; m._built = 0; render(); });
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
/* §12 — балансировщик уравнений (переиспользуем Chem8.equationBalancer) */
|
||||
function mount_p12() {
|
||||
var pick = $('p12-pick'), mount = $('p12-mount'); if (!pick || pick._built || !C().equationBalancer) return; pick._built = 1;
|
||||
function build() { var parts = pick.value.split('|'); C().equationBalancer(mount, { skeleton: parts[0], solution: parts[1].split(',').map(Number) }); }
|
||||
pick.addEventListener('change', build); build();
|
||||
}
|
||||
|
||||
W.CHEM8_WIDGETS = Object.assign(W.CHEM8_WIDGETS || {}, {
|
||||
p1: mount_p1, p2: mount_p2, pr1: mount_pr1, p3: mount_p3,
|
||||
p4: mount_p4, p5: mount_p5, p6: mount_p6,
|
||||
p7: mount_p7, p8: mount_p8, p9: mount_p9,
|
||||
p10: mount_p10, lo1: mount_lo1, p11: mount_p11
|
||||
});
|
||||
W.FLAG_MOUNTS = Object.assign(W.FLAG_MOUNTS || {}, { p12: mount_p12 });
|
||||
})(window);
|
||||
@@ -0,0 +1,192 @@
|
||||
/* chem7_ch2_widgets.js — интерактивы главы 2 «Кислород» (Химия 7).
|
||||
* Монтируются движком chem8_engine.js: window.CHEM8_WIDGETS[id] / window.FLAG_MOUNTS[id].
|
||||
* Используют window.Chem8 (chem8_svg.js): chemEq, formula, molarMass, arOf.
|
||||
* Без эмоджи; KaTeX-рендер — через window.chem8RenderMath.
|
||||
*/
|
||||
(function (W) {
|
||||
'use strict';
|
||||
function C() { return W.Chem8 || {}; }
|
||||
function $(id) { return document.getElementById(id); }
|
||||
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function gcd(a, b) { return b ? gcd(b, a % b) : a; }
|
||||
function ceq(src, opts){ return C().chemEq ? C().chemEq(src, opts || {}) : esc(src); }
|
||||
|
||||
var COL = { H:'#cbd5e1', O:'#ef4444', N:'#3b82f6', C:'#334155', S:'#eab308', P:'#f97316', Fe:'#b45309', Mg:'#22c55e', Cu:'#ea580c' };
|
||||
function ball(el, x, r){
|
||||
return '<circle cx="'+x+'" cy="30" r="'+r+'" fill="'+(COL[el]||'#94a3b8')+'" stroke="rgba(0,0,0,.25)"/>'
|
||||
+ '<text x="'+x+'" y="35" text-anchor="middle" font-size="12" font-weight="700" fill="#fff">'+el+'</text>';
|
||||
}
|
||||
function molSvg(atoms){ // atoms: [['O',2]]
|
||||
var list=[]; atoms.forEach(function(p){ for(var i=0;i<p[1];i++) list.push(p[0]); });
|
||||
var x=22, svg=''; list.forEach(function(el){ x+=16; svg+=ball(el,x,15); x+=24; });
|
||||
return '<svg viewBox="0 0 '+(x+10)+' 60" width="100%" style="max-width:'+(x+10)+'px;height:auto">'+svg+'</svg>';
|
||||
}
|
||||
|
||||
/* §13 — состав воздуха (стопочная полоса с кликом) */
|
||||
var AIR = [
|
||||
{ g:'Азот N₂', p:78, c:'#3b82f6', note:'не поддерживает горение и дыхание' },
|
||||
{ g:'Кислород O₂', p:21, c:'#ef4444', note:'нужен для дыхания и горения' },
|
||||
{ g:'Другие газы (Ar, CO₂…)', p:1, c:'#94a3b8', note:'аргон, углекислый газ и др. — около 1 %' }
|
||||
];
|
||||
function mount_p13() {
|
||||
var m = $('p13-air'); if (!m || m._built) return; m._built = 1;
|
||||
var bar = AIR.map(function (a, i) {
|
||||
return '<div class="air-seg" data-i="' + i + '" style="width:' + a.p + '%;background:' + a.c + ';height:46px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:.82rem;cursor:pointer;border-right:2px solid var(--card)">' + a.p + '%</div>';
|
||||
}).join('');
|
||||
m.innerHTML = '<div style="display:flex;border-radius:9px;overflow:hidden;border:1.5px solid var(--border)">' + bar + '</div>'
|
||||
+ '<div class="out" id="p13-air-out" style="margin-top:8px">Кликни по части диаграммы, чтобы узнать о газе.</div>';
|
||||
var out = $('p13-air-out');
|
||||
m.querySelectorAll('.air-seg').forEach(function (s) {
|
||||
s.addEventListener('click', function () {
|
||||
var a = AIR[+s.dataset.i]; out.className = 'out ok';
|
||||
out.innerHTML = '<b>' + esc(a.g) + '</b> — около ' + a.p + ' % воздуха. ' + esc(a.note) + '.';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ЛО2 — выбор способа собирания газа */
|
||||
var GASES = [
|
||||
{ g:'Кислород O₂', heavier:true, ways:['вытеснением воды', 'в сосуд отверстием вверх (тяжелее воздуха)'] },
|
||||
{ g:'Водород H₂', heavier:false, ways:['вытеснением воды', 'в сосуд отверстием вниз (легче воздуха)'] },
|
||||
{ g:'Углекислый газ CO₂', heavier:true, ways:['в сосуд отверстием вверх (тяжелее воздуха)'] }
|
||||
];
|
||||
function mount_lo2() {
|
||||
var m = $('lo2-coll'); if (!m || m._built) return; m._built = 1;
|
||||
var idx = 0;
|
||||
function render(){
|
||||
var g = GASES[idx];
|
||||
m.innerHTML = '<div class="fld"><label>Газ</label><select id="lo2-pick">'
|
||||
+ GASES.map(function(x,i){ return '<option value="'+i+'"'+(i===idx?' selected':'')+'>'+esc(x.g)+'</option>'; }).join('') + '</select></div>'
|
||||
+ '<div class="out ok" style="margin-top:6px"><b>' + esc(g.g) + '</b> ' + (g.heavier?'тяжелее':'легче') + ' воздуха.<br>Способы собирания:<br>'
|
||||
+ g.ways.map(function(w){ return '✓ ' + esc(w); }).join('<br>') + '</div>';
|
||||
$('lo2-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); });
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
/* §14 — кислород: элемент или простое вещество + модели O₂ / O₃ */
|
||||
function mount_p14() {
|
||||
var m = $('p14-tog'); if (!m || m._built) return; m._built = 1;
|
||||
var mode = 'el';
|
||||
function render(){
|
||||
m.innerHTML = '<div class="fld"><button class="btn'+(mode==='el'?' primary':'')+'" id="p14-el">Элемент O</button>'
|
||||
+ '<button class="btn'+(mode==='o2'?' primary':'')+'" id="p14-o2">Простое вещество O₂</button>'
|
||||
+ '<button class="btn'+(mode==='o3'?' primary':'')+'" id="p14-o3">Озон O₃</button></div>'
|
||||
+ '<div class="out" style="margin-top:8px">' + (
|
||||
mode==='el' ? '<b>Кислород — химический элемент</b> (символ O, Z = 8, A_r = 16). Так говорят, когда речь о <i>атомах</i> кислорода в составе веществ (например, в воде H₂O).'
|
||||
: mode==='o2' ? '<b>Кислород — простое вещество</b> O₂: молекула из двух атомов. Газ без цвета и запаха, немного тяжелее воздуха.' + molSvg([['O',2]])
|
||||
: '<b>Озон</b> O₃: молекула из трёх атомов кислорода — другое простое вещество того же элемента.' + molSvg([['O',3]])
|
||||
) + '</div>';
|
||||
$('p14-el').addEventListener('click',function(){mode='el';m._built=0;render();});
|
||||
$('p14-o2').addEventListener('click',function(){mode='o2';m._built=0;render();});
|
||||
$('p14-o3').addEventListener('click',function(){mode='o3';m._built=0;render();});
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
/* §15 — симулятор горения: вещество + O₂ → оксид */
|
||||
var FUELS = [
|
||||
{ el:'C', name:'углерод', eq:'C + O2 = CO2', note:'горит с образованием углекислого газа' },
|
||||
{ el:'S', name:'сера', eq:'S + O2 = SO2', note:'горит синим пламенем, резкий запах' },
|
||||
{ el:'P', name:'фосфор', eq:'4P + 5O2 = 2P2O5', note:'горит ярко, белый дым' },
|
||||
{ el:'Fe', name:'железо', eq:'3Fe + 2O2 = Fe3O4', note:'горит, разбрасывая искры' },
|
||||
{ el:'Mg', name:'магний', eq:'2Mg + O2 = 2MgO', note:'ослепительно яркое пламя' }
|
||||
];
|
||||
function flame(){
|
||||
return '<svg viewBox="0 0 60 70" width="56" height="66" style="vertical-align:middle"><path d="M30 8 C40 26 48 34 38 52 C46 46 46 60 30 64 C14 60 14 46 22 52 C12 34 22 26 30 8 Z" fill="#f97316"/><path d="M30 22 C36 34 40 40 33 52 C39 48 38 58 30 60 C22 58 22 50 26 52 C20 40 26 34 30 22 Z" fill="#fde047"/></svg>';
|
||||
}
|
||||
function mount_p15() {
|
||||
var m = $('p15-burn'); if (!m || m._built) return; m._built = 1;
|
||||
var idx = 0;
|
||||
function render(){
|
||||
var f = FUELS[idx];
|
||||
m.innerHTML = '<div class="fld"><label>Вещество</label><select id="p15-pick">'
|
||||
+ FUELS.map(function(x,i){ return '<option value="'+i+'"'+(i===idx?' selected':'')+'>'+esc(x.name)+' ('+x.el+')</option>'; }).join('') + '</select>'
|
||||
+ '<button class="btn primary" id="p15-go">Поджечь в кислороде</button></div>'
|
||||
+ '<div class="out" id="p15-out" style="margin-top:8px">Выбери вещество и подожги его в кислороде.</div>';
|
||||
$('p15-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); });
|
||||
$('p15-go').addEventListener('click', function(){
|
||||
var out = $('p15-out'); out.className='out ok';
|
||||
out.innerHTML = flame() + ' <b>' + esc(f.name[0].toUpperCase()+f.name.slice(1)) + ' горит в кислороде:</b> ' + esc(f.note) + '.<br>'
|
||||
+ '<div style="margin-top:6px;font-size:1.05rem">' + ceq(f.eq) + '</div>'
|
||||
+ '<div style="font-size:.84rem;color:var(--muted);margin-top:4px">Продукт — <b>оксид</b> (соединение элемента с кислородом).</div>';
|
||||
});
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
/* §16 — конструктор оксида (элемент + валентность, кислород II) + классификатор */
|
||||
var OXEL = [ ['Na',1], ['Ca',2], ['Mg',2], ['Cu',2], ['Zn',2], ['Al',3], ['C',4], ['S',4], ['P',5] ];
|
||||
function mount_p16() {
|
||||
var b = $('p16-bld');
|
||||
if (b && !b._built) { b._built = 1;
|
||||
b.innerHTML = '<div class="fld"><label>Элемент</label><select id="p16-el">'
|
||||
+ OXEL.map(function(e,i){ return '<option value="'+i+'"'+(e[0]==='Al'?' selected':'')+'>'+e[0]+' (валентность '+rom(e[1])+')</option>'; }).join('')
|
||||
+ '</select> + кислород (O, II)</div><div class="out" id="p16-out"></div>';
|
||||
function rom(n){ return ['','I','II','III','IV','V'][n]; }
|
||||
function upd(){
|
||||
var e = OXEL[+$('p16-el').value], lcm = e[1]*2/gcd(e[1],2), ix=lcm/e[1], iy=lcm/2;
|
||||
var raw = e[0] + (ix>1?ix:'') + 'O' + (iy>1?iy:'');
|
||||
var out=$('p16-out'); out.className='out ok';
|
||||
out.innerHTML = '<span class="bd">Валентности: '+e[0]+' = '+rom(e[1])+', O = II; НОК = '+lcm+'<br>Формула оксида: <b style="font-size:1.15rem">'+(C().formula?C().formula(raw):raw)+'</b></span>';
|
||||
}
|
||||
$('p16-el').addEventListener('change',upd); upd();
|
||||
}
|
||||
var cl = $('p16-cls');
|
||||
if (cl && W.Chem7Classify) W.Chem7Classify(cl);
|
||||
}
|
||||
|
||||
/* §17 — схема получения кислорода + роль катализатора */
|
||||
var PROD = [
|
||||
{ name:'Разложение перманганата калия (при нагревании)', eq:'2KMnO4 = K2MnO4 + MnO2 + O2^', cond:'t°', note:'Реакция разложения: из одного вещества — несколько. Идёт при нагревании.' },
|
||||
{ name:'Разложение пероксида водорода (катализатор MnO₂)', eq:'2H2O2 = 2H2O + O2^', cond:'MnO₂', note:'MnO₂ — катализатор: ускоряет реакцию, но сам в ней не расходуется.' }
|
||||
];
|
||||
function mount_p17() {
|
||||
var m = $('p17-prod'); if (!m || m._built) return; m._built = 1;
|
||||
var idx = 0;
|
||||
function render(){
|
||||
var p = PROD[idx];
|
||||
m.innerHTML = '<div class="fld"><label>Способ</label><select id="p17-pick">'
|
||||
+ PROD.map(function(x,i){ return '<option value="'+i+'"'+(i===idx?' selected':'')+'>'+esc(x.name)+'</option>'; }).join('') + '</select></div>'
|
||||
+ '<div class="out ok" style="margin-top:8px"><div style="font-size:1.05rem">' + ceq(p.eq, {cond:p.cond}) + '</div>'
|
||||
+ '<div style="font-size:.86rem;color:var(--muted);margin-top:6px">' + esc(p.note) + '</div></div>';
|
||||
$('p17-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); });
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
/* ПР2 — проверка кислорода тлеющей лучинкой */
|
||||
function mount_pr2() {
|
||||
var m = $('pr2-test'); if (!m || m._built) return; m._built = 1;
|
||||
m.innerHTML = '<button class="btn primary" id="pr2-go">Внести тлеющую лучинку в сосуд с газом</button><div class="out" id="pr2-out" style="margin-top:8px">Как доказать, что собранный газ — кислород?</div>';
|
||||
$('pr2-go').addEventListener('click', function(){
|
||||
var out=$('pr2-out'); out.className='out ok';
|
||||
out.innerHTML = flame() + ' Тлеющая лучинка <b>ярко вспыхивает</b> — значит, газ поддерживает горение. Это <b>кислород</b>.';
|
||||
});
|
||||
}
|
||||
|
||||
/* классификатор «оксид / не оксид» (используется в §16) */
|
||||
W.Chem7Classify = function(mount){
|
||||
if (!mount || mount._built) return; mount._built = 1;
|
||||
var items = [ {t:'CuO',ox:1}, {t:'NaCl',ox:0}, {t:'CO₂',ox:1}, {t:'H₂SO₄',ox:0}, {t:'Fe₂O₃',ox:1}, {t:'HCl',ox:0}, {t:'SO₂',ox:1}, {t:'CaO',ox:1} ];
|
||||
var pool = items.map(function(_,i){return i;}), placed={}, sel=null;
|
||||
function render(){
|
||||
var chips = pool.map(function(i){ return '<button class="c7-chip" data-i="'+i+'" style="padding:7px 12px;border:1.5px solid var(--border);border-radius:9px;background:var(--card);color:var(--text);font-weight:600;cursor:pointer;margin:3px">'+esc(items[i].t)+'</button>'; }).join('') || '<span style="color:var(--muted)">Готово.</span>';
|
||||
var cols = [['Оксид',1],['Не оксид',0]].map(function(b){
|
||||
var inb = Object.keys(placed).filter(function(k){return placed[k]===b[1];});
|
||||
var cells = inb.map(function(k){ var ok=items[k].ox===b[1]; return '<div style="padding:5px 10px;margin:3px 0;border-radius:8px;color:#fff;font-weight:600;background:'+(ok?'#059669':'#dc2626')+'">'+esc(items[k].t)+(ok?' ✓':' ✗')+'</div>'; }).join('') || '<div style="color:var(--muted);font-size:.82rem">сюда…</div>';
|
||||
return '<div class="c7-bucket" data-b="'+b[1]+'" style="flex:1;min-width:130px;border:1.5px dashed var(--border);border-radius:11px;padding:10px;background:var(--pri-soft)"><div style="font-weight:700;margin-bottom:6px">'+b[0]+'</div>'+cells+'</div>';
|
||||
}).join('');
|
||||
mount.innerHTML = '<div style="margin-bottom:10px">'+chips+'</div><div style="display:flex;gap:10px;flex-wrap:wrap">'+cols+'</div>';
|
||||
mount.querySelectorAll('.c7-chip').forEach(function(b){ b.addEventListener('click',function(){ mount.querySelectorAll('.c7-chip').forEach(function(x){x.style.outline='';}); sel=+b.dataset.i; b.style.outline='2px solid var(--pri)'; }); });
|
||||
mount.querySelectorAll('.c7-bucket').forEach(function(col){ col.addEventListener('click',function(){ if(sel==null)return; placed[sel]=+col.dataset.b; pool=pool.filter(function(x){return x!==sel;}); sel=null; render(); }); });
|
||||
}
|
||||
render();
|
||||
};
|
||||
|
||||
W.CHEM8_WIDGETS = Object.assign(W.CHEM8_WIDGETS || {}, {
|
||||
p13: mount_p13, lo2: mount_lo2, p14: mount_p14, p15: mount_p15,
|
||||
p16: mount_p16, p17: mount_p17, pr2: mount_pr2
|
||||
});
|
||||
W.FLAG_MOUNTS = Object.assign(W.FLAG_MOUNTS || {}, {});
|
||||
})(window);
|
||||
@@ -0,0 +1,53 @@
|
||||
/* chem7_svg.js — наглядные химические примитивы для учебника «Химия 7».
|
||||
*
|
||||
* Неймспейс: window.Chem7.*
|
||||
* Это ТОНКАЯ надстройка над window.Chem8 (chem8_svg.js): рендер формул и уравнений,
|
||||
* ионы переиспользуются из Chem8; здесь добавляется только то, что специфично для
|
||||
* первого курса химии 7 класса (валентность, разделение смесей, признаки реакций,
|
||||
* весы сохранения массы, горение, конструктор оксида/соли/основания, состав воздуха,
|
||||
* разложение воды, массовая доля элемента).
|
||||
*
|
||||
* Молекулярные модели (структурные / 2D / 3D) — НЕ здесь, а через biochem-core.js.
|
||||
*
|
||||
* Phase 0: переэкспортированы примитивы Chem8 (formula, ionLabel, chemEq, molarMass,
|
||||
* equationBalancer и т. п.); собственные звёздные виджеты — заглушки, наполняются в
|
||||
* фазах 1–4 (см. plans/textbooks-7/PLAN_CHEMISTRY_7.md, разд. B).
|
||||
*
|
||||
* Правила (CLAUDE.md / план):
|
||||
* - без эмоджи, только inline SVG .ic;
|
||||
* - в KaTeX-шаблонах двойной backslash (\\to, \\uparrow, \\downarrow);
|
||||
* - drag/слайдеры: window-listeners + state ВЫШЕ redraw(), без setPointerCapture.
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
var C8 = global.Chem8 || {};
|
||||
function noop() { /* заглушка фазы 0 — реализуется в фазах 1–4 */ }
|
||||
|
||||
var Chem7 = {
|
||||
/* ── переиспользуем примитивы Chem8 (рендер формул/уравнений/ионов, M_r) ── */
|
||||
formula: C8.formula || function (s) { return s; },
|
||||
ionLabel: C8.ionLabel || function (s) { return s; },
|
||||
chemEq: C8.chemEq || function (s) { return s; },
|
||||
molarMass: C8.molarMass || function () { return NaN; },
|
||||
elementCounts: C8.elementCounts || function () { return null; },
|
||||
arOf: C8.arOf || function () { return ''; },
|
||||
fmt: C8.fmt || function (x) { return String(x).replace('.', ','); },
|
||||
equationBalancer: C8.equationBalancer || noop,
|
||||
|
||||
/* ── собственные звёздные виджеты Химии 7 (Phase 0: заглушки) ── */
|
||||
valenceBuilder: noop, // §9 — конструктор формулы по валентности (НОК индексов)
|
||||
elementSymbolDrill: noop, // §3 — тренажёр символ ↔ название
|
||||
atomBalance: noop, // §4 — «весы атомов», во сколько раз тяжелее
|
||||
mixtureSeparator: noop, // §2 — разделитель смесей (фильтр/выпаривание/магнит/дистилляция)
|
||||
reactionSigns: noop, // §10 — детектор признаков реакции
|
||||
massConservation: noop, // §11 — «весы сохранения массы»
|
||||
combustionSim: noop, // §15 — симулятор горения (вещество + O₂ → оксид)
|
||||
compoundBuilder: noop, // §16,22,24 — конструктор оксида/соли/основания
|
||||
airComposition: noop, // §13 — диаграмма состава воздуха
|
||||
waterDecomp: noop, // §23 — разложение воды (H₂:O₂ = 2:1)
|
||||
massFraction: noop // Прил.3/§8 — калькулятор массовой доли элемента
|
||||
};
|
||||
|
||||
global.Chem7 = Chem7;
|
||||
})(window);
|
||||
+118
-22
@@ -2545,13 +2545,13 @@ class BenchSim {
|
||||
this._drag = null;
|
||||
this._nextId = 1;
|
||||
// source: object arrow by default. `ang` (deg) aims point/single/laser/parallel.
|
||||
this.source = { kind: 'object', xf: 0.07, h: 70, spread: 0.32, rays: 9, ang: 0 };
|
||||
this.source = { kind: 'object', xf: 0.07, yf: 0, h: 70, spread: 0.32, rays: 9, ang: 0, rayMode: 'char' };
|
||||
// elements along the bench, positioned by x-fraction; centred on the axis
|
||||
this.elements = [
|
||||
this._mk('lens', { xf: 0.40, f: 130, ap: 95 }),
|
||||
this._mk('screen', { xf: 0.86 }),
|
||||
];
|
||||
this.selectedId = null;
|
||||
this.selectedId = '__src'; // source selected by default so its controls show on open
|
||||
this._bindEvents();
|
||||
this._ro = new ResizeObserver(() => { this.fit(); this.draw(); });
|
||||
this._ro.observe(canvas.parentElement || canvas);
|
||||
@@ -2602,7 +2602,7 @@ class BenchSim {
|
||||
this._redraw(); // canvas only — never rebuild the inspector mid-slider-drag
|
||||
}
|
||||
setSource(key, val) {
|
||||
this.source[key] = (key === 'kind') ? val : +val;
|
||||
this.source[key] = (key === 'kind' || key === 'rayMode') ? val : +val; // string keys vs numeric
|
||||
this._redraw();
|
||||
}
|
||||
getSelected() { return this.elements.find(e => e.id === this.selectedId) || null; }
|
||||
@@ -2612,16 +2612,17 @@ class BenchSim {
|
||||
/* ── geometry helpers ── */
|
||||
_ex(el) { return el.xf * this.W; }
|
||||
_ay() { return this.H / 2; }
|
||||
_sy() { return this._ay() + (this.source.yf || 0) * (this.H / 2 - 14); } // source y (vertical position)
|
||||
|
||||
/* Emit the initial rays from the source. */
|
||||
_emitRays() {
|
||||
const ay = this._ay();
|
||||
const ay = this._sy(); // emission height respects the source vertical position
|
||||
const sx = this.source.xf * this.W;
|
||||
const rays = [];
|
||||
// white light → one sub-ray per spectral sample (they coincide until a prism disperses them)
|
||||
const wls = window._obWhiteLight ? OB_SPECTRAL.map(s => s.nm) : [window._obWavelength || 540];
|
||||
const push = (x, y, ang) => {
|
||||
for (const wl of wls) rays.push({ x, y, dx: Math.cos(ang), dy: Math.sin(ang), wl, pts: [{ x, y }], alive: true, bounces: 0 });
|
||||
const push = (x, y, ang, role) => {
|
||||
for (const wl of wls) rays.push({ x, y, dx: Math.cos(ang), dy: Math.sin(ang), wl, role: role || null, pts: [{ x, y }], alive: true, bounces: 0 });
|
||||
};
|
||||
const aim = (this.source.ang || 0) * Math.PI / 180;
|
||||
if (this.source.kind === 'single') {
|
||||
@@ -2643,11 +2644,26 @@ class BenchSim {
|
||||
} else if (this.source.kind === 'point') {
|
||||
const n = this.source.rays, A = this.source.spread;
|
||||
for (let i = 0; i < n; i++) push(sx, ay, aim - A + 2 * A * (i / (n - 1)));
|
||||
} else { // object arrow: fan from tip and base
|
||||
const n = this.source.rays, A = this.source.spread;
|
||||
[ay - this.source.h, ay].forEach(y0 => {
|
||||
for (let i = 0; i < n; i++) push(sx, y0, -A + 2 * A * (i / (n - 1)));
|
||||
});
|
||||
} else { // object arrow
|
||||
const axis = this._ay(); // optical axis (lens centre / focus sit here)
|
||||
const tipY = ay - this.source.h, baseY = ay;
|
||||
const firstLens = this.elements.filter(e => e.type === 'lens').sort((a, b) => a.xf - b.xf)[0];
|
||||
if (this.source.rayMode === 'char' && firstLens) {
|
||||
// textbook construction: 2–3 characteristic rays from the tip + axial ray from the base
|
||||
const lensX = firstLens.xf * this.W, f = firstLens.f;
|
||||
const aimAt = (tx, ty) => Math.atan2(ty - tipY, tx - sx);
|
||||
push(sx, tipY, 0, 'char1'); // 1) parallel to axis → through far focus F'
|
||||
push(sx, tipY, aimAt(lensX, axis), 'char2'); // 2) through the optical centre → straight
|
||||
const Fx = lensX - f; // front focal point
|
||||
if (f > 0 && Fx > sx + 5) push(sx, tipY, aimAt(Fx, axis), 'char3'); // 3) through F → emerges parallel
|
||||
push(sx, baseY, 0, 'base'); // base lies on the axis
|
||||
} else {
|
||||
// physical bundle: a fan from tip and base
|
||||
const n = Math.max(2, this.source.rays | 0), A = this.source.spread;
|
||||
[tipY, baseY].forEach(y0 => {
|
||||
for (let i = 0; i < n; i++) push(sx, y0, -A + 2 * A * (i / (n - 1)));
|
||||
});
|
||||
}
|
||||
}
|
||||
return rays;
|
||||
}
|
||||
@@ -2794,8 +2810,15 @@ class BenchSim {
|
||||
// image formed on screens (where rays land)
|
||||
this._drawScreenHits(ctx, rays);
|
||||
|
||||
// textbook construction overlay (labels, image arrow, dashed extensions)
|
||||
if (this.source.kind === 'object' && (this.source.rayMode || 'char') === 'char') {
|
||||
this._drawCharConstruction(ctx, rays, ay);
|
||||
}
|
||||
|
||||
if (typeof _drawOBFXLayer === 'function') {
|
||||
_drawOBFXLayer(ctx, 'freebuild', { srcX: this.source.xf * W, srcY: ay - (this.source.h || 0) });
|
||||
// FX anchored at the actual source point (only the object arrow has a raised tip)
|
||||
const fxY = this._sy() - (this.source.kind === 'object' ? this.source.h : 0);
|
||||
_drawOBFXLayer(ctx, 'freebuild', { srcX: this.source.xf * W, srcY: fxY });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2819,7 +2842,61 @@ class BenchSim {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
_drawSource(ctx, ay) {
|
||||
_lineIntersect(a, b) {
|
||||
const den = a.dx * b.dy - a.dy * b.dx;
|
||||
if (Math.abs(den) < 1e-9) return null; // parallel → no finite intersection
|
||||
const t = ((b.x - a.x) * b.dy - (b.y - a.y) * b.dx) / den;
|
||||
return { x: a.x + t * a.dx, y: a.y + t * a.dy };
|
||||
}
|
||||
|
||||
// Textbook overlay for single-lens characteristic construction:
|
||||
// labels 1/2/3, the image arrow, and dashed back-extensions for a virtual image.
|
||||
_drawCharConstruction(ctx, rays, axis) {
|
||||
const lenses = this.elements.filter(e => e.type === 'lens');
|
||||
if (lenses.length !== 1) return; // construction is only clean for one lens
|
||||
const lensX = this._ex(lenses[0]);
|
||||
|
||||
ctx.save();
|
||||
// ── ray labels 1/2/3 near the object ──
|
||||
ctx.font = 'bold 11px Manrope, system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
const labelRay = (role, txt) => {
|
||||
const r = rays.find(x => x.role === role); if (!r || r.pts.length < 2) return;
|
||||
const p0 = r.pts[0], p1 = r.pts[1];
|
||||
const d = Math.hypot(p1.x - p0.x, p1.y - p0.y) || 1;
|
||||
const k = Math.min(40, d * 0.45);
|
||||
const lx = p0.x + (p1.x - p0.x) / d * k, ly = p0.y + (p1.y - p0.y) / d * k;
|
||||
ctx.fillStyle = 'rgba(13,13,26,0.7)'; ctx.beginPath(); ctx.arc(lx, ly, 8, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = '#7BF5A4'; ctx.fillText(txt, lx, ly);
|
||||
};
|
||||
labelRay('char1', '1'); labelRay('char2', '2'); labelRay('char3', '3');
|
||||
|
||||
// ── image point = intersection of the final segments of rays 1 and 2 ──
|
||||
const finalLine = (role) => {
|
||||
const r = rays.find(x => x.role === role); if (!r || r.pts.length < 2) return null;
|
||||
const b = r.pts[r.pts.length - 1], a = r.pts[r.pts.length - 2];
|
||||
return { x: a.x, y: a.y, dx: b.x - a.x, dy: b.y - a.y };
|
||||
};
|
||||
const L1 = finalLine('char1'), L2 = finalLine('char2');
|
||||
const P = (L1 && L2) ? this._lineIntersect(L1, L2) : null;
|
||||
if (P && isFinite(P.x) && isFinite(P.y)) {
|
||||
const real = P.x > lensX + 2; // real image forms to the right of the lens
|
||||
const col = real ? '#EF476F' : '#FFD166';
|
||||
if (!real) {
|
||||
// virtual image: extend the diverging rays backward (dashed) to the apparent source P
|
||||
ctx.strokeStyle = col; ctx.setLineDash([5, 4]); ctx.lineWidth = 1;
|
||||
[L1, L2].forEach(L => { if (!L) return; ctx.beginPath(); ctx.moveTo(L.x, L.y); ctx.lineTo(P.x, P.y); ctx.stroke(); });
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
this._arrow(ctx, P.x, axis, P.x, P.y, col); // image arrow: base (axis) → tip (P)
|
||||
ctx.fillStyle = col; ctx.beginPath(); ctx.arc(P.x, P.y, 3.5, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.font = '10px Manrope, system-ui, sans-serif';
|
||||
ctx.fillText(real ? 'изображение' : 'мнимое изобр.', P.x, P.y + (P.y < axis ? -14 : 16));
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
_drawSource(ctx, _ayIgnored) {
|
||||
const ay = this._sy(); // draw at the source vertical position
|
||||
const sx = this.source.xf * this.W;
|
||||
const aim = (this.source.ang || 0) * Math.PI / 180;
|
||||
ctx.save();
|
||||
@@ -2938,11 +3015,12 @@ class BenchSim {
|
||||
};
|
||||
const hit = (mx, my) => {
|
||||
const ay = this._ay();
|
||||
// source first (it can sit off-axis), grab around its actual vertical position
|
||||
const sx = this.source.xf * this.W, sy = this._sy();
|
||||
if (Math.abs(mx - sx) < 16 && Math.abs(my - sy) < 70) return { kind: 'src' };
|
||||
for (const el of this.elements) {
|
||||
if (Math.abs(mx - this._ex(el)) < 14 && Math.abs(my - ay) < 120) return { kind: 'el', id: el.id };
|
||||
}
|
||||
const sx = this.source.xf * this.W;
|
||||
if (Math.abs(mx - sx) < 16 && Math.abs(my - ay) < 120) return { kind: 'src' };
|
||||
return null;
|
||||
};
|
||||
on(cv, 'pointerdown', e => {
|
||||
@@ -2957,10 +3035,14 @@ class BenchSim {
|
||||
});
|
||||
on(cv, 'pointermove', e => {
|
||||
if (!this._drag) { const { mx, my } = pos(e); cv.style.cursor = hit(mx, my) ? 'grab' : 'default'; return; }
|
||||
const { mx } = pos(e);
|
||||
const { mx, my } = pos(e);
|
||||
const xf = Math.max(0.02, Math.min(0.98, mx / this.W));
|
||||
if (this._drag.kind === 'src') this.source.xf = xf;
|
||||
else { const el = this.elements.find(x => x.id === this._drag.id); if (el) el.xf = xf; }
|
||||
if (this._drag.kind === 'src') {
|
||||
this.source.xf = xf;
|
||||
// vertical drag too → move the source up/down off the axis
|
||||
const yf = (my - this._ay()) / (this.H / 2 - 14);
|
||||
this.source.yf = Math.max(-0.95, Math.min(0.95, yf));
|
||||
} else { const el = this.elements.find(x => x.id === this._drag.id); if (el) el.xf = xf; }
|
||||
this._redraw(); // position drag → redraw canvas, keep inspector intact
|
||||
});
|
||||
on(cv, 'pointerup', e => { this._drag = null; try { cv.releasePointerCapture(e.pointerId); } catch (_) {} });
|
||||
@@ -4444,9 +4526,20 @@ function _benchPropsHTML() {
|
||||
let h = '<div class="gp-section-title" style="margin:4px 0 6px">Источник</div>';
|
||||
h += _benchBtnRow(['object:Предмет', 'point:Точка', 'parallel:Параллель', 'single:Луч', 'laser:Лазер'],
|
||||
k => s.kind === k, k => "benchSourceKind('" + k + "')");
|
||||
if (s.kind === 'object') h += _benchCtl('Высота', 0, 'h', 20, 120, 2, s.h, true);
|
||||
h += _benchCtl('Положение ↕', 0, 'yf', -0.9, 0.9, 0.02, +(s.yf || 0).toFixed(2), true); // vertical position (any kind)
|
||||
if (s.kind === 'object') {
|
||||
h += _benchCtl('Размер стрелки', 0, 'h', 20, 120, 2, s.h, true);
|
||||
// ray mode: textbook characteristic rays vs physical bundle
|
||||
h += _benchBtnRow(['char:Характ. лучи', 'bundle:Пучок'],
|
||||
k => (s.rayMode || 'char') === k, k => "benchSourceParam('rayMode','" + k + "');_benchUpdateUI()");
|
||||
if ((s.rayMode || 'char') === 'bundle') {
|
||||
h += _benchCtl('Лучей', 0, 'rays', 3, 15, 1, s.rays, true);
|
||||
h += _benchCtl('Раствор', 0, 'spread', 0.1, 0.6, 0.02, s.spread, true);
|
||||
} else {
|
||||
h += '<div class="pp-hint">2–3 характеристических луча от вершины + осевой от основания (как в учебнике).</div>';
|
||||
}
|
||||
}
|
||||
if (s.kind === 'point') h += _benchCtl('Раствор', 0, 'spread', 0.1, 0.6, 0.02, s.spread, true);
|
||||
if (s.kind !== 'object' && s.kind !== 'parallel') {} // no spread for single/laser
|
||||
if (s.kind !== 'object') h += _benchCtl('Угол°', 0, 'ang', -60, 60, 1, s.ang || 0, true);
|
||||
return h;
|
||||
}
|
||||
@@ -4483,10 +4576,13 @@ function _benchUpdateUI() {
|
||||
if (!benchSim) return;
|
||||
const listEl = document.getElementById('bench-list');
|
||||
if (listEl) {
|
||||
listEl.innerHTML = benchSim.elements.map(e =>
|
||||
// permanent "Источник" chip so the source is always selectable (not only via canvas)
|
||||
const srcChip = '<button class="preset-btn' + (benchSim.selectedId === '__src' ? ' active' : '') +
|
||||
'" style="font-size:.68rem" onclick="benchSelect(\'__src\')">Источник</button>';
|
||||
listEl.innerHTML = srcChip + benchSim.elements.map(e =>
|
||||
'<button class="preset-btn' + (e.id === benchSim.selectedId ? ' active' : '') +
|
||||
'" style="font-size:.68rem" onclick="benchSelect(' + e.id + ')">' + _benchElName(e) + '</button>'
|
||||
).join('') || '<div class="pp-hint">Пусто</div>';
|
||||
).join('');
|
||||
}
|
||||
const propsEl = document.getElementById('bench-props');
|
||||
if (propsEl) propsEl.innerHTML = _benchPropsHTML();
|
||||
|
||||
@@ -838,8 +838,7 @@
|
||||
|
||||
const user = LS.getUser();
|
||||
document.getElementById('nav-user').textContent = user?.name || user?.email || '';
|
||||
document.getElementById('nav-avatar').textContent =
|
||||
(user?.name || 'LS').split(' ').slice(0,2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
|
||||
const isTeacher = ['admin','teacher'].includes(user?.role);
|
||||
LS.showBoardIfAllowed();
|
||||
|
||||
@@ -458,8 +458,7 @@
|
||||
|
||||
const user = LS.getUser();
|
||||
document.getElementById('nav-user').textContent = user?.name || user?.email || '';
|
||||
document.getElementById('nav-avatar').textContent =
|
||||
(user?.name || 'LS').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
|
||||
const isTeacher = ['admin', 'teacher'].includes(user?.role);
|
||||
if (!isTeacher) { location.href = '/dashboard'; throw new Error(); }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -172,8 +172,8 @@
|
||||
<script src="/js/mobile.js"></script>
|
||||
<script>
|
||||
(async function () {
|
||||
const user = LS.initPage();
|
||||
if (!user || (user.role !== 'teacher' && user.role !== 'admin')) {
|
||||
const { user, isTeacher } = LS.initPage();
|
||||
if (!user || !isTeacher) {
|
||||
location.href = '/dashboard'; return;
|
||||
}
|
||||
LS.showBoardIfAllowed();
|
||||
|
||||
+1
-2
@@ -605,8 +605,7 @@ let _petCooldownTimer = null;
|
||||
const user = LS.getUser();
|
||||
LS.applyRoleSidebar(user);
|
||||
if (user) {
|
||||
document.getElementById('nav-avatar').textContent =
|
||||
(user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
document.getElementById('nav-user').textContent = user.name || '—';
|
||||
LS.showBoardIfAllowed();
|
||||
}
|
||||
|
||||
@@ -1493,7 +1493,7 @@
|
||||
document.getElementById('nav-user').textContent = name;
|
||||
const ini = name.split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
document.getElementById('big-avatar').textContent = ini;
|
||||
document.getElementById('nav-avatar').textContent = ini;
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), { ...LS.getUser(), name });
|
||||
} catch(e) { showMsg(msg, LS.esc(e.message||'Ошибка'),'err'); }
|
||||
finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
@@ -346,8 +346,7 @@
|
||||
|
||||
const user = LS.getUser();
|
||||
document.getElementById('nav-user').textContent = user?.name || user?.email || '';
|
||||
document.getElementById('nav-avatar').textContent =
|
||||
(user?.name || 'LS').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
|
||||
const isTeacher = ['admin', 'teacher'].includes(user?.role);
|
||||
if (!isTeacher) { location.href = '/dashboard'; throw new Error(); }
|
||||
|
||||
@@ -873,7 +873,7 @@ async function init() {
|
||||
const user = LS.getUser?.();
|
||||
if (user) {
|
||||
document.getElementById('nav-user').textContent = user.name?.split(' ')[0] || '—';
|
||||
document.getElementById('nav-avatar').textContent = (user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
}
|
||||
habitats = await LS.get('/api/red-book/habitats').catch(() => []);
|
||||
renderTabs();
|
||||
|
||||
@@ -802,7 +802,7 @@ if (localStorage.getItem('ls_sb_collapsed') === '1') document.getElementById('ap
|
||||
const user = LS.getUser?.();
|
||||
if (user) {
|
||||
document.getElementById('nav-user').textContent = user.name?.split(' ')[0] || '—';
|
||||
document.getElementById('nav-avatar').textContent = (user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
}
|
||||
resize(); // init canvas size so raycaster works correctly
|
||||
loadGraph();
|
||||
|
||||
@@ -301,7 +301,7 @@ async function init() {
|
||||
const user = LS.getUser?.();
|
||||
if (user) {
|
||||
document.getElementById('nav-user').textContent = user.name?.split(' ')[0] || '—';
|
||||
document.getElementById('nav-avatar').textContent = (user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
}
|
||||
const data = await LS.get('/api/red-book/species?limit=100').catch(() => ({ species: [] }));
|
||||
allSpecies = data.species || [];
|
||||
|
||||
@@ -799,8 +799,7 @@ async function init() {
|
||||
const user = LS.getUser?.() || null;
|
||||
if (user) {
|
||||
document.getElementById('nav-user').textContent = user.name?.split(' ')[0] || 'Профиль';
|
||||
const initials = (user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
document.getElementById('nav-avatar').textContent = initials;
|
||||
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
|
||||
}
|
||||
|
||||
// Sidebar collapse
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
<!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>Химия 7 · Глава 1 · «Первоначальные химические понятия»</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@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<link rel="stylesheet" href="/css/chem8-textbook.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"></script>
|
||||
<script src="/js/api.js" defer></script>
|
||||
<script src="/js/xp.js" defer></script>
|
||||
<script src="/js/biochem-core.js" defer></script>
|
||||
<script src="/js/chem8_svg.js" defer></script>
|
||||
<script src="/js/chem7_svg.js" defer></script>
|
||||
<script src="/js/chem7_ch1_widgets.js" defer></script>
|
||||
<script src="/js/chem8_engine.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="hdr">
|
||||
<div class="hdr-row">
|
||||
<div>
|
||||
<h1>Химия 7 · Глава 1</h1>
|
||||
<div class="hdr-sub">Первоначальные химические понятия: вещество, атом, элемент, молекула, формула, валентность, реакция и уравнение</div>
|
||||
</div>
|
||||
<div class="hdr-side">
|
||||
<a href="/textbook/chemistry-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К главам</a>
|
||||
<button id="theme-btn" class="hdr-btn"><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">
|
||||
<h2>С чего начинается химия</h2>
|
||||
<p>Химия изучает вещества, их свойства и превращения. В этой главе вы научитесь главному «языку» химии: узнаете об атомах и химических элементах, научитесь записывать состав веществ формулами, определять валентность и составлять уравнения химических реакций.</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"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="psel">
|
||||
<div class="psel-title">Параграфы главы</div>
|
||||
<div id="psel-grid" class="psel-grid"></div>
|
||||
</section>
|
||||
|
||||
<section id="sec-p1" class="sec"><div class="sec-header"><span class="sec-num">§ 1</span><h2 class="sec-h">Химия — наука о веществах</h2></div><div id="p1-body"></div></section>
|
||||
<section id="sec-p2" class="sec"><div class="sec-header"><span class="sec-num">§ 2</span><h2 class="sec-h">Чистые вещества и смеси</h2></div><div id="p2-body"></div></section>
|
||||
<section id="sec-pr1" class="sec"><div class="sec-header"><span class="sec-num">ПР 1</span><h2 class="sec-h">Практическая работа: знакомство с лабораторией. Разделение смесей</h2></div><div id="pr1-body"></div></section>
|
||||
<section id="sec-p3" class="sec"><div class="sec-header"><span class="sec-num">§ 3</span><h2 class="sec-h">Атомы. Химические элементы</h2></div><div id="p3-body"></div></section>
|
||||
<section id="sec-p4" class="sec"><div class="sec-header"><span class="sec-num">§ 4</span><h2 class="sec-h">Относительная атомная масса химических элементов</h2></div><div id="p4-body"></div></section>
|
||||
<section id="sec-p5" class="sec"><div class="sec-header"><span class="sec-num">§ 5</span><h2 class="sec-h">Молекулы. Простые вещества</h2></div><div id="p5-body"></div></section>
|
||||
<section id="sec-p6" class="sec"><div class="sec-header"><span class="sec-num">§ 6</span><h2 class="sec-h">Сложные вещества</h2></div><div id="p6-body"></div></section>
|
||||
<section id="sec-p7" class="sec"><div class="sec-header"><span class="sec-num">§ 7</span><h2 class="sec-h">Химическая формула</h2></div><div id="p7-body"></div></section>
|
||||
<section id="sec-p8" class="sec"><div class="sec-header"><span class="sec-num">§ 8</span><h2 class="sec-h">Относительная молекулярная масса</h2></div><div id="p8-body"></div></section>
|
||||
<section id="sec-p9" class="sec"><div class="sec-header"><span class="sec-num">§ 9</span><h2 class="sec-h">Валентность</h2></div><div id="p9-body"></div></section>
|
||||
<section id="sec-p10" class="sec"><div class="sec-header"><span class="sec-num">§ 10</span><h2 class="sec-h">Явления физические и химические. Признаки химических реакций</h2></div><div id="p10-body"></div></section>
|
||||
<section id="sec-lo1" class="sec"><div class="sec-header"><span class="sec-num">Лаб. 1</span><h2 class="sec-h">Лабораторный опыт: признаки протекания химических реакций</h2></div><div id="lo1-body"></div></section>
|
||||
<section id="sec-p11" class="sec"><div class="sec-header"><span class="sec-num">§ 11</span><h2 class="sec-h">Закон сохранения массы веществ. Химические уравнения</h2></div><div id="p11-body"></div></section>
|
||||
<section id="sec-p12" class="sec"><div class="sec-header"><span class="sec-num">§ 12</span><h2 class="sec-h">Составление уравнений химических реакций</h2></div><div id="p12-body"></div></section>
|
||||
<section id="sec-final1" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final1-body"></div></section>
|
||||
|
||||
</div>
|
||||
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||
</main>
|
||||
|
||||
<footer class="foot">Интерактивный учебник «Химия — 7 класс» · Глава 1 · «Первоначальные химические понятия» · LearnSpace</footer>
|
||||
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
window.CHEM8_CFG = { slug:'chemistry-7-ch1', themeKey:'chemistry7_theme', xpKey:'chemistry7_xp',
|
||||
progKey:'chemistry7_ch1_progress', achKey:'chemistry7_ch1_ach', hubHref:'/textbook/chemistry-7' };
|
||||
|
||||
window.PARAS = [
|
||||
{id:'p1', num:'§ 1', name:'Химия — наука о веществах', sub:'вещество · свойства'},
|
||||
{id:'p2', num:'§ 2', name:'Чистые вещества и смеси', sub:'разделение смесей'},
|
||||
{id:'pr1', num:'ПР 1', name:'Практическая работа', sub:'разделение смесей'},
|
||||
{id:'p3', num:'§ 3', name:'Атомы. Химические элементы', sub:'символы элементов'},
|
||||
{id:'p4', num:'§ 4', name:'Относительная атомная масса', sub:'$A_r$'},
|
||||
{id:'p5', num:'§ 5', name:'Молекулы. Простые вещества', sub:'$O_2$, $H_2$'},
|
||||
{id:'p6', num:'§ 6', name:'Сложные вещества', sub:'$H_2O$, $CO_2$'},
|
||||
{id:'p7', num:'§ 7', name:'Химическая формула', sub:'индекс · состав'},
|
||||
{id:'p8', num:'§ 8', name:'Относительная молекулярная масса', sub:'$M_r=\\sum A_r$'},
|
||||
{id:'p9', num:'§ 9', name:'Валентность', sub:'H — I, O — II'},
|
||||
{id:'p10', num:'§ 10', name:'Физические и химические явления', sub:'признаки реакций'},
|
||||
{id:'lo1', num:'Лаб. 1', name:'Признаки реакций', sub:'опыт'},
|
||||
{id:'p11', num:'§ 11', name:'Закон сохранения массы', sub:'уравнения'},
|
||||
{id:'p12', num:'§ 12', name:'Составление уравнений', sub:'коэффициенты'},
|
||||
{id:'final1', num:'★', name:'Финал главы', sub:'босс · ачивка', final:true}
|
||||
];
|
||||
|
||||
window.ACH_LABELS = { start:'Начало главы 1!', p1_done:'§1 изучен!', p2_done:'§2 изучен!',
|
||||
pr1_done:'Практическая работа 1 выполнена!', p3_done:'§3 изучен!',
|
||||
p4_done:'§4 изучен!', p5_done:'§5 изучен!', p6_done:'§6 изучен!',
|
||||
p7_done:'§7 изучен!', p8_done:'§8 изучен!', p9_done:'§9 изучен!',
|
||||
p10_done:'§10 изучен!', lo1_done:'Лабораторный опыт 1 выполнен!', p11_done:'§11 изучен!', p12_done:'§12 изучен!',
|
||||
final1_tasks:'Глава 1 пройдена! Вы — Мастер первоначальных понятий!' };
|
||||
window.SIDEBARS = {
|
||||
p1:{ title:'Шпаргалка §1', rows:[['Вещество','то, из чего состоит тело'],['Тело','предмет из вещества'],['Свойства','цвет, запах, плотность, $t_{пл}$…']] },
|
||||
p2:{ title:'Шпаргалка §2', rows:[['Чистое','постоянный состав'],['Смесь','2+ вещества'],['Разделение','по различию свойств']] },
|
||||
pr1:{ title:'Практическая 1', rows:[['Цель','разделить смесь'],['Соль+песок','раствор → фильтр → выпаривание'],['ТБ','аккуратно с нагревом']] },
|
||||
p3:{ title:'Шпаргалка §3', rows:[['Атом','мельчайшая частица'],['Элемент','атомы с одинаковым $Z$'],['Символ','H, O, Fe, Cu…']] },
|
||||
p4:{ title:'Шпаргалка §4', rows:[['$A_r$','во сколько раз тяжелее'],['Эталон','$1/12$ массы $^{12}$C'],['Пример','$A_r(\\text{O})=16$']] },
|
||||
p5:{ title:'Шпаргалка §5', rows:[['Молекула','частица из атомов'],['Простое','1 элемент: $O_2$, $H_2$'],['Атомность','$O_2$, $O_3$']] },
|
||||
p6:{ title:'Шпаргалка §6', rows:[['Сложное','разные элементы'],['Примеры','$H_2O$, $CO_2$, $NH_3$'],['Состав','можно разложить']] },
|
||||
p7:{ title:'Шпаргалка §7', rows:[['Индекс','число атомов'],['Коэффициент','число молекул'],['Состав','качеств. + количеств.']] },
|
||||
p8:{ title:'Шпаргалка §8', rows:[['$M_r$','$=\\sum A_r$'],['$M_r(H_2O)$','18'],['$M_r(H_2SO_4)$','98']] },
|
||||
p9:{ title:'Шпаргалка §9', rows:[['Валентность','число связей'],['H — I, O — II',''],['Формула','по НОК валентностей']] },
|
||||
p10:{ title:'Шпаргалка §10', rows:[['Физическое','форма/состояние'],['Химическое','новые вещества'],['Признаки','цвет, газ, осадок, запах, тепло']] },
|
||||
lo1:{ title:'Лаб. опыт 1', rows:[['Цель','наблюдать признаки реакций'],['Признак','новое вещество']] },
|
||||
p11:{ title:'Шпаргалка §11', rows:[['Закон','масса сохраняется'],['$m$ реаг.','= $m$ прод.'],['Авторы','Ломоносов, Лавуазье']] },
|
||||
p12:{ title:'Шпаргалка §12', rows:[['Уравнивают','коэффициентами'],['Индексы','не трогать'],['Баланс','атомы слева = справа']] },
|
||||
final1:{ title:'Финал главы 1', rows:[['§§1–12','все понятия'],['Награда','ачивка + XP']] }
|
||||
};
|
||||
window.TIPS = [
|
||||
{ sec:'p1', html:'Тело — это <b>предмет</b> (гвоздь, стакан), а вещество — <b>то, из чего</b> он сделан (железо, стекло). Из одного вещества можно сделать много тел.' },
|
||||
{ sec:'p2', html:'Способ разделения подбирают по тому, <b>чем различаются</b> вещества смеси: растворимостью, магнитными свойствами, температурой кипения, плотностью.' },
|
||||
{ sec:'pr1', html:'Соль растворяется в воде, а песок — нет. Сначала отфильтруй песок, потом выпари воду — на дне останется соль.' },
|
||||
{ sec:'p3', html:'Химический элемент определяется <b>зарядом ядра</b> (числом протонов) — это и есть порядковый номер $Z$.' },
|
||||
{ sec:'p4', html:'$A_r$ показывает, во сколько раз масса атома больше $1/12$ массы атома углерода-12. $A_r(\\text{H})=1$, $A_r(\\text{O})=16$, $A_r(\\text{Fe})=56$.' },
|
||||
{ sec:'p5', html:'<b>Простое</b> вещество — атомы одного элемента ($O_2$, $Fe$). Кислород $O_2$ и озон $O_3$ — разные простые вещества одного и того же элемента.' },
|
||||
{ sec:'p6', html:'<b>Сложное</b> вещество образовано атомами <b>разных</b> элементов ($H_2O$ — водород и кислород) и может быть разложено на простые.' },
|
||||
{ sec:'p7', html:'Индекс относится к атому/группе слева от него. В $H_2SO_4$: 2 атома H, 1 атом S, 4 атома O. Коэффициент (число перед формулой) — это число молекул.' },
|
||||
{ sec:'p8', html:'$M_r$ — сумма $A_r$ всех атомов формулы. $M_r(\\text{CO}_2)=12+2\\cdot16=44$.' },
|
||||
{ sec:'p9', html:'Кислород в соединениях имеет валентность II, водород — I. Зная их, можно определить валентность другого элемента и составить формулу по НОК.' },
|
||||
{ sec:'p10', html:'Признаки химической реакции: изменение цвета, выделение газа, образование осадка, появление запаха, выделение или поглощение тепла и света.' },
|
||||
{ sec:'lo1', html:'Если после действия появились новые вещества (изменился цвет, выпал осадок, выделился газ) — произошла химическая реакция.' },
|
||||
{ sec:'p11', html:'Атомы в реакции не исчезают и не появляются, поэтому масса веществ <b>до</b> реакции равна массе <b>после</b> (М. В. Ломоносов, А. Лавуазье).' },
|
||||
{ sec:'p12', html:'Уравнивают реакцию только <b>коэффициентами</b> (числами перед формулами). Индексы внутри формул менять нельзя.' },
|
||||
{ sec:'final1', html:'Собери всё: вещество/смесь, атом/элемент, формула, $M_r$, валентность, признаки реакции, закон сохранения, уравнение.' }
|
||||
];
|
||||
|
||||
/* ── задачи (тренажёр) ── */
|
||||
window.POOLS = {
|
||||
p1:[
|
||||
{q:'Что изучает химия?',opts:['Только движение тел','Вещества, их свойства и превращения','Только живые организмы','Звёзды и планеты'],a:1,ex:'Химия — наука о веществах, их свойствах и превращениях.'},
|
||||
{q:'Что из перечисленного — физическое тело (а не вещество)?',opts:['Вода','Медь','Гвоздь','Кислород'],a:2,ex:'Гвоздь — предмет (тело); медь, вода, кислород — вещества.'},
|
||||
{q:'«Железо» — это…',opts:['Физическое тело','Вещество','Смесь','Явление'],a:1,ex:'Железо — вещество, из которого можно сделать разные тела.'},
|
||||
{q:'Какое из свойств НЕ является свойством вещества?',opts:['Цвет','Плотность','Растворимость','Номер дома'],a:3,ex:'Цвет, плотность, растворимость — свойства вещества; номер дома — нет.'}
|
||||
],
|
||||
p2:[
|
||||
{q:'Смесь песка и воды — какая?',opts:['Однородная','Неоднородная','Чистое вещество','Раствор'],a:1,ex:'Песчинки видны и не растворяются — смесь неоднородная.'},
|
||||
{q:'Как разделить раствор соли в воде, чтобы получить соль?',opts:['Фильтрованием','Выпариванием','Магнитом','Никак'],a:1,ex:'Вода испаряется при нагревании, соль остаётся.'},
|
||||
{q:'Раствор сахара в воде — это…',opts:['Чистое вещество','Однородная смесь','Неоднородная смесь','Простое вещество'],a:1,ex:'Сахар растворён равномерно — однородная смесь (раствор).'},
|
||||
{q:'Чем удобно разделить смесь железных опилок и серы?',opts:['Магнитом','Выпариванием','Перегонкой','Фильтрованием'],a:0,ex:'Железо притягивается магнитом, сера — нет.'}
|
||||
],
|
||||
p3:[
|
||||
{q:'Атом — это…',opts:['Самое крупное тело','Мельчайшая химически неделимая частица','Смесь веществ','Молекула воды'],a:1,ex:'Атом — мельчайшая химически неделимая частица вещества.'},
|
||||
{q:'Химический элемент — это…',opts:['Любая частица','Вид атомов с одинаковым зарядом ядра','Молекула','Смесь атомов'],a:1,ex:'Элемент определяется зарядом ядра (числом протонов).'},
|
||||
{q:'Каков порядковый номер $Z$ кислорода?',hint:'число протонов в ядре',unit:'',a:8,ex:'У кислорода Z = 8.'},
|
||||
{q:'Какой символ у железа?',opts:['Fe','Ir','F','Zn'],a:0,ex:'Железо — Fe (от лат. ferrum).'}
|
||||
],
|
||||
p4:[
|
||||
{q:'Что показывает относительная атомная масса $A_r$?',opts:['Массу атома в граммах','Во сколько раз масса атома больше $1/12$ массы атома $^{12}$C','Число протонов','Число молекул'],a:1,ex:'$A_r$ — сравнение массы атома с $1/12$ массы атома углерода-12.'},
|
||||
{q:'$A_r(\\text{S})=32$, $A_r(\\text{O})=16$. Во сколько раз атом серы тяжелее атома кислорода?',hint:'$32/16$',unit:'раза',a:2,ex:'$32/16=2$.'},
|
||||
{q:'$A_r(\\text{Mg})=24$, $A_r(\\text{C})=12$. Во сколько раз атом магния тяжелее атома углерода?',hint:'$24/12$',unit:'раза',a:2,ex:'$24/12=2$.'},
|
||||
{q:'Относительная атомная масса $A_r$…',opts:['Измеряется в граммах','Безразмерна','Измеряется в литрах','Равна числу нейтронов'],a:1,ex:'$A_r$ — безразмерная величина сравнения.'}
|
||||
],
|
||||
p5:[
|
||||
{q:'Молекула $\\text{O}_2$ образует…',opts:['Сложное вещество','Простое вещество','Смесь','Раствор'],a:1,ex:'Атомы одного элемента — простое вещество.'},
|
||||
{q:'Простое вещество — это вещество, образованное…',opts:['Атомами одного элемента','Атомами разных элементов','Только молекулами','Только смесями'],a:0,ex:'Простое — один вид атомов.'},
|
||||
{q:'Кислород $\\text{O}_2$ и озон $\\text{O}_3$ — это…',opts:['Одно и то же вещество','Разные простые вещества одного элемента','Сложные вещества','Смесь газов'],a:1,ex:'Один элемент — кислород, но разные простые вещества.'},
|
||||
{q:'Сколько атомов в молекуле озона $\\text{O}_3$?',hint:'смотри индекс',unit:'',a:3,ex:'Озон $O_3$ — три атома кислорода.'}
|
||||
],
|
||||
p6:[
|
||||
{q:'Вода $\\text{H}_2\\text{O}$ — это…',opts:['Простое вещество','Сложное вещество','Смесь','Атом'],a:1,ex:'Атомы разных элементов (H и O) — сложное вещество.'},
|
||||
{q:'Сложное вещество образовано…',opts:['Атомами одного элемента','Атомами разных элементов','Только смесью','Одним атомом'],a:1,ex:'Сложное — разные элементы.'},
|
||||
{q:'Какое из веществ — простое?',opts:['$\\text{H}_2\\text{O}$','$\\text{CO}_2$','$\\text{N}_2$','$\\text{NH}_3$'],a:2,ex:'$N_2$ — один элемент (азот).'},
|
||||
{q:'Сколько разных химических элементов в молекуле метана $\\text{CH}_4$?',hint:'углерод и водород',unit:'',a:2,ex:'C и H — два элемента.'}
|
||||
],
|
||||
p7:[
|
||||
{q:'Что показывает индекс в химической формуле?',opts:['Число молекул','Число атомов элемента','Массу вещества','Заряд'],a:1,ex:'Индекс — число атомов элемента (или группы) в формуле.'},
|
||||
{q:'Сколько атомов кислорода в формуле $\\text{H}_2\\text{SO}_4$?',hint:'индекс при O',unit:'',a:4,ex:'4 атома кислорода.'},
|
||||
{q:'Сколько всего атомов в молекуле воды $\\text{H}_2\\text{O}$?',hint:'2 H + 1 O',unit:'',a:3,ex:'2 + 1 = 3 атома.'},
|
||||
{q:'Число, стоящее перед формулой (коэффициент), показывает…',opts:['Число атомов','Число молекул','Валентность','Массу'],a:1,ex:'Коэффициент — число молекул вещества.'}
|
||||
],
|
||||
p8:[
|
||||
{q:'Чему равна $M_r(\\text{H}_2\\text{O})$?',hint:'$2\\cdot1+16$',unit:'',a:18,ex:'$M_r=18$.'},
|
||||
{q:'Чему равна $M_r(\\text{CO}_2)$?',hint:'$12+2\\cdot16$',unit:'',a:44,ex:'$12+32=44$.'},
|
||||
{q:'Чему равна $M_r(\\text{H}_2\\text{SO}_4)$?',hint:'$2+32+4\\cdot16$',unit:'',a:98,ex:'$2+32+64=98$.'},
|
||||
{q:'Чему равна $M_r(\\text{CaCO}_3)$?',hint:'$40+12+3\\cdot16$',unit:'',a:100,ex:'$40+12+48=100$.'}
|
||||
],
|
||||
p9:[
|
||||
{q:'За единицу валентности принята валентность атома…',opts:['кислорода','водорода','углерода','железа'],a:1,ex:'Единица валентности — валентность водорода (I).'},
|
||||
{q:'Какова валентность кислорода в соединениях?',hint:'постоянная',unit:'',a:2,ex:'Кислород почти всегда двухвалентен (II).'},
|
||||
{q:'Какова валентность хлора в молекуле $\\text{HCl}$?',hint:'равна числу атомов H',unit:'',a:1,ex:'Хлор соединён с 1 атомом H → валентность I.'},
|
||||
{q:'Какова формула оксида алюминия (Al — III, O — II)?',opts:['AlO','Al₂O₃','AlO₂','Al₃O₂'],a:1,ex:'НОК(3,2)=6 → индексы 2 и 3 → Al₂O₃.'}
|
||||
],
|
||||
p10:[
|
||||
{q:'Что НЕ является признаком химической реакции?',opts:['Изменение цвета','Выделение газа','Образование осадка','Изменение формы предмета'],a:3,ex:'Изменение формы — физическое изменение, не признак реакции.'},
|
||||
{q:'Таяние льда — это явление…',opts:['Химическое','Физическое','Реакция','Горение'],a:1,ex:'Вода не превращается в другое вещество — физическое явление.'},
|
||||
{q:'Ржавление железа — это явление…',opts:['Физическое','Химическое','Растворение','Плавление'],a:1,ex:'Образуется новое вещество (ржавчина) — химическое явление.'},
|
||||
{q:'Появление резкого запаха при горении серы — это…',opts:['Признак химической реакции','Физическое изменение','Не имеет значения','Изменение формы'],a:0,ex:'Образовался новый газ — признак реакции.'}
|
||||
],
|
||||
p11:[
|
||||
{q:'Закон сохранения массы означает, что…',opts:['Масса всегда растёт','Масса реагентов равна массе продуктов','Атомы исчезают','Масса уменьшается'],a:1,ex:'Атомы не исчезают — масса сохраняется.'},
|
||||
{q:'При реакции $4$ г водорода с $32$ г кислорода сколько граммов воды образуется?',hint:'$4+32$',unit:'г',a:36,ex:'$4+32=36$ г.'},
|
||||
{q:'Сожгли $12$ г углерода, получили $44$ г $\\text{CO}_2$. Сколько граммов кислорода вступило?',hint:'$44-12$',unit:'г',a:32,ex:'$44-12=32$ г.'},
|
||||
{q:'Кто открыл закон сохранения массы веществ?',opts:['Д. И. Менделеев','М. В. Ломоносов','И. Ньютон','Архимед'],a:1,ex:'М. В. Ломоносов (и позже А. Лавуазье).'}
|
||||
],
|
||||
p12:[
|
||||
{q:'В уравнении $2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$ коэффициент перед $\\text{H}_2\\text{O}$ равен…',hint:'смотри на воду',unit:'',a:2,ex:'Коэффициент 2.'},
|
||||
{q:'Что можно изменять при уравнивании реакции?',opts:['Только индексы','Только коэффициенты','И индексы, и коэффициенты','Ничего'],a:1,ex:'Уравнивают только коэффициентами.'},
|
||||
{q:'В уравнении $3\\text{Fe}+2\\text{O}_2=\\text{Fe}_3\\text{O}_4$ коэффициент перед $\\text{Fe}$ равен…',hint:'железо слева',unit:'',a:3,ex:'Коэффициент 3.'},
|
||||
{q:'Если в реакции 2 атома фосфора, сколько их должно быть в продуктах?',hint:'закон сохранения',unit:'',a:2,ex:'Столько же — атомы не исчезают.'}
|
||||
],
|
||||
final1:[
|
||||
{q:'$M_r(\\text{H}_2\\text{SO}_4)=?$',hint:'$2+32+64$',unit:'',a:98,ex:'$98$.'},
|
||||
{q:'Валентность серы в $\\text{SO}_2$ (кислород — II)?',hint:'$2\\cdot\\text{II}$',unit:'',a:4,ex:'IV.'},
|
||||
{q:'Коэффициент перед $\\text{H}_2$ в $2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$?',hint:'',unit:'',a:2,ex:'2.'},
|
||||
{q:'Сколько атомов кислорода в формуле $\\text{Fe}_3\\text{O}_4$?',hint:'индекс при O',unit:'',a:4,ex:'4.'},
|
||||
{q:'$\\text{N}_2$ — простое или сложное вещество?',opts:['Простое','Сложное'],a:0,ex:'Один элемент — простое.'},
|
||||
{q:'Сожгли $4$ г серы, получили $8$ г $\\text{SO}_2$. Сколько граммов кислорода вступило?',hint:'$8-4$',unit:'г',a:4,ex:'$8-4=4$ г.'}
|
||||
]
|
||||
};
|
||||
|
||||
/* ── вспомогательные конструкторы контента ── */
|
||||
function rememberBox(items){
|
||||
return '<div class="remember-box"><div class="remember-box-title">'
|
||||
+'<svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Запомни!</div><ul>'
|
||||
+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ul></div>';
|
||||
}
|
||||
function qList(items){
|
||||
return '<div class="section-title">Вопросы и задания</div><ol class="q-list">'
|
||||
+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ol>';
|
||||
}
|
||||
function wgt(title, inner){
|
||||
return '<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg> '+title+'</div>'+inner+'</div>';
|
||||
}
|
||||
|
||||
/* ── BUILDERS: реальные (Волна 1) + заглушки для остальных ── */
|
||||
function build_p1(){
|
||||
document.getElementById('p1-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 1 · Химия 7</div><h2>Химия — наука о веществах</h2>'
|
||||
+'<div class="ph-desc">С чего начинается химия: чем тело отличается от вещества и как химики изучают вещества.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">вещество</span><span class="ph-tag">тело</span><span class="ph-tag">свойства</span></div></div>'
|
||||
+makeCard('theory','Вещества и тела','§1','<p><b>Химия</b> — наука о веществах, их свойствах и превращениях. <b>Физическое тело</b> — это предмет (гвоздь, стакан, ложка). <b>Вещество</b> — то, из чего состоит тело (железо, стекло, вода).</p>'
|
||||
+'<div class="def-box">Из <b>одного</b> вещества можно изготовить <b>много разных</b> тел: из стекла — стакан, колбу, линзу. И наоборот, одно тело может состоять из нескольких веществ.</div>')
|
||||
+makeCard('theory','Свойства веществ','§1','<p>Каждое вещество узнают по его <b>свойствам</b>: цвет, запах, агрегатное состояние, плотность, температуры плавления и кипения, растворимость в воде, тепло- и электропроводность.</p>'
|
||||
+'<p>По набору свойств одно вещество отличают от другого: например, медь — красная и проводит ток, а сера — жёлтая и ток не проводит.</p>')
|
||||
+makeCard('rule','Наблюдение и эксперимент','§1','<p>Химия — <b>экспериментальная</b> наука. Главные методы — <b>наблюдение</b> и <b>опыт (эксперимент)</b>. Опыты проводят в химической лаборатории, используя оборудование: пробирки, колбы, стаканы, спиртовку, штатив. Работать нужно по <b>правилам техники безопасности</b>.</p>')
|
||||
+wgt('Распредели: физическое тело или вещество?','<div id="p1-cls"></div>')
|
||||
+rememberBox(['Тело — предмет; вещество — то, из чего он сделан.','Вещество узнают по свойствам (цвет, плотность, растворимость…).','Химия изучает вещества опытным путём в лаборатории.'])
|
||||
+qList(['Чем тело отличается от вещества? Приведи примеры.','Назови 3 свойства, по которым можно отличить медь от серы.','Почему в лаборатории важно соблюдать правила безопасности?'])
|
||||
+secNav(null,'p2')+readButton('p1');
|
||||
wireReadBtn('p1');
|
||||
}
|
||||
|
||||
function build_p2(){
|
||||
document.getElementById('p2-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 2 · Химия 7</div><h2>Чистые вещества и смеси</h2>'
|
||||
+'<div class="ph-desc">Чем чистое вещество отличается от смеси и как смеси разделяют на составные части.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">смесь</span><span class="ph-tag">однородная</span><span class="ph-tag">разделение</span></div></div>'
|
||||
+makeCard('theory','Чистое вещество и смесь','§2','<p><b>Чистое вещество</b> имеет постоянный состав и постоянные свойства. <b>Смесь</b> состоит из двух и более веществ, и её свойства зависят от состава.</p>'
|
||||
+'<div class="def-box"><b>Однородные</b> смеси: состав одинаков во всём объёме, частицы не видны (раствор соли или сахара в воде, воздух). <b>Неоднородные</b>: составные части видны (песок в воде, смесь железа и серы).</div>')
|
||||
+makeCard('rule','Способы разделения смесей','§2','<p>Смеси разделяют, используя <b>различие свойств</b> веществ:</p>'
|
||||
+'<ul><li><b>Фильтрование</b> — нерастворимое отделяют от жидкости.</li><li><b>Выпаривание</b> — выделяют растворённое вещество, испаряя воду.</li><li><b>Отстаивание</b> (делительная воронка) — несмешивающиеся жидкости разной плотности.</li><li><b>Перегонка (дистилляция)</b> — вещества с разной температурой кипения.</li><li><b>Действие магнитом</b> — если одно вещество притягивается.</li></ul>')
|
||||
+makeCard('example','Разделение смеси соли и песка',null,'<div class="exa-step">1) Добавить воду — соль растворится, песок нет. 2) <b>Фильтрование</b> — песок останется на фильтре. 3) <b>Выпаривание</b> фильтрата — получим чистую соль.</div>')
|
||||
+wgt('Подбери способ разделения смеси','<div id="p2-sep"></div>')
|
||||
+rememberBox(['Чистое вещество — постоянный состав и свойства.','Смеси бывают однородные и неоднородные.','Способ разделения выбирают по различию свойств веществ.'])
|
||||
+qList(['Приведи пример однородной и неоднородной смеси.','Как разделить смесь воды и растительного масла?','Почему смесь соли и песка нельзя разделить только фильтрованием?'])
|
||||
+secNav('p1','pr1')+readButton('p2');
|
||||
wireReadBtn('p2');
|
||||
}
|
||||
|
||||
function build_pr1(){
|
||||
document.getElementById('pr1-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">Практическая работа 1</div><h2>Знакомство с лабораторией. Разделение смесей</h2>'
|
||||
+'<div class="ph-desc">Познакомиться с лабораторным оборудованием и разделить неоднородную смесь на чистые вещества.</div></div>'
|
||||
+makeCard('lab','Оборудование и порядок работы',null,'<p><b>Оборудование:</b> стакан, стеклянная палочка, воронка с фильтром, фарфоровая чашка, спиртовка, штатив.</p>'
|
||||
+'<ol><li>Рассмотри смесь поваренной соли и песка — она <b>неоднородная</b>.</li><li>Пересыпь смесь в стакан, добавь воды и размешай — соль растворится.</li><li><b>Профильтруй</b> смесь: песок останется на фильтре, раствор соли пройдёт.</li><li><b>Выпари</b> фильтрат в фарфоровой чашке — вода испарится, останется соль.</li><li>Сделай вывод: какие свойства позволили разделить смесь.</li></ol>'
|
||||
+'<div class="note-safe"><svg viewBox="0 0 24 24"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg> Со спиртовкой и горячей чашкой работай осторожно; не пробуй вещества на вкус.</div>')
|
||||
+wgt('Тренажёр: выбери способ разделения','<div id="pr1-sep"></div>')
|
||||
+secNav('p2','p3')+readButton('pr1');
|
||||
wireReadBtn('pr1');
|
||||
}
|
||||
|
||||
function build_p3(){
|
||||
document.getElementById('p3-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 3 · Химия 7</div><h2>Атомы. Химические элементы</h2>'
|
||||
+'<div class="ph-desc">Из каких мельчайших частиц состоят вещества и что такое химический элемент.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">атом</span><span class="ph-tag">элемент</span><span class="ph-tag">символ</span></div></div>'
|
||||
+makeCard('theory','Атомы','§3','<p>Все вещества состоят из мельчайших частиц. <b>Атом</b> — мельчайшая химически неделимая частица вещества. Атомы очень малы: их нельзя увидеть даже в обычный микроскоп.</p>')
|
||||
+makeCard('theory','Химические элементы','§3','<p><b>Химический элемент</b> — это вид атомов с одинаковым зарядом ядра. Каждый элемент имеет <b>название</b> и <b>символ</b> (знак): водород — $\\text{H}$, кислород — $\\text{O}$, железо — $\\text{Fe}$. Порядковый номер элемента $Z$ равен числу протонов в ядре его атомов.</p>'
|
||||
+'<div class="def-box">Известно более 100 химических элементов. В земной коре больше всего атомов кислорода и кремния, а во Вселенной — водорода.</div>')
|
||||
+wgt('Каталог элементов: клик → номер, название, $A_r$','<div class="el-grid" id="p3-el"></div><div class="el-info" id="p3-elinfo">Выбери элемент, чтобы увидеть его характеристики.</div>')
|
||||
+wgt('Тренажёр: символ → название элемента','<div id="p3-drill"></div>')
|
||||
+rememberBox(['Атом — мельчайшая химически неделимая частица.','Химический элемент — вид атомов с одинаковым зарядом ядра.','У каждого элемента есть символ и порядковый номер $Z$.'])
|
||||
+qList(['Чем атом отличается от химического элемента?','Запиши символы водорода, кислорода, железа и меди.','Что показывает порядковый номер элемента?'])
|
||||
+secNav('pr1','p4')+readButton('p3');
|
||||
wireReadBtn('p3');
|
||||
}
|
||||
|
||||
function build_p4(){
|
||||
document.getElementById('p4-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 4 · Химия 7</div><h2>Относительная атомная масса химических элементов</h2>'
|
||||
+'<div class="ph-formula">$A_r(\\text{O}) = 16$</div>'
|
||||
+'<div class="ph-desc">Как сравнивают массы атомов, которые невозможно взвесить по отдельности.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">$A_r$</span><span class="ph-tag">а.е.м.</span></div></div>'
|
||||
+makeCard('theory','Зачем нужна относительная масса','§4','<p>Атомы очень малы, их масса в граммах — крошечное число, с которым неудобно работать. Поэтому массы атомов <b>сравнивают</b> между собой. За единицу принята $\\tfrac{1}{12}$ массы атома углерода-12 — это <b>атомная единица массы</b> (а.е.м.).</p>')
|
||||
+makeCard('rule','Относительная атомная масса','§4','<div class="def-box"><b>Относительная атомная масса</b> $A_r$ показывает, во сколько раз масса атома больше $\\tfrac{1}{12}$ массы атома углерода-12. Это <b>безразмерная</b> величина.</div>'
|
||||
+'<p>Значения $A_r$ берут из периодической таблицы: $A_r(\\text{H})=1$, $A_r(\\text{C})=12$, $A_r(\\text{O})=16$, $A_r(\\text{S})=32$, $A_r(\\text{Fe})=56$.</p>')
|
||||
+makeCard('example','Сравнение масс атомов',null,'<p>Во сколько раз атом серы тяжелее атома кислорода?</p><div class="exa-step">$\\dfrac{A_r(\\text{S})}{A_r(\\text{O})}=\\dfrac{32}{16}=2$ раза.</div>')
|
||||
+wgt('Весы атомов: во сколько раз тяжелее','<div id="p4-bal"></div>')
|
||||
+rememberBox(['$A_r$ — безразмерная величина.','Эталон — $1/12$ массы атома углерода-12.','Значения $A_r$ берут из периодической таблицы.'])
|
||||
+qList(['Что принято за атомную единицу массы?','Во сколько раз атом магния ($A_r=24$) тяжелее атома углерода ($A_r=12$)?','Почему $A_r$ не имеет единиц измерения?'])
|
||||
+secNav('p3','p5')+readButton('p4');
|
||||
wireReadBtn('p4');
|
||||
}
|
||||
|
||||
function build_p5(){
|
||||
document.getElementById('p5-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 5 · Химия 7</div><h2>Молекулы. Простые вещества</h2>'
|
||||
+'<div class="ph-desc">Из чего состоят вещества и какие вещества называют простыми.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">молекула</span><span class="ph-tag">простое вещество</span></div></div>'
|
||||
+makeCard('theory','Молекулы','§5','<p><b>Молекула</b> — частица, состоящая из нескольких атомов, соединённых вместе. Многие вещества состоят из молекул: кислород — из молекул $\\text{O}_2$, вода — из молекул $\\text{H}_2\\text{O}$.</p>')
|
||||
+makeCard('theory','Простые вещества','§5','<div class="def-box"><b>Простое вещество</b> образовано атомами <b>одного</b> химического элемента: $\\text{O}_2$, $\\text{H}_2$, $\\text{N}_2$, а также металлы — железо $\\text{Fe}$, медь $\\text{Cu}$.</div>'
|
||||
+'<p>Один элемент может образовать <b>несколько</b> простых веществ. Например, элемент кислород образует кислород $\\text{O}_2$ и озон $\\text{O}_3$ — это разные простые вещества.</p>')
|
||||
+wgt('Молекулы простых веществ','<div id="p5-gal"></div>')
|
||||
+rememberBox(['Молекула — частица из нескольких атомов.','Простое вещество — атомы одного элемента.','Один элемент может давать разные простые вещества ($O_2$ и $O_3$).'])
|
||||
+qList(['Что такое молекула? Приведи пример.','Чем простое вещество отличается от химического элемента?','Назови два простых вещества, образованных элементом кислород.'])
|
||||
+secNav('p4','p6')+readButton('p5');
|
||||
wireReadBtn('p5');
|
||||
}
|
||||
|
||||
function build_p6(){
|
||||
document.getElementById('p6-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 6 · Химия 7</div><h2>Сложные вещества</h2>'
|
||||
+'<div class="ph-desc">Какие вещества называют сложными и чем они отличаются от простых.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">сложное вещество</span><span class="ph-tag">состав</span></div></div>'
|
||||
+makeCard('theory','Сложные вещества','§6','<div class="def-box"><b>Сложное вещество</b> образовано атомами <b>разных</b> химических элементов: вода $\\text{H}_2\\text{O}$ (водород и кислород), углекислый газ $\\text{CO}_2$, метан $\\text{CH}_4$, аммиак $\\text{NH}_3$.</div>'
|
||||
+'<p>Сложные вещества можно <b>разложить</b> на простые. Например, при разложении воды получаются простые вещества — водород и кислород.</p>')
|
||||
+makeCard('example','Простое или сложное?',null,'<p>$\\text{N}_2$ — атомы только азота → <b>простое</b>. $\\text{NH}_3$ — атомы азота и водорода → <b>сложное</b>.</p>')
|
||||
+wgt('Распредели: простое или сложное вещество?','<div id="p6-cls"></div>')
|
||||
+wgt('Молекулы сложных веществ','<div id="p6-gal"></div>')
|
||||
+rememberBox(['Сложное вещество — атомы разных элементов.','Сложные вещества можно разложить на простые.','Число элементов в формуле подскажет: 1 — простое, 2+ — сложное.'])
|
||||
+qList(['Чем сложное вещество отличается от простого?','Из каких элементов состоит углекислый газ $\\text{CO}_2$?','Приведи пример разложения сложного вещества на простые.'])
|
||||
+secNav('p5','p7')+readButton('p6');
|
||||
wireReadBtn('p6');
|
||||
}
|
||||
|
||||
function build_p7(){
|
||||
document.getElementById('p7-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 7 · Химия 7</div><h2>Химическая формула</h2>'
|
||||
+'<div class="ph-formula">$\\text{H}_2\\text{O}$</div>'
|
||||
+'<div class="ph-desc">Как с помощью формулы записывают, из каких атомов и в каком числе состоит вещество.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">индекс</span><span class="ph-tag">коэффициент</span><span class="ph-tag">состав</span></div></div>'
|
||||
+makeCard('theory','Что показывает формула','§7','<p><b>Химическая формула</b> показывает состав вещества: <b>качественный</b> (из каких элементов) и <b>количественный</b> (сколько атомов каждого элемента).</p>'
|
||||
+'<div class="def-box"><b>Индекс</b> — маленькое число справа внизу: показывает число атомов элемента ($\\text{H}_2\\text{O}$ — 2 атома H, 1 атом O). <b>Коэффициент</b> — число перед формулой: показывает число молекул ($2\\text{H}_2\\text{O}$ — две молекулы воды).</div>')
|
||||
+makeCard('example','Чтение формул',null,'<p>$\\text{H}_2\\text{O}$ читают «аш-два-о», $\\text{H}_2\\text{SO}_4$ — «аш-два-эс-о-четыре», $\\text{CH}_4$ — «цэ-аш-четыре».</p>')
|
||||
+wgt('Разбор формулы на состав','<div class="fld"><label>Формула</label><input type="text" id="p7-in" value="H2SO4" style="width:150px;font-family:var(--mono)"><button class="btn primary" id="p7-go">Разобрать</button></div>'
|
||||
+'<div class="fld" style="gap:6px"><button class="btn p7-ex" data-f="H2O">H₂O</button><button class="btn p7-ex" data-f="CO2">CO₂</button><button class="btn p7-ex" data-f="Ca(OH)2">Ca(OH)₂</button><button class="btn p7-ex" data-f="H2SO4">H₂SO₄</button></div>'
|
||||
+'<div class="out" id="p7-out">Введи формулу и нажми «Разобрать».</div>')
|
||||
+rememberBox(['Индекс — число атомов; стоит справа внизу.','Коэффициент — число молекул; стоит перед формулой.','Скобки: индекс умножает всё внутри — Ca(OH)₂ = 1 Ca, 2 O, 2 H.'])
|
||||
+qList(['Чем индекс отличается от коэффициента?','Сколько атомов каждого элемента в $\\text{H}_3\\text{PO}_4$?','Что означает запись $3\\text{H}_2\\text{O}$?'])
|
||||
+secNav('p6','p8')+readButton('p7');
|
||||
wireReadBtn('p7');
|
||||
}
|
||||
|
||||
function build_p8(){
|
||||
document.getElementById('p8-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 8 · Химия 7</div><h2>Относительная молекулярная масса</h2>'
|
||||
+'<div class="ph-formula">$M_r=\\sum A_r$</div>'
|
||||
+'<div class="ph-desc">Как по формуле рассчитать, во сколько раз молекула тяжелее эталона.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">$M_r$</span><span class="ph-tag">$\\sum A_r$</span></div></div>'
|
||||
+makeCard('rule','Относительная молекулярная масса','§8','<div class="def-box"><b>Относительная молекулярная масса</b> $M_r$ равна сумме относительных атомных масс всех атомов в формуле. Это безразмерная величина: она показывает, во сколько раз молекула тяжелее $\\tfrac{1}{12}$ массы атома углерода-12.</div>')
|
||||
+makeCard('example','Расчёт $M_r$',null,'<p>$M_r(\\text{H}_2\\text{O})=2\\cdot A_r(\\text{H})+A_r(\\text{O})=2\\cdot1+16=18$.</p><div class="exa-step">$M_r(\\text{H}_2\\text{SO}_4)=2\\cdot1+32+4\\cdot16=98$.</div>')
|
||||
+wgt('Калькулятор $M_r$ по формуле','<div class="fld"><label>Формула</label><input type="text" id="p8-in" value="CaCO3" style="width:150px;font-family:var(--mono)"><button class="btn primary" id="p8-go">Вычислить</button></div>'
|
||||
+'<div class="fld" style="gap:6px"><button class="btn p8-ex" data-f="H2O">H₂O</button><button class="btn p8-ex" data-f="CO2">CO₂</button><button class="btn p8-ex" data-f="H2SO4">H₂SO₄</button><button class="btn p8-ex" data-f="CaCO3">CaCO₃</button></div>'
|
||||
+'<div class="out" id="p8-out">Введи формулу и нажми «Вычислить».</div>')
|
||||
+rememberBox(['$M_r$ — сумма $A_r$ всех атомов формулы.','$M_r$ безразмерна.','Индекс умножает $A_r$ соответствующего элемента.'])
|
||||
+qList(['Вычисли $M_r(\\text{Na}_2\\text{CO}_3)$.','Во сколько раз молекула воды тяжелее $\\tfrac{1}{12}$ атома углерода-12?','Вычисли $M_r(\\text{Fe}_2\\text{O}_3)$.'])
|
||||
+secNav('p7','p9')+readButton('p8');
|
||||
wireReadBtn('p8');
|
||||
}
|
||||
|
||||
function build_p9(){
|
||||
document.getElementById('p9-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 9 · Химия 7</div><h2>Валентность</h2>'
|
||||
+'<div class="ph-formula">H — I, O — II</div>'
|
||||
+'<div class="ph-desc">Сколько связей образует атом и как по валентности составить формулу вещества.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">валентность</span><span class="ph-tag">НОК</span></div></div>'
|
||||
+makeCard('theory','Что такое валентность','§9','<p><b>Валентность</b> — способность атома соединяться с определённым числом атомов других элементов. За единицу валентности принята валентность атома <b>водорода (I)</b>. В $\\text{HCl}$ хлор одновалентен, в $\\text{H}_2\\text{O}$ кислород двухвалентен, в $\\text{NH}_3$ азот трёхвалентен, в $\\text{CH}_4$ углерод четырёхвалентен.</p>')
|
||||
+makeCard('rule','Составление формул по валентности','§9','<p>Кислород в соединениях обычно имеет валентность <b>II</b>, водород — <b>I</b>. Зная валентности, формулу составляют так, чтобы суммарное число единиц валентности обоих элементов совпало (по наименьшему общему кратному).</p>'
|
||||
+'<div class="def-box">Постоянные валентности: H — I; O — II; Na, K — I; Mg, Ca, Zn — II; Al — III.</div>')
|
||||
+makeCard('example','Оксид алюминия',null,'<p>Al — III, O — II. НОК(3, 2) = 6. Индексы: Al → 6/3 = 2, O → 6/2 = 3.</p><div class="exa-step">Формула: $\\text{Al}_2\\text{O}_3$.</div>')
|
||||
+wgt('Конструктор формулы по валентности','<div id="p9-bld"></div>')
|
||||
+rememberBox(['Валентность — число связей атома; единица — валентность водорода.','Кислород — II, водород — I (постоянные).','Формулу составляют по НОК валентностей.'])
|
||||
+qList(['Определи валентность серы в $\\text{SO}_2$ (O — II).','Составь формулу соединения кальция (II) с кислородом.','Чему равна валентность азота в $\\text{NH}_3$?'])
|
||||
+secNav('p8','p10')+readButton('p9');
|
||||
wireReadBtn('p9');
|
||||
}
|
||||
|
||||
function build_p10(){
|
||||
document.getElementById('p10-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 10 · Химия 7</div><h2>Явления физические и химические. Признаки химических реакций</h2>'
|
||||
+'<div class="ph-desc">Чем химическое явление отличается от физического и как распознать химическую реакцию.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">явление</span><span class="ph-tag">реакция</span><span class="ph-tag">признаки</span></div></div>'
|
||||
+makeCard('theory','Физические и химические явления','§10','<p>При <b>физическом явлении</b> вещество не превращается в другое — меняются лишь форма, размеры или состояние (таяние льда, испарение воды, измельчение мела).</p>'
|
||||
+'<div class="def-box">При <b>химическом явлении (реакции)</b> из одних веществ образуются <b>другие</b> вещества с новыми свойствами (горение, ржавление, скисание молока).</div>')
|
||||
+makeCard('rule','Признаки химических реакций','§10','<p>О том, что произошла химическая реакция, судят по <b>признакам</b>:</p>'
|
||||
+'<ul><li>изменение цвета;</li><li>выделение газа (пузырьки);</li><li>образование или растворение осадка;</li><li>появление запаха;</li><li>выделение или поглощение тепла и света.</li></ul>')
|
||||
+makeCard('example','Нагревание малахита',null,'<p>Зелёный порошок малахита при нагревании чернеет (образуется $\\text{CuO}$) и выделяет газы. Видны сразу два признака — изменение цвета и выделение газа.</p>')
|
||||
+wgt('Детектор признаков реакции','<div id="p10-signs"></div>')
|
||||
+rememberBox(['Физическое явление — вещество остаётся тем же.','Химическая реакция — образуются новые вещества.','Признаки реакции: цвет, газ, осадок, запах, тепло/свет.'])
|
||||
+qList(['Приведи примеры физического и химического явлений.','Какие признаки реакции наблюдаются при горении?','Почему ржавление железа — химическое явление?'])
|
||||
+secNav('p9','lo1')+readButton('p10');
|
||||
wireReadBtn('p10');
|
||||
}
|
||||
|
||||
function build_lo1(){
|
||||
document.getElementById('lo1-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">Лабораторный опыт 1</div><h2>Признаки протекания химических реакций</h2>'
|
||||
+'<div class="ph-desc">Пронаблюдать признаки, по которым узнают химическую реакцию.</div></div>'
|
||||
+makeCard('lab','Ход работы',null,'<ol><li>Прилей к раствору соли меди раствор щёлочи — наблюдай образование <b>осадка</b> и изменение цвета.</li><li>Капни на мел (или соду) кислоту — наблюдай выделение <b>газа</b> (пузырьки).</li><li>Нагрей выданное вещество — отметь изменение цвета или выделение газа.</li><li>Запиши, какой признак реакции наблюдался в каждом опыте, и сделай вывод.</li></ol>'
|
||||
+'<div class="note-safe"><svg viewBox="0 0 24 24"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg> Кислоты и щёлочи едкие — не допускай попадания на кожу; нагревай осторожно.</div>')
|
||||
+wgt('Определи признаки в опытах','<div id="lo1-signs"></div>')
|
||||
+secNav('p10','p11')+readButton('lo1');
|
||||
wireReadBtn('lo1');
|
||||
}
|
||||
|
||||
function build_p11(){
|
||||
document.getElementById('p11-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 11 · Химия 7</div><h2>Закон сохранения массы веществ. Химические уравнения</h2>'
|
||||
+'<div class="ph-formula">$m_{реагентов}=m_{продуктов}$</div>'
|
||||
+'<div class="ph-desc">Почему масса веществ в реакции не меняется и как это записывают уравнением.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">закон сохранения</span><span class="ph-tag">уравнение</span></div></div>'
|
||||
+makeCard('theory','Закон сохранения массы','§11','<div class="def-box"><b>Закон сохранения массы:</b> масса веществ, вступивших в реакцию, равна массе веществ, образовавшихся в результате реакции. Открыт М. В. Ломоносовым и подтверждён А. Лавуазье.</div>'
|
||||
+'<p>Причина проста: атомы в реакции не исчезают и не появляются — они лишь по-новому соединяются. Поэтому их общее число (и масса) сохраняется.</p>')
|
||||
+makeCard('example','Химическое уравнение',null,'<p>Реакцию записывают <b>уравнением</b>: слева — реагенты, справа — продукты. Например: $2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$. Число атомов каждого элемента слева и справа одинаково.</p>')
|
||||
+wgt('Весы сохранения массы','<div id="p11-bal"></div>')
|
||||
+rememberBox(['Масса реагентов = масса продуктов.','Атомы лишь перегруппировываются, их число сохраняется.','Реакцию записывают химическим уравнением.'])
|
||||
+qList(['Сформулируй закон сохранения массы.','Почему масса не изменяется в ходе реакции?','При сжигании 12 г угля в кислороде получили 44 г углекислого газа. Сколько кислорода вступило в реакцию?'])
|
||||
+secNav('lo1','p12')+readButton('p11');
|
||||
wireReadBtn('p11');
|
||||
}
|
||||
|
||||
function build_p12(){
|
||||
document.getElementById('p12-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 12 · Химия 7</div><h2>Составление уравнений химических реакций</h2>'
|
||||
+'<div class="ph-formula">подбор коэффициентов</div>'
|
||||
+'<div class="ph-desc">Как уравнять реакцию, чтобы число атомов слева и справа совпало.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">коэффициенты</span><span class="ph-tag">баланс</span></div></div>'
|
||||
+makeCard('rule','Как уравнять реакцию','§12','<ol><li>Записать формулы реагентов и продуктов.</li><li>Подобрать <b>коэффициенты</b> (числа перед формулами) так, чтобы число атомов каждого элемента слева и справа стало одинаковым.</li><li>Проверить баланс по каждому элементу.</li></ol>'
|
||||
+'<div class="def-box">Менять <b>индексы</b> внутри формул нельзя — это изменило бы сами вещества. Уравнивают только <b>коэффициентами</b>.</div>')
|
||||
+makeCard('example','Горение водорода',null,'<div class="exa-step">$\\text{H}_2+\\text{O}_2 \\to$ ? Ставим коэффициенты: $2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$. Слева и справа: 4 атома H и 2 атома O.</div>')
|
||||
+'<div class="flag-card"><div class="flag-title">Балансировщик: расставь коэффициенты</div><div class="flag-help">Подбери коэффициенты так, чтобы число атомов каждого элемента совпало слева и справа.</div>'
|
||||
+'<div class="fld" style="gap:6px"><label>Реакция</label><select id="p12-pick"><option value="H2 + O2 -> H2O|2,1,2">H₂ + O₂ → H₂O</option><option value="P + O2 -> P2O5|4,5,2">P + O₂ → P₂O₅</option><option value="Fe + O2 -> Fe3O4|3,2,1">Fe + O₂ → Fe₃O₄</option><option value="CH4 + O2 -> CO2 + H2O|1,2,1,2">CH₄ + O₂ → CO₂ + H₂O</option></select></div><div id="p12-mount"></div></div>'
|
||||
+rememberBox(['Уравнивают только коэффициентами.','Индексы в формулах не трогают.','После расстановки проверь число атомов каждого элемента.'])
|
||||
+qList(['Почему при уравнивании нельзя менять индексы?','Уравняй реакцию $\\text{P}+\\text{O}_2\\to\\text{P}_2\\text{O}_5$.','Что показывает коэффициент перед формулой?'])
|
||||
+secNav('p11','final1')+readButton('p12');
|
||||
wireReadBtn('p12');
|
||||
}
|
||||
|
||||
function build_final1(){
|
||||
document.getElementById('final1-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">Финал главы 1</div><h2>Босс: первоначальные химические понятия</h2>'
|
||||
+'<div class="ph-formula">вещество · атом · формула · $M_r$ · валентность · уравнение</div>'
|
||||
+'<div class="ph-desc">Шесть интегрированных задач на всё, что изучено в главе. Реши все — получи звание «Мастер первоначальных понятий».</div></div>'
|
||||
+makeCard('rule','Шпаргалка главы 1',null,'<ul>'
|
||||
+'<li><b>Тело</b> — предмет, <b>вещество</b> — из чего он сделан; смеси разделяют по различию свойств.</li>'
|
||||
+'<li><b>Атом</b> — мельчайшая частица; <b>элемент</b> — атомы с одинаковым $Z$; $A_r$ — относительная атомная масса.</li>'
|
||||
+'<li><b>Простое</b> вещество — 1 элемент, <b>сложное</b> — разные; <b>формула</b> показывает состав.</li>'
|
||||
+'<li>$M_r=\\sum A_r$; <b>валентность</b>: H — I, O — II; формула — по НОК валентностей.</li>'
|
||||
+'<li>Признаки реакции: цвет, газ, осадок, запах, тепло; <b>масса сохраняется</b>; уравнивают <b>коэффициентами</b>.</li></ul>')
|
||||
+'<p style="margin:10px 0;color:var(--muted);font-size:.9rem">Реши задачи ниже — за каждую +5 XP, за полный разгром босса — звание и бонус.</p>'
|
||||
+secNav('p12',null);
|
||||
}
|
||||
|
||||
/* заглушки для ещё не наполненных § (фазы — следующие волны) */
|
||||
(function(){
|
||||
var P = window.PARAS, B = {};
|
||||
function ph(p, prev, next){
|
||||
return function(){
|
||||
var el = document.getElementById(p.id + '-body'); if (!el) return;
|
||||
el.innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">' + p.num + ' · Химия 7</div><h2>' + p.name + '</h2>'
|
||||
+ '<div class="ph-desc">Содержание этого ' + (p.final ? 'раздела' : 'параграфа') + ' готовится.</div></div>'
|
||||
+ makeCard('theory', p.name, p.num,
|
||||
'<p>Скоро здесь появятся теория, наглядные SVG-схемы и интерактивные тренажёры по теме «' + p.name + '». Пока доступна навигация по главе' + (p.final ? '.' : ' и отметка о прочтении.') + '</p>')
|
||||
+ secNav(prev, next) + (p.final ? '' : readButton(p.id));
|
||||
if (!p.final) wireReadBtn(p.id);
|
||||
};
|
||||
}
|
||||
for (var i = 0; i < P.length; i++) {
|
||||
B[P[i].id] = ph(P[i], i > 0 ? P[i-1].id : null, i < P.length-1 ? P[i+1].id : null);
|
||||
}
|
||||
window.BUILDERS = B;
|
||||
})();
|
||||
|
||||
/* переопределяем заглушки реальными builder'ами Волны 1 */
|
||||
window.BUILDERS.p1 = build_p1;
|
||||
window.BUILDERS.p2 = build_p2;
|
||||
window.BUILDERS.pr1 = build_pr1;
|
||||
window.BUILDERS.p3 = build_p3;
|
||||
window.BUILDERS.p4 = build_p4;
|
||||
window.BUILDERS.p5 = build_p5;
|
||||
window.BUILDERS.p6 = build_p6;
|
||||
window.BUILDERS.p7 = build_p7;
|
||||
window.BUILDERS.p8 = build_p8;
|
||||
window.BUILDERS.p9 = build_p9;
|
||||
window.BUILDERS.p10 = build_p10;
|
||||
window.BUILDERS.lo1 = build_lo1;
|
||||
window.BUILDERS.p11 = build_p11;
|
||||
window.BUILDERS.p12 = build_p12;
|
||||
window.BUILDERS.final1 = build_final1;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,227 @@
|
||||
<!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>Химия 7 · Глава 2 · «Кислород»</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@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<link rel="stylesheet" href="/css/chem8-textbook.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"></script>
|
||||
<script src="/js/api.js" defer></script>
|
||||
<script src="/js/xp.js" defer></script>
|
||||
<script src="/js/biochem-core.js" defer></script>
|
||||
<script src="/js/chem8_svg.js" defer></script>
|
||||
<script src="/js/chem7_svg.js" defer></script>
|
||||
<script src="/js/chem7_ch2_widgets.js" defer></script>
|
||||
<script src="/js/chem8_engine.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="hdr">
|
||||
<div class="hdr-row">
|
||||
<div>
|
||||
<h1>Химия 7 · Глава 2</h1>
|
||||
<div class="hdr-sub">Кислород: воздух, горение, оксиды, получение кислорода</div>
|
||||
</div>
|
||||
<div class="hdr-side">
|
||||
<a href="/textbook/chemistry-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К главам</a>
|
||||
<button id="theme-btn" class="hdr-btn"><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">
|
||||
<h2>Кислород — газ, который даёт жизнь и огонь</h2>
|
||||
<p>Кислород — самый распространённый элемент на Земле. Без него невозможны дыхание и горение. В этой главе вы узнаете состав воздуха, познакомитесь с кислородом как простым веществом, изучите горение и оксиды, научитесь получать кислород в лаборатории.</p>
|
||||
<div class="hero-row">
|
||||
<button class="btn-primary" onclick="goTo('p13')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 13</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"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="psel">
|
||||
<div class="psel-title">Параграфы главы</div>
|
||||
<div id="psel-grid" class="psel-grid"></div>
|
||||
</section>
|
||||
|
||||
<section id="sec-p13" class="sec"><div class="sec-header"><span class="sec-num">§ 13</span><h2 class="sec-h">Воздух как смесь газов</h2></div><div id="p13-body"></div></section>
|
||||
<section id="sec-lo2" class="sec"><div class="sec-header"><span class="sec-num">Лаб. 2</span><h2 class="sec-h">Лабораторный опыт: сборка приборов для получения и собирания газов</h2></div><div id="lo2-body"></div></section>
|
||||
<section id="sec-p14" class="sec"><div class="sec-header"><span class="sec-num">§ 14</span><h2 class="sec-h">Кислород как химический элемент и простое вещество</h2></div><div id="p14-body"></div></section>
|
||||
<section id="sec-p15" class="sec"><div class="sec-header"><span class="sec-num">§ 15</span><h2 class="sec-h">Химические свойства кислорода</h2></div><div id="p15-body"></div></section>
|
||||
<section id="sec-p16" class="sec"><div class="sec-header"><span class="sec-num">§ 16</span><h2 class="sec-h">Оксиды</h2></div><div id="p16-body"></div></section>
|
||||
<section id="sec-p17" class="sec"><div class="sec-header"><span class="sec-num">§ 17</span><h2 class="sec-h">Получение кислорода</h2></div><div id="p17-body"></div></section>
|
||||
<section id="sec-pr2" class="sec"><div class="sec-header"><span class="sec-num">ПР 2</span><h2 class="sec-h">Практическая работа: получение кислорода и изучение его свойств</h2></div><div id="pr2-body"></div></section>
|
||||
<section id="sec-final2" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final2-body"></div></section>
|
||||
|
||||
</div>
|
||||
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||
</main>
|
||||
|
||||
<footer class="foot">Интерактивный учебник «Химия — 7 класс» · Глава 2 · «Кислород» · LearnSpace</footer>
|
||||
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
window.CHEM8_CFG = { slug:'chemistry-7-ch2', themeKey:'chemistry7_theme', xpKey:'chemistry7_xp',
|
||||
progKey:'chemistry7_ch2_progress', achKey:'chemistry7_ch2_ach', hubHref:'/textbook/chemistry-7' };
|
||||
|
||||
window.PARAS = [
|
||||
{id:'p13', num:'§ 13', name:'Воздух как смесь газов', sub:'$N_2$ · $O_2$'},
|
||||
{id:'lo2', num:'Лаб. 2', name:'Приборы для газов', sub:'опыт'},
|
||||
{id:'p14', num:'§ 14', name:'Кислород: элемент и вещество', sub:'$O$ · $O_2$ · $O_3$'},
|
||||
{id:'p15', num:'§ 15', name:'Химические свойства кислорода', sub:'горение'},
|
||||
{id:'p16', num:'§ 16', name:'Оксиды', sub:'Э + $O$'},
|
||||
{id:'p17', num:'§ 17', name:'Получение кислорода', sub:'разложение · катализатор'},
|
||||
{id:'pr2', num:'ПР 2', name:'Практическая работа', sub:'получение $O_2$'},
|
||||
{id:'final2', num:'★', name:'Финал главы', sub:'босс · ачивка', final:true}
|
||||
];
|
||||
|
||||
window.ACH_LABELS = { start:'Начало главы 2!', p13_done:'§13 изучен!', lo2_done:'Лабораторный опыт 2 выполнен!',
|
||||
p14_done:'§14 изучен!', p15_done:'§15 изучен!', final2_tasks:'Глава 2 пройдена!' };
|
||||
window.SIDEBARS = {
|
||||
p13:{ title:'Шпаргалка §13', rows:[['Воздух','смесь газов'],['$N_2$','≈ 78 %'],['$O_2$','≈ 21 %']] },
|
||||
lo2:{ title:'Лаб. опыт 2', rows:[['Прибор','пробирка + трубка'],['Собирание','воздуха или воды']] },
|
||||
p14:{ title:'Шпаргалка §14', rows:[['Элемент','O, $Z=8$'],['Вещество','$O_2$'],['Озон','$O_3$']] },
|
||||
p15:{ title:'Шпаргалка §15', rows:[['Горение','+ $O_2$'],['Продукт','оксид'],['Окисление','медленное и быстрое']] }
|
||||
};
|
||||
window.TIPS = [
|
||||
{ sec:'p13', html:'Воздух — <b>смесь</b> газов: примерно $78\\,\\%$ азота $N_2$ и $21\\,\\%$ кислорода $O_2$, около $1\\,\\%$ — другие газы.' },
|
||||
{ sec:'lo2', html:'Газ, который <b>тяжелее</b> воздуха (как $O_2$), собирают в сосуд отверстием <b>вверх</b>; <b>легче</b> воздуха ($H_2$) — отверстием вниз; нерастворимый — вытеснением воды.' },
|
||||
{ sec:'p14', html:'$O$ — <b>элемент</b> (атом в составе веществ). $O_2$ — <b>простое вещество</b>. Кислород $O_2$ и озон $O_3$ — разные простые вещества одного элемента.' },
|
||||
{ sec:'p15', html:'При горении вещество соединяется с кислородом — образуется <b>оксид</b>. Реакции с кислородом называют реакциями <b>окисления</b>.' }
|
||||
];
|
||||
|
||||
window.POOLS = {
|
||||
p13:[
|
||||
{q:'Воздух — это…',opts:['Чистое вещество','Смесь газов','Простое вещество','Один газ — кислород'],a:1,ex:'Воздух — смесь нескольких газов.'},
|
||||
{q:'Какова примерная объёмная доля кислорода в воздухе (%)?',hint:'около 21',unit:'%',a:21,ex:'Кислорода ≈ 21 %.'},
|
||||
{q:'Какова примерная объёмная доля азота в воздухе (%)?',hint:'около 78',unit:'%',a:78,ex:'Азота ≈ 78 %.'},
|
||||
{q:'Какой газ воздуха необходим для дыхания и горения?',opts:['Азот','Кислород','Углекислый газ','Аргон'],a:1,ex:'Кислород $O_2$.'}
|
||||
],
|
||||
p14:[
|
||||
{q:'Запись «$\\text{O}$» обозначает…',opts:['Химический элемент (атом)','Молекулу кислорода','Озон','Смесь'],a:0,ex:'O — символ химического элемента.'},
|
||||
{q:'$\\text{O}_2$ — это…',opts:['Химический элемент','Простое вещество','Сложное вещество','Смесь'],a:1,ex:'Молекула из атомов одного элемента — простое вещество.'},
|
||||
{q:'Чему равна относительная атомная масса кислорода $A_r(\\text{O})$?',hint:'из таблицы',unit:'',a:16,ex:'$A_r(\\text{O})=16$.'},
|
||||
{q:'$\\text{O}_2$ и $\\text{O}_3$ — это…',opts:['Одно вещество','Разные простые вещества одного элемента','Сложные вещества','Смесь газов'],a:1,ex:'Кислород и озон — разные простые вещества элемента кислород.'}
|
||||
],
|
||||
p15:[
|
||||
{q:'Продукт горения простого вещества в кислороде — это…',opts:['Кислота','Оксид','Соль','Металл'],a:1,ex:'Образуется оксид — соединение с кислородом.'},
|
||||
{q:'При горении серы в кислороде образуется…',opts:['$\\text{SO}_2$','$\\text{H}_2\\text{S}$','$\\text{S}$','$\\text{SO}_3$ только'],a:0,ex:'$S+O_2=SO_2$ (с резким запахом).'},
|
||||
{q:'Реакция $\\text{C}+\\text{O}_2=\\text{CO}_2$ относится к реакциям…',opts:['Разложения','Соединения','Обмена','Замещения'],a:1,ex:'Из двух веществ одно — соединение.'},
|
||||
{q:'В уравнении $2\\text{Mg}+\\text{O}_2=2\\text{MgO}$ коэффициент перед $\\text{MgO}$ равен…',hint:'смотри на оксид',unit:'',a:2,ex:'Коэффициент 2.'}
|
||||
]
|
||||
};
|
||||
|
||||
function rememberBox(items){
|
||||
return '<div class="remember-box"><div class="remember-box-title">'
|
||||
+'<svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Запомни!</div><ul>'
|
||||
+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ul></div>';
|
||||
}
|
||||
function qList(items){
|
||||
return '<div class="section-title">Вопросы и задания</div><ol class="q-list">'
|
||||
+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ol>';
|
||||
}
|
||||
function wgt(title, inner){
|
||||
return '<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg> '+title+'</div>'+inner+'</div>';
|
||||
}
|
||||
|
||||
function build_p13(){
|
||||
document.getElementById('p13-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 13 · Химия 7</div><h2>Воздух как смесь газов</h2>'
|
||||
+'<div class="ph-desc">Из каких газов состоит воздух и почему он так важен.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">смесь</span><span class="ph-tag">$N_2$</span><span class="ph-tag">$O_2$</span></div></div>'
|
||||
+makeCard('theory','Состав воздуха','§13','<p>Воздух — это <b>смесь газов</b>. Основные из них: <b>азот</b> $\\text{N}_2$ (около 78 %) и <b>кислород</b> $\\text{O}_2$ (около 21 %). Ещё около 1 % приходится на другие газы — аргон, углекислый газ и др.</p>'
|
||||
+'<div class="def-box">Воздух — <b>однородная</b> смесь: газы перемешаны равномерно. Именно кислород воздуха обеспечивает дыхание живых организмов и горение.</div>')
|
||||
+wgt('Состав воздуха: кликни по части диаграммы','<div id="p13-air"></div>')
|
||||
+rememberBox(['Воздух — смесь газов.','Азота ≈ 78 %, кислорода ≈ 21 %.','Кислород нужен для дыхания и горения.'])
|
||||
+qList(['Из каких газов в основном состоит воздух?','Какова доля кислорода в воздухе?','Почему воздух называют смесью, а не чистым веществом?'])
|
||||
+secNav(null,'lo2')+readButton('p13');
|
||||
wireReadBtn('p13');
|
||||
}
|
||||
|
||||
function build_lo2(){
|
||||
document.getElementById('lo2-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">Лабораторный опыт 2</div><h2>Сборка простейших приборов для получения и собирания газов</h2>'
|
||||
+'<div class="ph-desc">Научиться собирать прибор для получения газа и выбирать способ его собирания.</div></div>'
|
||||
+makeCard('lab','Прибор и способы собирания',null,'<p>Простейший прибор: <b>пробирка с пробкой и газоотводной трубкой</b>. Газ из пробирки по трубке поступает в сосуд для собирания.</p>'
|
||||
+'<ul><li><b>Вытеснением воздуха.</b> Газ <b>тяжелее</b> воздуха ($O_2$, $CO_2$) собирают в сосуд, держа его отверстием <b>вверх</b>; <b>легче</b> воздуха ($H_2$) — отверстием <b>вниз</b>.</li><li><b>Вытеснением воды.</b> Так собирают газы, мало растворимые в воде (например, кислород).</li></ul>'
|
||||
+'<div class="note-safe"><svg viewBox="0 0 24 24"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg> Проверь герметичность прибора перед опытом; нагревай пробирку осторожно.</div>')
|
||||
+wgt('Выбери способ собирания газа','<div id="lo2-coll"></div>')
|
||||
+secNav('p13','p14')+readButton('lo2');
|
||||
wireReadBtn('lo2');
|
||||
}
|
||||
|
||||
function build_p14(){
|
||||
document.getElementById('p14-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 14 · Химия 7</div><h2>Кислород как химический элемент и простое вещество</h2>'
|
||||
+'<div class="ph-desc">Чем отличается элемент кислород от простого вещества кислород.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">$O$</span><span class="ph-tag">$O_2$</span><span class="ph-tag">$O_3$</span></div></div>'
|
||||
+makeCard('theory','Элемент и простое вещество','§14','<p><b>Кислород как элемент</b> — это атомы O ($Z=8$, $A_r=16$). Они входят в состав очень многих веществ (воды $\\text{H}_2\\text{O}$, оксидов, песка $\\text{SiO}_2$). Кислород — самый распространённый элемент земной коры.</p>'
|
||||
+'<div class="def-box"><b>Кислород как простое вещество</b> — это газ $\\text{O}_2$ (молекула из двух атомов). Элемент кислород образует и другое простое вещество — <b>озон</b> $\\text{O}_3$.</div>')
|
||||
+makeCard('theory','Физические свойства кислорода','§14','<p>$\\text{O}_2$ — газ без цвета и запаха, немного тяжелее воздуха, мало растворим в воде. При сильном охлаждении превращается в голубоватую жидкость.</p>')
|
||||
+wgt('Элемент O или простое вещество?','<div id="p14-tog"></div>')
|
||||
+rememberBox(['Элемент кислород — атомы O в составе веществ.','Простое вещество — газ $O_2$.','Озон $O_3$ — другое простое вещество того же элемента.'])
|
||||
+qList(['Когда говорят о кислороде-элементе, а когда — о простом веществе?','Назови физические свойства кислорода $O_2$.','Чем озон отличается от кислорода?'])
|
||||
+secNav('lo2','p15')+readButton('p14');
|
||||
wireReadBtn('p14');
|
||||
}
|
||||
|
||||
function build_p15(){
|
||||
document.getElementById('p15-body').innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">§ 15 · Химия 7</div><h2>Химические свойства кислорода</h2>'
|
||||
+'<div class="ph-formula">вещество $+ \\text{O}_2 \\to$ оксид</div>'
|
||||
+'<div class="ph-desc">Как кислород реагирует с другими веществами и что такое горение и окисление.</div>'
|
||||
+'<div class="ph-tags"><span class="ph-tag">горение</span><span class="ph-tag">оксид</span><span class="ph-tag">окисление</span></div></div>'
|
||||
+makeCard('theory','Кислород — активное вещество','§15','<p>Кислород реагирует со многими простыми и сложными веществами. Реакции, идущие с выделением тепла и света, называют <b>горением</b>. Продукты горения простых веществ в кислороде — <b>оксиды</b>.</p>'
|
||||
+'<div class="def-box"><b>Окисление</b> — реакция вещества с кислородом. Оно бывает быстрым (горение) и <b>медленным</b> (дыхание, гниение, ржавление железа).</div>')
|
||||
+makeCard('example','Горение веществ в кислороде',null,'<ul><li>$\\text{C}+\\text{O}_2=\\text{CO}_2$</li><li>$\\text{S}+\\text{O}_2=\\text{SO}_2$ (резкий запах)</li><li>$4\\text{P}+5\\text{O}_2=2\\text{P}_2\\text{O}_5$</li><li>$3\\text{Fe}+2\\text{O}_2=\\text{Fe}_3\\text{O}_4$ (искры)</li></ul>')
|
||||
+wgt('Симулятор горения в кислороде','<div id="p15-burn"></div>')
|
||||
+rememberBox(['Горение — реакция с кислородом, идёт с выделением тепла и света.','Продукт горения простого вещества — оксид.','Окисление бывает быстрым (горение) и медленным.'])
|
||||
+qList(['Что образуется при горении вещества в кислороде?','Приведи пример медленного окисления.','Запиши уравнение горения углерода.'])
|
||||
+secNav('p14','p16')+readButton('p15');
|
||||
wireReadBtn('p15');
|
||||
}
|
||||
|
||||
/* заглушки для ещё не наполненных § (следующая волна) */
|
||||
(function(){
|
||||
var P = window.PARAS, B = {};
|
||||
function ph(p, prev, next){
|
||||
return function(){
|
||||
var el = document.getElementById(p.id + '-body'); if (!el) return;
|
||||
el.innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">' + p.num + ' · Химия 7</div><h2>' + p.name + '</h2>'
|
||||
+ '<div class="ph-desc">Содержание этого ' + (p.final ? 'раздела' : 'параграфа') + ' готовится.</div></div>'
|
||||
+ makeCard('theory', p.name, p.num,
|
||||
'<p>Скоро здесь появятся теория, наглядные SVG-схемы и интерактивные тренажёры по теме «' + p.name + '». Пока доступна навигация по главе' + (p.final ? '.' : ' и отметка о прочтении.') + '</p>')
|
||||
+ secNav(prev, next) + (p.final ? '' : readButton(p.id));
|
||||
if (!p.final) wireReadBtn(p.id);
|
||||
};
|
||||
}
|
||||
for (var i = 0; i < P.length; i++) {
|
||||
B[P[i].id] = ph(P[i], i > 0 ? P[i-1].id : null, i < P.length-1 ? P[i+1].id : null);
|
||||
}
|
||||
window.BUILDERS = B;
|
||||
})();
|
||||
|
||||
/* реальные builder'ы Волны 1 главы 2 */
|
||||
window.BUILDERS.p13 = build_p13;
|
||||
window.BUILDERS.lo2 = build_lo2;
|
||||
window.BUILDERS.p14 = build_p14;
|
||||
window.BUILDERS.p15 = build_p15;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,120 @@
|
||||
<!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>Химия 7 · Глава 3 · «Водород»</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@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<link rel="stylesheet" href="/css/chem8-textbook.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"></script>
|
||||
<script src="/js/api.js" defer></script>
|
||||
<script src="/js/xp.js" defer></script>
|
||||
<script src="/js/biochem-core.js" defer></script>
|
||||
<script src="/js/chem8_svg.js" defer></script>
|
||||
<script src="/js/chem7_svg.js" defer></script>
|
||||
<script src="/js/chem8_engine.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="hdr">
|
||||
<div class="hdr-row">
|
||||
<div>
|
||||
<h1>Химия 7 · Глава 3</h1>
|
||||
<div class="hdr-sub">Водород: свойства, кислоты и индикаторы, ряд активности, соли</div>
|
||||
</div>
|
||||
<div class="hdr-side">
|
||||
<a href="/textbook/chemistry-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К главам</a>
|
||||
<button id="theme-btn" class="hdr-btn"><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">
|
||||
<h2>Водород — самый лёгкий элемент Вселенной</h2>
|
||||
<p>Водород — первый элемент периодической системы и самый распространённый во Вселенной. В этой главе вы изучите его свойства, познакомитесь с кислотами и индикаторами, узнаете, как металлы вытесняют водород из кислот, и что такое соли.</p>
|
||||
<div class="hero-row">
|
||||
<button class="btn-primary" onclick="goTo('p18')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 18</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"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="psel">
|
||||
<div class="psel-title">Параграфы главы</div>
|
||||
<div id="psel-grid" class="psel-grid"></div>
|
||||
</section>
|
||||
|
||||
<section id="sec-p18" class="sec"><div class="sec-header"><span class="sec-num">§ 18</span><h2 class="sec-h">Водород — химический элемент и простое вещество</h2></div><div id="p18-body"></div></section>
|
||||
<section id="sec-p19" class="sec"><div class="sec-header"><span class="sec-num">§ 19</span><h2 class="sec-h">Химические свойства водорода</h2></div><div id="p19-body"></div></section>
|
||||
<section id="sec-p20" class="sec"><div class="sec-header"><span class="sec-num">§ 20</span><h2 class="sec-h">Понятие о кислотах</h2></div><div id="p20-body"></div></section>
|
||||
<section id="sec-lo3" class="sec"><div class="sec-header"><span class="sec-num">Лаб. 3</span><h2 class="sec-h">Лабораторный опыт: действие кислот на индикаторы</h2></div><div id="lo3-body"></div></section>
|
||||
<section id="sec-p21" class="sec"><div class="sec-header"><span class="sec-num">§ 21</span><h2 class="sec-h">Взаимодействие кислот с металлами</h2></div><div id="p21-body"></div></section>
|
||||
<section id="sec-lo4" class="sec"><div class="sec-header"><span class="sec-num">Лаб. 4</span><h2 class="sec-h">Лабораторный опыт: взаимодействие серной и соляной кислот с металлами</h2></div><div id="lo4-body"></div></section>
|
||||
<section id="sec-p22" class="sec"><div class="sec-header"><span class="sec-num">§ 22</span><h2 class="sec-h">Соли — продукты замещения атомов водорода в кислотах на металлы</h2></div><div id="p22-body"></div></section>
|
||||
<section id="sec-pr3" class="sec"><div class="sec-header"><span class="sec-num">ПР 3</span><h2 class="sec-h">Практическая работа: получение водорода и изучение его свойств</h2></div><div id="pr3-body"></div></section>
|
||||
<section id="sec-final3" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final3-body"></div></section>
|
||||
|
||||
</div>
|
||||
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||
</main>
|
||||
|
||||
<footer class="foot">Интерактивный учебник «Химия — 7 класс» · Глава 3 · «Водород» · LearnSpace</footer>
|
||||
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
window.CHEM8_CFG = { slug:'chemistry-7-ch3', themeKey:'chemistry7_theme', xpKey:'chemistry7_xp',
|
||||
progKey:'chemistry7_ch3_progress', achKey:'chemistry7_ch3_ach', hubHref:'/textbook/chemistry-7' };
|
||||
|
||||
window.PARAS = [
|
||||
{id:'p18', num:'§ 18', name:'Водород: элемент и вещество', sub:'$H$ · $H_2$'},
|
||||
{id:'p19', num:'§ 19', name:'Химические свойства водорода', sub:'восстановление'},
|
||||
{id:'p20', num:'§ 20', name:'Понятие о кислотах', sub:'индикаторы'},
|
||||
{id:'lo3', num:'Лаб. 3', name:'Кислоты и индикаторы', sub:'опыт'},
|
||||
{id:'p21', num:'§ 21', name:'Кислоты и металлы', sub:'ряд активности'},
|
||||
{id:'lo4', num:'Лаб. 4', name:'Кислоты с металлами', sub:'опыт'},
|
||||
{id:'p22', num:'§ 22', name:'Соли', sub:'замещение'},
|
||||
{id:'pr3', num:'ПР 3', name:'Практическая работа', sub:'получение $H_2$'},
|
||||
{id:'final3', num:'★', name:'Финал главы', sub:'босс · ачивка', final:true}
|
||||
];
|
||||
|
||||
window.ACH_LABELS = { start:'Начало главы 3!', final3_tasks:'Глава 3 пройдена!' };
|
||||
window.SIDEBARS = { p18:{ title:'Глава 3 · Химия 7', rows:[['Раздел','Водород'],['§§','18–22'],['Лаб/ПР','ЛО 3,4 · ПР 3']] } };
|
||||
window.TIPS = [{ sec:'p18', html:'Глава наполняется содержанием по фазам. Сейчас доступны навигация по параграфам и отметка о прочтении (+10 XP).' }];
|
||||
|
||||
/* Phase 0: заглушки-builder'ы из PARAS (теория и интерактивы добавляются в фазах 1–4). */
|
||||
(function(){
|
||||
var P = window.PARAS, B = {};
|
||||
function ph(p, prev, next){
|
||||
return function(){
|
||||
var el = document.getElementById(p.id + '-body'); if (!el) return;
|
||||
el.innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">' + p.num + ' · Химия 7</div><h2>' + p.name + '</h2>'
|
||||
+ '<div class="ph-desc">Содержание этого ' + (p.final ? 'раздела' : 'параграфа') + ' готовится.</div></div>'
|
||||
+ makeCard('theory', p.name, p.num,
|
||||
'<p>Скоро здесь появятся теория, наглядные SVG-схемы, молекулярные модели и интерактивные тренажёры по теме «' + p.name + '». Пока доступна навигация по главе' + (p.final ? '.' : ' и отметка о прочтении.') + '</p>')
|
||||
+ secNav(prev, next) + (p.final ? '' : readButton(p.id));
|
||||
if (!p.final) wireReadBtn(p.id);
|
||||
};
|
||||
}
|
||||
for (var i = 0; i < P.length; i++) {
|
||||
B[P[i].id] = ph(P[i], i > 0 ? P[i-1].id : null, i < P.length-1 ? P[i+1].id : null);
|
||||
}
|
||||
window.BUILDERS = B;
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,116 @@
|
||||
<!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>Химия 7 · Глава 4 · «Вода»</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@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<link rel="stylesheet" href="/css/chem8-textbook.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"></script>
|
||||
<script src="/js/api.js" defer></script>
|
||||
<script src="/js/xp.js" defer></script>
|
||||
<script src="/js/biochem-core.js" defer></script>
|
||||
<script src="/js/chem8_svg.js" defer></script>
|
||||
<script src="/js/chem7_svg.js" defer></script>
|
||||
<script src="/js/chem8_engine.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="hdr">
|
||||
<div class="hdr-row">
|
||||
<div>
|
||||
<h1>Химия 7 · Глава 4</h1>
|
||||
<div class="hdr-sub">Вода: состав и свойства, основания и индикаторы, нейтрализация, охрана природы</div>
|
||||
</div>
|
||||
<div class="hdr-side">
|
||||
<a href="/textbook/chemistry-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К главам</a>
|
||||
<button id="theme-btn" class="hdr-btn"><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">
|
||||
<h2>Вода — самое важное вещество на Земле</h2>
|
||||
<p>Вода знакома каждому, но в химии она удивительна. В этой главе вы изучите состав и свойства воды, познакомитесь с основаниями и индикаторами, узнаете о реакции нейтрализации и о том, как беречь воду и окружающую среду.</p>
|
||||
<div class="hero-row">
|
||||
<button class="btn-primary" onclick="goTo('p23')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 23</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"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="psel">
|
||||
<div class="psel-title">Параграфы главы</div>
|
||||
<div id="psel-grid" class="psel-grid"></div>
|
||||
</section>
|
||||
|
||||
<section id="sec-p23" class="sec"><div class="sec-header"><span class="sec-num">§ 23</span><h2 class="sec-h">Состав, физические и химические свойства воды</h2></div><div id="p23-body"></div></section>
|
||||
<section id="sec-p24" class="sec"><div class="sec-header"><span class="sec-num">§ 24</span><h2 class="sec-h">Основания как сложные вещества</h2></div><div id="p24-body"></div></section>
|
||||
<section id="sec-lo5" class="sec"><div class="sec-header"><span class="sec-num">Лаб. 5</span><h2 class="sec-h">Лабораторный опыт: действие щелочей на индикаторы</h2></div><div id="lo5-body"></div></section>
|
||||
<section id="sec-p25" class="sec"><div class="sec-header"><span class="sec-num">§ 25</span><h2 class="sec-h">Реакция нейтрализации</h2></div><div id="p25-body"></div></section>
|
||||
<section id="sec-pr4" class="sec"><div class="sec-header"><span class="sec-num">ПР 4</span><h2 class="sec-h">Практическая работа: реакция нейтрализации</h2></div><div id="pr4-body"></div></section>
|
||||
<section id="sec-p26" class="sec"><div class="sec-header"><span class="sec-num">§ 26</span><h2 class="sec-h">Охрана окружающей среды</h2></div><div id="p26-body"></div></section>
|
||||
<section id="sec-final4" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final4-body"></div></section>
|
||||
|
||||
</div>
|
||||
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||
</main>
|
||||
|
||||
<footer class="foot">Интерактивный учебник «Химия — 7 класс» · Глава 4 · «Вода» · LearnSpace</footer>
|
||||
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
window.CHEM8_CFG = { slug:'chemistry-7-ch4', themeKey:'chemistry7_theme', xpKey:'chemistry7_xp',
|
||||
progKey:'chemistry7_ch4_progress', achKey:'chemistry7_ch4_ach', hubHref:'/textbook/chemistry-7' };
|
||||
|
||||
window.PARAS = [
|
||||
{id:'p23', num:'§ 23', name:'Состав и свойства воды', sub:'$H_2O$'},
|
||||
{id:'p24', num:'§ 24', name:'Основания', sub:'Me + OH'},
|
||||
{id:'lo5', num:'Лаб. 5', name:'Щёлочи и индикаторы', sub:'опыт'},
|
||||
{id:'p25', num:'§ 25', name:'Реакция нейтрализации', sub:'кислота + основание'},
|
||||
{id:'pr4', num:'ПР 4', name:'Практическая работа', sub:'нейтрализация'},
|
||||
{id:'p26', num:'§ 26', name:'Охрана окружающей среды', sub:'вода · экология'},
|
||||
{id:'final4', num:'★', name:'Финал главы', sub:'босс · ачивка', final:true}
|
||||
];
|
||||
|
||||
window.ACH_LABELS = { start:'Начало главы 4!', final4_tasks:'Глава 4 пройдена!' };
|
||||
window.SIDEBARS = { p23:{ title:'Глава 4 · Химия 7', rows:[['Раздел','Вода'],['§§','23–26'],['Лаб/ПР','ЛО 5 · ПР 4']] } };
|
||||
window.TIPS = [{ sec:'p23', html:'Глава наполняется содержанием по фазам. Сейчас доступны навигация по параграфам и отметка о прочтении (+10 XP).' }];
|
||||
|
||||
/* Phase 0: заглушки-builder'ы из PARAS (теория и интерактивы добавляются в фазах 1–4). */
|
||||
(function(){
|
||||
var P = window.PARAS, B = {};
|
||||
function ph(p, prev, next){
|
||||
return function(){
|
||||
var el = document.getElementById(p.id + '-body'); if (!el) return;
|
||||
el.innerHTML =
|
||||
'<div class="para-hero"><div class="ph-label">' + p.num + ' · Химия 7</div><h2>' + p.name + '</h2>'
|
||||
+ '<div class="ph-desc">Содержание этого ' + (p.final ? 'раздела' : 'параграфа') + ' готовится.</div></div>'
|
||||
+ makeCard('theory', p.name, p.num,
|
||||
'<p>Скоро здесь появятся теория, наглядные SVG-схемы, молекулярные модели и интерактивные тренажёры по теме «' + p.name + '». Пока доступна навигация по главе' + (p.final ? '.' : ' и отметка о прочтении.') + '</p>')
|
||||
+ secNav(prev, next) + (p.final ? '' : readButton(p.id));
|
||||
if (!p.final) wireReadBtn(p.id);
|
||||
};
|
||||
}
|
||||
for (var i = 0; i < P.length; i++) {
|
||||
B[P[i].id] = ph(P[i], i > 0 ? P[i-1].id : null, i < P.length-1 ? P[i+1].id : null);
|
||||
}
|
||||
window.BUILDERS = B;
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,540 @@
|
||||
<!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>Химия 7 класс — учебник</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">
|
||||
<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"></script>
|
||||
<script src="/js/api.js" defer></script>
|
||||
<script src="/js/xp.js" defer></script>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#f0fdf4; --card:#fff;
|
||||
--text:#0f291c; --muted:#5b7a68;
|
||||
--border:#bbf7d0;
|
||||
--pri:#059669; --pri-d:#047857;
|
||||
--pri-soft:#d1fae5;
|
||||
--c1:#059669; --c1-d:#047857; /* гл.1 — emerald */
|
||||
--c2:#0891b2; --c2-d:#0e7490; /* гл.2 — cyan */
|
||||
--c3:#7c3aed; --c3-d:#6d28d9; /* гл.3 — violet */
|
||||
--c4:#2563eb; --c4-d:#1d4ed8; /* гл.4 — blue */
|
||||
--sh:0 4px 16px rgba(5,150,105,.10);
|
||||
--sh-h:0 12px 36px rgba(5,150,105,.18);
|
||||
}
|
||||
html.dark{
|
||||
--bg:#0a1a12; --card:#10241a;
|
||||
--text:#d1fae5; --muted:#8fb8a2;
|
||||
--border:#1f4030;
|
||||
--pri-soft:rgba(5,150,105,.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}
|
||||
|
||||
/* HEADER */
|
||||
.hdr{position:relative;background:linear-gradient(110deg,#065f46 0%,#059669 55%,#6ee7b7 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(209,250,229,.18)}
|
||||
.hdr::before{content:'ХИМИЯ';position:absolute;right:-14px;top:-18%;font-family:'Outfit',sans-serif;font-size:clamp(5rem,16vw,13rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(209,250,229,.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}
|
||||
|
||||
/* OVERALL PROGRESS */
|
||||
.prog-overall{background:linear-gradient(135deg,var(--pri-soft),rgba(110,231,183,.12));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,#059669,#6ee7b7);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(5,150,105,.16);border-radius:5px;overflow:hidden;margin-top:6px}
|
||||
.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#6ee7b7);border-radius:5px;transition:width .5s}
|
||||
.po-xp{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,#10b981,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(5,150,105,.24)}
|
||||
|
||||
/* CHAPTER GRID */
|
||||
.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
|
||||
@media(min-width:680px){.ch-grid{grid-template-columns: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}
|
||||
.ch-cover-wm{position:absolute;right:-4px;top:-14px;font-size:4rem;font-weight:900;font-family:'Outfit',sans-serif;line-height:1;color:rgba(255,255,255,.22);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:.9;margin-top:4px;position:relative;z-index:1;font-weight:500}
|
||||
|
||||
.ch-cover.cc1{background:linear-gradient(135deg,#064e3b,#059669 60%,#34d399)}
|
||||
.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,#1e3a8a,#2563eb 60%,#60a5fa)}
|
||||
|
||||
.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-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),#34d399)}
|
||||
.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),#60a5fa)}
|
||||
|
||||
/* COURSE FINAL */
|
||||
.final-wrap{margin:0 0 28px;background:var(--card);border:1.5px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:var(--sh)}
|
||||
.final-head{padding:18px 22px;background:linear-gradient(135deg,#065f46 0%,#059669 55%,#34d399 100%);color:#fff;cursor:pointer;display:flex;align-items:center;gap:14px;user-select:none;transition:filter .15s}
|
||||
.final-head:hover{filter:brightness(1.06)}
|
||||
.final-head-icon{width:46px;height:46px;border-radius:12px;background:rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.final-head-icon svg{width:26px;height:26px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||
.final-head-text{flex:1;min-width:0}
|
||||
.final-head-tag{display:inline-block;padding:3px 9px;background:rgba(255,255,255,.24);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px}
|
||||
.final-head-title{font-family:'Outfit',sans-serif;font-size:1.18rem;font-weight:800;letter-spacing:-.01em;line-height:1.25}
|
||||
.final-head-sub{font-size:.84rem;opacity:.9;margin-top:2px}
|
||||
.final-chevron{flex-shrink:0;transition:transform .25s}
|
||||
.final-chevron svg{width:24px;height:24px;stroke:#fff;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
|
||||
.final-wrap.open .final-chevron{transform:rotate(180deg)}
|
||||
.final-body{display:none;padding:22px}
|
||||
.final-wrap.open .final-body{display:block}
|
||||
.fin-section-title{font-family:'Outfit',sans-serif;font-size:1.12rem;font-weight:800;color:var(--text);margin:8px 0 14px;display:flex;align-items:center;gap:9px}
|
||||
.fin-section-title svg{width:20px;height:20px;stroke:var(--pri);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||
.cheat-grid{display:grid;grid-template-columns:1fr;gap:14px;margin-bottom:28px}
|
||||
@media(min-width:680px){.cheat-grid{grid-template-columns:1fr 1fr}}
|
||||
.cheat-card{border:1.5px solid var(--border);border-radius:13px;padding:14px 16px;background:var(--card);position:relative;overflow:hidden}
|
||||
.cheat-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px}
|
||||
.cheat-card.c1::before{background:#059669}.cheat-card.c2::before{background:#0891b2}.cheat-card.c3::before{background:#7c3aed}.cheat-card.c4::before{background:#2563eb}
|
||||
.cheat-head{display:flex;align-items:center;gap:9px;margin-bottom:9px;padding-left:6px}
|
||||
.cheat-badge{font-size:.68rem;font-weight:800;padding:2px 8px;border-radius:99px;color:#fff;letter-spacing:.04em;text-transform:uppercase}
|
||||
.cheat-card.c1 .cheat-badge{background:#059669}.cheat-card.c2 .cheat-badge{background:#0891b2}.cheat-card.c3 .cheat-badge{background:#7c3aed}.cheat-card.c4 .cheat-badge{background:#2563eb}
|
||||
.cheat-title{font-weight:800;color:var(--text);font-size:.96rem}
|
||||
.cheat-list{list-style:none;padding-left:6px;margin:0}
|
||||
.cheat-list li{padding:6px 0;border-bottom:1px dashed var(--border);font-size:.9rem;line-height:1.5;color:var(--text)}
|
||||
.cheat-list li:last-child{border-bottom:0}
|
||||
.boss-overall-bar{background:linear-gradient(135deg,var(--pri-soft),rgba(110,231,183,.08));border:1px solid var(--border);border-radius:12px;padding:13px 16px;margin:6px 0 18px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
|
||||
.boss-overall-bar .lab{font-weight:700;font-size:.95rem;color:var(--text);min-width:200px}
|
||||
.boss-overall-bar .bar{flex:1;min-width:160px;height:9px;background:rgba(5,150,105,.16);border-radius:5px;overflow:hidden}
|
||||
.boss-overall-bar .fill{height:100%;background:linear-gradient(90deg,var(--pri),#6ee7b7);transition:width .5s;border-radius:5px}
|
||||
.boss-card{background:var(--card);border:2px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;transition:border-color .35s,box-shadow .35s}
|
||||
.boss-card.solved{border-color:#10b981;box-shadow:0 0 0 3px rgba(16,185,129,.18)}
|
||||
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}
|
||||
.boss-tag{font-size:.68rem;font-weight:800;padding:3px 9px;border-radius:99px;background:var(--pri-soft);color:var(--pri-d);letter-spacing:.04em;text-transform:uppercase}
|
||||
.boss-title{font-family:'Outfit',sans-serif;font-weight:800;color:var(--text);font-size:1rem;flex:1;min-width:0}
|
||||
.boss-q{padding:12px 14px;background:var(--pri-soft);border-radius:10px;font-size:.95rem;line-height:1.55;margin-bottom:10px;color:var(--text)}
|
||||
.boss-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
|
||||
.boss-input{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'JetBrains Mono',monospace;width:130px;text-align:center;font-size:.95rem}
|
||||
.boss-input:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
|
||||
.boss-btn{padding:8px 16px;border-radius:9px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:700;font-size:.88rem;cursor:pointer;font-family:inherit;transition:.15s}
|
||||
.boss-btn:hover{background:var(--pri-soft);border-color:var(--pri)}
|
||||
.boss-btn.primary{background:linear-gradient(135deg,var(--pri),#34d399);color:#fff;border-color:transparent}
|
||||
.boss-fb{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none;line-height:1.45}
|
||||
.boss-fb.ok{display:block;background:#d1fae5;color:#065f46;border-left:4px solid #10b981}
|
||||
.boss-fb.fail{display:block;background:#fee2e2;color:#7f1d1d;border-left:4px solid #dc2626}
|
||||
html.dark .boss-fb.ok{background:rgba(16,185,129,.18);color:#a7f3d0}html.dark .boss-fb.fail{background:rgba(220,38,38,.18);color:#fecaca}
|
||||
.boss-hint-txt{margin-top:8px;padding:9px 13px;background:rgba(16,185,129,.12);border-left:3px solid #10b981;border-radius:6px;font-size:.86rem;color:var(--text);display:none;line-height:1.5}
|
||||
.boss-hint-txt.show{display:block}
|
||||
.final-cta{margin-top:24px;padding:18px 20px;border-radius:14px;background:linear-gradient(135deg,#d1fae5,#a7f3d0);border:1.5px solid #34d399;display:none;align-items:center;gap:14px;flex-wrap:wrap}
|
||||
.final-cta.show{display:flex}
|
||||
html.dark .final-cta{background:linear-gradient(135deg,rgba(16,185,129,.18),rgba(5,150,105,.15));border-color:#059669}
|
||||
.final-cta-icon{width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg,#34d399,#10b981);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.final-cta-icon svg{width:28px;height:28px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||
.final-cta-txt{flex:1;min-width:180px}
|
||||
.final-cta-title{font-weight:800;color:#065f46;font-size:1.05rem;font-family:'Outfit',sans-serif}
|
||||
html.dark .final-cta-title{color:#a7f3d0}
|
||||
.final-cta-sub{font-size:.86rem;color:#047857;margin-top:2px}html.dark .final-cta-sub{color:#6ee7b7}
|
||||
.final-cta-btn{padding:10px 18px;border-radius:10px;background:linear-gradient(135deg,var(--pri),#10b981);color:#fff;text-decoration:none;font-weight:800;font-size:.9rem;display:inline-flex;align-items:center;gap:7px;transition:filter .15s}
|
||||
.final-cta-btn:hover{filter:brightness(1.1)}
|
||||
.final-cta-btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||
|
||||
/* ACHIEVEMENT STRIP */
|
||||
.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:#10b981;box-shadow:0 0 0 3px rgba(16,185,129,.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,#34d399,#10b981)}
|
||||
.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:#065f46}
|
||||
html.dark .ach-strip.lit .ach-title{color:#a7f3d0}
|
||||
|
||||
.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>Химия — 7 класс</h1>
|
||||
<div class="hdr-sub">Первый курс химии: вещества и реакции, кислород, водород, вода. 4 главы, 26 параграфов, 5 лабораторных опытов, 4 практические работы.</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/chemistry-7-ch1" class="ch-card k1" id="ch-1">
|
||||
<div class="ch-cover cc1">
|
||||
<div class="ch-cover-wm">Aₕ</div>
|
||||
<div class="ch-num">Глава 1</div>
|
||||
<div class="ch-title">Первоначальные химические понятия</div>
|
||||
<div class="ch-range">§1–§12 · ЛО 1 · ПР 1</div>
|
||||
</div>
|
||||
<div class="ch-body">
|
||||
<div class="ch-desc">Вещества и смеси, атомы и химические элементы, относительная атомная масса, молекулы, простые и сложные вещества, химическая формула и $M_r$, валентность, признаки реакций, закон сохранения массы и химические уравнения.</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/chemistry-7-ch2" class="ch-card k2" id="ch-2">
|
||||
<div class="ch-cover cc2">
|
||||
<div class="ch-cover-wm">O₂</div>
|
||||
<div class="ch-num">Глава 2</div>
|
||||
<div class="ch-title">Кислород</div>
|
||||
<div class="ch-range">§13–§17 · ЛО 2 · ПР 2</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/chemistry-7-ch3" class="ch-card k3" id="ch-3">
|
||||
<div class="ch-cover cc3">
|
||||
<div class="ch-cover-wm">H₂</div>
|
||||
<div class="ch-num">Глава 3</div>
|
||||
<div class="ch-title">Водород</div>
|
||||
<div class="ch-range">§18–§22 · ЛО 3,4 · ПР 3</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/chemistry-7-ch4" class="ch-card k4" id="ch-4">
|
||||
<div class="ch-cover cc4">
|
||||
<div class="ch-cover-wm">H₂O</div>
|
||||
<div class="ch-num">Глава 4</div>
|
||||
<div class="ch-title">Вода</div>
|
||||
<div class="ch-range">§23–§26 · ЛО 5 · ПР 4</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-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>
|
||||
|
||||
</div>
|
||||
|
||||
<section class="final-wrap" id="course-final">
|
||||
<div class="final-head" id="final-head" tabindex="0" role="button" aria-expanded="false" aria-controls="final-body">
|
||||
<div class="final-head-icon">
|
||||
<svg viewBox="0 0 24 24"><path d="M7 4h10v6a5 5 0 0 1-10 0V4z"/><path d="M5 4h2v2H5a2 2 0 0 1 0-4M19 4h-2v2h2a2 2 0 0 0 0-4M9 20h6M12 15v5"/></svg>
|
||||
</div>
|
||||
<div class="final-head-text">
|
||||
<div class="final-head-tag">Финал курса</div>
|
||||
<div class="final-head-title">Босс-проверка по всему курсу</div>
|
||||
<div class="final-head-sub">Шпаргалка курса и интегрированные боссы по всем 4 главам. Победи всех — получи «Химик 7 класса».</div>
|
||||
</div>
|
||||
<div class="final-chevron"><svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></div>
|
||||
</div>
|
||||
<div class="final-body" id="final-body">
|
||||
|
||||
<div class="fin-section-title"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg> Шпаргалка курса</div>
|
||||
<div class="cheat-grid">
|
||||
<div class="cheat-card c1"><div class="cheat-head"><span class="cheat-badge">Гл. 1</span><span class="cheat-title">Первоначальные понятия</span></div><ul class="cheat-list"><li>$M_r=\sum A_r$ всех атомов</li><li>Валентность по водороду: H — I, O — II</li><li>Простое — один элемент, сложное — разные</li><li>В реакции масса сохраняется → коэффициенты</li></ul></div>
|
||||
<div class="cheat-card c2"><div class="cheat-head"><span class="cheat-badge">Гл. 2</span><span class="cheat-title">Кислород</span></div><ul class="cheat-list"><li>Воздух: $N_2$ 78 %, $O_2$ 21 %</li><li>Горение: вещество $+ O_2 \to$ оксид</li><li>Оксид — соединение элемента с O</li><li>$O_2$ из разложения (катализатор $MnO_2$)</li></ul></div>
|
||||
<div class="cheat-card c3"><div class="cheat-head"><span class="cheat-badge">Гл. 3</span><span class="cheat-title">Водород</span></div><ul class="cheat-list"><li>$H_2$ — самый лёгкий газ</li><li>Кислота = H + кислотный остаток</li><li>Ряд активности: до $H_2$ — вытесняют газ</li><li>Соль = металл + кислотный остаток</li></ul></div>
|
||||
<div class="cheat-card c4"><div class="cheat-head"><span class="cheat-badge">Гл. 4</span><span class="cheat-title">Вода</span></div><ul class="cheat-list"><li>Разложение воды: $H_2 : O_2 = 2 : 1$</li><li>Основание = металл + OH</li><li>Индикаторы: лакмус, метилоранж, фенолфталеин</li><li>Нейтрализация: кислота + основание → соль + вода</li></ul></div>
|
||||
</div>
|
||||
|
||||
<div class="fin-section-title"><svg viewBox="0 0 24 24"><path d="M14.5 3.5l-5 5L4 4l1.5 6L3 12l5 1 1 5 2.5-2.5 6 1.5-4.5-5.5 5-5"/></svg> 8 интегрированных боссов</div>
|
||||
<div class="boss-overall-bar"><div class="lab" id="fin-boss-lab">Боссов побеждено: 0 / 8</div><div class="bar"><div class="fill" id="fin-boss-fill" style="width:0%"></div></div></div>
|
||||
<div id="fin-bosses-container"></div>
|
||||
|
||||
<div class="final-cta" id="final-cta">
|
||||
<div class="final-cta-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="final-cta-txt"><div class="final-cta-title">Курс «Химия 7» пройден!</div><div class="final-cta-sub">Вы прошли итоговую проверку по всем 4 главам. +150 XP, ачивка «Химик 7 класса» получена.</div></div>
|
||||
<a href="/textbooks" class="final-cta-btn">К каталогу <svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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">Химик 7 класса</div>
|
||||
<div class="ach-sub" id="ach-sub">Изучите все 26 параграфов курса, чтобы получить достижение.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="foot">
|
||||
Интерактивный учебник «Химия — 7 класс» · LearnSpace
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/* THEME */
|
||||
(function(){
|
||||
var saved = localStorage.getItem('chemistry7_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('chemistry7_theme', dark ? 'dark' : 'light');
|
||||
localStorage.setItem('theme', dark ? 'dark' : 'light');
|
||||
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
|
||||
});
|
||||
})();
|
||||
|
||||
/* PROGRESS */
|
||||
var TOTAL = 26;
|
||||
var CH_PARA = {
|
||||
'chemistry-7-ch1': 12,
|
||||
'chemistry-7-ch2': 5,
|
||||
'chemistry-7-ch3': 5,
|
||||
'chemistry-7-ch4': 4
|
||||
};
|
||||
var CH_IDX = {
|
||||
'chemistry-7-ch1': 1,
|
||||
'chemistry-7-ch2': 2,
|
||||
'chemistry-7-ch3': 3,
|
||||
'chemistry-7-ch4': 4
|
||||
};
|
||||
|
||||
function setChProg(idx, readCount, total) {
|
||||
var pct = total ? Math.min(100, Math.round(readCount * 100 / total)) : 0;
|
||||
var labelEl = document.getElementById('prog-' + idx);
|
||||
var fillEl = document.getElementById('fill-' + idx);
|
||||
var btnEl = document.getElementById('btn-' + idx);
|
||||
if (labelEl) labelEl.textContent = pct + '%';
|
||||
if (fillEl) fillEl.style.width = pct + '%';
|
||||
if (btnEl) {
|
||||
if (readCount > 0 && readCount < total) btnEl.textContent = 'Продолжить';
|
||||
else if (readCount >= total) btnEl.textContent = 'Открыть снова';
|
||||
else btnEl.textContent = 'Открыть главу';
|
||||
}
|
||||
return pct;
|
||||
}
|
||||
|
||||
var FIN_ACH_KEY = 'chemistry7_course_master';
|
||||
|
||||
function renderProgress(children) {
|
||||
var totalRead = 0;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var ch = children[i];
|
||||
var idx = CH_IDX[ch.slug];
|
||||
if (!idx) continue;
|
||||
var total = ch.para_count || CH_PARA[ch.slug] || 1;
|
||||
var read = ch.progress ? ch.progress.read.length : 0;
|
||||
if (read > total) read = total;
|
||||
totalRead += read;
|
||||
setChProg(idx, read, total);
|
||||
}
|
||||
|
||||
var pct = Math.min(100, Math.round(totalRead * 100 / TOTAL));
|
||||
var overallEl = document.getElementById('overall-text');
|
||||
var fillEl = document.getElementById('overall-fill');
|
||||
if (overallEl) overallEl.textContent = totalRead + ' из ' + TOTAL + ' параграфов \xb7 ' + pct + '%';
|
||||
if (fillEl) fillEl.style.width = pct + '%';
|
||||
|
||||
var xpBadge = document.getElementById('hero-xp-badge');
|
||||
var xp = parseInt(localStorage.getItem('chemistry7_xp') || '0', 10) || 0;
|
||||
if (xpBadge && xp > 0) {
|
||||
xpBadge.style.display = '';
|
||||
xpBadge.textContent = xp + ' XP';
|
||||
}
|
||||
|
||||
var mastered = localStorage.getItem(FIN_ACH_KEY) === '1';
|
||||
if (totalRead >= TOTAL || mastered) {
|
||||
var strip = document.getElementById('ach-strip');
|
||||
var sub = document.getElementById('ach-sub');
|
||||
if (strip) strip.classList.add('lit');
|
||||
if (sub) sub.textContent = mastered
|
||||
? 'Выполнено! Вы — Химик 7 класса.'
|
||||
: 'Выполнено! Вы изучили весь курс химии 7 класса.';
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== ФИНАЛ КУРСА: 8 интегрированных боссов ===== */
|
||||
var FIN_BOSS_KEY = 'chemistry7_course_bosses';
|
||||
var FIN_BOSSES = [
|
||||
{ n:1, tag:'Гл.1', title:'Относительная молекулярная масса', q:'Чему равна $M_r(\\text{H}_2\\text{SO}_4)$? ($A_r$: H 1, S 32, O 16)', hint:'$2\\cdot1+32+4\\cdot16=98$.', ans:98 },
|
||||
{ n:2, tag:'Гл.1', title:'Валентность', q:'Чему равна валентность серы в оксиде $\\text{SO}_2$? (кислород всегда II)', hint:'$2\\cdot\\text{II}=4$ единицы на 1 атом S → IV.', ans:4 },
|
||||
{ n:3, tag:'Гл.1', title:'Уравнение реакции', q:'В уравнении $2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$ какой коэффициент стоит перед $\\text{H}_2$?', hint:'Слева 2 молекулы водорода.', ans:2 },
|
||||
{ n:4, tag:'Гл.1', title:'Закон сохранения массы', q:'Сожгли $4$ г серы и получили $8$ г $\\text{SO}_2$. Сколько граммов кислорода вступило в реакцию?', hint:'$m(\\text{O}_2)=m(\\text{SO}_2)-m(\\text{S})=8-4$.', ans:4 },
|
||||
{ n:5, tag:'Гл.2', title:'Состав воздуха', q:'Чему примерно равна объёмная доля кислорода в воздухе (в процентах)?', hint:'Кислорода около 21 %.', ans:21 },
|
||||
{ n:6, tag:'Гл.2', title:'Оксиды', q:'Сколько атомов кислорода в формуле $\\text{Fe}_3\\text{O}_4$?', hint:'Индекс при O равен 4.', ans:4 },
|
||||
{ n:7, tag:'Гл.3', title:'Ряд активности', q:'Сколько из металлов Mg, Cu, Zn, Ag вытесняют водород из соляной кислоты?', hint:'Левее $\\text{H}_2$ стоят Mg и Zn → 2.', ans:2 },
|
||||
{ n:8, tag:'Гл.4', title:'Реакция нейтрализации', q:'В реакции $\\text{HCl}+\\text{NaOH}$ образуются соль и вода. Сколько новых веществ получилось?', hint:'$\\text{NaCl}$ и $\\text{H}_2\\text{O}$ → 2.', ans:2 }
|
||||
];
|
||||
function loadFinBossState(){ try{ return JSON.parse(localStorage.getItem(FIN_BOSS_KEY)||'{}')||{}; }catch(e){ return {}; } }
|
||||
function saveFinBossState(s){ try{ localStorage.setItem(FIN_BOSS_KEY, JSON.stringify(s)); }catch(e){} }
|
||||
function finRenderKatex(root){ if(typeof window.renderMathInElement!=='function')return; try{ window.renderMathInElement(root,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],throwOnError:false}); }catch(e){} }
|
||||
function updateFinBossBar(state){ var won=0; for(var k in state) if(state[k])won++; var lab=document.getElementById('fin-boss-lab'),fill=document.getElementById('fin-boss-fill'); if(lab)lab.textContent='Боссов побеждено: '+won+' / '+FIN_BOSSES.length; if(fill)fill.style.width=Math.round(won*100/FIN_BOSSES.length)+'%'; return won; }
|
||||
function maybeUnlockMaster(state){
|
||||
if(localStorage.getItem(FIN_ACH_KEY)==='1')return;
|
||||
var won=0; for(var k in state) if(state[k])won++; if(won<FIN_BOSSES.length)return;
|
||||
localStorage.setItem(FIN_ACH_KEY,'1');
|
||||
var xp=parseInt(localStorage.getItem('chemistry7_xp')||'0',10)||0; localStorage.setItem('chemistry7_xp',String(xp+150));
|
||||
try{ if(window.LS&&window.LS.xp&&window.LS.xp.add) window.LS.xp.add(150,'chemistry7-master'); }catch(e){}
|
||||
try{ if(window.confetti) window.confetti({particleCount:220,spread:110,origin:{y:.6}}); }catch(e){}
|
||||
var strip=document.getElementById('ach-strip'),sub=document.getElementById('ach-sub');
|
||||
if(strip)strip.classList.add('lit'); if(sub)sub.textContent='Выполнено! Вы — Химик 7 класса.';
|
||||
var cta=document.getElementById('final-cta'); if(cta)cta.classList.add('show');
|
||||
var xb=document.getElementById('hero-xp-badge'); if(xb){ xb.style.display=''; xb.textContent=(parseInt(localStorage.getItem('chemistry7_xp')||'0',10)||0)+' XP'; }
|
||||
}
|
||||
function buildFinBoss(b,state){
|
||||
var solved=!!state[b.n], step=b.step||'1';
|
||||
return '<div class="boss-card'+(solved?' solved':'')+'" id="fb-'+b.n+'-card">'
|
||||
+'<div class="boss-head"><span class="boss-tag">'+b.tag+'</span><span class="boss-title">Босс '+b.n+'. '+b.title+'</span></div>'
|
||||
+'<div class="boss-q">'+b.q+'</div>'
|
||||
+'<div class="boss-row"><input type="number" step="'+step+'" class="boss-input" id="fb-'+b.n+'-inp" placeholder="число"'+(solved?' value="'+b.ans+'" disabled':'')+'>'
|
||||
+'<button class="boss-btn primary" id="fb-'+b.n+'-go"'+(solved?' disabled':'')+'>Атаковать</button>'
|
||||
+'<button class="boss-btn" id="fb-'+b.n+'-hint">Подсказка</button></div>'
|
||||
+'<div class="boss-hint-txt" id="fb-'+b.n+'-ht">'+b.hint+'</div>'
|
||||
+'<div class="boss-fb'+(solved?' ok':'')+'" id="fb-'+b.n+'-fbk">'+(solved?'Победа! Босс повержен.':'')+'</div></div>';
|
||||
}
|
||||
function bindFinBoss(b){
|
||||
var go=document.getElementById('fb-'+b.n+'-go'), hint=document.getElementById('fb-'+b.n+'-hint'),
|
||||
inp=document.getElementById('fb-'+b.n+'-inp'), fbk=document.getElementById('fb-'+b.n+'-fbk'),
|
||||
ht=document.getElementById('fb-'+b.n+'-ht'), card=document.getElementById('fb-'+b.n+'-card');
|
||||
if(!go)return;
|
||||
if(hint)hint.addEventListener('click',function(){ if(ht)ht.classList.toggle('show'); });
|
||||
var state=loadFinBossState(); if(state[b.n])return;
|
||||
go.addEventListener('click',function(){
|
||||
var v=parseFloat((inp.value||'').replace(',','.'));
|
||||
if(isNaN(v)){ fbk.className='boss-fb fail'; fbk.textContent='Введите число.'; return; }
|
||||
var tol=(typeof b.tol==='number')?b.tol:1e-9;
|
||||
if(Math.abs(v-b.ans)<=tol){
|
||||
fbk.className='boss-fb ok'; fbk.textContent='Победа! +15 XP. Босс повержен.'; card.classList.add('solved'); go.disabled=true; inp.disabled=true;
|
||||
var s=loadFinBossState(); if(!s[b.n]){ s[b.n]=true; saveFinBossState(s);
|
||||
var xp=parseInt(localStorage.getItem('chemistry7_xp')||'0',10)||0; localStorage.setItem('chemistry7_xp',String(xp+15));
|
||||
try{ if(window.LS&&window.LS.xp&&window.LS.xp.add) window.LS.xp.add(15,'chemistry7-fin-boss-'+b.n); }catch(e){}
|
||||
var xb=document.getElementById('hero-xp-badge'); if(xb){ xb.style.display=''; xb.textContent=(parseInt(localStorage.getItem('chemistry7_xp')||'0',10)||0)+' XP'; }
|
||||
updateFinBossBar(s); maybeUnlockMaster(s);
|
||||
}
|
||||
} else { fbk.className='boss-fb fail'; fbk.textContent='Не то. Перепроверь решение и попробуй снова.'; }
|
||||
});
|
||||
inp.addEventListener('keydown',function(e){ if(e.key==='Enter'){ e.preventDefault(); go.click(); } });
|
||||
}
|
||||
var FIN_BOSSES_RENDERED=false;
|
||||
function renderFinBosses(){
|
||||
if(FIN_BOSSES_RENDERED)return;
|
||||
var cont=document.getElementById('fin-bosses-container'); if(!cont)return;
|
||||
var state=loadFinBossState(), html='';
|
||||
for(var i=0;i<FIN_BOSSES.length;i++) html+=buildFinBoss(FIN_BOSSES[i],state);
|
||||
cont.innerHTML=html;
|
||||
for(var j=0;j<FIN_BOSSES.length;j++) bindFinBoss(FIN_BOSSES[j]);
|
||||
finRenderKatex(document.getElementById('course-final'));
|
||||
updateFinBossBar(state);
|
||||
if(localStorage.getItem(FIN_ACH_KEY)==='1'){ var cta=document.getElementById('final-cta'); if(cta)cta.classList.add('show'); }
|
||||
FIN_BOSSES_RENDERED=true;
|
||||
}
|
||||
|
||||
/* FINAL ACCORDION */
|
||||
(function bindFinalAccordion(){
|
||||
var head = document.getElementById('final-head');
|
||||
var wrap = document.getElementById('course-final');
|
||||
if (!head || !wrap) return;
|
||||
function toggle(){
|
||||
var willOpen = !wrap.classList.contains('open');
|
||||
wrap.classList.toggle('open');
|
||||
head.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
||||
if (willOpen) { renderFinBosses(); finRenderKatex(wrap); }
|
||||
}
|
||||
head.addEventListener('click', toggle);
|
||||
head.addEventListener('keydown', function(e){
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
|
||||
});
|
||||
})();
|
||||
|
||||
function loadProgress() {
|
||||
if (typeof window.LS === 'undefined' || typeof window.LS.api !== 'function') {
|
||||
renderProgress([]);
|
||||
return;
|
||||
}
|
||||
window.LS.api('/api/textbooks/chemistry-7/children')
|
||||
.then(function(data) {
|
||||
if (data && data.children) renderProgress(data.children);
|
||||
else renderProgress([]);
|
||||
})
|
||||
.catch(function() { renderProgress([]); });
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadProgress);
|
||||
} else {
|
||||
loadProgress();
|
||||
}
|
||||
window.addEventListener('focus', loadProgress);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -121,17 +121,15 @@
|
||||
> Все 7 фильтров в UI редактора теперь с данными; контроллер их уже валидирует,
|
||||
> XP начисляется. data_json совпадает с UI (balance: reactants/products/coefficients;
|
||||
> match: pairs; classify/complete: target/equation/choices/answer).
|
||||
> Сделано также: 5.2 живая поэлементная проверка баланса; 5.3 3D-build с
|
||||
> изоморфизмом графа (Morgan-хеш) + 4 задания; 5.5 ачивки bc_* привязаны к
|
||||
> событиям. Осталось только 5.4 (адаптивность/«задача дня»/streak).
|
||||
> Осталось: drag-and-drop (5.2), 3D-build (5.3), адаптивность (5.4), ачивки bc_* (5.5).
|
||||
|
||||
Слоты ачивок `bc_5_challenges`/`bc_20_challenges` уже есть в `_shared.js` — довести до конца и расширить вызовы.
|
||||
|
||||
- [x] 5.1 Засидированы типы challenge: balance, match, classify, complete (16 заданий, идемпотентный сидер).
|
||||
- [x] 5.2 Живая поэлементная проверка баланса (счётчик атомов слева=справа по мере ввода). _(числовой ввод с мгновенной обратной связью вместо литерального drag-and-drop.)_
|
||||
- [x] 5.3 3D-build challenge: `structuralMatch`/`canonicalHash` (Morgan) в контроллере — проверка связности против эталона (molecule_id), отличает изомеры (этанол≠диметиловый эфир); +4 structure-build задания.
|
||||
- [ ] 5.2 UI drag-and-drop для balance/match (сейчас только выбор/ввод).
|
||||
- [ ] 5.3 3D-build challenge: собрать молекулу и проверить не только формулу, но и связность/геометрию.
|
||||
- [ ] 5.4 Адаптивная сложность + «задача дня»; streak по биохимии.
|
||||
- [x] 5.5 Ачивки `bc_first_molecule`/`bc_5_challenges`/`bc_20_challenges` привязаны: `checkAchievements` вызывается после решения задачи и сохранения молекулы; счётчики из `bio_user_challenges`/`bio_user_molecules` в `checkPhase3Achievements`.
|
||||
- [ ] 5.5 Привязать ачивки `bc_5_challenges`/`bc_20_challenges` (и новые: «собрал АТФ», «сбалансировал 10 реакций», «прошёл цикл Кребса») к событиям; проверить начисление в `gamificationController`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -39,14 +39,7 @@
|
||||
- [x] 3.2 Пресеты систем (микроскоп/телескоп/проектор/зеркальная) — Фаза 1; экспорт PNG (`benchExportPng`, кнопка «Снимок PNG»).
|
||||
- [x] 3.3 Экран ловит изображение: светящиеся пятна (additive `lighter`) в точках попадания лучей, по λ — видно формирование изображения и спектр.
|
||||
|
||||
### Фаза 4 — Контент и улучшения — [x]
|
||||
- [x] Источники: **одиночный луч** и **лазер** (узкий пучок) + **угол прицеливания** (point/single/laser/parallel наклоняются на `ang`).
|
||||
- [x] Новые элементы: **граница сред** (Снеллиус на вертикальной плоскости + ПВО — проверено: 30°→19.47°, ПВО при 50°) и **стеклянная пластина** (параллельный сдвиг, преломление на входе/выходе).
|
||||
- [x] Отсечение апертурой: лучи вне апертуры линзы/зеркала поглощаются (видно виньетирование, размер апертуры значим).
|
||||
- [x] Метки **F и 2F** у собирающей линзы.
|
||||
- [x] Числовые значения у слайдеров инспектора (живое обновление без пересборки панели).
|
||||
|
||||
Бэклог: точная двухгранная призма (Снеллиус на гранях вместо тонкопризменного); профиль интенсивности на экране; поворот/наклон элементов и 2D-перетаскивание (yf); делитель пучка (форк луча); удаление legacy `FreeBuildSim`.
|
||||
Бэклог: точная двухгранная призма (Снеллиус на гранях вместо тонкопризменного), апертурное отсечение лучей вне линзы (сейчас проходят прямо), профиль интенсивности на экране, поворот элементов, удаление legacy `FreeBuildSim`.
|
||||
|
||||
---
|
||||
История: создан 2026-05-30.
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
# План реализации: Физика 10 (Беларусь, Громыко, 2019)
|
||||
|
||||
**Источник:** `fizika_10kl_gromika_rus_2019.pdf` (267 стр., 2 части, 6 глав, 37 §)
|
||||
**Издательство:** «Адукацыя і выхаванне», 2019
|
||||
**Уровень:** базовый + повышенный (с электронным приложением).
|
||||
|
||||
> **Самый большой учебник во всём проекте** — 37 параграфов против 19 в Алгебре 9 и 16 в Геометрии 9. Учитывает весь опыт 10 предыдущих учебников: единый KaTeX-эскейп, ноль пикселей в подписях, ноль эмодзи, 3D-движок `g3d.js`, snap-точки в slider'ах, цветовая кодировка, drag-to-rotate. **Главная фишка** — настоящие физические **симуляции**: броуновское движение, изопроцессы (PV-диаграммы), линии поля, электрические цепи, движение заряженных частиц в магнитном поле, явление индукции.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Содержание учебника
|
||||
|
||||
### Часть 1. МОЛЕКУЛЯРНАЯ ФИЗИКА
|
||||
|
||||
**Глава 1. Основы молекулярно-кинетической теории (§§1–10)**
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §1 | Основные положения МКТ | 3 положения: вещество — частицы, частицы движутся, частицы взаимодействуют |
|
||||
| §2 | Масса и размеры молекул. Количество вещества | $N_A = 6.022 \cdot 10^{23}$, $M = m \cdot N_A$, $\nu = N/N_A = m/M$ |
|
||||
| §3 | Макро- и микропараметры. Идеальный газ. Основное уравнение МКТ | $p = \dfrac{1}{3} n m_0 \overline{v^2} = \dfrac{2}{3} n \overline{E_k}$ |
|
||||
| §4 | Тепловое равновесие. Температура | $\overline{E_k} = \dfrac{3}{2} kT$, шкала Кельвина: $T = t + 273$ |
|
||||
| §5 | Уравнение состояния идеального газа | $pV = \nu RT$, $pV = \dfrac{m}{M} RT$ (Клапейрон–Менделеев) |
|
||||
| §6 | Изопроцессы | Изотерма $pV = \text{const}$, изобара $V/T = \text{const}$, изохора $p/T = \text{const}$ |
|
||||
| §7 | Строение и свойства твёрдых тел | Кристаллы (моно-, поликристаллы), аморфные тела, анизотропия |
|
||||
| §8 | Строение и свойства жидкостей | Поверхностное натяжение, смачивание |
|
||||
| §9 | Испарение и конденсация. Насыщенный пар | Динамическое равновесие, давление насыщенного пара |
|
||||
| §10 | Влажность воздуха | $\varphi = \dfrac{p}{p_н} \cdot 100\%$, точка росы, психрометр |
|
||||
|
||||
**Глава 2. Основы термодинамики (§§11–15)**
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §11 | Внутренняя энергия | $U = \dfrac{3}{2} \nu R T$ (одноатомный газ) |
|
||||
| §12 | Работа в термодинамике | $A = p \Delta V$ (изобарный), графически площадь под кривой |
|
||||
| §13 | Количество теплоты | $Q = cm \Delta T$, $Q = \lambda m$ (плавление), $Q = rm$ (испарение), $Q = qm$ (сгорание) |
|
||||
| §14 | Первый закон термодинамики | $\Delta U = Q + A_{внеш}$ или $Q = \Delta U + A_{газ}$ |
|
||||
| §15 | Тепловые двигатели. КПД | $\eta = \dfrac{A}{Q_1} = \dfrac{Q_1 - Q_2}{Q_1}$, $\eta_{Карно} = \dfrac{T_1 - T_2}{T_1}$ |
|
||||
|
||||
### Часть 2. ЭЛЕКТРОДИНАМИКА
|
||||
|
||||
**Глава 3. Электростатика (§§16–24)**
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §16 | Электрический заряд. Закон сохранения | $q = ne$, $e = 1.6 \cdot 10^{-19}$ Кл, $\sum q = \text{const}$ |
|
||||
| §17 | Закон Кулона | $F = k \dfrac{|q_1 q_2|}{r^2}$, $k = 9 \cdot 10^9$ Н·м²/Кл² |
|
||||
| §18 | Электростатическое поле | Силовая характеристика, источник |
|
||||
| §19 | Напряжённость поля. Принцип суперпозиции | $\vec{E} = \dfrac{\vec{F}}{q_{пр}}$, $\vec{E} = \sum \vec{E_i}$ |
|
||||
| §20 | Линии напряжённости | Силовые линии, поле точечного заряда, диполя |
|
||||
| §21 | Работа поля. Потенциал | $A = qU$, $\varphi = \dfrac{W_p}{q}$ |
|
||||
| §22 | Разность потенциалов. Напряжение | $U = \varphi_1 - \varphi_2$, $E = U/d$ (однородное) |
|
||||
| §23 | Конденсаторы. Электроёмкость | $C = q/U$, $C = \dfrac{\varepsilon \varepsilon_0 S}{d}$ |
|
||||
| §24 | Энергия поля конденсатора | $W = \dfrac{CU^2}{2} = \dfrac{q^2}{2C} = \dfrac{qU}{2}$ |
|
||||
|
||||
**Глава 4. Постоянный электрический ток (§§25–26)**
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §25 | ЭДС источника тока | $\mathcal{E} = A_{ст}/q$, ЭДС, сторонние силы |
|
||||
| §26 | Закон Ома для полной цепи | $I = \dfrac{\mathcal{E}}{R + r}$, КПД $\eta = U/\mathcal{E}$ |
|
||||
|
||||
**Глава 5. Магнитное поле. Электромагнитная индукция (§§27–33)**
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §27 | Магнитное поле тока | Опыт Эрстеда, взаимодействие проводников |
|
||||
| §28 | Индукция магнитного поля | $\vec{B}$, правило буравчика, линии индукции |
|
||||
| §29 | Сила Ампера | $F_A = BIL\sin\alpha$, правило левой руки |
|
||||
| §30 | Сила Лоренца | $F_L = qvB\sin\alpha$, движение по окружности: $R = \dfrac{mv}{qB}$ |
|
||||
| §31 | Магнитный поток. Электромагнитная индукция | $\Phi = BS\cos\alpha$, опыты Фарадея |
|
||||
| §32 | Правило Ленца. Закон ЭМИ | $\mathcal{E}_i = -\dfrac{d\Phi}{dt}$ |
|
||||
| §33 | Самоиндукция. Индуктивность | $\mathcal{E}_{si} = -L \dfrac{dI}{dt}$, $W_L = \dfrac{LI^2}{2}$ |
|
||||
|
||||
**Глава 6. Электрический ток в различных средах (§§34–37)**
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §34 | Ток в металлах. Сверхпроводимость | Свободные электроны, $\rho(T) = \rho_0(1+\alpha t)$ |
|
||||
| §35 | Ток в электролитах | Электролиз, законы Фарадея: $m = kIt$, $k = \dfrac{M}{F n}$ |
|
||||
| §36 | Ток в газах. Плазма | Самостоятельный/несамостоятельный разряд, виды разрядов |
|
||||
| §37 | Ток в полупроводниках | Собственная и примесная проводимость, n-тип, p-тип, p-n-переход |
|
||||
|
||||
**ИТОГО:** 6 глав, 37 параграфов, **2 части**.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 SVG-СТАНДАРТ КАЧЕСТВА ДЛЯ ФИЗИКИ
|
||||
|
||||
Это первая физика в проекте — нужны новые хелперы. Унаследует всё от математических учебников + добавляет physics-specific.
|
||||
|
||||
### Унаследованные хелперы (из Алгебры 11 / Геометрии 11)
|
||||
- `axes2D(W, H, pad, xmin, xmax, ymin, ymax)` — координатная плоскость
|
||||
- `plotFunc(f, xmin, xmax, toX, toY, color, N)` — график функции
|
||||
- `pointWithDrop(x, fx, toX, toY, color, label)` — точка с проекциями
|
||||
- `asymptote(orientation, value, ...)` — асимптота
|
||||
- `snapToValue(value, snaps, tolerance)` — snap-точки
|
||||
- `rightAngleMark`, `angleArcAuto`, `unitVec`, `deg2rad`
|
||||
- **`g3d.js`** — мини-3D движок (для линий магнитной индукции, движения частиц)
|
||||
|
||||
### Новые physics-specific хелперы (в `frontend/js/phys.js`)
|
||||
|
||||
```javascript
|
||||
// === Стрелка вектора (2D) ===
|
||||
function drawArrow(x1, y1, x2, y2, color, width, headSize) {
|
||||
// Возвращает SVG: линия + треугольная стрелка на конце
|
||||
const dx = x2 - x1, dy = y2 - y1;
|
||||
const len = Math.sqrt(dx*dx + dy*dy);
|
||||
const ux = dx / len, uy = dy / len;
|
||||
const px = -uy, py = ux; // перпендикуляр
|
||||
const h = headSize || 10;
|
||||
const w = (headSize || 10) * 0.6;
|
||||
const tipX = x2, tipY = y2;
|
||||
const baseX = x2 - ux * h, baseY = y2 - uy * h;
|
||||
const leftX = baseX + px * w, leftY = baseY + py * w;
|
||||
const rightX = baseX - px * w, rightY = baseY - py * w;
|
||||
return `<line x1="${x1}" y1="${y1}" x2="${baseX.toFixed(1)}" y2="${baseY.toFixed(1)}" stroke="${color}" stroke-width="${width || 2}"/>`
|
||||
+ `<polygon points="${tipX},${tipY} ${leftX.toFixed(1)},${leftY.toFixed(1)} ${rightX.toFixed(1)},${rightY.toFixed(1)}" fill="${color}"/>`;
|
||||
}
|
||||
|
||||
// === Линии электрического поля от точечного заряда ===
|
||||
function fieldLinesPointCharge(cx, cy, sign, scale, numLines) {
|
||||
// sign: +1 или -1 (от знака заряда)
|
||||
// numLines: число линий (12-24)
|
||||
// Возвращает массив SVG-линий — стрелки наружу для +q, внутрь для -q
|
||||
let s = '';
|
||||
const N = numLines || 16;
|
||||
for (let i = 0; i < N; i++) {
|
||||
const a = 2 * Math.PI * i / N;
|
||||
const r1 = 15, r2 = scale;
|
||||
const x1 = cx + r1 * Math.cos(a), y1 = cy + r1 * Math.sin(a);
|
||||
const x2 = cx + r2 * Math.cos(a), y2 = cy + r2 * Math.sin(a);
|
||||
if (sign > 0) s += drawArrow(x1, y1, x2, y2, '#dc2626', 1.5);
|
||||
else s += drawArrow(x2, y2, x1, y1, '#2563eb', 1.5);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// === Линии поля диполя (две точечных зарядов противоположных) ===
|
||||
function fieldLinesDipole(c1, c2, scale, numLines) {
|
||||
// Параметрически — линия от +q к -q вдоль кривых, перпендикулярных эквипотенциалям
|
||||
// Упрощённо: использовать numerical integration по полю
|
||||
// ...
|
||||
}
|
||||
|
||||
// === Электрическая схема: компоненты ===
|
||||
function batteryEMF(x, y, EMF, orientation) {
|
||||
// Возвращает SVG условного обозначения батареи (две полоски + подпись ε)
|
||||
}
|
||||
function resistor(x, y, w, h, R, orientation) {
|
||||
// Возвращает SVG зигзага резистора + подпись R
|
||||
}
|
||||
function capacitor(x, y, C, orientation) {
|
||||
// Две параллельные линии + подпись C
|
||||
}
|
||||
function ammeter(x, y) {
|
||||
// Круг с буквой A
|
||||
}
|
||||
function voltmeter(x, y) {
|
||||
// Круг с буквой V
|
||||
}
|
||||
function inductor(x, y, L, orientation) {
|
||||
// Серия маленьких полукругов
|
||||
}
|
||||
function lightbulb(x, y) {
|
||||
// Круг с крестиком (или волной)
|
||||
}
|
||||
|
||||
// === Магнитное поле: стрелки В через плоскость экрана ===
|
||||
function magneticField(x, y, direction, dx, dy) {
|
||||
// direction: 'in' (крест) или 'out' (точка)
|
||||
// Рисует сетку 5x5 крестиков (вошло в плоскость) или точек (вышло из плоскости)
|
||||
let s = '';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let j = 0; j < 5; j++) {
|
||||
const cx = x + i * dx, cy = y + j * dy;
|
||||
if (direction === 'in') {
|
||||
s += `<circle cx="${cx}" cy="${cy}" r="6" fill="none" stroke="#0891b2" stroke-width="1.5"/>`;
|
||||
s += `<line x1="${cx-4}" y1="${cy-4}" x2="${cx+4}" y2="${cy+4}" stroke="#0891b2" stroke-width="1.5"/>`;
|
||||
s += `<line x1="${cx-4}" y1="${cy+4}" x2="${cx+4}" y2="${cy-4}" stroke="#0891b2" stroke-width="1.5"/>`;
|
||||
} else {
|
||||
s += `<circle cx="${cx}" cy="${cy}" r="6" fill="none" stroke="#0891b2" stroke-width="1.5"/>`;
|
||||
s += `<circle cx="${cx}" cy="${cy}" r="2" fill="#0891b2"/>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// === PV-диаграмма с изопроцессами ===
|
||||
function pvDiagram(W, H, pad, pMax, vMax) {
|
||||
// Возвращает axes2D-объект с диапазоном (0..vMax, 0..pMax)
|
||||
// Подписи осей: P (атм или Па), V (л или м³)
|
||||
}
|
||||
|
||||
// === Молекула газа (частица) ===
|
||||
function molecule(x, y, r, color) {
|
||||
return `<circle cx="${x}" cy="${y}" r="${r}" fill="${color}" stroke="#0f172a" stroke-width="0.8"/>`;
|
||||
}
|
||||
|
||||
// === Симуляция броуновского движения / движения молекул газа ===
|
||||
class GasSimulation {
|
||||
constructor(opts) {
|
||||
this.W = opts.W; this.H = opts.H;
|
||||
this.N = opts.N || 30;
|
||||
this.particles = [];
|
||||
for (let i = 0; i < this.N; i++) {
|
||||
this.particles.push({
|
||||
x: Math.random() * this.W,
|
||||
y: Math.random() * this.H,
|
||||
vx: (Math.random() - 0.5) * opts.speed,
|
||||
vy: (Math.random() - 0.5) * opts.speed
|
||||
});
|
||||
}
|
||||
}
|
||||
step(dt) {
|
||||
this.particles.forEach(p => {
|
||||
p.x += p.vx * dt; p.y += p.vy * dt;
|
||||
// отражение от стенок
|
||||
if (p.x < 0 || p.x > this.W) p.vx = -p.vx;
|
||||
if (p.y < 0 || p.y > this.H) p.vy = -p.vy;
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return this.particles.map(p => molecule(p.x, p.y, 4, '#2563eb')).join('');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Цветовая кодировка для физики
|
||||
|
||||
- **Положительный заряд** — красный (`#dc2626`)
|
||||
- **Отрицательный заряд** — синий (`#2563eb`)
|
||||
- **Электрическое поле $\vec{E}$** — оранжевый (`#ea580c`)
|
||||
- **Магнитное поле $\vec{B}$** — фиолетовый (`#7c3aed`)
|
||||
- **Сила** — зелёный (`#10b981`)
|
||||
- **Скорость** — голубой (`#0891b2`)
|
||||
- **Молекулы газа** — синие точки
|
||||
- **Температура** — градиент от синего (холод) к красному (горячо)
|
||||
- **Ток** — янтарный (`#d97706`)
|
||||
- **Резистор / провод** — тёмно-серый (`#374151`)
|
||||
- **Изотерма** (P-V) — оранжевый
|
||||
- **Изобара** (V-T) — синий
|
||||
- **Изохора** (P-T) — зелёный
|
||||
|
||||
### Правила (обязательны с §1)
|
||||
|
||||
1. **Все единицы измерения через KaTeX** — `\\text{Н}`, `\\text{м}^2$, `\\text{Дж/(моль·К)}` — никогда сырой текст.
|
||||
2. **Slider'ы — в реальных физических величинах** с разумными диапазонами:
|
||||
- Температура: 100..500 К (или $-100..200$ °C)
|
||||
- Давление: 0.5..5 атм или $10^4..10^6$ Па
|
||||
- Объём: 1..10 л или $0.001..0.01$ м³
|
||||
- Заряд: $\pm 10^{-9}..10^{-6}$ Кл (нКл, мкКл)
|
||||
- Расстояние между зарядами: 0.01..1 м
|
||||
- Напряжение: 1..220 В
|
||||
- Ток: 0.01..10 А
|
||||
- Магнитная индукция: $10^{-3}..1$ Тл
|
||||
3. **Snap-точки** на эталонных значениях: $T = 273$ К ($0$ °C), $T = 0$ K, $p = 1$ атм.
|
||||
4. **Все формулы с КаTeX**, двойные backslash.
|
||||
5. **Векторы — со стрелками** через `drawArrow`. Никогда `→` в тексте — через KaTeX `\\vec{E}`.
|
||||
6. **Подписи единиц измерения** — обязательны на всех осях графиков, во всех результатах калькуляторов.
|
||||
7. **Симуляции — через `requestAnimationFrame`** с возможностью паузы и сброса.
|
||||
8. **Эмодзи запрещены.**
|
||||
9. **Никаких пикселей в подписях** — всё в СИ или производных единицах.
|
||||
|
||||
### Типы интерактивов по темам
|
||||
|
||||
| Тип | Применение |
|
||||
|-----|-----------|
|
||||
| **Симуляция движения молекул** | §1 (МКТ), §3 (давление как удары о стенку), §9 (испарение) |
|
||||
| **PV/VT/PT диаграммы** | §6 (изопроцессы — главное!), §15 (цикл Карно) |
|
||||
| **Калькулятор Клапейрона–Менделеева** | §5 |
|
||||
| **Психрометрический анализ** | §10 |
|
||||
| **Поток теплоты симуляция** | §13 |
|
||||
| **Цикл Карно — анимированная PV-диаграмма** | §15 |
|
||||
| **Закон Кулона: 3D-визуализатор сил** | §17 |
|
||||
| **Линии напряжённости электрического поля** | §20 (главный визуал электростатики) |
|
||||
| **Эквипотенциальные поверхности** | §22 |
|
||||
| **Конденсатор-конструктор** | §23 |
|
||||
| **Электрическая цепь — конструктор** | §25, §26 |
|
||||
| **Правило буравчика / левой руки — 3D** | §28, §29, §30 |
|
||||
| **Движение заряженной частицы в B-поле — 3D-траектория** | §30 (главный визуал магнетизма) |
|
||||
| **Опыты Фарадея — анимация катушки и магнита** | §31 |
|
||||
| **Самоиндукция — LR-цепь, ток как функция времени** | §33 |
|
||||
| **p-n переход — визуализация дырок и электронов** | §37 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 ПОРЯДОК РЕАЛИЗАЦИИ
|
||||
|
||||
Учитывая 37 параграфов — самый большой проект, разбиваем на **8 фаз**.
|
||||
|
||||
### Phase 0: Архитектура (фундамент)
|
||||
- `physics_10_hub.html` — палитра **yellow/amber** (классический физический цвет) или **orange**
|
||||
- 6 ch-файлов: `physics_10_ch1.html` (МКТ), `_ch2.html` (термодинамика), `_ch3.html` (электростатика), `_ch4.html` (пост. ток), `_ch5.html` (магнитное поле), `_ch6.html` (ток в средах)
|
||||
- Миграция `0XX_physics_10_hub.sql` (следующий доступный номер)
|
||||
- **`frontend/js/phys.js`** — новый модуль physics-хелперов (drawArrow, fieldLines, magneticField, схемы, GasSimulation, pvDiagram)
|
||||
- 2D-хелперы (axes2D, plotFunc, etc.) и G3D подключены в каждый ch
|
||||
- KaTeX CDN + auto-render
|
||||
- POLISH CSS + bump-score JS
|
||||
|
||||
### Phase 1: Молекулярная физика — Глава 1 (10 §) — 5 волн
|
||||
Самая большая глава учебника. По 2 §§ на волну.
|
||||
- **Wave 1**: §1 (МКТ основные положения, БРОУНОВСКОЕ ДВИЖЕНИЕ симуляция) + §2 (масса/количество вещества, калькулятор $N$, $\nu$, $m$).
|
||||
- **Wave 2**: §3 (давление через удары молекул, симуляция $p = \frac{1}{3} n m \overline{v^2}$) + §4 (температура, шкалы, $\overline{E_k} = \frac{3}{2}kT$).
|
||||
- **Wave 3**: §5 (уравнение Клапейрона–Менделеева, калькулятор) + §6 (**изопроцессы — главный визуал главы**: PV/VT/PT диаграммы с slider'ом T, V, p).
|
||||
- **Wave 4**: §7 (твёрдые тела, виды кристаллов SVG) + §8 (жидкости, поверхностное натяжение симуляция).
|
||||
- **Wave 5**: §9 (испарение, насыщенный пар, динамическое равновесие симуляция) + §10 (влажность, психрометрический калькулятор) + **Финал главы 1** (7 боссов + ачивка «Мастер МКТ»).
|
||||
|
||||
### Phase 2: Молекулярная физика — Глава 2 «Термодинамика» (5 §) — 3 волны
|
||||
- **Wave 1**: §11 (внутренняя энергия, $U = \frac{3}{2} \nu R T$) + §12 (работа в термодинамике, площадь под P-V кривой).
|
||||
- **Wave 2**: §13 (количество теплоты — все 4 формулы) + §14 (первый закон термодинамики, применение к изопроцессам).
|
||||
- **Wave 3**: §15 (тепловые двигатели, **цикл Карно — анимация PV-цикла**, КПД) + **Финал главы 2** (5 боссов + ачивка «Мастер термодинамики»).
|
||||
|
||||
### Phase 3: Электростатика (9 §) — 5 волн
|
||||
- **Wave 1**: §16 (заряд, закон сохранения) + §17 (**закон Кулона — визуализатор сил**, slider $q_1, q_2, r$).
|
||||
- **Wave 2**: §18 (электрическое поле) + §19 (напряжённость, суперпозиция).
|
||||
- **Wave 3**: §20 (**линии напряжённости — главный визуал** — поле точечного заряда, диполя, двух одноимённых) + §21 (работа поля, потенциал).
|
||||
- **Wave 4**: §22 (разность потенциалов, напряжение, $E = U/d$) + §23 (конденсаторы, $C = \varepsilon \varepsilon_0 S/d$).
|
||||
- **Wave 5**: §24 (энергия поля конденсатора) + **Финал главы 3** (7 боссов + ачивка «Мастер электростатики»).
|
||||
|
||||
### Phase 4: Постоянный ток (2 §) — 1 волна
|
||||
- **Wave 1**: §25 (ЭДС, сторонние силы) + §26 (закон Ома для полной цепи, **конструктор электрической цепи**) + **Финал главы 4** (5 боссов).
|
||||
|
||||
### Phase 5: Магнитное поле и индукция (7 §) — 4 волны
|
||||
- **Wave 1**: §27 (опыт Эрстеда, симуляция) + §28 (индукция, правило буравчика, **3D-линии магнитной индукции вокруг проводника**).
|
||||
- **Wave 2**: §29 (сила Ампера, правило левой руки, симуляция) + §30 (**сила Лоренца — 3D-движение частицы по спирали в B-поле**).
|
||||
- **Wave 3**: §31 (магнитный поток, **опыты Фарадея — анимация катушки и магнита**) + §32 (правило Ленца, закон ЭМИ).
|
||||
- **Wave 4**: §33 (самоиндукция, индуктивность, LR-цепь) + **Финал главы 5** (7 боссов + ачивка «Мастер магнетизма»).
|
||||
|
||||
### Phase 6: Ток в различных средах (4 §) — 2 волны
|
||||
- **Wave 1**: §34 (металлы, сверхпроводимость) + §35 (электролиты, законы Фарадея).
|
||||
- **Wave 2**: §36 (газы, плазма, виды разрядов) + §37 (полупроводники, **p-n переход — визуализация дырок и электронов**, n-/p-тип) + **Финал главы 6** (5 боссов + ачивка «Мастер токов»).
|
||||
|
||||
### Phase 7: Финал курса Физика 10
|
||||
- Итоговая шпаргалка (6 mini-карточек: МКТ, термодинамика, электростатика, ток, магнетизм, ток в средах)
|
||||
- **10 интегрированных боссов**, комбинирующих темы из разных глав:
|
||||
1. **МКТ + термодинамика**: $U$ и $T$ моноатомного газа
|
||||
2. **Изопроцесс + работа**: $A$ за изобарное расширение
|
||||
3. **Цикл Карно**: КПД при $T_1 = 600$ К, $T_2 = 300$ К
|
||||
4. **Закон Кулона + суперпозиция**: сила на 3-й заряд от двух источников
|
||||
5. **Конденсатор + энергия**: $W$ от $C$, $U$
|
||||
6. **Закон Ома для полной цепи + КПД**
|
||||
7. **Сила Лоренца + радиус**: $R = mv/(qB)$
|
||||
8. **Закон ЭМИ Фарадея**: $\mathcal{E}_i$ от $d\Phi/dt$
|
||||
9. **Самоиндукция + энергия**: $W_L = LI^2/2$
|
||||
10. **Магистр электродинамики**: синтез
|
||||
- Ачивка **«Магистр физики 10»** (+150 XP — крупнейший курс)
|
||||
- Кнопка «К каталогу учебников» → `/textbooks`
|
||||
|
||||
---
|
||||
|
||||
## 📦 Структура каждого § (стандарт)
|
||||
|
||||
### Wave 0 главы — skeleton (включено в Phase 0)
|
||||
- CSS + цветовая палитра 3 акцентов на §
|
||||
- Sections + stub-builders
|
||||
- Hub-карточка обновляется
|
||||
- Миграция в БД
|
||||
|
||||
### Wave N — наполнение § (по 2 § на волну)
|
||||
|
||||
**Каждый § содержит:**
|
||||
- **3 теоретические карточки** (`theory`, `rule`, `example`) с SVG-схемами/графиками
|
||||
- **4 интерактива** (`.wg` виджеты):
|
||||
1. **Симуляция** или **3D/SVG-визуализатор** (главный)
|
||||
2. **Калькулятор закона** (ввод параметров → формула с подстановкой → результат с единицами)
|
||||
3. **DnD/Квикфайр** (понятийный)
|
||||
4. **Тренажёр расчётных задач** (5-6 задач с числовым ответом, допуск 1-5%)
|
||||
- **Кнопка «Я прочитал § (+10 XP)»**
|
||||
- **Прогресс/XP**: IV1 15%/10XP, IV2 15%/10XP, IV3 25%/15XP, IV4 25%/15XP, чтение 30%/10XP
|
||||
|
||||
### Wave финал главы
|
||||
- **Итоговая шпаргалка** с формулами
|
||||
- **5-7 интегрированных боссов** (синтез тем главы)
|
||||
- **Ачивка «Мастер главы N»** + 50 XP + confetti
|
||||
- **Кнопка** перехода к следующей главе
|
||||
|
||||
---
|
||||
|
||||
## 📊 Оценка объёма
|
||||
|
||||
| Глава | § | LOC |
|
||||
|-------|---|-----|
|
||||
| Phase 0: skeleton + phys.js | — | 2500 |
|
||||
| Глава 1 МКТ (§§1-10) | 10 | 13 000 |
|
||||
| Глава 2 Термодинамика (§§11-15) | 5 | 6 500 |
|
||||
| Глава 3 Электростатика (§§16-24) | 9 | 12 000 |
|
||||
| Глава 4 Пост. ток (§§25-26) | 2 | 3 000 |
|
||||
| Глава 5 Магнетизм (§§27-33) | 7 | 10 500 |
|
||||
| Глава 6 Ток в средах (§§34-37) | 4 | 5 500 |
|
||||
| Phase 7 финал курса | — | 1 500 |
|
||||
| **Итого** | **37** | **~54 500 LOC** |
|
||||
|
||||
Это **больше**, чем Геометрия 11 (~30K) или Алгебра 9 (~30K). Самый объёмный курс.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Критические правила (опыт всех 10 предыдущих учебников)
|
||||
|
||||
### ❌ НЕ делать
|
||||
- **Slider'ы в пиксельных диапазонах** (40..150). Только в реальных физических единицах с правильным масштабом отрисовки.
|
||||
- **Подписи без единиц измерения** ($p = 100$ — это что? Па? атм?). Всегда: $p = 1$ атм $= 10^5$ Па.
|
||||
- **Эмодзи** (`⚠`, `🌡️`, `🔋`). Только inline SVG.
|
||||
- **Одиночный `\` перед буквой** в JS template literals: `\dfrac`, `\sin`, `\vec`. ВСЕГДА `\\dfrac`.
|
||||
- **Сырой KaTeX в `<option>`** — KaTeX там не рендерится. Только unicode-текст в `<option>`.
|
||||
- **Эталонные значения вместо реальных** — в задачах используй $\pi = 3.14$, $g = 10$ м/с², $k_B = 1.38 \cdot 10^{-23}$ Дж/К ровно.
|
||||
- **Symbol `\degree`** не существует в KaTeX. Используй `^\\circ` для °.
|
||||
- **Бесконечные симуляции без паузы** — всегда кнопка «Пауза / Сброс» и `cancelAnimationFrame` при unmount.
|
||||
|
||||
### ✅ Обязательно
|
||||
- **Все единицы СИ в формулах** — никаких ватт в задаче без обозначения «Вт».
|
||||
- **Snap-точки в slider'ах** для эталонных значений: $T = 273$ К, $p = 1$ атм, $g = 10$ м/с².
|
||||
- **Цветовая кодировка** едина по всем § (заряд+/−, поле $E$/$B$, сила, скорость, ток).
|
||||
- **KaTeX-аудит** через `node` после каждого Wave (паттерн `[^\\]\\[a-z]{2,}` в JS-блоках).
|
||||
- **JS parse-check** через `new Function(scriptBody)` после каждого Wave.
|
||||
- **Авторов нет** — в hub footer'е только «Интерактивный учебник «Физика — 10 класс» · LearnSpace» (как сделали для остальных).
|
||||
- **Симуляции** должны иметь паузу, перезапуск, и быть **физически корректными**: реальная скорость, реальная масса, реальные коэффициенты.
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Запуск
|
||||
|
||||
**Phase 0**: skeleton 6 ch-файлов + hub + миграция + новый модуль `phys.js` со всеми physics-хелперами.
|
||||
|
||||
Палитра hub: **yellow/amber** (классическая для физики) — `--pri:#ca8a04` (yellow-600), `--pri-d:#a16207` (yellow-700), `--pri-soft:#fef3c7`. Header gradient: `linear-gradient(110deg,#713f12 0%,#ca8a04 55%,#fde047 100%)`. Эта палитра отличается от Алгебры 11 (teal) и Геометрии 11 (cyan).
|
||||
|
||||
После Phase 0 → Phase 1 Wave 1 (§1 МКТ + §2 масса молекул).
|
||||
|
||||
После завершения всех 8 Phase Физика 10 — план одного 10-класса завершён (можно потом план Алгебры 10 + Геометрии 10).
|
||||
@@ -0,0 +1,251 @@
|
||||
# План реализации: Алгебра 11 (Беларусь, Арефьева/Пирютко, 2020)
|
||||
|
||||
**Источник:** `Algebra_11kl_Arefieva_rus_2020.pdf` (276 стр., 3 главы, 10 §)
|
||||
**Авторы:** И. Г. Арефьева, О. Н. Пирютко
|
||||
**Издательство:** «Народная асвета», 2020
|
||||
|
||||
> Этот план учитывает **весь опыт Алгебры 9 + Геометрии 9** — никаких пикселей в подписях, никаких эмодзи, KaTeX с двойным экранированием с первого дня, единая SVG-инфраструктура, продуманный UX.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Содержание учебника
|
||||
|
||||
### Глава 1. Обобщение понятия степени (§1–§3)
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §1 | Степень с рациональным/действительным показателем | $a^{m/n} = \sqrt[n]{a^m}$, свойства, $a^{\sqrt{2}}$ через приближения |
|
||||
| §2 | Степенная функция $y = x^\alpha$ и её свойства | 6 типов в зависимости от $\alpha$ |
|
||||
| §3 | Определение логарифма. Основное логарифмическое тождество | $\log_a b = c \Leftrightarrow a^c = b$, $a^{\log_a b} = b$ |
|
||||
|
||||
### Глава 2. Показательная функция (§4–§6)
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §4 | Показательная функция $y = a^x$ | $D=\mathbb{R}$, $E=(0;+\infty)$, при $a>1$ возрастает, при $0<a<1$ убывает |
|
||||
| §5 | Показательные уравнения | $a^{f(x)} = a^{g(x)} \Leftrightarrow f(x)=g(x)$, методы: одинак. основание, замена, графический |
|
||||
| §6 | Показательные неравенства | При $a>1$ знак сохраняется, при $0<a<1$ — меняется |
|
||||
|
||||
### Глава 3. Логарифмическая функция (§7–§10)
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §7 | Свойства логарифмов | $\log_a(bc)$, $\log_a(b/c)$, $\log_a b^n$, переход к новому основанию |
|
||||
| §8 | Логарифмическая функция $y = \log_a x$. Свойства | $D=(0;+\infty)$, $E=\mathbb{R}$, обратная к показательной |
|
||||
| §9 | Логарифмические уравнения | $\log_a f(x) = \log_a g(x)$, замена, ОДЗ, метод приведения к одному основанию |
|
||||
| §10 | Логарифмические неравенства | Двойной учёт монотонности и ОДЗ |
|
||||
|
||||
**Итого: 10 параграфов в 3 главах.**
|
||||
|
||||
---
|
||||
|
||||
## 🎨 SVG-СТАНДАРТ КАЧЕСТВА (унаследовано от Геом 9)
|
||||
|
||||
### Хелперы (обязательны с §1)
|
||||
|
||||
Вынести в начало `<script>` каждого ch-файла. Уже работают в Алгебре 9 — копируем 1-в-1:
|
||||
|
||||
```js
|
||||
// 1. Координатная плоскость
|
||||
function axes2D(W, H, pad, xmin, xmax, ymin, ymax){
|
||||
// Возвращает {content, toX(v), toY(v), ux, uy}
|
||||
}
|
||||
|
||||
// 2. График функции y = f(x)
|
||||
function plotFunc(f, xmin, xmax, toX, toY, color, N){
|
||||
// Возвращает <path d="..."/>
|
||||
}
|
||||
|
||||
// 3. Маркер прямого угла L
|
||||
function rightAngleMark(V, uIn, wIn, s) { /* polyline */ }
|
||||
|
||||
// 4. Дуга угла с автовыбором sweep
|
||||
function angleArcAuto(V, uA, uB, R) { /* path */ }
|
||||
|
||||
// 5. Единичный вектор от p1 до p2
|
||||
function unitVec(p1, p2) { /* {x, y} */ }
|
||||
|
||||
// 6. deg → rad
|
||||
function deg2rad(d) { return d * Math.PI / 180; }
|
||||
```
|
||||
|
||||
### Новые хелперы для Алгебры 11
|
||||
|
||||
```js
|
||||
// 7. Точка на графике с проекциями на оси
|
||||
function pointWithDrop(x, f, toX, toY, color) {
|
||||
// Возвращает SVG: точка + пунктир к оси X + пунктир к оси Y + подписи координат
|
||||
}
|
||||
|
||||
// 8. Асимптота (для y=a^x: y=0; для y=log_a x: x=0)
|
||||
function asymptote(orientation, value, toX, toY, xmin, xmax, ymin, ymax) {
|
||||
// orientation: 'h' (y=value) | 'v' (x=value)
|
||||
// Возвращает <line stroke-dasharray="6 4">
|
||||
}
|
||||
|
||||
// 9. Сравнительный график двух функций (для a>1 vs 0<a<1)
|
||||
function compareGraph(W, H, fLeft, fRight, labels) {
|
||||
// Двухпанельный SVG с одинаковыми осями
|
||||
}
|
||||
|
||||
// 10. Слайдер с эталонными метками (a=1/2, 1, e, 2, 10)
|
||||
function snapSlider(value, snaps, tolerance) {
|
||||
// Snap к ближайшей эталонной точке при касании
|
||||
}
|
||||
|
||||
// 11. Логарифмическая линейка-визуализатор (для §7)
|
||||
function logScale(W, base, vals) {
|
||||
// Шкала log_a, проецирующая b на a^x = b
|
||||
}
|
||||
```
|
||||
|
||||
### Правила (обязательны с §1)
|
||||
|
||||
1. **Координаты — ВСЕГДА через `axes2D` + `toX`/`toY`.** Никаких сырых пикселей в коде графиков.
|
||||
2. **Диапазоны slider'ов — в учебных единицах**, не в пикселях. Пример: `min="0.1" max="10" step="0.1"` для $a$, не `min="40" max="150"`.
|
||||
3. **Snap-точки** для важных значений: $a=2, e (\approx 2.718), 10, 1/2, 1/e, 1/10$.
|
||||
4. **KaTeX в JS template literals — ВСЕГДА `\\dfrac`, `\\log`, `\\sqrt`** (двойной слэш).
|
||||
5. **Подписи на графиках** через KaTeX + `renderMath(box)` после `box.innerHTML`.
|
||||
6. **Цветовая кодировка:**
|
||||
- При $a > 1$ — **синий** (`#2563eb`)
|
||||
- При $0 < a < 1$ — **оранжевый** (`#ea580c`)
|
||||
- Граничный случай $a = 1$ — серый (`#94a3b8`)
|
||||
- Асимптоты — серый пунктир (`#94a3b8`, `stroke-dasharray="6 4"`)
|
||||
7. **ViewBox с запасом ≥ 24px** для подписей вне сетки.
|
||||
8. **`touch-action: none`** на каждом draggable SVG.
|
||||
9. **Drag через `window.addEventListener` + `{passive: false}`**, state ВЫШЕ `redraw()`.
|
||||
10. **Эмодзи ЗАПРЕЩЕНЫ.** Только inline SVG `.ic`.
|
||||
11. **Подписи длин/значений — в учебных единицах**, не в пикселях. Никогда `b₁ = 120` где 120 это пиксели.
|
||||
12. **Заголовок интерактива должен СОВПАДАТЬ** с количеством объектов в SVG. Не «Три эталонных треугольника», когда нарисовано два.
|
||||
|
||||
### Типы SVG по темам
|
||||
|
||||
| § | Тема | Тип SVG |
|
||||
|---|------|---------|
|
||||
| §1 | Степень с рац. показателем | Slider $a$ + табло «$a^{m/n}$ = $\sqrt[n]{a^m}$» с реальной подстановкой. Калькулятор степени без графики. |
|
||||
| §2 | Степенная функция $y=x^\alpha$ | **Координатная плоскость** + slider $\alpha \in [-3; 3]$, точка $\alpha=1$ — прямая, $\alpha=2$ — парабола, $\alpha=-1$ — гипербола. **6 эталонных кривых на одном графике** с включаемой видимостью. |
|
||||
| §3 | Определение логарифма | **Slider $a$ + slider $b$**, точка пересечения $y = a^x$ с горизонталью $y = b$ показывает $\log_a b$. Анимированный поиск. |
|
||||
| §4 | Показательная функция | **Двухпанельный график** $a > 1$ vs $0 < a < 1$, slider $a$ от $0.1$ до $10$ со snap к $2, e, 10$. Асимптота $y=0$ пунктиром. Точка $(0, 1)$, $(1, a)$ — выделена. |
|
||||
| §5 | Показательные уравнения | **Графический решатель**: два графика $y = a^{f(x)}$, $y = a^{g(x)}$, точки пересечения. Также пошаговый разбор алгебраического решения. |
|
||||
| §6 | Показательные неравенства | **Числовая прямая** + закрашенная область решений + сравнение с осью при $a>1$ и $a<1$. |
|
||||
| §7 | Свойства логарифмов | **Логарифмическая линейка** — slider $b$, $c$ показывает, что $\log(bc)$ это сумма длин. |
|
||||
| §8 | Логарифмическая функция | **Двухпанельный график** $a > 1$ vs $0 < a < 1$ + отражение $y = a^x$ через $y = x$ (показать обратность). |
|
||||
| §9 | Логарифмические уравнения | **Графический решатель** + анимация ОДЗ (выколотые точки на оси x). |
|
||||
| §10 | Логарифмические неравенства | **Числовая прямая** с учётом ОДЗ + монотонности. |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 ПОРЯДОК РЕАЛИЗАЦИИ
|
||||
|
||||
### Phase 0: Архитектура (фундамент)
|
||||
- `algebra_11_hub.html` skeleton (палитра **emerald/teal**, чтобы отличить от algebra_7 розовой, algebra_9 индиго)
|
||||
- `algebra_11_ch1.html`, `_ch2.html`, `_ch3.html` skeletons
|
||||
- Миграция `022_algebra_11_hub.sql` (slug `algebra-11`, sort_order 9)
|
||||
- SVG-хелперы (axes2D, plotFunc, pointWithDrop, asymptote, compareGraph) уже встроены в skeleton
|
||||
- KaTeX CDN + auto-render в head
|
||||
- CSS-блок «POLISH» (анимации появления, hover, bump-score) — копия из Геом 9
|
||||
- ICONS объект целиком из algebra_9_ch4
|
||||
|
||||
### Phase 1: Алгебра 11 Глава 1 «Обобщение степени» (3 §) — 3 волны
|
||||
- **Wave 1**: §1 «Степень с рац. показателем». 3 теории + 4 интерактива (slider $a^{m/n}$, калькулятор, DnD «корень↔степень», тренажёр).
|
||||
- **Wave 2**: §2 «Степенная функция $y=x^\alpha$». 6 типов графиков на одном координатном поле, slider $\alpha$, цветовая кодировка чёт./нечёт./дробн./отриц.
|
||||
- **Wave 3**: §3 «Определение логарифма». Slider $a$, slider $b$, визуализация $\log_a b$ через пересечение $y = a^x$ и $y = b$. + Финал главы 1 (4 mini-карточки + 5 боссов + ачивка «Магистр степеней»).
|
||||
|
||||
### Phase 2: Алгебра 11 Глава 2 «Показательная функция» (3 §) — 3 волны
|
||||
- **Wave 1**: §4 «Показательная функция $y = a^x$». **Главный интерактив всей главы** — двухпанельный график $a>1$ vs $0<a<1$, slider $a$ со snap-точками, анимированные асимптоты, отметка точки $(0,1)$. 4 интерактива.
|
||||
- **Wave 2**: §5 «Показательные уравнения». 5 методов решения: одинаковое основание / однородные / замена переменной / графический / тригонометрические подстановки. Визуальный разбор каждого метода + тренажёр.
|
||||
- **Wave 3**: §6 «Показательные неравенства» + Финал главы 2 (5 боссов + ачивка «Магистр показательной функции»).
|
||||
|
||||
### Phase 3: Алгебра 11 Глава 3 «Логарифмическая функция» (4 §) — 4 волны
|
||||
- **Wave 1**: §7 «Свойства логарифмов». **Логарифмическая линейка** (Slide rule visualizer). DnD-сортер свойств. Калькулятор с пошаговым применением свойств.
|
||||
- **Wave 2**: §8 «Логарифмическая функция $y = \log_a x$». Парный двухпанельный график + отражение через $y = x$ для демонстрации обратной функции.
|
||||
- **Wave 3**: §9 «Логарифмические уравнения». 4 метода: к одному основанию / потенциирование / замена / графический. + Проверка ОДЗ.
|
||||
- **Wave 4**: §10 «Логарифмические неравенства» + Финал главы 3 (5 боссов + ачивка «Магистр логарифмов»).
|
||||
|
||||
### Phase 4: Финал курса (Phase 5 в Алгебре 9 формате)
|
||||
- Итоговая шпаргалка (5 mini-карточек: степени / степенная / показательная / логарифмы / лог. функция)
|
||||
- **7 интегрированных боссов**, комбинирующих темы из разных глав:
|
||||
1. **Степень + логарифм**: вычислить $4^{\log_2 5}$
|
||||
2. **Показательное уравнение + замена**: $4^x - 3 \cdot 2^x + 2 = 0$
|
||||
3. **Логарифмическое неравенство + ОДЗ**: $\log_{1/2}(x-1) > -2$
|
||||
4. **Свойства log**: упростить $\log_3 \tfrac{72}{8} - \log_3 \tfrac{9}{2}$
|
||||
5. **Графический синтез**: найти кол-во решений $2^x = \log_2 x$
|
||||
6. **Логарифмирование показательного**: решить $5^{2x} = 7$ через $\log_5$
|
||||
7. **Магистр функций**: при каком $a$ функция $y = (a-2)^x$ убывает?
|
||||
- Ачивка «Магистр алгебры 11» + 50 XP + confetti
|
||||
- Кнопка «К каталогу учебников» → `/textbooks`
|
||||
|
||||
---
|
||||
|
||||
## 📦 Структура каждой главы
|
||||
|
||||
### Wave 0 — skeleton (включено в Phase 0)
|
||||
- CSS (палитра 3 цветовых акцента на §)
|
||||
- Sections со stub-builder'ами
|
||||
- Hub-карточка обновляется
|
||||
- Миграция в БД
|
||||
|
||||
### Wave N — наполнение § (по 1–2 § на волну)
|
||||
|
||||
Каждый § = ровно тот же шаблон, что и Геометрия 9:
|
||||
- **3 теоретических карточки** (`makeCard` типа theory/rule/example) с SVG-графиками
|
||||
- **4 интерактива** (`.wg` виджеты):
|
||||
1. **SVG-конструктор** с slider'ом (главный визуализатор темы)
|
||||
2. **Калькулятор/пошаговый разбор** (ввод параметров → формула с подстановкой)
|
||||
3. **Квикфайр-викторина** (6–8 заданий, 2–4 кнопки)
|
||||
4. **Тренажёр** (5–6 задач с числовым ответом, допуск 0.01)
|
||||
- **Кнопка «Я прочитал §» (+10 XP, +30%)**
|
||||
- **Прогресс**: IV1 15%/10XP, IV2 15%/10XP, IV3 25%/15XP, IV4 25%/15XP, чтение 30%/10XP
|
||||
|
||||
### Wave финал главы
|
||||
- **Итоговая шпаргалка** — mini-cards с формулой на §
|
||||
- **5 интегрированных боссов** (комбинирующих темы) — каждый +10 XP, +18% к `finalN`
|
||||
- **Достижение «Мастер главы N»** при 5/5 + 50 XP + confetti
|
||||
- **Кнопка «Дальше: Глава N+1 →»** (или «К хабу» для последней)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Оценка объёма
|
||||
|
||||
| Глава | § | Ожидаемый LOC |
|
||||
|-------|---|---------------|
|
||||
| Глава 1 (§1-3) | 3 | ~5500 |
|
||||
| Глава 2 (§4-6) | 3 | ~5800 (главные графики функций) |
|
||||
| Глава 3 (§7-10) | 4 | ~7000 |
|
||||
| Hub + финал курса | — | ~900 |
|
||||
| **Итого** | **10** | **~19 000 LOC** |
|
||||
|
||||
Для сравнения: Алгебра 9 = 19 § = ~30 000 LOC. Здесь меньше § (10 вместо 19), но каждый требует визуально насыщенных графиков.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Критические правила (на основе опыта Алгебры 9 + Геом 9)
|
||||
|
||||
### ❌ НЕ делать
|
||||
- **Slider'ы в пиксельных диапазонах** (40..150). Только в учебных единицах с понятным шагом.
|
||||
- **Подписи длин в пикселях** (`R = 120`, `c = 150`). Только в учебных единицах.
|
||||
- **«Три треугольника» когда нарисовано два** — заголовок и контент должны совпадать.
|
||||
- **Эмодзи `⚠`, `❌`, `✅`, `🔥`** в коде. Только inline SVG.
|
||||
- **Одиночный `\` перед буквой** в template literals: `\dfrac`, `\sin`, `\log`. ВСЕГДА `\\dfrac`.
|
||||
- **Прямой угол в нестандартной позиции** (хотя в 11 классе их меньше — больше графики).
|
||||
- **Sweep=1 без проверки направления** — используем `angleArcAuto`.
|
||||
- **Графики с плохими асимптотами** — например, $y = a^x$ должен явно показывать асимптоту $y=0$, а $y = \log_a x$ — $x=0$.
|
||||
|
||||
### ✅ Обязательно
|
||||
- **KaTeX-аудит на каждом коммите** — `grep -nP "[^\\\\]\\\\[a-z]{2,}"` для JS-секций.
|
||||
- **JS parse-check** через `new Function(scriptBody)` после каждого Wave.
|
||||
- **Координаты — формулами**, не «на глаз».
|
||||
- **Drag — state ВЫШЕ `redraw()`**.
|
||||
- **Цветовая кодировка $a>1$ vs $0<a<1$** во всех § Глав 2 и 3 — пусть будет неизменна.
|
||||
- **Эталонные snap-точки** в slider'ах: $a = 1/2, e, 2, 10$.
|
||||
- **Все builder-функции в registry должны быть НЕ stub'ами** в конце Wave финала.
|
||||
- **Использовать `ast-index` для поиска** (НЕ grep — правило проекта).
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Запуск
|
||||
|
||||
**Phase 0**: skeleton всех 3 ch-файлов Алгебры 11 + hub + миграция + 11 SVG-хелперов.
|
||||
Цвет: **emerald/teal** (свежий, отличается от algebra-9 индиго и algebra-7 розового).
|
||||
Сразу с первого Wave — все правила качества из этого плана.
|
||||
|
||||
После Phase 0 → Phase 1 Wave 1 (§1 Степень с рац. показателем).
|
||||
|
||||
После завершения всех 4 Phase Алгебры 11 — переход к плану Геометрии 11.
|
||||
@@ -0,0 +1,349 @@
|
||||
# План реализации: Геометрия 11 (Беларусь, Латотин/Чеботаревский, 2020)
|
||||
|
||||
**Источник:** `geometriya_11kl_latotin_rus_2020.pdf` (237 стр., 4 раздела, 11 §)
|
||||
**Авторы:** Л. А. Латотин, Б. Д. Чеботаревский, И. В. Горбунова, О. Е. Цыбулько
|
||||
**Издательство:** «Белорусская энциклопедия имени Петруся Бровки», 2020
|
||||
**Уровень:** Базовый + повышенный.
|
||||
|
||||
> Это **стереометрия в выпускном классе**. Главный вызов — настоящие 3D-фигуры в браузере: призма, цилиндр, пирамида, конус, сфера, многогранники. Реализуем собственный мини-3D движок поверх SVG для полного контроля и нулевых зависимостей. Повторение (§8-§11) сделаем компактно — это обзор всей школьной геометрии.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Содержание учебника
|
||||
|
||||
### Раздел 1. Призма и цилиндр (§1–§2)
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §1 | Призма | $S_{бок} = P \cdot l$ (наклонная), $S_{бок} = P_{осн} \cdot h$ (прямая), $V = S_{осн} \cdot h$. Прямая, правильная, наклонная. Параллелепипед, прямоугольный, куб. |
|
||||
| §2 | Цилиндр | $S_{бок} = 2\pi R h$, $S_{полн} = 2\pi R(R+h)$, $V = \pi R^2 h$. Сечения, развёртка. |
|
||||
|
||||
### Раздел 2. Пирамида и конус (§3–§4)
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §3 | Пирамида | $V = \tfrac{1}{3} S_{осн} h$, правильная: $S_{бок} = \tfrac{1}{2} P_{осн} \cdot l$ (l — апофема). Усечённая пирамида. |
|
||||
| §4 | Конус | $S_{бок} = \pi R l$, $V = \tfrac{1}{3} \pi R^2 h$, развёртка — сектор. Усечённый конус. |
|
||||
|
||||
### Раздел 3. Сфера и шар (§5–§7)
|
||||
| § | Тема | Ключевые формулы |
|
||||
|---|------|------------------|
|
||||
| §5 | Сфера | Уравнение $(x-a)^2 + (y-b)^2 + (z-c)^2 = R^2$, касательная плоскость. |
|
||||
| §6 | Шар | $S = 4\pi R^2$, $V = \tfrac{4}{3}\pi R^3$. Шаровой сегмент, сектор, слой. Вписанные и описанные многогранники. |
|
||||
| §7 | Правильные многогранники | 5 платоновых тел: тетраэдр, куб, октаэдр, додекаэдр, икосаэдр. Двойственность. |
|
||||
|
||||
### Раздел 4. Повторение всей геометрии (§8–§11)
|
||||
| § | Тема | Объём |
|
||||
|---|------|-------|
|
||||
| §8 | Геометрические фигуры и их свойства (повторение планиметрии) | 156-188 стр. |
|
||||
| §9 | Геометрические величины (площади, объёмы) | 156-188 стр. |
|
||||
| §10 | Координаты и векторы (включая 3D-векторы) | 189-199 стр. |
|
||||
| §11 | Геометрические построения | 200-219 стр. |
|
||||
|
||||
**Итого: 11 параграфов в 4 разделах.** § 1–7 — новый материал стереометрии. § 8–11 — итоговое повторение.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 SVG-СТАНДАРТ 3D-КАЧЕСТВА
|
||||
|
||||
Это самое большое нововведение всего проекта. Нужен мини-3D движок поверх SVG.
|
||||
|
||||
### Мини-3D движок (хелпер `g3d.js` или встроен в каждый ch)
|
||||
|
||||
```js
|
||||
// === Матрицы 3D-поворота ===
|
||||
function matRotY(angle) {
|
||||
const c = Math.cos(angle), s = Math.sin(angle);
|
||||
return [[c, 0, s], [0, 1, 0], [-s, 0, c]];
|
||||
}
|
||||
function matRotX(angle) {
|
||||
const c = Math.cos(angle), s = Math.sin(angle);
|
||||
return [[1, 0, 0], [0, c, -s], [0, s, c]];
|
||||
}
|
||||
function matMul(A, B) { /* умножение матриц 3x3 */ }
|
||||
function vecApply(M, v) {
|
||||
return {
|
||||
x: M[0][0]*v.x + M[0][1]*v.y + M[0][2]*v.z,
|
||||
y: M[1][0]*v.x + M[1][1]*v.y + M[1][2]*v.z,
|
||||
z: M[2][0]*v.x + M[2][1]*v.y + M[2][2]*v.z
|
||||
};
|
||||
}
|
||||
|
||||
// === Проекция (изометрическая или перспективная) ===
|
||||
function projectIso(v, cx, cy, scale) {
|
||||
// Изометрическая: x' = scale * (v.x - v.z), y' = scale * (v.y - (v.x + v.z)/2)
|
||||
const angle = Math.PI / 6;
|
||||
return {
|
||||
x: cx + scale * (v.x * Math.cos(angle) - v.z * Math.cos(angle)),
|
||||
y: cy - scale * (v.y - v.x * Math.sin(angle) - v.z * Math.sin(angle))
|
||||
};
|
||||
}
|
||||
|
||||
// Перспективная проекция с виртуальной камерой
|
||||
function projectPersp(v, camDist, cx, cy, scale) {
|
||||
const z = v.z + camDist;
|
||||
const k = scale / z;
|
||||
return { x: cx + v.x * k, y: cy - v.y * k };
|
||||
}
|
||||
|
||||
// === Сцена ===
|
||||
function createScene(W, H, opts) {
|
||||
return {
|
||||
W, H,
|
||||
cx: W / 2, cy: H / 2,
|
||||
scale: opts.scale || 60,
|
||||
rotX: opts.rotX || -0.3, // наклон вниз
|
||||
rotY: opts.rotY || 0.6, // поворот в сторону
|
||||
objects: [],
|
||||
render() { /* возвращает SVG content */ }
|
||||
};
|
||||
}
|
||||
|
||||
// === Фигуры ===
|
||||
function prismVertices(n, R, h) {
|
||||
// Правильная n-угольная призма с основанием R и высотой h
|
||||
const bottom = [], top = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = 2 * Math.PI * i / n - Math.PI/2;
|
||||
bottom.push({ x: R * Math.cos(a), y: -h/2, z: R * Math.sin(a) });
|
||||
top.push({ x: R * Math.cos(a), y: h/2, z: R * Math.sin(a) });
|
||||
}
|
||||
return { vertices: [...bottom, ...top], faces: prismFaces(n) };
|
||||
}
|
||||
|
||||
function pyramidVertices(n, R, h) {
|
||||
const base = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = 2 * Math.PI * i / n - Math.PI/2;
|
||||
base.push({ x: R * Math.cos(a), y: -h/2, z: R * Math.sin(a) });
|
||||
}
|
||||
const apex = { x: 0, y: h/2, z: 0 };
|
||||
return { vertices: [...base, apex], faces: pyramidFaces(n) };
|
||||
}
|
||||
|
||||
function cylinderMesh(R, h, segments) {
|
||||
// segments = 32 (число делений окружности)
|
||||
// Возвращает массив отрезков образующих + 2 эллипса
|
||||
}
|
||||
|
||||
function coneMesh(R, h, segments) { /* аналогично */ }
|
||||
|
||||
function sphereMesh(R, latSegs, lonSegs) {
|
||||
// Сетка из параллелей и меридианов
|
||||
}
|
||||
|
||||
// === Скрытие невидимых рёбер (back-face culling + Z-sort) ===
|
||||
function sortFacesByZ(faces, M) {
|
||||
return faces.sort((a, b) => {
|
||||
const za = avgZ(a, M), zb = avgZ(b, M);
|
||||
return zb - za; // дальние сначала
|
||||
});
|
||||
}
|
||||
|
||||
function isVisibleFace(face, M, camDist) {
|
||||
// Нормаль грани через cross product, проверка знака с z-вектором камеры
|
||||
}
|
||||
|
||||
// === Renderer ===
|
||||
function renderObject(obj, M, projector, opts) {
|
||||
const projected = obj.vertices.map(v => projector(vecApply(M, v)));
|
||||
let s = '';
|
||||
// Скрытые рёбра — пунктиром, видимые — сплошной линией
|
||||
obj.faces.forEach(face => {
|
||||
const points = face.indices.map(i => projected[i].x + ',' + projected[i].y).join(' ');
|
||||
const visible = isVisibleFace(face, M);
|
||||
s += `<polygon points="${points}" fill="${opts.fill}" fill-opacity="${visible ? 0.18 : 0}" stroke="${visible ? '#0f172a' : '#94a3b8'}" stroke-width="${visible ? 1.8 : 1}" stroke-dasharray="${visible ? '' : '4 3'}"/>`;
|
||||
});
|
||||
return s;
|
||||
}
|
||||
```
|
||||
|
||||
### Drag для вращения сцены
|
||||
|
||||
```js
|
||||
function attachOrbitControl(svg, scene) {
|
||||
let dragging = false, lastX = 0, lastY = 0;
|
||||
svg.style.touchAction = 'none';
|
||||
svg.style.cursor = 'grab';
|
||||
function onDown(e) {
|
||||
dragging = true; svg.style.cursor = 'grabbing';
|
||||
const p = (e.touches ? e.touches[0] : e);
|
||||
lastX = p.clientX; lastY = p.clientY;
|
||||
e.preventDefault();
|
||||
}
|
||||
function onMove(e) {
|
||||
if (!dragging) return;
|
||||
const p = (e.touches ? e.touches[0] : e);
|
||||
const dx = p.clientX - lastX, dy = p.clientY - lastY;
|
||||
scene.rotY += dx * 0.012;
|
||||
scene.rotX += dy * 0.012;
|
||||
scene.rotX = Math.max(-1.2, Math.min(1.2, scene.rotX)); // clamp
|
||||
lastX = p.clientX; lastY = p.clientY;
|
||||
scene.redraw();
|
||||
e.preventDefault();
|
||||
}
|
||||
function onUp() { dragging = false; svg.style.cursor = 'grab'; }
|
||||
svg.addEventListener('mousedown', onDown, { passive: false });
|
||||
svg.addEventListener('touchstart', onDown, { passive: false });
|
||||
window.addEventListener('mousemove', onMove, { passive: false });
|
||||
window.addEventListener('touchmove', onMove, { passive: false });
|
||||
window.addEventListener('mouseup', onUp);
|
||||
window.addEventListener('touchend', onUp);
|
||||
}
|
||||
```
|
||||
|
||||
### Правила (обязательны с §1)
|
||||
|
||||
1. **Изометрическая проекция по умолчанию** (классическая для школьных стереометрических чертежей).
|
||||
2. **Drag-to-rotate** на каждом 3D-SVG, slider'ы $\alpha$/$\beta$ — для точного контроля.
|
||||
3. **Кнопки «Сверху», «Сбоку», «Спереди»** — мгновенный возврат к стандартным проекциям.
|
||||
4. **Невидимые рёбра — пунктиром** (`stroke-dasharray="4 3"`), видимые — сплошной линией.
|
||||
5. **Полупрозрачная заливка граней** (`fill-opacity="0.18"`) для понимания глубины.
|
||||
6. **Подписи вершин** — снаружи фигуры (после проекции).
|
||||
7. **Подсветка сечения** — другой цвет грани (например, розовый/розовый-полупрозрачный).
|
||||
8. **Slider'ы — в учебных единицах** (рёбра 1..8 ед., высота 1..10 ед.), НЕ в пикселях.
|
||||
9. **Цветовая кодировка:**
|
||||
- Основание призмы/пирамиды — розовое (`#fce7f3`, обводка `#db2777`)
|
||||
- Боковая поверхность — голубое (`#dbeafe`, обводка `#0891b2`)
|
||||
- Высота, апофема — красный (`#dc2626`) пунктиром
|
||||
- Радиус основания — фиолетовый (`#7c3aed`)
|
||||
- Сечения — оранжевое (`#fed7aa`, обводка `#ea580c`)
|
||||
10. **KaTeX везде с двойным экранированием** в JS template literals.
|
||||
11. **Эмодзи запрещены.** Только inline SVG.
|
||||
|
||||
### Типы 3D-SVG по темам
|
||||
|
||||
| § | Тема | Тип SVG |
|
||||
|---|------|---------|
|
||||
| §1 | Призма | Slider $n$ (3..8) + $h$. Правильная призма с возможностью вращения. Опционально: показ диагоналей основания, диагоналей призмы, высот граней. Развёртка (плоская). |
|
||||
| §2 | Цилиндр | Slider $R$, $h$. Прозрачный цилиндр с осью. Сечения: осевое (прямоугольник), параллельное основанию (круг), наклонное (эллипс). Развёртка. Касательная плоскость. |
|
||||
| §3 | Пирамида | Slider $n$ (3..6), $R$ (основание), $h$. Правильная пирамида с апофемой. Усечённая пирамида (slider $r$ верхнего основания). Подсветка апофемы. |
|
||||
| §4 | Конус | Slider $R$, $h$. Конус с образующей и осью. Развёртка — сектор. Усечённый конус. Касательная плоскость по образующей. |
|
||||
| §5 | Сфера | 3D-сфера с сеткой параллелей и меридианов. Slider $R$, центр $(a,b,c)$. Касательная плоскость в точке. |
|
||||
| §6 | Шар | Шаровой сегмент, сектор, слой. Slider высоты сегмента. Шар, вписанный в куб; куб, вписанный в шар. |
|
||||
| §7 | Правильные многогранники | 5 платоновых тел с возможностью переключения. Вращение, развёртки. Двойственность (тетраэдр-тетраэдр, куб-октаэдр, икосаэдр-додекаэдр). |
|
||||
| §8 | Повторение планиметрии | 2D-фигуры из Геом 9 (без 3D). Компактные интерактивы. |
|
||||
| §9 | Геометрические величины | Площади и объёмы — комбинация 2D и 3D в зависимости от темы. |
|
||||
| §10 | Координаты и векторы | 3D-координатная система с осями. Векторы как стрелки в 3D. Сумма, разность, скалярное произведение. |
|
||||
| §11 | Геометрические построения | 2D-построения циркулем и линейкой (как в Геом 9). |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 ПОРЯДОК РЕАЛИЗАЦИИ
|
||||
|
||||
### Phase 0: Архитектура (фундамент)
|
||||
- `geometry_11_hub.html` skeleton (палитра **cyan/sky**, чтобы отличить)
|
||||
- `geometry_11_ch1.html` (§1-2 Призма + Цилиндр)
|
||||
- `geometry_11_ch2.html` (§3-4 Пирамида + Конус)
|
||||
- `geometry_11_ch3.html` (§5-7 Сфера + Шар + Правильные многогранники)
|
||||
- `geometry_11_ch4.html` (§8-11 Повторение)
|
||||
- Миграция `023_geometry_11_hub.sql` (slug `geometry-11`, sort_order 10)
|
||||
- Мини-3D движок (`g3d` встроен в каждый ch как глобальный скрипт)
|
||||
- KaTeX CDN, CSS POLISH, ICONS из geom9_ch4
|
||||
|
||||
### Phase 1: Раздел 1 «Призма и цилиндр» (2 §) — 3 волны
|
||||
- **Wave 1**: §1 «Призма». 3 теории + 4 интерактива:
|
||||
1. **3D-конструктор призмы**: slider $n$ (3..8), $a$ (сторона), $h$ (высота). Drag-to-rotate. Развёртка по кнопке.
|
||||
2. **Калькулятор площадей и объёма**: ввод $n, a, h$ → $S_{осн}, P, S_{бок}, S_{полн}, V$ с пошаговым выводом.
|
||||
3. **DnD-сортер**: фигуры → типы (правильная / прямая / наклонная / параллелепипед / куб).
|
||||
4. **Тренажёр**: 6 задач на $V, S$ призмы.
|
||||
- **Wave 2**: §2 «Цилиндр». 3 теории + 4 интерактива:
|
||||
1. **3D-конструктор цилиндра**: slider $R$, $h$. Drag-to-rotate. Сечения: осевое, параллельное, наклонное (slider угла наклона плоскости).
|
||||
2. **Калькулятор**: $S_{бок}, S_{полн}, V$.
|
||||
3. **Квикфайр «Какое сечение?»**: какая фигура получится — круг / эллипс / прямоугольник.
|
||||
4. **Тренажёр**.
|
||||
- **Wave 3**: Финал раздела 1 (4 mini-шпаргалки + 5 боссов + ачивка «Мастер призмы и цилиндра»).
|
||||
|
||||
### Phase 2: Раздел 2 «Пирамида и конус» (2 §) — 3 волны
|
||||
- **Wave 1**: §3 «Пирамида». 3D-конструктор с slider'ом $n$, $R$, $h$. Подсветка апофемы. Усечённая пирамида.
|
||||
- **Wave 2**: §4 «Конус». 3D-конус с осью, образующей. Развёртка-сектор с slider'ом угла раскрытия. Касательная плоскость.
|
||||
- **Wave 3**: Финал раздела 2 (5 боссов + ачивка «Мастер пирамиды и конуса»).
|
||||
|
||||
### Phase 3: Раздел 3 «Сфера, шар, многогранники» (3 §) — 4 волны
|
||||
- **Wave 1**: §5 «Сфера». 3D-сфера с сеткой параллелей/меридианов. Касательная плоскость в точке. Уравнение сферы в координатах. Slider центра $(a,b,c)$ и $R$.
|
||||
- **Wave 2**: §6 «Шар». Площадь поверхности, объём. Шаровой сегмент/сектор/слой. Вписанные и описанные многогранники.
|
||||
- **Wave 3**: §7 «Правильные многогранники». 5 платоновых тел с переключателем. Свойства каждого. Двойственность.
|
||||
- **Wave 4**: Финал раздела 3 (5 боссов + ачивка «Мастер сфер»).
|
||||
|
||||
### Phase 4: Раздел 4 «Повторение» (4 §) — компактно, 3 волны
|
||||
- **Wave 1**: §8 «Планиметрия» + §9 «Площади и объёмы». Каждый — 2 теории + 3 интерактива (без 3D). Это обзор, не углубление.
|
||||
- **Wave 2**: §10 «Координаты и векторы 3D». 3D-визуализатор векторов (стрелки в координатной системе). Slider'ы координат вектора. Скалярное произведение, угол.
|
||||
- **Wave 3**: §11 «Построения» + Финал раздела 4 (3 боссов на каждое §).
|
||||
|
||||
### Phase 5: Финал всего курса
|
||||
- Итоговая шпаргалка по 4 разделам.
|
||||
- **9 интегрированных боссов** (геометрия 11 объёмная — больше боссов):
|
||||
1. **Призма + Цилиндр**: цилиндр вписан в правильную 6-угольную призму...
|
||||
2. **Пирамида + Сечение**: найти угол наклона апофемы.
|
||||
3. **Конус + Развёртка**: найти угол сектора по $R$ и $l$.
|
||||
4. **Шар + Многогранник**: вписать шар в куб.
|
||||
5. **Сфера + Координаты**: уравнение касательной плоскости.
|
||||
6. **Многогранники + Объёмы**: найти объём октаэдра через куб.
|
||||
7. **3D-векторы**: скалярное произведение, угол.
|
||||
8. **Параллелепипед + Диагональ**: $d^2 = a^2 + b^2 + c^2$.
|
||||
9. **Магистр стереометрии**: синтез всех формул.
|
||||
- Ачивка «Магистр геометрии 11» + 100 XP бонус + confetti.
|
||||
- Кнопка «К каталогу учебников».
|
||||
|
||||
---
|
||||
|
||||
## 📦 Структура каждого § (стандарт)
|
||||
|
||||
То же, что в Геом 9 / Алгебра 9 / Алгебра 11:
|
||||
- 3 теоретических карточки (`makeCard`)
|
||||
- 4 интерактива (один из них — 3D-конструктор, остальные могут быть 2D-калькуляторы, DnD, квикфайр, тренажёр)
|
||||
- Кнопка «Я прочитал § (+10 XP)»
|
||||
- Прогресс/XP: IV1 15%/10XP, IV2 15%/10XP, IV3 25%/15XP, IV4 25%/15XP, чтение 30%/10XP
|
||||
|
||||
### Финал раздела
|
||||
- 4 mini-шпаргалки (по §) с ключевыми формулами
|
||||
- 5 боссов (каждый +10 XP, +18% к `finalN`)
|
||||
- Ачивка `rN_done` при 5/5 (+50 XP бонус)
|
||||
- Кнопка к следующему разделу
|
||||
|
||||
---
|
||||
|
||||
## 📊 Оценка объёма
|
||||
|
||||
| Раздел | § | Особенности | LOC |
|
||||
|--------|---|-------------|-----|
|
||||
| Phase 0: skeleton | — | 4 ch + hub + 3D-движок | 1500 |
|
||||
| Раздел 1 (§1-2) | 2 | Призма, цилиндр — главный 3D | 6000 |
|
||||
| Раздел 2 (§3-4) | 2 | Пирамида, конус | 5500 |
|
||||
| Раздел 3 (§5-7) | 3 | Сфера, шар, многогранники | 8000 |
|
||||
| Раздел 4 (§8-11) | 4 | Повторение — компактно | 6000 |
|
||||
| Финал курса | — | 9 боссов | 1200 |
|
||||
| **Итого** | **11** | | **~28 000 LOC** |
|
||||
|
||||
Это больше, чем Геом 9 (~26K LOC), из-за 3D-движка и стереометрии.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Критические правила
|
||||
|
||||
### ❌ НЕ делать
|
||||
- **3D в пиксельных координатах**. Вершины фигур — всегда в учебных единицах, проекция масштабирует к пикселям.
|
||||
- **«Магические числа» в матрицах поворота**. Всё через `matRotX`, `matRotY`, `matMul`.
|
||||
- **Слайдеры рёбер $a$ от 40 до 150**. Только в единицах: $a \in [1; 8]$.
|
||||
- **Drag без `passive: false`** — будет глючить на тач-устройствах.
|
||||
- **Hidden surface removal на глаз** — используем sort by avg-Z или back-face culling.
|
||||
- **Подписи вершин внутри фигуры** — всегда снаружи, после проекции.
|
||||
- **Сечения с пересечениями граней без проверки** — может быть некрасиво. Проверяем алгоритм.
|
||||
|
||||
### ✅ Обязательно
|
||||
- **Drag-to-rotate** на каждом 3D-SVG (не только slider'ы).
|
||||
- **3 предустановленные кнопки проекции** (сверху/сбоку/спереди).
|
||||
- **Slider'ы в учебных единицах** с разумным масштабом проекции.
|
||||
- **JS parse-check** после каждого Wave.
|
||||
- **KaTeX-аудит** через grep regex после каждого Wave.
|
||||
- **3D-движок — общий файл `js/g3d.js`** или встроен в каждый ch (предпочтительно общий, чтобы не дублировать).
|
||||
- **Все builder-функции в registry — НЕ stub'ы** в конце Wave финала.
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Запуск
|
||||
|
||||
**Phase 0**: skeleton 4 ch-файлов Геом 11 + hub + миграция + мини-3D движок.
|
||||
Палитра: **cyan/sky** (свежая, отличается от geom-9 розовой/rose).
|
||||
|
||||
После Phase 0 → Phase 1 Wave 1 (§1 Призма — самая первая 3D-фигура).
|
||||
|
||||
После завершения всех 5 Phase Геометрии 11 — план двух 11-классов завершён.
|
||||
@@ -0,0 +1,425 @@
|
||||
# План реализации: Химия 7 (Беларусь) — интерактивный наглядный учебник
|
||||
|
||||
> Цель: создать **с нуля** интерактивный наглядный учебник по **всей программе 7 класса**
|
||||
> в **современной архитектуре hub + главы** (как Химия 8 / Физика 7–11 / Алгебра / Геометрия),
|
||||
> на уровне их качества, с поправкой на содержание 7 класса.
|
||||
>
|
||||
> **7 класс — это ПЕРВЫЙ курс химии у школьника.** Поэтому всё качественно и наглядно,
|
||||
> БЕЗ количественной химии 8 класса (нет моля, молярной массы, расчётов по уравнениям,
|
||||
> периодического закона, строения атома, химической связи, ТЭД, ионных уравнений,
|
||||
> степени окисления). Акцент — на **первоначальные понятия** (вещество, атом, элемент,
|
||||
> молекула, формула, валентность, химическая реакция, уравнение) и **первое знакомство**
|
||||
> с кислородом, водородом, водой и важнейшими классами соединений (оксиды, кислоты, соли,
|
||||
> основания) на уровне состава, наглядных свойств и качественных реакций.
|
||||
>
|
||||
> **Архитектура (по образцу Химии 8):** `chemistry_7_hub.html` (хаб-каталог глав) + **4 файла глав**.
|
||||
> В БД — родитель `chemistry-7` + 4 ребёнка с `parent_slug`. Каждая глава — самостоятельная
|
||||
> страница со своими § (см. карту ниже), модульный CSS/JS на предмет. Единый стандарт с
|
||||
> [[../textbooks-8/PLAN_CHEMISTRY_8.md]] и [[../textbooks-9/PLAN_CHEMISTRY_9.md]].
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Источник
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Книга | `himiya_7kl_shimanovich_rus_2023 (1).pdf` |
|
||||
| Авторы | Шиманович И. Е., Красицкий В. А., Сечко О. И., Хвалюк В. Н. |
|
||||
| Изд. | Минск, «Народная асвета», 2023 (2-е издание, пересмотренное), 175 с., тираж 121 000 |
|
||||
| Структура | **4 главы, 26 §, 5 лабораторных опытов, 4 практические работы, 7 приложений** |
|
||||
| Справочные таблицы (приложения) | ряд активности металлов (Прил.4), расчёт массовой доли элемента (Прил.3), бытовые названия веществ (Прил.6), единицы массы/объёма (Прил.7), вода в природе (Прил.5) |
|
||||
|
||||
PDF лежит в `G:\Dev\Тесты\Методички\тест_6 класс\Книги\`. Оглавление — стр. 3–4 PDF.
|
||||
Авторов **в hub НЕ показываем** (политика проекта, см. [[../textbooks-7/PLAN_PHYSICS_7.md]]).
|
||||
|
||||
> **Важные отличия от Химии 8:**
|
||||
> 1. Это **первый** курс химии — объяснять с нуля, простым языком, максимум наглядности.
|
||||
> 2. **Качественно, не количественно**: валентность (а не степень окисления), `M_r` как
|
||||
> «во сколько раз тяжелее» (без моля и `M`), баланс уравнений подбором (без стехиометрии).
|
||||
> 3. Классы соединений даются **в порядке знакомства по ходу курса** (оксиды — в теме «Кислород»,
|
||||
> кислоты и соли — в теме «Водород», основания — в теме «Вода»), а НЕ единой главой как в 8 кл.
|
||||
> 4. Индикаторы (лакмус, метилоранж, фенолфталеин), ряд активности, признаки реакций —
|
||||
> **ядро наглядности** курса.
|
||||
>
|
||||
> **Соответствие «глава книги → файл главы → slug»:**
|
||||
>
|
||||
> | Глава книги | § | Файл | slug | Цвет |
|
||||
> |---|---|---|---|---|
|
||||
> | Гл.I Первоначальные химические понятия | 1–12 | `chemistry_7_ch1.html` | `chemistry-7-ch1` | emerald |
|
||||
> | Гл.II Кислород | 13–17 | `chemistry_7_ch2.html` | `chemistry-7-ch2` | sky/cyan |
|
||||
> | Гл.III Водород | 18–22 | `chemistry_7_ch3.html` | `chemistry-7-ch3` | violet |
|
||||
> | Гл.IV Вода | 23–26 | `chemistry_7_ch4.html` | `chemistry-7-ch4` | blue |
|
||||
>
|
||||
> Хаб: `chemistry_7_hub.html` / slug `chemistry-7` (родитель в каталоге).
|
||||
> Палитра hub — **emerald/green** (первые шаги в химии, природа, вода), не пересекается с
|
||||
> Химией 8 (amber) и Химией 9 (amber).
|
||||
|
||||
---
|
||||
|
||||
## 📗 ПОЛНАЯ КАРТА СОДЕРЖАНИЯ (26 §)
|
||||
|
||||
Колонка **«Интерактив»** — главный наглядный элемент сверх текста (минимум 1 «звёздный»
|
||||
виджет на §; полный набор — в стандарте ниже).
|
||||
|
||||
### ГЛАВА I. Первоначальные химические понятия (§§1–12) — *emerald*
|
||||
| § | Тема | Ключ | Интерактив (звёздный виджет) |
|
||||
|---|------|------|------------------------------|
|
||||
| §1 | Химия — наука о веществах | вещество vs тело; свойства веществ; наблюдение/опыт; лаб. оборудование, ТБ | **Виртуальная лаборатория-знакомство** (наведи на оборудование → название/назначение + правило ТБ) + DnD «тело / вещество» |
|
||||
| §2 | Чистые вещества и смеси | однородные/неоднородные смеси; способы разделения | **Симулятор разделения смесей** (фильтрование, выпаривание, отстаивание, дистилляция, магнит) + классификатор |
|
||||
| §3 | Атомы. Химические элементы | атом; химический элемент; символы элементов | **Тренажёр символов элементов** (название↔символ) + мини-ПСХЭ как каталог (`miniPeriodic`) |
|
||||
| §4 | Относительная атомная масса | `A_r`; а.е.м. = 1/12 массы атома C | **«Весы атомов»**: во сколько раз атом X тяжелее H/C; поиск `A_r` по элементу |
|
||||
| §5 | Молекулы. Простые вещества | молекула; простое вещество; атомность O₂, O₃, H₂, металлы | **3D-модели простых веществ** (biochem-core: H₂, O₂, O₃, N₂) + классификатор «атом/молекула/простое» |
|
||||
| §6 | Сложные вещества | атомы разных элементов: H₂O, CO₂, CH₄, NH₃ | **3D-модели сложных веществ** + DnD «простое / сложное» |
|
||||
| §7 | Химическая формула | индекс, коэффициент; качественный/количественный состав; чтение формул | **Парсер/конструктор формулы** (формула → какие атомы и сколько) + «читалка» (аш-два-о) |
|
||||
| §8 | Относительная молекулярная масса | `M_r = Σ A_r·index` | **Калькулятор `M_r`** (biochem-core) с пошаговым разбором + калькулятор массовой доли элемента `w(A)` (Прил.3) |
|
||||
| §9 | Валентность | валентность по водороду; H–I, O–II; составление формул по валентности | **Конструктор формул по валентности** (2 элемента + валентности → НОК индексов + проверка) + «черточки валентности» |
|
||||
| §10 | Явления физические и химические. Признаки химических реакций | признаки: цвет, осадок, газ, запах, тепло/свет | **Детектор признаков реакции** (малахит→CuO+H₂O+CO₂; CuSO₄+NaOH→синий осадок) `testTube` + DnD «физ./хим. явление» |
|
||||
| §11 | Закон сохранения массы. Химические уравнения | m(реагентов)=m(продуктов); Ломоносов/Лавуазье | **«Весы сохранения массы»** (анимация: реагенты ⇄ продукты, баланс) + знакомство с уравнением |
|
||||
| §12 | Составление уравнений химических реакций | подбор коэффициентов; баланс атомов | **Балансировщик уравнений** (`equationBalancer`: P+O₂→P₂O₅, Fe+O₂→Fe₃O₄, H₂+O₂→H₂O, CH₄+O₂→CO₂+H₂O) |
|
||||
|
||||
**Лаб. опыт 1** (после §10): признаки протекания химических реакций.
|
||||
**Практическая работа 1** (после §2): знакомство с химической лабораторией, разделение смесей.
|
||||
|
||||
### ГЛАВА II. Кислород (§§13–17) — *sky/cyan*
|
||||
| § | Тема | Ключ | Интерактив |
|
||||
|---|------|------|------------|
|
||||
| §13 | Воздух как смесь газов | состав воздуха: N₂ 78 %, O₂ 21 %, Ar, CO₂ | **Интерактивная диаграмма состава воздуха** (круговая, клик → доля газа) + связь со §2 (смесь) |
|
||||
| §14 | Кислород как химический элемент и простое вещество | O (элемент) vs O₂ (вещество); озон O₃; нахождение в природе; физ. свойства | **Переключатель «элемент ↔ простое вещество»** + 3D O₂/O₃ + «паспорт» кислорода |
|
||||
| §15 | Химические свойства кислорода | горение: C, S, P, Fe + O₂ → оксиды; окисление, медленное окисление | **Симулятор горения** (выбери вещество → реакция с O₂ → оксид + уравнение + пламя) `chemEq` |
|
||||
| §16 | Оксиды | оксид = Э + O (бинарное); названия; CuO, CO₂, SO₂, P₂O₅, Fe₃O₄, H₂O | **Конструктор оксида** (элемент + валентность → формула) + классификатор «оксид / не оксид» |
|
||||
| §17 | Получение кислорода | разложение KMnO₄, H₂O₂ (+кат. MnO₂); катализатор; реакция разложения | **Схема получения O₂** (разложение при нагреве; катализатор — ускоряет, не расходуется, анимация) |
|
||||
|
||||
**Лаб. опыт 2** (после §13): сборка простейших приборов для получения и собирания газов.
|
||||
**Практическая работа 2** (после §17): получение кислорода и изучение его свойств (тлеющая лучинка → вспыхивает).
|
||||
|
||||
### ГЛАВА III. Водород (§§18–22) — *violet*
|
||||
| § | Тема | Ключ | Интерактив |
|
||||
|---|------|------|------------|
|
||||
| §18 | Водород — химический элемент и простое вещество | H, H₂; самый лёгкий газ; нахождение; физ. свойства | **3D-модель H₂** + «паспорт» водорода + демонстрация «легче воздуха» |
|
||||
| §19 | Химические свойства водорода | H₂+O₂→H₂O (гремучий газ); H₂+CuO→Cu+H₂O (восстановление); восстановитель | **Симулятор реакций H₂** (восстановление оксида: чёрный CuO → красная Cu) `chemEq` |
|
||||
| §20 | Понятие о кислотах | кислота = H + кислотный остаток; HCl, H₂SO₄, HNO₃, H₂SO₃, H₂CO₃; индикаторы | **`indicatorScale`** (лакмус красный, метилоранж розовый в кислоте) + конструктор «кислота → остаток» |
|
||||
| §21 | Взаимодействие кислот с металлами | Me + кислота → соль + H₂↑; ряд активности (Прил.4); Cu/Ag не реагируют | **Интерактивный ряд активности** (`activitySeries`: K…H₂…Au, клик металл → реагирует/нет, пузырьки H₂) + Zn+HCl (`testTube`) |
|
||||
| §22 | Соли — продукты замещения H на металл | соль = Me + кислотный остаток; реакция замещения; хлориды/сульфаты/нитраты | **Конструктор солей** (металл + кислотный остаток → формула по валентности) + анимация замещения |
|
||||
|
||||
**Лаб. опыт 3** (после §20): действие кислот на индикаторы.
|
||||
**Лаб. опыт 4** (после §21): взаимодействие серной и соляной кислот с металлами.
|
||||
**Практическая работа 3** (после §22): получение водорода и изучение его свойств (Zn+HCl, «гремучий газ»).
|
||||
|
||||
### ГЛАВА IV. Вода (§§23–26) — *blue*
|
||||
| § | Тема | Ключ | Интерактив |
|
||||
|---|------|------|------------|
|
||||
| §23 | Состав, физические и химические свойства воды | H₂O; разложение эл. током → H₂+O₂ (2:1); Na+H₂O→щёлочь+H₂; оксид+H₂O→кислота/основание; круговорот | **Разложение воды** (анимация 2:1 H₂:O₂) + реакции воды (Na, CaO, CO₂) + круговорот воды (Прил.5) |
|
||||
| §24 | Основания как сложные вещества | основание = Me + OH; NaOH, KOH, Ca(OH)₂; щёлочи vs нерастворимые; индикаторы | **Конструктор оснований** Me(OH)ₙ + **`indicatorScale`** (фенолфталеин малиновый, лакмус синий) + классификатор |
|
||||
| §25 | Реакция нейтрализации | кислота + основание → соль + вода; HCl+NaOH→NaCl+H₂O; экзотермическая | **Анимация нейтрализации** (фенолфталеин малиновый → бесцветный; sim `titration`) `chemEq` |
|
||||
| §26 | Охрана окружающей среды | загрязнение воды/воздуха; кислотные дожди; очистка воды; бережное отношение | **Инфографика-исследование** (источники загрязнения, очистка воды — Прил.5) |
|
||||
|
||||
**Лаб. опыт 5** (после §24): действие щелочей на индикаторы.
|
||||
**Практическая работа 4** (после §25): реакция нейтрализации.
|
||||
|
||||
**Итого**: 26 §, 4 главы, **5 лаб. опытов** (§10, §13, §20, §21, §24), **4 практические работы** (§2, §17, §22, §25), 7 приложений.
|
||||
|
||||
---
|
||||
|
||||
## ⚗️ ХИМИЧЕСКИЙ СТАНДАРТ КАЧЕСТВА
|
||||
|
||||
### A. Движки и переиспользуемые активы (всё уже есть в проекте)
|
||||
|
||||
| Что нужно | Берём из | Файл / id |
|
||||
|-----------|----------|-----------|
|
||||
| Парсинг формул, `M_r`/`A_r`, состав вещества | biochem-core | `frontend/js/biochem-core.js` ✅ |
|
||||
| 2D/3D шаростержневые модели молекул | biochem-core | `frontend/js/biochem-core.js` ✅ |
|
||||
| Рендер формул/ионов/уравнений (`formula`, `ionLabel`, `chemEq`) | chem8_svg | `frontend/js/chem8_svg.js` (`window.Chem8`) ✅ |
|
||||
| Индикаторы, ряд активности, классификаторы, `testTube` | chem8_svg (виджеты разд. B Химии 8) | переиспользовать / доработать ✅ |
|
||||
| Интерактивная ПСХЭ (как каталог элементов) | sim `periodic` | реестр `_register-all.js` ✅ |
|
||||
| Песочница реакций | sim `chemsandbox` | реестр ✅ |
|
||||
| Титрование / нейтрализация | sim `titration` | реестр ✅ |
|
||||
| Качественный анализ / индикаторы | sim `qualanalysis` | реестр ✅ |
|
||||
|
||||
Монтаж sim: контейнер `<div id="sim-<id>"></div>` + `openSim('<id>')` (или прямой mount через
|
||||
`window.LabRegistry`), как на остальных страницах. Глоссарий — по образцу `chem8_glossary.js`.
|
||||
|
||||
### B. Хелпер `/js/chem7_svg.js` (тонкая надстройка над `chem8_svg.js`)
|
||||
|
||||
> **Рекомендация:** НЕ дублировать химические примитивы. `formula`, `ionLabel`, `chemEq`,
|
||||
> `indicatorScale`, `activitySeries`, `testTube`, `classifier` уже есть/планируются в
|
||||
> `chem8_svg.js` (`window.Chem8`). `chem7_svg.js` должен **переиспользовать** их (через
|
||||
> `window.Chem8.*`) и добавить только то, что специфично для 7 класса. План Химии 9 уже
|
||||
> рекомендует свести химические примитивы в общий `chem_svg.js` — при совместной разработке
|
||||
> довести до единого shared-модуля. Молекулы — **только через `biochem-core.js`**.
|
||||
|
||||
```js
|
||||
// 1. Конструктор формулы по валентности (звёздный виджет §9):
|
||||
// выбор 2 элементов + их валентностей → НОК → индексы → формула + структурная схема
|
||||
const valenceBuilder = (mount, {elements}) => { /* H-I, O-II, Cl-I, ...; НОК(v1,v2) */ };
|
||||
|
||||
// 2. Тренажёр символов элементов (§3): название ↔ символ (карточки/квикфайр)
|
||||
const elementSymbolDrill = (mount, {set}) => { /* H,O,C,N,Na,Cl,Fe,Cu,Ca,Al,Zn,S,P,Mg,K */ };
|
||||
|
||||
// 3. «Весы атомов» (§4): сравнение масс атомов в а.е.м., во сколько раз тяжелее
|
||||
const atomBalance = (mount, {a, b}) => { /* визуальные весы + A_r */ };
|
||||
|
||||
// 4. Разделитель смесей (§2): смесь → выбор метода (фильтр/выпаривание/магнит/дистилляция)
|
||||
const mixtureSeparator = (mount, {mixtures}) => { /* анимация разделения, проверка метода */ };
|
||||
|
||||
// 5. Детектор признаков реакции (§10): анимация опыта + чек-лист признаков (цвет/осадок/газ/запах/тепло)
|
||||
const reactionSigns = (mount, {demo}) => { /* малахит↘, CuSO4+NaOH↘синий, S+O2 запах */ };
|
||||
|
||||
// 6. «Весы сохранения массы» (§11): слева реагенты, справа продукты, стрелка баланса m=m
|
||||
const massConservation = (mount, {eq}) => { /* m(реаг)=m(прод), визуальные весы */ };
|
||||
|
||||
// 7. Балансировщик уравнений подбором (§12): матрица атомов, подсветка дисбаланса, без стехиометрии
|
||||
const equationBalancer = (mount, {skeleton}) => { /* коэффициенты, проверка баланса атомов */ };
|
||||
|
||||
// 8. Симулятор горения (§15): вещество (C/S/P/Fe) + O2 → оксид + пламя/искры + уравнение
|
||||
const combustionSim = (mount, {fuel}) => { /* SVG-пламя + chemEq продукта */ };
|
||||
|
||||
// 9. Конструктор оксида/соли/основания (§16,22,24): Me/Э + валентность/остаток → формула
|
||||
const compoundBuilder = (mount, {kind}) => { /* kind: 'oxide'|'salt'|'base'; по валентности */ };
|
||||
|
||||
// 10. Диаграмма состава воздуха (§13): интерактивная круговая (N2 78, O2 21, Ar, CO2)
|
||||
const airComposition = (mount) => { /* клик сектор → газ + доля */ };
|
||||
|
||||
// 11. Разложение воды (§23): анимация электролиза, объёмы H2:O2 = 2:1
|
||||
const waterDecomp = (mount) => { /* две пробирки над электродами, 2:1 */ };
|
||||
|
||||
// 12. Калькулятор массовой доли элемента (Прил.3, §8): w(A) = A_r·x / M_r, пошагово
|
||||
const massFraction = (mount, {formula}) => { /* w(O) в H3PO4 = 65,3 % */ };
|
||||
```
|
||||
|
||||
`indicatorScale`, `activitySeries`, `testTube`, `classifier`, `miniPeriodic` — **из `chem8_svg.js`**
|
||||
(если там пока заглушки — реализовать там же и переиспользовать в обоих учебниках).
|
||||
|
||||
### C. Правила рендера химии (обязательны с §1)
|
||||
|
||||
1. **Формулы веществ** — нижние индексы для атомов (`H₂O`, `CaCO₃`) через `Chem8.formula`/
|
||||
`chemEq`, не «сырой» текст. Зарядов ионов и степеней окисления в 7 классе НЕТ.
|
||||
2. **Уравнения реакций** — всегда сбалансированы; стрелки `=`/`→`, `↑` (газ), `↓` (осадок),
|
||||
условия над стрелкой (`t`, кат., эл. ток). Только реакции из программы 7 класса.
|
||||
3. **Валентность**, а не степень окисления: схемы «черточек» (H–Cl, H–O–H), составление формул
|
||||
по валентности (НОК индексов).
|
||||
4. **Признак реакции** — для каждой качественной/наглядной реакции показывать видимый признак:
|
||||
цвет осадка, пузырьки газа, изменение окраски индикатора, пламя (через `testTube`/`indicatorScale`/`combustionSim`).
|
||||
5. **Молекулярные модели** — структурная формула + 3D (biochem-core) для изучаемых веществ
|
||||
(H₂, O₂, O₃, N₂, H₂O, CO₂, CH₄, NH₃, HCl).
|
||||
6. **Цвета — химически достоверные**: осадок Cu(OH)₂ голубой, малахит зелёный→CuO чёрный,
|
||||
медь красная, сера жёлтая; индикаторы: лакмус (кислота — красный, щёлочь — синий),
|
||||
метилоранж (кислота — розовый), фенолфталеин (щёлочь — малиновый).
|
||||
7. **Безопасность** — где уместно (кислоты, щёлочи, Na+вода, гремучий газ) — заметка-«скрепка»;
|
||||
в практических работах — блок «Правила ТБ».
|
||||
8. **KaTeX-эскейпы** — в JS-шаблонах двойной backslash (`\\to`, `\\downarrow`, `\\uparrow`).
|
||||
9. **Drag/слайдеры** — `window`-listeners + `{passive:false}` + state ВЫШЕ `redraw()`
|
||||
(стандарт геометрии), `touch-action:none` на draggable SVG/canvas, **без `setPointerCapture`**.
|
||||
10. **Без эмоджи** — только inline SVG `.ic` (правило проекта [[feedback_no_emoji]]).
|
||||
11. **Простой язык** — это первый курс химии; определения короткие, на каждое понятие — наглядность.
|
||||
|
||||
### D. Типы интерактивов по темам 7 класса
|
||||
|
||||
| Тип темы | Интерактив |
|
||||
|----------|------------|
|
||||
| Вещества/смеси (§1,2,13) | `mixtureSeparator`, классификатор, `airComposition`, виртуальная лаборатория |
|
||||
| Атомы/элементы (§3,4) | `elementSymbolDrill`, `miniPeriodic`, `atomBalance` |
|
||||
| Молекулы/вещества (§5,6) | biochem-core 3D, классификатор «простое/сложное» |
|
||||
| Формулы/`M_r`/валентность (§7,8,9) | парсер формулы, калькулятор `M_r`, `valenceBuilder`, `massFraction` |
|
||||
| Реакции/уравнения (§10,11,12) | `reactionSigns`, `massConservation`, `equationBalancer` |
|
||||
| Кислород/горение/оксиды (§14,15,16,17) | `combustionSim`, `compoundBuilder('oxide')`, схема получения, sim `chemsandbox` |
|
||||
| Водород/кислоты/соли (§18,19,20,21,22) | 3D H₂, `indicatorScale`, `activitySeries`, `testTube`, `compoundBuilder('salt')` |
|
||||
| Вода/основания/нейтрализация (§23,24,25) | `waterDecomp`, `compoundBuilder('base')`, `indicatorScale`, sim `titration` |
|
||||
| Прикладное/экология (§26, Прил.) | инфографика-исследование, бытовые названия (Прил.6), единицы (Прил.7) |
|
||||
|
||||
---
|
||||
|
||||
## 📦 СТРУКТУРА КАЖДОГО § (стандарт наполнения)
|
||||
|
||||
**Теория (3 карточки):**
|
||||
- `theory` — основное определение/понятие + наглядная SVG/модель
|
||||
- `rule` — ключевое правило/закономерность/формула (рамка)
|
||||
- `example` — разобранный пример (реакция / составление формулы / опыт) с пошаговым рендером
|
||||
- (для прикладных §) `apply` — применение/значение (инфографика)
|
||||
|
||||
**Интерактивы (3–5 на §):**
|
||||
1. **Звёздный виджет** темы (из карты содержания)
|
||||
2. **Конструктор/симулятор** (drag / slider / sim из реестра / biochem-core 3D)
|
||||
3. **DnD-классификатор** (вещество/смесь, простое/сложное, физ./хим. явление, оксид/кислота/соль/основание)
|
||||
4. **Тренажёр** — 5 задач с inline-наглядностью (формула / уравнение / модель / признак в условии)
|
||||
5. **Босс §** — 3–4 интеграционные задачи (+5 XP каждая)
|
||||
|
||||
**Дополнительно:** пополнение глоссария (термины §, `[[ссылки]]`), «Вопросы и задания» из
|
||||
учебника (адаптированные, с проверкой), проходящий jsdom-тест страницы.
|
||||
|
||||
**Финал главы:** итоговая шпаргалка (mini-cards), карта связей (SVG-граф понятий главы),
|
||||
5–7 интегрированных боссов (+10 XP), achievement «Мастер главы N» (+50 XP, confetti),
|
||||
кнопка перехода к следующей главе.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 ПОРЯДОК РЕАЛИЗАЦИИ (по фазам)
|
||||
|
||||
### Phase 0 — Фундамент (hub + каркасы глав)
|
||||
- **`chemistry_7_hub.html`** — хаб-каталог 4 глав по образцу `chemistry_8_hub.html`: палитра
|
||||
**emerald** (`--pri:#059669`/`#047857`, `--pri-soft:#d1fae5`; header gradient
|
||||
`linear-gradient(110deg,#065f46 0%,#059669 55%,#6ee7b7 100%)`), водяной знак «ХИМИЯ»,
|
||||
карточки глав с прогрессом (грузятся из `/api/textbooks/chemistry-7/children`),
|
||||
блок «Финал курса» (шпаргалка + боссы — наполняется в Phase 5), achievement-strip
|
||||
«Химик 7 класса», тема (localStorage `chemistry7_theme`), KaTeX CDN, `/js/api.js`+`/js/xp.js`.
|
||||
- **4 файла глав** `chemistry_7_ch1..ch4.html` — на Phase 0 валидные каркасы-заглушки
|
||||
(header с водяным знаком, hero, sidebar-оглавление §, контейнер параграфов, XP/tracker-интеграция),
|
||||
наполнение § — в Phase 1–4. Подключить `?v=YYYYMMDD` (cache-busting) на все JS;
|
||||
sidebar-фикс `@media(min-width:981px){#sidebar-btn{display:none}}`.
|
||||
- **`/js/chem7_svg.js`** (хелперы B — заглушки → реализация по фазам); подключить
|
||||
`chem8_svg.js`, `biochem-core.js` и нужные симуляторы на страницах глав; `chem7_glossary.js`.
|
||||
- **Миграция `046_chemistry7_hub.sql`** (следующий номер после `045_bio_pathways.sql`):
|
||||
**INSERT** родителя `chemistry-7` (`html_path='chemistry_7_hub.html'`, `para_count=26`,
|
||||
`color='emerald'`, `parent_slug=NULL`, `subject='chemistry'`, `grade=7`) + **4 детей**
|
||||
`chemistry-7-ch1..ch4` (`parent_slug='chemistry-7'`, свои `html_path`/`para_count`/`color`/`sort_order`)
|
||||
— по образцу `041_chemistry8_hub.sql`. Применить `npm run migrate` + рестарт dev-сервера.
|
||||
- jsdom-тест-каркас: хаб строится, все 5 файлов парсятся, ссылки глав ведут на существующие slug.
|
||||
|
||||
### Phase 1 — Глава I «Первоначальные химические понятия» (§§1–12) + ЛО1 + ПР1
|
||||
Самая фундаментальная: закладываем движки, от которых зависит весь курс — `elementSymbolDrill`,
|
||||
парсер формулы + калькулятор `M_r` (biochem-core), `valenceBuilder`, `equationBalancer`,
|
||||
`reactionSigns`, `massConservation`, `mixtureSeparator`. По 2–3 § за волну.
|
||||
|
||||
### Phase 2 — Глава II «Кислород» (§§13–17) + ЛО2 + ПР2
|
||||
`airComposition`, `combustionSim`, `compoundBuilder('oxide')`, схема получения O₂ (катализатор),
|
||||
sim `chemsandbox`. Первые реакции горения и понятие оксида.
|
||||
|
||||
### Phase 3 — Глава III «Водород» (§§18–22) + ЛО3 + ЛО4 + ПР3
|
||||
3D H₂, восстановление CuO, `indicatorScale` (лакмус/метилоранж), `activitySeries` (ряд активности,
|
||||
Прил.4), `testTube` (Zn+HCl, пузырьки H₂), `compoundBuilder('salt')`. Понятия «кислота» и «соль».
|
||||
|
||||
### Phase 4 — Глава IV «Вода» (§§23–26) + ЛО5 + ПР4
|
||||
`waterDecomp` (разложение 2:1), реакции воды, `compoundBuilder('base')`, `indicatorScale`
|
||||
(фенолфталеин), нейтрализация (sim `titration`), экология/круговорот воды (Прил.5).
|
||||
|
||||
### Phase 5 — Финалы глав + общий финал учебника
|
||||
Шпаргалки и карты связей по каждой главе; интегрированные боссы + achievements;
|
||||
**большой финал**: «химический паспорт вещества» (тело/вещество → состав → формула → реакции),
|
||||
итоговый босс-квест, ачивка «Химик 7 класса»; глоссарий собран и связан `[[ссылками]]`;
|
||||
страница приложений-справочников (Прил.3,4,6,7 как интерактивные виджеты).
|
||||
|
||||
### Phase 6 — Качество и админка
|
||||
Полный прогон jsdom-тестов (каждый § — builder не stub); аудит баланса всех уравнений и
|
||||
KaTeX/`chemEq`-эскейпов; синхронизация с админкой (если новые sim в `lab.html` →
|
||||
обновить `ADMIN_SIMS` в `admin.html` — [[feedback_sims_admin_sync]]); проверка доступа
|
||||
по классам/ученикам ([[project_content_access]], `/api/access`); появление в каталоге
|
||||
`textbooks.html` в секции 7 класса (рядом с Физикой 7, Алгеброй 7, Геометрией 7).
|
||||
|
||||
> Рекомендуемый темп: внутри фазы — по 2–3 § за «волну», каждая волна = commit +
|
||||
> проходящий jsdom-тест (правило CLAUDE.md: commit изменённых файлов + push сразу).
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ ИНТЕГРАЦИЯ С ПРОЕКТОМ
|
||||
|
||||
| Точка | Действие |
|
||||
|-------|----------|
|
||||
| **БД каталог** | `chemistry-7` в `textbooks` **отсутствует** → миграция `046_chemistry7_hub.sql`: INSERT родитель + 4 ребёнка (образец — `041_chemistry8_hub.sql`). Каталог `/api/textbooks` показывает только `parent_slug IS NULL`; хаб тянет детей через `/api/textbooks/chemistry-7/children`. |
|
||||
| **Прогресс/XP** | Автоматически: `textbook-xp-widget.js` (+5 XP/§), `textbook-tracker.js`, `LS.xp`. Доп. XP за боссов — по образцу `phys7_ch1_widgets.js`. localStorage-ключи прогресса — `chem7-*`. |
|
||||
| **Симуляторы** | Реестр `frontend/js/labs/_register-all.js`. Нужные химические sim уже зарегистрированы: `periodic`, `chemsandbox`, `titration`, `qualanalysis`. |
|
||||
| **Молекулы** | `biochem-core.js` (парсинг, `M_r`, 2D/3D-модели). |
|
||||
| **Хим. примитивы** | `chem8_svg.js` (`window.Chem8`: `formula`, `ionLabel`, `chemEq` + виджеты) — переиспользовать; `chem7_svg.js` — тонкая надстройка (валентность, горение, смеси, весы массы). |
|
||||
| **Бэкенд** | Роуты готовы: `backend/src/routes/textbooks.js` (catalog/children/progress/bookmarks). Доступ: `backend/src/services/contentAccess.js`. |
|
||||
| **Глоссарий** | `chem7_glossary.js` по образцу `chem8_glossary.js` (всплывающие определения терминов). |
|
||||
| **Тесты** | `cd backend && npm test` (jsdom). На каждый § — тест: страница строится, builder не stub, уравнения сбалансированы. Учитывать 3 pre-existing baseline-фейла (`BASELINE_FAILS=3`). |
|
||||
| **Админка** | Новые sim в `lab.html` → синхронно `ADMIN_SIMS` в `admin.html`. |
|
||||
| **Каталог-страница** | `frontend/textbooks.html` — карточка появляется автоматически из каталога (секция 7 класса). |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ КРИТИЧЕСКИЕ ПРАВИЛА
|
||||
|
||||
### ❌ НЕ делать
|
||||
- Контент 8 класса: моль, молярная масса/объём, расчёты по уравнениям, периодический закон,
|
||||
строение атома, химическая связь, ТЭД, ионные уравнения, степень окисления — **в 7 классе НЕТ**.
|
||||
- «Сырые» формулы текстом — только `Chem8.formula`/`chemEq`/KaTeX.
|
||||
- Несбалансированные уравнения (аудит баланса перед commit). Реакции — только из программы 7 кл.
|
||||
- Дублировать молекулярный движок или химические примитивы — переиспользовать `biochem-core.js` и `chem8_svg.js`.
|
||||
- `setPointerCapture` (теряется после `innerHTML`-replace) → `window`-listeners + state-flag.
|
||||
- `\to`, `\uparrow`, `\downarrow` без удвоения backslash в JS-шаблонах.
|
||||
- Эмодзи — запрещены; только inline SVG `.ic` ([[feedback_no_emoji]]).
|
||||
- Авторов учебника в hub/footer — НЕ упоминаем.
|
||||
- **Grep tool — запрещён**; поиск только `ast-index` (правила проекта, [[reference_sqlite_node]]).
|
||||
|
||||
### ✅ Обязательно
|
||||
- Каждый commit → jsdom-тест 100 % pass (сверх baseline). Push сразу после коммита, файлы поимённо.
|
||||
- Аудит баланса уравнений + KaTeX-эскейпов после каждой волны.
|
||||
- Каждая наглядная/качественная реакция = уравнение **+ видимый признак** (цвет/газ/осадок/индикатор/пламя).
|
||||
- Цвета осадков/индикаторов/пламени — химически достоверные.
|
||||
- Все builder-функции в конце финальной волны главы — НЕ stub'ы.
|
||||
- Простой, дружелюбный язык первого курса; на каждое понятие — наглядность.
|
||||
- Перед работой по ветке `feature/lab-content-engine` — `git fetch` (параллельные сессии,
|
||||
[[project_concurrent_sessions_branch]]).
|
||||
|
||||
---
|
||||
|
||||
## 📊 Оценка объёма
|
||||
|
||||
| Глава | § | Лаб/ПР | Ожидаемый LOC |
|
||||
|--------|---|--------|---------------|
|
||||
| Гл.I Первоначальные понятия | 12 | ЛО1 + ПР1 | ~10 000 (+`valenceBuilder`, `equationBalancer`, `reactionSigns`, `massConservation`, `mixtureSeparator`) |
|
||||
| Гл.II Кислород | 5 | ЛО2 + ПР2 | ~5 000 (+`combustionSim`, `compoundBuilder('oxide')`, `airComposition`) |
|
||||
| Гл.III Водород | 5 | ЛО3,4 + ПР3 | ~6 000 (+`indicatorScale`, `activitySeries`, `testTube`, `compoundBuilder('salt')`) |
|
||||
| Гл.IV Вода | 4 | ЛО5 + ПР4 | ~5 000 (+`waterDecomp`, `compoundBuilder('base')`, sim `titration`) |
|
||||
| Финалы глав + общий финал + приложения | — | — | ~4 000 |
|
||||
| `/js/chem7_svg.js` хелперы | — | — | ~2 500 |
|
||||
| Хаб + 4 каркаса глав (Phase 0) | — | — | ~2 500 |
|
||||
| **Итого** | **26** | **5 ЛО + 4 ПР** | **~35 000 LOC** |
|
||||
|
||||
Меньше Химии 8 (~64 000) — курс короче (26 § против 52) и качественный (без тяжёлых
|
||||
количественных движков). По плотности интерактива — на уровне Химии 8 / Физики 7.
|
||||
|
||||
---
|
||||
|
||||
## 🏆 Достижения и XP (оценка)
|
||||
|
||||
| Slug | Название | Условие | XP |
|
||||
|------|----------|---------|-----|
|
||||
| `chemistry7_ch1_master` | Первооткрыватель | Все боссы финала главы I | 50 |
|
||||
| `chemistry7_ch2_master` | Повелитель кислорода | Все боссы финала главы II | 50 |
|
||||
| `chemistry7_ch3_master` | Знаток водорода | Все боссы финала главы III | 50 |
|
||||
| `chemistry7_ch4_master` | Хранитель воды | Все боссы финала главы IV | 50 |
|
||||
| `chemistry7_course_master` | Химик 7 класса | Все 4 ачивки глав + финальный босс-квест | 150 |
|
||||
|
||||
XP-оценка полного прохождения: 26 § × ~50 XP + 4 ач × 50 + финал 150 ≈ **1 700 XP**.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Чем Химия 7 отличается от Химии 8
|
||||
|
||||
| Аспект | Химия 7 (первый курс) | Химия 8 |
|
||||
|--------|----------------------|---------|
|
||||
| Количество вещества (моль) | **Нет** | Есть (вводный раздел) |
|
||||
| `M_r` / `M` | `M_r` как «во сколько раз тяжелее» (без моля) | Молярная масса `M`, расчёты |
|
||||
| Состав по формуле | Качественный/количественный; массовая доля элемента (Прил.3) | + расчёты по уравнениям |
|
||||
| Валентность vs степень окисления | **Валентность** | Степень окисления, ОВР |
|
||||
| Классы соединений | Знакомство по ходу курса (оксиды, кислоты, соли, основания) | Системно, единой главой + генетическая связь |
|
||||
| Периодический закон, строение атома, связь | **Нет** | Есть (главы 2–4) |
|
||||
| ТЭД, ионные уравнения, растворы (`w`,`c`) | **Нет** | Есть (глава 6) |
|
||||
| Кислоты/основания | Понятие, индикаторы, реакции с Me, нейтрализация | + классификация, свойства, получение |
|
||||
| Главный визуал курса | Балансировщик уравнений + ряд активности + индикаторы | Количественные расчёты + ПСХЭ |
|
||||
|
||||
Курсы дополняют друг друга: Химия 7 закладывает язык химии (вещество, формула, валентность,
|
||||
уравнение) и первое знакомство с веществами; Химия 8 переводит это в количественную плоскость.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Связанные планы
|
||||
- [PLAN_CHEMISTRY_8.md](../textbooks-8/PLAN_CHEMISTRY_8.md) — образец архитектуры (hub + главы), химический стандарт качества, `chem8_svg.js`, миграция-шаблон.
|
||||
- [PLAN_CHEMISTRY_9.md](../textbooks-9/PLAN_CHEMISTRY_9.md) — химический стандарт, рекомендация о shared `chem_svg.js`.
|
||||
- [PLAN_PHYSICS_7.md](PLAN_PHYSICS_7.md) — образец полного курса 7 класса (фазы, ачивки, финал курса, политика «без авторов», cache-busting, sidebar-фикс).
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Запуск
|
||||
|
||||
**Phase 0**: `chemistry_7_hub.html` (по образцу `chemistry_8_hub.html`, палитра emerald) +
|
||||
4 каркаса глав (`chemistry_7_ch1..ch4.html`) + `/js/chem7_svg.js` (скелет) + подключение
|
||||
`chem8_svg.js`/`biochem-core.js`/симуляторов + миграция `046_chemistry7_hub.sql` (родитель + 4 ребёнка)
|
||||
+ `npm run migrate` + рестарт + jsdom-каркас.
|
||||
**Phase 1**: Глава I (§§1–12) — закладываем движки (`valenceBuilder`, `equationBalancer`,
|
||||
парсер формулы, `M_r`, `reactionSigns`), от которых зависят все главы.
|
||||
|
||||
Дальше — последовательно по главам (Phase 2 → 4), затем финалы (Phase 5) и качество (Phase 6).
|
||||
|
||||
После завершения **Химия 7 → первый учебник, полностью покрывающий химию с нуля** для самого
|
||||
младшего класса в линейке (7→8→9), закрывает «нижнюю» ступень химии.
|
||||
@@ -0,0 +1,363 @@
|
||||
# План реализации: Химия 9 (Беларусь) — интерактивный наглядный учебник
|
||||
|
||||
> Цель: превратить существующий «текстовый» каркас `chemistry_9.html` в **максимально
|
||||
> полный, интерактивный и наглядный** учебник по всей программе 9 класса —
|
||||
> на уровне качества уже сделанных Физики 9 / Алгебры 9 / Геометрии 9, но с
|
||||
> химической спецификой (молекулы, уравнения реакций, ионы, осадки, газы, симуляторы).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Источник
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Книга | `Himiya_Shimanovich_9kl_rus_2025.pdf` |
|
||||
| Авторы | Шиманович И. Е., Василевская Е. И., Зураев А. В., Красицкий В. А., Сечко О. И. |
|
||||
| Под ред. | И. Е. Шимановича |
|
||||
| Изд. | Минск, «Адукацыя і выхаванне», 2025 (2-е изд., переработанное), 288 с. |
|
||||
| ISBN | 978-985-34-0189-9 |
|
||||
| Структура | **4 главы, 53 §, 6 лабораторных опытов, 4 практические работы** |
|
||||
| Справочные таблицы | ПСХЭ Менделеева, таблица растворимости, ряд активности металлов (форзацы) |
|
||||
|
||||
PDF лежит в `G:\Dev\Тесты\Методички\тест_6 класс\Книги\` (есть и издание 2019 г.).
|
||||
|
||||
---
|
||||
|
||||
## 📗 ПОЛНАЯ КАРТА СОДЕРЖАНИЯ (53 §)
|
||||
|
||||
Колонка **«Интерактив»** — главный наглядный элемент, который страница ДОЛЖНА давать
|
||||
сверх текста (минимум 1 «звёздный» виджет на §; полный набор виджетов — ниже в стандарте).
|
||||
|
||||
### ГЛАВА 1. Повторение курса 8 класса (§§1–3) — *amber*
|
||||
| § | Тема | Ключ | Интерактив (звёздный виджет) |
|
||||
|---|------|------|------------------------------|
|
||||
| §1 | Основные классы неорганических веществ | оксиды/кислоты/основания/соли, генетическая связь | **Классификатор**: drag формулы → класс; граф генетических переходов |
|
||||
| §2 | Строение атома и периодический закон | $Z$, $p/n/e$, $1s^22s^2…$, периодичность | **Интерактивная ПСХЭ** (`periodic`) + конструктор электронной конфигурации |
|
||||
| §3 | Химическая связь | ионная / ковалентная (полярн./неполярн.) / металлическая, ЭО | **Визуализатор связи**: slider ЭО → тип связи; диполь, электронные пары |
|
||||
|
||||
### ГЛАВА 2. Растворы. Теория электролитической диссоциации (§§4–11) — *blue*
|
||||
| § | Тема | Ключ | Интерактив |
|
||||
|---|------|------|------------|
|
||||
| §4 | Растворы как смеси веществ | раствор = растворитель + растворённое в-во | Анимация растворения (частицы) |
|
||||
| §5 | Качественные характеристики состава растворов | насыщ./ненасыщ./пересыщ., растворимость, кривые растворимости | **График растворимости** $s=f(t)$ с подвижной точкой |
|
||||
| §6 | Количественные характеристики. Массовая доля | $w=\dfrac{m_{в-ва}}{m_{р-ра}}$, $c=\dfrac{n}{V}$ | **Калькулятор раствора** (w, c, разбавление, смешение) |
|
||||
| §7 | Электролитическая диссоциация в растворах | электролиты/неэлектролиты, гидратация ионов | **Анимация диссоциации** NaCl/HCl в воде |
|
||||
| §8 | Диссоциация кислот, оснований и солей | $HCl\to H^++Cl^-$, ступенчатая дисс. | Конструктор уравнений диссоциации |
|
||||
| §9 | Реакции ионного обмена | условия необратимости: ↓ ↑ H₂O | **Предсказатель РИО** + конвертер молек.↔полн.ион.↔сокр.ион. |
|
||||
| §10 | Свойства кислот/оснований/солей с точки зрения ТЭД | ионные сущности реакций | Матрица реакций «реагент × реагент» |
|
||||
| §11 | Вода и растворы в жизни человека | значение воды, растворы в быту | Инфографика-исследование |
|
||||
|
||||
**Практическая работа 1** (после §6): приготовление раствора с заданной $w$.
|
||||
**Практическая работа 2** (после §9): реакции ионного обмена.
|
||||
|
||||
### ГЛАВА 3. Неметаллы (§§12–39) — *cyan / teal*
|
||||
| § | Тема | Ключ | Интерактив |
|
||||
|---|------|------|------------|
|
||||
| §12 | Общая характеристика неметаллов | положение в ПСХЭ, ЭО, окислители | Подсветка неметаллов в ПСХЭ |
|
||||
| §13 | Простые вещества неметаллы | аллотропия (O₂/O₃, алмаз/графит/фуллерен) | **3D-модели аллотропов** (biochem 3D) |
|
||||
| §14 | Водород | получение, св-ва, $H_2+O_2$ | Сборка прибора получения H₂ |
|
||||
| §15 | Галогены | $F,Cl,Br,I$, окислит. способность ↓ | **Ряд активности галогенов** + вытеснение |
|
||||
| §16 | Хлороводород. Соляная кислота | $HCl$, свойства | Диссоциация + реакции HCl |
|
||||
| §17 | Хлориды. Применение HCl и хлоридов | качеств. реакция на $Cl^-$ | **Качественная реакция** AgCl↓ (анимация осадка) |
|
||||
| §18 | Кислород — элемент и простое вещество | $O_2$, окисление, горение | Симулятор горения |
|
||||
| §19 | Кислород в природе. Получение и применение | круговорот, способы получения | Инфографика круговорота |
|
||||
| §20 | Сера — элемент и простое вещество | аллотропия серы, $S+Me$ | 3D-модель S₈ |
|
||||
| §21 | Сероводород | $H_2S$, св-ва, сульфиды | Реакции H₂S |
|
||||
| §22 | Оксид серы(IV) и оксид серы(VI) | $SO_2, SO_3$, кислотные оксиды | Кислотные дожди (инфографика) |
|
||||
| §23 | Серная кислота | $H_2SO_4$ конц./разб., безопасность | **Анимация разбавления** (кислоту в воду!) |
|
||||
| §24 | Реакции получения серной кислоты. Практический выход | контактный способ, $\eta$ | Калькулятор практического выхода |
|
||||
| §25 | Сульфаты. Применение | качеств. реакция на $SO_4^{2-}$ | **Качественная реакция** BaSO₄↓ |
|
||||
| §26 | Азот — элемент и простое вещество | $N_2$, тройная связь | 3D-модель N₂ |
|
||||
| §27 | Аммиак | $NH_3$, донорно-акц. связь | **Фонтан аммиака** (анимация) |
|
||||
| §28 | Соли аммония | $NH_4^+$, разложение | Качеств. реакция на $NH_4^+$ |
|
||||
| §29 | Азотная кислота | $HNO_3$, особые окислит. св-ва | Реакции с металлами (без H₂) |
|
||||
| §30 | Нитраты. Применение HNO₃ и нитратов | разложение нитратов, селитры | Карта разложения нитратов |
|
||||
| §31 | Фосфор — элемент и простое вещество | аллотропия (бел./красн.) | 3D-модель P₄ |
|
||||
| §32 | Кислородсодержащие соединения фосфора | $P_2O_5, H_3PO_4$, фосфаты | Ступенчатая диссоциация H₃PO₄ |
|
||||
| §33 | Минеральные удобрения | N/P/K, аммофос, суперфосфат | **NPK-калькулятор** удобрений |
|
||||
| §34 | Углерод — элемент и простое вещество | аллотропия (алмаз/графит/фуллерен), адсорбция | **3D-модели** + адсорбция |
|
||||
| §35 | Оксиды углерода | $CO$ (яд), $CO_2$ | Свойства CO/CO₂ |
|
||||
| §36 | Угольная кислота и её соли | $H_2CO_3$, карбонаты/гидрокарбонаты, жёсткость | **Качеств. реакция** на $CO_3^{2-}$ (вскипание) + сталактиты |
|
||||
| §37 | Кремний — элемент и простое вещество | $Si$, полупроводник | 3D-решётка (как алмаз) |
|
||||
| §38 | Оксид кремния(IV). Кремниевая кислота | $SiO_2$, силикаты | Структура SiO₂ |
|
||||
| §39 | Строительные материалы | стекло, керамика, цемент, бетон | Конструктор «варки стекла» |
|
||||
|
||||
**Лаб. опыты**: 1 — на $Cl^-$ (после §17), 2 — на $SO_4^{2-}$ (после §25), 3 — на $NH_4^+$ (после §28), 4 — на $CO_3^{2-}$ (после §36).
|
||||
**Практическая работа 3** (после §39): решение экспериментальных задач «Неметаллы».
|
||||
|
||||
### ГЛАВА 4. Металлы (§§40–53) — *deep-orange / rose*
|
||||
| § | Тема | Ключ | Интерактив |
|
||||
|---|------|------|------------|
|
||||
| §40 | Металлы. Общая характеристика элементов | положение в ПСХЭ, $Me^0-ne^-\to Me^{n+}$ | Подсветка металлов в ПСХЭ |
|
||||
| §41 | Простые вещества металлы. Физические свойства. Сплавы | металлич. связь, «электронный газ», сплавы | **Модель металлич. связи** + конструктор сплавов |
|
||||
| §42 | Общие химические свойства металлов | ряд активности, $Me$+кислота/соль/вода | **Интерактивный ряд активности** (предсказание реакции) |
|
||||
| §43 | Щелочные металлы | $Li,Na,K…$, реакция с водой | Анимация $Na+H_2O$ |
|
||||
| §44 | Важнейшие соединения натрия и калия | $NaOH, Na_2CO_3$, применение | Применение (инфографика) |
|
||||
| §45 | Электролиз расплавов солей | катод/анод, $2NaCl\to2Na+Cl_2$ | **Симулятор электролиза** (`electrolysis`) |
|
||||
| §46 | Магний и щёлочноземельные металлы | $Mg,Ca,Sr,Ba$, цвета пламени | **Цвета пламени** (интерактив) |
|
||||
| §47 | Важнейшие соединения магния и кальция | $CaO, Ca(OH)_2, CaCO_3$, гипс | Превращения Ca-соединений |
|
||||
| §48 | Качественное обнаружение ионов Ca²⁺/Ba²⁺. Жёсткость воды | $Ba^{2+}+SO_4^{2-}$, жёсткость, умягчение | **Симулятор жёсткости воды** + устранение |
|
||||
| §49 | Алюминий | $Al$, амфотерность, алюмотермия | Реакции Al (амфотерность) |
|
||||
| §50 | Оксид и гидроксид алюминия. Соли алюминия | $Al_2O_3, Al(OH)_3$ амфотерны | **Амфотерность** (реакция и с кислотой, и со щёлочью) |
|
||||
| §51 | Железо. Физические и химические свойства. Коррозия | $Fe$, $Fe^{2+}/Fe^{3+}$, ржавление | **Симулятор коррозии** |
|
||||
| §52 | Важнейшие соединения железа. Качественные реакции на ионы железа | $FeO/Fe_2O_3$, $Fe^{2+}/Fe^{3+}$ обнаружение | **Качеств. реакции** на $Fe^{2+}, Fe^{3+}$ |
|
||||
| §53 | Получение железа. Применение | доменная печь, чугун/сталь | **Анимация доменной печи** |
|
||||
|
||||
**Лаб. опыт 5** (после §48): качеств. реакции на $Ca^{2+}/Ba^{2+}$.
|
||||
**Лаб. опыт 6** (после §52): получение и окисление $Fe(OH)_2$.
|
||||
**Практическая работа 4** (после §52): решение экспериментальных задач «Металлы».
|
||||
|
||||
**Итого**: 53 §, 4 главы, 6 лаб. опытов, 4 практические работы.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 ТЕКУЩЕЕ СОСТОЯНИЕ `chemistry_9.html` (gap-анализ)
|
||||
|
||||
Файл уже существует: `frontend/textbooks/chemistry_9.html` (~860 КБ, 10 659 строк).
|
||||
|
||||
**Что есть ✅**
|
||||
- Скелет страницы по образцу физики: header, para-selector (карточки секций), табы, sidebar
|
||||
- Все 53 § как `data-para="p1"..."p53"` (структура совпадает с реальной книгой)
|
||||
- Amber-палитра + dark mode, адаптив
|
||||
- ~58 маркеров `boss/quiz/тренаж` — есть зачатки тренажёров
|
||||
- Автоначисление XP через `/js/textbook-xp-widget.js` (+5 XP за первый клик по §)
|
||||
- Прогресс/закладки через `/js/textbook-tracker.js` + БД (`textbooks`, `textbook_progress`, `textbook_bookmarks`)
|
||||
|
||||
**Чего НЕТ ❌ (главный объём работы)**
|
||||
- **Наглядность**: всего **12 `<svg>` на весь файл**, **0 `<canvas>`**, **0 симуляторов** (`openSim` не вызывается ни разу)
|
||||
- **0 молекулярных моделей** (хотя `biochem-core.js` умеет 2D/3D шаростержневые)
|
||||
- **0 рендера уравнений реакций** (нет стрелок реакций `→ ⇌ ↑ ↓ →[t]`, коэффициентов, состояний)
|
||||
- **0 глоссария** (в других учебниках есть)
|
||||
- Таблица растворимости и ряд активности — **только картинки на форзаце, не интерактивны**
|
||||
- Заголовок/водяной знак содержат артефакты от копирования шаблона «МЕТАЛЛЫ»; title `«§1–60»` — **устарел** (реально 53 §)
|
||||
- Секционная группировка карточек (`psel-c1..c6`) не выровнена на 4 главы книги
|
||||
|
||||
**Вывод**: каркас переиспользуем, но содержательно страница «текстовая». План = наполнить
|
||||
её химическим интерактивом и наглядностью + исправить метаданные.
|
||||
|
||||
---
|
||||
|
||||
## ⚗️ ХИМИЧЕСКИЙ СТАНДАРТ КАЧЕСТВА
|
||||
|
||||
### A. Движки и переиспользуемые активы
|
||||
|
||||
| Что нужно | Берём из | Файл |
|
||||
|-----------|----------|------|
|
||||
| Парсинг формул, молярная масса, формула Хилла | biochem-core | `frontend/js/biochem-core.js` |
|
||||
| 2D/3D шаростержневые модели, VSEPR-геометрия | biochem-core | `frontend/js/biochem-core.js` |
|
||||
| Интерактивная ПСХЭ | sim `periodic` | реестр `frontend/js/labs/` |
|
||||
| Электролиз | sim `electrolysis` | реестр |
|
||||
| Титрование | sim `titration` | реестр |
|
||||
| Качественный анализ | sim `qualanalysis` | реестр |
|
||||
| Растворы/концентрации | sim `solutions` | реестр |
|
||||
| Стехиометрия | sim `stoichiometry` | реестр |
|
||||
| Песочница реакций | sim `chemsandbox` | реестр |
|
||||
|
||||
Симуляторы монтируются как в других страницах: контейнер `<div id="sim-<id>"></div>` +
|
||||
вызов `openSim('<id>')` (или прямой mount через `window.LabRegistry`).
|
||||
|
||||
### B. Новый общий хелпер `/js/chem9_svg.js` (по образцу `geom7_svg.js`, `alg10_svg.js`)
|
||||
|
||||
Чисто химические примитивы, которых нет в biochem-core:
|
||||
|
||||
```js
|
||||
// 1. Рендер уравнения реакции: коэффициенты, состояния, стрелки, условия над стрелкой
|
||||
// chemEq('2Na + 2H2O -> 2NaOH + H2^', {cond:'', arrow:'->'}) → HTML с верхн./нижн. индексами, ↑↓, ⇌, →[t°]
|
||||
const chemEq = (src, opts={}) => { /* токенизация формул, KaTeX-подобный рендер */ };
|
||||
|
||||
// 2. Ион с зарядом: ionLabel('SO4', -2) → 'SO₄²⁻'
|
||||
const ionLabel = (formula, charge) => { /* нижние индексы + надстрочный заряд */ };
|
||||
|
||||
// 3. Пробирка с осадком / газом / окраской (SVG-анимация)
|
||||
const testTube = ({fill, precipitate, gas, color, label}) => { /* svg */ };
|
||||
|
||||
// 4. Орбитальная диаграмма (стрелки в клетках): orbitalDiagram('1s2 2s2 2p6 3s2 3p2')
|
||||
const orbitalDiagram = (config) => { /* svg клетки + ↑↓ */ };
|
||||
|
||||
// 5. Интерактивная таблица растворимости (виджет с подсветкой пары катион×анион)
|
||||
const solubilityTable = (mount, {highlight}) => { /* P/Н/М/— из форзаца */ };
|
||||
|
||||
// 6. Интерактивный ряд активности металлов (drag/клик → предсказание реакции)
|
||||
const activitySeries = (mount, opts) => { /* K Ca Na Mg Al Zn Fe ... Au */ };
|
||||
|
||||
// 7. Мини-ПСХЭ с подсветкой (элемент/группа/период/семейство)
|
||||
const miniPeriodic = (mount, {highlight, onClick}) => { /* 118 элементов */ };
|
||||
|
||||
// 8. Индикатор + шкала pH (лакмус/фенолфталеин/метилоранж)
|
||||
const indicatorScale = (mount, {ph, indicator}) => { /* цвет полоски */ };
|
||||
|
||||
// 9. Анимация диссоциации/растворения (частицы ионов разлетаются)
|
||||
const dissociationAnim = (mount, {salt}) => { /* canvas/SVG-частицы */ };
|
||||
```
|
||||
|
||||
Молекулы (структурные/шаростержневые/3D) — **через `biochem-core.js`**, не дублировать.
|
||||
|
||||
### C. Правила рендера химии (обязательны с §1)
|
||||
|
||||
1. **Формулы веществ** — нижние индексы для атомов ($H_2O$, $CaCO_3$), верхние для зарядов ионов ($SO_4^{2-}$); единый рендер через `chemEq`/`ionLabel`, не «сырой» текст.
|
||||
2. **Уравнения реакций** — всегда сбалансированы; стрелки: `=`/`→` (необратимая), `⇌` (обратимая), `↑` (газ), `↓` (осадок), условия над стрелкой ($t$, $\text{кат.}$, $\text{эл.ток}$).
|
||||
3. **Состояние** — для качественных реакций обязательно показывать признак: цвет осадка, пузырьки газа, изменение окраски индикатора (через `testTube`/`indicatorScale`).
|
||||
4. **Ионные уравнения** — для §9–10 и всех качественных реакций давать тройку: молекулярное → полное ионное → сокращённое ионное (виджет-конвертер).
|
||||
5. **Молекулярные модели** — структурная формула + 3D (biochem-core); для каждого изучаемого простого вещества/важного соединения хотя бы 1 модель.
|
||||
6. **Цвета — химически достоверные**: осадки ($AgCl$ белый, $Cu(OH)_2$ голубой, $Fe(OH)_3$ бурый, $Fe(OH)_2$ зеленоватый), пламя ($Ca$ кирпично-красный, $Sr$ карминовый, $Ba$ жёлто-зелёный, $Na$ жёлтый, $K$ фиолетовый).
|
||||
7. **Безопасность** — где уместно (разбавление $H_2SO_4$, ядовитость $CO$, $H_2S$) — заметка-«скрепка» с предупреждением.
|
||||
8. **KaTeX-эскейпы** — в JS-шаблонах двойной backslash (`\\to`, `\\downarrow`, `\\rightleftharpoons`).
|
||||
9. **Drag/слайдеры** — `window`-listeners + `{passive:false}` + state ВЫШЕ `redraw()` (как в стандарте геометрии), `touch-action:none` на draggable SVG/canvas.
|
||||
10. **Без эмоджи** — только inline SVG `.ic`/`.ico` (правило проекта).
|
||||
|
||||
### D. Типы интерактивов по химическим темам
|
||||
|
||||
| Тип темы | Интерактив |
|
||||
|----------|------------|
|
||||
| Строение атома / ПСХЭ (§2, 12, 40) | `miniPeriodic`, `orbitalDiagram`, sim `periodic` |
|
||||
| Химическая связь (§3) | slider ЭО → тип связи, диполь, e-пары |
|
||||
| Растворы (§4–6) | калькулятор $w$/$c$/разбавления, график растворимости, sim `solutions` |
|
||||
| Диссоциация / РИО (§7–10) | `dissociationAnim`, конвертер ионных уравнений, `solubilityTable` |
|
||||
| Аллотропия (§13, 20, 31, 34, 37) | 3D-модели biochem-core |
|
||||
| Качественные реакции (§17, 25, 28, 36, 48, 52) | `testTube` + ионное уравнение + признак |
|
||||
| Кислоты/основания (§16, 23, 29, 32) | `indicatorScale`, ступенчатая диссоциация |
|
||||
| Ряд активности / свойства металлов (§42, 43, 49) | `activitySeries`, предсказатель реакции |
|
||||
| Электролиз (§45) | sim `electrolysis` |
|
||||
| Окисл.-восст. / горение (§14, 18) | анимация горения, баланс ОВР |
|
||||
| Производство (§24, 39, 45, 53) | пошаговые схемы (контактный способ, варка стекла, доменная печь) |
|
||||
| Удобрения / прикладное (§33, 44, 47, 11) | калькуляторы, инфографика |
|
||||
|
||||
---
|
||||
|
||||
## 📦 СТРУКТУРА КАЖДОГО § (стандарт наполнения)
|
||||
|
||||
Каждый § после доработки содержит:
|
||||
|
||||
**Теория (3–4 карточки):**
|
||||
- `theory` — основное определение/понятие + наглядная SVG/модель
|
||||
- `rule` — ключевая закономерность/формула (рамка)
|
||||
- `example` — разобранный пример (реакция/расчёт) с пошаговым рендером
|
||||
- (для прикладных §) `apply` — применение/значение (инфографика)
|
||||
|
||||
**Интерактивы (4–6 на §):**
|
||||
1. **Звёздный виджет** темы (из карты содержания выше)
|
||||
2. **Конструктор/симулятор** (slider / drag / sim из реестра)
|
||||
3. **Калькулятор** (молярная масса, $w$, выход, по уравнению) — где применимо
|
||||
4. **DnD-сортер / классификатор** (классы веществ, тип связи, признак реакции)
|
||||
5. **Тренажёр** — 5 задач с inline-наглядностью (формула/уравнение/модель в условии)
|
||||
6. **Босс §** — 4 интеграционные задачи (+5 XP каждая)
|
||||
|
||||
**Дополнительно:**
|
||||
- Пополнение **глоссария** (термины §)
|
||||
- **Вопросы и задания** из учебника (адаптированные, с проверкой)
|
||||
- jsdom-тест страницы проходит
|
||||
|
||||
**Финал главы:**
|
||||
- **Итоговая шпаргалка** — mini-cards (формула/реакция/иконка) по каждому §
|
||||
- **Карта связей** — SVG-граф понятий главы (генетические связи веществ)
|
||||
- **7 интегрированных боссов** (каждый ≥2 темы, +10 XP)
|
||||
- **Achievement** «Мастер главы N» (+50 XP, confetti)
|
||||
- Кнопка перехода к следующей главе
|
||||
|
||||
---
|
||||
|
||||
## 🚀 ПОРЯДОК РЕАЛИЗАЦИИ (по фазам)
|
||||
|
||||
### Phase 0 — Фундамент
|
||||
- Исправить метаданные `chemistry_9.html`: `<title>Химия 9 — §1–53`, убрать артефакт «МЕТАЛЛЫ» в water-mark/CSS, починить кодировку заголовков (UTF-8).
|
||||
- Перегруппировать карточки para-selector на **4 главы** книги (цвета: I amber, II blue, III teal/cyan, IV deep-orange).
|
||||
- Создать `/js/chem9_svg.js` с хелперами A–B (заглушки → реализация по мере фаз).
|
||||
- Подключить `biochem-core.js` и нужные симуляторы на страницу.
|
||||
- **Миграция БД** (следующий номер после `040_content_access.sql` → `041_chemistry9_fix.sql`): обновить запись `chemistry-9` — `para_count = 53`, корректные `title`/`author`/`description`, `color='amber'`. (Слаг и html_path уже есть из migration 004.)
|
||||
- jsdom-тест-каркас для страницы.
|
||||
|
||||
### Phase 1 — Глава 1 «Повторение» (§§1–3) — разогрев
|
||||
Простые темы, но закладываем 3 базовых движка: `miniPeriodic`, `orbitalDiagram`, визуализатор связи. Эти виджеты переиспользуются во всех 4 главах.
|
||||
|
||||
### Phase 2 — Глава 2 «Растворы. ТЭД» (§§4–11) + ПР1, ПР2
|
||||
Самая «расчётно-логическая». Закладываем `solubilityTable`, конвертер ионных уравнений, `dissociationAnim`, калькулятор раствора, sim `solutions`. Эти движки критичны для всей химии 9.
|
||||
|
||||
### Phase 3 — Глава 3 «Неметаллы» (§§12–25): галогены, кислород, сера + лаб.1, лаб.2
|
||||
SVG/модели-тяжёлая. 3D-аллотропы (biochem-core), первые качественные реакции (`testTube`: $AgCl↓$, $BaSO_4↓$), горение, безопасность $H_2SO_4$.
|
||||
|
||||
### Phase 4 — Глава 3 «Неметаллы» (§§26–39): азот, фосфор, углерод, кремний + лаб.3, лаб.4, ПР3
|
||||
Фонтан аммиака, NPK-калькулятор, качеств. реакция на $CO_3^{2-}$, сталактиты, варка стекла/цемент.
|
||||
|
||||
### Phase 5 — Глава 4 «Металлы» (§§40–48): общие свойства, щелочные, ЩЗМ + лаб.5
|
||||
`activitySeries`, sim `electrolysis`, цвета пламени, симулятор жёсткости воды, качеств. на $Ca^{2+}/Ba^{2+}$.
|
||||
|
||||
### Phase 6 — Глава 4 «Металлы» (§§49–53): алюминий, железо + лаб.6, ПР4
|
||||
Амфотерность $Al(OH)_3$, качеств. на $Fe^{2+}/Fe^{3+}$, симулятор коррозии, анимация доменной печи.
|
||||
|
||||
### Phase 7 — Финалы глав + общий финал учебника
|
||||
- Шпаргалки и карты связей по каждой главе
|
||||
- Интегрированные боссы + achievements
|
||||
- **Большой финал**: генетическая карта «неметаллы ↔ металлы ↔ соли», итоговый босс-квест, ачивка «Химик 9 класса»
|
||||
- Глоссарий собран целиком, связан `[[ссылками]]`
|
||||
|
||||
### Phase 8 — Качество и админка
|
||||
- Полный прогон jsdom-тестов (каждый § — builder не stub)
|
||||
- Аудит KaTeX/`chemEq`-эскейпов, баланса всех уравнений
|
||||
- Синхронизация с админкой (если симуляторы добавлялись в `lab.html` → обновить `ADMIN_SIMS` в `admin.html` — правило проекта)
|
||||
- Проверка доступа по классам/ученикам (`contentAccess`, `/api/access`)
|
||||
|
||||
> Рекомендуемый темп: внутри фазы — по 2–3 § за «волну», каждая волна = commit + проходящий jsdom-тест.
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ ИНТЕГРАЦИЯ С ПРОЕКТОМ
|
||||
|
||||
| Точка | Действие |
|
||||
|-------|----------|
|
||||
| **БД каталог** | `textbooks` уже содержит `chemistry-9` (migration 004). Миграция `041` — поправить `para_count=53`, title/description. |
|
||||
| **Прогресс/XP** | Автоматически: `textbook-xp-widget.js` (+5 XP/§), `textbook-tracker.js`, `LS.xp`. Доп. XP за боссов — по образцу `phys7_ch1_widgets.js`. |
|
||||
| **Симуляторы** | Реестр `frontend/js/labs/_register-all.js` + `lab-glue.js`. Химические sim уже зарегистрированы (`periodic`, `electrolysis`, `titration`, `qualanalysis`, `solutions`, `stoichiometry`, `chemsandbox`). |
|
||||
| **Молекулы** | `biochem-core.js` (парсинг, молярная масса, 2D/3D, VSEPR). |
|
||||
| **Бэкенд** | Роуты готовы: `backend/src/routes/textbooks.js` (catalog/progress/bookmarks). Доступ: `backend/src/services/contentAccess.js`. |
|
||||
| **Глоссарий** | Реализовать виджет на странице (общего нет) — всплывающие определения по терминам. |
|
||||
| **Тесты** | `cd backend && npm test` (jsdom). На каждый § — тест: страница строится, builder не stub, уравнения сбалансированы. |
|
||||
| **Админка** | Если добавляются новые sim в `lab.html` → синхронно обновить `ADMIN_SIMS` в `admin.html`. |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ КРИТИЧЕСКИЕ ПРАВИЛА
|
||||
|
||||
### ❌ НЕ делать
|
||||
- Не оставлять «сырые» формулы текстом — только через `chemEq`/`ionLabel`/KaTeX.
|
||||
- Не публиковать несбалансированные уравнения (аудит баланса перед commit).
|
||||
- Не дублировать молекулярный движок — использовать `biochem-core.js`.
|
||||
- Не использовать `setPointerCapture` (теряется после `innerHTML`-replace) → `window`-listeners + state-flag.
|
||||
- `\to`, `\downarrow`, `\rightleftharpoons` без удвоения backslash в JS-шаблонах.
|
||||
- Slider-диапазоны за пределы химически возможного (концентрации, температуры).
|
||||
- Эмоджи — запрещены; только inline SVG `.ic`.
|
||||
- Grep tool — запрещён; поиск только `ast-index`.
|
||||
|
||||
### ✅ Обязательно
|
||||
- Каждый commit → jsdom-тест 100% pass.
|
||||
- Аудит баланса уравнений + KaTeX-эскейпов после каждой волны.
|
||||
- Качественная реакция = уравнение (молек.+ионное) **+ видимый признак** (`testTube`/индикатор).
|
||||
- Цвета осадков/пламени — химически достоверные.
|
||||
- Все builder-функции в конце финальной волны главы — НЕ stub'ы.
|
||||
- Коммитить только изменённые файлы (не `git add -A`), сразу push (правило CLAUDE.md).
|
||||
|
||||
---
|
||||
|
||||
## 📊 Оценка объёма
|
||||
|
||||
| Глава | § | Лаб/ПР | Ожидаемый LOC |
|
||||
|-------|---|--------|---------------|
|
||||
| Гл.1 Повторение | 3 | — | ~3 500 (+движки ПСХЭ/орбитали/связь) |
|
||||
| Гл.2 Растворы. ТЭД | 8 | ПР1, ПР2 | ~9 000 (+`solubilityTable`, ионные ур-я, `solutions`) |
|
||||
| Гл.3 Неметаллы | 28 | 4 лаб, ПР3 | ~28 000 (модели, качеств. реакции, безопасность) |
|
||||
| Гл.4 Металлы | 14 | 2 лаб, ПР4 | ~16 000 (ряд активности, электролиз, коррозия, доменная печь) |
|
||||
| Финалы глав + общий | — | — | ~5 000 |
|
||||
| `/js/chem9_svg.js` хелперы | — | — | ~2 500 |
|
||||
| **Итого** | **53** | **6 лаб + 4 ПР** | **~64 000 LOC** |
|
||||
|
||||
Существующий каркас (~10 700 строк) переиспользуется; основной прирост — интерактив,
|
||||
наглядность и движки.
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Запуск
|
||||
|
||||
**Phase 0**: чистка метаданных `chemistry_9.html` + перегруппировка на 4 главы +
|
||||
`/js/chem9_svg.js` (скелет) + подключение `biochem-core.js`/симуляторов + миграция `041`.
|
||||
**Phase 1**: Глава 1 (§§1–3) — закладываем `miniPeriodic`, `orbitalDiagram`, визуализатор связи.
|
||||
|
||||
Дальше — последовательно по главам (Phase 2 → 6), затем финалы (Phase 7) и качество (Phase 8).
|
||||
Reference in New Issue
Block a user