diff --git a/backend/src/db/migrations/009_physics_8.sql b/backend/src/db/migrations/009_physics_8.sql
deleted file mode 100644
index e376c2a..0000000
--- a/backend/src/db/migrations/009_physics_8.sql
+++ /dev/null
@@ -1,12 +0,0 @@
--- Add Physics 8 textbooks (3 parts: thermal, electrical, optical phenomena)
--- by Исаченкова Л. А. (2018). 40 paragraphs total, split across 3 files.
-INSERT OR IGNORE INTO textbooks (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order) VALUES
- ('physics-8-thermal', 'physics', 8, 'Физика 8 — Тепловые явления', 'Исаченкова Л. А.',
- 'Часть 1. §1–§11: внутренняя энергия, теплопередача, удельная теплоёмкость, фазовые переходы, тепловые двигатели.',
- 'physics8_thermal.html', 11, 'amber', 4),
- ('physics-8-electro', 'physics', 8, 'Физика 8 — Электрические явления', 'Исаченкова Л. А.',
- 'Часть 2. §12–§31: электризация, закон Кулона, электрический ток, закон Ома, работа и мощность тока, электромагнитные явления.',
- 'physics8_electro.html', 20, 'blue', 5),
- ('physics-8-optics', 'physics', 8, 'Физика 8 — Световые явления', 'Исаченкова Л. А.',
- 'Часть 3. §32–§40: источники света, отражение и преломление, линзы, оптические приборы, цвет и спектр.',
- 'physics8_optics.html', 9, 'violet', 6);
diff --git a/backend/src/db/migrations/010_physics_8_merge.sql b/backend/src/db/migrations/010_physics_8_merge.sql
deleted file mode 100644
index 917c192..0000000
--- a/backend/src/db/migrations/010_physics_8_merge.sql
+++ /dev/null
@@ -1,11 +0,0 @@
--- Объединить 3 отдельных учебника физики 8 (thermal/electro/optics) в один hub-учебник.
--- Hub-страница physics_8.html содержит карточки 3 разделов и ссылки на исходные файлы.
-
--- 1. Удаляем 3 прежние записи (создали их часом раньше, прогресса пользователей ещё нет).
-DELETE FROM textbooks WHERE slug IN ('physics-8-thermal','physics-8-electro','physics-8-optics');
-
--- 2. Регистрируем единый учебник «Физика 8» с hub-страницей.
-INSERT OR IGNORE INTO textbooks (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order) VALUES
- ('physics-8', 'physics', 8, 'Физика — 8 класс', 'Исаченкова Л. А.',
- 'Интерактивный учебник по физике 8 класса. 40 параграфов в трёх разделах: Тепловые явления (§1–§11), Электрические явления (§12–§31), Световые явления (§32–§40).',
- 'physics_8.html', 40, 'blue', 4);
diff --git a/backend/src/db/migrations/015_physics_8_hub.sql b/backend/src/db/migrations/015_physics_8_hub.sql
deleted file mode 100644
index f790245..0000000
--- a/backend/src/db/migrations/015_physics_8_hub.sql
+++ /dev/null
@@ -1,29 +0,0 @@
--- Physics 8 hub migration.
--- Converts physics-8 from a monolithic entry into a hub with 3 children,
--- mirroring the algebra-8 hub pattern established in migration 014.
-
--- 1. Insert 3 child rows for the sub-textbooks.
-INSERT OR IGNORE INTO textbooks
- (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
-VALUES
- ('physics-8-thermal', 'physics', 8, 'Физика 8 · Тепловые явления',
- '',
- 'Внутренняя энергия, теплопередача, удельная теплоёмкость, фазовые переходы, тепловые двигатели.',
- 'physics8_thermal.html', 11, 'orange', 1, 1, 'physics-8'),
- ('physics-8-electro', 'physics', 8, 'Физика 8 · Электрические явления',
- '',
- 'Электростатика, закон Кулона, электрический ток, закон Ома, электрические цепи.',
- 'physics8_electro.html', 20, 'blue', 2, 1, 'physics-8'),
- ('physics-8-optics', 'physics', 8, 'Физика 8 · Световые явления',
- '',
- 'Источники света, отражение, преломление, линзы, оптические приборы.',
- 'physics8_optics.html', 9, 'purple', 3, 1, 'physics-8');
-
--- 2. Update the parent physics-8 row: clear author, set para_count=40, update description.
-UPDATE textbooks
-SET
- author = '',
- para_count = 40,
- html_path = 'physics_8.html',
- description = 'Полный курс физики 8 класса в трёх разделах: тепловые явления (§1–§11), электрические явления (§12–§31), световые явления (§32–§40).'
-WHERE slug = 'physics-8';
diff --git a/backend/src/db/migrations/037_physics_8_hub.sql b/backend/src/db/migrations/037_physics_8_hub.sql
index 879d7b6..e2ac551 100644
--- a/backend/src/db/migrations/037_physics_8_hub.sql
+++ b/backend/src/db/migrations/037_physics_8_hub.sql
@@ -1,19 +1,27 @@
--- Physics 8 hub migration.
--- Rebuilds physics-8 as a full 3-chapter + lab textbook in the style of physics-10:
+-- Physics 8 hub migration (self-sufficient).
+-- Creates / rebuilds physics-8 as a full 3-chapter + lab textbook in the style of physics-10:
-- physics-8 (hub, html_path = physics_8_hub.html)
-- physics-8-ch1 (Тепловые явления, §§1–11) → physics_8_ch1.html
-- physics-8-ch2 (Электромагнитные явления, §§12–31) → physics_8_ch2.html
-- physics-8-ch3 (Световые явления, §§32–40) → physics_8_ch3.html
-- physics-8-lab (Лабораторный практикум, 7 ЛР) → physics_8_lab.html
--
--- Replaces the old legacy children created in migration 015
--- (physics-8-thermal / physics-8-electro / physics-8-optics), which pointed
--- to monolithic legacy files. Author left empty per project policy.
+-- This migration is the single source of truth for physics-8 textbook structure.
+-- It is idempotent and does not depend on any prior physics-8 migration.
--- 1. Remove legacy children (HTML files are kept on disk as backup, just unlinked from DB).
+-- 1. Remove legacy children if they ever existed (no-op on fresh DBs).
DELETE FROM textbooks WHERE slug IN ('physics-8-thermal', 'physics-8-electro', 'physics-8-optics');
--- 2. Update the parent physics-8 hub row.
+-- 2. Ensure the parent physics-8 hub row exists.
+INSERT OR IGNORE INTO textbooks
+ (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
+VALUES
+ ('physics-8', 'physics', 8, 'Физика — 8 класс',
+ '',
+ 'Полный курс физики 8 класса: тепловые явления (§§1–11), электромагнитные явления (§§12–31), световые явления (§§32–40), 7 виртуальных лабораторных работ.',
+ 'physics_8_hub.html', 47, 'violet', 4, 1);
+
+-- 3. Update the parent physics-8 hub row (covers case where it already existed from legacy migrations).
UPDATE textbooks
SET
author = '',
diff --git a/frontend/js/phys.js b/frontend/js/phys.js
index 9e596b8..098ca3e 100644
--- a/frontend/js/phys.js
+++ b/frontend/js/phys.js
@@ -353,6 +353,81 @@ function phaseGraphTT(W, H, pad, points, tMaxAll, TminAll, TmaxAll) {
return { svg: s, toX: toX, toY: toY };
}
+// === Анимация конвекции — тороидальный поток частиц ===
+// Используется в §4 Физики 8 (главный визуал «конвекция в жидкости/газе»).
+// opts: { N — число частиц (default 24), w, h — размеры области, speed (отн. ед., default 1),
+// tHot, tCold — температуры для окраски (default 100, 20) }.
+function createConvectionSim(opts) {
+ opts = opts || {};
+ const N = opts.N || 24;
+ const w = opts.w || 220;
+ const h = opts.h || 140;
+ const speed = opts.speed != null ? opts.speed : 1;
+ const tHot = opts.tHot != null ? opts.tHot : 100;
+ const tCold = opts.tCold != null ? opts.tCold : 20;
+ // Каждая частица движется по фазе вдоль контура прямоугольника:
+ // правая сторона — вверх (нагрев / подъём), верхняя — влево, левая — вниз (охлаждение), нижняя — вправо.
+ const parts = new Array(N);
+ for (let i = 0; i < N; i++) parts[i] = { phase: i / N };
+ return {
+ N: N, w: w, h: h, speed: speed,
+ _tHot: tHot, _tCold: tCold,
+ setSpeed(v) { this.speed = v; },
+ setHot(v) { this._tHot = v; },
+ setCold(v) { this._tCold = v; },
+ step(dt) {
+ const dPhase = 0.18 * this.speed * dt;
+ for (let i = 0; i < this.N; i++) {
+ parts[i].phase = (parts[i].phase + dPhase) % 1;
+ if (parts[i].phase < 0) parts[i].phase += 1;
+ }
+ },
+ // Возвращает координаты частицы (cx, cy) и температуру по её положению.
+ // phase ∈ [0,1): 0..0.25 — правая (подъём, t→tHot), 0.25..0.5 — верх (t=tHot),
+ // 0.5..0.75 — левая (опускание, t→tCold), 0.75..1 — низ (t=tCold).
+ _xy(phase, x, y) {
+ const margin = 12;
+ const W = this.w - 2 * margin;
+ const H = this.h - 2 * margin;
+ let lx, ly, t;
+ if (phase < 0.25) {
+ const k = phase / 0.25;
+ lx = W; ly = H * (1 - k);
+ t = this._tCold + (this._tHot - this._tCold) * k;
+ } else if (phase < 0.5) {
+ const k = (phase - 0.25) / 0.25;
+ lx = W * (1 - k); ly = 0;
+ t = this._tHot;
+ } else if (phase < 0.75) {
+ const k = (phase - 0.5) / 0.25;
+ lx = 0; ly = H * k;
+ t = this._tHot - (this._tHot - this._tCold) * k;
+ } else {
+ const k = (phase - 0.75) / 0.25;
+ lx = W * k; ly = H;
+ t = this._tCold;
+ }
+ return { cx: x + margin + lx, cy: y + margin + ly, t: t };
+ },
+ render(x, y) {
+ const tMin = Math.min(this._tCold, this._tHot);
+ const tMax = Math.max(this._tCold, this._tHot);
+ let s = '';
+ // Контур сосуда
+ s += `
Все 3 правила послед. соединения подтверждены опытом:
' +'Вывод: закон Ома и правила последовательного соединения выполняются.
'); @@ -938,10 +938,10 @@ function build_lr5(){ +'' +'' +'Все 3 правила параллельного соединения подтверждены:
' +'Вывод: общее $R$ меньше любого из $R_1$, $R_2$, что соответствует теории.
'); @@ -1031,7 +1031,7 @@ function build_lr7(){ h += '| Опыт | $\\alpha$, ° | $\\beta$, ° | $\\alpha - \\beta$ |
|---|