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:
@@ -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');
|
||||
@@ -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;
|
||||
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user