2ffe376b2d
Главное приобретение волны: библиотека geom7_svg.js — задел на ВСЮ геометрию 7. 14 функций-хелперов: - point, segment, ray, line — базовые примитивы с подписями/тиками - circle с опц. центром, радиусом, подписью R - arc, angle — дуги углов через atan2; кратчайший путь - rightAngleMark — L-форма ВНУТРЬ угла (полилиния по двум направлениям) - protractor — полукруглый транспортир с делениями каждые 10° - polyline, polygon — ломаная/замкнутый полигон - parallelMark — стрелочки на отрезках - svgBox — обёртка с сеткой и фоном - distance, midPoint, vec, unit, perp, rotate — математика - renderMath — KaTeX с правильными делимитерами Глава 1 — 7 § + Финал по учебнику Казакова 2022 (стр. 8-50): - §1 Повторение 5-6 классов (длина, единицы, точка между двумя) - §2 Предмет геометрии (аксиомы vs теоремы, планиметрия/стереометрия) - §3 Прямая, луч, отрезок, ломаная (SVG-иллюстрации каждого) - §4 Окружность и круг (свойство точки относительно окружности) - §5 Угол и виды углов (острый/прямой/тупой/развёрнутый — SVG) + биссектриса - §6 Смежные и вертикальные углы (с SVG: дополнительные лучи + пересечение) - §7 Перпендикулярные прямые (теоремы единственности) Интерактивы: 2-3 на §, всего 17: - викторины с цветными кнопками (тип угла, аксиома/теорема, верно/нет) - тренажёры (длины, углы, биссектрисы, перпендикуляры) Финал: 5 боссов × 5 этапов = 25 этапов. Темы: §1-2, §3-4, §5, §6, §7. amber-тема, KaTeX, sidebar-шпаргалка с формулами, прогресс/XP, /api/textbooks/geometry-7-ch1/progress. JS парсится OK (75 КБ), HTTP 200, 113 КБ. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
264 lines
13 KiB
JavaScript
264 lines
13 KiB
JavaScript
/* geom7_svg.js — библиотека SVG-хелперов для всей Геометрии 7
|
|
* Используется во всех 5 главах. Намеренно без зависимостей.
|
|
*
|
|
* Координатная система SVG: y растёт вниз — мы учитываем это в хелперах углов.
|
|
* Все функции возвращают строку SVG-фрагмента, готовую к вставке в innerHTML.
|
|
*
|
|
* Публичный API: window.GEOM7.{point,segment,ray,line,circle,arc,angle,
|
|
* rightAngleMark,protractor,polyline,polygon,parallelMark,
|
|
* midPoint,distance,vec,unit,perp,rotate,svgBox}
|
|
*/
|
|
(function(){
|
|
'use strict';
|
|
|
|
if (window.GEOM7 && window.GEOM7.__installed) return;
|
|
const G = window.GEOM7 = window.GEOM7 || {};
|
|
G.__installed = true;
|
|
|
|
/* === Математика === */
|
|
G.distance = (p1, p2) => Math.hypot(p2.x - p1.x, p2.y - p1.y);
|
|
G.midPoint = (p1, p2) => ({ x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 });
|
|
G.vec = (p1, p2) => ({ x:p2.x-p1.x, y:p2.y-p1.y });
|
|
G.unit = (v) => { const L = Math.hypot(v.x, v.y) || 1; return { x:v.x/L, y:v.y/L }; };
|
|
G.perp = (v) => ({ x:-v.y, y:v.x });
|
|
G.rotate = (p, a) => ({ x:p.x*Math.cos(a) - p.y*Math.sin(a), y:p.x*Math.sin(a) + p.y*Math.cos(a) });
|
|
|
|
/* === SVG-обёртка (даёт область чертежа с фоном/сеткой) === */
|
|
G.svgBox = function(w, h, opts){
|
|
opts = opts || {};
|
|
const grid = opts.grid !== false;
|
|
const bg = opts.bg || '#fafafa';
|
|
const cellSize = opts.cell || 20;
|
|
let gridSvg = '';
|
|
if(grid){
|
|
gridSvg = '<defs><pattern id="g7-grid-'+(opts.id||'def')+'" width="'+cellSize+'" height="'+cellSize+'" patternUnits="userSpaceOnUse">'
|
|
+'<path d="M '+cellSize+' 0 L 0 0 0 '+cellSize+'" fill="none" stroke="#e2e8f0" stroke-width="0.6"/>'
|
|
+'</pattern></defs>'
|
|
+'<rect width="'+w+'" height="'+h+'" fill="url(#g7-grid-'+(opts.id||'def')+')"/>';
|
|
}
|
|
return { open:'<svg viewBox="0 0 '+w+' '+h+'" style="max-width:'+(opts.maxWidth||'100%')+';display:block;margin:'+(opts.margin||'0 auto')+';background:'+bg+';border-radius:10px;border:1px solid #e2e8f0">'+gridSvg, close:'</svg>' };
|
|
};
|
|
|
|
/* === Точка === */
|
|
G.point = function(x, y, label, opts){
|
|
opts = opts || {};
|
|
const r = opts.r || 4;
|
|
const color = opts.color || '#d97706';
|
|
const lx = x + (opts.dx !== undefined ? opts.dx : 8);
|
|
const ly = y + (opts.dy !== undefined ? opts.dy : -6);
|
|
let s = '<circle cx="'+x+'" cy="'+y+'" r="'+r+'" fill="'+color+'" stroke="#fff" stroke-width="1.5"/>';
|
|
if(label) s += '<text x="'+lx+'" y="'+ly+'" font-size="'+(opts.fontSize||14)+'" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+(opts.labelColor||color)+'">'+label+'</text>';
|
|
return s;
|
|
};
|
|
|
|
/* === Отрезок === */
|
|
G.segment = function(p1, p2, opts){
|
|
opts = opts || {};
|
|
const color = opts.color || '#0891b2';
|
|
const w = opts.width || 2.5;
|
|
let s = '<line x1="'+p1.x+'" y1="'+p1.y+'" x2="'+p2.x+'" y2="'+p2.y+'" stroke="'+color+'" stroke-width="'+w+'" stroke-linecap="round"';
|
|
if(opts.dash) s += ' stroke-dasharray="'+opts.dash+'"';
|
|
s += '/>';
|
|
/* Метки длины (ticks) */
|
|
if(opts.ticks){
|
|
const mid = G.midPoint(p1, p2);
|
|
const v = G.unit(G.vec(p1, p2));
|
|
const n = G.perp(v);
|
|
const tickLen = opts.tickLen || 5;
|
|
for(let i = 0; i < opts.ticks; i++){
|
|
const off = (i - (opts.ticks - 1)/2) * 4;
|
|
const cx = mid.x + v.x * off;
|
|
const cy = mid.y + v.y * off;
|
|
s += '<line x1="'+(cx + n.x * tickLen)+'" y1="'+(cy + n.y * tickLen)+'" x2="'+(cx - n.x * tickLen)+'" y2="'+(cy - n.y * tickLen)+'" stroke="'+color+'" stroke-width="1.5"/>';
|
|
}
|
|
}
|
|
/* Подпись длины */
|
|
if(opts.label){
|
|
const mid = G.midPoint(p1, p2);
|
|
const n = G.perp(G.unit(G.vec(p1, p2)));
|
|
const off = opts.labelOffset || 14;
|
|
s += '<text x="'+(mid.x + n.x * off)+'" y="'+(mid.y + n.y * off + 4)+'" text-anchor="middle" font-size="12" font-family="JetBrains Mono,monospace" fill="'+(opts.labelColor||'#475569')+'" font-weight="600">'+opts.label+'</text>';
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/* === Луч (от p1 в направлении p2, продолжается за p2 до края — приблизительно) === */
|
|
G.ray = function(p1, p2, opts){
|
|
opts = opts || {};
|
|
const v = G.unit(G.vec(p1, p2));
|
|
const extend = opts.extend || 400; /* продолжить на N px за p2 */
|
|
const end = { x:p2.x + v.x * extend, y:p2.y + v.y * extend };
|
|
const color = opts.color || '#0891b2';
|
|
const w = opts.width || 2.5;
|
|
return '<line x1="'+p1.x+'" y1="'+p1.y+'" x2="'+end.x+'" y2="'+end.y+'" stroke="'+color+'" stroke-width="'+w+'" stroke-linecap="round"/>';
|
|
};
|
|
|
|
/* === Прямая (бесконечная — продолжается в обе стороны) === */
|
|
G.line = function(p1, p2, opts){
|
|
opts = opts || {};
|
|
const v = G.unit(G.vec(p1, p2));
|
|
const extend = opts.extend || 400;
|
|
const start = { x:p1.x - v.x * extend, y:p1.y - v.y * extend };
|
|
const end = { x:p2.x + v.x * extend, y:p2.y + v.y * extend };
|
|
return G.segment(start, end, opts);
|
|
};
|
|
|
|
/* === Окружность === */
|
|
G.circle = function(c, r, opts){
|
|
opts = opts || {};
|
|
const color = opts.color || '#7c3aed';
|
|
let s = '<circle cx="'+c.x+'" cy="'+c.y+'" r="'+r+'" fill="'+(opts.fill||'none')+'" stroke="'+color+'" stroke-width="'+(opts.width||2.5)+'"';
|
|
if(opts.dash) s += ' stroke-dasharray="'+opts.dash+'"';
|
|
s += '/>';
|
|
if(opts.center) s += G.point(c.x, c.y, opts.centerLabel || '', { color:color, dx:6, dy:-6 });
|
|
/* Радиус */
|
|
if(opts.radius){
|
|
const angle = (opts.radiusAngle !== undefined) ? opts.radiusAngle : Math.PI / 4;
|
|
const rEnd = { x:c.x + r * Math.cos(angle), y:c.y - r * Math.sin(angle) };
|
|
s += G.segment(c, rEnd, { color:color, width:1.5, dash:'4 3' });
|
|
if(opts.radiusLabel){
|
|
const mid = G.midPoint(c, rEnd);
|
|
s += '<text x="'+(mid.x+8)+'" y="'+(mid.y-4)+'" font-size="11" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="600">'+opts.radiusLabel+'</text>';
|
|
}
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/* === Дуга угла (от V с двумя направлениями) ===
|
|
* vAng1, vAng2 — углы в радианах (стандартная мат-конвенция: 0 = вправо, ↑ положительный CCW)
|
|
* НО: в SVG y вниз, поэтому работаем напрямую с углами SVG (0 = вправо, ↑ против часовой если y вверх).
|
|
* Мы передаём углы в SVG-конвенции (для нашей системы — y вниз).
|
|
*/
|
|
G.arc = function(c, r, a1, a2, opts){
|
|
opts = opts || {};
|
|
const x1 = c.x + r * Math.cos(a1), y1 = c.y + r * Math.sin(a1);
|
|
const x2 = c.x + r * Math.cos(a2), y2 = c.y + r * Math.sin(a2);
|
|
let delta = a2 - a1; while(delta < 0) delta += 2 * Math.PI; while(delta >= 2 * Math.PI) delta -= 2 * Math.PI;
|
|
const large = delta > Math.PI ? 1 : 0;
|
|
const sweep = opts.sweep !== undefined ? opts.sweep : 1;
|
|
const color = opts.color || '#dc2626';
|
|
return '<path d="M '+x1+' '+y1+' A '+r+' '+r+' 0 '+large+' '+sweep+' '+x2+' '+y2+'" fill="'+(opts.fill||'none')+'" stroke="'+color+'" stroke-width="'+(opts.width||2)+'"/>';
|
|
};
|
|
|
|
/* === Угол: вершина V, на которой угол; точки A, B — на сторонах. Возвращает дугу + (опционально) подпись. === */
|
|
G.angle = function(V, A, B, opts){
|
|
opts = opts || {};
|
|
const r = opts.r || 24;
|
|
const a1 = Math.atan2(A.y - V.y, A.x - V.x);
|
|
const a2 = Math.atan2(B.y - V.y, B.x - V.x);
|
|
/* Берём кратчайшую дугу */
|
|
let delta = a2 - a1; while(delta > Math.PI) delta -= 2 * Math.PI; while(delta < -Math.PI) delta += 2 * Math.PI;
|
|
const sweep = delta > 0 ? 1 : 0;
|
|
const large = Math.abs(delta) > Math.PI ? 1 : 0;
|
|
const x1 = V.x + r * Math.cos(a1), y1 = V.y + r * Math.sin(a1);
|
|
const x2 = V.x + r * Math.cos(a2), y2 = V.y + r * Math.sin(a2);
|
|
const color = opts.color || '#dc2626';
|
|
let s = '<path d="M '+x1+' '+y1+' A '+r+' '+r+' 0 '+large+' '+sweep+' '+x2+' '+y2+'" fill="'+(opts.fill||'none')+'" stroke="'+color+'" stroke-width="'+(opts.width||2)+'"/>';
|
|
if(opts.label){
|
|
/* Центр подписи — середина биссектрисы */
|
|
const midA = (a1 + a2) / 2 + (Math.abs(delta) > Math.PI ? Math.PI : 0);
|
|
const lr = r + (opts.labelOffset || 12);
|
|
const lx = V.x + lr * Math.cos(midA);
|
|
const ly = V.y + lr * Math.sin(midA);
|
|
s += '<text x="'+lx+'" y="'+(ly+5)+'" text-anchor="middle" font-size="'+(opts.fontSize||13)+'" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+color+'">'+opts.label+'</text>';
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/* === Маркер прямого угла (L-форма ВНУТРЬ угла) === */
|
|
G.rightAngleMark = function(V, P1, P2, opts){
|
|
opts = opts || {};
|
|
const s = opts.size || 12;
|
|
const u1 = G.unit(G.vec(V, P1));
|
|
const u2 = G.unit(G.vec(V, P2));
|
|
const p1 = { x:V.x + s * u1.x, y:V.y + s * u1.y };
|
|
const c = { x:p1.x + s * u2.x, y:p1.y + s * u2.y };
|
|
const p2 = { x:V.x + s * u2.x, y:V.y + s * u2.y };
|
|
const color = opts.color || '#dc2626';
|
|
return '<polyline points="'+p1.x+','+p1.y+' '+c.x+','+c.y+' '+p2.x+','+p2.y+'" fill="none" stroke="'+color+'" stroke-width="'+(opts.width||1.8)+'"/>';
|
|
};
|
|
|
|
/* === Транспортир (полукруг 180°) === */
|
|
G.protractor = function(c, R, opts){
|
|
opts = opts || {};
|
|
const color = opts.color || '#0891b2';
|
|
const fill = opts.fill || '#fef3c7';
|
|
/* Полукруг по горизонтали (повернуть на 180°) */
|
|
let s = '<path d="M '+(c.x - R)+' '+c.y+' A '+R+' '+R+' 0 0 1 '+(c.x + R)+' '+c.y+' Z" fill="'+fill+'" stroke="'+color+'" stroke-width="2"/>';
|
|
/* Деления */
|
|
for(let deg = 0; deg <= 180; deg += 10){
|
|
const a = Math.PI - deg * Math.PI / 180; /* 180° → слева, 0° → справа */
|
|
const r2 = (deg % 30 === 0) ? R - 12 : R - 6;
|
|
const x1 = c.x + R * Math.cos(a), y1 = c.y - R * Math.sin(a);
|
|
const x2 = c.x + r2 * Math.cos(a), y2 = c.y - r2 * Math.sin(a);
|
|
s += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="'+color+'" stroke-width="1.2"/>';
|
|
if(deg % 30 === 0){
|
|
const xl = c.x + (R - 22) * Math.cos(a), yl = c.y - (R - 22) * Math.sin(a);
|
|
s += '<text x="'+xl+'" y="'+(yl+4)+'" text-anchor="middle" font-size="9" font-family="JetBrains Mono,monospace" fill="'+color+'">'+deg+'</text>';
|
|
}
|
|
}
|
|
/* Центральная точка */
|
|
s += G.point(c.x, c.y, '', { r:3, color:color });
|
|
return s;
|
|
};
|
|
|
|
/* === Ломаная === */
|
|
G.polyline = function(pts, opts){
|
|
opts = opts || {};
|
|
const color = opts.color || '#0891b2';
|
|
const ptsStr = pts.map(p => p.x + ',' + p.y).join(' ');
|
|
let s = '<polyline points="'+ptsStr+'" fill="none" stroke="'+color+'" stroke-width="'+(opts.width||2.5)+'" stroke-linecap="round" stroke-linejoin="round"';
|
|
if(opts.dash) s += ' stroke-dasharray="'+opts.dash+'"';
|
|
s += '/>';
|
|
if(opts.points) pts.forEach((p, i) => { s += G.point(p.x, p.y, opts.labels ? opts.labels[i] : '', { color:color }); });
|
|
return s;
|
|
};
|
|
|
|
/* === Полигон (замкнутый) === */
|
|
G.polygon = function(pts, opts){
|
|
opts = opts || {};
|
|
const color = opts.color || '#0891b2';
|
|
const ptsStr = pts.map(p => p.x + ',' + p.y).join(' ');
|
|
let s = '<polygon points="'+ptsStr+'" fill="'+(opts.fill||'rgba(8,145,178,.10)')+'" stroke="'+color+'" stroke-width="'+(opts.width||2.5)+'" stroke-linejoin="round"/>';
|
|
if(opts.points) pts.forEach((p, i) => { s += G.point(p.x, p.y, opts.labels ? opts.labels[i] : '', { color:color }); });
|
|
return s;
|
|
};
|
|
|
|
/* === Маркер параллельности (стрелки на отрезках) === */
|
|
G.parallelMark = function(p1, p2, opts){
|
|
opts = opts || {};
|
|
const count = opts.count || 1;
|
|
const v = G.unit(G.vec(p1, p2));
|
|
const n = G.perp(v);
|
|
const mid = G.midPoint(p1, p2);
|
|
const color = opts.color || '#7c3aed';
|
|
let s = '';
|
|
const arrow = 6;
|
|
for(let i = 0; i < count; i++){
|
|
const off = (i - (count - 1)/2) * 5;
|
|
const cx = mid.x + v.x * off;
|
|
const cy = mid.y + v.y * off;
|
|
/* Стрелка → > указывает в направлении v */
|
|
s += '<polyline points="'+(cx - v.x * arrow - n.x * arrow * 0.7)+','+(cy - v.y * arrow - n.y * arrow * 0.7)+' '+cx+','+cy+' '+(cx - v.x * arrow + n.x * arrow * 0.7)+','+(cy - v.y * arrow + n.y * arrow * 0.7)+'" fill="none" stroke="'+color+'" stroke-width="1.5" stroke-linecap="round"/>';
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/* === KaTeX render (если доступен) — с правильными делимитерами === */
|
|
G.renderMath = function(root){
|
|
if(!root || !window.renderMathInElement) return;
|
|
try{
|
|
window.renderMathInElement(root, {
|
|
delimiters: [
|
|
{ left:'$$', right:'$$', display:true },
|
|
{ left:'$', right:'$', display:false },
|
|
{ left:'\\[', right:'\\]', display:true },
|
|
{ left:'\\(', right:'\\)', display:false }
|
|
],
|
|
throwOnError: false
|
|
});
|
|
}catch(e){}
|
|
};
|
|
|
|
})();
|