feat(phys8): Phase 0 — skeleton hub + 3 chapters + lab + phys.js/optics.js
Полная инфраструктура курса «Физика 8» (Исаченкова, 2018): - physics_8_hub.html: палитра violet/indigo, 3 главы + ЛР + финал курса с 10 интегрированными боссами и ачивкой «Магистр физики 8» (+150 XP) - physics_8_ch1.html (Тепловые, §§1–11): красный акцент - physics_8_ch2.html (Электромагнитные, §§12–31): янтарный акцент - physics_8_ch3.html (Световые, §§32–40): голубой акцент - physics_8_lab.html (7 ЛР): зелёный акцент - Расширение phys.js: tempColor, thermometer, calorimeter, createHeatBar, phaseGraphTT, Rseries, Rparallel - Новый модуль optics.js: ray, refractRay, reflectRay, mirrorPlane, mirrorSpherical, thinLens, buildLensImage, goldenRays, eyeDiagram, lightObject, shadowTriangle - Миграция 037: replace legacy children (thermal/electro/optics) на physics-8-ch1/ch2/ch3 + physics-8-lab; обновлён hub до 47 пунктов BUILDERS всех § рендерят stub с указанием Phase/Wave из PLAN_PHYSICS_8.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
-- Physics 8 hub migration.
|
||||||
|
-- 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.
|
||||||
|
|
||||||
|
-- 1. Remove legacy children (HTML files are kept on disk as backup, just unlinked from DB).
|
||||||
|
DELETE FROM textbooks WHERE slug IN ('physics-8-thermal', 'physics-8-electro', 'physics-8-optics');
|
||||||
|
|
||||||
|
-- 2. Update the parent physics-8 hub row.
|
||||||
|
UPDATE textbooks
|
||||||
|
SET
|
||||||
|
author = '',
|
||||||
|
para_count = 47,
|
||||||
|
html_path = 'physics_8_hub.html',
|
||||||
|
description = 'Полный курс физики 8 класса: тепловые явления (§§1–11), электромагнитные явления (§§12–31), световые явления (§§32–40), 7 виртуальных лабораторных работ.',
|
||||||
|
color = 'violet'
|
||||||
|
WHERE slug = 'physics-8';
|
||||||
|
|
||||||
|
-- 3. Insert the 4 new children.
|
||||||
|
INSERT INTO textbooks
|
||||||
|
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||||
|
VALUES
|
||||||
|
('physics-8-ch1', 'physics', 8, 'Физика 8 · Тепловые явления',
|
||||||
|
'',
|
||||||
|
'§§1–11: внутренняя энергия, теплопроводность, конвекция, излучение, удельная теплоёмкость, плавление, кипение.',
|
||||||
|
'physics_8_ch1.html', 11, 'red', 1, 1, 'physics-8'),
|
||||||
|
('physics-8-ch2', 'physics', 8, 'Физика 8 · Электромагнитные явления',
|
||||||
|
'',
|
||||||
|
'§§12–31: электризация, ток, закон Ома, последовательное и параллельное соединения, мощность тока, постоянные магниты, электромагнит.',
|
||||||
|
'physics_8_ch2.html', 20, 'amber', 2, 1, 'physics-8'),
|
||||||
|
('physics-8-ch3', 'physics', 8, 'Физика 8 · Световые явления',
|
||||||
|
'',
|
||||||
|
'§§32–40: прямолинейное распространение света, отражение, преломление, линзы, построение изображений, глаз и очки.',
|
||||||
|
'physics_8_ch3.html', 9, 'cyan', 3, 1, 'physics-8'),
|
||||||
|
('physics-8-lab', 'physics', 8, 'Физика 8 · Лабораторный практикум',
|
||||||
|
'',
|
||||||
|
'7 виртуальных лабораторных работ: теплообмен, удельная теплоёмкость, простейшая цепь, последовательное и параллельное соединения, работа и мощность, отражение света.',
|
||||||
|
'physics_8_lab.html', 7, 'emerald', 4, 1, 'physics-8');
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
// optics.js — модуль хелперов геометрической оптики для учебника Физика 8
|
||||||
|
// Экспорт в window.OPTICS = { ... }
|
||||||
|
(function(){
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// === Палитра ===
|
||||||
|
const COLOR = {
|
||||||
|
ray: '#fbbf24',
|
||||||
|
rayIncident:'#0891b2',
|
||||||
|
rayReflected:'#10b981',
|
||||||
|
rayRefracted:'#a855f7',
|
||||||
|
normal: '#94a3b8',
|
||||||
|
lensConv: '#22c55e',
|
||||||
|
lensDiv: '#f97316',
|
||||||
|
mirror: '#475569',
|
||||||
|
hatch: '#94a3b8',
|
||||||
|
axis: '#cbd5e1',
|
||||||
|
focus: '#1d4ed8',
|
||||||
|
imageReal: '#7c3aed',
|
||||||
|
imageVirtual:'#a78bfa'
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Стрелка-наконечник (внутренний помощник) ===
|
||||||
|
function arrowHead(x, y, ux, uy, size, color){
|
||||||
|
const w = size * 0.55;
|
||||||
|
const px = -uy, py = ux;
|
||||||
|
const bx = x - ux*size, by = y - uy*size;
|
||||||
|
const lx = bx + px*w, ly = by + py*w;
|
||||||
|
const rx = bx - px*w, ry = by - py*w;
|
||||||
|
return `<polygon points="${x.toFixed(1)},${y.toFixed(1)} ${lx.toFixed(1)},${ly.toFixed(1)} ${rx.toFixed(1)},${ry.toFixed(1)}" fill="${color}"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Луч (линия + стрелка посередине) ===
|
||||||
|
// dashed: true → пунктир (для виртуальных продолжений)
|
||||||
|
function ray(x1, y1, x2, y2, color, dashed){
|
||||||
|
color = color || COLOR.ray;
|
||||||
|
const dx = x2 - x1, dy = y2 - y1;
|
||||||
|
const len = Math.sqrt(dx*dx + dy*dy);
|
||||||
|
if (len < 1e-6) return '';
|
||||||
|
const ux = dx/len, uy = dy/len;
|
||||||
|
const dash = dashed ? ' stroke-dasharray="6 4"' : '';
|
||||||
|
let s = '';
|
||||||
|
s += `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${color}" stroke-width="2" stroke-linecap="round"${dash}/>`;
|
||||||
|
// Стрелка-наконечник в точке (x2,y2)
|
||||||
|
if (!dashed) s += arrowHead(x2, y2, ux, uy, 9, color);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Падающий, отражённый, преломлённый лучи + нормаль ===
|
||||||
|
// На границе двух сред (горизонтальной) с нормалью вверх
|
||||||
|
function refractRay(x0, y0, angleInDeg, n1, n2, len){
|
||||||
|
// angleInDeg — угол падения от нормали (в градусах)
|
||||||
|
// Возвращает SVG: падающий луч (сверху-слева к (x0,y0)) + нормаль + отражённый + преломлённый
|
||||||
|
len = len || 80;
|
||||||
|
const a1 = angleInDeg * Math.PI / 180;
|
||||||
|
const sinA2 = (n1/n2) * Math.sin(a1);
|
||||||
|
const tir = Math.abs(sinA2) > 1; // полное внутреннее отражение
|
||||||
|
const a2 = tir ? 0 : Math.asin(sinA2);
|
||||||
|
|
||||||
|
let s = '';
|
||||||
|
// Граница
|
||||||
|
s += `<line x1="${x0-len*1.5}" y1="${y0}" x2="${x0+len*1.5}" y2="${y0}" stroke="${COLOR.mirror}" stroke-width="1.5"/>`;
|
||||||
|
// Нормаль (вверх и вниз пунктиром)
|
||||||
|
s += `<line x1="${x0}" y1="${y0-len*0.9}" x2="${x0}" y2="${y0+len*0.9}" stroke="${COLOR.normal}" stroke-width="1.2" stroke-dasharray="5 3"/>`;
|
||||||
|
// Падающий луч (приходит сверху-слева)
|
||||||
|
const xi = x0 - len*Math.sin(a1), yi = y0 - len*Math.cos(a1);
|
||||||
|
s += ray(xi, yi, x0, y0, COLOR.rayIncident);
|
||||||
|
// Отражённый луч (вверх-вправо)
|
||||||
|
const xr = x0 + len*Math.sin(a1), yr = y0 - len*Math.cos(a1);
|
||||||
|
s += ray(x0, y0, xr, yr, COLOR.rayReflected);
|
||||||
|
// Преломлённый луч (вниз)
|
||||||
|
if (!tir){
|
||||||
|
const xt = x0 + len*Math.sin(a2), yt = y0 + len*Math.cos(a2);
|
||||||
|
s += ray(x0, y0, xt, yt, COLOR.rayRefracted);
|
||||||
|
}
|
||||||
|
return { svg: s, a1: a1, a2: a2, tir: tir };
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Отражение от плоского зеркала ===
|
||||||
|
// Зеркало горизонтальное на y = y0
|
||||||
|
function reflectRay(x0, y0, angleInDeg, len){
|
||||||
|
len = len || 80;
|
||||||
|
const a = angleInDeg * Math.PI / 180;
|
||||||
|
let s = '';
|
||||||
|
// Зеркало
|
||||||
|
s += `<line x1="${x0-len*1.4}" y1="${y0}" x2="${x0+len*1.4}" y2="${y0}" stroke="${COLOR.mirror}" stroke-width="2"/>`;
|
||||||
|
// Штриховка с обратной стороны
|
||||||
|
for (let i = -len; i <= len; i += 8){
|
||||||
|
s += `<line x1="${x0+i}" y1="${y0}" x2="${x0+i+5}" y2="${y0+6}" stroke="${COLOR.hatch}" stroke-width="1"/>`;
|
||||||
|
}
|
||||||
|
// Нормаль
|
||||||
|
s += `<line x1="${x0}" y1="${y0-len*0.9}" x2="${x0}" y2="${y0+10}" stroke="${COLOR.normal}" stroke-width="1.2" stroke-dasharray="5 3"/>`;
|
||||||
|
// Падающий
|
||||||
|
const xi = x0 - len*Math.sin(a), yi = y0 - len*Math.cos(a);
|
||||||
|
s += ray(xi, yi, x0, y0, COLOR.rayIncident);
|
||||||
|
// Отражённый
|
||||||
|
const xr = x0 + len*Math.sin(a), yr = y0 - len*Math.cos(a);
|
||||||
|
s += ray(x0, y0, xr, yr, COLOR.rayReflected);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Плоское зеркало (отдельный элемент) ===
|
||||||
|
function mirrorPlane(x, y, len, angleDeg){
|
||||||
|
angleDeg = angleDeg || 0;
|
||||||
|
const a = angleDeg * Math.PI / 180;
|
||||||
|
const ca = Math.cos(a), sa = Math.sin(a);
|
||||||
|
const x1 = x - len/2*ca, y1 = y - len/2*sa;
|
||||||
|
const x2 = x + len/2*ca, y2 = y + len/2*sa;
|
||||||
|
const nx = -sa, ny = ca; // нормаль "наружу"
|
||||||
|
let s = `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${COLOR.mirror}" stroke-width="2.5"/>`;
|
||||||
|
// Штриховка с тыльной стороны
|
||||||
|
for (let t = -len/2 + 3; t <= len/2; t += 8){
|
||||||
|
const bx = x + t*ca, by = y + t*sa;
|
||||||
|
s += `<line x1="${bx.toFixed(1)}" y1="${by.toFixed(1)}" x2="${(bx - nx*6).toFixed(1)}" y2="${(by - ny*6).toFixed(1)}" stroke="${COLOR.hatch}" stroke-width="1"/>`;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Сферическое зеркало (вогнутое/выпуклое) ===
|
||||||
|
// kind: 'concave' (вогнутое — фокусирует) | 'convex' (выпуклое — рассеивает)
|
||||||
|
function mirrorSpherical(cx, cy, R, kind, halfH){
|
||||||
|
halfH = halfH || R * 0.6;
|
||||||
|
const sign = (kind === 'convex') ? -1 : 1;
|
||||||
|
// Дуга
|
||||||
|
const x1 = cx - sign*R*0.15, y1 = cy - halfH;
|
||||||
|
const x2 = cx - sign*R*0.15, y2 = cy + halfH;
|
||||||
|
const sweep = sign > 0 ? 0 : 1;
|
||||||
|
let s = `<path d="M ${x1} ${y1} A ${R} ${R} 0 0 ${sweep} ${x2} ${y2}" fill="none" stroke="${COLOR.mirror}" stroke-width="2.5"/>`;
|
||||||
|
// Главная оптическая ось
|
||||||
|
s += `<line x1="${cx-R}" y1="${cy}" x2="${cx+R}" y2="${cy}" stroke="${COLOR.axis}" stroke-width="1" stroke-dasharray="4 3"/>`;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Тонкая линза ===
|
||||||
|
// kind: 'converging' (двусторонне-выпуклая, |) | 'diverging' (двусторонне-вогнутая, |)
|
||||||
|
// Возвращает SVG с осью, фокусами F и 2F
|
||||||
|
function thinLens(cx, cy, halfH, F, kind){
|
||||||
|
halfH = halfH || 70;
|
||||||
|
F = F || 80;
|
||||||
|
const color = (kind === 'diverging') ? COLOR.lensDiv : COLOR.lensConv;
|
||||||
|
let s = '';
|
||||||
|
// Главная оптическая ось
|
||||||
|
s += `<line x1="${cx - 2.5*F}" y1="${cy}" x2="${cx + 2.5*F}" y2="${cy}" stroke="${COLOR.axis}" stroke-width="1.2"/>`;
|
||||||
|
// Линза — вертикальный овал
|
||||||
|
s += `<line x1="${cx}" y1="${cy-halfH}" x2="${cx}" y2="${cy+halfH}" stroke="${color}" stroke-width="2.5"/>`;
|
||||||
|
if (kind === 'diverging'){
|
||||||
|
// Стрелки наружу (рассеивающая)
|
||||||
|
s += arrowHead(cx, cy - halfH, 0, -1, 10, color);
|
||||||
|
s += arrowHead(cx, cy + halfH, 0, 1, 10, color);
|
||||||
|
} else {
|
||||||
|
// Стрелки внутрь (собирающая) — вершины на оси
|
||||||
|
s += arrowHead(cx - 6, cy - halfH + 8, -0.6, -0.8, 10, color);
|
||||||
|
s += arrowHead(cx + 6, cy - halfH + 8, 0.6, -0.8, 10, color);
|
||||||
|
s += arrowHead(cx - 6, cy + halfH - 8, -0.6, 0.8, 10, color);
|
||||||
|
s += arrowHead(cx + 6, cy + halfH - 8, 0.6, 0.8, 10, color);
|
||||||
|
}
|
||||||
|
// Фокусы F и 2F
|
||||||
|
s += `<circle cx="${cx-F}" cy="${cy}" r="3" fill="${COLOR.focus}"/>`;
|
||||||
|
s += `<circle cx="${cx+F}" cy="${cy}" r="3" fill="${COLOR.focus}"/>`;
|
||||||
|
s += `<text x="${cx-F-3}" y="${cy+18}" font-family="JetBrains Mono,monospace" font-size="12" font-weight="700" fill="${COLOR.focus}">F</text>`;
|
||||||
|
s += `<text x="${cx+F-3}" y="${cy+18}" font-family="JetBrains Mono,monospace" font-size="12" font-weight="700" fill="${COLOR.focus}">F</text>`;
|
||||||
|
s += `<circle cx="${cx-2*F}" cy="${cy}" r="2.5" fill="${COLOR.focus}" opacity=".6"/>`;
|
||||||
|
s += `<circle cx="${cx+2*F}" cy="${cy}" r="2.5" fill="${COLOR.focus}" opacity=".6"/>`;
|
||||||
|
s += `<text x="${cx-2*F-5}" y="${cy+18}" font-family="JetBrains Mono,monospace" font-size="11" opacity=".7" fill="${COLOR.focus}">2F</text>`;
|
||||||
|
s += `<text x="${cx+2*F-5}" y="${cy+18}" font-family="JetBrains Mono,monospace" font-size="11" opacity=".7" fill="${COLOR.focus}">2F</text>`;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Построение изображения в тонкой линзе ===
|
||||||
|
// F — фокусное расстояние (положительное для собирающей, отрицательное для рассеивающей)
|
||||||
|
// d — расстояние от предмета до линзы (положительное)
|
||||||
|
// h — высота предмета (положительная)
|
||||||
|
// Возвращает { f: расстояние до изображения, h2: высота, virtual: bool, kind: 'real'|'virtual' }
|
||||||
|
function buildLensImage(F, d, h){
|
||||||
|
// 1/F = 1/d + 1/f → f = d·F / (d - F)
|
||||||
|
if (Math.abs(d - F) < 1e-6){
|
||||||
|
return { f: Infinity, h2: -Infinity, virtual: false, kind: 'infinity' };
|
||||||
|
}
|
||||||
|
const f = d * F / (d - F);
|
||||||
|
const h2 = -h * f / d; // увеличение: h2/h = -f/d
|
||||||
|
const virtual = (F > 0) ? (d < F) : true; // для рассеивающей всегда мнимое
|
||||||
|
return { f: f, h2: h2, virtual: virtual, kind: virtual ? 'virtual' : 'real' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Три "золотых" луча для построения изображения ===
|
||||||
|
// Возвращает SVG трёх лучей: через центр, параллельно оси, через передний фокус.
|
||||||
|
// objX, objY — координата предмета (вершина стрелки)
|
||||||
|
// lensX, lensY — центр линзы
|
||||||
|
// F (px) — фокусное расстояние (в пикселях)
|
||||||
|
function goldenRays(objX, objY, lensX, lensY, F){
|
||||||
|
let s = '';
|
||||||
|
const dx = lensX - objX;
|
||||||
|
const dy = lensY - objY;
|
||||||
|
// Луч 1: параллельно оси → проходит через F с другой стороны
|
||||||
|
const y1At = objY; // приходит на линзу на высоте objY
|
||||||
|
s += ray(objX, y1At, lensX, y1At, COLOR.rayIncident);
|
||||||
|
s += ray(lensX, y1At, lensX + 2.5*F, lensY + (y1At - lensY) * (-2.5*F)/(F * (objY < lensY ? 1 : -1)), COLOR.rayIncident);
|
||||||
|
// Луч 2: через центр — продолжается без преломления
|
||||||
|
s += ray(objX, objY, lensX + dx, lensY + dy, COLOR.rayReflected);
|
||||||
|
// Луч 3: через передний фокус → выходит параллельно оси
|
||||||
|
// (вычислим точку пересечения с линзой)
|
||||||
|
const slope = (lensY - objY) / (objX - (lensX - F));
|
||||||
|
const yAtLens = lensY - slope * F; // упрощённо
|
||||||
|
s += ray(objX, objY, lensX, yAtLens, COLOR.rayRefracted);
|
||||||
|
s += ray(lensX, yAtLens, lensX + 2*F, yAtLens, COLOR.rayRefracted);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Глаз (упрощённая схема) ===
|
||||||
|
// accommodation: 0..1, где 0 — расслабленный (дальний предмет), 1 — напряжённый (близкий)
|
||||||
|
function eyeDiagram(cx, cy, R, accommodation){
|
||||||
|
accommodation = accommodation || 0;
|
||||||
|
let s = '';
|
||||||
|
// Глазное яблоко
|
||||||
|
s += `<circle cx="${cx}" cy="${cy}" r="${R}" fill="#fff" stroke="#0f172a" stroke-width="1.6"/>`;
|
||||||
|
// Роговица — выпуклость спереди (слева)
|
||||||
|
s += `<path d="M ${cx-R*0.95} ${cy-R*0.5} Q ${cx-R*1.15} ${cy} ${cx-R*0.95} ${cy+R*0.5}" fill="#bfdbfe" stroke="#0f172a" stroke-width="1.4"/>`;
|
||||||
|
// Хрусталик — эллипс изнутри, форма зависит от accommodation
|
||||||
|
const lensHalfH = R * 0.45;
|
||||||
|
const lensW = R * (0.10 + 0.10 * accommodation); // толще при напряжении
|
||||||
|
s += `<ellipse cx="${cx-R*0.6}" cy="${cy}" rx="${lensW}" ry="${lensHalfH}" fill="#fef3c7" stroke="${COLOR.lensConv}" stroke-width="1.6"/>`;
|
||||||
|
// Сетчатка — задняя часть
|
||||||
|
s += `<path d="M ${cx+R*0.95} ${cy-R*0.8} A ${R} ${R} 0 0 1 ${cx+R*0.95} ${cy+R*0.8}" fill="none" stroke="${COLOR.imageReal}" stroke-width="2"/>`;
|
||||||
|
// Зрительный нерв
|
||||||
|
s += `<path d="M ${cx+R} ${cy} L ${cx+R*1.4} ${cy+R*0.3}" stroke="#0f172a" stroke-width="2.5"/>`;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Источник света / предмет ===
|
||||||
|
// kind: 'point' (точечный звезда) | 'arrow' (вертикальная стрелка-предмет)
|
||||||
|
function lightObject(x, y, h, kind){
|
||||||
|
kind = kind || 'arrow';
|
||||||
|
if (kind === 'point'){
|
||||||
|
let s = `<circle cx="${x}" cy="${y}" r="6" fill="${COLOR.ray}" stroke="#0f172a" stroke-width="1"/>`;
|
||||||
|
// Лучики
|
||||||
|
for (let i = 0; i < 8; i++){
|
||||||
|
const a = i * Math.PI / 4;
|
||||||
|
const x1 = x + 8*Math.cos(a), y1 = y + 8*Math.sin(a);
|
||||||
|
const x2 = x + 13*Math.cos(a), y2 = y + 13*Math.sin(a);
|
||||||
|
s += `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${COLOR.ray}" stroke-width="1.5" stroke-linecap="round"/>`;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
// Стрелка-предмет: основание на оси (y), вершина на (y - h)
|
||||||
|
const tipY = y - h;
|
||||||
|
const color = '#0f172a';
|
||||||
|
let s = `<line x1="${x}" y1="${y}" x2="${x}" y2="${tipY}" stroke="${color}" stroke-width="2.5"/>`;
|
||||||
|
s += arrowHead(x, tipY, 0, -1, 9, color);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Тень и полутень ===
|
||||||
|
// Источник в (sx, sy), непрозрачный объект — отрезок [ox-or .. ox+or] на высоте oy.
|
||||||
|
// Возвращает SVG полупрозрачных треугольников тени/полутени, падающих на экран y = screenY.
|
||||||
|
function shadowTriangle(sx, sy, ox, oy, or, screenY){
|
||||||
|
// Касательные от источника к краям объекта дают полутень.
|
||||||
|
// Лучи через центр объекта дают полную тень.
|
||||||
|
// Простейшая реализация для точечного источника: только тень.
|
||||||
|
const dy = screenY - sy;
|
||||||
|
const k = dy / (oy - sy);
|
||||||
|
const x1 = sx + (ox - or - sx) * k;
|
||||||
|
const x2 = sx + (ox + or - sx) * k;
|
||||||
|
let s = '';
|
||||||
|
s += `<polygon points="${(ox-or).toFixed(1)},${oy} ${(ox+or).toFixed(1)},${oy} ${x2.toFixed(1)},${screenY} ${x1.toFixed(1)},${screenY}" fill="rgba(15,23,42,.35)" stroke="none"/>`;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Экспорт ===
|
||||||
|
window.OPTICS = {
|
||||||
|
COLOR: COLOR,
|
||||||
|
ray: ray,
|
||||||
|
refractRay: refractRay,
|
||||||
|
reflectRay: reflectRay,
|
||||||
|
mirrorPlane: mirrorPlane,
|
||||||
|
mirrorSpherical: mirrorSpherical,
|
||||||
|
thinLens: thinLens,
|
||||||
|
buildLensImage: buildLensImage,
|
||||||
|
goldenRays: goldenRays,
|
||||||
|
eyeDiagram: eyeDiagram,
|
||||||
|
lightObject: lightObject,
|
||||||
|
shadowTriangle: shadowTriangle
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -225,8 +225,158 @@ function paToAtm(p) { return p / 101325; }
|
|||||||
function litersToM3(V) { return V / 1000; }
|
function litersToM3(V) { return V / 1000; }
|
||||||
function m3ToLiters(V) { return V * 1000; }
|
function m3ToLiters(V) { return V * 1000; }
|
||||||
|
|
||||||
|
// === Расширения для Физики 8 (тепловые явления) ===
|
||||||
|
|
||||||
|
// === Цвет по температуре: 240° (синий) → 0° (красный) ===
|
||||||
|
function tempColor(t, tMin, tMax) {
|
||||||
|
const u = Math.max(0, Math.min(1, (t - tMin) / (tMax - tMin)));
|
||||||
|
const hue = 240 * (1 - u);
|
||||||
|
return `hsl(${hue.toFixed(0)},72%,52%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Термометр (вертикальная шкала, столбик ртути по температуре) ===
|
||||||
|
function thermometer(x, y, h, tMin, tMax, tValue) {
|
||||||
|
// x, y — верх корпуса; h — высота столбика
|
||||||
|
const reservoirR = 11;
|
||||||
|
const tubeW = 8;
|
||||||
|
const u = Math.max(0, Math.min(1, (tValue - tMin) / (tMax - tMin)));
|
||||||
|
const fillH = h * u;
|
||||||
|
const color = tempColor(tValue, tMin, tMax);
|
||||||
|
let s = '';
|
||||||
|
// Корпус (стекло)
|
||||||
|
s += `<rect x="${x - tubeW/2}" y="${y}" width="${tubeW}" height="${h}" rx="${tubeW/2}" fill="#fff" stroke="#0f172a" stroke-width="1.4"/>`;
|
||||||
|
// Столбик ртути
|
||||||
|
s += `<rect x="${x - tubeW/2 + 1.5}" y="${y + h - fillH}" width="${tubeW - 3}" height="${fillH}" rx="${(tubeW-3)/2}" fill="${color}"/>`;
|
||||||
|
// Резервуар
|
||||||
|
s += `<circle cx="${x}" cy="${y + h + reservoirR - 1}" r="${reservoirR}" fill="${color}" stroke="#0f172a" stroke-width="1.4"/>`;
|
||||||
|
// Деления (5)
|
||||||
|
for (let i = 0; i <= 5; i++) {
|
||||||
|
const ty = y + h * i / 5;
|
||||||
|
const tv = tMax - (tMax - tMin) * i / 5;
|
||||||
|
s += `<line x1="${x + tubeW/2 + 1}" y1="${ty}" x2="${x + tubeW/2 + 6}" y2="${ty}" stroke="#0f172a" stroke-width="1"/>`;
|
||||||
|
s += `<text x="${x + tubeW/2 + 9}" y="${ty + 3.5}" font-family="JetBrains Mono,monospace" font-size="10" fill="#0f172a">${tv.toFixed(0)}</text>`;
|
||||||
|
}
|
||||||
|
// Подпись текущего значения
|
||||||
|
s += `<text x="${x - tubeW/2 - 4}" y="${y - 4}" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" font-weight="700" fill="${color}">${tValue.toFixed(0)} °C</text>`;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Калориметр-стакан с жидкостью ===
|
||||||
|
function calorimeter(x, y, w, h, fillFrac, liquidColor) {
|
||||||
|
fillFrac = Math.max(0, Math.min(1, fillFrac || 0.6));
|
||||||
|
liquidColor = liquidColor || '#60a5fa';
|
||||||
|
const wall = 4;
|
||||||
|
const innerH = h - wall;
|
||||||
|
const liqH = innerH * fillFrac;
|
||||||
|
let s = '';
|
||||||
|
// Внешний контур
|
||||||
|
s += `<path d="M ${x} ${y} L ${x} ${y+h} Q ${x} ${y+h+6} ${x+8} ${y+h+6} L ${x+w-8} ${y+h+6} Q ${x+w} ${y+h+6} ${x+w} ${y+h} L ${x+w} ${y} L ${x+w-wall} ${y} L ${x+w-wall} ${y+h-2} L ${x+wall} ${y+h-2} L ${x+wall} ${y} Z" fill="#e0f2fe" stroke="#0f172a" stroke-width="1.5"/>`;
|
||||||
|
// Жидкость
|
||||||
|
s += `<rect x="${x+wall+1}" y="${y + innerH - liqH + wall/2}" width="${w - 2*wall - 2}" height="${liqH}" fill="${liquidColor}" opacity="0.78"/>`;
|
||||||
|
// Мениск
|
||||||
|
s += `<line x1="${x+wall+1}" y1="${y + innerH - liqH + wall/2}" x2="${x+w-wall-1}" y2="${y + innerH - liqH + wall/2}" stroke="${liquidColor}" stroke-width="1.6" opacity="0.95"/>`;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Симуляция теплопроводности по стержню (одномерное уравнение тепла) ===
|
||||||
|
// Возвращает объект с .step(dt), .render(x, y, w, h), .reset(), .setTHot(t), .setTCold(t)
|
||||||
|
function createHeatBar(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
const N = opts.N || 30;
|
||||||
|
const tHot = opts.tHot != null ? opts.tHot : 200;
|
||||||
|
const tCold = opts.tCold != null ? opts.tCold : 0;
|
||||||
|
const alpha = opts.alpha || 0.25; // коэф. диффузии (отн. ед.)
|
||||||
|
const T = new Array(N);
|
||||||
|
for (let i = 0; i < N; i++) T[i] = tCold;
|
||||||
|
return {
|
||||||
|
N: N, alpha: alpha, T: T,
|
||||||
|
_tHot: tHot, _tCold: tCold,
|
||||||
|
setTHot(v) { this._tHot = v; },
|
||||||
|
setTCold(v) { this._tCold = v; },
|
||||||
|
reset() {
|
||||||
|
for (let i = 0; i < this.N; i++) this.T[i] = this._tCold;
|
||||||
|
},
|
||||||
|
step(dt) {
|
||||||
|
// Конечно-разностное уравнение теплопроводности с граничными условиями Дирихле.
|
||||||
|
const Tnew = new Array(this.N);
|
||||||
|
Tnew[0] = this._tHot;
|
||||||
|
Tnew[this.N - 1] = this._tCold;
|
||||||
|
for (let i = 1; i < this.N - 1; i++) {
|
||||||
|
Tnew[i] = this.T[i] + this.alpha * dt * (this.T[i-1] - 2*this.T[i] + this.T[i+1]);
|
||||||
|
}
|
||||||
|
this.T = Tnew;
|
||||||
|
},
|
||||||
|
render(x, y, w, h) {
|
||||||
|
const segW = w / this.N;
|
||||||
|
const tMin = Math.min(this._tCold, this._tHot);
|
||||||
|
const tMax = Math.max(this._tCold, this._tHot);
|
||||||
|
let s = '';
|
||||||
|
for (let i = 0; i < this.N; i++) {
|
||||||
|
const c = tempColor(this.T[i], tMin, tMax);
|
||||||
|
s += `<rect x="${(x + i*segW).toFixed(1)}" y="${y}" width="${(segW + 0.5).toFixed(1)}" height="${h}" fill="${c}"/>`;
|
||||||
|
}
|
||||||
|
// Контур стержня
|
||||||
|
s += `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="#0f172a" stroke-width="1.5"/>`;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// === График фазовых переходов T(t) с горизонтальными плато ===
|
||||||
|
// points: массив сегментов [{tStart, tEnd, Tstart, Tend, label?}]
|
||||||
|
// W, H, pad — размеры графика; tMaxAll, TminAll, TmaxAll — диапазоны
|
||||||
|
function phaseGraphTT(W, H, pad, points, tMaxAll, TminAll, TmaxAll) {
|
||||||
|
const toX = t => pad + (W - 2*pad) * t / tMaxAll;
|
||||||
|
const toY = T => H - pad - (H - 2*pad) * (T - TminAll) / (TmaxAll - TminAll);
|
||||||
|
let s = '';
|
||||||
|
// Оси
|
||||||
|
s += `<line x1="${pad}" y1="${H-pad}" x2="${W-pad}" y2="${H-pad}" stroke="#0f172a" stroke-width="1.5"/>`;
|
||||||
|
s += `<line x1="${pad}" y1="${pad}" x2="${pad}" y2="${H-pad}" stroke="#0f172a" stroke-width="1.5"/>`;
|
||||||
|
s += `<text x="${W-pad+4}" y="${H-pad+4}" font-family="Inter,sans-serif" font-size="11" fill="#0f172a">t</text>`;
|
||||||
|
s += `<text x="${pad-4}" y="${pad-4}" text-anchor="end" font-family="Inter,sans-serif" font-size="11" fill="#0f172a">T,°C</text>`;
|
||||||
|
// Сегменты
|
||||||
|
let d = '';
|
||||||
|
let first = true;
|
||||||
|
for (const seg of points) {
|
||||||
|
if (first) { d += `M ${toX(seg.tStart).toFixed(1)} ${toY(seg.Tstart).toFixed(1)} `; first = false; }
|
||||||
|
d += `L ${toX(seg.tEnd).toFixed(1)} ${toY(seg.Tend).toFixed(1)} `;
|
||||||
|
}
|
||||||
|
s += `<path d="${d}" stroke="#dc2626" stroke-width="2.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>`;
|
||||||
|
// Подписи сегментов
|
||||||
|
for (const seg of points) {
|
||||||
|
if (seg.label) {
|
||||||
|
const mx = (toX(seg.tStart) + toX(seg.tEnd)) / 2;
|
||||||
|
const my = (toY(seg.Tstart) + toY(seg.Tend)) / 2 - 8;
|
||||||
|
s += `<text x="${mx.toFixed(1)}" y="${my.toFixed(1)}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="600" fill="#475569">${seg.label}</text>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { svg: s, toX: toX, toY: toY };
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Электронные хелперы для электрических задач ===
|
||||||
|
// Параллельное и последовательное сопротивление
|
||||||
|
function Rseries() {
|
||||||
|
let R = 0;
|
||||||
|
for (let i = 0; i < arguments.length; i++) R += arguments[i];
|
||||||
|
return R;
|
||||||
|
}
|
||||||
|
function Rparallel() {
|
||||||
|
let inv = 0;
|
||||||
|
for (let i = 0; i < arguments.length; i++) {
|
||||||
|
if (arguments[i] > 0) inv += 1 / arguments[i];
|
||||||
|
}
|
||||||
|
return inv > 0 ? 1 / inv : Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
// === Экспорт ===
|
// === Экспорт ===
|
||||||
window.PHYS = {
|
window.PHYS = {
|
||||||
|
tempColor: tempColor,
|
||||||
|
thermometer: thermometer,
|
||||||
|
calorimeter: calorimeter,
|
||||||
|
createHeatBar: createHeatBar,
|
||||||
|
phaseGraphTT: phaseGraphTT,
|
||||||
|
Rseries: Rseries,
|
||||||
|
Rparallel: Rparallel,
|
||||||
drawArrow: drawArrow,
|
drawArrow: drawArrow,
|
||||||
fieldLinesPointCharge: fieldLinesPointCharge,
|
fieldLinesPointCharge: fieldLinesPointCharge,
|
||||||
chargeMark: chargeMark,
|
chargeMark: chargeMark,
|
||||||
|
|||||||
@@ -0,0 +1,668 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<title>Физика 8 · Глава 1 · «Тепловые явления»</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
|
||||||
|
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/g3d.js" defer></script>
|
||||||
|
<script src="/js/phys.js" defer></script>
|
||||||
|
<script src="/js/optics.js" defer></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#fef2f2; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
|
||||||
|
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
|
||||||
|
--pri:#7c3aed; --pri2:#5b21b6; --pri-soft:#ede9fe;
|
||||||
|
--acc:#a78bfa; --acc2:#7c3aed; --acc-soft:#ede9fe;
|
||||||
|
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
|
||||||
|
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
|
||||||
|
}
|
||||||
|
.dark{--bg:#0a0a0e; --card:#13120a; --card-soft:#18160a; --text:#fef9e7; --ink:#fef9e7; --muted:#a39070; --border:#2a2512}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||||
|
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
|
||||||
|
button,input,select,textarea{font-family:inherit;font-size:inherit}
|
||||||
|
button{cursor:pointer;border:0;background:transparent;color:inherit}
|
||||||
|
a{color:inherit;text-decoration:none}
|
||||||
|
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
|
||||||
|
|
||||||
|
.hdr{position:relative;background:linear-gradient(110deg,#7f1d1d 0%,#dc2626 55%,#fca5a5 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.2);min-height:130px}
|
||||||
|
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||||||
|
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
|
||||||
|
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
|
||||||
|
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||||
|
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
|
||||||
|
.hdr-btn:hover{background:rgba(255,255,255,.24)}
|
||||||
|
|
||||||
|
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
|
||||||
|
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
|
||||||
|
.col-main{min-width:0}
|
||||||
|
|
||||||
|
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
|
||||||
|
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
|
||||||
|
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
|
||||||
|
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
|
||||||
|
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
|
||||||
|
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
|
||||||
|
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
|
||||||
|
.hero-progress{flex:1;min-width:200px;max-width:280px}
|
||||||
|
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
|
||||||
|
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
|
||||||
|
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
|
||||||
|
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
|
||||||
|
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
|
||||||
|
|
||||||
|
.psel{margin-bottom:24px}
|
||||||
|
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
|
||||||
|
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
|
||||||
|
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
|
||||||
|
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
|
||||||
|
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
|
||||||
|
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
|
||||||
|
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
|
||||||
|
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
|
||||||
|
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
|
||||||
|
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
|
||||||
|
.psel-card.final{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft))}
|
||||||
|
.psel-card.final .psel-num{color:var(--warn)}
|
||||||
|
|
||||||
|
.sec[id="sec-p1"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p2"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p3"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p4"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p5"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p6"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p7"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p8"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p9"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p10"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-p11"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
.sec[id="sec-final1"]{ --sec-acc:#dc2626; --sec-acc-d:#991b1b; --sec-acc-soft:#fee2e2; }
|
||||||
|
|
||||||
|
.sec{display:none;position:relative;animation:fadeIn .35s ease}
|
||||||
|
.sec.active{display:block}
|
||||||
|
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||||||
|
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
|
||||||
|
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
|
||||||
|
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
|
||||||
|
|
||||||
|
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.04);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
|
||||||
|
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(0,0,0,.08)}
|
||||||
|
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
|
||||||
|
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
|
||||||
|
.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}
|
||||||
|
.card-icon .ic{width:18px;height:18px}
|
||||||
|
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
|
||||||
|
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
|
||||||
|
.card-body{font-size:.94rem;line-height:1.65}
|
||||||
|
.card-body p{margin-bottom:8px}
|
||||||
|
.card-body p:last-child{margin-bottom:0}
|
||||||
|
|
||||||
|
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
|
||||||
|
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
|
||||||
|
.btn:active{transform:scale(.96)}
|
||||||
|
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
|
||||||
|
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
|
||||||
|
|
||||||
|
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
|
||||||
|
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
|
||||||
|
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
|
||||||
|
|
||||||
|
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
|
||||||
|
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
|
||||||
|
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||||
|
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
|
||||||
|
.sidecard-row b{color:var(--pri);font-weight:700}
|
||||||
|
.sidecard-row:last-child{margin-bottom:0}
|
||||||
|
@media(max-width:980px){.col-side{position:static;max-height:none}}
|
||||||
|
|
||||||
|
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
|
||||||
|
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
|
||||||
|
.xp-bar{height:9px;background:rgba(0,0,0,.10);border-radius:6px;overflow:hidden;margin:7px 0}
|
||||||
|
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
|
||||||
|
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
|
||||||
|
|
||||||
|
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
|
||||||
|
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
|
||||||
|
|
||||||
|
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
|
||||||
|
.ach-popup.show{display:flex}
|
||||||
|
|
||||||
|
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
|
||||||
|
.col-side-backdrop.show{display:block}
|
||||||
|
@media(max-width:980px){
|
||||||
|
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
|
||||||
|
.col-side.open{transform:none}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
|
||||||
|
.search-modal.show{display:flex}
|
||||||
|
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
|
||||||
|
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
|
||||||
|
.search-results{flex:1;overflow-y:auto;padding:6px 0}
|
||||||
|
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
|
||||||
|
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
|
||||||
|
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
|
||||||
|
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
|
||||||
|
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
|
||||||
|
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
|
||||||
|
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
|
||||||
|
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
|
||||||
|
|
||||||
|
.sec{transition:opacity .25s}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Физика 8 · Глава 1</h1>
|
||||||
|
<div class="hdr-sub">Внутренняя энергия · теплопередача · фазовые переходы</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/physics-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 8</a>
|
||||||
|
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
|
||||||
|
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><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" data-gamified></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-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-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-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" id="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
<div class="col-side-backdrop" id="col-side-backdrop"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Физика 8» · Глава 1 · «Тепловые явления» · LearnSpace</footer>
|
||||||
|
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
<div id="search-modal" class="search-modal" role="dialog">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="search-input" class="search-input" placeholder="Поиск…" autocomplete="off">
|
||||||
|
<div id="search-results" class="search-results"></div>
|
||||||
|
<div class="search-foot"><span><kbd>↑↓</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const STATE = { current:'p1', progress:{}, achievements:new Map(), xp:0, level:1 };
|
||||||
|
const TOTAL_PARAS = 12;
|
||||||
|
const _TB_SLUG = 'physics-8-ch1';
|
||||||
|
const LS_PREFIX = 'physics8_ch1';
|
||||||
|
const LS_XP = 'physics8_xp';
|
||||||
|
|
||||||
|
const PARAS = [
|
||||||
|
{ id:'p1', num:'\u00a7 1', name:'Внутренняя энергия', sub:'$U$ зависит от $T$' },
|
||||||
|
{ id:'p2', num:'\u00a7 2', name:'Способы изменения внутренней энергии', sub:'Работа и теплопередача' },
|
||||||
|
{ id:'p3', num:'\u00a7 3', name:'Теплопроводность', sub:'Передача без переноса в-ва' },
|
||||||
|
{ id:'p4', num:'\u00a7 4', name:'Конвекция', sub:'Перенос потоками' },
|
||||||
|
{ id:'p5', num:'\u00a7 5', name:'Излучение', sub:'Тепловое излучение' },
|
||||||
|
{ id:'p6', num:'\u00a7 6', name:'Расчёт количества теплоты при нагревании и охлаждении. Удельная теплоёмкость', sub:'$Q = cm\\Delta T$' },
|
||||||
|
{ id:'p7', num:'\u00a7 7', name:'Горение. Удельная теплота сгорания топлива', sub:'$Q = qm$' },
|
||||||
|
{ id:'p8', num:'\u00a7 8', name:'Плавление и кристаллизация', sub:'$T_{пл}$, графики $T(t)$' },
|
||||||
|
{ id:'p9', num:'\u00a7 9', name:'Удельная теплота плавления и кристаллизации', sub:'$Q = \\lambda m$' },
|
||||||
|
{ id:'p10', num:'\u00a7 10', name:'Испарение жидкостей. Факторы, влияющие на скорость испарения', sub:'Зависит от $T$, $S$' },
|
||||||
|
{ id:'p11', num:'\u00a7 11', name:'Кипение жидкостей. Удельная теплота парообразования', sub:'$Q = Lm$' },
|
||||||
|
{ id:'final1', num:'\u2605', name:'Финал главы', sub:'Итоги · 7 боссов', final:true }
|
||||||
|
];
|
||||||
|
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
|
||||||
|
|
||||||
|
const ACH_LABELS = {
|
||||||
|
start:"Начало главы 1!",
|
||||||
|
p1_done:"Внутренняя энергия освоен!",
|
||||||
|
p2_done:"Способы изменения внутренней энергии освоен!",
|
||||||
|
p3_done:"Теплопроводность освоен!",
|
||||||
|
p4_done:"Конвекция освоен!",
|
||||||
|
p5_done:"Излучение освоен!",
|
||||||
|
p6_done:"Расчёт количества теплоты при нагревании и охлаждении. Удельная теплоёмкость освоен!",
|
||||||
|
p7_done:"Горение. Удельная теплота сгорания топлива освоен!",
|
||||||
|
p8_done:"Плавление и кристаллизация освоен!",
|
||||||
|
p9_done:"Удельная теплота плавления и кристаллизации освоен!",
|
||||||
|
p10_done:"Испарение жидкостей. Факторы, влияющие на скорость испарения освоен!",
|
||||||
|
p11_done:"Кипение жидкостей. Удельная теплота парообразования освоен!",
|
||||||
|
ch1_done:"Глава 1 пройдена!"
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIDEBARS = {
|
||||||
|
p1:{title:"Шпаргалка § 1",rows:[["В разработке","Phase 1 Wave 1"]]},
|
||||||
|
p2:{title:"Шпаргалка § 2",rows:[["В разработке","Phase 1 Wave 1"]]},
|
||||||
|
p3:{title:"Шпаргалка § 3",rows:[["В разработке","Phase 1 Wave 2"]]},
|
||||||
|
p4:{title:"Шпаргалка § 4",rows:[["В разработке","Phase 1 Wave 2"]]},
|
||||||
|
p5:{title:"Шпаргалка § 5",rows:[["В разработке","Phase 1 Wave 2"]]},
|
||||||
|
p6:{title:"Шпаргалка § 6",rows:[["В разработке","Phase 1 Wave 3"]]},
|
||||||
|
p7:{title:"Шпаргалка § 7",rows:[["В разработке","Phase 1 Wave 3"]]},
|
||||||
|
p8:{title:"Шпаргалка § 8",rows:[["В разработке","Phase 1 Wave 4"]]},
|
||||||
|
p9:{title:"Шпаргалка § 9",rows:[["В разработке","Phase 1 Wave 4"]]},
|
||||||
|
p10:{title:"Шпаргалка § 10",rows:[["В разработке","Phase 1 Wave 5"]]},
|
||||||
|
p11:{title:"Шпаргалка § 11",rows:[["В разработке","Phase 1 Wave 5"]]},
|
||||||
|
final1:{title:"Шпаргалка ★",rows:[["В разработке","Phase 1 Wave 5"]]}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TIPS=[
|
||||||
|
{sec:'p1',html:"Параграф § 1 будет реализован в Phase 1 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p2',html:"Параграф § 2 будет реализован в Phase 1 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p3',html:"Параграф § 3 будет реализован в Phase 1 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p4',html:"Параграф § 4 будет реализован в Phase 1 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p5',html:"Параграф § 5 будет реализован в Phase 1 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p6',html:"Параграф § 6 будет реализован в Phase 1 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p7',html:"Параграф § 7 будет реализован в Phase 1 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p8',html:"Параграф § 8 будет реализован в Phase 1 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p9',html:"Параграф § 9 будет реализован в Phase 1 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p10',html:"Параграф § 10 будет реализован в Phase 1 Wave 5. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p11',html:"Параграф § 11 будет реализован в Phase 1 Wave 5. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'final1',html:"Параграф ★ будет реализован в Phase 1 Wave 5. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."}
|
||||||
|
];
|
||||||
|
|
||||||
|
const BUILDERS = {
|
||||||
|
p1: ()=>{ const box=document.getElementById('p1-body'); box.innerHTML = buildStub('p1', 'Внутренняя энергия', 'Phase 1 Wave 1') + secNavFor('p1') + readButton('p1'); renderMath(box); wireReadBtn('p1'); },
|
||||||
|
p2: ()=>{ const box=document.getElementById('p2-body'); box.innerHTML = buildStub('p2', 'Способы изменения внутренней энергии', 'Phase 1 Wave 1') + secNavFor('p2') + readButton('p2'); renderMath(box); wireReadBtn('p2'); },
|
||||||
|
p3: ()=>{ const box=document.getElementById('p3-body'); box.innerHTML = buildStub('p3', 'Теплопроводность', 'Phase 1 Wave 2') + secNavFor('p3') + readButton('p3'); renderMath(box); wireReadBtn('p3'); },
|
||||||
|
p4: ()=>{ const box=document.getElementById('p4-body'); box.innerHTML = buildStub('p4', 'Конвекция', 'Phase 1 Wave 2') + secNavFor('p4') + readButton('p4'); renderMath(box); wireReadBtn('p4'); },
|
||||||
|
p5: ()=>{ const box=document.getElementById('p5-body'); box.innerHTML = buildStub('p5', 'Излучение', 'Phase 1 Wave 2') + secNavFor('p5') + readButton('p5'); renderMath(box); wireReadBtn('p5'); },
|
||||||
|
p6: ()=>{ const box=document.getElementById('p6-body'); box.innerHTML = buildStub('p6', 'Расчёт количества теплоты при нагревании и охлаждении. Удельная теплоёмкость', 'Phase 1 Wave 3') + secNavFor('p6') + readButton('p6'); renderMath(box); wireReadBtn('p6'); },
|
||||||
|
p7: ()=>{ const box=document.getElementById('p7-body'); box.innerHTML = buildStub('p7', 'Горение. Удельная теплота сгорания топлива', 'Phase 1 Wave 3') + secNavFor('p7') + readButton('p7'); renderMath(box); wireReadBtn('p7'); },
|
||||||
|
p8: ()=>{ const box=document.getElementById('p8-body'); box.innerHTML = buildStub('p8', 'Плавление и кристаллизация', 'Phase 1 Wave 4') + secNavFor('p8') + readButton('p8'); renderMath(box); wireReadBtn('p8'); },
|
||||||
|
p9: ()=>{ const box=document.getElementById('p9-body'); box.innerHTML = buildStub('p9', 'Удельная теплота плавления и кристаллизации', 'Phase 1 Wave 4') + secNavFor('p9') + readButton('p9'); renderMath(box); wireReadBtn('p9'); },
|
||||||
|
p10: ()=>{ const box=document.getElementById('p10-body'); box.innerHTML = buildStub('p10', 'Испарение жидкостей. Факторы, влияющие на скорость испарения', 'Phase 1 Wave 5') + secNavFor('p10') + readButton('p10'); renderMath(box); wireReadBtn('p10'); },
|
||||||
|
p11: ()=>{ const box=document.getElementById('p11-body'); box.innerHTML = buildStub('p11', 'Кипение жидкостей. Удельная теплота парообразования', 'Phase 1 Wave 5') + secNavFor('p11') + readButton('p11'); renderMath(box); wireReadBtn('p11'); },
|
||||||
|
final1: ()=>{ const box=document.getElementById('final1-body'); box.innerHTML = buildStub('final1', 'Финал главы', 'Phase 1 Wave 5') + secNavFor('final1') + readButton('final1'); renderMath(box); wireReadBtn('final1'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
|
||||||
|
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
|
||||||
|
|
||||||
|
function loadProgress(){
|
||||||
|
try{
|
||||||
|
const s=localStorage.getItem(LS_PREFIX+'_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
|
||||||
|
const a=localStorage.getItem(LS_PREFIX+'_achievements');
|
||||||
|
if(a){ const p=JSON.parse(a); if(Array.isArray(p)) p.forEach(id=>STATE.achievements.set(id, ACH_LABELS[id]||id)); else if(p&&typeof p==='object'){ for(const[id,t] of Object.entries(p)) STATE.achievements.set(id,(t&&t!==id)?t:(ACH_LABELS[id]||id)); } }
|
||||||
|
STATE.xp=+(localStorage.getItem(LS_XP)||0); STATE.level=calcLevel(STATE.xp);
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
function saveProgress(){
|
||||||
|
try{
|
||||||
|
localStorage.setItem(LS_PREFIX+'_progress', JSON.stringify(STATE.progress));
|
||||||
|
localStorage.setItem(LS_PREFIX+'_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
|
||||||
|
localStorage.setItem(LS_XP, String(STATE.xp));
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
function bumpProgress(key, delta){
|
||||||
|
STATE.progress[key]=Math.max(0,Math.min(100,(STATE.progress[key]||0)+delta));
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
if(STATE.progress[key]>=50) markParaRead(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _markedRead=new Set();
|
||||||
|
let _pendingProgressBody=null, _progressTimer=null;
|
||||||
|
function _flushProgress(){
|
||||||
|
const body=_pendingProgressBody; _pendingProgressBody=null; if(!body) return;
|
||||||
|
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||||||
|
fetch('/api/textbooks/'+_TB_SLUG+'/progress',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tok},body:JSON.stringify(body),keepalive:true}).catch(()=>{});
|
||||||
|
}
|
||||||
|
function _queueProgress(patch){ _pendingProgressBody=Object.assign(_pendingProgressBody||{},patch); if(_progressTimer) clearTimeout(_progressTimer); _progressTimer=setTimeout(_flushProgress, 600); }
|
||||||
|
function markLastPara(id){ _queueProgress({last_para:id}); }
|
||||||
|
function markParaRead(id){ if(_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({mark_read:id}); }
|
||||||
|
window.addEventListener('beforeunload', _flushProgress);
|
||||||
|
function loadServerReadState(){
|
||||||
|
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||||||
|
fetch('/api/textbooks/'+_TB_SLUG,{headers:{'Authorization':'Bearer '+tok}}).then(r=>r.ok?r.json():null).then(d=>{
|
||||||
|
if(!d||!d.progress) return;
|
||||||
|
(d.progress.read||[]).forEach(k=>{_markedRead.add(k); if((STATE.progress[k]||0)<50) STATE.progress[k]=100;});
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
}).catch(()=>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addXp(n,src){
|
||||||
|
if(!n) return;
|
||||||
|
const prev=STATE.level; STATE.xp=Math.max(0,(STATE.xp||0)+n); STATE.level=calcLevel(STATE.xp);
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
if(window.LS&&window.LS.xp) window.LS.xp.add(n, LS_PREFIX+'-'+(src||'misc'));
|
||||||
|
if(STATE.level>prev){
|
||||||
|
const pop=document.getElementById('ach-popup');
|
||||||
|
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshProgressUI(){
|
||||||
|
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
|
||||||
|
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
|
||||||
|
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
|
||||||
|
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
|
||||||
|
const xpBadge=document.getElementById('hero-xp-badge');
|
||||||
|
if(xpBadge){ xpBadge.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
|
||||||
|
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
|
||||||
|
}
|
||||||
|
|
||||||
|
function achievement(id,text){
|
||||||
|
if(STATE.achievements.has(id)) return;
|
||||||
|
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
|
||||||
|
const pop=document.getElementById('ach-popup');
|
||||||
|
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
|
||||||
|
addXp(20,'ach-'+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParaSelector(){
|
||||||
|
const g=document.getElementById('psel-grid'); g.innerHTML='';
|
||||||
|
PARAS.forEach(p=>{
|
||||||
|
const card=document.createElement('div');
|
||||||
|
card.className='psel-card'+(p.final?' final':'');
|
||||||
|
card.dataset.id=p.id; card.dataset.progCard=p.id;
|
||||||
|
card.innerHTML='<div class="psel-num">'+p.num+'</div><div class="psel-name">'+p.name+'</div><div class="psel-prog"><div class="psel-prog-fill"></div></div>';
|
||||||
|
card.addEventListener('click', ()=>goTo(p.id));
|
||||||
|
g.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILT=new Set();
|
||||||
|
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
|
||||||
|
function goTo(id){
|
||||||
|
STATE.current=id; ensureBuilt(id);
|
||||||
|
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
|
||||||
|
const el=document.getElementById('sec-'+id); if(el) el.classList.add('active');
|
||||||
|
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id===id));
|
||||||
|
buildSidebar(id);
|
||||||
|
window.scrollTo({top:0,behavior:'smooth'});
|
||||||
|
if((STATE.progress[id]||0)<10) bumpProgress(id, 10);
|
||||||
|
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
|
||||||
|
markLastPara(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSidebar(id){
|
||||||
|
const box=document.getElementById('sidebar-content');
|
||||||
|
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
|
||||||
|
let html='';
|
||||||
|
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||||
|
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||||
|
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||||
|
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
|
||||||
|
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||||
|
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||||
|
html+='</div>';
|
||||||
|
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
|
||||||
|
if(tip){
|
||||||
|
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
|
||||||
|
}
|
||||||
|
if(STATE.achievements.size>0){
|
||||||
|
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
|
||||||
|
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ '+text+'</div>'; });
|
||||||
|
html+='</div>';
|
||||||
|
}
|
||||||
|
box.innerHTML=html;
|
||||||
|
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTheme(){
|
||||||
|
const t=localStorage.getItem(LS_PREFIX+'_theme')||'light';
|
||||||
|
if(t==='dark') document.documentElement.classList.add('dark');
|
||||||
|
document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';
|
||||||
|
document.getElementById('theme-btn').addEventListener('click', ()=>{
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
const dark=document.documentElement.classList.contains('dark');
|
||||||
|
localStorage.setItem(LS_PREFIX+'_theme', dark?'dark':'light');
|
||||||
|
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
|
||||||
|
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'✓ Верно!':'✗ Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
|
||||||
|
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
|
||||||
|
function ipow(base, exp){ let r=1; for(let i=0;i<Math.abs(exp);i++) r*=base; return exp<0 ? 1/r : r; }
|
||||||
|
function gcd(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ const t=b; b=a%b; a=t; } return a||1; }
|
||||||
|
function makeCard(kind, title, num, body){
|
||||||
|
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
|
||||||
|
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === SVG-хелперы === */
|
||||||
|
function axes2D(W, H, pad, xmin, xmax, ymin, ymax){
|
||||||
|
const ux = (W - 2*pad) / (xmax - xmin);
|
||||||
|
const uy = (H - 2*pad) / (ymax - ymin);
|
||||||
|
const toX = v => pad + (v - xmin) * ux;
|
||||||
|
const toY = v => H - pad - (v - ymin) * uy;
|
||||||
|
let g = '';
|
||||||
|
g += '<g stroke="#e5e7eb" stroke-width="1">';
|
||||||
|
for (let x = Math.ceil(xmin); x <= xmax; x++){
|
||||||
|
g += '<line x1="'+toX(x)+'" y1="'+pad+'" x2="'+toX(x)+'" y2="'+(H-pad)+'"/>';
|
||||||
|
}
|
||||||
|
for (let y = Math.ceil(ymin); y <= ymax; y++){
|
||||||
|
g += '<line x1="'+pad+'" y1="'+toY(y)+'" x2="'+(W-pad)+'" y2="'+toY(y)+'"/>';
|
||||||
|
}
|
||||||
|
g += '</g>';
|
||||||
|
const y0 = toY(0), x0 = toX(0);
|
||||||
|
g += '<line x1="'+pad+'" y1="'+y0+'" x2="'+(W-pad)+'" y2="'+y0+'" stroke="#0f172a" stroke-width="1.5"/>';
|
||||||
|
g += '<line x1="'+x0+'" y1="'+pad+'" x2="'+x0+'" y2="'+(H-pad)+'" stroke="#0f172a" stroke-width="1.5"/>';
|
||||||
|
g += '<text x="'+(W-pad+2)+'" y="'+(y0-4)+'" font-size="11" fill="#0f172a">x</text>';
|
||||||
|
g += '<text x="'+(x0+4)+'" y="'+(pad-2)+'" font-size="11" fill="#0f172a">y</text>';
|
||||||
|
g += '<g font-size="10" fill="#64748b">';
|
||||||
|
for (let x = Math.ceil(xmin); x <= xmax; x++){
|
||||||
|
if (x !== 0) g += '<text x="'+(toX(x)-3)+'" y="'+(y0+12)+'">'+x+'</text>';
|
||||||
|
}
|
||||||
|
for (let y = Math.ceil(ymin); y <= ymax; y++){
|
||||||
|
if (y !== 0) g += '<text x="'+(x0+4)+'" y="'+(toY(y)+3)+'">'+y+'</text>';
|
||||||
|
}
|
||||||
|
g += '<text x="'+(x0+4)+'" y="'+(y0+12)+'">0</text>';
|
||||||
|
g += '</g>';
|
||||||
|
return { content: g, toX, toY, ux, uy };
|
||||||
|
}
|
||||||
|
function plotFunc(f, xmin, xmax, toX, toY, color, N){
|
||||||
|
N = N || 200;
|
||||||
|
let d = '';
|
||||||
|
let prevValid = false;
|
||||||
|
for (let i = 0; i <= N; i++){
|
||||||
|
const x = xmin + (xmax - xmin) * i / N;
|
||||||
|
let y;
|
||||||
|
try { y = f(x); } catch(e){ y = NaN; }
|
||||||
|
if (!isFinite(y) || isNaN(y) || y < -1e4 || y > 1e4){ prevValid = false; continue; }
|
||||||
|
d += (prevValid ? ' L' : ' M') + toX(x).toFixed(2) + ',' + toY(y).toFixed(2);
|
||||||
|
prevValid = true;
|
||||||
|
}
|
||||||
|
return '<path d="'+d+'" stroke="'+color+'" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
}
|
||||||
|
function pointWithDrop(x, fx, toX, toY, color, label){
|
||||||
|
const px = toX(x), py = toY(fx);
|
||||||
|
let s = '';
|
||||||
|
s += '<line x1="'+px+'" y1="'+py+'" x2="'+px+'" y2="'+toY(0)+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
|
||||||
|
s += '<line x1="'+px+'" y1="'+py+'" x2="'+toX(0)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
|
||||||
|
s += '<circle cx="'+px+'" cy="'+py+'" r="4.5" fill="'+color+'" stroke="#fff" stroke-width="2"/>';
|
||||||
|
if (label){
|
||||||
|
s += '<text x="'+(px+8)+'" y="'+(py-8)+'" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="'+color+'">'+label+'</text>';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
function asymptote(orientation, value, toX, toY, xmin, xmax, ymin, ymax, color){
|
||||||
|
color = color || '#94a3b8';
|
||||||
|
if (orientation === 'h'){
|
||||||
|
const y = toY(value);
|
||||||
|
return '<line x1="'+toX(xmin)+'" y1="'+y+'" x2="'+toX(xmax)+'" y2="'+y+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
|
||||||
|
} else {
|
||||||
|
const x = toX(value);
|
||||||
|
return '<line x1="'+x+'" y1="'+toY(ymin)+'" x2="'+x+'" y2="'+toY(ymax)+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function snapToValue(value, snapPoints, tolerance){
|
||||||
|
tolerance = tolerance || 0.1;
|
||||||
|
for (const sp of snapPoints){
|
||||||
|
if (Math.abs(value - sp) < tolerance) return sp;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function rightAngleMark(V, uIn, wIn, s){
|
||||||
|
s = s || 9;
|
||||||
|
const p1 = {x: V.x + s*uIn.x, y: V.y + s*uIn.y};
|
||||||
|
const c = {x: p1.x + s*wIn.x, y: p1.y + s*wIn.y};
|
||||||
|
const p2 = {x: V.x + s*wIn.x, y: V.y + s*wIn.y};
|
||||||
|
return p1.x+','+p1.y+' '+c.x+','+c.y+' '+p2.x+','+p2.y;
|
||||||
|
}
|
||||||
|
function angleArcAuto(V, uA, uB, R){
|
||||||
|
const sA = {x: V.x + R*uA.x, y: V.y + R*uA.y};
|
||||||
|
const eB = {x: V.x + R*uB.x, y: V.y + R*uB.y};
|
||||||
|
const cross = uA.x*uB.y - uA.y*uB.x;
|
||||||
|
const sweep = cross > 0 ? 1 : 0;
|
||||||
|
return 'M'+sA.x+','+sA.y+' A'+R+','+R+' 0 0,'+sweep+' '+eB.x+','+eB.y;
|
||||||
|
}
|
||||||
|
function unitVec(p1, p2){
|
||||||
|
const dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||||
|
const len = Math.sqrt(dx*dx + dy*dy) || 1;
|
||||||
|
return {x: dx/len, y: dy/len};
|
||||||
|
}
|
||||||
|
function deg2rad(d){ return d * Math.PI / 180; }
|
||||||
|
|
||||||
|
const ICONS = {
|
||||||
|
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
|
||||||
|
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
||||||
|
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
|
||||||
|
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
|
||||||
|
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
|
||||||
|
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
|
||||||
|
};
|
||||||
|
|
||||||
|
function secNavFor(curId){
|
||||||
|
const idx = PARAS.findIndex(p => p.id === curId);
|
||||||
|
const prev = idx > 0 ? PARAS[idx-1].id : null;
|
||||||
|
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
|
||||||
|
return secNav(prev, next);
|
||||||
|
}
|
||||||
|
function secNav(prev, next){
|
||||||
|
function lbl(id){ if(!id) return ''; const p=PARAS.find(x=>x.id===id); return p?p.num:id; }
|
||||||
|
let h='<div class="sec-nav">';
|
||||||
|
h+=prev?'<button class="btn" onclick="goTo(\''+prev+'\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> '+lbl(prev)+'</button>':'<span></span>';
|
||||||
|
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+lbl(next)+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
|
||||||
|
h+='</div>'; return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readButton(paraId){
|
||||||
|
const p = PARAS.find(x => x.id === paraId);
|
||||||
|
const labelTail = p && p.final ? 'финал' : (p ? p.num : '?');
|
||||||
|
return '<div style="margin-top:18px;display:flex;justify-content:center">'
|
||||||
|
+'<button class="btn primary" id="'+paraId+'-read-btn">'
|
||||||
|
+'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
|
||||||
|
+' Я прочитал \u2014 '+labelTail+' (+10 XP)'
|
||||||
|
+'</button></div>';
|
||||||
|
}
|
||||||
|
function wireReadBtn(paraId){
|
||||||
|
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
|
||||||
|
btn.addEventListener('click', ()=>{
|
||||||
|
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
|
||||||
|
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
|
||||||
|
const aId = paraId+'_done';
|
||||||
|
if(ACH_LABELS[aId]) achievement(aId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSorter(cfg){
|
||||||
|
const placed = {}; const pool = document.getElementById(cfg.poolId); const scope = document.querySelector(cfg.scopeSelector);
|
||||||
|
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
|
||||||
|
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
|
||||||
|
let armed = null;
|
||||||
|
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
|
||||||
|
function attach(elm,itId){ let ghost=null,dragging=false,sx=0,sy=0; elm.addEventListener('pointerdown',ev=>{ if(ev.button!==undefined&&ev.button!==0) return;
|
||||||
|
ev.preventDefault(); if(ev.target.classList&&ev.target.classList.contains('dnd-x')){ ev.stopPropagation(); if(placed[itId]){delete placed[itId];render();}else if(armed===itId){armed=null;render();} return; } sx=ev.clientX;sy=ev.clientY; const r=elm.getBoundingClientRect(); const ox=ev.clientX-r.left,oy=ev.clientY-r.top; try{elm.setPointerCapture(ev.pointerId);}catch(e){} function onMove(e){ const dx=e.clientX-sx,dy=e.clientY-sy; if(!dragging&&Math.hypot(dx,dy)>8){ dragging=true; ghost=elm.cloneNode(true); ghost.classList.remove('armed'); ghost.style.cssText='position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:'+r.width+'px;left:'+(e.clientX-ox)+'px;top:'+(e.clientY-oy)+'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if(dragging&&ghost){ ghost.style.left=(e.clientX-ox)+'px';ghost.style.top=(e.clientY-oy)+'px'; const under=document.elementsFromPoint(e.clientX,e.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); const tgt=under.find(n=>n.classList&&(n.classList.contains('drop-box')||n.classList.contains('dnd-pool'))); if(tgt)tgt.classList.add('over'); } } function onUp(e){ elm.removeEventListener('pointermove',onMove);elm.removeEventListener('pointerup',onUp);elm.removeEventListener('pointercancel',onUp);elm.classList.remove('dragging'); if(ghost){ghost.remove();ghost=null;} scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); if(dragging){ const under=document.elementsFromPoint(e.clientX,e.clientY); const box=under.find(n=>n.classList&&n.classList.contains('drop-box')); const pl=under.find(n=>n.classList&&n.classList.contains('dnd-pool')); if(box){const di=box.querySelector('[data-cat]');if(di){placed[itId]=di.dataset.cat;armed=null;render();return;}}else if(pl){delete placed[itId];armed=null;render();return;} }else{ if(placed[itId]){delete placed[itId];armed=null;render();}else{armed=(armed===itId)?null:itId;render();} } dragging=false; } elm.addEventListener('pointermove',onMove);elm.addEventListener('pointerup',onUp);elm.addEventListener('pointercancel',onUp); }); }
|
||||||
|
function attachBoxTaps(){ scope.querySelectorAll('.drop-box').forEach(box=>{ box.addEventListener('click',ev=>{ if(!armed)return; if(ev.target.closest('.dnd-chip'))return; const di=box.querySelector('[data-cat]'); if(di){placed[armed]=di.dataset.cat;armed=null;render();} }); }); }
|
||||||
|
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); cfg.cats.forEach(cat=>{const box=scope.querySelector('.drop-items[data-cat="'+cat+'"]');if(!box)return;box.innerHTML='';cfg.items.forEach(it=>{if(placed[it.id]!==cat)return;box.appendChild(buildChip(it,true));});}); if(window.renderMathInElement)try{renderMath(scope);}catch(_){} }
|
||||||
|
attachBoxTaps(); render();
|
||||||
|
return {placed,render,reset(){ for(const k in placed)delete placed[k];armed=null;render(); }};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStub(id, name, phase){
|
||||||
|
return '<div class="card" style="background:linear-gradient(135deg,var(--sec-acc-soft),var(--card));border:1.5px dashed var(--sec-acc)">'
|
||||||
|
+ '<div class="card-header"><div class="card-icon theory">'+ICONS.theory+'</div><div class="card-title">В разработке</div></div>'
|
||||||
|
+ '<div class="card-body"><p>Контент <b>'+name+'</b> будет реализован в <b>'+phase+'</b> по плану <code>PLAN_PHYSICS_8.md</code>.</p>'
|
||||||
|
+ '<p style="margin-top:8px;color:var(--muted);font-size:.9rem">Phase 0 \u2014 это каркас (skeleton). Все 4 интерактива, 3 теоретические карточки и тренажёр задач будут добавлены в волне.</p>'
|
||||||
|
+ '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Search ===== */
|
||||||
|
const SEARCH_INDEX = (function(){
|
||||||
|
const arr=[];
|
||||||
|
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
|
||||||
|
return arr;
|
||||||
|
})();
|
||||||
|
function initSearch(){
|
||||||
|
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
|
||||||
|
if(!modal||!inp||!out) return;
|
||||||
|
let cur=0,rows=[];
|
||||||
|
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
|
||||||
|
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
|
||||||
|
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'\u2026':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
|
||||||
|
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
|
||||||
|
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
|
||||||
|
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
|
||||||
|
function close(){ modal.classList.remove('show'); }
|
||||||
|
btn&&btn.addEventListener('click',open);
|
||||||
|
modal.addEventListener('click',e=>{if(e.target===modal)close();});
|
||||||
|
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
|
||||||
|
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
|
||||||
|
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSidebarToggle(){
|
||||||
|
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
|
||||||
|
if(!side||!btn) return;
|
||||||
|
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||||
|
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||||
|
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
|
||||||
|
back.addEventListener('click',close);
|
||||||
|
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||||
|
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo(PARAS[0].id);
|
||||||
|
setTimeout(()=>achievement('start'), 600);
|
||||||
|
if(window.LS&&window.LS.xp){
|
||||||
|
window.LS.xp.load().then(function(s){ if(s&&s.xp>STATE.xp){ STATE.xp=s.xp; STATE.level=calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if(STATE.current) buildSidebar(STATE.current); } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,731 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<title>Физика 8 · Глава 2 · «Электромагнитные явления»</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
|
||||||
|
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/g3d.js" defer></script>
|
||||||
|
<script src="/js/phys.js" defer></script>
|
||||||
|
<script src="/js/optics.js" defer></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#fffbeb; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
|
||||||
|
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
|
||||||
|
--pri:#7c3aed; --pri2:#5b21b6; --pri-soft:#ede9fe;
|
||||||
|
--acc:#a78bfa; --acc2:#7c3aed; --acc-soft:#ede9fe;
|
||||||
|
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
|
||||||
|
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
|
||||||
|
}
|
||||||
|
.dark{--bg:#0a0a0e; --card:#13120a; --card-soft:#18160a; --text:#fef9e7; --ink:#fef9e7; --muted:#a39070; --border:#2a2512}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||||
|
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
|
||||||
|
button,input,select,textarea{font-family:inherit;font-size:inherit}
|
||||||
|
button{cursor:pointer;border:0;background:transparent;color:inherit}
|
||||||
|
a{color:inherit;text-decoration:none}
|
||||||
|
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
|
||||||
|
|
||||||
|
.hdr{position:relative;background:linear-gradient(110deg,#78350f 0%,#d97706 55%,#fcd34d 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.2);min-height:130px}
|
||||||
|
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||||||
|
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
|
||||||
|
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
|
||||||
|
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||||
|
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
|
||||||
|
.hdr-btn:hover{background:rgba(255,255,255,.24)}
|
||||||
|
|
||||||
|
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
|
||||||
|
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
|
||||||
|
.col-main{min-width:0}
|
||||||
|
|
||||||
|
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
|
||||||
|
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
|
||||||
|
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
|
||||||
|
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
|
||||||
|
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
|
||||||
|
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
|
||||||
|
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
|
||||||
|
.hero-progress{flex:1;min-width:200px;max-width:280px}
|
||||||
|
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
|
||||||
|
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
|
||||||
|
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
|
||||||
|
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
|
||||||
|
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
|
||||||
|
|
||||||
|
.psel{margin-bottom:24px}
|
||||||
|
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
|
||||||
|
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
|
||||||
|
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
|
||||||
|
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
|
||||||
|
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
|
||||||
|
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
|
||||||
|
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
|
||||||
|
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
|
||||||
|
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
|
||||||
|
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
|
||||||
|
.psel-card.final{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft))}
|
||||||
|
.psel-card.final .psel-num{color:var(--warn)}
|
||||||
|
|
||||||
|
.sec[id="sec-p12"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p13"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p14"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p15"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p16"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p17"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p18"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p19"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p20"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p21"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p22"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p23"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p24"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p25"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p26"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p27"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p28"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p29"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p30"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-p31"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
.sec[id="sec-final2"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
|
||||||
|
|
||||||
|
.sec{display:none;position:relative;animation:fadeIn .35s ease}
|
||||||
|
.sec.active{display:block}
|
||||||
|
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||||||
|
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
|
||||||
|
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
|
||||||
|
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
|
||||||
|
|
||||||
|
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.04);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
|
||||||
|
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(0,0,0,.08)}
|
||||||
|
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
|
||||||
|
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
|
||||||
|
.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}
|
||||||
|
.card-icon .ic{width:18px;height:18px}
|
||||||
|
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
|
||||||
|
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
|
||||||
|
.card-body{font-size:.94rem;line-height:1.65}
|
||||||
|
.card-body p{margin-bottom:8px}
|
||||||
|
.card-body p:last-child{margin-bottom:0}
|
||||||
|
|
||||||
|
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
|
||||||
|
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
|
||||||
|
.btn:active{transform:scale(.96)}
|
||||||
|
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
|
||||||
|
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
|
||||||
|
|
||||||
|
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
|
||||||
|
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
|
||||||
|
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
|
||||||
|
|
||||||
|
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
|
||||||
|
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
|
||||||
|
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||||
|
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
|
||||||
|
.sidecard-row b{color:var(--pri);font-weight:700}
|
||||||
|
.sidecard-row:last-child{margin-bottom:0}
|
||||||
|
@media(max-width:980px){.col-side{position:static;max-height:none}}
|
||||||
|
|
||||||
|
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
|
||||||
|
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
|
||||||
|
.xp-bar{height:9px;background:rgba(0,0,0,.10);border-radius:6px;overflow:hidden;margin:7px 0}
|
||||||
|
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
|
||||||
|
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
|
||||||
|
|
||||||
|
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
|
||||||
|
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
|
||||||
|
|
||||||
|
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
|
||||||
|
.ach-popup.show{display:flex}
|
||||||
|
|
||||||
|
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
|
||||||
|
.col-side-backdrop.show{display:block}
|
||||||
|
@media(max-width:980px){
|
||||||
|
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
|
||||||
|
.col-side.open{transform:none}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
|
||||||
|
.search-modal.show{display:flex}
|
||||||
|
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
|
||||||
|
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
|
||||||
|
.search-results{flex:1;overflow-y:auto;padding:6px 0}
|
||||||
|
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
|
||||||
|
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
|
||||||
|
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
|
||||||
|
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
|
||||||
|
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
|
||||||
|
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
|
||||||
|
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
|
||||||
|
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
|
||||||
|
|
||||||
|
.sec{transition:opacity .25s}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Физика 8 · Глава 2</h1>
|
||||||
|
<div class="hdr-sub">Электризация · ток · закон Ома · магнитное поле</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/physics-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 8</a>
|
||||||
|
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
|
||||||
|
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><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>Заряды притягиваются и отталкиваются, образуют электрическое поле. По проводнику течёт ток, и закон Ома связывает $U$, $I$, $R$. У постоянных магнитов и проводников с током есть магнитное поле.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('p12')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 12</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" data-gamified></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel">
|
||||||
|
<div class="psel-title">Параграфы главы</div>
|
||||||
|
<div id="psel-grid" class="psel-grid"></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-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-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-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-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-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-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-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-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-p27" class="sec"><div class="sec-header"><span class="sec-num">§ 27</span><h2 class="sec-h">Использование и экономия электроэнергии. Безопасность</h2></div><div id="p27-body"></div></section>
|
||||||
|
<section id="sec-p28" class="sec"><div class="sec-header"><span class="sec-num">§ 28</span><h2 class="sec-h">Постоянные магниты</h2></div><div id="p28-body"></div></section>
|
||||||
|
<section id="sec-p29" class="sec"><div class="sec-header"><span class="sec-num">§ 29</span><h2 class="sec-h">Магнитное поле</h2></div><div id="p29-body"></div></section>
|
||||||
|
<section id="sec-p30" class="sec"><div class="sec-header"><span class="sec-num">§ 30</span><h2 class="sec-h">Магнитное поле тока</h2></div><div id="p30-body"></div></section>
|
||||||
|
<section id="sec-p31" class="sec"><div class="sec-header"><span class="sec-num">§ 31</span><h2 class="sec-h">Магнитное поле прямого проводника и катушки с током. Электромагнит</h2></div><div id="p31-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" id="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
<div class="col-side-backdrop" id="col-side-backdrop"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Физика 8» · Глава 2 · «Электромагнитные явления» · LearnSpace</footer>
|
||||||
|
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
<div id="search-modal" class="search-modal" role="dialog">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="search-input" class="search-input" placeholder="Поиск…" autocomplete="off">
|
||||||
|
<div id="search-results" class="search-results"></div>
|
||||||
|
<div class="search-foot"><span><kbd>↑↓</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const STATE = { current:'p12', progress:{}, achievements:new Map(), xp:0, level:1 };
|
||||||
|
const TOTAL_PARAS = 21;
|
||||||
|
const _TB_SLUG = 'physics-8-ch2';
|
||||||
|
const LS_PREFIX = 'physics8_ch2';
|
||||||
|
const LS_XP = 'physics8_xp';
|
||||||
|
|
||||||
|
const PARAS = [
|
||||||
|
{ id:'p12', num:'\u00a7 12', name:'Электризация тел. Взаимодействие зарядов', sub:'Два рода зарядов' },
|
||||||
|
{ id:'p13', num:'\u00a7 13', name:'Проводники и диэлектрики', sub:'Свободные носители' },
|
||||||
|
{ id:'p14', num:'\u00a7 14', name:'Электризация через влияние', sub:'Индукция' },
|
||||||
|
{ id:'p15', num:'\u00a7 15', name:'Электрический заряд. Элементарный заряд', sub:'$e = 1{,}6 \\cdot 10^{-19}$ Кл' },
|
||||||
|
{ id:'p16', num:'\u00a7 16', name:'Строение атома. Ионы', sub:'Ядро + электроны' },
|
||||||
|
{ id:'p17', num:'\u00a7 17', name:'Электрическое поле. Электрическое напряжение', sub:'$U$ как работа поля' },
|
||||||
|
{ id:'p18', num:'\u00a7 18', name:'Единица электрического напряжения. Расчёт работы в электрическом поле', sub:'$A = qU$' },
|
||||||
|
{ id:'p19', num:'\u00a7 19', name:'Электрический ток. Источники тока', sub:'Упорядоченное движение' },
|
||||||
|
{ id:'p20', num:'\u00a7 20', name:'Сила и направление электрического тока', sub:'$I = q/t$' },
|
||||||
|
{ id:'p21', num:'\u00a7 21', name:'Электрическая цепь. Измерение силы тока и напряжения', sub:'A — последов., V — паралл.' },
|
||||||
|
{ id:'p22', num:'\u00a7 22', name:'Связь силы тока и напряжения. Закон Ома для участка электрической цепи', sub:'$I = U/R$' },
|
||||||
|
{ id:'p23', num:'\u00a7 23', name:'Единица сопротивления. Расчёт сопротивления', sub:'$R = \\rho l/S$' },
|
||||||
|
{ id:'p24', num:'\u00a7 24', name:'Последовательное соединение проводников. Реостат', sub:'$R = R_1 + R_2$' },
|
||||||
|
{ id:'p25', num:'\u00a7 25', name:'Параллельное соединение проводников', sub:'$1/R = 1/R_1 + 1/R_2$' },
|
||||||
|
{ id:'p26', num:'\u00a7 26', name:'Работа и мощность электрического тока. Закон Джоуля — Ленца', sub:'$P = UI$, $Q = I^2 Rt$' },
|
||||||
|
{ id:'p27', num:'\u00a7 27', name:'Использование и экономия электроэнергии. Безопасность', sub:'кВт·ч, ТБ' },
|
||||||
|
{ id:'p28', num:'\u00a7 28', name:'Постоянные магниты', sub:'N, S, поле Земли' },
|
||||||
|
{ id:'p29', num:'\u00a7 29', name:'Магнитное поле', sub:'$\\vec{B}$, линии' },
|
||||||
|
{ id:'p30', num:'\u00a7 30', name:'Магнитное поле тока', sub:'Опыт Эрстеда' },
|
||||||
|
{ id:'p31', num:'\u00a7 31', name:'Магнитное поле прямого проводника и катушки с током. Электромагнит', sub:'Правило правой руки' },
|
||||||
|
{ id:'final2', num:'\u2605', name:'Финал главы', sub:'Итоги · 10 боссов', final:true }
|
||||||
|
];
|
||||||
|
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
|
||||||
|
|
||||||
|
const ACH_LABELS = {
|
||||||
|
start:"Начало главы 2!",
|
||||||
|
p12_done:"Электризация тел. Взаимодействие зарядов освоен!",
|
||||||
|
p13_done:"Проводники и диэлектрики освоен!",
|
||||||
|
p14_done:"Электризация через влияние освоен!",
|
||||||
|
p15_done:"Электрический заряд. Элементарный заряд освоен!",
|
||||||
|
p16_done:"Строение атома. Ионы освоен!",
|
||||||
|
p17_done:"Электрическое поле. Электрическое напряжение освоен!",
|
||||||
|
p18_done:"Единица электрического напряжения. Расчёт работы в электрическом поле освоен!",
|
||||||
|
p19_done:"Электрический ток. Источники тока освоен!",
|
||||||
|
p20_done:"Сила и направление электрического тока освоен!",
|
||||||
|
p21_done:"Электрическая цепь. Измерение силы тока и напряжения освоен!",
|
||||||
|
p22_done:"Связь силы тока и напряжения. Закон Ома для участка электрической цепи освоен!",
|
||||||
|
p23_done:"Единица сопротивления. Расчёт сопротивления освоен!",
|
||||||
|
p24_done:"Последовательное соединение проводников. Реостат освоен!",
|
||||||
|
p25_done:"Параллельное соединение проводников освоен!",
|
||||||
|
p26_done:"Работа и мощность электрического тока. Закон Джоуля — Ленца освоен!",
|
||||||
|
p27_done:"Использование и экономия электроэнергии. Безопасность освоен!",
|
||||||
|
p28_done:"Постоянные магниты освоен!",
|
||||||
|
p29_done:"Магнитное поле освоен!",
|
||||||
|
p30_done:"Магнитное поле тока освоен!",
|
||||||
|
p31_done:"Магнитное поле прямого проводника и катушки с током. Электромагнит освоен!",
|
||||||
|
ch2_done:"Глава 2 пройдена!"
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIDEBARS = {
|
||||||
|
p12:{title:"Шпаргалка § 12",rows:[["В разработке","Phase 2 Wave 1"]]},
|
||||||
|
p13:{title:"Шпаргалка § 13",rows:[["В разработке","Phase 2 Wave 1"]]},
|
||||||
|
p14:{title:"Шпаргалка § 14",rows:[["В разработке","Phase 2 Wave 1"]]},
|
||||||
|
p15:{title:"Шпаргалка § 15",rows:[["В разработке","Phase 2 Wave 2"]]},
|
||||||
|
p16:{title:"Шпаргалка § 16",rows:[["В разработке","Phase 2 Wave 2"]]},
|
||||||
|
p17:{title:"Шпаргалка § 17",rows:[["В разработке","Phase 2 Wave 3"]]},
|
||||||
|
p18:{title:"Шпаргалка § 18",rows:[["В разработке","Phase 2 Wave 3"]]},
|
||||||
|
p19:{title:"Шпаргалка § 19",rows:[["В разработке","Phase 3 Wave 1"]]},
|
||||||
|
p20:{title:"Шпаргалка § 20",rows:[["В разработке","Phase 3 Wave 1"]]},
|
||||||
|
p21:{title:"Шпаргалка § 21",rows:[["В разработке","Phase 3 Wave 2"]]},
|
||||||
|
p22:{title:"Шпаргалка § 22",rows:[["В разработке","Phase 3 Wave 2"]]},
|
||||||
|
p23:{title:"Шпаргалка § 23",rows:[["В разработке","Phase 3 Wave 3"]]},
|
||||||
|
p24:{title:"Шпаргалка § 24",rows:[["В разработке","Phase 3 Wave 3"]]},
|
||||||
|
p25:{title:"Шпаргалка § 25",rows:[["В разработке","Phase 3 Wave 3"]]},
|
||||||
|
p26:{title:"Шпаргалка § 26",rows:[["В разработке","Phase 3 Wave 4"]]},
|
||||||
|
p27:{title:"Шпаргалка § 27",rows:[["В разработке","Phase 3 Wave 4"]]},
|
||||||
|
p28:{title:"Шпаргалка § 28",rows:[["В разработке","Phase 4 Wave 1"]]},
|
||||||
|
p29:{title:"Шпаргалка § 29",rows:[["В разработке","Phase 4 Wave 1"]]},
|
||||||
|
p30:{title:"Шпаргалка § 30",rows:[["В разработке","Phase 4 Wave 2"]]},
|
||||||
|
p31:{title:"Шпаргалка § 31",rows:[["В разработке","Phase 4 Wave 2"]]},
|
||||||
|
final2:{title:"Шпаргалка ★",rows:[["В разработке","Phase 4 Wave 2"]]}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TIPS=[
|
||||||
|
{sec:'p12',html:"Параграф § 12 будет реализован в Phase 2 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p13',html:"Параграф § 13 будет реализован в Phase 2 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p14',html:"Параграф § 14 будет реализован в Phase 2 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p15',html:"Параграф § 15 будет реализован в Phase 2 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p16',html:"Параграф § 16 будет реализован в Phase 2 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p17',html:"Параграф § 17 будет реализован в Phase 2 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p18',html:"Параграф § 18 будет реализован в Phase 2 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p19',html:"Параграф § 19 будет реализован в Phase 3 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p20',html:"Параграф § 20 будет реализован в Phase 3 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p21',html:"Параграф § 21 будет реализован в Phase 3 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p22',html:"Параграф § 22 будет реализован в Phase 3 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p23',html:"Параграф § 23 будет реализован в Phase 3 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p24',html:"Параграф § 24 будет реализован в Phase 3 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p25',html:"Параграф § 25 будет реализован в Phase 3 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p26',html:"Параграф § 26 будет реализован в Phase 3 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p27',html:"Параграф § 27 будет реализован в Phase 3 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p28',html:"Параграф § 28 будет реализован в Phase 4 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p29',html:"Параграф § 29 будет реализован в Phase 4 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p30',html:"Параграф § 30 будет реализован в Phase 4 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p31',html:"Параграф § 31 будет реализован в Phase 4 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'final2',html:"Параграф ★ будет реализован в Phase 4 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."}
|
||||||
|
];
|
||||||
|
|
||||||
|
const BUILDERS = {
|
||||||
|
p12: ()=>{ const box=document.getElementById('p12-body'); box.innerHTML = buildStub('p12', 'Электризация тел. Взаимодействие зарядов', 'Phase 2 Wave 1') + secNavFor('p12') + readButton('p12'); renderMath(box); wireReadBtn('p12'); },
|
||||||
|
p13: ()=>{ const box=document.getElementById('p13-body'); box.innerHTML = buildStub('p13', 'Проводники и диэлектрики', 'Phase 2 Wave 1') + secNavFor('p13') + readButton('p13'); renderMath(box); wireReadBtn('p13'); },
|
||||||
|
p14: ()=>{ const box=document.getElementById('p14-body'); box.innerHTML = buildStub('p14', 'Электризация через влияние', 'Phase 2 Wave 1') + secNavFor('p14') + readButton('p14'); renderMath(box); wireReadBtn('p14'); },
|
||||||
|
p15: ()=>{ const box=document.getElementById('p15-body'); box.innerHTML = buildStub('p15', 'Электрический заряд. Элементарный заряд', 'Phase 2 Wave 2') + secNavFor('p15') + readButton('p15'); renderMath(box); wireReadBtn('p15'); },
|
||||||
|
p16: ()=>{ const box=document.getElementById('p16-body'); box.innerHTML = buildStub('p16', 'Строение атома. Ионы', 'Phase 2 Wave 2') + secNavFor('p16') + readButton('p16'); renderMath(box); wireReadBtn('p16'); },
|
||||||
|
p17: ()=>{ const box=document.getElementById('p17-body'); box.innerHTML = buildStub('p17', 'Электрическое поле. Электрическое напряжение', 'Phase 2 Wave 3') + secNavFor('p17') + readButton('p17'); renderMath(box); wireReadBtn('p17'); },
|
||||||
|
p18: ()=>{ const box=document.getElementById('p18-body'); box.innerHTML = buildStub('p18', 'Единица электрического напряжения. Расчёт работы в электрическом поле', 'Phase 2 Wave 3') + secNavFor('p18') + readButton('p18'); renderMath(box); wireReadBtn('p18'); },
|
||||||
|
p19: ()=>{ const box=document.getElementById('p19-body'); box.innerHTML = buildStub('p19', 'Электрический ток. Источники тока', 'Phase 3 Wave 1') + secNavFor('p19') + readButton('p19'); renderMath(box); wireReadBtn('p19'); },
|
||||||
|
p20: ()=>{ const box=document.getElementById('p20-body'); box.innerHTML = buildStub('p20', 'Сила и направление электрического тока', 'Phase 3 Wave 1') + secNavFor('p20') + readButton('p20'); renderMath(box); wireReadBtn('p20'); },
|
||||||
|
p21: ()=>{ const box=document.getElementById('p21-body'); box.innerHTML = buildStub('p21', 'Электрическая цепь. Измерение силы тока и напряжения', 'Phase 3 Wave 2') + secNavFor('p21') + readButton('p21'); renderMath(box); wireReadBtn('p21'); },
|
||||||
|
p22: ()=>{ const box=document.getElementById('p22-body'); box.innerHTML = buildStub('p22', 'Связь силы тока и напряжения. Закон Ома для участка электрической цепи', 'Phase 3 Wave 2') + secNavFor('p22') + readButton('p22'); renderMath(box); wireReadBtn('p22'); },
|
||||||
|
p23: ()=>{ const box=document.getElementById('p23-body'); box.innerHTML = buildStub('p23', 'Единица сопротивления. Расчёт сопротивления', 'Phase 3 Wave 3') + secNavFor('p23') + readButton('p23'); renderMath(box); wireReadBtn('p23'); },
|
||||||
|
p24: ()=>{ const box=document.getElementById('p24-body'); box.innerHTML = buildStub('p24', 'Последовательное соединение проводников. Реостат', 'Phase 3 Wave 3') + secNavFor('p24') + readButton('p24'); renderMath(box); wireReadBtn('p24'); },
|
||||||
|
p25: ()=>{ const box=document.getElementById('p25-body'); box.innerHTML = buildStub('p25', 'Параллельное соединение проводников', 'Phase 3 Wave 3') + secNavFor('p25') + readButton('p25'); renderMath(box); wireReadBtn('p25'); },
|
||||||
|
p26: ()=>{ const box=document.getElementById('p26-body'); box.innerHTML = buildStub('p26', 'Работа и мощность электрического тока. Закон Джоуля — Ленца', 'Phase 3 Wave 4') + secNavFor('p26') + readButton('p26'); renderMath(box); wireReadBtn('p26'); },
|
||||||
|
p27: ()=>{ const box=document.getElementById('p27-body'); box.innerHTML = buildStub('p27', 'Использование и экономия электроэнергии. Безопасность', 'Phase 3 Wave 4') + secNavFor('p27') + readButton('p27'); renderMath(box); wireReadBtn('p27'); },
|
||||||
|
p28: ()=>{ const box=document.getElementById('p28-body'); box.innerHTML = buildStub('p28', 'Постоянные магниты', 'Phase 4 Wave 1') + secNavFor('p28') + readButton('p28'); renderMath(box); wireReadBtn('p28'); },
|
||||||
|
p29: ()=>{ const box=document.getElementById('p29-body'); box.innerHTML = buildStub('p29', 'Магнитное поле', 'Phase 4 Wave 1') + secNavFor('p29') + readButton('p29'); renderMath(box); wireReadBtn('p29'); },
|
||||||
|
p30: ()=>{ const box=document.getElementById('p30-body'); box.innerHTML = buildStub('p30', 'Магнитное поле тока', 'Phase 4 Wave 2') + secNavFor('p30') + readButton('p30'); renderMath(box); wireReadBtn('p30'); },
|
||||||
|
p31: ()=>{ const box=document.getElementById('p31-body'); box.innerHTML = buildStub('p31', 'Магнитное поле прямого проводника и катушки с током. Электромагнит', 'Phase 4 Wave 2') + secNavFor('p31') + readButton('p31'); renderMath(box); wireReadBtn('p31'); },
|
||||||
|
final2: ()=>{ const box=document.getElementById('final2-body'); box.innerHTML = buildStub('final2', 'Финал главы', 'Phase 4 Wave 2') + secNavFor('final2') + readButton('final2'); renderMath(box); wireReadBtn('final2'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
|
||||||
|
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
|
||||||
|
|
||||||
|
function loadProgress(){
|
||||||
|
try{
|
||||||
|
const s=localStorage.getItem(LS_PREFIX+'_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
|
||||||
|
const a=localStorage.getItem(LS_PREFIX+'_achievements');
|
||||||
|
if(a){ const p=JSON.parse(a); if(Array.isArray(p)) p.forEach(id=>STATE.achievements.set(id, ACH_LABELS[id]||id)); else if(p&&typeof p==='object'){ for(const[id,t] of Object.entries(p)) STATE.achievements.set(id,(t&&t!==id)?t:(ACH_LABELS[id]||id)); } }
|
||||||
|
STATE.xp=+(localStorage.getItem(LS_XP)||0); STATE.level=calcLevel(STATE.xp);
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
function saveProgress(){
|
||||||
|
try{
|
||||||
|
localStorage.setItem(LS_PREFIX+'_progress', JSON.stringify(STATE.progress));
|
||||||
|
localStorage.setItem(LS_PREFIX+'_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
|
||||||
|
localStorage.setItem(LS_XP, String(STATE.xp));
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
function bumpProgress(key, delta){
|
||||||
|
STATE.progress[key]=Math.max(0,Math.min(100,(STATE.progress[key]||0)+delta));
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
if(STATE.progress[key]>=50) markParaRead(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _markedRead=new Set();
|
||||||
|
let _pendingProgressBody=null, _progressTimer=null;
|
||||||
|
function _flushProgress(){
|
||||||
|
const body=_pendingProgressBody; _pendingProgressBody=null; if(!body) return;
|
||||||
|
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||||||
|
fetch('/api/textbooks/'+_TB_SLUG+'/progress',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tok},body:JSON.stringify(body),keepalive:true}).catch(()=>{});
|
||||||
|
}
|
||||||
|
function _queueProgress(patch){ _pendingProgressBody=Object.assign(_pendingProgressBody||{},patch); if(_progressTimer) clearTimeout(_progressTimer); _progressTimer=setTimeout(_flushProgress, 600); }
|
||||||
|
function markLastPara(id){ _queueProgress({last_para:id}); }
|
||||||
|
function markParaRead(id){ if(_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({mark_read:id}); }
|
||||||
|
window.addEventListener('beforeunload', _flushProgress);
|
||||||
|
function loadServerReadState(){
|
||||||
|
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||||||
|
fetch('/api/textbooks/'+_TB_SLUG,{headers:{'Authorization':'Bearer '+tok}}).then(r=>r.ok?r.json():null).then(d=>{
|
||||||
|
if(!d||!d.progress) return;
|
||||||
|
(d.progress.read||[]).forEach(k=>{_markedRead.add(k); if((STATE.progress[k]||0)<50) STATE.progress[k]=100;});
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
}).catch(()=>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addXp(n,src){
|
||||||
|
if(!n) return;
|
||||||
|
const prev=STATE.level; STATE.xp=Math.max(0,(STATE.xp||0)+n); STATE.level=calcLevel(STATE.xp);
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
if(window.LS&&window.LS.xp) window.LS.xp.add(n, LS_PREFIX+'-'+(src||'misc'));
|
||||||
|
if(STATE.level>prev){
|
||||||
|
const pop=document.getElementById('ach-popup');
|
||||||
|
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshProgressUI(){
|
||||||
|
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
|
||||||
|
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
|
||||||
|
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
|
||||||
|
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
|
||||||
|
const xpBadge=document.getElementById('hero-xp-badge');
|
||||||
|
if(xpBadge){ xpBadge.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
|
||||||
|
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
|
||||||
|
}
|
||||||
|
|
||||||
|
function achievement(id,text){
|
||||||
|
if(STATE.achievements.has(id)) return;
|
||||||
|
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
|
||||||
|
const pop=document.getElementById('ach-popup');
|
||||||
|
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
|
||||||
|
addXp(20,'ach-'+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParaSelector(){
|
||||||
|
const g=document.getElementById('psel-grid'); g.innerHTML='';
|
||||||
|
PARAS.forEach(p=>{
|
||||||
|
const card=document.createElement('div');
|
||||||
|
card.className='psel-card'+(p.final?' final':'');
|
||||||
|
card.dataset.id=p.id; card.dataset.progCard=p.id;
|
||||||
|
card.innerHTML='<div class="psel-num">'+p.num+'</div><div class="psel-name">'+p.name+'</div><div class="psel-prog"><div class="psel-prog-fill"></div></div>';
|
||||||
|
card.addEventListener('click', ()=>goTo(p.id));
|
||||||
|
g.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILT=new Set();
|
||||||
|
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
|
||||||
|
function goTo(id){
|
||||||
|
STATE.current=id; ensureBuilt(id);
|
||||||
|
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
|
||||||
|
const el=document.getElementById('sec-'+id); if(el) el.classList.add('active');
|
||||||
|
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id===id));
|
||||||
|
buildSidebar(id);
|
||||||
|
window.scrollTo({top:0,behavior:'smooth'});
|
||||||
|
if((STATE.progress[id]||0)<10) bumpProgress(id, 10);
|
||||||
|
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
|
||||||
|
markLastPara(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSidebar(id){
|
||||||
|
const box=document.getElementById('sidebar-content');
|
||||||
|
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
|
||||||
|
let html='';
|
||||||
|
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||||
|
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||||
|
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||||
|
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
|
||||||
|
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||||
|
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||||
|
html+='</div>';
|
||||||
|
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
|
||||||
|
if(tip){
|
||||||
|
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
|
||||||
|
}
|
||||||
|
if(STATE.achievements.size>0){
|
||||||
|
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
|
||||||
|
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ '+text+'</div>'; });
|
||||||
|
html+='</div>';
|
||||||
|
}
|
||||||
|
box.innerHTML=html;
|
||||||
|
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTheme(){
|
||||||
|
const t=localStorage.getItem(LS_PREFIX+'_theme')||'light';
|
||||||
|
if(t==='dark') document.documentElement.classList.add('dark');
|
||||||
|
document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';
|
||||||
|
document.getElementById('theme-btn').addEventListener('click', ()=>{
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
const dark=document.documentElement.classList.contains('dark');
|
||||||
|
localStorage.setItem(LS_PREFIX+'_theme', dark?'dark':'light');
|
||||||
|
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
|
||||||
|
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'✓ Верно!':'✗ Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
|
||||||
|
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
|
||||||
|
function ipow(base, exp){ let r=1; for(let i=0;i<Math.abs(exp);i++) r*=base; return exp<0 ? 1/r : r; }
|
||||||
|
function gcd(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ const t=b; b=a%b; a=t; } return a||1; }
|
||||||
|
function makeCard(kind, title, num, body){
|
||||||
|
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
|
||||||
|
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === SVG-хелперы === */
|
||||||
|
function axes2D(W, H, pad, xmin, xmax, ymin, ymax){
|
||||||
|
const ux = (W - 2*pad) / (xmax - xmin);
|
||||||
|
const uy = (H - 2*pad) / (ymax - ymin);
|
||||||
|
const toX = v => pad + (v - xmin) * ux;
|
||||||
|
const toY = v => H - pad - (v - ymin) * uy;
|
||||||
|
let g = '';
|
||||||
|
g += '<g stroke="#e5e7eb" stroke-width="1">';
|
||||||
|
for (let x = Math.ceil(xmin); x <= xmax; x++){
|
||||||
|
g += '<line x1="'+toX(x)+'" y1="'+pad+'" x2="'+toX(x)+'" y2="'+(H-pad)+'"/>';
|
||||||
|
}
|
||||||
|
for (let y = Math.ceil(ymin); y <= ymax; y++){
|
||||||
|
g += '<line x1="'+pad+'" y1="'+toY(y)+'" x2="'+(W-pad)+'" y2="'+toY(y)+'"/>';
|
||||||
|
}
|
||||||
|
g += '</g>';
|
||||||
|
const y0 = toY(0), x0 = toX(0);
|
||||||
|
g += '<line x1="'+pad+'" y1="'+y0+'" x2="'+(W-pad)+'" y2="'+y0+'" stroke="#0f172a" stroke-width="1.5"/>';
|
||||||
|
g += '<line x1="'+x0+'" y1="'+pad+'" x2="'+x0+'" y2="'+(H-pad)+'" stroke="#0f172a" stroke-width="1.5"/>';
|
||||||
|
g += '<text x="'+(W-pad+2)+'" y="'+(y0-4)+'" font-size="11" fill="#0f172a">x</text>';
|
||||||
|
g += '<text x="'+(x0+4)+'" y="'+(pad-2)+'" font-size="11" fill="#0f172a">y</text>';
|
||||||
|
g += '<g font-size="10" fill="#64748b">';
|
||||||
|
for (let x = Math.ceil(xmin); x <= xmax; x++){
|
||||||
|
if (x !== 0) g += '<text x="'+(toX(x)-3)+'" y="'+(y0+12)+'">'+x+'</text>';
|
||||||
|
}
|
||||||
|
for (let y = Math.ceil(ymin); y <= ymax; y++){
|
||||||
|
if (y !== 0) g += '<text x="'+(x0+4)+'" y="'+(toY(y)+3)+'">'+y+'</text>';
|
||||||
|
}
|
||||||
|
g += '<text x="'+(x0+4)+'" y="'+(y0+12)+'">0</text>';
|
||||||
|
g += '</g>';
|
||||||
|
return { content: g, toX, toY, ux, uy };
|
||||||
|
}
|
||||||
|
function plotFunc(f, xmin, xmax, toX, toY, color, N){
|
||||||
|
N = N || 200;
|
||||||
|
let d = '';
|
||||||
|
let prevValid = false;
|
||||||
|
for (let i = 0; i <= N; i++){
|
||||||
|
const x = xmin + (xmax - xmin) * i / N;
|
||||||
|
let y;
|
||||||
|
try { y = f(x); } catch(e){ y = NaN; }
|
||||||
|
if (!isFinite(y) || isNaN(y) || y < -1e4 || y > 1e4){ prevValid = false; continue; }
|
||||||
|
d += (prevValid ? ' L' : ' M') + toX(x).toFixed(2) + ',' + toY(y).toFixed(2);
|
||||||
|
prevValid = true;
|
||||||
|
}
|
||||||
|
return '<path d="'+d+'" stroke="'+color+'" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
}
|
||||||
|
function pointWithDrop(x, fx, toX, toY, color, label){
|
||||||
|
const px = toX(x), py = toY(fx);
|
||||||
|
let s = '';
|
||||||
|
s += '<line x1="'+px+'" y1="'+py+'" x2="'+px+'" y2="'+toY(0)+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
|
||||||
|
s += '<line x1="'+px+'" y1="'+py+'" x2="'+toX(0)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
|
||||||
|
s += '<circle cx="'+px+'" cy="'+py+'" r="4.5" fill="'+color+'" stroke="#fff" stroke-width="2"/>';
|
||||||
|
if (label){
|
||||||
|
s += '<text x="'+(px+8)+'" y="'+(py-8)+'" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="'+color+'">'+label+'</text>';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
function asymptote(orientation, value, toX, toY, xmin, xmax, ymin, ymax, color){
|
||||||
|
color = color || '#94a3b8';
|
||||||
|
if (orientation === 'h'){
|
||||||
|
const y = toY(value);
|
||||||
|
return '<line x1="'+toX(xmin)+'" y1="'+y+'" x2="'+toX(xmax)+'" y2="'+y+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
|
||||||
|
} else {
|
||||||
|
const x = toX(value);
|
||||||
|
return '<line x1="'+x+'" y1="'+toY(ymin)+'" x2="'+x+'" y2="'+toY(ymax)+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function snapToValue(value, snapPoints, tolerance){
|
||||||
|
tolerance = tolerance || 0.1;
|
||||||
|
for (const sp of snapPoints){
|
||||||
|
if (Math.abs(value - sp) < tolerance) return sp;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function rightAngleMark(V, uIn, wIn, s){
|
||||||
|
s = s || 9;
|
||||||
|
const p1 = {x: V.x + s*uIn.x, y: V.y + s*uIn.y};
|
||||||
|
const c = {x: p1.x + s*wIn.x, y: p1.y + s*wIn.y};
|
||||||
|
const p2 = {x: V.x + s*wIn.x, y: V.y + s*wIn.y};
|
||||||
|
return p1.x+','+p1.y+' '+c.x+','+c.y+' '+p2.x+','+p2.y;
|
||||||
|
}
|
||||||
|
function angleArcAuto(V, uA, uB, R){
|
||||||
|
const sA = {x: V.x + R*uA.x, y: V.y + R*uA.y};
|
||||||
|
const eB = {x: V.x + R*uB.x, y: V.y + R*uB.y};
|
||||||
|
const cross = uA.x*uB.y - uA.y*uB.x;
|
||||||
|
const sweep = cross > 0 ? 1 : 0;
|
||||||
|
return 'M'+sA.x+','+sA.y+' A'+R+','+R+' 0 0,'+sweep+' '+eB.x+','+eB.y;
|
||||||
|
}
|
||||||
|
function unitVec(p1, p2){
|
||||||
|
const dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||||
|
const len = Math.sqrt(dx*dx + dy*dy) || 1;
|
||||||
|
return {x: dx/len, y: dy/len};
|
||||||
|
}
|
||||||
|
function deg2rad(d){ return d * Math.PI / 180; }
|
||||||
|
|
||||||
|
const ICONS = {
|
||||||
|
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
|
||||||
|
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
||||||
|
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
|
||||||
|
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
|
||||||
|
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
|
||||||
|
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
|
||||||
|
};
|
||||||
|
|
||||||
|
function secNavFor(curId){
|
||||||
|
const idx = PARAS.findIndex(p => p.id === curId);
|
||||||
|
const prev = idx > 0 ? PARAS[idx-1].id : null;
|
||||||
|
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
|
||||||
|
return secNav(prev, next);
|
||||||
|
}
|
||||||
|
function secNav(prev, next){
|
||||||
|
function lbl(id){ if(!id) return ''; const p=PARAS.find(x=>x.id===id); return p?p.num:id; }
|
||||||
|
let h='<div class="sec-nav">';
|
||||||
|
h+=prev?'<button class="btn" onclick="goTo(\''+prev+'\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> '+lbl(prev)+'</button>':'<span></span>';
|
||||||
|
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+lbl(next)+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
|
||||||
|
h+='</div>'; return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readButton(paraId){
|
||||||
|
const p = PARAS.find(x => x.id === paraId);
|
||||||
|
const labelTail = p && p.final ? 'финал' : (p ? p.num : '?');
|
||||||
|
return '<div style="margin-top:18px;display:flex;justify-content:center">'
|
||||||
|
+'<button class="btn primary" id="'+paraId+'-read-btn">'
|
||||||
|
+'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
|
||||||
|
+' Я прочитал \u2014 '+labelTail+' (+10 XP)'
|
||||||
|
+'</button></div>';
|
||||||
|
}
|
||||||
|
function wireReadBtn(paraId){
|
||||||
|
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
|
||||||
|
btn.addEventListener('click', ()=>{
|
||||||
|
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
|
||||||
|
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
|
||||||
|
const aId = paraId+'_done';
|
||||||
|
if(ACH_LABELS[aId]) achievement(aId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSorter(cfg){
|
||||||
|
const placed = {}; const pool = document.getElementById(cfg.poolId); const scope = document.querySelector(cfg.scopeSelector);
|
||||||
|
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
|
||||||
|
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
|
||||||
|
let armed = null;
|
||||||
|
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
|
||||||
|
function attach(elm,itId){ let ghost=null,dragging=false,sx=0,sy=0; elm.addEventListener('pointerdown',ev=>{ if(ev.button!==undefined&&ev.button!==0) return;
|
||||||
|
ev.preventDefault(); if(ev.target.classList&&ev.target.classList.contains('dnd-x')){ ev.stopPropagation(); if(placed[itId]){delete placed[itId];render();}else if(armed===itId){armed=null;render();} return; } sx=ev.clientX;sy=ev.clientY; const r=elm.getBoundingClientRect(); const ox=ev.clientX-r.left,oy=ev.clientY-r.top; try{elm.setPointerCapture(ev.pointerId);}catch(e){} function onMove(e){ const dx=e.clientX-sx,dy=e.clientY-sy; if(!dragging&&Math.hypot(dx,dy)>8){ dragging=true; ghost=elm.cloneNode(true); ghost.classList.remove('armed'); ghost.style.cssText='position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:'+r.width+'px;left:'+(e.clientX-ox)+'px;top:'+(e.clientY-oy)+'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if(dragging&&ghost){ ghost.style.left=(e.clientX-ox)+'px';ghost.style.top=(e.clientY-oy)+'px'; const under=document.elementsFromPoint(e.clientX,e.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); const tgt=under.find(n=>n.classList&&(n.classList.contains('drop-box')||n.classList.contains('dnd-pool'))); if(tgt)tgt.classList.add('over'); } } function onUp(e){ elm.removeEventListener('pointermove',onMove);elm.removeEventListener('pointerup',onUp);elm.removeEventListener('pointercancel',onUp);elm.classList.remove('dragging'); if(ghost){ghost.remove();ghost=null;} scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); if(dragging){ const under=document.elementsFromPoint(e.clientX,e.clientY); const box=under.find(n=>n.classList&&n.classList.contains('drop-box')); const pl=under.find(n=>n.classList&&n.classList.contains('dnd-pool')); if(box){const di=box.querySelector('[data-cat]');if(di){placed[itId]=di.dataset.cat;armed=null;render();return;}}else if(pl){delete placed[itId];armed=null;render();return;} }else{ if(placed[itId]){delete placed[itId];armed=null;render();}else{armed=(armed===itId)?null:itId;render();} } dragging=false; } elm.addEventListener('pointermove',onMove);elm.addEventListener('pointerup',onUp);elm.addEventListener('pointercancel',onUp); }); }
|
||||||
|
function attachBoxTaps(){ scope.querySelectorAll('.drop-box').forEach(box=>{ box.addEventListener('click',ev=>{ if(!armed)return; if(ev.target.closest('.dnd-chip'))return; const di=box.querySelector('[data-cat]'); if(di){placed[armed]=di.dataset.cat;armed=null;render();} }); }); }
|
||||||
|
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); cfg.cats.forEach(cat=>{const box=scope.querySelector('.drop-items[data-cat="'+cat+'"]');if(!box)return;box.innerHTML='';cfg.items.forEach(it=>{if(placed[it.id]!==cat)return;box.appendChild(buildChip(it,true));});}); if(window.renderMathInElement)try{renderMath(scope);}catch(_){} }
|
||||||
|
attachBoxTaps(); render();
|
||||||
|
return {placed,render,reset(){ for(const k in placed)delete placed[k];armed=null;render(); }};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStub(id, name, phase){
|
||||||
|
return '<div class="card" style="background:linear-gradient(135deg,var(--sec-acc-soft),var(--card));border:1.5px dashed var(--sec-acc)">'
|
||||||
|
+ '<div class="card-header"><div class="card-icon theory">'+ICONS.theory+'</div><div class="card-title">В разработке</div></div>'
|
||||||
|
+ '<div class="card-body"><p>Контент <b>'+name+'</b> будет реализован в <b>'+phase+'</b> по плану <code>PLAN_PHYSICS_8.md</code>.</p>'
|
||||||
|
+ '<p style="margin-top:8px;color:var(--muted);font-size:.9rem">Phase 0 \u2014 это каркас (skeleton). Все 4 интерактива, 3 теоретические карточки и тренажёр задач будут добавлены в волне.</p>'
|
||||||
|
+ '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Search ===== */
|
||||||
|
const SEARCH_INDEX = (function(){
|
||||||
|
const arr=[];
|
||||||
|
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
|
||||||
|
return arr;
|
||||||
|
})();
|
||||||
|
function initSearch(){
|
||||||
|
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
|
||||||
|
if(!modal||!inp||!out) return;
|
||||||
|
let cur=0,rows=[];
|
||||||
|
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
|
||||||
|
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
|
||||||
|
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'\u2026':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
|
||||||
|
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
|
||||||
|
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
|
||||||
|
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
|
||||||
|
function close(){ modal.classList.remove('show'); }
|
||||||
|
btn&&btn.addEventListener('click',open);
|
||||||
|
modal.addEventListener('click',e=>{if(e.target===modal)close();});
|
||||||
|
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
|
||||||
|
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
|
||||||
|
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSidebarToggle(){
|
||||||
|
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
|
||||||
|
if(!side||!btn) return;
|
||||||
|
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||||
|
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||||
|
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
|
||||||
|
back.addEventListener('click',close);
|
||||||
|
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||||
|
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo(PARAS[0].id);
|
||||||
|
setTimeout(()=>achievement('start'), 600);
|
||||||
|
if(window.LS&&window.LS.xp){
|
||||||
|
window.LS.xp.load().then(function(s){ if(s&&s.xp>STATE.xp){ STATE.xp=s.xp; STATE.level=calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if(STATE.current) buildSidebar(STATE.current); } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,654 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<title>Физика 8 · Глава 3 · «Световые явления»</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
|
||||||
|
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/g3d.js" defer></script>
|
||||||
|
<script src="/js/phys.js" defer></script>
|
||||||
|
<script src="/js/optics.js" defer></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#ecfeff; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
|
||||||
|
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
|
||||||
|
--pri:#7c3aed; --pri2:#5b21b6; --pri-soft:#ede9fe;
|
||||||
|
--acc:#a78bfa; --acc2:#7c3aed; --acc-soft:#ede9fe;
|
||||||
|
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
|
||||||
|
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
|
||||||
|
}
|
||||||
|
.dark{--bg:#0a0a0e; --card:#13120a; --card-soft:#18160a; --text:#fef9e7; --ink:#fef9e7; --muted:#a39070; --border:#2a2512}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||||
|
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
|
||||||
|
button,input,select,textarea{font-family:inherit;font-size:inherit}
|
||||||
|
button{cursor:pointer;border:0;background:transparent;color:inherit}
|
||||||
|
a{color:inherit;text-decoration:none}
|
||||||
|
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
|
||||||
|
|
||||||
|
.hdr{position:relative;background:linear-gradient(110deg,#164e63 0%,#0891b2 55%,#67e8f9 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.2);min-height:130px}
|
||||||
|
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||||||
|
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
|
||||||
|
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
|
||||||
|
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||||
|
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
|
||||||
|
.hdr-btn:hover{background:rgba(255,255,255,.24)}
|
||||||
|
|
||||||
|
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
|
||||||
|
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
|
||||||
|
.col-main{min-width:0}
|
||||||
|
|
||||||
|
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
|
||||||
|
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
|
||||||
|
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
|
||||||
|
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
|
||||||
|
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
|
||||||
|
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
|
||||||
|
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
|
||||||
|
.hero-progress{flex:1;min-width:200px;max-width:280px}
|
||||||
|
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
|
||||||
|
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
|
||||||
|
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
|
||||||
|
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
|
||||||
|
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
|
||||||
|
|
||||||
|
.psel{margin-bottom:24px}
|
||||||
|
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
|
||||||
|
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
|
||||||
|
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
|
||||||
|
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
|
||||||
|
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
|
||||||
|
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
|
||||||
|
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
|
||||||
|
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
|
||||||
|
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
|
||||||
|
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
|
||||||
|
.psel-card.final{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft))}
|
||||||
|
.psel-card.final .psel-num{color:var(--warn)}
|
||||||
|
|
||||||
|
.sec[id="sec-p32"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
.sec[id="sec-p33"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
.sec[id="sec-p34"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
.sec[id="sec-p35"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
.sec[id="sec-p36"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
.sec[id="sec-p37"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
.sec[id="sec-p38"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
.sec[id="sec-p39"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
.sec[id="sec-p40"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
.sec[id="sec-final3"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
|
||||||
|
.sec{display:none;position:relative;animation:fadeIn .35s ease}
|
||||||
|
.sec.active{display:block}
|
||||||
|
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||||||
|
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
|
||||||
|
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
|
||||||
|
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
|
||||||
|
|
||||||
|
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.04);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
|
||||||
|
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(0,0,0,.08)}
|
||||||
|
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
|
||||||
|
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
|
||||||
|
.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}
|
||||||
|
.card-icon .ic{width:18px;height:18px}
|
||||||
|
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
|
||||||
|
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
|
||||||
|
.card-body{font-size:.94rem;line-height:1.65}
|
||||||
|
.card-body p{margin-bottom:8px}
|
||||||
|
.card-body p:last-child{margin-bottom:0}
|
||||||
|
|
||||||
|
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
|
||||||
|
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
|
||||||
|
.btn:active{transform:scale(.96)}
|
||||||
|
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
|
||||||
|
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
|
||||||
|
|
||||||
|
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
|
||||||
|
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
|
||||||
|
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
|
||||||
|
|
||||||
|
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
|
||||||
|
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
|
||||||
|
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||||
|
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
|
||||||
|
.sidecard-row b{color:var(--pri);font-weight:700}
|
||||||
|
.sidecard-row:last-child{margin-bottom:0}
|
||||||
|
@media(max-width:980px){.col-side{position:static;max-height:none}}
|
||||||
|
|
||||||
|
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
|
||||||
|
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
|
||||||
|
.xp-bar{height:9px;background:rgba(0,0,0,.10);border-radius:6px;overflow:hidden;margin:7px 0}
|
||||||
|
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
|
||||||
|
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
|
||||||
|
|
||||||
|
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
|
||||||
|
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
|
||||||
|
|
||||||
|
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
|
||||||
|
.ach-popup.show{display:flex}
|
||||||
|
|
||||||
|
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
|
||||||
|
.col-side-backdrop.show{display:block}
|
||||||
|
@media(max-width:980px){
|
||||||
|
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
|
||||||
|
.col-side.open{transform:none}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
|
||||||
|
.search-modal.show{display:flex}
|
||||||
|
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
|
||||||
|
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
|
||||||
|
.search-results{flex:1;overflow-y:auto;padding:6px 0}
|
||||||
|
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
|
||||||
|
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
|
||||||
|
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
|
||||||
|
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
|
||||||
|
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
|
||||||
|
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
|
||||||
|
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
|
||||||
|
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
|
||||||
|
|
||||||
|
.sec{transition:opacity .25s}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Физика 8 · Глава 3</h1>
|
||||||
|
<div class="hdr-sub">Свет · отражение · преломление · линзы · глаз</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/physics-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 8</a>
|
||||||
|
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
|
||||||
|
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><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>Свет распространяется прямолинейно со скоростью $c = 3 \cdot 10^8$ м/с. Закон отражения и закон преломления (Снеллиуса) объясняют поведение пучков света. Линзы строят изображения; глаз — это оптическая система.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('p32')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 32</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" data-gamified></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel">
|
||||||
|
<div class="psel-title">Параграфы главы</div>
|
||||||
|
<div id="psel-grid" class="psel-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="sec-p32" class="sec"><div class="sec-header"><span class="sec-num">§ 32</span><h2 class="sec-h">Источники света</h2></div><div id="p32-body"></div></section>
|
||||||
|
<section id="sec-p33" class="sec"><div class="sec-header"><span class="sec-num">§ 33</span><h2 class="sec-h">Скорость света. Прямолинейное распространение света</h2></div><div id="p33-body"></div></section>
|
||||||
|
<section id="sec-p34" class="sec"><div class="sec-header"><span class="sec-num">§ 34</span><h2 class="sec-h">Отражение света</h2></div><div id="p34-body"></div></section>
|
||||||
|
<section id="sec-p35" class="sec"><div class="sec-header"><span class="sec-num">§ 35</span><h2 class="sec-h">Зеркала. Изображение в плоском зеркале</h2></div><div id="p35-body"></div></section>
|
||||||
|
<section id="sec-p36" class="sec"><div class="sec-header"><span class="sec-num">§ 36</span><h2 class="sec-h">Преломление света</h2></div><div id="p36-body"></div></section>
|
||||||
|
<section id="sec-p37" class="sec"><div class="sec-header"><span class="sec-num">§ 37</span><h2 class="sec-h">Линзы. Оптическая сила линзы</h2></div><div id="p37-body"></div></section>
|
||||||
|
<section id="sec-p38" class="sec"><div class="sec-header"><span class="sec-num">§ 38</span><h2 class="sec-h">Построение изображений в тонких линзах</h2></div><div id="p38-body"></div></section>
|
||||||
|
<section id="sec-p39" class="sec"><div class="sec-header"><span class="sec-num">§ 39</span><h2 class="sec-h">Глаз как оптическая система</h2></div><div id="p39-body"></div></section>
|
||||||
|
<section id="sec-p40" class="sec"><div class="sec-header"><span class="sec-num">§ 40</span><h2 class="sec-h">Дефекты зрения. Очки</h2></div><div id="p40-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" id="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
<div class="col-side-backdrop" id="col-side-backdrop"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Физика 8» · Глава 3 · «Световые явления» · LearnSpace</footer>
|
||||||
|
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
<div id="search-modal" class="search-modal" role="dialog">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="search-input" class="search-input" placeholder="Поиск…" autocomplete="off">
|
||||||
|
<div id="search-results" class="search-results"></div>
|
||||||
|
<div class="search-foot"><span><kbd>↑↓</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const STATE = { current:'p32', progress:{}, achievements:new Map(), xp:0, level:1 };
|
||||||
|
const TOTAL_PARAS = 10;
|
||||||
|
const _TB_SLUG = 'physics-8-ch3';
|
||||||
|
const LS_PREFIX = 'physics8_ch3';
|
||||||
|
const LS_XP = 'physics8_xp';
|
||||||
|
|
||||||
|
const PARAS = [
|
||||||
|
{ id:'p32', num:'\u00a7 32', name:'Источники света', sub:'Тепловые, люминесцентные' },
|
||||||
|
{ id:'p33', num:'\u00a7 33', name:'Скорость света. Прямолинейное распространение света', sub:'$c = 3 \\cdot 10^8$ м/с' },
|
||||||
|
{ id:'p34', num:'\u00a7 34', name:'Отражение света', sub:'$\\alpha = \\beta$' },
|
||||||
|
{ id:'p35', num:'\u00a7 35', name:'Зеркала. Изображение в плоском зеркале', sub:'Мнимое, симметричное' },
|
||||||
|
{ id:'p36', num:'\u00a7 36', name:'Преломление света', sub:'$\\sin\\alpha/\\sin\\beta = n$' },
|
||||||
|
{ id:'p37', num:'\u00a7 37', name:'Линзы. Оптическая сила линзы', sub:'$D = 1/F$' },
|
||||||
|
{ id:'p38', num:'\u00a7 38', name:'Построение изображений в тонких линзах', sub:'3 «золотых» луча' },
|
||||||
|
{ id:'p39', num:'\u00a7 39', name:'Глаз как оптическая система', sub:'Аккомодация, $\\geq 25$ см' },
|
||||||
|
{ id:'p40', num:'\u00a7 40', name:'Дефекты зрения. Очки', sub:'Близо- и дальнозоркость' },
|
||||||
|
{ id:'final3', num:'\u2605', name:'Финал главы', sub:'Итоги · 7 боссов', final:true }
|
||||||
|
];
|
||||||
|
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
|
||||||
|
|
||||||
|
const ACH_LABELS = {
|
||||||
|
start:"Начало главы 3!",
|
||||||
|
p32_done:"Источники света освоен!",
|
||||||
|
p33_done:"Скорость света. Прямолинейное распространение света освоен!",
|
||||||
|
p34_done:"Отражение света освоен!",
|
||||||
|
p35_done:"Зеркала. Изображение в плоском зеркале освоен!",
|
||||||
|
p36_done:"Преломление света освоен!",
|
||||||
|
p37_done:"Линзы. Оптическая сила линзы освоен!",
|
||||||
|
p38_done:"Построение изображений в тонких линзах освоен!",
|
||||||
|
p39_done:"Глаз как оптическая система освоен!",
|
||||||
|
p40_done:"Дефекты зрения. Очки освоен!",
|
||||||
|
ch3_done:"Глава 3 пройдена!"
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIDEBARS = {
|
||||||
|
p32:{title:"Шпаргалка § 32",rows:[["В разработке","Phase 5 Wave 1"]]},
|
||||||
|
p33:{title:"Шпаргалка § 33",rows:[["В разработке","Phase 5 Wave 1"]]},
|
||||||
|
p34:{title:"Шпаргалка § 34",rows:[["В разработке","Phase 5 Wave 2"]]},
|
||||||
|
p35:{title:"Шпаргалка § 35",rows:[["В разработке","Phase 5 Wave 2"]]},
|
||||||
|
p36:{title:"Шпаргалка § 36",rows:[["В разработке","Phase 5 Wave 3"]]},
|
||||||
|
p37:{title:"Шпаргалка § 37",rows:[["В разработке","Phase 5 Wave 3"]]},
|
||||||
|
p38:{title:"Шпаргалка § 38",rows:[["В разработке","Phase 5 Wave 4"]]},
|
||||||
|
p39:{title:"Шпаргалка § 39",rows:[["В разработке","Phase 5 Wave 4"]]},
|
||||||
|
p40:{title:"Шпаргалка § 40",rows:[["В разработке","Phase 5 Wave 4"]]},
|
||||||
|
final3:{title:"Шпаргалка ★",rows:[["В разработке","Phase 5 Wave 4"]]}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TIPS=[
|
||||||
|
{sec:'p32',html:"Параграф § 32 будет реализован в Phase 5 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p33',html:"Параграф § 33 будет реализован в Phase 5 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p34',html:"Параграф § 34 будет реализован в Phase 5 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p35',html:"Параграф § 35 будет реализован в Phase 5 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p36',html:"Параграф § 36 будет реализован в Phase 5 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p37',html:"Параграф § 37 будет реализован в Phase 5 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p38',html:"Параграф § 38 будет реализован в Phase 5 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p39',html:"Параграф § 39 будет реализован в Phase 5 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'p40',html:"Параграф § 40 будет реализован в Phase 5 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'final3',html:"Параграф ★ будет реализован в Phase 5 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."}
|
||||||
|
];
|
||||||
|
|
||||||
|
const BUILDERS = {
|
||||||
|
p32: ()=>{ const box=document.getElementById('p32-body'); box.innerHTML = buildStub('p32', 'Источники света', 'Phase 5 Wave 1') + secNavFor('p32') + readButton('p32'); renderMath(box); wireReadBtn('p32'); },
|
||||||
|
p33: ()=>{ const box=document.getElementById('p33-body'); box.innerHTML = buildStub('p33', 'Скорость света. Прямолинейное распространение света', 'Phase 5 Wave 1') + secNavFor('p33') + readButton('p33'); renderMath(box); wireReadBtn('p33'); },
|
||||||
|
p34: ()=>{ const box=document.getElementById('p34-body'); box.innerHTML = buildStub('p34', 'Отражение света', 'Phase 5 Wave 2') + secNavFor('p34') + readButton('p34'); renderMath(box); wireReadBtn('p34'); },
|
||||||
|
p35: ()=>{ const box=document.getElementById('p35-body'); box.innerHTML = buildStub('p35', 'Зеркала. Изображение в плоском зеркале', 'Phase 5 Wave 2') + secNavFor('p35') + readButton('p35'); renderMath(box); wireReadBtn('p35'); },
|
||||||
|
p36: ()=>{ const box=document.getElementById('p36-body'); box.innerHTML = buildStub('p36', 'Преломление света', 'Phase 5 Wave 3') + secNavFor('p36') + readButton('p36'); renderMath(box); wireReadBtn('p36'); },
|
||||||
|
p37: ()=>{ const box=document.getElementById('p37-body'); box.innerHTML = buildStub('p37', 'Линзы. Оптическая сила линзы', 'Phase 5 Wave 3') + secNavFor('p37') + readButton('p37'); renderMath(box); wireReadBtn('p37'); },
|
||||||
|
p38: ()=>{ const box=document.getElementById('p38-body'); box.innerHTML = buildStub('p38', 'Построение изображений в тонких линзах', 'Phase 5 Wave 4') + secNavFor('p38') + readButton('p38'); renderMath(box); wireReadBtn('p38'); },
|
||||||
|
p39: ()=>{ const box=document.getElementById('p39-body'); box.innerHTML = buildStub('p39', 'Глаз как оптическая система', 'Phase 5 Wave 4') + secNavFor('p39') + readButton('p39'); renderMath(box); wireReadBtn('p39'); },
|
||||||
|
p40: ()=>{ const box=document.getElementById('p40-body'); box.innerHTML = buildStub('p40', 'Дефекты зрения. Очки', 'Phase 5 Wave 4') + secNavFor('p40') + readButton('p40'); renderMath(box); wireReadBtn('p40'); },
|
||||||
|
final3: ()=>{ const box=document.getElementById('final3-body'); box.innerHTML = buildStub('final3', 'Финал главы', 'Phase 5 Wave 4') + secNavFor('final3') + readButton('final3'); renderMath(box); wireReadBtn('final3'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
|
||||||
|
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
|
||||||
|
|
||||||
|
function loadProgress(){
|
||||||
|
try{
|
||||||
|
const s=localStorage.getItem(LS_PREFIX+'_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
|
||||||
|
const a=localStorage.getItem(LS_PREFIX+'_achievements');
|
||||||
|
if(a){ const p=JSON.parse(a); if(Array.isArray(p)) p.forEach(id=>STATE.achievements.set(id, ACH_LABELS[id]||id)); else if(p&&typeof p==='object'){ for(const[id,t] of Object.entries(p)) STATE.achievements.set(id,(t&&t!==id)?t:(ACH_LABELS[id]||id)); } }
|
||||||
|
STATE.xp=+(localStorage.getItem(LS_XP)||0); STATE.level=calcLevel(STATE.xp);
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
function saveProgress(){
|
||||||
|
try{
|
||||||
|
localStorage.setItem(LS_PREFIX+'_progress', JSON.stringify(STATE.progress));
|
||||||
|
localStorage.setItem(LS_PREFIX+'_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
|
||||||
|
localStorage.setItem(LS_XP, String(STATE.xp));
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
function bumpProgress(key, delta){
|
||||||
|
STATE.progress[key]=Math.max(0,Math.min(100,(STATE.progress[key]||0)+delta));
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
if(STATE.progress[key]>=50) markParaRead(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _markedRead=new Set();
|
||||||
|
let _pendingProgressBody=null, _progressTimer=null;
|
||||||
|
function _flushProgress(){
|
||||||
|
const body=_pendingProgressBody; _pendingProgressBody=null; if(!body) return;
|
||||||
|
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||||||
|
fetch('/api/textbooks/'+_TB_SLUG+'/progress',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tok},body:JSON.stringify(body),keepalive:true}).catch(()=>{});
|
||||||
|
}
|
||||||
|
function _queueProgress(patch){ _pendingProgressBody=Object.assign(_pendingProgressBody||{},patch); if(_progressTimer) clearTimeout(_progressTimer); _progressTimer=setTimeout(_flushProgress, 600); }
|
||||||
|
function markLastPara(id){ _queueProgress({last_para:id}); }
|
||||||
|
function markParaRead(id){ if(_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({mark_read:id}); }
|
||||||
|
window.addEventListener('beforeunload', _flushProgress);
|
||||||
|
function loadServerReadState(){
|
||||||
|
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||||||
|
fetch('/api/textbooks/'+_TB_SLUG,{headers:{'Authorization':'Bearer '+tok}}).then(r=>r.ok?r.json():null).then(d=>{
|
||||||
|
if(!d||!d.progress) return;
|
||||||
|
(d.progress.read||[]).forEach(k=>{_markedRead.add(k); if((STATE.progress[k]||0)<50) STATE.progress[k]=100;});
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
}).catch(()=>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addXp(n,src){
|
||||||
|
if(!n) return;
|
||||||
|
const prev=STATE.level; STATE.xp=Math.max(0,(STATE.xp||0)+n); STATE.level=calcLevel(STATE.xp);
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
if(window.LS&&window.LS.xp) window.LS.xp.add(n, LS_PREFIX+'-'+(src||'misc'));
|
||||||
|
if(STATE.level>prev){
|
||||||
|
const pop=document.getElementById('ach-popup');
|
||||||
|
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshProgressUI(){
|
||||||
|
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
|
||||||
|
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
|
||||||
|
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
|
||||||
|
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
|
||||||
|
const xpBadge=document.getElementById('hero-xp-badge');
|
||||||
|
if(xpBadge){ xpBadge.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
|
||||||
|
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
|
||||||
|
}
|
||||||
|
|
||||||
|
function achievement(id,text){
|
||||||
|
if(STATE.achievements.has(id)) return;
|
||||||
|
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
|
||||||
|
const pop=document.getElementById('ach-popup');
|
||||||
|
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
|
||||||
|
addXp(20,'ach-'+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParaSelector(){
|
||||||
|
const g=document.getElementById('psel-grid'); g.innerHTML='';
|
||||||
|
PARAS.forEach(p=>{
|
||||||
|
const card=document.createElement('div');
|
||||||
|
card.className='psel-card'+(p.final?' final':'');
|
||||||
|
card.dataset.id=p.id; card.dataset.progCard=p.id;
|
||||||
|
card.innerHTML='<div class="psel-num">'+p.num+'</div><div class="psel-name">'+p.name+'</div><div class="psel-prog"><div class="psel-prog-fill"></div></div>';
|
||||||
|
card.addEventListener('click', ()=>goTo(p.id));
|
||||||
|
g.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILT=new Set();
|
||||||
|
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
|
||||||
|
function goTo(id){
|
||||||
|
STATE.current=id; ensureBuilt(id);
|
||||||
|
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
|
||||||
|
const el=document.getElementById('sec-'+id); if(el) el.classList.add('active');
|
||||||
|
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id===id));
|
||||||
|
buildSidebar(id);
|
||||||
|
window.scrollTo({top:0,behavior:'smooth'});
|
||||||
|
if((STATE.progress[id]||0)<10) bumpProgress(id, 10);
|
||||||
|
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
|
||||||
|
markLastPara(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSidebar(id){
|
||||||
|
const box=document.getElementById('sidebar-content');
|
||||||
|
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
|
||||||
|
let html='';
|
||||||
|
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||||
|
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||||
|
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||||
|
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
|
||||||
|
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||||
|
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||||
|
html+='</div>';
|
||||||
|
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
|
||||||
|
if(tip){
|
||||||
|
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
|
||||||
|
}
|
||||||
|
if(STATE.achievements.size>0){
|
||||||
|
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
|
||||||
|
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ '+text+'</div>'; });
|
||||||
|
html+='</div>';
|
||||||
|
}
|
||||||
|
box.innerHTML=html;
|
||||||
|
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTheme(){
|
||||||
|
const t=localStorage.getItem(LS_PREFIX+'_theme')||'light';
|
||||||
|
if(t==='dark') document.documentElement.classList.add('dark');
|
||||||
|
document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';
|
||||||
|
document.getElementById('theme-btn').addEventListener('click', ()=>{
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
const dark=document.documentElement.classList.contains('dark');
|
||||||
|
localStorage.setItem(LS_PREFIX+'_theme', dark?'dark':'light');
|
||||||
|
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
|
||||||
|
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'✓ Верно!':'✗ Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
|
||||||
|
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
|
||||||
|
function ipow(base, exp){ let r=1; for(let i=0;i<Math.abs(exp);i++) r*=base; return exp<0 ? 1/r : r; }
|
||||||
|
function gcd(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ const t=b; b=a%b; a=t; } return a||1; }
|
||||||
|
function makeCard(kind, title, num, body){
|
||||||
|
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
|
||||||
|
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === SVG-хелперы === */
|
||||||
|
function axes2D(W, H, pad, xmin, xmax, ymin, ymax){
|
||||||
|
const ux = (W - 2*pad) / (xmax - xmin);
|
||||||
|
const uy = (H - 2*pad) / (ymax - ymin);
|
||||||
|
const toX = v => pad + (v - xmin) * ux;
|
||||||
|
const toY = v => H - pad - (v - ymin) * uy;
|
||||||
|
let g = '';
|
||||||
|
g += '<g stroke="#e5e7eb" stroke-width="1">';
|
||||||
|
for (let x = Math.ceil(xmin); x <= xmax; x++){
|
||||||
|
g += '<line x1="'+toX(x)+'" y1="'+pad+'" x2="'+toX(x)+'" y2="'+(H-pad)+'"/>';
|
||||||
|
}
|
||||||
|
for (let y = Math.ceil(ymin); y <= ymax; y++){
|
||||||
|
g += '<line x1="'+pad+'" y1="'+toY(y)+'" x2="'+(W-pad)+'" y2="'+toY(y)+'"/>';
|
||||||
|
}
|
||||||
|
g += '</g>';
|
||||||
|
const y0 = toY(0), x0 = toX(0);
|
||||||
|
g += '<line x1="'+pad+'" y1="'+y0+'" x2="'+(W-pad)+'" y2="'+y0+'" stroke="#0f172a" stroke-width="1.5"/>';
|
||||||
|
g += '<line x1="'+x0+'" y1="'+pad+'" x2="'+x0+'" y2="'+(H-pad)+'" stroke="#0f172a" stroke-width="1.5"/>';
|
||||||
|
g += '<text x="'+(W-pad+2)+'" y="'+(y0-4)+'" font-size="11" fill="#0f172a">x</text>';
|
||||||
|
g += '<text x="'+(x0+4)+'" y="'+(pad-2)+'" font-size="11" fill="#0f172a">y</text>';
|
||||||
|
g += '<g font-size="10" fill="#64748b">';
|
||||||
|
for (let x = Math.ceil(xmin); x <= xmax; x++){
|
||||||
|
if (x !== 0) g += '<text x="'+(toX(x)-3)+'" y="'+(y0+12)+'">'+x+'</text>';
|
||||||
|
}
|
||||||
|
for (let y = Math.ceil(ymin); y <= ymax; y++){
|
||||||
|
if (y !== 0) g += '<text x="'+(x0+4)+'" y="'+(toY(y)+3)+'">'+y+'</text>';
|
||||||
|
}
|
||||||
|
g += '<text x="'+(x0+4)+'" y="'+(y0+12)+'">0</text>';
|
||||||
|
g += '</g>';
|
||||||
|
return { content: g, toX, toY, ux, uy };
|
||||||
|
}
|
||||||
|
function plotFunc(f, xmin, xmax, toX, toY, color, N){
|
||||||
|
N = N || 200;
|
||||||
|
let d = '';
|
||||||
|
let prevValid = false;
|
||||||
|
for (let i = 0; i <= N; i++){
|
||||||
|
const x = xmin + (xmax - xmin) * i / N;
|
||||||
|
let y;
|
||||||
|
try { y = f(x); } catch(e){ y = NaN; }
|
||||||
|
if (!isFinite(y) || isNaN(y) || y < -1e4 || y > 1e4){ prevValid = false; continue; }
|
||||||
|
d += (prevValid ? ' L' : ' M') + toX(x).toFixed(2) + ',' + toY(y).toFixed(2);
|
||||||
|
prevValid = true;
|
||||||
|
}
|
||||||
|
return '<path d="'+d+'" stroke="'+color+'" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
}
|
||||||
|
function pointWithDrop(x, fx, toX, toY, color, label){
|
||||||
|
const px = toX(x), py = toY(fx);
|
||||||
|
let s = '';
|
||||||
|
s += '<line x1="'+px+'" y1="'+py+'" x2="'+px+'" y2="'+toY(0)+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
|
||||||
|
s += '<line x1="'+px+'" y1="'+py+'" x2="'+toX(0)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
|
||||||
|
s += '<circle cx="'+px+'" cy="'+py+'" r="4.5" fill="'+color+'" stroke="#fff" stroke-width="2"/>';
|
||||||
|
if (label){
|
||||||
|
s += '<text x="'+(px+8)+'" y="'+(py-8)+'" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="'+color+'">'+label+'</text>';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
function asymptote(orientation, value, toX, toY, xmin, xmax, ymin, ymax, color){
|
||||||
|
color = color || '#94a3b8';
|
||||||
|
if (orientation === 'h'){
|
||||||
|
const y = toY(value);
|
||||||
|
return '<line x1="'+toX(xmin)+'" y1="'+y+'" x2="'+toX(xmax)+'" y2="'+y+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
|
||||||
|
} else {
|
||||||
|
const x = toX(value);
|
||||||
|
return '<line x1="'+x+'" y1="'+toY(ymin)+'" x2="'+x+'" y2="'+toY(ymax)+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function snapToValue(value, snapPoints, tolerance){
|
||||||
|
tolerance = tolerance || 0.1;
|
||||||
|
for (const sp of snapPoints){
|
||||||
|
if (Math.abs(value - sp) < tolerance) return sp;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function rightAngleMark(V, uIn, wIn, s){
|
||||||
|
s = s || 9;
|
||||||
|
const p1 = {x: V.x + s*uIn.x, y: V.y + s*uIn.y};
|
||||||
|
const c = {x: p1.x + s*wIn.x, y: p1.y + s*wIn.y};
|
||||||
|
const p2 = {x: V.x + s*wIn.x, y: V.y + s*wIn.y};
|
||||||
|
return p1.x+','+p1.y+' '+c.x+','+c.y+' '+p2.x+','+p2.y;
|
||||||
|
}
|
||||||
|
function angleArcAuto(V, uA, uB, R){
|
||||||
|
const sA = {x: V.x + R*uA.x, y: V.y + R*uA.y};
|
||||||
|
const eB = {x: V.x + R*uB.x, y: V.y + R*uB.y};
|
||||||
|
const cross = uA.x*uB.y - uA.y*uB.x;
|
||||||
|
const sweep = cross > 0 ? 1 : 0;
|
||||||
|
return 'M'+sA.x+','+sA.y+' A'+R+','+R+' 0 0,'+sweep+' '+eB.x+','+eB.y;
|
||||||
|
}
|
||||||
|
function unitVec(p1, p2){
|
||||||
|
const dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||||
|
const len = Math.sqrt(dx*dx + dy*dy) || 1;
|
||||||
|
return {x: dx/len, y: dy/len};
|
||||||
|
}
|
||||||
|
function deg2rad(d){ return d * Math.PI / 180; }
|
||||||
|
|
||||||
|
const ICONS = {
|
||||||
|
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
|
||||||
|
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
||||||
|
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
|
||||||
|
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
|
||||||
|
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
|
||||||
|
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
|
||||||
|
};
|
||||||
|
|
||||||
|
function secNavFor(curId){
|
||||||
|
const idx = PARAS.findIndex(p => p.id === curId);
|
||||||
|
const prev = idx > 0 ? PARAS[idx-1].id : null;
|
||||||
|
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
|
||||||
|
return secNav(prev, next);
|
||||||
|
}
|
||||||
|
function secNav(prev, next){
|
||||||
|
function lbl(id){ if(!id) return ''; const p=PARAS.find(x=>x.id===id); return p?p.num:id; }
|
||||||
|
let h='<div class="sec-nav">';
|
||||||
|
h+=prev?'<button class="btn" onclick="goTo(\''+prev+'\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> '+lbl(prev)+'</button>':'<span></span>';
|
||||||
|
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+lbl(next)+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
|
||||||
|
h+='</div>'; return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readButton(paraId){
|
||||||
|
const p = PARAS.find(x => x.id === paraId);
|
||||||
|
const labelTail = p && p.final ? 'финал' : (p ? p.num : '?');
|
||||||
|
return '<div style="margin-top:18px;display:flex;justify-content:center">'
|
||||||
|
+'<button class="btn primary" id="'+paraId+'-read-btn">'
|
||||||
|
+'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
|
||||||
|
+' Я прочитал \u2014 '+labelTail+' (+10 XP)'
|
||||||
|
+'</button></div>';
|
||||||
|
}
|
||||||
|
function wireReadBtn(paraId){
|
||||||
|
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
|
||||||
|
btn.addEventListener('click', ()=>{
|
||||||
|
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
|
||||||
|
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
|
||||||
|
const aId = paraId+'_done';
|
||||||
|
if(ACH_LABELS[aId]) achievement(aId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSorter(cfg){
|
||||||
|
const placed = {}; const pool = document.getElementById(cfg.poolId); const scope = document.querySelector(cfg.scopeSelector);
|
||||||
|
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
|
||||||
|
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
|
||||||
|
let armed = null;
|
||||||
|
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
|
||||||
|
function attach(elm,itId){ let ghost=null,dragging=false,sx=0,sy=0; elm.addEventListener('pointerdown',ev=>{ if(ev.button!==undefined&&ev.button!==0) return;
|
||||||
|
ev.preventDefault(); if(ev.target.classList&&ev.target.classList.contains('dnd-x')){ ev.stopPropagation(); if(placed[itId]){delete placed[itId];render();}else if(armed===itId){armed=null;render();} return; } sx=ev.clientX;sy=ev.clientY; const r=elm.getBoundingClientRect(); const ox=ev.clientX-r.left,oy=ev.clientY-r.top; try{elm.setPointerCapture(ev.pointerId);}catch(e){} function onMove(e){ const dx=e.clientX-sx,dy=e.clientY-sy; if(!dragging&&Math.hypot(dx,dy)>8){ dragging=true; ghost=elm.cloneNode(true); ghost.classList.remove('armed'); ghost.style.cssText='position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:'+r.width+'px;left:'+(e.clientX-ox)+'px;top:'+(e.clientY-oy)+'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if(dragging&&ghost){ ghost.style.left=(e.clientX-ox)+'px';ghost.style.top=(e.clientY-oy)+'px'; const under=document.elementsFromPoint(e.clientX,e.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); const tgt=under.find(n=>n.classList&&(n.classList.contains('drop-box')||n.classList.contains('dnd-pool'))); if(tgt)tgt.classList.add('over'); } } function onUp(e){ elm.removeEventListener('pointermove',onMove);elm.removeEventListener('pointerup',onUp);elm.removeEventListener('pointercancel',onUp);elm.classList.remove('dragging'); if(ghost){ghost.remove();ghost=null;} scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); if(dragging){ const under=document.elementsFromPoint(e.clientX,e.clientY); const box=under.find(n=>n.classList&&n.classList.contains('drop-box')); const pl=under.find(n=>n.classList&&n.classList.contains('dnd-pool')); if(box){const di=box.querySelector('[data-cat]');if(di){placed[itId]=di.dataset.cat;armed=null;render();return;}}else if(pl){delete placed[itId];armed=null;render();return;} }else{ if(placed[itId]){delete placed[itId];armed=null;render();}else{armed=(armed===itId)?null:itId;render();} } dragging=false; } elm.addEventListener('pointermove',onMove);elm.addEventListener('pointerup',onUp);elm.addEventListener('pointercancel',onUp); }); }
|
||||||
|
function attachBoxTaps(){ scope.querySelectorAll('.drop-box').forEach(box=>{ box.addEventListener('click',ev=>{ if(!armed)return; if(ev.target.closest('.dnd-chip'))return; const di=box.querySelector('[data-cat]'); if(di){placed[armed]=di.dataset.cat;armed=null;render();} }); }); }
|
||||||
|
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); cfg.cats.forEach(cat=>{const box=scope.querySelector('.drop-items[data-cat="'+cat+'"]');if(!box)return;box.innerHTML='';cfg.items.forEach(it=>{if(placed[it.id]!==cat)return;box.appendChild(buildChip(it,true));});}); if(window.renderMathInElement)try{renderMath(scope);}catch(_){} }
|
||||||
|
attachBoxTaps(); render();
|
||||||
|
return {placed,render,reset(){ for(const k in placed)delete placed[k];armed=null;render(); }};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStub(id, name, phase){
|
||||||
|
return '<div class="card" style="background:linear-gradient(135deg,var(--sec-acc-soft),var(--card));border:1.5px dashed var(--sec-acc)">'
|
||||||
|
+ '<div class="card-header"><div class="card-icon theory">'+ICONS.theory+'</div><div class="card-title">В разработке</div></div>'
|
||||||
|
+ '<div class="card-body"><p>Контент <b>'+name+'</b> будет реализован в <b>'+phase+'</b> по плану <code>PLAN_PHYSICS_8.md</code>.</p>'
|
||||||
|
+ '<p style="margin-top:8px;color:var(--muted);font-size:.9rem">Phase 0 \u2014 это каркас (skeleton). Все 4 интерактива, 3 теоретические карточки и тренажёр задач будут добавлены в волне.</p>'
|
||||||
|
+ '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Search ===== */
|
||||||
|
const SEARCH_INDEX = (function(){
|
||||||
|
const arr=[];
|
||||||
|
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
|
||||||
|
return arr;
|
||||||
|
})();
|
||||||
|
function initSearch(){
|
||||||
|
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
|
||||||
|
if(!modal||!inp||!out) return;
|
||||||
|
let cur=0,rows=[];
|
||||||
|
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
|
||||||
|
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
|
||||||
|
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'\u2026':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
|
||||||
|
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
|
||||||
|
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
|
||||||
|
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
|
||||||
|
function close(){ modal.classList.remove('show'); }
|
||||||
|
btn&&btn.addEventListener('click',open);
|
||||||
|
modal.addEventListener('click',e=>{if(e.target===modal)close();});
|
||||||
|
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
|
||||||
|
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
|
||||||
|
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSidebarToggle(){
|
||||||
|
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
|
||||||
|
if(!side||!btn) return;
|
||||||
|
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||||
|
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||||
|
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
|
||||||
|
back.addEventListener('click',close);
|
||||||
|
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||||
|
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo(PARAS[0].id);
|
||||||
|
setTimeout(()=>achievement('start'), 600);
|
||||||
|
if(window.LS&&window.LS.xp){
|
||||||
|
window.LS.xp.load().then(function(s){ if(s&&s.xp>STATE.xp){ STATE.xp=s.xp; STATE.level=calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if(STATE.current) buildSidebar(STATE.current); } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,856 @@
|
|||||||
|
<!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>Физика 8 класс — учебник</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:#faf5ff; --card:#fff;
|
||||||
|
--text:#0f172a; --muted:#475569;
|
||||||
|
--border:#e9d5ff;
|
||||||
|
--pri:#7c3aed; --pri-d:#5b21b6;
|
||||||
|
--pri-soft:#ede9fe;
|
||||||
|
--ch1:#dc2626; --ch1-d:#991b1b;
|
||||||
|
--ch2:#d97706; --ch2-d:#92400e;
|
||||||
|
--ch3:#0891b2; --ch3-d:#0e7490;
|
||||||
|
--ch4:#10b981; --ch4-d:#047857;
|
||||||
|
--sh:0 4px 16px rgba(124,58,237,.10);
|
||||||
|
--sh-h:0 12px 36px rgba(124,58,237,.18);
|
||||||
|
}
|
||||||
|
html.dark{
|
||||||
|
--bg:#15102a; --card:#1f1640;
|
||||||
|
--text:#ede9fe; --muted:#a78bfa;
|
||||||
|
--border:#3b2871;
|
||||||
|
--pri-soft:rgba(124,58,237,.18);
|
||||||
|
}
|
||||||
|
*{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,#312e81 0%,#7c3aed 55%,#c4b5fd 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(237,233,254,.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(237,233,254,.14);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,.14);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,.24)}
|
||||||
|
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.85rem;font-weight:900;letter-spacing:-.01em}
|
||||||
|
.hdr-sub{font-size:.92rem;opacity:.88;margin-top:4px}
|
||||||
|
.hdr-side{margin-left:auto;display:flex;gap:8px}
|
||||||
|
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.14);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,.24)}
|
||||||
|
.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(196,181,253,.18));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,#7c3aed,#c4b5fd);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}
|
||||||
|
.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(124,58,237,.14);border-radius:5px;overflow:hidden;margin-top:6px}
|
||||||
|
.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#c4b5fd);border-radius:5px;transition:width .5s}
|
||||||
|
.po-xp{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,#a78bfa,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(124,58,237,.24)}
|
||||||
|
|
||||||
|
/* CHAPTER GRID — 4 sections: 3 chapters + lab */
|
||||||
|
.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:-8px;top:-22px;font-size:5.2rem;font-weight:900;font-family:'Outfit',sans-serif;line-height:1;color:rgba(255,255,255,.20);pointer-events:none;letter-spacing:-.04em}
|
||||||
|
.ch-num{display:inline-block;padding:4px 10px;background:rgba(255,255,255,.22);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:.88;margin-top:4px;position:relative;z-index:1;font-weight:500}
|
||||||
|
|
||||||
|
.ch-cover.ch1{background:linear-gradient(135deg,#7f1d1d,#dc2626 60%,#f87171)}
|
||||||
|
.ch-cover.ch2{background:linear-gradient(135deg,#78350f,#d97706 60%,#fbbf24)}
|
||||||
|
.ch-cover.ch3{background:linear-gradient(135deg,#164e63,#0891b2 60%,#22d3ee)}
|
||||||
|
.ch-cover.ch4{background:linear-gradient(135deg,#064e3b,#10b981 60%,#6ee7b7)}
|
||||||
|
|
||||||
|
.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.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-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.ch1-card .ch-action{background:linear-gradient(135deg,var(--ch1),#f87171)}
|
||||||
|
.ch-card.ch2-card .ch-action{background:linear-gradient(135deg,var(--ch2),#fbbf24)}
|
||||||
|
.ch-card.ch3-card .ch-action{background:linear-gradient(135deg,var(--ch3),#22d3ee)}
|
||||||
|
.ch-card.ch4-card .ch-action{background:linear-gradient(135deg,var(--ch4),#6ee7b7)}
|
||||||
|
|
||||||
|
/* 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:#a855f7;box-shadow:0 0 0 3px rgba(168,85,247,.22)}
|
||||||
|
.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,#a78bfa,#7c3aed)}
|
||||||
|
.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:#5b21b6}
|
||||||
|
|
||||||
|
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
|
||||||
|
|
||||||
|
/* 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,#312e81 0%,#7c3aed 55%,#a78bfa 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,.18);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,.22);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.18rem;font-weight:800;color:var(--text);margin:8px 0 14px;letter-spacing:-.005em;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 SHEET */
|
||||||
|
.cheat-grid{display:grid;grid-template-columns:1fr;gap:14px;margin-bottom:28px}
|
||||||
|
@media(min-width:680px){.cheat-grid{grid-template-columns:1fr 1fr}}
|
||||||
|
@media(min-width:1000px){.cheat-grid{grid-template-columns:repeat(3,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:linear-gradient(180deg,var(--ch1),var(--ch1-d))}
|
||||||
|
.cheat-card.c2::before{background:linear-gradient(180deg,var(--ch2),var(--ch2-d))}
|
||||||
|
.cheat-card.c3::before{background:linear-gradient(180deg,var(--ch3),var(--ch3-d))}
|
||||||
|
.cheat-head{display:flex;align-items:center;gap:9px;margin-bottom:9px;padding-left:6px}
|
||||||
|
.cheat-badge{font-size:.7rem;font-weight:800;padding:2px 8px;border-radius:99px;color:#fff;letter-spacing:.05em;text-transform:uppercase}
|
||||||
|
.cheat-card.c1 .cheat-badge{background:var(--ch1)}
|
||||||
|
.cheat-card.c2 .cheat-badge{background:var(--ch2)}
|
||||||
|
.cheat-card.c3 .cheat-badge{background:var(--ch3)}
|
||||||
|
.cheat-title{font-weight:800;color:var(--text);font-size:.98rem}
|
||||||
|
.cheat-list{list-style:none;padding-left:6px;margin:0}
|
||||||
|
.cheat-list li{padding:6px 0;border-bottom:1px dashed var(--border);font-size:.92rem;line-height:1.5;color:var(--text)}
|
||||||
|
.cheat-list li:last-child{border-bottom:0}
|
||||||
|
|
||||||
|
/* BOSS PROGRESS */
|
||||||
|
.boss-overall-bar{background:linear-gradient(135deg,rgba(124,58,237,.08),rgba(196,181,253,.10));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(124,58,237,.14);border-radius:5px;overflow:hidden}
|
||||||
|
.boss-overall-bar .fill{height:100%;background:linear-gradient(90deg,var(--pri),#c4b5fd,#a78bfa);transition:width .5s;border-radius:5px}
|
||||||
|
|
||||||
|
/* BOSS CARDS */
|
||||||
|
.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,transform .2s}
|
||||||
|
.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:.7rem;font-weight:800;padding:3px 9px;border-radius:99px;background:rgba(124,58,237,.14);color:var(--pri-d);letter-spacing:.04em;text-transform:uppercase}
|
||||||
|
html.dark .boss-tag{color:#c4b5fd}
|
||||||
|
.boss-title{font-family:'Outfit',sans-serif;font-weight:800;color:var(--text);font-size:1.02rem;flex:1;min-width:0}
|
||||||
|
.boss-q{padding:12px 14px;background:rgba(124,58,237,.06);border-radius:10px;font-size:.96rem;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;transition:border-color .15s}
|
||||||
|
.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:background .15s,border-color .15s,transform .1s}
|
||||||
|
.boss-btn:hover{background:var(--pri-soft);border-color:var(--pri)}
|
||||||
|
.boss-btn:active{transform:scale(.96)}
|
||||||
|
.boss-btn.primary{background:linear-gradient(135deg,var(--pri),#a78bfa);color:#fff;border-color:transparent}
|
||||||
|
.boss-btn.primary:hover{filter:brightness(1.08)}
|
||||||
|
.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(245,158,11,.12);border-left:3px solid #f59e0b;border-radius:6px;font-size:.86rem;color:var(--text);display:none;line-height:1.5}
|
||||||
|
.boss-hint-txt.show{display:block}
|
||||||
|
|
||||||
|
/* FINAL CTA */
|
||||||
|
.final-cta{margin-top:24px;padding:18px 20px;border-radius:14px;background:linear-gradient(135deg,#ede9fe,#ddd6fe);border:1.5px solid #a78bfa;display:none;align-items:center;gap:14px;flex-wrap:wrap}
|
||||||
|
.final-cta.show{display:flex}
|
||||||
|
html.dark .final-cta{background:linear-gradient(135deg,rgba(124,58,237,.22),rgba(91,33,182,.18));border-color:#7c3aed}
|
||||||
|
.final-cta-icon{width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg,#a78bfa,#7c3aed);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:#5b21b6;font-size:1.05rem;font-family:'Outfit',sans-serif}
|
||||||
|
html.dark .final-cta-title{color:#ddd6fe}
|
||||||
|
.final-cta-sub{font-size:.86rem;color:#6d28d9;margin-top:2px}
|
||||||
|
html.dark .final-cta-sub{color:#c4b5fd}
|
||||||
|
.final-cta-btn{padding:10px 18px;border-radius:10px;background:linear-gradient(135deg,var(--pri),#a78bfa);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}
|
||||||
|
</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>Физика — 8 класс</h1>
|
||||||
|
<div class="hdr-sub">Полный курс физики 8 класса: тепловые явления, электромагнитные явления, световые явления, лабораторный практикум</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">f</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/physics-8-ch1" class="ch-card ch1-card" id="ch-1">
|
||||||
|
<div class="ch-cover ch1">
|
||||||
|
<div class="ch-cover-wm">Q</div>
|
||||||
|
<div class="ch-num">Глава 1</div>
|
||||||
|
<div class="ch-title">Тепловые явления</div>
|
||||||
|
<div class="ch-range">§1–§11 + Финал</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-8-ch2" class="ch-card ch2-card" id="ch-2">
|
||||||
|
<div class="ch-cover ch2">
|
||||||
|
<div class="ch-cover-wm">I</div>
|
||||||
|
<div class="ch-num">Глава 2</div>
|
||||||
|
<div class="ch-title">Электромагнитные явления</div>
|
||||||
|
<div class="ch-range">§12–§31 + Финал</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-8-ch3" class="ch-card ch3-card" id="ch-3">
|
||||||
|
<div class="ch-cover ch3">
|
||||||
|
<div class="ch-cover-wm">F</div>
|
||||||
|
<div class="ch-num">Глава 3</div>
|
||||||
|
<div class="ch-title">Световые явления</div>
|
||||||
|
<div class="ch-range">§32–§40 + Финал</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-8-lab" class="ch-card ch4-card" id="ch-4">
|
||||||
|
<div class="ch-cover ch4">
|
||||||
|
<div class="ch-cover-wm">ЛР</div>
|
||||||
|
<div class="ch-num">Лаборатория</div>
|
||||||
|
<div class="ch-title">Лабораторный практикум</div>
|
||||||
|
<div class="ch-range">7 виртуальных ЛР</div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-body">
|
||||||
|
<div class="ch-desc">7 виртуальных лабораторных работ: теплообмен, удельная теплоёмкость, простейшая цепь, последовательное и параллельное соединения, работа и мощность тока, отражение света.</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">Шпаргалка курса и 10 интегрированных боссов по всем 3 главам. Победи всех — получи «Магистр физики 8» и +150 XP.</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>$Q = cm\Delta T$ — нагрев / охлаждение</li>
|
||||||
|
<li>$Q = qm$ — сгорание топлива</li>
|
||||||
|
<li>$Q = \lambda m$ — плавление</li>
|
||||||
|
<li>$Q = L m$ — парообразование</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>$q = Ne$, $e = 1{,}6 \cdot 10^{-19}$ Кл</li>
|
||||||
|
<li>$A = qU$, $U = A/q$</li>
|
||||||
|
<li>$I = q/t$, $I = U/R$ — закон Ома</li>
|
||||||
|
<li>$R = \rho l / S$</li>
|
||||||
|
<li>Последов.: $R = R_1+R_2$, $U = U_1+U_2$</li>
|
||||||
|
<li>Паралл.: $1/R = 1/R_1 + 1/R_2$</li>
|
||||||
|
<li>$P = UI$, $A = UIt$, $Q = I^2 Rt$</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>$c = 3 \cdot 10^8$ м/с</li>
|
||||||
|
<li>Закон отражения: $\alpha = \beta$</li>
|
||||||
|
<li>Закон Снеллиуса: $\dfrac{\sin\alpha}{\sin\beta} = n$</li>
|
||||||
|
<li>$D = 1/F$ (дптр)</li>
|
||||||
|
<li>$\dfrac{1}{F} = \dfrac{1}{d} + \dfrac{1}{f}$ (тонкая линза)</li>
|
||||||
|
<li>Близо-: $D < 0$; дально-: $D > 0$</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>
|
||||||
|
10 интегрированных боссов
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="boss-overall-bar">
|
||||||
|
<div class="lab" id="fin-boss-lab">Боссов побеждено: 0 / 10</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">Курс Физика 8 пройден!</div>
|
||||||
|
<div class="final-cta-sub">Вы прошли всю итоговую проверку курса. +150 XP, ачивка «Магистр физики 8» получена.</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">Магистр физики 8</div>
|
||||||
|
<div class="ach-sub" id="ach-sub">Прочитайте все 40 параграфов и выполните все 7 лабораторных работ, чтобы получить достижение</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">
|
||||||
|
Интерактивный учебник «Физика — 8 класс» · LearnSpace
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* THEME */
|
||||||
|
(function(){
|
||||||
|
var saved = localStorage.getItem('physics8_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('physics8_theme', dark ? 'dark' : 'light');
|
||||||
|
localStorage.setItem('theme', dark ? 'dark' : 'light');
|
||||||
|
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
/* PROGRESS */
|
||||||
|
var TOTAL = 47; // 11 + 20 + 9 + 7
|
||||||
|
var CH_PARA = {
|
||||||
|
'physics-8-ch1': 11,
|
||||||
|
'physics-8-ch2': 20,
|
||||||
|
'physics-8-ch3': 9,
|
||||||
|
'physics-8-lab': 7
|
||||||
|
};
|
||||||
|
var CH_IDX = {
|
||||||
|
'physics-8-ch1': 1,
|
||||||
|
'physics-8-ch2': 2,
|
||||||
|
'physics-8-ch3': 3,
|
||||||
|
'physics-8-lab': 4
|
||||||
|
};
|
||||||
|
|
||||||
|
function setChProg(idx, readCount, total) {
|
||||||
|
var pct = total ? 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 = (idx === 4 ? 'Продолжить ЛР' : 'Продолжить');
|
||||||
|
else if (readCount >= total) btnEl.textContent = 'Открыть снова';
|
||||||
|
else btnEl.textContent = (idx === 4 ? 'Открыть практикум' : 'Открыть главу');
|
||||||
|
}
|
||||||
|
return pct;
|
||||||
|
}
|
||||||
|
|
||||||
|
var FIN_ACH_KEY = 'physics8_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 read = ch.progress ? ch.progress.read.length : 0;
|
||||||
|
var total = ch.para_count || CH_PARA[ch.slug] || 1;
|
||||||
|
totalRead += read;
|
||||||
|
setChProg(idx, read, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pct = Math.round(totalRead * 100 / TOTAL);
|
||||||
|
var 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('physics8_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) {
|
||||||
|
if (mastered) sub.textContent = 'Выполнено! Вы — Магистр физики 8.';
|
||||||
|
else sub.textContent = 'Выполнено! Вы прошли весь курс физики 8 класса.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* COURSE FINAL — lazy bosses */
|
||||||
|
var FIN_BOSS_KEY = 'physics8_course_bosses';
|
||||||
|
|
||||||
|
var FIN_BOSSES = [
|
||||||
|
{
|
||||||
|
n: 1,
|
||||||
|
title: 'Электрочайник: тепло + работа тока',
|
||||||
|
tag: 'Гл. 1 + 2',
|
||||||
|
q: 'Электрочайник нагревает $m = 1$ кг воды от $t_1 = 20$ °C до кипения ($t_2 = 100$ °C) за время $\\tau = 5$ мин. Сетевое напряжение $U = 220$ В, КПД $\\eta = 100\\%$. Найди силу тока $I$ в А (округли до сотых). ($c_{воды} = 4200$ Дж/(кг·К))',
|
||||||
|
hint: '$Q = cm\\Delta T = 4200 \\cdot 1 \\cdot 80 = 336\\,000$ Дж. Работа тока $A = UIt$, при $\\eta=100\\%$ $A=Q$: $I = \\dfrac{Q}{Ut} = \\dfrac{336\\,000}{220 \\cdot 300} = 5{,}09$ А.',
|
||||||
|
ans: 5.09,
|
||||||
|
tol: 0.05,
|
||||||
|
step: '0.01'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 2,
|
||||||
|
title: 'Плавление льда электроплиткой',
|
||||||
|
tag: 'Гл. 1 + 2',
|
||||||
|
q: 'Сколько энергии в кВт·ч нужно, чтобы расплавить $m = 2$ кг льда при $t = 0$ °C? ($\\lambda_{льда} = 3{,}34 \\cdot 10^5$ Дж/кг; округли до сотых).',
|
||||||
|
hint: '$Q = \\lambda m = 3{,}34 \\cdot 10^5 \\cdot 2 = 6{,}68 \\cdot 10^5$ Дж $= \\dfrac{6{,}68\\cdot10^5}{3{,}6\\cdot10^6} \\approx 0{,}19$ кВт·ч.',
|
||||||
|
ans: 0.19,
|
||||||
|
tol: 0.01,
|
||||||
|
step: '0.01'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 3,
|
||||||
|
title: 'Закон Джоуля — Ленца',
|
||||||
|
tag: 'Гл. 2',
|
||||||
|
q: 'По нихромовой спирали с сопротивлением $R = 50$ Ом течёт ток $I = 4$ А в течение $\\tau = 10$ мин. Сколько теплоты выделится в Дж?',
|
||||||
|
hint: '$Q = I^2 R t = 16 \\cdot 50 \\cdot 600 = 480\\,000$ Дж.',
|
||||||
|
ans: 480000,
|
||||||
|
tol: 1000,
|
||||||
|
step: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 4,
|
||||||
|
title: 'Смешанная цепь',
|
||||||
|
tag: 'Гл. 2',
|
||||||
|
q: 'К батарейке $U = 12$ В подключены последовательно резистор $R_1 = 4$ Ом и параллельный блок из двух резисторов $R_2 = R_3 = 6$ Ом. Найди общий ток в цепи в А.',
|
||||||
|
hint: 'Паралл. блок: $R_{23} = \\dfrac{6 \\cdot 6}{6+6} = 3$ Ом. Всего: $R = R_1 + R_{23} = 7$ Ом. $I = U/R = 12/7 \\approx 1{,}71$ А.',
|
||||||
|
ans: 1.71,
|
||||||
|
tol: 0.05,
|
||||||
|
step: '0.01'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 5,
|
||||||
|
title: 'Сопротивление провода',
|
||||||
|
tag: 'Гл. 2',
|
||||||
|
q: 'Найди сопротивление медного провода длиной $l = 100$ м и площадью сечения $S = 0{,}5$ мм² в Ом. ($\\rho_{меди} = 0{,}017$ Ом·мм²/м; округли до десятых)',
|
||||||
|
hint: '$R = \\rho l/S = 0{,}017 \\cdot 100 / 0{,}5 = 3{,}4$ Ом.',
|
||||||
|
ans: 3.4,
|
||||||
|
tol: 0.1,
|
||||||
|
step: '0.01'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 6,
|
||||||
|
title: 'Преломление',
|
||||||
|
tag: 'Гл. 3',
|
||||||
|
q: 'Луч переходит из воздуха ($n_1 = 1$) в воду ($n_2 = 1{,}33$). Угол падения $\\alpha = 45°$. Найди угол преломления $\\beta$ в градусах (округли до десятых).',
|
||||||
|
hint: '$\\sin\\beta = \\dfrac{n_1 \\sin\\alpha}{n_2} = \\dfrac{\\sin 45°}{1{,}33} \\approx \\dfrac{0{,}707}{1{,}33} \\approx 0{,}532$, $\\beta \\approx 32{,}1°$.',
|
||||||
|
ans: 32.1,
|
||||||
|
tol: 0.5,
|
||||||
|
step: '0.1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 7,
|
||||||
|
title: 'Тонкая линза',
|
||||||
|
tag: 'Гл. 3',
|
||||||
|
q: 'Собирающая линза с $F = 20$ см. Предмет на расстоянии $d = 30$ см от линзы. Найди расстояние от линзы до изображения $f$ в см.',
|
||||||
|
hint: '$\\dfrac{1}{F} = \\dfrac{1}{d} + \\dfrac{1}{f} \\Rightarrow f = \\dfrac{dF}{d-F} = \\dfrac{30 \\cdot 20}{30-20} = 60$ см.',
|
||||||
|
ans: 60,
|
||||||
|
tol: 1,
|
||||||
|
step: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 8,
|
||||||
|
title: 'Очки',
|
||||||
|
tag: 'Гл. 3',
|
||||||
|
q: 'Близорукому нужны очки с фокусным расстоянием $|F| = 50$ см. Какая оптическая сила линзы в дптр? Знак учти (для рассеивающей линзы — отрицательный).',
|
||||||
|
hint: '$D = 1/F = 1/(-0{,}5) = -2$ дптр.',
|
||||||
|
ans: -2,
|
||||||
|
tol: 0.1,
|
||||||
|
step: '0.1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 9,
|
||||||
|
title: 'Стоимость электроэнергии',
|
||||||
|
tag: 'Гл. 2',
|
||||||
|
q: 'В квартире лампа мощностью $P = 100$ Вт горит $6$ часов в день. Тариф $t = 0{,}20$ руб/(кВт·ч). Сколько рублей нужно заплатить за месяц (30 дней)? Округли до сотых.',
|
||||||
|
hint: 'Энергия за месяц: $W = P \\cdot \\tau = 0{,}1 \\cdot 6 \\cdot 30 = 18$ кВт·ч. Стоимость: $18 \\cdot 0{,}20 = 3{,}60$ руб.',
|
||||||
|
ans: 3.6,
|
||||||
|
tol: 0.05,
|
||||||
|
step: '0.01'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 10,
|
||||||
|
title: 'Магистр физики 8',
|
||||||
|
tag: 'синтез курса',
|
||||||
|
q: 'Электролампа $U=220$ В мощностью $P=60$ Вт горит на сетчатке глаза изображение нити. До лампы $d=2$ м, фокус хрусталика глаза $F=2{,}0$ см. Найди $f$ (расстояние от хрусталика до изображения) в см. Округли до сотых.',
|
||||||
|
hint: '$\\dfrac{1}{f} = \\dfrac{1}{F} - \\dfrac{1}{d} = \\dfrac{1}{0{,}02} - \\dfrac{1}{2} = 50 - 0{,}5 = 49{,}5$ м$^{-1}$. $f = 1/49{,}5 \\approx 0{,}0202$ м $= 2{,}02$ см.',
|
||||||
|
ans: 2.02,
|
||||||
|
tol: 0.02,
|
||||||
|
step: '0.01'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
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');
|
||||||
|
var 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');
|
||||||
|
|
||||||
|
/* +150 XP */
|
||||||
|
var xp = parseInt(localStorage.getItem('physics8_xp') || '0', 10) || 0;
|
||||||
|
localStorage.setItem('physics8_xp', String(xp + 150));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (window.LS && typeof window.LS.addXp === 'function') {
|
||||||
|
window.LS.addXp(150, 'physics8-master');
|
||||||
|
} else if (typeof window.addXp === 'function') {
|
||||||
|
window.addXp(150, 'physics8-master');
|
||||||
|
}
|
||||||
|
} catch(e){}
|
||||||
|
|
||||||
|
try { if (typeof window.confetti === 'function') window.confetti({particleCount: 220, spread: 110, origin: {y: .6}}); } catch(e){}
|
||||||
|
|
||||||
|
var strip = document.getElementById('ach-strip');
|
||||||
|
var sub = document.getElementById('ach-sub');
|
||||||
|
if (strip) strip.classList.add('lit');
|
||||||
|
if (sub) sub.textContent = 'Выполнено! Вы — Магистр физики 8.';
|
||||||
|
|
||||||
|
var cta = document.getElementById('final-cta');
|
||||||
|
if (cta) cta.classList.add('show');
|
||||||
|
|
||||||
|
var xpBadge = document.getElementById('hero-xp-badge');
|
||||||
|
if (xpBadge) {
|
||||||
|
var newXp = parseInt(localStorage.getItem('physics8_xp') || '0', 10) || 0;
|
||||||
|
xpBadge.style.display = '';
|
||||||
|
xpBadge.textContent = newXp + ' XP';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFinBoss(b, state){
|
||||||
|
var solvedClass = state[b.n] ? ' solved' : '';
|
||||||
|
var step = b.step || '1';
|
||||||
|
var displayAns = (typeof b.ans === 'number' && step !== '1') ? b.ans.toFixed(2) : b.ans;
|
||||||
|
return '<div class="boss-card' + solvedClass + '" id="fin-boss-' + 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" id="fin-boss-' + b.n + '-q">' + b.q + '</div>'
|
||||||
|
+ '<div class="boss-row">'
|
||||||
|
+ '<input type="number" step="' + step + '" class="boss-input" id="fin-boss-' + b.n + '-inp" placeholder="число"' + (state[b.n] ? ' value="' + displayAns + '" disabled' : '') + '>'
|
||||||
|
+ '<button class="boss-btn primary" id="fin-boss-' + b.n + '-go"' + (state[b.n] ? ' disabled' : '') + '>Атаковать</button>'
|
||||||
|
+ '<button class="boss-btn" id="fin-boss-' + b.n + '-hint">Подсказка</button>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="boss-hint-txt" id="fin-boss-' + b.n + '-hinttxt">' + b.hint + '</div>'
|
||||||
|
+ '<div class="boss-fb' + (state[b.n] ? ' ok' : '') + '" id="fin-boss-' + b.n + '-fb">' + (state[b.n] ? 'Победа! +15 XP. Босс уже повержен.' : '') + '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindFinBoss(b){
|
||||||
|
var state = loadFinBossState();
|
||||||
|
var goBtn = document.getElementById('fin-boss-' + b.n + '-go');
|
||||||
|
var hintBtn = document.getElementById('fin-boss-' + b.n + '-hint');
|
||||||
|
var inp = document.getElementById('fin-boss-' + b.n + '-inp');
|
||||||
|
var fb = document.getElementById('fin-boss-' + b.n + '-fb');
|
||||||
|
var hintTx = document.getElementById('fin-boss-' + b.n + '-hinttxt');
|
||||||
|
var card = document.getElementById('fin-boss-' + b.n + '-card');
|
||||||
|
if (!goBtn) return;
|
||||||
|
|
||||||
|
if (hintBtn) hintBtn.addEventListener('click', function(){
|
||||||
|
if (hintTx) hintTx.classList.toggle('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state[b.n]) return;
|
||||||
|
|
||||||
|
goBtn.addEventListener('click', function(){
|
||||||
|
var v = parseFloat((inp.value || '').replace(',', '.'));
|
||||||
|
if (isNaN(v)) {
|
||||||
|
fb.className = 'boss-fb fail';
|
||||||
|
fb.textContent = 'Введите число.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var tol = (typeof b.tol === 'number') ? b.tol : 1e-9;
|
||||||
|
if (Math.abs(v - b.ans) < tol) {
|
||||||
|
fb.className = 'boss-fb ok';
|
||||||
|
fb.textContent = 'Победа! +15 XP. Босс повержен.';
|
||||||
|
card.classList.add('solved');
|
||||||
|
goBtn.disabled = true;
|
||||||
|
inp.disabled = true;
|
||||||
|
|
||||||
|
var s = loadFinBossState();
|
||||||
|
if (!s[b.n]) {
|
||||||
|
s[b.n] = true;
|
||||||
|
saveFinBossState(s);
|
||||||
|
|
||||||
|
var xp = parseInt(localStorage.getItem('physics8_xp') || '0', 10) || 0;
|
||||||
|
localStorage.setItem('physics8_xp', String(xp + 15));
|
||||||
|
try {
|
||||||
|
if (window.LS && typeof window.LS.addXp === 'function') window.LS.addXp(15, 'fin-boss-' + b.n);
|
||||||
|
else if (typeof window.addXp === 'function') window.addXp(15, 'fin-boss-' + b.n);
|
||||||
|
} catch(e){}
|
||||||
|
|
||||||
|
var xpBadge = document.getElementById('hero-xp-badge');
|
||||||
|
if (xpBadge) {
|
||||||
|
var nXp = parseInt(localStorage.getItem('physics8_xp') || '0', 10) || 0;
|
||||||
|
xpBadge.style.display = '';
|
||||||
|
xpBadge.textContent = nXp + ' XP';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFinBossBar(s);
|
||||||
|
maybeUnlockMaster(s);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fb.className = 'boss-fb fail';
|
||||||
|
fb.textContent = 'Не то. Перепроверь решение и попробуй снова.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
inp.addEventListener('keydown', function(e){
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); goBtn.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();
|
||||||
|
var 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]);
|
||||||
|
|
||||||
|
var wrap = document.getElementById('course-final');
|
||||||
|
finRenderKatex(wrap);
|
||||||
|
|
||||||
|
updateFinBossBar(state);
|
||||||
|
|
||||||
|
if (localStorage.getItem(FIN_ACH_KEY) === '1') {
|
||||||
|
var cta = document.getElementById('final-cta');
|
||||||
|
if (cta) cta.classList.add('show');
|
||||||
|
var strip = document.getElementById('ach-strip');
|
||||||
|
var sub = document.getElementById('ach-sub');
|
||||||
|
if (strip) strip.classList.add('lit');
|
||||||
|
if (sub) sub.textContent = 'Выполнено! Вы — Магистр физики 8.';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 syncMasterOnLoad(){
|
||||||
|
if (localStorage.getItem(FIN_ACH_KEY) === '1') {
|
||||||
|
var strip = document.getElementById('ach-strip');
|
||||||
|
var sub = document.getElementById('ach-sub');
|
||||||
|
if (strip) strip.classList.add('lit');
|
||||||
|
if (sub) sub.textContent = 'Выполнено! Вы — Магистр физики 8.';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
function loadProgress() {
|
||||||
|
if (typeof window.LS === 'undefined' || typeof window.LS.api !== 'function') {
|
||||||
|
renderProgress([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.LS.api('/api/textbooks/physics-8/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>
|
||||||
@@ -0,0 +1,634 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<title>Физика 8 · Лабораторный практикум</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
|
||||||
|
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/g3d.js" defer></script>
|
||||||
|
<script src="/js/phys.js" defer></script>
|
||||||
|
<script src="/js/optics.js" defer></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#ecfdf5; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
|
||||||
|
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
|
||||||
|
--pri:#7c3aed; --pri2:#5b21b6; --pri-soft:#ede9fe;
|
||||||
|
--acc:#a78bfa; --acc2:#7c3aed; --acc-soft:#ede9fe;
|
||||||
|
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
|
||||||
|
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
|
||||||
|
}
|
||||||
|
.dark{--bg:#0a0a0e; --card:#13120a; --card-soft:#18160a; --text:#fef9e7; --ink:#fef9e7; --muted:#a39070; --border:#2a2512}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||||
|
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
|
||||||
|
button,input,select,textarea{font-family:inherit;font-size:inherit}
|
||||||
|
button{cursor:pointer;border:0;background:transparent;color:inherit}
|
||||||
|
a{color:inherit;text-decoration:none}
|
||||||
|
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
|
||||||
|
|
||||||
|
.hdr{position:relative;background:linear-gradient(110deg,#064e3b 0%,#10b981 55%,#86efac 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.2);min-height:130px}
|
||||||
|
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||||||
|
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
|
||||||
|
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
|
||||||
|
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||||
|
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
|
||||||
|
.hdr-btn:hover{background:rgba(255,255,255,.24)}
|
||||||
|
|
||||||
|
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
|
||||||
|
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
|
||||||
|
.col-main{min-width:0}
|
||||||
|
|
||||||
|
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
|
||||||
|
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
|
||||||
|
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
|
||||||
|
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
|
||||||
|
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
|
||||||
|
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
|
||||||
|
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
|
||||||
|
.hero-progress{flex:1;min-width:200px;max-width:280px}
|
||||||
|
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
|
||||||
|
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
|
||||||
|
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
|
||||||
|
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
|
||||||
|
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
|
||||||
|
|
||||||
|
.psel{margin-bottom:24px}
|
||||||
|
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
|
||||||
|
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
|
||||||
|
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
|
||||||
|
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
|
||||||
|
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
|
||||||
|
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
|
||||||
|
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
|
||||||
|
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
|
||||||
|
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
|
||||||
|
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
|
||||||
|
.psel-card.final{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft))}
|
||||||
|
.psel-card.final .psel-num{color:var(--warn)}
|
||||||
|
|
||||||
|
.sec[id="sec-lr1"]{ --sec-acc:#10b981; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
|
||||||
|
.sec[id="sec-lr2"]{ --sec-acc:#10b981; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
|
||||||
|
.sec[id="sec-lr3"]{ --sec-acc:#10b981; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
|
||||||
|
.sec[id="sec-lr4"]{ --sec-acc:#10b981; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
|
||||||
|
.sec[id="sec-lr5"]{ --sec-acc:#10b981; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
|
||||||
|
.sec[id="sec-lr6"]{ --sec-acc:#10b981; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
|
||||||
|
.sec[id="sec-lr7"]{ --sec-acc:#10b981; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
|
||||||
|
|
||||||
|
.sec{display:none;position:relative;animation:fadeIn .35s ease}
|
||||||
|
.sec.active{display:block}
|
||||||
|
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||||||
|
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
|
||||||
|
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
|
||||||
|
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
|
||||||
|
|
||||||
|
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.04);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
|
||||||
|
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(0,0,0,.08)}
|
||||||
|
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
|
||||||
|
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
|
||||||
|
.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}
|
||||||
|
.card-icon .ic{width:18px;height:18px}
|
||||||
|
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
|
||||||
|
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
|
||||||
|
.card-body{font-size:.94rem;line-height:1.65}
|
||||||
|
.card-body p{margin-bottom:8px}
|
||||||
|
.card-body p:last-child{margin-bottom:0}
|
||||||
|
|
||||||
|
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
|
||||||
|
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
|
||||||
|
.btn:active{transform:scale(.96)}
|
||||||
|
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
|
||||||
|
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
|
||||||
|
|
||||||
|
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
|
||||||
|
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
|
||||||
|
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
|
||||||
|
|
||||||
|
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
|
||||||
|
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
|
||||||
|
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||||
|
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
|
||||||
|
.sidecard-row b{color:var(--pri);font-weight:700}
|
||||||
|
.sidecard-row:last-child{margin-bottom:0}
|
||||||
|
@media(max-width:980px){.col-side{position:static;max-height:none}}
|
||||||
|
|
||||||
|
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
|
||||||
|
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
|
||||||
|
.xp-bar{height:9px;background:rgba(0,0,0,.10);border-radius:6px;overflow:hidden;margin:7px 0}
|
||||||
|
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
|
||||||
|
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
|
||||||
|
|
||||||
|
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
|
||||||
|
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
|
||||||
|
|
||||||
|
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
|
||||||
|
.ach-popup.show{display:flex}
|
||||||
|
|
||||||
|
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
|
||||||
|
.col-side-backdrop.show{display:block}
|
||||||
|
@media(max-width:980px){
|
||||||
|
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
|
||||||
|
.col-side.open{transform:none}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
|
||||||
|
.search-modal.show{display:flex}
|
||||||
|
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
|
||||||
|
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
|
||||||
|
.search-results{flex:1;overflow-y:auto;padding:6px 0}
|
||||||
|
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
|
||||||
|
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
|
||||||
|
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
|
||||||
|
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
|
||||||
|
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
|
||||||
|
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
|
||||||
|
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
|
||||||
|
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
|
||||||
|
|
||||||
|
.sec{transition:opacity .25s}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Физика 8 · Лабораторный практикум</h1>
|
||||||
|
<div class="hdr-sub">7 виртуальных лабораторных работ</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/physics-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 8</a>
|
||||||
|
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
|
||||||
|
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><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>Лабораторный практикум 8 класса</h2>
|
||||||
|
<p>7 виртуальных лабораторных работ по тепловым явлениям, электрическим цепям и оптике. Каждая работа — это симуляция эксперимента + измерения + расчёт + автоматический отчёт.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('lr1')"><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" data-gamified></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel">
|
||||||
|
<div class="psel-title">Параграфы главы</div>
|
||||||
|
<div id="psel-grid" class="psel-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="sec-lr1" class="sec"><div class="sec-header"><span class="sec-num">ЛР 1</span><h2 class="sec-h">Изучение явления теплообмена при смешивании воды разной температуры</h2></div><div id="lr1-body"></div></section>
|
||||||
|
<section id="sec-lr2" class="sec"><div class="sec-header"><span class="sec-num">ЛР 2</span><h2 class="sec-h">Определение удельной теплоёмкости твёрдого тела</h2></div><div id="lr2-body"></div></section>
|
||||||
|
<section id="sec-lr3" class="sec"><div class="sec-header"><span class="sec-num">ЛР 3</span><h2 class="sec-h">Сборка простейшей электрической цепи и измерение силы тока и напряжения</h2></div><div id="lr3-body"></div></section>
|
||||||
|
<section id="sec-lr4" class="sec"><div class="sec-header"><span class="sec-num">ЛР 4</span><h2 class="sec-h">Изучение последовательного соединения проводников</h2></div><div id="lr4-body"></div></section>
|
||||||
|
<section id="sec-lr5" class="sec"><div class="sec-header"><span class="sec-num">ЛР 5</span><h2 class="sec-h">Изучение параллельного соединения проводников</h2></div><div id="lr5-body"></div></section>
|
||||||
|
<section id="sec-lr6" class="sec"><div class="sec-header"><span class="sec-num">ЛР 6</span><h2 class="sec-h">Определение работы и мощности электрического тока</h2></div><div id="lr6-body"></div></section>
|
||||||
|
<section id="sec-lr7" class="sec"><div class="sec-header"><span class="sec-num">ЛР 7</span><h2 class="sec-h">Изучение явления отражения света</h2></div><div id="lr7-body"></div></section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
<div class="col-side-backdrop" id="col-side-backdrop"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Физика 8» · Лабораторный практикум · LearnSpace</footer>
|
||||||
|
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
<div id="search-modal" class="search-modal" role="dialog">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="search-input" class="search-input" placeholder="Поиск…" autocomplete="off">
|
||||||
|
<div id="search-results" class="search-results"></div>
|
||||||
|
<div class="search-foot"><span><kbd>↑↓</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const STATE = { current:'lr1', progress:{}, achievements:new Map(), xp:0, level:1 };
|
||||||
|
const TOTAL_PARAS = 7;
|
||||||
|
const _TB_SLUG = 'physics-8-lab';
|
||||||
|
const LS_PREFIX = 'physics8_lab';
|
||||||
|
const LS_XP = 'physics8_xp';
|
||||||
|
|
||||||
|
const PARAS = [
|
||||||
|
{ id:'lr1', num:'ЛР 1', name:'Изучение явления теплообмена при смешивании воды разной температуры', sub:'§ 6' },
|
||||||
|
{ id:'lr2', num:'ЛР 2', name:'Определение удельной теплоёмкости твёрдого тела', sub:'§ 6' },
|
||||||
|
{ id:'lr3', num:'ЛР 3', name:'Сборка простейшей электрической цепи и измерение силы тока и напряжения', sub:'§ 21' },
|
||||||
|
{ id:'lr4', num:'ЛР 4', name:'Изучение последовательного соединения проводников', sub:'§ 24' },
|
||||||
|
{ id:'lr5', num:'ЛР 5', name:'Изучение параллельного соединения проводников', sub:'§ 25' },
|
||||||
|
{ id:'lr6', num:'ЛР 6', name:'Определение работы и мощности электрического тока', sub:'§ 26' },
|
||||||
|
{ id:'lr7', num:'ЛР 7', name:'Изучение явления отражения света', sub:'§ 34' }
|
||||||
|
];
|
||||||
|
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
|
||||||
|
|
||||||
|
const ACH_LABELS = {
|
||||||
|
start:"Начало практикума!",
|
||||||
|
lr1_done:"Изучение явления теплообмена при смешивании воды разной температуры завершена!",
|
||||||
|
lr2_done:"Определение удельной теплоёмкости твёрдого тела завершена!",
|
||||||
|
lr3_done:"Сборка простейшей электрической цепи и измерение силы тока и напряжения завершена!",
|
||||||
|
lr4_done:"Изучение последовательного соединения проводников завершена!",
|
||||||
|
lr5_done:"Изучение параллельного соединения проводников завершена!",
|
||||||
|
lr6_done:"Определение работы и мощности электрического тока завершена!",
|
||||||
|
lr7_done:"Изучение явления отражения света завершена!",
|
||||||
|
lab_done:"Практикум пройден!"
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIDEBARS = {
|
||||||
|
lr1:{title:"Шпаргалка ЛР 1",rows:[["В разработке","Phase 6 Wave 1"]]},
|
||||||
|
lr2:{title:"Шпаргалка ЛР 2",rows:[["В разработке","Phase 6 Wave 1"]]},
|
||||||
|
lr3:{title:"Шпаргалка ЛР 3",rows:[["В разработке","Phase 6 Wave 1"]]},
|
||||||
|
lr4:{title:"Шпаргалка ЛР 4",rows:[["В разработке","Phase 6 Wave 1"]]},
|
||||||
|
lr5:{title:"Шпаргалка ЛР 5",rows:[["В разработке","Phase 6 Wave 1"]]},
|
||||||
|
lr6:{title:"Шпаргалка ЛР 6",rows:[["В разработке","Phase 6 Wave 1"]]},
|
||||||
|
lr7:{title:"Шпаргалка ЛР 7",rows:[["В разработке","Phase 6 Wave 1"]]}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TIPS=[
|
||||||
|
{sec:'lr1',html:"Параграф ЛР 1 будет реализован в Phase 6 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'lr2',html:"Параграф ЛР 2 будет реализован в Phase 6 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'lr3',html:"Параграф ЛР 3 будет реализован в Phase 6 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'lr4',html:"Параграф ЛР 4 будет реализован в Phase 6 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'lr5',html:"Параграф ЛР 5 будет реализован в Phase 6 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'lr6',html:"Параграф ЛР 6 будет реализован в Phase 6 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
|
||||||
|
{sec:'lr7',html:"Параграф ЛР 7 будет реализован в Phase 6 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."}
|
||||||
|
];
|
||||||
|
|
||||||
|
const BUILDERS = {
|
||||||
|
lr1: ()=>{ const box=document.getElementById('lr1-body'); box.innerHTML = buildStub('lr1', 'Изучение явления теплообмена при смешивании воды разной температуры', 'Phase 6 Wave 1') + secNavFor('lr1') + readButton('lr1'); renderMath(box); wireReadBtn('lr1'); },
|
||||||
|
lr2: ()=>{ const box=document.getElementById('lr2-body'); box.innerHTML = buildStub('lr2', 'Определение удельной теплоёмкости твёрдого тела', 'Phase 6 Wave 1') + secNavFor('lr2') + readButton('lr2'); renderMath(box); wireReadBtn('lr2'); },
|
||||||
|
lr3: ()=>{ const box=document.getElementById('lr3-body'); box.innerHTML = buildStub('lr3', 'Сборка простейшей электрической цепи и измерение силы тока и напряжения', 'Phase 6 Wave 1') + secNavFor('lr3') + readButton('lr3'); renderMath(box); wireReadBtn('lr3'); },
|
||||||
|
lr4: ()=>{ const box=document.getElementById('lr4-body'); box.innerHTML = buildStub('lr4', 'Изучение последовательного соединения проводников', 'Phase 6 Wave 1') + secNavFor('lr4') + readButton('lr4'); renderMath(box); wireReadBtn('lr4'); },
|
||||||
|
lr5: ()=>{ const box=document.getElementById('lr5-body'); box.innerHTML = buildStub('lr5', 'Изучение параллельного соединения проводников', 'Phase 6 Wave 1') + secNavFor('lr5') + readButton('lr5'); renderMath(box); wireReadBtn('lr5'); },
|
||||||
|
lr6: ()=>{ const box=document.getElementById('lr6-body'); box.innerHTML = buildStub('lr6', 'Определение работы и мощности электрического тока', 'Phase 6 Wave 1') + secNavFor('lr6') + readButton('lr6'); renderMath(box); wireReadBtn('lr6'); },
|
||||||
|
lr7: ()=>{ const box=document.getElementById('lr7-body'); box.innerHTML = buildStub('lr7', 'Изучение явления отражения света', 'Phase 6 Wave 1') + secNavFor('lr7') + readButton('lr7'); renderMath(box); wireReadBtn('lr7'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
|
||||||
|
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
|
||||||
|
|
||||||
|
function loadProgress(){
|
||||||
|
try{
|
||||||
|
const s=localStorage.getItem(LS_PREFIX+'_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
|
||||||
|
const a=localStorage.getItem(LS_PREFIX+'_achievements');
|
||||||
|
if(a){ const p=JSON.parse(a); if(Array.isArray(p)) p.forEach(id=>STATE.achievements.set(id, ACH_LABELS[id]||id)); else if(p&&typeof p==='object'){ for(const[id,t] of Object.entries(p)) STATE.achievements.set(id,(t&&t!==id)?t:(ACH_LABELS[id]||id)); } }
|
||||||
|
STATE.xp=+(localStorage.getItem(LS_XP)||0); STATE.level=calcLevel(STATE.xp);
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
function saveProgress(){
|
||||||
|
try{
|
||||||
|
localStorage.setItem(LS_PREFIX+'_progress', JSON.stringify(STATE.progress));
|
||||||
|
localStorage.setItem(LS_PREFIX+'_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
|
||||||
|
localStorage.setItem(LS_XP, String(STATE.xp));
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
function bumpProgress(key, delta){
|
||||||
|
STATE.progress[key]=Math.max(0,Math.min(100,(STATE.progress[key]||0)+delta));
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
if(STATE.progress[key]>=50) markParaRead(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _markedRead=new Set();
|
||||||
|
let _pendingProgressBody=null, _progressTimer=null;
|
||||||
|
function _flushProgress(){
|
||||||
|
const body=_pendingProgressBody; _pendingProgressBody=null; if(!body) return;
|
||||||
|
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||||||
|
fetch('/api/textbooks/'+_TB_SLUG+'/progress',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tok},body:JSON.stringify(body),keepalive:true}).catch(()=>{});
|
||||||
|
}
|
||||||
|
function _queueProgress(patch){ _pendingProgressBody=Object.assign(_pendingProgressBody||{},patch); if(_progressTimer) clearTimeout(_progressTimer); _progressTimer=setTimeout(_flushProgress, 600); }
|
||||||
|
function markLastPara(id){ _queueProgress({last_para:id}); }
|
||||||
|
function markParaRead(id){ if(_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({mark_read:id}); }
|
||||||
|
window.addEventListener('beforeunload', _flushProgress);
|
||||||
|
function loadServerReadState(){
|
||||||
|
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||||||
|
fetch('/api/textbooks/'+_TB_SLUG,{headers:{'Authorization':'Bearer '+tok}}).then(r=>r.ok?r.json():null).then(d=>{
|
||||||
|
if(!d||!d.progress) return;
|
||||||
|
(d.progress.read||[]).forEach(k=>{_markedRead.add(k); if((STATE.progress[k]||0)<50) STATE.progress[k]=100;});
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
}).catch(()=>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addXp(n,src){
|
||||||
|
if(!n) return;
|
||||||
|
const prev=STATE.level; STATE.xp=Math.max(0,(STATE.xp||0)+n); STATE.level=calcLevel(STATE.xp);
|
||||||
|
saveProgress(); refreshProgressUI();
|
||||||
|
if(window.LS&&window.LS.xp) window.LS.xp.add(n, LS_PREFIX+'-'+(src||'misc'));
|
||||||
|
if(STATE.level>prev){
|
||||||
|
const pop=document.getElementById('ach-popup');
|
||||||
|
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshProgressUI(){
|
||||||
|
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
|
||||||
|
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
|
||||||
|
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
|
||||||
|
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
|
||||||
|
const xpBadge=document.getElementById('hero-xp-badge');
|
||||||
|
if(xpBadge){ xpBadge.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
|
||||||
|
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
|
||||||
|
}
|
||||||
|
|
||||||
|
function achievement(id,text){
|
||||||
|
if(STATE.achievements.has(id)) return;
|
||||||
|
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
|
||||||
|
const pop=document.getElementById('ach-popup');
|
||||||
|
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
|
||||||
|
addXp(20,'ach-'+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParaSelector(){
|
||||||
|
const g=document.getElementById('psel-grid'); g.innerHTML='';
|
||||||
|
PARAS.forEach(p=>{
|
||||||
|
const card=document.createElement('div');
|
||||||
|
card.className='psel-card'+(p.final?' final':'');
|
||||||
|
card.dataset.id=p.id; card.dataset.progCard=p.id;
|
||||||
|
card.innerHTML='<div class="psel-num">'+p.num+'</div><div class="psel-name">'+p.name+'</div><div class="psel-prog"><div class="psel-prog-fill"></div></div>';
|
||||||
|
card.addEventListener('click', ()=>goTo(p.id));
|
||||||
|
g.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILT=new Set();
|
||||||
|
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
|
||||||
|
function goTo(id){
|
||||||
|
STATE.current=id; ensureBuilt(id);
|
||||||
|
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
|
||||||
|
const el=document.getElementById('sec-'+id); if(el) el.classList.add('active');
|
||||||
|
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id===id));
|
||||||
|
buildSidebar(id);
|
||||||
|
window.scrollTo({top:0,behavior:'smooth'});
|
||||||
|
if((STATE.progress[id]||0)<10) bumpProgress(id, 10);
|
||||||
|
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
|
||||||
|
markLastPara(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSidebar(id){
|
||||||
|
const box=document.getElementById('sidebar-content');
|
||||||
|
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
|
||||||
|
let html='';
|
||||||
|
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||||
|
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||||
|
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||||
|
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
|
||||||
|
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||||
|
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||||
|
html+='</div>';
|
||||||
|
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
|
||||||
|
if(tip){
|
||||||
|
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
|
||||||
|
}
|
||||||
|
if(STATE.achievements.size>0){
|
||||||
|
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
|
||||||
|
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ '+text+'</div>'; });
|
||||||
|
html+='</div>';
|
||||||
|
}
|
||||||
|
box.innerHTML=html;
|
||||||
|
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTheme(){
|
||||||
|
const t=localStorage.getItem(LS_PREFIX+'_theme')||'light';
|
||||||
|
if(t==='dark') document.documentElement.classList.add('dark');
|
||||||
|
document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';
|
||||||
|
document.getElementById('theme-btn').addEventListener('click', ()=>{
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
const dark=document.documentElement.classList.contains('dark');
|
||||||
|
localStorage.setItem(LS_PREFIX+'_theme', dark?'dark':'light');
|
||||||
|
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
|
||||||
|
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'✓ Верно!':'✗ Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
|
||||||
|
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
|
||||||
|
function ipow(base, exp){ let r=1; for(let i=0;i<Math.abs(exp);i++) r*=base; return exp<0 ? 1/r : r; }
|
||||||
|
function gcd(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ const t=b; b=a%b; a=t; } return a||1; }
|
||||||
|
function makeCard(kind, title, num, body){
|
||||||
|
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
|
||||||
|
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === SVG-хелперы === */
|
||||||
|
function axes2D(W, H, pad, xmin, xmax, ymin, ymax){
|
||||||
|
const ux = (W - 2*pad) / (xmax - xmin);
|
||||||
|
const uy = (H - 2*pad) / (ymax - ymin);
|
||||||
|
const toX = v => pad + (v - xmin) * ux;
|
||||||
|
const toY = v => H - pad - (v - ymin) * uy;
|
||||||
|
let g = '';
|
||||||
|
g += '<g stroke="#e5e7eb" stroke-width="1">';
|
||||||
|
for (let x = Math.ceil(xmin); x <= xmax; x++){
|
||||||
|
g += '<line x1="'+toX(x)+'" y1="'+pad+'" x2="'+toX(x)+'" y2="'+(H-pad)+'"/>';
|
||||||
|
}
|
||||||
|
for (let y = Math.ceil(ymin); y <= ymax; y++){
|
||||||
|
g += '<line x1="'+pad+'" y1="'+toY(y)+'" x2="'+(W-pad)+'" y2="'+toY(y)+'"/>';
|
||||||
|
}
|
||||||
|
g += '</g>';
|
||||||
|
const y0 = toY(0), x0 = toX(0);
|
||||||
|
g += '<line x1="'+pad+'" y1="'+y0+'" x2="'+(W-pad)+'" y2="'+y0+'" stroke="#0f172a" stroke-width="1.5"/>';
|
||||||
|
g += '<line x1="'+x0+'" y1="'+pad+'" x2="'+x0+'" y2="'+(H-pad)+'" stroke="#0f172a" stroke-width="1.5"/>';
|
||||||
|
g += '<text x="'+(W-pad+2)+'" y="'+(y0-4)+'" font-size="11" fill="#0f172a">x</text>';
|
||||||
|
g += '<text x="'+(x0+4)+'" y="'+(pad-2)+'" font-size="11" fill="#0f172a">y</text>';
|
||||||
|
g += '<g font-size="10" fill="#64748b">';
|
||||||
|
for (let x = Math.ceil(xmin); x <= xmax; x++){
|
||||||
|
if (x !== 0) g += '<text x="'+(toX(x)-3)+'" y="'+(y0+12)+'">'+x+'</text>';
|
||||||
|
}
|
||||||
|
for (let y = Math.ceil(ymin); y <= ymax; y++){
|
||||||
|
if (y !== 0) g += '<text x="'+(x0+4)+'" y="'+(toY(y)+3)+'">'+y+'</text>';
|
||||||
|
}
|
||||||
|
g += '<text x="'+(x0+4)+'" y="'+(y0+12)+'">0</text>';
|
||||||
|
g += '</g>';
|
||||||
|
return { content: g, toX, toY, ux, uy };
|
||||||
|
}
|
||||||
|
function plotFunc(f, xmin, xmax, toX, toY, color, N){
|
||||||
|
N = N || 200;
|
||||||
|
let d = '';
|
||||||
|
let prevValid = false;
|
||||||
|
for (let i = 0; i <= N; i++){
|
||||||
|
const x = xmin + (xmax - xmin) * i / N;
|
||||||
|
let y;
|
||||||
|
try { y = f(x); } catch(e){ y = NaN; }
|
||||||
|
if (!isFinite(y) || isNaN(y) || y < -1e4 || y > 1e4){ prevValid = false; continue; }
|
||||||
|
d += (prevValid ? ' L' : ' M') + toX(x).toFixed(2) + ',' + toY(y).toFixed(2);
|
||||||
|
prevValid = true;
|
||||||
|
}
|
||||||
|
return '<path d="'+d+'" stroke="'+color+'" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
}
|
||||||
|
function pointWithDrop(x, fx, toX, toY, color, label){
|
||||||
|
const px = toX(x), py = toY(fx);
|
||||||
|
let s = '';
|
||||||
|
s += '<line x1="'+px+'" y1="'+py+'" x2="'+px+'" y2="'+toY(0)+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
|
||||||
|
s += '<line x1="'+px+'" y1="'+py+'" x2="'+toX(0)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
|
||||||
|
s += '<circle cx="'+px+'" cy="'+py+'" r="4.5" fill="'+color+'" stroke="#fff" stroke-width="2"/>';
|
||||||
|
if (label){
|
||||||
|
s += '<text x="'+(px+8)+'" y="'+(py-8)+'" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="'+color+'">'+label+'</text>';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
function asymptote(orientation, value, toX, toY, xmin, xmax, ymin, ymax, color){
|
||||||
|
color = color || '#94a3b8';
|
||||||
|
if (orientation === 'h'){
|
||||||
|
const y = toY(value);
|
||||||
|
return '<line x1="'+toX(xmin)+'" y1="'+y+'" x2="'+toX(xmax)+'" y2="'+y+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
|
||||||
|
} else {
|
||||||
|
const x = toX(value);
|
||||||
|
return '<line x1="'+x+'" y1="'+toY(ymin)+'" x2="'+x+'" y2="'+toY(ymax)+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function snapToValue(value, snapPoints, tolerance){
|
||||||
|
tolerance = tolerance || 0.1;
|
||||||
|
for (const sp of snapPoints){
|
||||||
|
if (Math.abs(value - sp) < tolerance) return sp;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function rightAngleMark(V, uIn, wIn, s){
|
||||||
|
s = s || 9;
|
||||||
|
const p1 = {x: V.x + s*uIn.x, y: V.y + s*uIn.y};
|
||||||
|
const c = {x: p1.x + s*wIn.x, y: p1.y + s*wIn.y};
|
||||||
|
const p2 = {x: V.x + s*wIn.x, y: V.y + s*wIn.y};
|
||||||
|
return p1.x+','+p1.y+' '+c.x+','+c.y+' '+p2.x+','+p2.y;
|
||||||
|
}
|
||||||
|
function angleArcAuto(V, uA, uB, R){
|
||||||
|
const sA = {x: V.x + R*uA.x, y: V.y + R*uA.y};
|
||||||
|
const eB = {x: V.x + R*uB.x, y: V.y + R*uB.y};
|
||||||
|
const cross = uA.x*uB.y - uA.y*uB.x;
|
||||||
|
const sweep = cross > 0 ? 1 : 0;
|
||||||
|
return 'M'+sA.x+','+sA.y+' A'+R+','+R+' 0 0,'+sweep+' '+eB.x+','+eB.y;
|
||||||
|
}
|
||||||
|
function unitVec(p1, p2){
|
||||||
|
const dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||||
|
const len = Math.sqrt(dx*dx + dy*dy) || 1;
|
||||||
|
return {x: dx/len, y: dy/len};
|
||||||
|
}
|
||||||
|
function deg2rad(d){ return d * Math.PI / 180; }
|
||||||
|
|
||||||
|
const ICONS = {
|
||||||
|
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
|
||||||
|
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
||||||
|
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
|
||||||
|
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
|
||||||
|
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
|
||||||
|
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
|
||||||
|
};
|
||||||
|
|
||||||
|
function secNavFor(curId){
|
||||||
|
const idx = PARAS.findIndex(p => p.id === curId);
|
||||||
|
const prev = idx > 0 ? PARAS[idx-1].id : null;
|
||||||
|
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
|
||||||
|
return secNav(prev, next);
|
||||||
|
}
|
||||||
|
function secNav(prev, next){
|
||||||
|
function lbl(id){ if(!id) return ''; const p=PARAS.find(x=>x.id===id); return p?p.num:id; }
|
||||||
|
let h='<div class="sec-nav">';
|
||||||
|
h+=prev?'<button class="btn" onclick="goTo(\''+prev+'\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> '+lbl(prev)+'</button>':'<span></span>';
|
||||||
|
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+lbl(next)+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
|
||||||
|
h+='</div>'; return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readButton(paraId){
|
||||||
|
const p = PARAS.find(x => x.id === paraId);
|
||||||
|
const labelTail = p && p.final ? 'финал' : (p ? p.num : '?');
|
||||||
|
return '<div style="margin-top:18px;display:flex;justify-content:center">'
|
||||||
|
+'<button class="btn primary" id="'+paraId+'-read-btn">'
|
||||||
|
+'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
|
||||||
|
+' Я прочитал \u2014 '+labelTail+' (+10 XP)'
|
||||||
|
+'</button></div>';
|
||||||
|
}
|
||||||
|
function wireReadBtn(paraId){
|
||||||
|
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
|
||||||
|
btn.addEventListener('click', ()=>{
|
||||||
|
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
|
||||||
|
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
|
||||||
|
const aId = paraId+'_done';
|
||||||
|
if(ACH_LABELS[aId]) achievement(aId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSorter(cfg){
|
||||||
|
const placed = {}; const pool = document.getElementById(cfg.poolId); const scope = document.querySelector(cfg.scopeSelector);
|
||||||
|
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
|
||||||
|
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
|
||||||
|
let armed = null;
|
||||||
|
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
|
||||||
|
function attach(elm,itId){ let ghost=null,dragging=false,sx=0,sy=0; elm.addEventListener('pointerdown',ev=>{ if(ev.button!==undefined&&ev.button!==0) return;
|
||||||
|
ev.preventDefault(); if(ev.target.classList&&ev.target.classList.contains('dnd-x')){ ev.stopPropagation(); if(placed[itId]){delete placed[itId];render();}else if(armed===itId){armed=null;render();} return; } sx=ev.clientX;sy=ev.clientY; const r=elm.getBoundingClientRect(); const ox=ev.clientX-r.left,oy=ev.clientY-r.top; try{elm.setPointerCapture(ev.pointerId);}catch(e){} function onMove(e){ const dx=e.clientX-sx,dy=e.clientY-sy; if(!dragging&&Math.hypot(dx,dy)>8){ dragging=true; ghost=elm.cloneNode(true); ghost.classList.remove('armed'); ghost.style.cssText='position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:'+r.width+'px;left:'+(e.clientX-ox)+'px;top:'+(e.clientY-oy)+'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if(dragging&&ghost){ ghost.style.left=(e.clientX-ox)+'px';ghost.style.top=(e.clientY-oy)+'px'; const under=document.elementsFromPoint(e.clientX,e.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); const tgt=under.find(n=>n.classList&&(n.classList.contains('drop-box')||n.classList.contains('dnd-pool'))); if(tgt)tgt.classList.add('over'); } } function onUp(e){ elm.removeEventListener('pointermove',onMove);elm.removeEventListener('pointerup',onUp);elm.removeEventListener('pointercancel',onUp);elm.classList.remove('dragging'); if(ghost){ghost.remove();ghost=null;} scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); if(dragging){ const under=document.elementsFromPoint(e.clientX,e.clientY); const box=under.find(n=>n.classList&&n.classList.contains('drop-box')); const pl=under.find(n=>n.classList&&n.classList.contains('dnd-pool')); if(box){const di=box.querySelector('[data-cat]');if(di){placed[itId]=di.dataset.cat;armed=null;render();return;}}else if(pl){delete placed[itId];armed=null;render();return;} }else{ if(placed[itId]){delete placed[itId];armed=null;render();}else{armed=(armed===itId)?null:itId;render();} } dragging=false; } elm.addEventListener('pointermove',onMove);elm.addEventListener('pointerup',onUp);elm.addEventListener('pointercancel',onUp); }); }
|
||||||
|
function attachBoxTaps(){ scope.querySelectorAll('.drop-box').forEach(box=>{ box.addEventListener('click',ev=>{ if(!armed)return; if(ev.target.closest('.dnd-chip'))return; const di=box.querySelector('[data-cat]'); if(di){placed[armed]=di.dataset.cat;armed=null;render();} }); }); }
|
||||||
|
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); cfg.cats.forEach(cat=>{const box=scope.querySelector('.drop-items[data-cat="'+cat+'"]');if(!box)return;box.innerHTML='';cfg.items.forEach(it=>{if(placed[it.id]!==cat)return;box.appendChild(buildChip(it,true));});}); if(window.renderMathInElement)try{renderMath(scope);}catch(_){} }
|
||||||
|
attachBoxTaps(); render();
|
||||||
|
return {placed,render,reset(){ for(const k in placed)delete placed[k];armed=null;render(); }};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStub(id, name, phase){
|
||||||
|
return '<div class="card" style="background:linear-gradient(135deg,var(--sec-acc-soft),var(--card));border:1.5px dashed var(--sec-acc)">'
|
||||||
|
+ '<div class="card-header"><div class="card-icon theory">'+ICONS.theory+'</div><div class="card-title">В разработке</div></div>'
|
||||||
|
+ '<div class="card-body"><p>Контент <b>'+name+'</b> будет реализован в <b>'+phase+'</b> по плану <code>PLAN_PHYSICS_8.md</code>.</p>'
|
||||||
|
+ '<p style="margin-top:8px;color:var(--muted);font-size:.9rem">Phase 0 \u2014 это каркас (skeleton). Все 4 интерактива, 3 теоретические карточки и тренажёр задач будут добавлены в волне.</p>'
|
||||||
|
+ '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Search ===== */
|
||||||
|
const SEARCH_INDEX = (function(){
|
||||||
|
const arr=[];
|
||||||
|
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
|
||||||
|
return arr;
|
||||||
|
})();
|
||||||
|
function initSearch(){
|
||||||
|
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
|
||||||
|
if(!modal||!inp||!out) return;
|
||||||
|
let cur=0,rows=[];
|
||||||
|
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
|
||||||
|
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
|
||||||
|
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'\u2026':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
|
||||||
|
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
|
||||||
|
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
|
||||||
|
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
|
||||||
|
function close(){ modal.classList.remove('show'); }
|
||||||
|
btn&&btn.addEventListener('click',open);
|
||||||
|
modal.addEventListener('click',e=>{if(e.target===modal)close();});
|
||||||
|
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
|
||||||
|
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
|
||||||
|
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSidebarToggle(){
|
||||||
|
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
|
||||||
|
if(!side||!btn) return;
|
||||||
|
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||||
|
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||||
|
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
|
||||||
|
back.addEventListener('click',close);
|
||||||
|
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||||
|
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo(PARAS[0].id);
|
||||||
|
setTimeout(()=>achievement('start'), 600);
|
||||||
|
if(window.LS&&window.LS.xp){
|
||||||
|
window.LS.xp.load().then(function(s){ if(s&&s.xp>STATE.xp){ STATE.xp=s.xp; STATE.level=calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if(STATE.current) buildSidebar(STATE.current); } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user