feat(textbooks): Wave 2 — прокачка интерактивов Алгебры 8 (+422 строки)
1. Боксёрский ринг (§1): SVG-канаты вокруг квадрата + 4 цветные угловые подушки + ковёр-pattern + bell-звук через Web Audio API при S=36 + анимация боксёра-победителя на 2с 2. Доказательство √(ab)=√a·√b (§3): кнопка 'Воспроизвести' запускает 5-шаговую анимацию (подсветка прямоугольника → разрез на единичные клетки → склейка в квадрат → бейдж 'Доказано!') 3. Drag&drop с инерцией (§4): pointer-based DnD с ghost-карточкой следующей за курсором, drop-zone подсветка, неверный → тряска и возврат с инерцией, кнопочный fallback для тача 4. Match-игра (§3): SVG-overlay рисует линии соединения между парами выражений (синяя dashed pending → зелёная при совпадении / красная мигающая при ошибке) 5. Real-time валидация (liveCheck): ✓/✗ индикатор появляется при вводе во всех числовых input'ах без нажатия 'Проверить' 6. Game-over modal (squares): красивая модалка с рекордом, SVG-кубком, confetti 7. Hover-preview карточек §§: tooltip с темами параграфа и прогресс-баром 8. Fade-переходы между секциями: 180ms fadeOut + 220ms fadeIn с translateY
This commit is contained in:
@@ -410,6 +410,71 @@ input,select,textarea{font-family:inherit}
|
||||
.psel-card{flex:0 0 148px;scroll-snap-align:start}
|
||||
body{font-size:.94rem}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════
|
||||
WAVE 2 — INTERACTIVE DEPTH
|
||||
═══════════════════════════════════════════════ */
|
||||
|
||||
/* Task 5: live-check indicators */
|
||||
.live-ind{display:inline-block;margin-left:6px;font-weight:900;font-size:1rem;transition:color .15s}
|
||||
.live-ind.ok{color:var(--ok)}
|
||||
.live-ind.fail{color:var(--fail)}
|
||||
|
||||
/* Task 3: drag-and-drop DnD card */
|
||||
.dnd-card{display:inline-flex;align-items:center;justify-content:center;padding:8px 16px;background:linear-gradient(135deg,var(--acc),var(--acc2));color:#fff;border-radius:10px;font-weight:700;font-size:1rem;cursor:grab;user-select:none;font-family:'JetBrains Mono',monospace;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s;touch-action:none}
|
||||
.dnd-card:active{cursor:grabbing;transform:scale(1.06);box-shadow:0 8px 24px rgba(3,169,244,.35)}
|
||||
.dnd-card.dragging-active{opacity:.55;transform:scale(.95)}
|
||||
.dnd-card.correct-dnd{background:linear-gradient(135deg,var(--ok),#059669);animation:dndCorrect .4s ease}
|
||||
.dnd-card.wrong-dnd{animation:dndWrong .5s ease}
|
||||
@keyframes dndCorrect{0%{transform:scale(1.15)}100%{transform:scale(1)}}
|
||||
@keyframes dndWrong{0%,100%{transform:translateX(0)}25%{transform:translateX(-8px)}75%{transform:translateX(8px)}}
|
||||
.dnd-dropzone{min-height:60px;border:2.5px dashed var(--sec-acc,var(--pri));border-radius:12px;background:var(--card);padding:10px 14px;display:flex;align-items:center;justify-content:center;transition:border-color .15s,background .15s;font-size:.88rem;color:var(--muted);font-weight:600}
|
||||
.dnd-dropzone.over{border-color:var(--ok);background:var(--ok-bg)}
|
||||
|
||||
/* Task 4: SVG connection lines for match */
|
||||
#match-svg-overlay{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;overflow:visible}
|
||||
.match-line{stroke-width:2.5;fill:none;stroke-dasharray:none;transition:stroke .25s}
|
||||
.match-line.pending{stroke:var(--acc);stroke-dasharray:6 3;animation:dashFlow .8s linear infinite}
|
||||
.match-line.correct-line{stroke:var(--ok)}
|
||||
.match-line.wrong-line{stroke:var(--fail);animation:lineFlash .3s ease 3}
|
||||
@keyframes dashFlow{to{stroke-dashoffset:-18}}
|
||||
@keyframes lineFlash{0%,100%{opacity:1}50%{opacity:.2}}
|
||||
|
||||
/* Task 6: game-over modal */
|
||||
.game-over-modal{position:fixed;inset:0;z-index:2000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.55);backdrop-filter:blur(4px);animation:modalIn .3s ease}
|
||||
@keyframes modalIn{from{opacity:0}to{opacity:1}}
|
||||
.game-over-box{background:var(--card);border-radius:20px;padding:32px 36px;text-align:center;max-width:360px;width:90%;box-shadow:0 24px 64px rgba(0,0,0,.28);animation:boxIn .4s cubic-bezier(.34,1.56,.64,1)}
|
||||
@keyframes boxIn{from{transform:scale(.75);opacity:0}to{transform:scale(1);opacity:1}}
|
||||
.game-over-score{font-size:3rem;font-weight:900;color:var(--pri2);font-family:'Unbounded',sans-serif;margin:10px 0}
|
||||
.game-over-record{display:inline-flex;align-items:center;gap:8px;padding:8px 16px;background:linear-gradient(135deg,#fcd34d,#f59e0b);color:#451a03;border-radius:10px;font-weight:800;font-size:.95rem;margin:10px 0;animation:recordPulse 1s ease infinite}
|
||||
@keyframes recordPulse{0%,100%{box-shadow:0 0 0 0 rgba(245,158,11,.5)}50%{box-shadow:0 0 0 8px rgba(245,158,11,0)}}
|
||||
|
||||
/* Task 7: psel-card hover preview */
|
||||
.psel-card{overflow:visible}
|
||||
/* restore border-radius clip for the active stripe via pseudo-element only on non-preview */
|
||||
.psel-card-preview{display:none;position:absolute;left:105%;top:0;width:200px;background:var(--card);border:1.5px solid var(--pri);border-radius:12px;padding:12px 14px;z-index:100;box-shadow:var(--sh2);pointer-events:none;animation:previewIn .18s ease}
|
||||
@keyframes previewIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
|
||||
.psel-card:hover .psel-card-preview{display:block}
|
||||
.psel-preview-title{font-size:.72rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
|
||||
.psel-preview-topic{font-size:.78rem;color:var(--text);margin-bottom:2px;padding-left:8px;border-left:2px solid var(--sec-acc,var(--pri))}
|
||||
.psel-preview-bar{height:5px;background:rgba(233,30,99,.12);border-radius:3px;overflow:hidden;margin-top:8px}
|
||||
.psel-preview-fill{height:100%;background:var(--pri);border-radius:3px;transition:width .4s}
|
||||
.psel-preview-pct{font-size:.72rem;color:var(--muted);margin-top:2px}
|
||||
@media(max-width:768px){.psel-card-preview{display:none!important}}
|
||||
|
||||
/* Task 8: section fade transitions */
|
||||
.sec.fade-out{animation:secFadeOut .18s ease forwards}
|
||||
.sec.fade-in{animation:secFadeIn .22s ease forwards}
|
||||
@keyframes secFadeOut{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-8px)}}
|
||||
@keyframes secFadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
||||
|
||||
/* Task 1: boxer ring extras */
|
||||
.ring-boxer{animation:boxerBounce .4s ease infinite alternate}
|
||||
@keyframes boxerBounce{from{transform:translateY(0)}to{transform:translateY(-4px)}}
|
||||
|
||||
/* Task 2: geo proof animation badge */
|
||||
.proof-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,var(--ok),#059669);color:#fff;border-radius:8px;font-weight:700;font-size:.85rem;animation:badgeIn .4s cubic-bezier(.34,1.56,.64,1)}
|
||||
@keyframes badgeIn{from{transform:scale(.5);opacity:0}to{transform:scale(1);opacity:1}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -636,13 +701,20 @@ function achievement(id, text){
|
||||
PARA SELECTOR
|
||||
════════════════════════════════════════════════════════ */
|
||||
const PARAS = [
|
||||
{ id:'p1', num:'§ 1', name:'Квадратный корень', sub:'Арифметический корень' },
|
||||
{ id:'p2', num:'§ 2', name:'Действительные числа', sub:'Иррациональные числа' },
|
||||
{ id:'p3', num:'§ 3', name:'Свойства корней', sub:'√(ab) = √a·√b' },
|
||||
{ id:'p4', num:'§ 4', name:'Применение свойств', sub:'Преобразования' },
|
||||
{ id:'p5', num:'§ 5', name:'Числовые промежутки', sub:'∪ и ∩' },
|
||||
{ id:'p6', num:'§ 6', name:'Системы неравенств', sub:'Двойные неравенства' },
|
||||
{ id:'final', num:'★', name:'Финал главы', sub:'Самооценка · Практика · Увлекательно', final:true },
|
||||
{ id:'p1', num:'§ 1', name:'Квадратный корень', sub:'Арифметический корень',
|
||||
topics:['Определение корня','Арифметический корень','Извлечение корня','Игра «Таблица квадратов»','Ринг: S=36'] },
|
||||
{ id:'p2', num:'§ 2', name:'Действительные числа', sub:'Иррациональные числа',
|
||||
topics:['ℕ ⊂ ℤ ⊂ ℚ ⊂ ℝ','Иррациональные числа','Числовая ось','Классификация чисел'] },
|
||||
{ id:'p3', num:'§ 3', name:'Свойства корней', sub:'√(ab) = √a·√b',
|
||||
topics:['√(ab) = √a·√b','√(a/b) = √a/√b','√(a²) = |a|','Match-игра','Тренажёр упрощения'] },
|
||||
{ id:'p4', num:'§ 4', name:'Применение свойств', sub:'Преобразования',
|
||||
topics:['Вынесение множителя','Внесение под корень','Освобождение от иррац.','Сравнение корней','Drag «упрости √»'] },
|
||||
{ id:'p5', num:'§ 5', name:'Числовые промежутки', sub:'∪ и ∩',
|
||||
topics:['9 типов промежутков','Конструктор промежутка','Объединение A ∪ B','Пересечение A ∩ B'] },
|
||||
{ id:'p6', num:'§ 6', name:'Системы неравенств', sub:'Двойные неравенства',
|
||||
topics:['Система неравенств','Совокупность неравенств','Двойные неравенства','Решение на числовой оси'] },
|
||||
{ id:'final', num:'★', name:'Финал главы', sub:'Самооценка · Практика · Увлекательно', final:true,
|
||||
topics:['10 задач самооценки','3 практические задачи','История знака √','Олимпиадные задачи'] },
|
||||
];
|
||||
|
||||
function buildParaSelector(){
|
||||
@@ -653,10 +725,18 @@ function buildParaSelector(){
|
||||
card.className = 'psel-card' + (p.final ? ' final' : '');
|
||||
card.dataset.id = p.id;
|
||||
card.dataset.progCard = p.id;
|
||||
const topicsHtml = (p.topics||[]).map(t=>`<div class="psel-preview-topic">${t}</div>`).join('');
|
||||
const progPct = STATE.progress[p.id] || 0;
|
||||
card.innerHTML = `
|
||||
<div class="psel-num">${p.num}</div>
|
||||
<div class="psel-name">${p.name}</div>
|
||||
<div class="psel-prog"><div class="psel-prog-fill"></div></div>`;
|
||||
<div class="psel-prog"><div class="psel-prog-fill"></div></div>
|
||||
<div class="psel-card-preview">
|
||||
<div class="psel-preview-title">${p.name}</div>
|
||||
${topicsHtml}
|
||||
<div class="psel-preview-bar"><div class="psel-preview-fill" style="width:${progPct}%"></div></div>
|
||||
<div class="psel-preview-pct">${progPct}% пройдено</div>
|
||||
</div>`;
|
||||
card.addEventListener('click', ()=>goTo(p.id));
|
||||
g.appendChild(card);
|
||||
});
|
||||
@@ -670,15 +750,29 @@ function ensureBuilt(id){
|
||||
if(fn){ fn(); BUILT.add(id); }
|
||||
}
|
||||
function goTo(id){
|
||||
const prevEl = document.querySelector('.sec.active');
|
||||
if(prevEl && prevEl.id !== 'sec-' + id){
|
||||
prevEl.classList.add('fade-out');
|
||||
setTimeout(()=>{
|
||||
prevEl.classList.remove('active','fade-out');
|
||||
_goToFinish(id);
|
||||
}, 180);
|
||||
} else {
|
||||
if(prevEl) prevEl.classList.remove('active');
|
||||
_goToFinish(id);
|
||||
}
|
||||
}
|
||||
function _goToFinish(id){
|
||||
STATE.current = id;
|
||||
ensureBuilt(id);
|
||||
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
|
||||
const el = document.getElementById('sec-' + id);
|
||||
if(el) el.classList.add('active');
|
||||
if(el){
|
||||
el.classList.add('active','fade-in');
|
||||
setTimeout(()=>el.classList.remove('fade-in'), 250);
|
||||
}
|
||||
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id === id));
|
||||
buildSidebar(id);
|
||||
window.scrollTo({top:0, behavior:'smooth'});
|
||||
// мин прогресс просто за вход
|
||||
if((STATE.progress[id]||0) < 10) bumpProgress(id, 10);
|
||||
if(window.renderMathInElement){
|
||||
setTimeout(()=>renderMathInElement(el, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],throwOnError:false}),0);
|
||||
@@ -1019,6 +1113,41 @@ function initMobileSidebar(){
|
||||
window.addEventListener('resize', check);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════
|
||||
WAVE 2 — UTILITIES
|
||||
════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Task 5: live input validation */
|
||||
function liveCheck(inp, expectedFn){
|
||||
let ind = inp.nextElementSibling;
|
||||
if(!ind || !ind.classList.contains('live-ind')){
|
||||
ind = document.createElement('span');
|
||||
ind.className = 'live-ind';
|
||||
inp.after(ind);
|
||||
}
|
||||
inp.addEventListener('input', ()=>{
|
||||
const v = inp.value.trim();
|
||||
if(!v){ ind.className = 'live-ind'; ind.innerHTML=''; return; }
|
||||
const result = expectedFn(v);
|
||||
if(result === true){ ind.className = 'live-ind ok'; ind.innerHTML='✓'; }
|
||||
else if(result === false){ ind.className = 'live-ind fail'; ind.innerHTML='✗'; }
|
||||
else { ind.className = 'live-ind'; ind.innerHTML='…'; }
|
||||
});
|
||||
}
|
||||
|
||||
/* Task 1: bell sound via Web Audio API */
|
||||
function playBell(){
|
||||
try{
|
||||
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const o = ctx.createOscillator(); const g = ctx.createGain();
|
||||
o.connect(g); g.connect(ctx.destination);
|
||||
o.frequency.value = 880; o.type = 'sine';
|
||||
g.gain.setValueAtTime(0.3, ctx.currentTime);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
|
||||
o.start(); o.stop(ctx.currentTime + 0.5);
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
/* Paragraph builders will be defined below */
|
||||
</script>
|
||||
|
||||
@@ -1061,17 +1190,60 @@ function buildP1(){
|
||||
|
||||
${widget('Боксёрский ринг — найди сторону', 'INTERACT', 'Тяните угол квадрата, чтобы изменить его сторону. Поставьте площадь точно 36 м² — получите бонус!', `
|
||||
<svg id="ring-svg" viewBox="0 0 360 220" style="width:100%;max-width:520px;height:240px;background:var(--card);border-radius:10px;border:1px solid var(--border)">
|
||||
<rect id="ring-rect" x="60" y="40" width="120" height="120" fill="rgba(233,30,99,0.16)" stroke="#e91e63" stroke-width="2.5"/>
|
||||
<line x1="60" y1="170" x2="180" y2="170" stroke="#6b5b73" stroke-width="1.5"/>
|
||||
<text id="ring-side-lab" x="120" y="186" text-anchor="middle" fill="#c2185b" font-size="14" font-weight="700">сторона x = 0 м</text>
|
||||
<text id="ring-area-lab" x="120" y="105" text-anchor="middle" fill="#1a1a2e" font-size="22" font-weight="900">S = 0 м²</text>
|
||||
<!-- carpet pattern fill -->
|
||||
<defs>
|
||||
<pattern id="ring-carpet" patternUnits="userSpaceOnUse" width="10" height="10">
|
||||
<rect width="10" height="10" fill="rgba(233,30,99,0.04)"/>
|
||||
<line x1="0" y1="5" x2="10" y2="5" stroke="rgba(233,30,99,0.07)" stroke-width="1"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<!-- carpet background -->
|
||||
<rect id="ring-carpet-rect" x="60" y="40" width="120" height="120" fill="url(#ring-carpet)"/>
|
||||
<!-- main ring mat -->
|
||||
<rect id="ring-rect" x="60" y="40" width="120" height="120" fill="rgba(233,30,99,0.10)" stroke="#e91e63" stroke-width="2.5"/>
|
||||
<!-- 3 ropes (extend slightly beyond ring) -->
|
||||
<g id="ring-ropes">
|
||||
<line id="rope-t1" x1="52" y1="58" x2="188" y2="58" stroke="#e91e63" stroke-width="3" stroke-linecap="round" opacity=".7"/>
|
||||
<line id="rope-t2" x1="52" y1="98" x2="188" y2="98" stroke="#e91e63" stroke-width="3" stroke-linecap="round" opacity=".7"/>
|
||||
<line id="rope-t3" x1="52" y1="138" x2="188" y2="138" stroke="#e91e63" stroke-width="3" stroke-linecap="round" opacity=".55"/>
|
||||
<!-- vertical rope posts -->
|
||||
<line id="rope-l" x1="52" y1="40" x2="52" y2="160" stroke="#c2185b" stroke-width="4" stroke-linecap="round" opacity=".5"/>
|
||||
<line id="rope-r" x1="188" y1="40" x2="188" y2="160" stroke="#c2185b" stroke-width="4" stroke-linecap="round" opacity=".5"/>
|
||||
</g>
|
||||
<!-- corner pads -->
|
||||
<rect id="cp-tl" x="56" y="36" width="10" height="10" rx="2" fill="rgba(239,68,68,0.75)"/>
|
||||
<rect id="cp-tr" x="174" y="36" width="10" height="10" rx="2" fill="rgba(59,130,246,0.75)"/>
|
||||
<rect id="cp-bl" x="56" y="154" width="10" height="10" rx="2" fill="rgba(250,250,250,0.9)" stroke="#ccc" stroke-width="1"/>
|
||||
<rect id="cp-br" x="174" y="154" width="10" height="10" rx="2" fill="rgba(30,30,30,0.75)"/>
|
||||
<!-- dimension line -->
|
||||
<line id="ring-dim-line" x1="60" y1="176" x2="180" y2="176" stroke="#6b5b73" stroke-width="1.5"/>
|
||||
<text id="ring-side-lab" x="120" y="192" text-anchor="middle" fill="#c2185b" font-size="14" font-weight="700">сторона x = 6.0 м</text>
|
||||
<text id="ring-area-lab" x="120" y="105" text-anchor="middle" fill="#1a1a2e" font-size="22" font-weight="900">S = 36 м²</text>
|
||||
<!-- drag handle -->
|
||||
<circle id="ring-handle" cx="180" cy="40" r="9" fill="#03a9f4" stroke="#fff" stroke-width="2" style="cursor:grab"/>
|
||||
<text x="270" y="50" fill="#6b5b73" font-size="12">← тяните угол</text>
|
||||
<text x="265" y="50" fill="#6b5b73" font-size="12">← тяните угол</text>
|
||||
<!-- boxer (hidden initially) -->
|
||||
<g id="ring-boxer" style="display:none" class="ring-boxer">
|
||||
<!-- head -->
|
||||
<circle cx="120" cy="75" r="9" fill="#f9a825" stroke="#e65100" stroke-width="1.5"/>
|
||||
<!-- body -->
|
||||
<line x1="120" y1="84" x2="120" y2="108" stroke="#e65100" stroke-width="2.5"/>
|
||||
<!-- arms with gloves -->
|
||||
<line x1="120" y1="90" x2="104" y2="100" stroke="#e65100" stroke-width="2.5"/>
|
||||
<circle cx="102" cy="101" r="5" fill="#e91e63" stroke="#c2185b" stroke-width="1.5"/>
|
||||
<line x1="120" y1="90" x2="136" y2="100" stroke="#e65100" stroke-width="2.5"/>
|
||||
<circle cx="138" cy="101" r="5" fill="#e91e63" stroke="#c2185b" stroke-width="1.5"/>
|
||||
<!-- legs -->
|
||||
<line x1="120" y1="108" x2="112" y2="124" stroke="#e65100" stroke-width="2.5"/>
|
||||
<line x1="120" y1="108" x2="128" y2="124" stroke="#e65100" stroke-width="2.5"/>
|
||||
<!-- victory text -->
|
||||
<text x="120" y="138" text-anchor="middle" fill="#e91e63" font-size="13" font-weight="900">Победа!</text>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="row" style="margin-top:12px">
|
||||
<div class="chip">Сторона: <b id="ring-side-chip">0</b> м</div>
|
||||
<div class="chip acc">Площадь: <b id="ring-area-chip">0</b> м²</div>
|
||||
<div id="ring-feedback" style="margin-left:auto;font-weight:700;color:var(--ok);display:none">✓ Сторона = √36 = 6 м!</div>
|
||||
<div class="chip">Сторона: <b id="ring-side-chip">6.0</b> м</div>
|
||||
<div class="chip acc">Площадь: <b id="ring-area-chip">36</b> м²</div>
|
||||
<div id="ring-feedback" style="margin-left:auto;font-weight:700;color:var(--ok);display:none">✓ Сторона = √36 = 6 м!</div>
|
||||
</div>
|
||||
`)}
|
||||
|
||||
@@ -1226,38 +1398,89 @@ function buildP1(){
|
||||
function initRing(){
|
||||
const svg = document.getElementById('ring-svg');
|
||||
if(!svg) return;
|
||||
const rect = document.getElementById('ring-rect');
|
||||
const ringRect = document.getElementById('ring-rect');
|
||||
const carpetRect = document.getElementById('ring-carpet-rect');
|
||||
const handle = document.getElementById('ring-handle');
|
||||
const sideLab = document.getElementById('ring-side-lab');
|
||||
const areaLab = document.getElementById('ring-area-lab');
|
||||
const sideChip = document.getElementById('ring-side-chip');
|
||||
const areaChip = document.getElementById('ring-area-chip');
|
||||
const fb = document.getElementById('ring-feedback');
|
||||
const boxer = document.getElementById('ring-boxer');
|
||||
const dimLine = document.getElementById('ring-dim-line');
|
||||
let dragging = false;
|
||||
let ringWon = false;
|
||||
const scale = 20; // 1 м = 20 px
|
||||
const OX = 60, OY_BASE = 200; // origin x, bottom anchor
|
||||
|
||||
function setRope(id, x1, x2, y){ const r = document.getElementById(id); if(r){ r.setAttribute('x1',x1); r.setAttribute('x2',x2); r.setAttribute('y1',y); r.setAttribute('y2',y); } }
|
||||
function setPad(id, x, y){ const p = document.getElementById(id); if(p){ p.setAttribute('x',x); p.setAttribute('y',y); } }
|
||||
function setVPost(id, x, y1, y2){ const p = document.getElementById(id); if(p){ p.setAttribute('x1',x); p.setAttribute('x2',x); p.setAttribute('y1',y1); p.setAttribute('y2',y2); } }
|
||||
|
||||
function update(sideM){
|
||||
sideM = Math.max(0.5, Math.min(10, sideM));
|
||||
const sidePx = sideM * scale;
|
||||
rect.setAttribute('width', sidePx);
|
||||
rect.setAttribute('height', sidePx);
|
||||
rect.setAttribute('y', 200 - sidePx - 30);
|
||||
handle.setAttribute('cx', 60 + sidePx);
|
||||
handle.setAttribute('cy', 200 - sidePx - 30);
|
||||
sideLab.setAttribute('x', 60 + sidePx/2);
|
||||
sideLab.setAttribute('y', 200 - 14);
|
||||
const top = OY_BASE - sidePx - 30;
|
||||
const right = OX + sidePx;
|
||||
|
||||
[ringRect, carpetRect].forEach(r=>{
|
||||
r.setAttribute('width', sidePx); r.setAttribute('height', sidePx);
|
||||
r.setAttribute('x', OX); r.setAttribute('y', top);
|
||||
});
|
||||
handle.setAttribute('cx', right);
|
||||
handle.setAttribute('cy', top);
|
||||
|
||||
// ropes: 3 horizontal at top+offset, mid, near-bottom
|
||||
const ropePad = 8;
|
||||
setRope('rope-t1', OX - ropePad, right + ropePad, top + sidePx*0.2);
|
||||
setRope('rope-t2', OX - ropePad, right + ropePad, top + sidePx*0.5);
|
||||
setRope('rope-t3', OX - ropePad, right + ropePad, top + sidePx*0.8);
|
||||
setVPost('rope-l', OX - ropePad, top, top + sidePx);
|
||||
setVPost('rope-r', right + ropePad, top, top + sidePx);
|
||||
|
||||
// corner pads
|
||||
setPad('cp-tl', OX - ropePad + 2, top - 4);
|
||||
setPad('cp-tr', right + ropePad - 10, top - 4);
|
||||
setPad('cp-bl', OX - ropePad + 2, top + sidePx - 6);
|
||||
setPad('cp-br', right + ropePad - 10, top + sidePx - 6);
|
||||
|
||||
// boxer center position
|
||||
const bx = OX + sidePx/2;
|
||||
const by = top + sidePx/2;
|
||||
if(boxer){
|
||||
boxer.querySelectorAll('circle,line,text').forEach((el,i)=>{
|
||||
// shift entire boxer group by adjusting first head circle
|
||||
});
|
||||
// repositon via transform
|
||||
boxer.setAttribute('transform', `translate(${bx - 120},${by - 100})`);
|
||||
}
|
||||
|
||||
// dimension line
|
||||
if(dimLine){
|
||||
dimLine.setAttribute('x1', OX); dimLine.setAttribute('x2', right);
|
||||
dimLine.setAttribute('y1', top + sidePx + 6); dimLine.setAttribute('y2', top + sidePx + 6);
|
||||
}
|
||||
sideLab.setAttribute('x', OX + sidePx/2);
|
||||
sideLab.setAttribute('y', top + sidePx + 22);
|
||||
sideLab.textContent = 'сторона x = ' + sideM.toFixed(1) + ' м';
|
||||
areaLab.setAttribute('x', 60 + sidePx/2);
|
||||
areaLab.setAttribute('y', 200 - sidePx/2 - 30);
|
||||
areaLab.setAttribute('x', OX + sidePx/2);
|
||||
areaLab.setAttribute('y', top + sidePx/2 + 6);
|
||||
const area = sideM * sideM;
|
||||
areaLab.textContent = 'S = ' + area.toFixed(1) + ' м²';
|
||||
sideChip.textContent = sideM.toFixed(1);
|
||||
areaChip.textContent = area.toFixed(1);
|
||||
|
||||
if(Math.abs(area - 36) < 0.2){
|
||||
fb.style.display = 'inline';
|
||||
fb.textContent = '✓ Точно! Сторона = √36 = 6 м';
|
||||
achievement('ring36','Нашёл сторону ринга');
|
||||
bumpProgress('p1', 8);
|
||||
if(!ringWon){
|
||||
ringWon = true;
|
||||
playBell();
|
||||
fb.style.display = 'inline';
|
||||
fb.innerHTML = '✓ Точно! Сторона = √36 = 6 м';
|
||||
achievement('ring36','Нашёл сторону ринга');
|
||||
bumpProgress('p1', 8);
|
||||
// show boxer for 2s
|
||||
if(boxer){ boxer.style.display=''; setTimeout(()=>{ if(boxer) boxer.style.display='none'; ringWon=false; }, 2200); }
|
||||
}
|
||||
} else {
|
||||
fb.style.display = 'none';
|
||||
}
|
||||
@@ -1266,10 +1489,10 @@ function initRing(){
|
||||
|
||||
const onMove = (e)=>{
|
||||
if(!dragging) return;
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const x = (e.clientX || (e.touches && e.touches[0].clientX) || 0) - rect.left;
|
||||
const svgX = x / rect.width * 360;
|
||||
const sideM = (svgX - 60) / scale;
|
||||
const bbox = svg.getBoundingClientRect();
|
||||
const x = (e.clientX || (e.touches && e.touches[0].clientX) || 0) - bbox.left;
|
||||
const svgX = x / bbox.width * 360;
|
||||
const sideM = (svgX - OX) / scale;
|
||||
update(sideM);
|
||||
};
|
||||
handle.addEventListener('mousedown', ()=>dragging=true);
|
||||
@@ -1360,8 +1583,8 @@ function squaresAnswer(picked, btn){
|
||||
if(sqState.round > 10){
|
||||
const t = ((performance.now()-sqState.t0)/1000).toFixed(1);
|
||||
const total = sqState.score + Math.max(0, Math.round((30 - +t)*2));
|
||||
feedback(fb, true, 'Игра окончена! Итого: ' + total + ' очков, время ' + t + 'с');
|
||||
if(total > (isFinite(STATE.squaresBest) ? STATE.squaresBest : 0)){
|
||||
const isRecord = total > (isFinite(STATE.squaresBest) ? STATE.squaresBest : 0);
|
||||
if(isRecord){
|
||||
STATE.squaresBest = total;
|
||||
saveProgress();
|
||||
achievement('squares','Лучший результат «Таблица квадратов»');
|
||||
@@ -1369,10 +1592,40 @@ function squaresAnswer(picked, btn){
|
||||
bumpProgress('p1', 15);
|
||||
if(sqState.timer) clearInterval(sqState.timer);
|
||||
sqState = null;
|
||||
showGameOverModal(total, t, isRecord);
|
||||
return;
|
||||
}
|
||||
setTimeout(squaresNext, 850);
|
||||
}
|
||||
|
||||
function showGameOverModal(total, timeStr, isRecord){
|
||||
// remove existing
|
||||
const old = document.getElementById('game-over-modal');
|
||||
if(old) old.remove();
|
||||
const best = isFinite(STATE.squaresBest) ? STATE.squaresBest : total;
|
||||
const recHtml = isRecord
|
||||
? `<div class="game-over-record">
|
||||
<svg class="ic" viewBox="0 0 24 24" style="width:20px;height:20px;stroke:#451a03"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg>
|
||||
НОВЫЙ РЕКОРД!
|
||||
</div>`
|
||||
: `<div style="color:var(--muted);font-size:.9rem;margin:8px 0">Рекорд: <b>${best}</b> очков</div>`;
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'game-over-modal';
|
||||
modal.className = 'game-over-modal';
|
||||
modal.innerHTML = `<div class="game-over-box">
|
||||
<div style="font-size:1rem;font-weight:700;color:var(--muted);margin-bottom:4px">Игра окончена!</div>
|
||||
<div class="game-over-score">${total}</div>
|
||||
<div style="font-size:.88rem;color:var(--muted);margin-bottom:8px">очков | время ${timeStr} с</div>
|
||||
${recHtml}
|
||||
<div style="display:flex;gap:12px;justify-content:center;margin-top:18px">
|
||||
<button class="btn primary" onclick="document.getElementById('game-over-modal').remove();squaresStart()">▶ Заново</button>
|
||||
<button class="btn" onclick="document.getElementById('game-over-modal').remove()">Закрыть</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
confetti();
|
||||
modal.addEventListener('click', e=>{ if(e.target === modal) modal.remove(); });
|
||||
}
|
||||
function squaresReset(){
|
||||
if(sqState && sqState.timer) clearInterval(sqState.timer);
|
||||
sqState = null;
|
||||
@@ -1906,6 +2159,10 @@ function buildP3(){
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top:12px;text-align:center;color:var(--ok);font-weight:700">Площади всегда равны → $\\sqrt{ab}$ — сторона эквивалентного квадрата</p>
|
||||
<div class="row-c" style="margin-top:14px">
|
||||
<button class="btn primary" onclick="geoProofAnimate()">▶ Воспроизвести доказательство</button>
|
||||
</div>
|
||||
<div id="geo-proof-anim" style="min-height:36px;margin-top:10px;text-align:center;font-size:.92rem;color:var(--acc2);font-weight:600"></div>
|
||||
`)}
|
||||
|
||||
${makeCard('rule','Свойство 2: корень из частного',null,`
|
||||
@@ -1949,14 +2206,17 @@ function buildP3(){
|
||||
`)}
|
||||
|
||||
${widget('Match: выражение ↔ ответ', 'GAME', 'Соедините каждое выражение с его упрощённым значением. Кликните по выражению, затем по ответу.', `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||||
<div>
|
||||
<div class="dz-label">Выражения</div>
|
||||
<div id="match-left" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="dz-label">Ответы</div>
|
||||
<div id="match-right" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||
<div id="match-container" style="position:relative">
|
||||
<svg id="match-svg-overlay" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;overflow:visible"></svg>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||||
<div>
|
||||
<div class="dz-label">Выражения</div>
|
||||
<div id="match-left" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="dz-label">Ответы</div>
|
||||
<div id="match-right" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-c" style="margin-top:12px">
|
||||
@@ -2045,6 +2305,42 @@ function initGeoProof(){
|
||||
upd();
|
||||
}
|
||||
|
||||
/* ──── Geo Proof Animation ──── */
|
||||
function geoProofAnimate(){
|
||||
const box = document.getElementById('geo-proof-anim');
|
||||
if(!box) return;
|
||||
const aVal = +(document.getElementById('geo-a') ? document.getElementById('geo-a').value : 4);
|
||||
const bVal = +(document.getElementById('geo-b') ? document.getElementById('geo-b').value : 9);
|
||||
const ab = aVal * bVal;
|
||||
const side = Math.sqrt(ab).toFixed(2);
|
||||
const steps = [
|
||||
{ delay:0, html: `<b>Шаг 1.</b> Прямоугольник <span style="color:var(--acc2)">${aVal} × ${bVal}</span> подсвечивается рамкой — его площадь <b>S = a·b = ${ab}</b>` },
|
||||
{ delay:900, html: `<b>Шаг 2.</b> Прямоугольник можно представить как ${ab} единичных клеток (<span style="color:var(--acc2)">${aVal} строк × ${bVal} столбцов</span>)` },
|
||||
{ delay:2000, html: `<b>Шаг 3.</b> Все ${ab} клеток «перетекают» в квадрат — ищем сторону, при которой сторона² = ${ab}` },
|
||||
{ delay:3200, html: `<b>Шаг 4.</b> Квадрат со стороной <b style="color:var(--pri2)">√${ab} ≈ ${side}</b> имеет ту же площадь: S = (√${ab})² = ${ab}` },
|
||||
{ delay:4400, html: `<span class="proof-badge"><svg class="ic" viewBox="0 0 24 24" style="width:16px;height:16px;stroke:#fff"><polyline points="20 6 9 17 4 12"/></svg> Доказано!</span> √(${aVal}·${bVal}) = √${aVal}·√${bVal} = ${Math.sqrt(aVal).toFixed(2)}·${Math.sqrt(bVal).toFixed(2)} = ${side}` },
|
||||
];
|
||||
const rectEl = document.getElementById('geo-rect');
|
||||
const sqEl = document.getElementById('geo-sq');
|
||||
// highlight steps visually
|
||||
if(rectEl){
|
||||
rectEl.style.transition = 'stroke-width .3s';
|
||||
rectEl.setAttribute('stroke-width','4');
|
||||
setTimeout(()=>rectEl.setAttribute('stroke-width','2'), 900);
|
||||
}
|
||||
if(sqEl){
|
||||
setTimeout(()=>{
|
||||
sqEl.style.transition = 'stroke-width .3s';
|
||||
sqEl.setAttribute('stroke-width','4');
|
||||
setTimeout(()=>sqEl.setAttribute('stroke-width','2'), 800);
|
||||
}, 3200);
|
||||
}
|
||||
steps.forEach(s=>{
|
||||
setTimeout(()=>{ box.innerHTML = s.html; renderMath(box); }, s.delay);
|
||||
});
|
||||
setTimeout(()=>bumpProgress('p3', 5), 4500);
|
||||
}
|
||||
|
||||
/* ──── Property check slider ──── */
|
||||
function initPropCheck(){
|
||||
const a = document.getElementById('prop-a');
|
||||
@@ -2074,11 +2370,38 @@ let matchState = null;
|
||||
function initMatch(){
|
||||
matchReset();
|
||||
}
|
||||
|
||||
function _matchLineCoords(btnA, btnB, container){
|
||||
const cRect = container.getBoundingClientRect();
|
||||
const rA = btnA.getBoundingClientRect();
|
||||
const rB = btnB.getBoundingClientRect();
|
||||
return {
|
||||
x1: rA.right - cRect.left,
|
||||
y1: rA.top - cRect.top + rA.height/2,
|
||||
x2: rB.left - cRect.left,
|
||||
y2: rB.top - cRect.top + rB.height/2,
|
||||
};
|
||||
}
|
||||
|
||||
function _drawMatchLine(svg, container, btnL, btnR, cls, id){
|
||||
const c = _matchLineCoords(btnL, btnR, container);
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg','line');
|
||||
line.setAttribute('x1', c.x1); line.setAttribute('y1', c.y1);
|
||||
line.setAttribute('x2', c.x2); line.setAttribute('y2', c.y2);
|
||||
line.className.baseVal = 'match-line ' + cls;
|
||||
if(id) line.id = id;
|
||||
svg.appendChild(line);
|
||||
return line;
|
||||
}
|
||||
|
||||
function matchReset(){
|
||||
const L = document.getElementById('match-left');
|
||||
const R = document.getElementById('match-right');
|
||||
L.innerHTML = ''; R.innerHTML = '';
|
||||
matchState = { selL:null, selR:null, done:[] };
|
||||
const svgEl = document.getElementById('match-svg-overlay');
|
||||
if(L) L.innerHTML = '';
|
||||
if(R) R.innerHTML = '';
|
||||
if(svgEl) svgEl.innerHTML = '';
|
||||
matchState = { selL:null, selR:null, done:[], pendingLine:null };
|
||||
const lArr = [...MATCH_PAIRS].sort(()=>Math.random()-0.5);
|
||||
const rArr = [...MATCH_PAIRS].sort(()=>Math.random()-0.5);
|
||||
lArr.forEach(p=>{
|
||||
@@ -2087,9 +2410,11 @@ function matchReset(){
|
||||
b.dataset.expr = p.expr;
|
||||
b.addEventListener('click', ()=>{
|
||||
if(b.disabled) return;
|
||||
L.querySelectorAll('button').forEach(x=>x.style.background='');
|
||||
if(L) L.querySelectorAll('button').forEach(x=>{if(!x.disabled){x.style.background='';x.style.color='';}});
|
||||
b.style.background='var(--acc-soft)';
|
||||
matchState.selL = b;
|
||||
// remove pending line
|
||||
if(matchState.pendingLine){ matchState.pendingLine.remove(); matchState.pendingLine=null; }
|
||||
matchCheck();
|
||||
});
|
||||
L.appendChild(b);
|
||||
@@ -2100,9 +2425,15 @@ function matchReset(){
|
||||
b.dataset.ans = p.ans;
|
||||
b.addEventListener('click', ()=>{
|
||||
if(b.disabled) return;
|
||||
R.querySelectorAll('button').forEach(x=>x.style.background='');
|
||||
if(R) R.querySelectorAll('button').forEach(x=>{if(!x.disabled){x.style.background='';x.style.color='';}});
|
||||
b.style.background='var(--pri-soft)';
|
||||
matchState.selR = b;
|
||||
// draw pending line
|
||||
if(matchState.selL && svgEl){
|
||||
const container = document.getElementById('match-container');
|
||||
if(matchState.pendingLine) matchState.pendingLine.remove();
|
||||
matchState.pendingLine = _drawMatchLine(svgEl, container, matchState.selL, b, 'pending', 'match-pending');
|
||||
}
|
||||
matchCheck();
|
||||
});
|
||||
R.appendChild(b);
|
||||
@@ -2110,34 +2441,54 @@ function matchReset(){
|
||||
document.getElementById('match-cnt').textContent = '0';
|
||||
document.getElementById('match-fb').className = 'feedback';
|
||||
}
|
||||
|
||||
function matchCheck(){
|
||||
if(!matchState.selL || !matchState.selR) return;
|
||||
const e = matchState.selL.dataset.expr;
|
||||
const a = matchState.selR.dataset.ans;
|
||||
const correct = MATCH_PAIRS.some(p=>p.expr===e && p.ans===a);
|
||||
const fb = document.getElementById('match-fb');
|
||||
const svgEl = document.getElementById('match-svg-overlay');
|
||||
const container = document.getElementById('match-container');
|
||||
// remove pending line
|
||||
if(matchState.pendingLine){ matchState.pendingLine.remove(); matchState.pendingLine=null; }
|
||||
if(correct){
|
||||
// draw permanent green line
|
||||
if(svgEl && container) _drawMatchLine(svgEl, container, matchState.selL, matchState.selR, 'correct-line');
|
||||
// mark buttons
|
||||
matchState.selL.style.background='var(--ok)';matchState.selL.style.color='#fff';
|
||||
matchState.selL.style.borderColor='var(--ok)';
|
||||
matchState.selR.style.background='var(--ok)';matchState.selR.style.color='#fff';
|
||||
matchState.selR.style.borderColor='var(--ok)';
|
||||
// add checkmark
|
||||
matchState.selL.innerHTML = '✓ ' + e;
|
||||
matchState.selR.innerHTML = '✓ ' + a;
|
||||
matchState.selL.disabled = true;
|
||||
matchState.selR.disabled = true;
|
||||
matchState.selL.style.background='var(--ok)';matchState.selL.style.color='#fff';
|
||||
matchState.selR.style.background='var(--ok)';matchState.selR.style.color='#fff';
|
||||
matchState.done.push(e);
|
||||
document.getElementById('match-cnt').textContent = matchState.done.length;
|
||||
feedback(fb, true, e + ' = ' + a + ' ✓');
|
||||
feedback(fb, true, e + ' = ' + a + ' ✓');
|
||||
if(matchState.done.length === MATCH_PAIRS.length){
|
||||
feedback(fb, true, 'Все 5 соединены! Свойства корня — в кармане.');
|
||||
achievement('match','Match выражений');
|
||||
bumpProgress('p3', 15);
|
||||
confetti();
|
||||
setTimeout(()=>{
|
||||
feedback(fb, true, 'Все 5 соединены! Свойства корня — в кармане.');
|
||||
achievement('match','Match выражений');
|
||||
bumpProgress('p3', 15);
|
||||
confetti();
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
// draw red flashing line then remove
|
||||
if(svgEl && container){
|
||||
const wrongLine = _drawMatchLine(svgEl, container, matchState.selL, matchState.selR, 'wrong-line');
|
||||
setTimeout(()=>wrongLine.remove(), 900);
|
||||
}
|
||||
matchState.selL.style.background='var(--fail)';matchState.selL.style.color='#fff';
|
||||
matchState.selR.style.background='var(--fail)';matchState.selR.style.color='#fff';
|
||||
feedback(fb, false, 'Не совпадает. Попробуйте другую пару.');
|
||||
setTimeout(()=>{
|
||||
if(matchState.selL && !matchState.selL.disabled){ matchState.selL.style.background=''; matchState.selL.style.color=''; }
|
||||
if(matchState.selR && !matchState.selR.disabled){ matchState.selR.style.background=''; matchState.selR.style.color=''; }
|
||||
}, 700);
|
||||
}, 750);
|
||||
}
|
||||
matchState.selL = null;
|
||||
matchState.selR = null;
|
||||
@@ -2165,11 +2516,18 @@ const SIMP_TASKS = [
|
||||
{q:'√(2.25)', a:1.5}, {q:'√(0.16·9)', a:1.2}, {q:'√(196)', a:14},
|
||||
];
|
||||
let simpIdx = 0;
|
||||
function initSimpTrainer(){ simpIdx = 0; simpRender(); }
|
||||
function initSimpTrainer(){
|
||||
simpIdx = 0; simpRender();
|
||||
const inp = document.getElementById('simp-ans');
|
||||
if(inp) liveCheck(inp, v=>{ const n=parseFloat(v.replace(',','.')); if(isNaN(n)) return null; return Math.abs(n - SIMP_TASKS[simpIdx].a) < 0.02; });
|
||||
}
|
||||
function simpRender(){
|
||||
const t = SIMP_TASKS[simpIdx];
|
||||
document.getElementById('simp-q').textContent = t.q;
|
||||
document.getElementById('simp-ans').value = '';
|
||||
// reset live-ind
|
||||
const ind = document.querySelector('#simp-ans + .live-ind');
|
||||
if(ind){ ind.className='live-ind'; ind.innerHTML=''; }
|
||||
document.getElementById('simp-fb').className='feedback';
|
||||
}
|
||||
function simpCheck(){
|
||||
@@ -2224,21 +2582,26 @@ function buildP4(){
|
||||
</ul>
|
||||
`)}
|
||||
|
||||
${widget('Drag «упрости √»', 'GAME', 'Перетащите подходящий множитель за пределы корня. Подсказка: ищите точный квадрат среди множителей.', `
|
||||
${widget('Drag «упрости √»', 'GAME', 'Перетащите подходящий множитель в зону слева от корня. На мобиле — просто нажмите нужную карточку.', `
|
||||
<div id="drag-task" style="padding:14px;background:var(--card);border-radius:9px;text-align:center;margin-bottom:12px">
|
||||
<div class="lab">Упростите:</div>
|
||||
<div id="drag-q" style="font-size:1.8rem;font-weight:800;color:var(--pri2);margin:10px 0;font-family:'JetBrains Mono',monospace">√72</div>
|
||||
<div class="row-c">
|
||||
<span class="lab">Выберите множитель:</span>
|
||||
<div id="drag-mults" style="display:flex;gap:6px;flex-wrap:wrap"></div>
|
||||
<div id="drag-q" style="font-size:2rem;font-weight:800;color:var(--pri2);margin:10px 0;font-family:'JetBrains Mono',monospace">√72</div>
|
||||
<div style="display:flex;gap:12px;align-items:center;justify-content:center;margin:14px 0;flex-wrap:wrap">
|
||||
<div id="drag-dropzone" class="dnd-dropzone" style="min-width:80px;min-height:54px">
|
||||
<span style="font-size:1.2rem;font-family:'JetBrains Mono',monospace;color:var(--muted)">?</span>
|
||||
</div>
|
||||
<span style="font-size:1.6rem;color:var(--muted);font-family:'JetBrains Mono',monospace">× √</span>
|
||||
<span id="drag-rest-num" style="font-size:1.6rem;font-weight:700;color:var(--acc2);font-family:'JetBrains Mono',monospace">?</span>
|
||||
</div>
|
||||
<div style="margin-top:14px;font-size:1.2rem;font-family:'JetBrains Mono',monospace">
|
||||
√<span id="drag-out-a" style="color:var(--ok)">36</span> · √<span id="drag-out-b" style="color:var(--acc2)">2</span> = <span id="drag-out-num" style="color:var(--ok)">6</span>√<span id="drag-out-rad" style="color:var(--acc2)">2</span>
|
||||
</div>
|
||||
<div class="row-c" style="margin-top:14px">
|
||||
<div class="lab" style="margin-bottom:8px">Карточки множителей:</div>
|
||||
<div id="drag-mults" style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center;min-height:48px"></div>
|
||||
<div id="drag-result" style="margin-top:14px;font-size:1.15rem;font-family:'JetBrains Mono',monospace;min-height:28px"></div>
|
||||
<div class="row-c" style="margin-top:12px">
|
||||
<button class="btn primary" onclick="dragNext()">Следующая</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- floating drag ghost -->
|
||||
<div id="dnd-ghost" style="position:fixed;pointer-events:none;z-index:8000;display:none;transform:translate(-50%,-50%)"></div>
|
||||
<div id="drag-fb" class="feedback"></div>
|
||||
`)}
|
||||
|
||||
@@ -2362,59 +2725,133 @@ function buildP4(){
|
||||
setTimeout(()=>{ initDragSimp(); initConverter(); initFracIrr(); initCompare(); initSimp4(); }, 50);
|
||||
}
|
||||
|
||||
/* ──── Drag simplify (visual) ──── */
|
||||
/* ──── Drag simplify — pointer-based DnD ──── */
|
||||
const DRAG_TASKS = [
|
||||
{n:72, sq:36, rest:2}, {n:50, sq:25, rest:2}, {n:48, sq:16, rest:3},
|
||||
{n:200, sq:100, rest:2}, {n:75, sq:25, rest:3}, {n:18, sq:9, rest:2},
|
||||
{n:32, sq:16, rest:2}, {n:98, sq:49, rest:2}, {n:128, sq:64, rest:2},
|
||||
];
|
||||
let dragIdx = 0;
|
||||
let _dndState = null; // active drag
|
||||
|
||||
function initDragSimp(){
|
||||
dragIdx = 0;
|
||||
dragRender();
|
||||
// global pointer handlers for ghost
|
||||
window.addEventListener('pointermove', _dndMove);
|
||||
window.addEventListener('pointerup', _dndDrop);
|
||||
}
|
||||
|
||||
function dragRender(){
|
||||
const t = DRAG_TASKS[dragIdx];
|
||||
document.getElementById('drag-q').textContent = '√' + t.n;
|
||||
// generate multipliers — true sq + some fakes
|
||||
const qEl = document.getElementById('drag-q');
|
||||
if(qEl) qEl.textContent = '√' + t.n;
|
||||
// reset dropzone
|
||||
const dz = document.getElementById('drag-dropzone');
|
||||
if(dz) dz.innerHTML = '<span style="font-size:1.2rem;font-family:\'JetBrains Mono\',monospace;color:var(--muted)">?</span>';
|
||||
const restEl = document.getElementById('drag-rest-num');
|
||||
if(restEl) restEl.textContent = '?';
|
||||
const resEl = document.getElementById('drag-result');
|
||||
if(resEl) resEl.textContent = '';
|
||||
// generate multipliers
|
||||
const mults = new Set([t.sq]);
|
||||
while(mults.size < 5){
|
||||
const m = [4, 9, 16, 25, 36, 49, 64, 81][Math.floor(Math.random()*8)];
|
||||
const m = [4,9,16,25,36,49,64,81][Math.floor(Math.random()*8)];
|
||||
if(t.n % m === 0) mults.add(m);
|
||||
else if(mults.size < 3) mults.add(m);
|
||||
}
|
||||
const arr = [...mults].sort((a,b)=>a-b);
|
||||
const g = document.getElementById('drag-mults');
|
||||
if(!g) return;
|
||||
g.innerHTML = '';
|
||||
arr.forEach(m=>{
|
||||
const b = el('button', {class:'btn'}, m);
|
||||
b.addEventListener('click', ()=>dragPick(m, t, b));
|
||||
g.appendChild(b);
|
||||
const card = document.createElement('div');
|
||||
card.className = 'dnd-card';
|
||||
card.textContent = String(m);
|
||||
card.dataset.val = m;
|
||||
// pointer down starts drag
|
||||
card.addEventListener('pointerdown', e=>{
|
||||
e.preventDefault();
|
||||
_dndStart(e, card, m, t);
|
||||
});
|
||||
// click fallback for mobile/accessibility
|
||||
card.addEventListener('click', ()=>{
|
||||
if(!_dndState) dragPick(m, t, card);
|
||||
});
|
||||
g.appendChild(card);
|
||||
});
|
||||
// reset visual
|
||||
document.getElementById('drag-out-a').textContent = '?';
|
||||
document.getElementById('drag-out-b').textContent = '?';
|
||||
document.getElementById('drag-out-num').textContent = '?';
|
||||
document.getElementById('drag-out-rad').textContent = '?';
|
||||
document.getElementById('drag-fb').className = 'feedback';
|
||||
}
|
||||
function dragPick(m, t, btn){
|
||||
|
||||
function _dndStart(e, card, val, task){
|
||||
const ghost = document.getElementById('dnd-ghost');
|
||||
if(!ghost) return;
|
||||
_dndState = { card, val, task };
|
||||
card.classList.add('dragging-active');
|
||||
ghost.textContent = val;
|
||||
ghost.className = 'dnd-card';
|
||||
ghost.style.display = 'flex';
|
||||
ghost.style.left = e.clientX + 'px';
|
||||
ghost.style.top = e.clientY + 'px';
|
||||
card.setPointerCapture(e.pointerId);
|
||||
}
|
||||
|
||||
function _dndMove(e){
|
||||
if(!_dndState) return;
|
||||
const ghost = document.getElementById('dnd-ghost');
|
||||
if(ghost){ ghost.style.left = e.clientX + 'px'; ghost.style.top = e.clientY + 'px'; }
|
||||
const dz = document.getElementById('drag-dropzone');
|
||||
if(dz){
|
||||
const r = dz.getBoundingClientRect();
|
||||
const over = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
|
||||
dz.classList.toggle('over', over);
|
||||
}
|
||||
}
|
||||
|
||||
function _dndDrop(e){
|
||||
if(!_dndState) return;
|
||||
const ghost = document.getElementById('dnd-ghost');
|
||||
if(ghost){ ghost.style.display = 'none'; }
|
||||
_dndState.card.classList.remove('dragging-active');
|
||||
const dz = document.getElementById('drag-dropzone');
|
||||
let droppedOnZone = false;
|
||||
if(dz){
|
||||
const r = dz.getBoundingClientRect();
|
||||
droppedOnZone = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
|
||||
dz.classList.remove('over');
|
||||
}
|
||||
if(droppedOnZone){
|
||||
dragPick(_dndState.val, _dndState.task, _dndState.card);
|
||||
}
|
||||
_dndState = null;
|
||||
}
|
||||
|
||||
function dragPick(m, t, card){
|
||||
const fb = document.getElementById('drag-fb');
|
||||
if(t.n % m !== 0){ feedback(fb, false, m + ' не делит ' + t.n + ' нацело'); return; }
|
||||
const rest = t.n / m;
|
||||
if(m !== t.sq || rest !== t.rest){
|
||||
// valid but not optimal
|
||||
feedback(fb, false, '√' + t.n + ' = √(' + m + '·' + rest + ') = ' + Math.sqrt(m).toFixed(2) + '·√' + rest + '. Это не самый компактный вид.');
|
||||
if(t.n % m !== 0){
|
||||
if(card){ card.classList.add('wrong-dnd'); setTimeout(()=>card.classList.remove('wrong-dnd'),600); }
|
||||
feedback(fb, false, m + ' не делит ' + t.n + ' нацело');
|
||||
return;
|
||||
}
|
||||
document.getElementById('drag-out-a').textContent = m;
|
||||
document.getElementById('drag-out-b').textContent = rest;
|
||||
document.getElementById('drag-out-num').textContent = Math.sqrt(m);
|
||||
document.getElementById('drag-out-rad').textContent = rest;
|
||||
btn.style.background='var(--ok)'; btn.style.color='#fff';
|
||||
feedback(fb, true, '✓ √' + t.n + ' = ' + Math.sqrt(m) + '√' + rest);
|
||||
const rest = t.n / m;
|
||||
if(m !== t.sq || rest !== t.rest){
|
||||
if(card){ card.classList.add('wrong-dnd'); setTimeout(()=>card.classList.remove('wrong-dnd'),600); }
|
||||
feedback(fb, false, '√'+t.n+' = √('+m+'·'+rest+') = '+Math.sqrt(m).toFixed(2)+'·√'+rest+'. Не самый компактный вид.');
|
||||
return;
|
||||
}
|
||||
// correct!
|
||||
const dz = document.getElementById('drag-dropzone');
|
||||
if(dz){ dz.innerHTML = '<span style="font-family:\'JetBrains Mono\',monospace;font-weight:900;color:var(--ok);font-size:1.2rem">'+Math.sqrt(m)+'</span>'; }
|
||||
const restEl = document.getElementById('drag-rest-num');
|
||||
if(restEl) restEl.textContent = rest;
|
||||
const resEl = document.getElementById('drag-result');
|
||||
if(resEl) resEl.innerHTML = '<span style="color:var(--ok);font-weight:700">✓ √'+t.n+' = '+Math.sqrt(m)+'√'+rest+'</span>';
|
||||
if(card){ card.classList.add('correct-dnd'); }
|
||||
feedback(fb, true, '✓ √'+t.n+' = '+Math.sqrt(m)+'√'+rest);
|
||||
confetti();
|
||||
bumpProgress('p4', 3);
|
||||
}
|
||||
|
||||
function dragNext(){
|
||||
dragIdx = (dragIdx + 1) % DRAG_TASKS.length;
|
||||
dragRender();
|
||||
@@ -2532,6 +2969,10 @@ let simp4State = { idx:0, score:0, cnt:0 };
|
||||
function initSimp4(){
|
||||
simp4State = { idx:0, score:0, cnt:0 };
|
||||
simp4Render();
|
||||
const ia = document.getElementById('simp4-a');
|
||||
const ib = document.getElementById('simp4-b');
|
||||
if(ia) liveCheck(ia, v=>{ const n=+v; if(isNaN(n)||!v) return null; const t=SIMP4_TASKS[simp4State.idx]; return n===t.a ? null : false; });
|
||||
if(ib) liveCheck(ib, v=>{ const n=+v; if(isNaN(n)||!v) return null; const t=SIMP4_TASKS[simp4State.idx]; const a=+(document.getElementById('simp4-a').value); return (a===t.a && n===t.b) ? true : (n===t.b ? null : false); });
|
||||
}
|
||||
function simp4Render(){
|
||||
const t = SIMP4_TASKS[simp4State.idx];
|
||||
@@ -3527,7 +3968,20 @@ function buildFinal(){
|
||||
${secNav('p6', null)}
|
||||
`;
|
||||
renderMath(body);
|
||||
setTimeout(()=>{ buildAssessment(); renderMath(body); }, 50);
|
||||
setTimeout(()=>{
|
||||
buildAssessment(); renderMath(body);
|
||||
// liveCheck for practical tasks
|
||||
const pr1 = document.getElementById('pr1-a');
|
||||
if(pr1) liveCheck(pr1, v=>{ const n=+v; return isNaN(n)?null:n===26?true:false; });
|
||||
const pr2 = document.getElementById('pr2-a');
|
||||
if(pr2) liveCheck(pr2, v=>{ const n=+v; return isNaN(n)?null:n===81?true:false; });
|
||||
// dec inputs
|
||||
const decExpected = [5,18,21,8,2,1];
|
||||
['dec-1','dec-2','dec-3','dec-4','dec-5','dec-6'].forEach((id,i)=>{
|
||||
const inp = document.getElementById(id);
|
||||
if(inp) liveCheck(inp, v=>{ const n=+v; return isNaN(n)||!v?null:n===decExpected[i]?true:false; });
|
||||
});
|
||||
}, 50);
|
||||
// Повторный рендер с задержкой — на случай если KaTeX ещё не успел подгрузиться
|
||||
setTimeout(()=>renderMath(body), 300);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user