feat(geom10 W0): инфра — миграция БД, stereo3d.js, hub + 4 stub-раздела

- Миграция 027: textbooks hub geometry-10 + 4 ребёнка (r1 blue, r2 emerald, r3 rose, r4 amber)
- frontend/js/stereo3d.js: библиотека 3D-проекций (Scene, CABINET/ISOMETRIC, cube/box/prism/pyramid/tetrahedron/plane/arrow/angle, авто-видимость рёбер)
- geometry_10_hub.html: 4 карточки разделов, общий прогресс, превью 4 тел через stereo3d
- 4 stub-файла разделов (r1-r4) с list параграфов и плашкой 'в разработке'
- backend/scripts/gen_geom10_stubs.js: генератор stub-файлов
This commit is contained in:
Maxim Dolgolyov
2026-05-29 14:37:07 +03:00
parent 3df79d081c
commit 0284611263
8 changed files with 2039 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env node
'use strict';
// Генератор stub-файлов разделов Геометрии 10. W0.
// Запуск: node backend/scripts/gen_geom10_stubs.js
const fs = require('fs');
const path = require('path');
const sections = [
{ file:'geometry_10_r1.html', num:1, slug:'geometry-10-r1',
title:'Введение в стереометрию',
sub:'Пространственные фигуры · Аксиомы · Сечения',
range:'§1–§3 + Финал', wm:'△',
primary:'#2563eb', primaryD:'#1d4ed8', soft:'#dbeafe', dark:'#1e3a8a',
paras:[
{ n:1, title:'Пространственные фигуры',
sub:'Призма, пирамида, цилиндр, конус, шар. Грани, рёбра, вершины.' },
{ n:2, title:'Прямые и плоскости',
sub:'Аксиомы стереометрии (A1–A3) и их следствия. 4 способа задания плоскости.' },
{ n:3, title:'Построения сечений',
sub:'Метод следов. Сечения куба, призмы, пирамиды.' }
] },
{ file:'geometry_10_r2.html', num:2, slug:'geometry-10-r2',
title:'Параллельность',
sub:'Прямые · Прямая и плоскость · Плоскости',
range:'§4–§6 + Финал', wm:'∥',
primary:'#059669', primaryD:'#047857', soft:'#d1fae5', dark:'#064e3b',
paras:[
{ n:4, title:'Расположение прямых в пространстве',
sub:'Пересекающиеся, параллельные, скрещивающиеся прямые. Угол между скрещивающимися.' },
{ n:5, title:'Прямая и плоскость',
sub:'Три случая. Признак параллельности прямой и плоскости.' },
{ n:6, title:'Две плоскости',
sub:'Пересекаются или параллельны. Признак параллельности плоскостей.' }
] },
{ file:'geometry_10_r3.html', num:3, slug:'geometry-10-r3',
title:'Перпендикулярность',
sub:'Прямая ⊥ плоскость · Расстояния · Углы · Двугранный угол',
range:'§7–§10 + Финал', wm:'⊥',
primary:'#e11d48', primaryD:'#be123c', soft:'#fee2e2', dark:'#7f1d1d',
paras:[
{ n:7, title:'Перпендикулярность прямой и плоскости',
sub:'Определение, признак перпендикулярности.' },
{ n:8, title:'Расстояния',
sub:'От точки до плоскости, между параллельными плоскостями, между скрещивающимися.' },
{ n:9, title:'Угол между прямой и плоскостью',
sub:'Наклонная и её проекция. Теорема о трёх перпендикулярах.' },
{ n:10, title:'Перпендикулярность плоскостей',
sub:'Двугранный угол. Признак перпендикулярности плоскостей.' }
] },
{ file:'geometry_10_r4.html', num:4, slug:'geometry-10-r4',
title:'Координаты и векторы',
sub:'ПДСК в пространстве · Векторы · Скалярное произведение',
range:'§11–§14 + Финал', wm:'→',
primary:'#d97706', primaryD:'#b45309', soft:'#fef3c7', dark:'#78350f',
paras:[
{ n:11, title:'Координаты в пространстве',
sub:'ПДСК. Точка (x; y; z). Расстояние между точками.' },
{ n:12, title:'Вектор. Действия над векторами',
sub:'Сложение, умножение на число, базис i, j, k. Разложение вектора.' },
{ n:13, title:'Скалярное произведение',
sub:'a · b = |a||b|cos α = x₁x₂ + y₁y₂ + z₁z₂. Условие перпендикулярности.' },
{ n:14, title:'Применение координат и векторов',
sub:'Уравнения, углы, расстояния. Векторно-координатный метод.' }
] }
];
function html(s){
const parasHtml = s.paras.map(p => `
<article class="para-card" data-para="${p.n}">
<div class="para-num">§ ${p.n}</div>
<div class="para-body">
<h2 class="para-title">${p.title}</h2>
<p class="para-sub">${p.sub}</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>`).join('\n');
return `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Геометрия 10 · ${s.title}</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>
<script src="/js/stereo3d.js?v=1" defer></script>
<style>
:root{
--bg:#f8fafc; --card:#fff;
--text:#0f172a; --muted:#475569;
--border:#e2e8f0;
--pri:${s.primary}; --pri-d:${s.primaryD};
--pri-soft:${s.soft};
--dark:${s.dark};
--sh:0 4px 16px rgba(0,0,0,.06);
}
html.dark{
--bg:#020617; --card:#0a1929;
--text:#dbeafe; --muted:#94a3b8;
--border:#1e293b;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(110deg,var(--dark) 0%,var(--pri) 55%,var(--pri-soft) 100%);color:#fff;padding:32px 24px 28px;overflow:hidden}
.hdr::before{content:'${s.wm}';position:absolute;right:8px;top:-20%;font-family:'Outfit',sans-serif;font-size:clamp(8rem,22vw,18rem);font-weight:900;color:rgba(255,255,255,.10);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.7rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.85;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:980px;margin:0 auto;padding:32px 24px 60px}
.intro-card{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:22px 26px;margin-bottom:28px;box-shadow:var(--sh)}
.intro-num{display:inline-block;padding:4px 10px;background:var(--pri-soft);color:var(--pri-d);border-radius:99px;font-size:.72rem;font-weight:800;letter-spacing:.06em;margin-bottom:8px;text-transform:uppercase}
.intro-card h2{font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:800;margin-bottom:6px}
.intro-card p{color:var(--muted);font-size:.95rem}
.para-grid{display:grid;grid-template-columns:1fr;gap:14px}
.para-card{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:18px 20px;display:flex;gap:16px;align-items:flex-start;transition:transform .15s,box-shadow .15s,border-color .15s}
.para-card:hover{transform:translateY(-2px);box-shadow:var(--sh);border-color:var(--pri)}
.para-num{font-family:'Outfit',sans-serif;font-size:1rem;font-weight:900;color:#fff;background:linear-gradient(135deg,var(--pri),var(--pri-d));padding:8px 12px;border-radius:9px;min-width:56px;text-align:center;letter-spacing:-.02em;flex-shrink:0}
.para-body{flex:1}
.para-title{font-family:'Outfit',sans-serif;font-size:1.05rem;font-weight:800;margin-bottom:4px;color:var(--text)}
.para-sub{font-size:.88rem;color:var(--muted);margin-bottom:10px;line-height:1.55}
.para-status{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:var(--muted);background:rgba(0,0,0,.04);padding:6px 10px;border-radius:8px;font-weight:600}
html.dark .para-status{background:rgba(255,255,255,.06)}
.para-status .ic{width:14px;height:14px}
.banner-soon{margin-top:30px;text-align:center;padding:20px;background:linear-gradient(135deg,var(--pri-soft),transparent);border:1px dashed var(--pri);border-radius:14px;color:var(--pri-d);font-weight:700;font-size:.92rem}
.banner-soon b{font-family:'Outfit',sans-serif}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/geometry-10" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К курсу геометрии 10
</a>
</div>
<div>
<h1>Раздел ${s.num}. ${s.title}</h1>
<div class="hdr-sub">${s.sub} · ${s.range}</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>
<div class="intro-card">
<span class="intro-num">Раздел ${s.num}</span>
<h2>${s.title}</h2>
<p>${s.sub}. Раздел содержит ${s.paras.length} параграф${s.paras.length===1?'':(s.paras.length<5?'а':'ов')} и финальный этап с боссами.</p>
</div>
<div class="para-grid">
${parasHtml}
</div>
<div class="banner-soon">
<b>Раздел в разработке.</b> Полная реализация — в следующих волнах. Уже доступна 3D-библиотека <code>stereo3d.js</code>.
</div>
</main>
<footer class="foot">
Геометрия — 10 класс · Раздел ${s.num} · LearnSpace
</footer>
<script>
'use strict';
(function(){
var saved = localStorage.getItem('geometry10_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('geometry10_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
</script>
</body>
</html>
`;
}
const outDir = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
for (const s of sections){
const fp = path.join(outDir, s.file);
fs.writeFileSync(fp, html(s), 'utf8');
console.log('Wrote:', fp);
}
console.log('Done.');
@@ -0,0 +1,32 @@
-- Geometry 10 hub migration.
-- Adds hub row + 4 section children for Геометрия 10 (Латотин/Чеботаревский/Горбунова, 2020).
-- Pattern mirrors 023_algebra_10_hub.sql.
-- 1. Hub row.
INSERT INTO textbooks
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
VALUES
('geometry-10', 'math', 10, 'Геометрия — 10 класс', '',
'Полный курс стереометрии 10 класса по учебнику Л. А. Латотина и Б. Д. Чеботаревского: введение в стереометрию (аксиомы, сечения), параллельность прямых и плоскостей, перпендикулярность, координаты и векторы в пространстве. 4 раздела, 14 параграфов, ~140 интерактивов, 24 босса. Все 3D-фигуры — через библиотеку stereo3d.js.',
'geometry_10_hub.html', 14, 'blue', 9, 1);
-- 2. Section children.
INSERT INTO textbooks
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
VALUES
('geometry-10-r1', 'math', 10, 'Геометрия 10 · Введение в стереометрию',
'',
'§1–§3: пространственные фигуры (призма, пирамида, цилиндр, конус, шар), аксиомы стереометрии и их следствия, построение сечений многогранников методом следов.',
'geometry_10_r1.html', 3, 'blue', 1, 1, 'geometry-10'),
('geometry-10-r2', 'math', 10, 'Геометрия 10 · Параллельность',
'',
'§4–§6: взаимное расположение прямых в пространстве (пересекающиеся, параллельные, скрещивающиеся), взаимное расположение прямой и плоскости, взаимное расположение двух плоскостей, признаки параллельности.',
'geometry_10_r2.html', 3, 'emerald', 2, 1, 'geometry-10'),
('geometry-10-r3', 'math', 10, 'Геометрия 10 · Перпендикулярность',
'',
'§7–§10: перпендикулярность прямой и плоскости, расстояния в пространстве, угол между прямой и плоскостью (теорема о трёх перпендикулярах), перпендикулярность плоскостей (двугранный угол).',
'geometry_10_r3.html', 4, 'rose', 3, 1, 'geometry-10'),
('geometry-10-r4', 'math', 10, 'Геометрия 10 · Координаты и векторы',
'',
'§11–§14: прямоугольная система координат в пространстве, векторы и действия над ними, скалярное произведение, применение векторно-координатного метода к решению задач.',
'geometry_10_r4.html', 4, 'amber', 4, 1, 'geometry-10');
+717
View File
@@ -0,0 +1,717 @@
/* stereo3d.js — библиотека 3D-проекций для Геометрии 10 (стереометрия).
*
* Чистый SVG-движок: проекция 3D → 2D через cabinet / isometric.
* Не зависит от Three.js, Canvas, WebGL. Возвращает строки SVG.
*
* Публичный API: window.STEREO3D = { Scene, views, util }.
*
* Использование:
* const scene = new STEREO3D.Scene(380, 320, {view:'CABINET', scale:50});
* scene.addCube({center:[0,0,0], size:2, label:'ABCDA1B1C1D1'});
* scene.addLabel('M', [0, 0, 1.2]);
* const svgHtml = scene.render();
*
* Соглашение об осях: x → вправо, y → вглубь, z → вверх.
* В SVG y растёт вниз, мы инвертируем при проекции.
*/
(function(){
'use strict';
if (window.STEREO3D && window.STEREO3D.__installed) return;
const S = window.STEREO3D = window.STEREO3D || {};
S.__installed = true;
/* ============================================================ */
/* ВЕКТОРНАЯ АЛГЕБРА */
/* ============================================================ */
function v3(a,b,c){ return [a,b,c]; }
function add(a,b){ return [a[0]+b[0], a[1]+b[1], a[2]+b[2]]; }
function sub(a,b){ return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]; }
function scale3(a,k){ return [a[0]*k, a[1]*k, a[2]*k]; }
function dot(a,b){ return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }
function cross(a,b){
return [
a[1]*b[2] - a[2]*b[1],
a[2]*b[0] - a[0]*b[2],
a[0]*b[1] - a[1]*b[0]
];
}
function len3(a){ return Math.hypot(a[0], a[1], a[2]); }
function norm3(a){ const L = len3(a)||1; return [a[0]/L, a[1]/L, a[2]/L]; }
function rotX(p, a){
const c = Math.cos(a), s = Math.sin(a);
return [p[0], p[1]*c - p[2]*s, p[1]*s + p[2]*c];
}
function rotY(p, a){
const c = Math.cos(a), s = Math.sin(a);
return [p[0]*c + p[2]*s, p[1], -p[0]*s + p[2]*c];
}
function rotZ(p, a){
const c = Math.cos(a), s = Math.sin(a);
return [p[0]*c - p[1]*s, p[0]*s + p[1]*c, p[2]];
}
S.util = { v3, add, sub, scale3, dot, cross, len3, norm3, rotX, rotY, rotZ };
/* ============================================================ */
/* ПРОЕКЦИИ */
/* ============================================================ */
const VIEWS = S.views = {
CABINET: {
project: function(p){
// x → вправо, y → вглубь под 30° со сжатием 0.5, z → вверх
const a = Math.PI / 6;
return [
p[0] + 0.5 * p[1] * Math.cos(a),
-p[2] + 0.5 * p[1] * Math.sin(a)
];
},
cameraDir: [0.5*Math.cos(Math.PI/6), 1, 0.5*Math.sin(Math.PI/6)]
},
ISOMETRIC: {
project: function(p){
// классическая изометрия: оси под 30° к горизонтали
const c = Math.cos(Math.PI/6), s = Math.sin(Math.PI/6);
return [
(p[0] - p[1]) * c,
-p[2] + (p[0] + p[1]) * s
];
},
cameraDir: [1, 1, 1]
}
};
/* ============================================================ */
/* МАТЕРИАЛЫ */
/* ============================================================ */
const MAT = {
edge: { stroke:'#1e293b', width:1.8 },
edgeHidden:{ stroke:'#94a3b8', width:1.2, dash:'4 3' },
edgeHi: { stroke:'#dc2626', width:2.8 },
edgePar: { stroke:'#10b981', width:2.4 },
edgePerp: { stroke:'#7c3aed', width:2.4 },
face: { fill:'#dbeafe', opacity:0.35, stroke:'#1e3a8a', strokeWidth:1.4 },
plane: { fill:'#3b82f6', opacity:0.18, stroke:'#1e3a8a', strokeWidth:1.2, dash:'6 4' },
vertex: { fill:'#1e293b', r:3.5 }
};
/* ============================================================ */
/* Scene */
/* ============================================================ */
class Scene {
constructor(W, H, opts){
opts = opts || {};
this.W = W;
this.H = H;
this.scale = opts.scale || 40;
this.center = opts.center || [W/2, H/2];
this.view = (typeof opts.view === 'string') ? (VIEWS[opts.view] || VIEWS.CABINET) : (opts.view || VIEWS.CABINET);
this.rotX = opts.rotX || 0;
this.rotY = opts.rotY || 0;
this.bg = opts.bg || '#fafafa';
this.border = (opts.border !== undefined) ? opts.border : '1px solid #e2e8f0';
this.radius = opts.radius || 10;
this.items = []; // отрисовка в порядке Z (заполняется в render)
this._id = opts.id || ('s3d-' + Math.random().toString(36).slice(2,7));
}
// Применить повороты + проекцию
project3(p){
let q = p;
if (this.rotX) q = rotX(q, this.rotX);
if (this.rotY) q = rotY(q, this.rotY);
const [px, py] = this.view.project(q);
return [this.center[0] + this.scale * px, this.center[1] + this.scale * py];
}
// То же, но возвращает все три величины (для сортировки по глубине)
project3Depth(p){
let q = p;
if (this.rotX) q = rotX(q, this.rotX);
if (this.rotY) q = rotY(q, this.rotY);
const [px, py] = this.view.project(q);
// глубина = проекция на направление камеры
const cam = this.view.cameraDir;
const depth = dot(q, cam);
return { x: this.center[0] + this.scale * px, y: this.center[1] + this.scale * py, depth };
}
/* === Добавление элементов === */
addEdge(p1, p2, opts){
this.items.push({ kind:'edge', p1, p2, opts: opts || {} });
return this;
}
addFace(points, opts){
this.items.push({ kind:'face', points, opts: opts || {} });
return this;
}
addVertex(p, label, opts){
this.items.push({ kind:'vertex', p, label, opts: opts || {} });
return this;
}
addLabel(label, p, opts){
this.items.push({ kind:'label', p, label, opts: opts || {} });
return this;
}
addArrow(p1, p2, opts){
this.items.push({ kind:'arrow', p1, p2, opts: opts || {} });
return this;
}
addPlane(point, normal, opts){
this.items.push({ kind:'plane', point, normal, opts: opts || {} });
return this;
}
addAngleMark(vertex, p1, p2, opts){
this.items.push({ kind:'angle', vertex, p1, p2, opts: opts || {} });
return this;
}
addRightAngleMark(vertex, p1, p2, opts){
this.items.push({ kind:'rightAngle', vertex, p1, p2, opts: opts || {} });
return this;
}
addDashedSegment(p1, p2, opts){
return this.addEdge(p1, p2, Object.assign({hidden:true}, opts||{}));
}
/* === Готовые тела === */
// Куб ABCDA1B1C1D1 — стандартное соглашение учебника
addCube(params){
params = params || {};
const c = params.center || [0,0,0];
const s = (params.size || 2) / 2;
const labels = params.labels !== false;
const labelMap = params.labelMap || ['A','B','C','D','A_1','B_1','C_1','D_1'];
// Нижняя грань (z = -s): A B C D против часовой при взгляде сверху
const A = add(c, [-s, -s, -s]);
const B = add(c, [+s, -s, -s]);
const C = add(c, [+s, +s, -s]);
const D = add(c, [-s, +s, -s]);
const A1 = add(c, [-s, -s, +s]);
const B1 = add(c, [+s, -s, +s]);
const C1 = add(c, [+s, +s, +s]);
const D1 = add(c, [-s, +s, +s]);
const verts = [A,B,C,D,A1,B1,C1,D1];
const faces = [
[0,1,2,3], // нижняя
[4,5,6,7], // верхняя
[0,1,5,4], // передняя
[1,2,6,5], // правая
[2,3,7,6], // задняя
[3,0,4,7] // левая
];
const edges = [
[0,1],[1,2],[2,3],[3,0], // низ
[4,5],[5,6],[6,7],[7,4], // верх
[0,4],[1,5],[2,6],[3,7] // вертикали
];
return this._addPolyhedron(verts, faces, edges,
labels ? labelMap.map((L,i)=>({label:L, point:verts[i]})) : [],
params);
}
// Параллелепипед с произвольными размерами
addBox(params){
params = params || {};
const c = params.center || [0,0,0];
const sz = params.size || [2,2,2];
const [ax, ay, az] = [sz[0]/2, sz[1]/2, sz[2]/2];
const labelMap = params.labelMap || ['A','B','C','D','A_1','B_1','C_1','D_1'];
const A = add(c, [-ax, -ay, -az]);
const B = add(c, [+ax, -ay, -az]);
const C = add(c, [+ax, +ay, -az]);
const D = add(c, [-ax, +ay, -az]);
const A1 = add(c, [-ax, -ay, +az]);
const B1 = add(c, [+ax, -ay, +az]);
const C1 = add(c, [+ax, +ay, +az]);
const D1 = add(c, [-ax, +ay, +az]);
const verts = [A,B,C,D,A1,B1,C1,D1];
const faces = [
[0,1,2,3],[4,5,6,7],[0,1,5,4],[1,2,6,5],[2,3,7,6],[3,0,4,7]
];
const edges = [
[0,1],[1,2],[2,3],[3,0],
[4,5],[5,6],[6,7],[7,4],
[0,4],[1,5],[2,6],[3,7]
];
return this._addPolyhedron(verts, faces, edges,
params.labels !== false ? labelMap.map((L,i)=>({label:L, point:verts[i]})) : [],
params);
}
// Тетраэдр (правильный или по 4 вершинам)
addTetrahedron(params){
params = params || {};
let verts;
if (params.vertices){
verts = params.vertices;
} else {
const c = params.center || [0,0,0];
const r = params.size || 1.6;
// правильный тетраэдр: вершины на сфере
verts = [
add(c, [ r, r, r]),
add(c, [ r, -r, -r]),
add(c, [-r, r, -r]),
add(c, [-r, -r, r])
];
}
const labelMap = params.labelMap || ['A','B','C','D'];
const faces = [[0,1,2],[0,1,3],[0,2,3],[1,2,3]];
const edges = [[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]];
return this._addPolyhedron(verts, faces, edges,
params.labels !== false ? labelMap.map((L,i)=>({label:L, point:verts[i]})) : [],
params);
}
// n-угольная призма (база — правильный n-угольник в плоскости xy)
addPrism(params){
params = params || {};
const n = params.n || 4;
const r = params.baseRadius || 1.4;
const h = params.height || 2;
const c = params.center || [0,0,0];
const bottom = [];
const top = [];
for (let i = 0; i < n; i++){
const a = 2 * Math.PI * i / n - Math.PI / 2;
bottom.push(add(c, [r * Math.cos(a), r * Math.sin(a), -h/2]));
top.push(add(c, [r * Math.cos(a), r * Math.sin(a), +h/2]));
}
const verts = bottom.concat(top);
const faces = [];
const bottomIdx = []; for (let i = 0; i < n; i++) bottomIdx.push(i);
const topIdx = []; for (let i = 0; i < n; i++) topIdx.push(i + n);
faces.push(bottomIdx);
faces.push(topIdx.slice().reverse());
for (let i = 0; i < n; i++){
const j = (i + 1) % n;
faces.push([i, j, j+n, i+n]);
}
const edges = [];
for (let i = 0; i < n; i++){
const j = (i + 1) % n;
edges.push([i, j]);
edges.push([i+n, j+n]);
edges.push([i, i+n]);
}
return this._addPolyhedron(verts, faces, edges, [], params);
}
// n-угольная пирамида
addPyramid(params){
params = params || {};
const n = params.n || 4;
const r = params.baseRadius || 1.4;
const h = params.height || 2;
const c = params.center || [0,0,0];
const base = [];
for (let i = 0; i < n; i++){
const a = 2 * Math.PI * i / n - Math.PI / 2;
base.push(add(c, [r * Math.cos(a), r * Math.sin(a), -h/2]));
}
const apex = add(c, [0, 0, h/2]);
const verts = base.concat([apex]);
const apexIdx = n;
const faces = [];
faces.push(base.map((_,i)=>i));
for (let i = 0; i < n; i++){
const j = (i + 1) % n;
faces.push([i, j, apexIdx]);
}
const edges = [];
for (let i = 0; i < n; i++){
const j = (i + 1) % n;
edges.push([i, j]);
edges.push([i, apexIdx]);
}
return this._addPolyhedron(verts, faces, edges, [], params);
}
// Конус (приближение многогранником)
addCone(params){
params = params || {};
const n = params.segments || 24;
return this.addPyramid(Object.assign({n}, params));
}
// Цилиндр (приближение призмой)
addCylinder(params){
params = params || {};
const n = params.segments || 24;
return this.addPrism(Object.assign({n}, params));
}
// Сфера (упрощённо — окружность по силуэту)
addSphere(params){
params = params || {};
const c = params.center || [0,0,0];
const r = params.radius || 1.4;
this.items.push({ kind:'sphere', center:c, radius:r, opts:params });
return this;
}
// Общий конструктор многогранника
_addPolyhedron(verts, faces, edges, vertLabels, params){
params = params || {};
const color = params.color || '#dbeafe';
const opacity = (params.opacity !== undefined) ? params.opacity : 0.35;
const showFaces = params.showFaces !== false;
const showHidden = params.showHidden !== false;
this.items.push({
kind:'polyhedron',
verts, faces, edges, vertLabels,
color, opacity, showFaces, showHidden,
hiOptsByEdge: params.highlightEdges || null,
hiOptsByFace: params.highlightFaces || null,
labels: vertLabels,
labelStyle: params.labelStyle || {}
});
return this;
}
/* === Рендер === */
render(){
const W = this.W, H = this.H;
const open = '<svg viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="xMidYMid meet" '
+ 'style="width:100%;height:auto;display:block;background:'+this.bg
+ ';border:'+this.border+';border-radius:'+this.radius+'px">';
const defs = this._defs();
// Собираем «плоские» элементы для отрисовки: faces (back), edges (hidden), faces (front), edges (visible), verts, labels.
const out = [];
// Сначала — обрабатываем все объекты в two passes:
// pass 1: задние грани + невидимые рёбра
// pass 2: передние грани + видимые рёбра
// pass 3: вершины
// pass 4: подписи + декораторы (углы, стрелки)
const flat = []; // {type, depth, svg}
for (const it of this.items){
if (it.kind === 'polyhedron'){
this._polyhedronToFlat(it, flat);
} else if (it.kind === 'edge'){
const a = this.project3Depth(it.p1);
const b = this.project3Depth(it.p2);
const mid = (a.depth + b.depth) / 2;
flat.push({ pass: it.opts.hidden ? 1 : 4, depth: mid, svg: this._edgeSvg(a, b, it.opts) });
} else if (it.kind === 'face'){
const pts = it.points.map(p => this.project3Depth(p));
const mid = pts.reduce((s,p)=>s+p.depth, 0)/pts.length;
flat.push({ pass: 2, depth: mid, svg: this._faceSvg(pts, it.opts) });
} else if (it.kind === 'vertex'){
const a = this.project3Depth(it.p);
flat.push({ pass: 5, depth: a.depth, svg: this._vertexSvg(a, it.label, it.opts) });
} else if (it.kind === 'label'){
const a = this.project3Depth(it.p);
flat.push({ pass: 6, depth: a.depth, svg: this._labelSvg(a, it.label, it.opts) });
} else if (it.kind === 'arrow'){
const a = this.project3Depth(it.p1);
const b = this.project3Depth(it.p2);
flat.push({ pass: 4, depth: (a.depth+b.depth)/2, svg: this._arrowSvg(a, b, it.opts) });
} else if (it.kind === 'plane'){
flat.push({ pass: 2, depth: dot(it.point, this.view.cameraDir), svg: this._planeSvg(it.point, it.normal, it.opts) });
} else if (it.kind === 'angle'){
const v = this.project3Depth(it.vertex);
flat.push({ pass: 6, depth: v.depth, svg: this._angleMarkSvg(it.vertex, it.p1, it.p2, it.opts) });
} else if (it.kind === 'rightAngle'){
const v = this.project3Depth(it.vertex);
flat.push({ pass: 6, depth: v.depth, svg: this._rightAngleMarkSvg(it.vertex, it.p1, it.p2, it.opts) });
} else if (it.kind === 'sphere'){
const c = this.project3Depth(it.center);
const r = this.scale * it.radius;
flat.push({ pass: 2, depth: c.depth, svg:
'<circle cx="'+c.x+'" cy="'+c.y+'" r="'+r+'" fill="'+(it.opts.color||'#dbeafe')+'" fill-opacity="0.35" stroke="#1e3a8a" stroke-width="1.4"/>'
+ '<ellipse cx="'+c.x+'" cy="'+c.y+'" rx="'+r+'" ry="'+(r*0.35)+'" fill="none" stroke="#1e3a8a" stroke-width="1" stroke-dasharray="4 3" opacity="0.6"/>'
});
}
}
// Сортируем по pass, потом по depth (меньшая глубина = дальше).
flat.sort((a,b)=> (a.pass - b.pass) || (a.depth - b.depth));
let body = '';
for (const f of flat) body += f.svg;
return open + defs + body + '</svg>';
}
_defs(){
return '<defs>'
+ '<marker id="'+this._id+'-arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">'
+ '<path d="M 0 0 L 10 5 L 0 10 z" fill="#1e293b"/>'
+ '</marker>'
+ '</defs>';
}
_polyhedronToFlat(it, flat){
const proj = it.verts.map(p => this.project3Depth(p));
// Для каждой грани вычисляем нормаль в 3D и определяем — обращена к камере или нет.
const cam = this.view.cameraDir;
const faceData = it.faces.map((idxs)=>{
const p0 = it.verts[idxs[0]], p1 = it.verts[idxs[1]], p2 = it.verts[idxs[2]];
// нормаль через cross product. Для выпуклых тел нужно учесть центр тела.
const n = norm3(cross(sub(p1, p0), sub(p2, p0)));
// центр грани
let cx=0, cy=0, cz=0;
for (const i of idxs){ cx+=it.verts[i][0]; cy+=it.verts[i][1]; cz+=it.verts[i][2]; }
const ctr = [cx/idxs.length, cy/idxs.length, cz/idxs.length];
// если нормаль смотрит ВНУТРЬ тела (к началу), инвертируем
// (примерное предположение что центр тела — среднее всех вершин)
let bx=0, by=0, bz=0;
for (const v of it.verts){ bx+=v[0]; by+=v[1]; bz+=v[2]; }
const body = [bx/it.verts.length, by/it.verts.length, bz/it.verts.length];
const outward = sub(ctr, body);
if (dot(n, outward) < 0){ n[0]=-n[0]; n[1]=-n[1]; n[2]=-n[2]; }
// повернуть нормаль теми же поворотами что и точки
let rn = n;
if (this.rotX) rn = rotX(rn, this.rotX);
if (this.rotY) rn = rotY(rn, this.rotY);
const visible = dot(rn, cam) > 0;
const meanDepth = idxs.reduce((s,i)=>s+proj[i].depth, 0)/idxs.length;
return { idxs, visible, meanDepth, n: rn };
});
// Какие рёбра видимые (хоть одна смежная грань видима)
const edgeFaceMap = new Map();
function edgeKey(a, b){ return Math.min(a,b)+'-'+Math.max(a,b); }
for (let fi = 0; fi < it.faces.length; fi++){
const fc = it.faces[fi];
for (let i = 0; i < fc.length; i++){
const k = edgeKey(fc[i], fc[(i+1)%fc.length]);
if (!edgeFaceMap.has(k)) edgeFaceMap.set(k, []);
edgeFaceMap.get(k).push(fi);
}
}
const edgeVisible = new Map();
for (const [k, fis] of edgeFaceMap){
const vis = fis.some(fi => faceData[fi].visible);
edgeVisible.set(k, vis);
}
// 1) задние грани (полупрозрачные) — если showFaces
if (it.showFaces){
for (const fd of faceData){
if (!fd.visible){
const pts = fd.idxs.map(i => proj[i]);
flat.push({ pass:2, depth:fd.meanDepth, svg: this._faceSvg(pts, { fill: it.color, opacity: it.opacity * 0.5, stroke:'none' }) });
}
}
}
// 2) невидимые рёбра — пунктир
if (it.showHidden){
for (const e of it.edges){
const k = edgeKey(e[0], e[1]);
if (!edgeVisible.get(k)){
const a = proj[e[0]], b = proj[e[1]];
flat.push({ pass:1, depth:(a.depth+b.depth)/2, svg: this._edgeSvg(a, b, { hidden:true }) });
}
}
}
// 3) передние грани (полупрозрачные) — поверх задних рёбер
if (it.showFaces){
for (const fd of faceData){
if (fd.visible){
const pts = fd.idxs.map(i => proj[i]);
flat.push({ pass:3, depth:fd.meanDepth, svg: this._faceSvg(pts, { fill: it.color, opacity: it.opacity, stroke:'none' }) });
}
}
}
// 4) видимые рёбра — сплошные
for (const e of it.edges){
const k = edgeKey(e[0], e[1]);
if (edgeVisible.get(k)){
const a = proj[e[0]], b = proj[e[1]];
let style = {};
if (it.hiOptsByEdge){
for (const h of it.hiOptsByEdge){
if ((h.edge[0] === e[0] && h.edge[1] === e[1]) || (h.edge[0] === e[1] && h.edge[1] === e[0])){
style = h; break;
}
}
}
flat.push({ pass:4, depth:(a.depth+b.depth)/2, svg: this._edgeSvg(a, b, style) });
}
}
// 5) подписи вершин
if (it.labels && it.labels.length){
for (let vi = 0; vi < it.labels.length; vi++){
const lab = it.labels[vi];
const proj1 = proj[vi];
flat.push({ pass:6, depth:proj1.depth, svg: this._vertexLabelSvg(proj1, lab.label, it.labelStyle) });
}
}
}
/* === SVG примитивы === */
_edgeSvg(a, b, opts){
opts = opts || {};
let stroke, width, dash;
if (opts.hidden){ stroke = opts.stroke || MAT.edgeHidden.stroke; width = opts.width || MAT.edgeHidden.width; dash = opts.dash || MAT.edgeHidden.dash; }
else if (opts.highlight === 'par'){ stroke = MAT.edgePar.stroke; width = MAT.edgePar.width; }
else if (opts.highlight === 'perp'){ stroke = MAT.edgePerp.stroke; width = MAT.edgePerp.width; }
else if (opts.highlight){ stroke = MAT.edgeHi.stroke; width = MAT.edgeHi.width; }
else { stroke = opts.stroke || MAT.edge.stroke; width = opts.width || MAT.edge.width; dash = opts.dash; }
let s = '<line x1="'+a.x.toFixed(2)+'" y1="'+a.y.toFixed(2)+'" x2="'+b.x.toFixed(2)+'" y2="'+b.y.toFixed(2)+'" stroke="'+stroke+'" stroke-width="'+width+'" stroke-linecap="round"';
if (dash) s += ' stroke-dasharray="'+dash+'"';
s += '/>';
return s;
}
_faceSvg(pts, opts){
opts = opts || {};
const d = pts.map(p => p.x.toFixed(2)+','+p.y.toFixed(2)).join(' ');
const fill = opts.fill || MAT.face.fill;
const op = (opts.opacity !== undefined) ? opts.opacity : MAT.face.opacity;
const stroke = opts.stroke || 'none';
const sw = opts.strokeWidth || MAT.face.strokeWidth;
return '<polygon points="'+d+'" fill="'+fill+'" fill-opacity="'+op+'" stroke="'+stroke+'" stroke-width="'+sw+'"/>';
}
_vertexSvg(p, label, opts){
opts = opts || {};
const r = opts.r || MAT.vertex.r;
const fill = opts.fill || MAT.vertex.fill;
let s = '<circle cx="'+p.x.toFixed(2)+'" cy="'+p.y.toFixed(2)+'" r="'+r+'" fill="'+fill+'"/>';
if (label) s += this._vertexLabelSvg(p, label, opts);
return s;
}
_vertexLabelSvg(p, label, opts){
opts = opts || {};
const dx = (opts.dx !== undefined) ? opts.dx : 8;
const dy = (opts.dy !== undefined) ? opts.dy : -6;
const fs = opts.fontSize || 14;
const fill = opts.color || '#1e293b';
// если в label есть _, форматируем подстрочный индекс
const html = formatLabel(label);
return '<text x="'+(p.x+dx).toFixed(2)+'" y="'+(p.y+dy).toFixed(2)+'" font-size="'+fs+'" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+fill+'">'+html+'</text>';
}
_labelSvg(p, label, opts){
opts = opts || {};
const dx = (opts.dx !== undefined) ? opts.dx : 0;
const dy = (opts.dy !== undefined) ? opts.dy : 0;
const fs = opts.fontSize || 13;
const fill = opts.color || '#475569';
const html = formatLabel(label);
const anchor = opts.anchor || 'middle';
return '<text x="'+(p.x+dx).toFixed(2)+'" y="'+(p.y+dy).toFixed(2)+'" font-size="'+fs+'" font-family="Inter,sans-serif" font-weight="600" fill="'+fill+'" text-anchor="'+anchor+'">'+html+'</text>';
}
_arrowSvg(a, b, opts){
opts = opts || {};
const stroke = opts.color || '#1e293b';
const w = opts.width || 2;
let s = '<line x1="'+a.x.toFixed(2)+'" y1="'+a.y.toFixed(2)+'" x2="'+b.x.toFixed(2)+'" y2="'+b.y.toFixed(2)+'" stroke="'+stroke+'" stroke-width="'+w+'" stroke-linecap="round" marker-end="url(#'+this._id+'-arr)"';
if (opts.dash) s += ' stroke-dasharray="'+opts.dash+'"';
s += '/>';
if (opts.label){
const mx = (a.x+b.x)/2, my = (a.y+b.y)/2;
s += '<text x="'+(mx+(opts.lx||0)).toFixed(2)+'" y="'+(my+(opts.ly||-6)).toFixed(2)+'" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+stroke+'">'+formatLabel(opts.label)+'</text>';
}
return s;
}
_planeSvg(point, normal, opts){
opts = opts || {};
const sz = opts.size || 3;
const fill = opts.fill || MAT.plane.fill;
const op = (opts.opacity !== undefined) ? opts.opacity : MAT.plane.opacity;
// строим два ортогональных вектора в плоскости нормали
const n = norm3(normal);
let u;
if (Math.abs(n[0]) < 0.9) u = norm3(cross(n, [1,0,0]));
else u = norm3(cross(n, [0,1,0]));
const w = norm3(cross(n, u));
const corners = [
add(point, add(scale3(u, +sz), scale3(w, +sz))),
add(point, add(scale3(u, +sz), scale3(w, -sz))),
add(point, add(scale3(u, -sz), scale3(w, -sz))),
add(point, add(scale3(u, -sz), scale3(w, +sz)))
].map(p => this.project3(p));
const d = corners.map(p => p[0].toFixed(2)+','+p[1].toFixed(2)).join(' ');
let s = '<polygon points="'+d+'" fill="'+fill+'" fill-opacity="'+op+'" stroke="'+(opts.stroke||MAT.plane.stroke)+'" stroke-width="'+(opts.strokeWidth||MAT.plane.strokeWidth)+'"';
if (opts.dash !== false) s += ' stroke-dasharray="'+(opts.dash||MAT.plane.dash)+'"';
s += '/>';
if (opts.label){
const c = this.project3(point);
s += '<text x="'+c[0].toFixed(2)+'" y="'+c[1].toFixed(2)+'" font-size="14" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="#1e3a8a">'+formatLabel(opts.label)+'</text>';
}
return s;
}
_angleMarkSvg(vertex, p1, p2, opts){
opts = opts || {};
const r3 = opts.r || 0.35;
const u = norm3(sub(p1, vertex));
const v = norm3(sub(p2, vertex));
// строим дугу в 3D через интерполяцию по углу
const steps = 12;
const dotUV = Math.max(-1, Math.min(1, dot(u, v)));
const ang = Math.acos(dotUV);
const w = norm3(sub(v, scale3(u, dotUV)));
const pts = [];
for (let i = 0; i <= steps; i++){
const t = ang * i / steps;
const dir = add(scale3(u, Math.cos(t)), scale3(w, Math.sin(t)));
pts.push(this.project3(add(vertex, scale3(dir, r3))));
}
const d = 'M ' + pts.map(p => p[0].toFixed(2)+','+p[1].toFixed(2)).join(' L ');
let s = '<path d="'+d+'" fill="none" stroke="'+(opts.color||'#d97706')+'" stroke-width="'+(opts.width||1.6)+'"/>';
if (opts.label){
const mid = pts[Math.floor(pts.length/2)];
s += '<text x="'+mid[0].toFixed(2)+'" y="'+mid[1].toFixed(2)+'" font-size="12" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+(opts.color||'#d97706')+'">'+formatLabel(opts.label)+'</text>';
}
return s;
}
_rightAngleMarkSvg(vertex, p1, p2, opts){
opts = opts || {};
const sz = opts.size || 0.2;
const u = norm3(sub(p1, vertex));
const v = norm3(sub(p2, vertex));
const a = add(vertex, scale3(u, sz));
const b = add(vertex, scale3(add(u, v), sz));
const c = add(vertex, scale3(v, sz));
const A = this.project3(a), B = this.project3(b), C = this.project3(c);
return '<polyline points="'+A[0].toFixed(2)+','+A[1].toFixed(2)+' '+B[0].toFixed(2)+','+B[1].toFixed(2)+' '+C[0].toFixed(2)+','+C[1].toFixed(2)+'" fill="none" stroke="'+(opts.color||'#7c3aed')+'" stroke-width="'+(opts.width||1.6)+'"/>';
}
}
/* === Форматирование подписей: A_1, B_1, alpha -> греческие, и т.д. === */
function formatLabel(s){
if (s == null) return '';
let out = String(s);
// греческие
out = out.replace(/\balpha\b/g, 'α')
.replace(/\bbeta\b/g, 'β')
.replace(/\bgamma\b/g, 'γ')
.replace(/\bphi\b/g, 'φ')
.replace(/\btheta\b/g, 'θ')
.replace(/\bpi\b/g, 'π');
// одиночные индексы _1, _2 — превращаем в <tspan>
out = out.replace(/_(\d)/g, '<tspan baseline-shift="sub" font-size="0.7em">$1</tspan>');
// _{abc} — тоже
out = out.replace(/_\{([^}]+)\}/g, '<tspan baseline-shift="sub" font-size="0.7em">$1</tspan>');
return out;
}
S.Scene = Scene;
S.formatLabel = formatLabel;
})();
+370
View File
@@ -0,0 +1,370 @@
<!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>Геометрия 10 класс — учебник</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>
<script src="/js/stereo3d.js?v=1" defer></script>
<style>
:root{
--bg:#eff6ff; --card:#fff;
--text:#0b1d33; --muted:#475569;
--border:#dbeafe;
--pri:#2563eb; --pri-d:#1d4ed8;
--pri-soft:#dbeafe;
--r1:#2563eb; --r1-d:#1d4ed8;
--r2:#059669; --r2-d:#047857;
--r3:#e11d48; --r3-d:#be123c;
--r4:#d97706; --r4-d:#b45309;
--sh:0 4px 16px rgba(37,99,235,.10);
--sh-h:0 12px 36px rgba(37,99,235,.18);
}
html.dark{
--bg:#020617; --card:#0a1929;
--text:#dbeafe; --muted:#94a3b8;
--border:#1e293b;
--pri-soft:rgba(37,99,235,.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}
.hdr{position:relative;background:linear-gradient(110deg,#1e3a8a 0%,#2563eb 55%,#93c5fd 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(219,234,254,.15)}
.hdr::before{content:'СТЕРЕОМЕТРИЯ';position:absolute;right:-14px;top:-18%;font-family:'Outfit',sans-serif;font-size:clamp(4rem,12vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(219,234,254,.10);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:.85;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}
.prog-overall{background:linear-gradient(135deg,var(--pri-soft),rgba(225,29,72,.10));border:1px solid var(--border);border-radius:14px;padding:14px 18px;margin-bottom:28px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.po-icon{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#2563eb,#93c5fd);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:900}
.po-text{flex:1;min-width:160px}
.po-label{font-size:.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
.po-bar{height:8px;background:rgba(37,99,235,.12);border-radius:5px;overflow:hidden;margin-top:6px}
.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#e11d48);border-radius:5px;transition:width .5s}
.po-xp{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,#f59e0b,#dc2626);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(220,38,38,.22)}
.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
@media(min-width:600px){.ch-grid{grid-template-columns:1fr 1fr}}
@media(min-width:1000px){.ch-grid{grid-template-columns:repeat(2,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:6rem;font-weight:900;font-family:'Outfit',sans-serif;line-height:1;color:rgba(255,255,255,.18);pointer-events:none}
.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.r1{background:linear-gradient(135deg,#1e3a8a,#2563eb 60%,#93c5fd)}
.ch-cover.r2{background:linear-gradient(135deg,#064e3b,#059669 60%,#86efac)}
.ch-cover.r3{background:linear-gradient(135deg,#7f1d1d,#e11d48 60%,#fda4af)}
.ch-cover.r4{background:linear-gradient(135deg,#78350f,#d97706 60%,#fcd34d)}
.ch-body{padding:16px 20px 18px;display:flex;flex-direction:column;flex:1}
.ch-desc{font-size:.88rem;color:var(--text);opacity:.82;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.r1-card .ch-prog-fill{background:linear-gradient(90deg,var(--r1),var(--r1-d))}
.ch-card.r2-card .ch-prog-fill{background:linear-gradient(90deg,var(--r2),var(--r2-d))}
.ch-card.r3-card .ch-prog-fill{background:linear-gradient(90deg,var(--r3),var(--r3-d))}
.ch-card.r4-card .ch-prog-fill{background:linear-gradient(90deg,var(--r4),var(--r4-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.r1-card .ch-action{background:linear-gradient(135deg,var(--r1),#93c5fd)}
.ch-card.r2-card .ch-action{background:linear-gradient(135deg,var(--r2),#86efac)}
.ch-card.r3-card .ch-action{background:linear-gradient(135deg,var(--r3),#fda4af)}
.ch-card.r4-card .ch-action{background:linear-gradient(135deg,var(--r4),#fcd34d)}
.ach-strip{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:18px 22px;margin-bottom:28px;display:flex;align-items:center;gap:16px;transition:border-color .4s,box-shadow .4s}
.ach-strip.lit{border-color:#f59e0b;box-shadow:0 0 0 3px rgba(245,158,11,.18)}
.ach-icon{width:52px;height:52px;border-radius:14px;background:rgba(0,0,0,.06);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .4s}
.ach-strip.lit .ach-icon{background:linear-gradient(135deg,#fbbf24,#f59e0b)}
.ach-icon svg{width:28px;height:28px;stroke:var(--muted);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ach-strip.lit .ach-icon svg{stroke:#fff}
.ach-text{flex:1}
.ach-title{font-weight:800;font-size:1.02rem;color:var(--text)}
.ach-sub{font-size:.85rem;color:var(--muted);margin-top:2px}
.ach-strip.lit .ach-title{color:#92400e}
.preview-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-bottom:30px}
.preview-cell{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:12px;text-align:center}
.preview-cell-label{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:6px;text-transform:uppercase;letter-spacing:.06em}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbooks" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К каталогу
</a>
</div>
<div>
<h1>Геометрия — 10 класс</h1>
<div class="hdr-sub">Стереометрия · 3D-фигуры · Координаты и векторы</div>
</div>
<div class="hdr-side">
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
<svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
<span id="theme-lab">Тёмная</span>
</button>
</div>
</div>
</header>
<main>
<section class="prog-overall">
<div class="po-icon"></div>
<div class="po-text">
<div class="po-label">Общий прогресс по курсу</div>
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
</div>
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
</section>
<div class="preview-row" id="preview-row">
<div class="preview-cell"><div id="pv-cube"></div><div class="preview-cell-label">Куб</div></div>
<div class="preview-cell"><div id="pv-pyr"></div><div class="preview-cell-label">Пирамида</div></div>
<div class="preview-cell"><div id="pv-prism"></div><div class="preview-cell-label">Призма</div></div>
<div class="preview-cell"><div id="pv-tetra"></div><div class="preview-cell-label">Тетраэдр</div></div>
</div>
<div class="ch-grid">
<a href="/textbook/geometry-10-r1" class="ch-card r1-card" id="r-1">
<div class="ch-cover r1">
<div class="ch-cover-wm"></div>
<div class="ch-num">Раздел 1</div>
<div class="ch-title">Введение в стереометрию</div>
<div class="ch-range">§1–§3 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Пространственные фигуры (призма, пирамида, цилиндр, конус, шар), аксиомы стереометрии и их следствия, метод сечений многогранников.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-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/geometry-10-r2" class="ch-card r2-card" id="r-2">
<div class="ch-cover r2">
<div class="ch-cover-wm"></div>
<div class="ch-num">Раздел 2</div>
<div class="ch-title">Параллельность</div>
<div class="ch-range">§4–§6 + Финал</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/geometry-10-r3" class="ch-card r3-card" id="r-3">
<div class="ch-cover r3">
<div class="ch-cover-wm"></div>
<div class="ch-num">Раздел 3</div>
<div class="ch-title">Перпендикулярность</div>
<div class="ch-range">§7–§10 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Перпендикулярность прямой и плоскости, расстояния в пространстве, угол между прямой и плоскостью (теорема о трёх перпендикулярах), двугранный угол.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-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/geometry-10-r4" class="ch-card r4-card" id="r-4">
<div class="ch-cover r4">
<div class="ch-cover-wm"></div>
<div class="ch-num">Раздел 4</div>
<div class="ch-title">Координаты и векторы</div>
<div class="ch-range">§11–§14 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Прямоугольная система координат в пространстве, векторы и действия над ними, скалярное произведение, применение векторно-координатного метода.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-4">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-4" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-4">Открыть раздел</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
</div>
<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">Магистр геометрии 10</div>
<div class="ach-sub" id="ach-sub">Прочитайте все 14 параграфов четырёх разделов, чтобы получить достижение</div>
</div>
</div>
</main>
<footer class="foot">
Интерактивный учебник «Геометрия — 10 класс» · Л. А. Латотин, Б. Д. Чеботаревский, И. В. Горбунова · LearnSpace
</footer>
<script>
'use strict';
(function(){
var saved = localStorage.getItem('geometry10_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('geometry10_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
var TOTAL = 14;
var CH_PARA = { 'geometry-10-r1': 3, 'geometry-10-r2': 3, 'geometry-10-r3': 4, 'geometry-10-r4': 4 };
var CH_IDX = { 'geometry-10-r1': 1, 'geometry-10-r2': 2, 'geometry-10-r3': 3, 'geometry-10-r4': 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 = 'Продолжить';
else if (readCount >= total) btnEl.textContent = 'Открыть снова';
else btnEl.textContent = 'Открыть раздел';
}
return pct;
}
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 + ' параграфов · ' + pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
var xpBadge = document.getElementById('hero-xp-badge');
var xp = parseInt(localStorage.getItem('geometry10_xp') || '0', 10) || 0;
if (xpBadge && xp > 0) { xpBadge.style.display = ''; xpBadge.textContent = xp + ' XP'; }
if (totalRead >= TOTAL) {
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) sub.textContent = 'Выполнено! Вы прочитали весь курс геометрии 10 класса.';
}
}
function loadProgress() {
if (typeof window.LS === 'undefined' || typeof window.LS.api !== 'function') {
renderProgress([]);
return;
}
window.LS.api('/api/textbooks/geometry-10/children')
.then(function(data) {
if (data && data.children) renderProgress(data.children);
else renderProgress([]);
})
.catch(function() { renderProgress([]); });
}
function renderPreviews(){
if (typeof window.STEREO3D === 'undefined') return;
var S = window.STEREO3D;
var pairs = [
{id:'pv-cube', fn:function(sc){sc.addCube({center:[0,0,0],size:1.8,labels:false,color:'#dbeafe'});}},
{id:'pv-pyr', fn:function(sc){sc.addPyramid({n:4,baseRadius:1.3,height:2,color:'#fee2e2'});}},
{id:'pv-prism', fn:function(sc){sc.addPrism({n:6,baseRadius:1.2,height:2,color:'#d1fae5'});}},
{id:'pv-tetra', fn:function(sc){sc.addTetrahedron({size:1.4,color:'#fef3c7'});}}
];
for (var i = 0; i < pairs.length; i++){
var el = document.getElementById(pairs[i].id);
if (!el) continue;
var sc = new S.Scene(180, 150, {view:'CABINET', scale:32, bg:'transparent', border:'none'});
pairs[i].fn(sc);
el.innerHTML = sc.render();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function(){ loadProgress(); renderPreviews(); });
} else {
loadProgress();
renderPreviews();
}
window.addEventListener('focus', loadProgress);
// stereo3d loads with defer — повторная попытка после загрузки
window.addEventListener('load', renderPreviews);
</script>
</body>
</html>
+168
View File
@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Геометрия 10 · Введение в стереометрию</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>
<script src="/js/stereo3d.js?v=1" defer></script>
<style>
:root{
--bg:#f8fafc; --card:#fff;
--text:#0f172a; --muted:#475569;
--border:#e2e8f0;
--pri:#2563eb; --pri-d:#1d4ed8;
--pri-soft:#dbeafe;
--dark:#1e3a8a;
--sh:0 4px 16px rgba(0,0,0,.06);
}
html.dark{
--bg:#020617; --card:#0a1929;
--text:#dbeafe; --muted:#94a3b8;
--border:#1e293b;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(110deg,var(--dark) 0%,var(--pri) 55%,var(--pri-soft) 100%);color:#fff;padding:32px 24px 28px;overflow:hidden}
.hdr::before{content:'△';position:absolute;right:8px;top:-20%;font-family:'Outfit',sans-serif;font-size:clamp(8rem,22vw,18rem);font-weight:900;color:rgba(255,255,255,.10);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.7rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.85;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:980px;margin:0 auto;padding:32px 24px 60px}
.intro-card{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:22px 26px;margin-bottom:28px;box-shadow:var(--sh)}
.intro-num{display:inline-block;padding:4px 10px;background:var(--pri-soft);color:var(--pri-d);border-radius:99px;font-size:.72rem;font-weight:800;letter-spacing:.06em;margin-bottom:8px;text-transform:uppercase}
.intro-card h2{font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:800;margin-bottom:6px}
.intro-card p{color:var(--muted);font-size:.95rem}
.para-grid{display:grid;grid-template-columns:1fr;gap:14px}
.para-card{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:18px 20px;display:flex;gap:16px;align-items:flex-start;transition:transform .15s,box-shadow .15s,border-color .15s}
.para-card:hover{transform:translateY(-2px);box-shadow:var(--sh);border-color:var(--pri)}
.para-num{font-family:'Outfit',sans-serif;font-size:1rem;font-weight:900;color:#fff;background:linear-gradient(135deg,var(--pri),var(--pri-d));padding:8px 12px;border-radius:9px;min-width:56px;text-align:center;letter-spacing:-.02em;flex-shrink:0}
.para-body{flex:1}
.para-title{font-family:'Outfit',sans-serif;font-size:1.05rem;font-weight:800;margin-bottom:4px;color:var(--text)}
.para-sub{font-size:.88rem;color:var(--muted);margin-bottom:10px;line-height:1.55}
.para-status{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:var(--muted);background:rgba(0,0,0,.04);padding:6px 10px;border-radius:8px;font-weight:600}
html.dark .para-status{background:rgba(255,255,255,.06)}
.para-status .ic{width:14px;height:14px}
.banner-soon{margin-top:30px;text-align:center;padding:20px;background:linear-gradient(135deg,var(--pri-soft),transparent);border:1px dashed var(--pri);border-radius:14px;color:var(--pri-d);font-weight:700;font-size:.92rem}
.banner-soon b{font-family:'Outfit',sans-serif}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/geometry-10" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К курсу геометрии 10
</a>
</div>
<div>
<h1>Раздел 1. Введение в стереометрию</h1>
<div class="hdr-sub">Пространственные фигуры · Аксиомы · Сечения · §1–§3 + Финал</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>
<div class="intro-card">
<span class="intro-num">Раздел 1</span>
<h2>Введение в стереометрию</h2>
<p>Пространственные фигуры · Аксиомы · Сечения. Раздел содержит 3 параграфа и финальный этап с боссами.</p>
</div>
<div class="para-grid">
<article class="para-card" data-para="1">
<div class="para-num">§ 1</div>
<div class="para-body">
<h2 class="para-title">Пространственные фигуры</h2>
<p class="para-sub">Призма, пирамида, цилиндр, конус, шар. Грани, рёбра, вершины.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="2">
<div class="para-num">§ 2</div>
<div class="para-body">
<h2 class="para-title">Прямые и плоскости</h2>
<p class="para-sub">Аксиомы стереометрии (A1–A3) и их следствия. 4 способа задания плоскости.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="3">
<div class="para-num">§ 3</div>
<div class="para-body">
<h2 class="para-title">Построения сечений</h2>
<p class="para-sub">Метод следов. Сечения куба, призмы, пирамиды.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
</div>
<div class="banner-soon">
<b>Раздел в разработке.</b> Полная реализация — в следующих волнах. Уже доступна 3D-библиотека <code>stereo3d.js</code>.
</div>
</main>
<footer class="foot">
Геометрия — 10 класс · Раздел 1 · LearnSpace
</footer>
<script>
'use strict';
(function(){
var saved = localStorage.getItem('geometry10_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('geometry10_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
</script>
</body>
</html>
+168
View File
@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Геометрия 10 · Параллельность</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>
<script src="/js/stereo3d.js?v=1" defer></script>
<style>
:root{
--bg:#f8fafc; --card:#fff;
--text:#0f172a; --muted:#475569;
--border:#e2e8f0;
--pri:#059669; --pri-d:#047857;
--pri-soft:#d1fae5;
--dark:#064e3b;
--sh:0 4px 16px rgba(0,0,0,.06);
}
html.dark{
--bg:#020617; --card:#0a1929;
--text:#dbeafe; --muted:#94a3b8;
--border:#1e293b;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(110deg,var(--dark) 0%,var(--pri) 55%,var(--pri-soft) 100%);color:#fff;padding:32px 24px 28px;overflow:hidden}
.hdr::before{content:'∥';position:absolute;right:8px;top:-20%;font-family:'Outfit',sans-serif;font-size:clamp(8rem,22vw,18rem);font-weight:900;color:rgba(255,255,255,.10);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.7rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.85;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:980px;margin:0 auto;padding:32px 24px 60px}
.intro-card{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:22px 26px;margin-bottom:28px;box-shadow:var(--sh)}
.intro-num{display:inline-block;padding:4px 10px;background:var(--pri-soft);color:var(--pri-d);border-radius:99px;font-size:.72rem;font-weight:800;letter-spacing:.06em;margin-bottom:8px;text-transform:uppercase}
.intro-card h2{font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:800;margin-bottom:6px}
.intro-card p{color:var(--muted);font-size:.95rem}
.para-grid{display:grid;grid-template-columns:1fr;gap:14px}
.para-card{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:18px 20px;display:flex;gap:16px;align-items:flex-start;transition:transform .15s,box-shadow .15s,border-color .15s}
.para-card:hover{transform:translateY(-2px);box-shadow:var(--sh);border-color:var(--pri)}
.para-num{font-family:'Outfit',sans-serif;font-size:1rem;font-weight:900;color:#fff;background:linear-gradient(135deg,var(--pri),var(--pri-d));padding:8px 12px;border-radius:9px;min-width:56px;text-align:center;letter-spacing:-.02em;flex-shrink:0}
.para-body{flex:1}
.para-title{font-family:'Outfit',sans-serif;font-size:1.05rem;font-weight:800;margin-bottom:4px;color:var(--text)}
.para-sub{font-size:.88rem;color:var(--muted);margin-bottom:10px;line-height:1.55}
.para-status{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:var(--muted);background:rgba(0,0,0,.04);padding:6px 10px;border-radius:8px;font-weight:600}
html.dark .para-status{background:rgba(255,255,255,.06)}
.para-status .ic{width:14px;height:14px}
.banner-soon{margin-top:30px;text-align:center;padding:20px;background:linear-gradient(135deg,var(--pri-soft),transparent);border:1px dashed var(--pri);border-radius:14px;color:var(--pri-d);font-weight:700;font-size:.92rem}
.banner-soon b{font-family:'Outfit',sans-serif}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/geometry-10" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К курсу геометрии 10
</a>
</div>
<div>
<h1>Раздел 2. Параллельность</h1>
<div class="hdr-sub">Прямые · Прямая и плоскость · Плоскости · §4–§6 + Финал</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>
<div class="intro-card">
<span class="intro-num">Раздел 2</span>
<h2>Параллельность</h2>
<p>Прямые · Прямая и плоскость · Плоскости. Раздел содержит 3 параграфа и финальный этап с боссами.</p>
</div>
<div class="para-grid">
<article class="para-card" data-para="4">
<div class="para-num">§ 4</div>
<div class="para-body">
<h2 class="para-title">Расположение прямых в пространстве</h2>
<p class="para-sub">Пересекающиеся, параллельные, скрещивающиеся прямые. Угол между скрещивающимися.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="5">
<div class="para-num">§ 5</div>
<div class="para-body">
<h2 class="para-title">Прямая и плоскость</h2>
<p class="para-sub">Три случая. Признак параллельности прямой и плоскости.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="6">
<div class="para-num">§ 6</div>
<div class="para-body">
<h2 class="para-title">Две плоскости</h2>
<p class="para-sub">Пересекаются или параллельны. Признак параллельности плоскостей.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
</div>
<div class="banner-soon">
<b>Раздел в разработке.</b> Полная реализация — в следующих волнах. Уже доступна 3D-библиотека <code>stereo3d.js</code>.
</div>
</main>
<footer class="foot">
Геометрия — 10 класс · Раздел 2 · LearnSpace
</footer>
<script>
'use strict';
(function(){
var saved = localStorage.getItem('geometry10_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('geometry10_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
</script>
</body>
</html>
+180
View File
@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Геометрия 10 · Перпендикулярность</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>
<script src="/js/stereo3d.js?v=1" defer></script>
<style>
:root{
--bg:#f8fafc; --card:#fff;
--text:#0f172a; --muted:#475569;
--border:#e2e8f0;
--pri:#e11d48; --pri-d:#be123c;
--pri-soft:#fee2e2;
--dark:#7f1d1d;
--sh:0 4px 16px rgba(0,0,0,.06);
}
html.dark{
--bg:#020617; --card:#0a1929;
--text:#dbeafe; --muted:#94a3b8;
--border:#1e293b;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(110deg,var(--dark) 0%,var(--pri) 55%,var(--pri-soft) 100%);color:#fff;padding:32px 24px 28px;overflow:hidden}
.hdr::before{content:'⊥';position:absolute;right:8px;top:-20%;font-family:'Outfit',sans-serif;font-size:clamp(8rem,22vw,18rem);font-weight:900;color:rgba(255,255,255,.10);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.7rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.85;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:980px;margin:0 auto;padding:32px 24px 60px}
.intro-card{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:22px 26px;margin-bottom:28px;box-shadow:var(--sh)}
.intro-num{display:inline-block;padding:4px 10px;background:var(--pri-soft);color:var(--pri-d);border-radius:99px;font-size:.72rem;font-weight:800;letter-spacing:.06em;margin-bottom:8px;text-transform:uppercase}
.intro-card h2{font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:800;margin-bottom:6px}
.intro-card p{color:var(--muted);font-size:.95rem}
.para-grid{display:grid;grid-template-columns:1fr;gap:14px}
.para-card{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:18px 20px;display:flex;gap:16px;align-items:flex-start;transition:transform .15s,box-shadow .15s,border-color .15s}
.para-card:hover{transform:translateY(-2px);box-shadow:var(--sh);border-color:var(--pri)}
.para-num{font-family:'Outfit',sans-serif;font-size:1rem;font-weight:900;color:#fff;background:linear-gradient(135deg,var(--pri),var(--pri-d));padding:8px 12px;border-radius:9px;min-width:56px;text-align:center;letter-spacing:-.02em;flex-shrink:0}
.para-body{flex:1}
.para-title{font-family:'Outfit',sans-serif;font-size:1.05rem;font-weight:800;margin-bottom:4px;color:var(--text)}
.para-sub{font-size:.88rem;color:var(--muted);margin-bottom:10px;line-height:1.55}
.para-status{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:var(--muted);background:rgba(0,0,0,.04);padding:6px 10px;border-radius:8px;font-weight:600}
html.dark .para-status{background:rgba(255,255,255,.06)}
.para-status .ic{width:14px;height:14px}
.banner-soon{margin-top:30px;text-align:center;padding:20px;background:linear-gradient(135deg,var(--pri-soft),transparent);border:1px dashed var(--pri);border-radius:14px;color:var(--pri-d);font-weight:700;font-size:.92rem}
.banner-soon b{font-family:'Outfit',sans-serif}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/geometry-10" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К курсу геометрии 10
</a>
</div>
<div>
<h1>Раздел 3. Перпендикулярность</h1>
<div class="hdr-sub">Прямая ⊥ плоскость · Расстояния · Углы · Двугранный угол · §7–§10 + Финал</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>
<div class="intro-card">
<span class="intro-num">Раздел 3</span>
<h2>Перпендикулярность</h2>
<p>Прямая ⊥ плоскость · Расстояния · Углы · Двугранный угол. Раздел содержит 4 параграфа и финальный этап с боссами.</p>
</div>
<div class="para-grid">
<article class="para-card" data-para="7">
<div class="para-num">§ 7</div>
<div class="para-body">
<h2 class="para-title">Перпендикулярность прямой и плоскости</h2>
<p class="para-sub">Определение, признак перпендикулярности.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="8">
<div class="para-num">§ 8</div>
<div class="para-body">
<h2 class="para-title">Расстояния</h2>
<p class="para-sub">От точки до плоскости, между параллельными плоскостями, между скрещивающимися.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="9">
<div class="para-num">§ 9</div>
<div class="para-body">
<h2 class="para-title">Угол между прямой и плоскостью</h2>
<p class="para-sub">Наклонная и её проекция. Теорема о трёх перпендикулярах.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="10">
<div class="para-num">§ 10</div>
<div class="para-body">
<h2 class="para-title">Перпендикулярность плоскостей</h2>
<p class="para-sub">Двугранный угол. Признак перпендикулярности плоскостей.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
</div>
<div class="banner-soon">
<b>Раздел в разработке.</b> Полная реализация — в следующих волнах. Уже доступна 3D-библиотека <code>stereo3d.js</code>.
</div>
</main>
<footer class="foot">
Геометрия — 10 класс · Раздел 3 · LearnSpace
</footer>
<script>
'use strict';
(function(){
var saved = localStorage.getItem('geometry10_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('geometry10_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
</script>
</body>
</html>
+180
View File
@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Геометрия 10 · Координаты и векторы</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>
<script src="/js/stereo3d.js?v=1" defer></script>
<style>
:root{
--bg:#f8fafc; --card:#fff;
--text:#0f172a; --muted:#475569;
--border:#e2e8f0;
--pri:#d97706; --pri-d:#b45309;
--pri-soft:#fef3c7;
--dark:#78350f;
--sh:0 4px 16px rgba(0,0,0,.06);
}
html.dark{
--bg:#020617; --card:#0a1929;
--text:#dbeafe; --muted:#94a3b8;
--border:#1e293b;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(110deg,var(--dark) 0%,var(--pri) 55%,var(--pri-soft) 100%);color:#fff;padding:32px 24px 28px;overflow:hidden}
.hdr::before{content:'→';position:absolute;right:8px;top:-20%;font-family:'Outfit',sans-serif;font-size:clamp(8rem,22vw,18rem);font-weight:900;color:rgba(255,255,255,.10);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.7rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.85;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:980px;margin:0 auto;padding:32px 24px 60px}
.intro-card{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:22px 26px;margin-bottom:28px;box-shadow:var(--sh)}
.intro-num{display:inline-block;padding:4px 10px;background:var(--pri-soft);color:var(--pri-d);border-radius:99px;font-size:.72rem;font-weight:800;letter-spacing:.06em;margin-bottom:8px;text-transform:uppercase}
.intro-card h2{font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:800;margin-bottom:6px}
.intro-card p{color:var(--muted);font-size:.95rem}
.para-grid{display:grid;grid-template-columns:1fr;gap:14px}
.para-card{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:18px 20px;display:flex;gap:16px;align-items:flex-start;transition:transform .15s,box-shadow .15s,border-color .15s}
.para-card:hover{transform:translateY(-2px);box-shadow:var(--sh);border-color:var(--pri)}
.para-num{font-family:'Outfit',sans-serif;font-size:1rem;font-weight:900;color:#fff;background:linear-gradient(135deg,var(--pri),var(--pri-d));padding:8px 12px;border-radius:9px;min-width:56px;text-align:center;letter-spacing:-.02em;flex-shrink:0}
.para-body{flex:1}
.para-title{font-family:'Outfit',sans-serif;font-size:1.05rem;font-weight:800;margin-bottom:4px;color:var(--text)}
.para-sub{font-size:.88rem;color:var(--muted);margin-bottom:10px;line-height:1.55}
.para-status{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:var(--muted);background:rgba(0,0,0,.04);padding:6px 10px;border-radius:8px;font-weight:600}
html.dark .para-status{background:rgba(255,255,255,.06)}
.para-status .ic{width:14px;height:14px}
.banner-soon{margin-top:30px;text-align:center;padding:20px;background:linear-gradient(135deg,var(--pri-soft),transparent);border:1px dashed var(--pri);border-radius:14px;color:var(--pri-d);font-weight:700;font-size:.92rem}
.banner-soon b{font-family:'Outfit',sans-serif}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/geometry-10" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К курсу геометрии 10
</a>
</div>
<div>
<h1>Раздел 4. Координаты и векторы</h1>
<div class="hdr-sub">ПДСК в пространстве · Векторы · Скалярное произведение · §11–§14 + Финал</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>
<div class="intro-card">
<span class="intro-num">Раздел 4</span>
<h2>Координаты и векторы</h2>
<p>ПДСК в пространстве · Векторы · Скалярное произведение. Раздел содержит 4 параграфа и финальный этап с боссами.</p>
</div>
<div class="para-grid">
<article class="para-card" data-para="11">
<div class="para-num">§ 11</div>
<div class="para-body">
<h2 class="para-title">Координаты в пространстве</h2>
<p class="para-sub">ПДСК. Точка (x; y; z). Расстояние между точками.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="12">
<div class="para-num">§ 12</div>
<div class="para-body">
<h2 class="para-title">Вектор. Действия над векторами</h2>
<p class="para-sub">Сложение, умножение на число, базис i, j, k. Разложение вектора.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="13">
<div class="para-num">§ 13</div>
<div class="para-body">
<h2 class="para-title">Скалярное произведение</h2>
<p class="para-sub">a · b = |a||b|cos α = x₁x₂ + y₁y₂ + z₁z₂. Условие перпендикулярности.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
<article class="para-card" data-para="14">
<div class="para-num">§ 14</div>
<div class="para-body">
<h2 class="para-title">Применение координат и векторов</h2>
<p class="para-sub">Уравнения, углы, расстояния. Векторно-координатный метод.</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в следующей волне реализации
</div>
</div>
</article>
</div>
<div class="banner-soon">
<b>Раздел в разработке.</b> Полная реализация — в следующих волнах. Уже доступна 3D-библиотека <code>stereo3d.js</code>.
</div>
</main>
<footer class="foot">
Геометрия — 10 класс · Раздел 4 · LearnSpace
</footer>
<script>
'use strict';
(function(){
var saved = localStorage.getItem('geometry10_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('geometry10_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
</script>
</body>
</html>