64bd44088d
- backend: POST /api/gamification/self-award (rate-limited, validated) - frontend/js/xp.js: load/add/flush/on клиент, ~150 LOC, дебаунс 300мс, keepalive fetch на unload/visibilitychange hidden - algebra_8.html и algebra_8_ch2.html: XP_LEVELS заменён на единую формулу с сервером; addXp/loadProgress подключены к window.LS.xp - При первой загрузке: merge max(local, server); далее сервер — источник правды
6854 lines
368 KiB
HTML
6854 lines
368 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||
<meta http-equiv="Pragma" content="no-cache">
|
||
<meta http-equiv="Expires" content="0">
|
||
<title>Алгебра 8 — Глава 1 · Квадратные корни и действительные числа</title>
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
|
||
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Manrope:wght@400;500;600;700;800&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
|
||
<script src="/js/api.js" defer></script>
|
||
<script src="/js/xp.js" defer></script>
|
||
<style>
|
||
:root{
|
||
--pri:#e91e63; --pri2:#c2185b; --pri-soft:#fce7f3;
|
||
--acc:#03a9f4; --acc2:#0288d1; --acc-soft:#e0f4ff;
|
||
--ok:#10b981; --ok-bg:#d1fae5;
|
||
--fail:#ef4444; --fail-bg:#fee2e2;
|
||
--warn:#f59e0b; --warn-bg:#fef3c7;
|
||
--bg:#fdf2f8; --card:#fff;
|
||
--text:#1a1a2e; --muted:#6b5b73;
|
||
--border:#fce7f3;
|
||
--sh:0 2px 12px rgba(233,30,99,.07);
|
||
--sh2:0 6px 24px rgba(233,30,99,.10);
|
||
}
|
||
.dark{
|
||
--bg:#1a0f1a; --card:#2a1929;
|
||
--text:#f5e6f0; --muted:#b0a0b0;
|
||
--border:#5a2a5a; --pri-soft:#4a1a3a;
|
||
--acc-soft:#1a3a4a;
|
||
--sh:0 2px 12px rgba(0,0,0,.4);
|
||
--sh2:0 6px 24px rgba(0,0,0,.5);
|
||
}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
html,body{height:100%;}
|
||
body{font-family:'Inter','Manrope',system-ui,sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column;min-height:100vh;line-height:1.5;transition:background .25s,color .25s}
|
||
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
|
||
input,select,textarea{font-family:inherit}
|
||
.ic{width:1em;height:1em;display:inline-block;vertical-align:-.125em;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||
|
||
/* HEADER */
|
||
.hdr{position:relative;background:linear-gradient(110deg,#c2185b 0%,#e91e63 55%,#ec407a 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(255,180,210,.2);min-height:130px}
|
||
.hdr::before{content:'АЛГЕБРА';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,220,235,.08);line-height:1;pointer-events:none;user-select:none;z-index:0}
|
||
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||
.hdr h1{font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
|
||
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
|
||
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center}
|
||
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s}
|
||
.hdr-btn:hover{background:rgba(255,255,255,.24)}
|
||
.hdr-search{padding:7px 11px 7px 32px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-size:.82rem;width:180px;outline:none;border:none;background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg>');background-repeat:no-repeat;background-position:9px center;background-size:14px}
|
||
.hdr-search::placeholder{color:rgba(255,255,255,.7)}
|
||
.hdr-search:focus{background-color:rgba(255,255,255,.22)}
|
||
|
||
/* MAIN */
|
||
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
|
||
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
|
||
|
||
.col-main{min-width:0}
|
||
|
||
/* HERO */
|
||
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 100%);border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
|
||
.hero::before{content:'√';position:absolute;right:-24px;top:-40px;font-size:14rem;font-weight:900;color:var(--pri);opacity:.08;font-family:'Inter',serif;line-height:1;pointer-events:none}
|
||
.hero h2{font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
|
||
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
|
||
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
|
||
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(233,30,99,.22);font-family:'Unbounded',sans-serif}
|
||
.hero-xp-badge svg{flex-shrink:0}
|
||
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
|
||
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(233,30,99,.28)}
|
||
.btn-secondary{padding:10px 18px;background:var(--card);color:var(--pri2);border:1.5px solid var(--pri);border-radius:11px;font-weight:700;font-size:.88rem;transition:background .15s}
|
||
.btn-secondary:hover{background:var(--pri-soft)}
|
||
.hero-progress{flex:1;min-width:200px;max-width:280px}
|
||
.hp-label{font-size:.7rem;font-weight:700;color:var(--pri2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px;display:block}
|
||
.hp-bar{height:8px;background:rgba(233,30,99,.12);border-radius:6px;overflow:hidden}
|
||
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:6px;width:0%;transition:width .4s}
|
||
.hp-text{font-size:.72rem;color:var(--muted);margin-top:3px;display:block}
|
||
|
||
/* PARA SELECTOR */
|
||
.psel{margin-bottom:24px}
|
||
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
|
||
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}
|
||
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative;overflow:hidden}
|
||
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
|
||
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
|
||
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc))}
|
||
.psel-num{font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
|
||
.psel-name{font-size:.88rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
|
||
.psel-prog{height:4px;background:rgba(233,30,99,.10);border-radius:3px;overflow:hidden}
|
||
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
|
||
.psel-card.final{background:linear-gradient(135deg,#fff5e1,#fce7f3)}
|
||
.dark .psel-card.final{background:linear-gradient(135deg,#3a2818,#4a1a3a)}
|
||
.psel-card.final .psel-num{color:var(--warn)}
|
||
|
||
/* CONTENT SECTIONS */
|
||
.sec{display:none;animation:fadeIn .35s ease}
|
||
.sec.active{display:block}
|
||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||
|
||
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--pri-soft)}
|
||
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:7px;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
|
||
.sec-h{font-size:1.7rem;font-weight:800;color:var(--pri2);letter-spacing:-.01em;line-height:1.25}
|
||
|
||
/* CARDS (типы блоков из учебника) */
|
||
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:var(--sh);position:relative;transition:transform .15s,box-shadow .15s}
|
||
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
|
||
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
|
||
.card-icon.repeat{background:#0ea5e9}
|
||
.card-icon.theory{background:#8b5cf6}
|
||
.card-icon.algo{background:#f59e0b}
|
||
.card-icon.rule{background:#ec4899}
|
||
.card-icon.example{background:#10b981}
|
||
.card-icon.oral{background:#06b6d4}
|
||
.card-icon.class{background:#3b82f6}
|
||
.card-icon.home{background:#f97316}
|
||
.card-icon.prev{background:#6366f1}
|
||
.card-icon .ic{width:18px;height:18px}
|
||
.card-title{font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
|
||
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--pri-soft);padding:3px 7px;border-radius:5px}
|
||
.card-body{font-size:.95rem;line-height:1.65}
|
||
.card-body p{margin-bottom:10px}
|
||
.card-body p:last-child{margin-bottom:0}
|
||
.card-body b{color:var(--pri2);font-weight:700}
|
||
.card-body ul{padding-left:22px;margin:8px 0}
|
||
.card-body li{margin-bottom:4px}
|
||
|
||
.formula-box{background:var(--pri-soft);border-left:4px solid var(--pri);border-radius:8px;padding:12px 16px;margin:10px 0;font-size:1.05rem}
|
||
.dark .formula-box{background:rgba(233,30,99,.12)}
|
||
|
||
.def-box{background:linear-gradient(135deg,var(--acc-soft),var(--card));border:1.5px solid var(--acc);border-radius:11px;padding:14px 16px;margin:12px 0}
|
||
.def-box-title{font-size:.78rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
|
||
|
||
.note-warn{background:var(--warn-bg);border-left:4px solid var(--warn);border-radius:7px;padding:10px 14px;font-size:.9rem;margin:10px 0}
|
||
.dark .note-warn{background:rgba(245,158,11,.15);color:#fef3c7}
|
||
|
||
/* INTERACTIVE WIDGETS */
|
||
.wg{background:linear-gradient(135deg,var(--card),var(--pri-soft));border:1.5px solid var(--pri);border-radius:14px;padding:18px 20px;margin-bottom:18px;box-shadow:var(--sh2);position:relative}
|
||
.wg-header{display:flex;align-items:center;gap:8px;margin-bottom:14px}
|
||
.wg-badge{padding:4px 9px;background:var(--pri);color:#fff;border-radius:6px;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
|
||
.wg-title{font-size:1.05rem;font-weight:800;color:var(--pri2);flex:1}
|
||
.wg-help{font-size:.76rem;color:var(--muted);font-style:italic;margin-bottom:10px}
|
||
|
||
.wg-canvas{width:100%;background:var(--card);border:1px dashed var(--border);border-radius:10px;display:block;cursor:grab}
|
||
.wg-canvas:active{cursor:grabbing}
|
||
|
||
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
|
||
.btn:hover{background:var(--pri-soft);border-color:var(--pri)}
|
||
.btn:active{transform:scale(.96)}
|
||
.btn.primary{background:var(--pri);color:#fff;border-color:var(--pri)}
|
||
.btn.primary:hover{background:var(--pri2);border-color:var(--pri2)}
|
||
.btn.acc{background:var(--acc);color:#fff;border-color:var(--acc)}
|
||
.btn.acc:hover{background:var(--acc2);border-color:var(--acc2)}
|
||
.btn.ok{background:var(--ok);color:#fff;border-color:var(--ok)}
|
||
.btn.small{padding:5px 11px;font-size:.78rem}
|
||
|
||
.inp{padding:7px 12px;border-radius:8px;background:var(--card);border:1.5px solid var(--border);font-size:.92rem;color:var(--text);font-family:'JetBrains Mono',monospace;width:auto;min-width:60px}
|
||
.inp:focus{outline:none;border-color:var(--pri);background:var(--pri-soft)}
|
||
.inp.wide{width:180px}
|
||
.inp.num{width:100px;text-align:center}
|
||
|
||
.slider{appearance:none;-webkit-appearance:none;height:6px;background:rgba(233,30,99,.18);border-radius:5px;width:100%;outline:none}
|
||
.slider::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;width:18px;height:18px;background:var(--pri);border-radius:50%;cursor:pointer;box-shadow:0 0 0 3px rgba(233,30,99,.25),0 2px 5px rgba(0,0,0,.18)}
|
||
.slider::-moz-range-thumb{width:18px;height:18px;background:var(--pri);border-radius:50%;cursor:pointer;border:none;box-shadow:0 0 0 3px rgba(233,30,99,.25),0 2px 5px rgba(0,0,0,.18)}
|
||
|
||
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
|
||
.row-c{display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap;margin-bottom:10px}
|
||
.col{display:flex;flex-direction:column;gap:8px}
|
||
.lab{font-size:.85rem;font-weight:600;color:var(--text);margin-right:4px}
|
||
.lab-mono{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--pri2)}
|
||
|
||
.chip{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;background:var(--pri-soft);border-radius:7px;font-size:.84rem;font-weight:600;color:var(--pri2)}
|
||
.chip.acc{background:var(--acc-soft);color:var(--acc2)}
|
||
.chip.ok{background:var(--ok-bg);color:#065f46}
|
||
.chip.fail{background:var(--fail-bg);color:#7f1d1d}
|
||
|
||
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
|
||
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
|
||
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
|
||
|
||
/* GAME / SCORE */
|
||
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--pri-soft);border-radius:10px;margin-bottom:12px}
|
||
.score-display b{color:var(--pri2);font-size:1.15rem}
|
||
|
||
/* DRAG-DROP */
|
||
.dz{min-height:60px;padding:12px;border:2px dashed var(--border);border-radius:10px;background:var(--card);display:flex;flex-wrap:wrap;gap:6px;align-content:flex-start;transition:border-color .15s,background .15s}
|
||
.dz.over{border-color:var(--pri);background:var(--pri-soft)}
|
||
.dz-label{font-size:.78rem;font-weight:700;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:.06em}
|
||
.drag-item{padding:6px 12px;background:linear-gradient(135deg,var(--acc),var(--acc2));color:#fff;border-radius:8px;font-weight:600;font-size:.88rem;cursor:grab;user-select:none;font-family:'JetBrains Mono',monospace;box-shadow:var(--sh);transition:transform .12s,box-shadow .12s}
|
||
.drag-item:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(3,169,244,.35)}
|
||
.drag-item:active{cursor:grabbing}
|
||
.drag-item.dragging{opacity:.5}
|
||
.drag-item.selected{outline:3px solid #FFD166;outline-offset:2px;box-shadow:0 0 0 6px rgba(255,209,102,.22),var(--sh)}
|
||
|
||
/* NUMBER LINE */
|
||
.num-line{position:relative;height:60px;background:var(--card);border-radius:8px;margin:14px 0;border:1px solid var(--border)}
|
||
.num-line-axis{position:absolute;top:30px;left:5%;right:5%;height:2px;background:var(--text)}
|
||
.num-line-axis::after{content:'';position:absolute;right:-8px;top:-6px;width:0;height:0;border-left:10px solid var(--text);border-top:6px solid transparent;border-bottom:6px solid transparent}
|
||
.num-tick{position:absolute;top:24px;width:2px;height:14px;background:var(--text)}
|
||
.num-tick-lab{position:absolute;top:42px;transform:translateX(-50%);font-size:.74rem;font-weight:600;color:var(--muted);font-family:'JetBrains Mono',monospace}
|
||
.num-point{position:absolute;top:22px;width:14px;height:14px;background:var(--pri);border-radius:50%;transform:translateX(-50%);border:2.5px solid var(--card);box-shadow:0 0 0 2px var(--pri);cursor:pointer;z-index:2}
|
||
.num-point-lab{position:absolute;top:0;transform:translateX(-50%);font-size:.78rem;font-weight:700;color:var(--pri);background:var(--card);padding:2px 6px;border-radius:5px;border:1px solid var(--pri);font-family:'JetBrains Mono',monospace}
|
||
.num-interval{position:absolute;top:28px;height:6px;background:linear-gradient(90deg,rgba(233,30,99,.25),rgba(233,30,99,.4));border-radius:3px}
|
||
|
||
/* SIDEBAR (Шпаргалка) */
|
||
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
|
||
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
|
||
.sidecard h4{font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
|
||
.sidecard-row b{color:var(--pri);font-weight:700}
|
||
@media(max-width:980px){.col-side{position:static;max-height:none}}
|
||
|
||
/* ACHIEVEMENT POPUP */
|
||
.ach-popup{position:fixed;top:18px;right:18px;background:linear-gradient(135deg,#fcd34d,#f59e0b);color:#451a03;padding:13px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 32px rgba(245,158,11,.4);z-index:1000;display:none;align-items:center;gap:10px;animation:slideIn .35s ease}
|
||
.ach-popup.show{display:flex}
|
||
@keyframes slideIn{from{transform:translateX(110%);opacity:0}to{transform:none;opacity:1}}
|
||
|
||
/* RADICAL SYMBOL */
|
||
.radical{font-family:'Inter',serif;font-size:1.1em;font-weight:600}
|
||
|
||
/* QUIZ */
|
||
.quiz{background:var(--card);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:12px}
|
||
.quiz-q{font-weight:600;margin-bottom:10px;font-size:.94rem}
|
||
.quiz-q .qn{display:inline-block;width:24px;height:24px;background:var(--pri);color:#fff;border-radius:50%;text-align:center;line-height:24px;font-size:.78rem;font-weight:800;margin-right:8px}
|
||
.quiz-opts{display:flex;flex-wrap:wrap;gap:6px}
|
||
.quiz-opt{padding:6px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:.86rem;font-weight:600;cursor:pointer;background:var(--card);transition:background .12s,border-color .12s}
|
||
.quiz-opt:hover{background:var(--pri-soft);border-color:var(--pri)}
|
||
.quiz-opt.correct{background:var(--ok-bg);border-color:var(--ok);color:#065f46}
|
||
.quiz-opt.wrong{background:var(--fail-bg);border-color:var(--fail);color:#7f1d1d}
|
||
|
||
/* TABLE */
|
||
.tbl{width:100%;border-collapse:collapse;margin:12px 0;font-size:.88rem}
|
||
.tbl th,.tbl td{padding:7px 10px;border:1px solid var(--border);text-align:center;font-family:'JetBrains Mono',monospace}
|
||
.tbl th{background:var(--pri-soft);color:var(--pri2);font-weight:700}
|
||
.tbl .cell-hl{background:var(--ok-bg);font-weight:700;color:#065f46;cursor:pointer}
|
||
.tbl .cell-hl:hover{background:#a7f3d0}
|
||
|
||
/* SQUARES TABLE GAME */
|
||
.squares-target{font-size:2.4rem;font-weight:900;color:var(--pri2);text-align:center;margin:14px 0;font-family:'JetBrains Mono',monospace;letter-spacing:.05em}
|
||
.squares-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;max-width:280px;margin:0 auto}
|
||
.squares-grid .btn{font-size:1.1rem;padding:14px 0;font-weight:800;font-family:'JetBrains Mono',monospace}
|
||
|
||
/* SPOILER */
|
||
.spoiler{margin:10px 0;border:1px dashed var(--pri);border-radius:8px;overflow:hidden}
|
||
.spoiler summary{padding:8px 14px;background:var(--pri-soft);font-weight:700;cursor:pointer;font-size:.88rem;color:var(--pri2);list-style:none;display:flex;align-items:center;gap:8px}
|
||
.spoiler summary::-webkit-details-marker{display:none}
|
||
.spoiler summary::before{content:'+';font-size:1.2rem;font-weight:900;color:var(--pri);width:18px;display:inline-block}
|
||
.spoiler[open] summary::before{content:'−'}
|
||
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
|
||
|
||
/* FOOTER */
|
||
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
|
||
.foot a{color:var(--pri)}
|
||
|
||
/* NAV BUTTONS */
|
||
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
|
||
|
||
/* GRID Vis */
|
||
.grid-vis{display:grid;gap:1px;background:var(--border);border:1px solid var(--border);width:fit-content;margin:10px 0}
|
||
.grid-vis .cell{width:18px;height:18px;background:var(--card)}
|
||
.grid-vis .cell.f{background:var(--acc)}
|
||
|
||
/* CIRCLES (Sets) */
|
||
.sets-vis{position:relative;height:200px;margin:14px auto;max-width:380px}
|
||
.set-circle{position:absolute;border-radius:50%;border:2px solid;display:flex;align-items:flex-start;justify-content:center;font-weight:800;font-size:.88rem;padding-top:6px;transition:transform .25s,box-shadow .25s;cursor:pointer}
|
||
.set-circle:hover{transform:scale(1.04);z-index:5}
|
||
.set-N{inset:60px 130px 60px 130px;background:rgba(16,185,129,.18);border-color:#10b981;color:#065f46}
|
||
.set-Z{inset:36px 90px 36px 90px;background:rgba(59,130,246,.13);border-color:#3b82f6;color:#1e40af}
|
||
.set-Q{inset:18px 50px 18px 50px;background:rgba(245,158,11,.10);border-color:#f59e0b;color:#92400e}
|
||
.set-R{inset:0;background:rgba(233,30,99,.08);border-color:var(--pri);color:var(--pri2)}
|
||
.set-info{position:absolute;top:50%;left:0;right:0;transform:translateY(-50%);background:var(--card);border:1px solid var(--border);border-radius:8px;padding:10px 14px;font-size:.84rem;font-weight:500;display:none;z-index:10;box-shadow:var(--sh2)}
|
||
.set-info.show{display:block}
|
||
|
||
/* ═══════════════════════════════════════════════
|
||
WAVE 1 — VISUAL POLISH
|
||
═══════════════════════════════════════════════ */
|
||
|
||
/* 1. Per-section accent colour variables */
|
||
.sec[id="sec-p1"]{--sec-acc:#e91e63;--sec-acc-d:#c2185b;--sec-acc-soft:#fce7f3}
|
||
.sec[id="sec-p2"]{--sec-acc:#9333ea;--sec-acc-d:#7c3aed;--sec-acc-soft:#ede9fe}
|
||
.sec[id="sec-p3"]{--sec-acc:#0891b2;--sec-acc-d:#0e7490;--sec-acc-soft:#cffafe}
|
||
.sec[id="sec-p4"]{--sec-acc:#ea580c;--sec-acc-d:#c2410c;--sec-acc-soft:#fed7aa}
|
||
.sec[id="sec-p5"]{--sec-acc:#16a34a;--sec-acc-d:#15803d;--sec-acc-soft:#dcfce7}
|
||
.sec[id="sec-p6"]{--sec-acc:#4f46e5;--sec-acc-d:#4338ca;--sec-acc-soft:#e0e7ff}
|
||
.sec[id="sec-final"]{--sec-acc:#d97706;--sec-acc-d:#b45309;--sec-acc-soft:#fef3c7}
|
||
|
||
/* sec-num badge picks up section accent */
|
||
.sec .sec-num{background:linear-gradient(135deg,var(--sec-acc),var(--sec-acc-d))}
|
||
/* sec-h title */
|
||
.sec .sec-h{color:var(--sec-acc-d)}
|
||
/* wg border + header badge */
|
||
.sec .wg{border-color:var(--sec-acc)}
|
||
.sec .wg-badge{background:var(--sec-acc)}
|
||
.sec .wg-title{color:var(--sec-acc-d)}
|
||
/* primary btn inside sections */
|
||
.sec .btn.primary{background:var(--sec-acc);border-color:var(--sec-acc)}
|
||
.sec .btn.primary:hover{background:var(--sec-acc-d);border-color:var(--sec-acc-d)}
|
||
/* psel-card active stripe in section accent — controlled via JS data-secacc */
|
||
.psel-card[data-secacc]::after{background:var(--psel-stripe,linear-gradient(90deg,var(--pri),var(--acc)))}
|
||
|
||
/* 2. Unbounded for key headings */
|
||
.hdr h1{font-family:'Unbounded','Inter',sans-serif}
|
||
.sec-h{font-family:'Unbounded','Inter',sans-serif;font-size:1.45rem;letter-spacing:-.02em}
|
||
.hero h2{font-family:'Unbounded','Inter',sans-serif}
|
||
.card-title{font-family:'Unbounded','Inter',sans-serif;font-size:.74rem}
|
||
.ach-popup{font-family:'Unbounded','Inter',sans-serif}
|
||
|
||
/* 3. Watermark pseudo-elements */
|
||
.sec{position:relative;overflow:visible}
|
||
.sec::before{
|
||
content:attr(data-watermark);
|
||
position:absolute;
|
||
right:-20px;top:10%;
|
||
font-family:'Unbounded',sans-serif;
|
||
font-size:clamp(6rem,18vw,14rem);
|
||
font-weight:900;
|
||
color:transparent;
|
||
-webkit-text-stroke:1.5px var(--sec-acc-soft,rgba(233,30,99,.09));
|
||
line-height:1;
|
||
pointer-events:none;
|
||
user-select:none;
|
||
z-index:0;
|
||
opacity:.4;
|
||
}
|
||
|
||
/* 4. Enhanced card + wg shadows */
|
||
.card{
|
||
box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(233,30,99,.06);
|
||
transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s;
|
||
}
|
||
.card:hover{
|
||
transform:translateY(-2px);
|
||
box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(233,30,99,.12);
|
||
}
|
||
.wg{
|
||
box-shadow:0 2px 8px rgba(0,0,0,.05),0 10px 28px rgba(233,30,99,.10);
|
||
transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s;
|
||
}
|
||
.wg:hover{
|
||
transform:translateY(-1px);
|
||
box-shadow:0 4px 12px rgba(0,0,0,.08),0 16px 40px rgba(233,30,99,.18);
|
||
}
|
||
|
||
/* 5. Animated hero gradient */
|
||
.hero{
|
||
background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);
|
||
background-size:200% 200%;
|
||
animation:heroShift 12s ease-in-out infinite;
|
||
}
|
||
@keyframes heroShift{
|
||
0%,100%{background-position:0% 50%}
|
||
50%{background-position:100% 50%}
|
||
}
|
||
|
||
/* 8. Achievement popup — bounce entry */
|
||
.ach-popup{
|
||
border-radius:14px;
|
||
padding:16px 22px;
|
||
gap:12px;
|
||
}
|
||
.ach-popup.show{animation:achBounce .5s cubic-bezier(.34,1.56,.64,1) forwards}
|
||
@keyframes achBounce{
|
||
from{transform:translateX(110%) scale(.85);opacity:0}
|
||
to{transform:none;opacity:1}
|
||
}
|
||
.ach-icon-pulse{
|
||
display:inline-flex;align-items:center;justify-content:center;
|
||
width:36px;height:36px;
|
||
background:rgba(255,255,255,.3);
|
||
border-radius:50%;
|
||
flex-shrink:0;
|
||
animation:iconPulse 1.8s ease-in-out infinite;
|
||
}
|
||
@keyframes iconPulse{
|
||
0%,100%{box-shadow:0 0 0 0 rgba(255,255,255,.5)}
|
||
50%{box-shadow:0 0 0 8px rgba(255,255,255,0)}
|
||
}
|
||
|
||
/* 7. Sparkle elements */
|
||
.sparkle-dot{
|
||
position:absolute;
|
||
width:8px;height:8px;
|
||
border-radius:50%;
|
||
pointer-events:none;
|
||
z-index:9998;
|
||
animation:sparkleOut .6s ease-out forwards;
|
||
}
|
||
@keyframes sparkleOut{
|
||
0%{opacity:1;transform:translate(0,0) scale(1)}
|
||
100%{opacity:0;transform:translate(var(--sx,20px),var(--sy,-30px)) scale(0)}
|
||
}
|
||
|
||
/* 9. Card-icon section-accent border */
|
||
.sec .card-icon{
|
||
outline:2px solid var(--sec-acc-soft,transparent);
|
||
outline-offset:1px;
|
||
}
|
||
|
||
/* 10. Mobile polish */
|
||
@media(max-width:768px){
|
||
.main{grid-template-columns:1fr;padding:12px;gap:16px}
|
||
.col-side{display:none}
|
||
.col-side.side-open{display:block;position:fixed;top:0;right:0;bottom:0;width:min(320px,92vw);z-index:500;overflow-y:auto;background:var(--bg);padding:16px;box-shadow:-4px 0 24px rgba(0,0,0,.18)}
|
||
.side-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:499}
|
||
.side-overlay.show{display:block}
|
||
.card{padding:12px 14px}
|
||
.wg{padding:14px 16px}
|
||
}
|
||
@media(max-width:480px){
|
||
.hdr h1{font-size:1.1rem}
|
||
.sec-h{font-size:1.15rem}
|
||
.hero h2{font-size:1.2rem}
|
||
.psel-grid{overflow-x:auto;display:flex;flex-wrap:nowrap;gap:8px;scroll-snap-type:x mandatory;padding-bottom:6px}
|
||
.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)}}
|
||
|
||
/* hover-preview карточек выключен — мешал перекрытием соседних рядов */
|
||
.psel-card-preview{display:none!important}
|
||
|
||
/* Карточки-кандидаты для «Упрости корень» (§4) */
|
||
.simp-card-btn{padding:10px 6px;border-radius:11px;border:2px solid var(--border);background:var(--card);cursor:pointer;font-family:'JetBrains Mono',monospace;transition:transform .12s,box-shadow .12s,border-color .12s,background .12s;display:flex;flex-direction:column;align-items:center;gap:2px;min-height:60px}
|
||
.simp-card-btn:hover{transform:translateY(-2px);border-color:var(--pri);box-shadow:0 4px 14px rgba(233,30,99,.18)}
|
||
.simp-card-btn .scb-num{font-size:1.4rem;font-weight:900;color:var(--text);line-height:1}
|
||
.simp-card-btn .scb-sub{font-size:.72rem;color:var(--muted);font-weight:600}
|
||
.simp-card-btn.correct-dnd{background:var(--ok-bg);border-color:var(--ok);color:#065f46;animation:simpPop .35s ease}
|
||
.simp-card-btn.correct-dnd .scb-num{color:#065f46}
|
||
.simp-card-btn.wrong-dnd{background:var(--fail-bg);border-color:var(--fail);animation:simpShake .35s ease}
|
||
.simp-card-btn.locked{opacity:.4;pointer-events:none}
|
||
@keyframes simpPop{0%{transform:scale(1)}50%{transform:scale(1.1)}100%{transform:scale(1)}}
|
||
@keyframes simpShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}
|
||
|
||
/* ═══════════════════════════════════════════════
|
||
BOSS BATTLES — CSS
|
||
═══════════════════════════════════════════════ */
|
||
.boss-card{margin:30px 0 14px;padding:22px;background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)));border:2.5px solid var(--sec-acc,var(--pri));border-radius:18px;box-shadow:0 8px 32px rgba(0,0,0,.08);position:relative;overflow:hidden}
|
||
.boss-card::before{content:'';position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,var(--sec-acc,var(--pri)),var(--acc));border-radius:18px 18px 0 0}
|
||
.boss-header{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||
.boss-header svg{color:var(--sec-acc,var(--pri))}
|
||
.boss-tag{font-family:'Unbounded',sans-serif;font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:var(--sec-acc-d,var(--pri2))}
|
||
.boss-title{font-family:'Unbounded',sans-serif;font-size:1.3rem;font-weight:900;color:var(--text)}
|
||
.boss-header .btn{margin-left:auto}
|
||
.boss-arena{margin-top:18px;padding:18px;background:var(--card);border-radius:13px;border:1px solid var(--border)}
|
||
.boss-progress{display:flex;align-items:center;gap:12px;margin-bottom:18px}
|
||
.boss-progress-bar{flex:1;height:10px;background:rgba(0,0,0,.08);border-radius:6px;overflow:hidden}
|
||
.boss-progress-fill{height:100%;background:linear-gradient(90deg,var(--sec-acc,var(--pri)),var(--acc));border-radius:6px;width:0%;transition:width .4s}
|
||
.boss-progress-text{font-size:.85rem;font-weight:700;color:var(--muted);min-width:80px;text-align:right}
|
||
.boss-task{font-size:1.15rem;font-weight:600;color:var(--text);margin:18px 0;padding:18px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:12px;text-align:center;min-height:80px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px}
|
||
.boss-controls{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin-bottom:10px}
|
||
.boss-controls .btn{font-size:.95rem;padding:10px 18px;min-width:90px}
|
||
.boss-controls .b-act{background:var(--card);border:1.5px solid var(--border);font-weight:600}
|
||
.boss-controls .b-act:hover{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
|
||
.boss-controls .b-act.correct{background:var(--ok);color:#fff;border-color:var(--ok)}
|
||
.boss-controls .b-act.wrong{background:var(--fail);color:#fff;border-color:var(--fail);animation:simpShake .4s ease}
|
||
.boss-aux{margin-top:8px;display:flex;gap:8px;justify-content:center}
|
||
.boss-feedback{margin-top:10px}
|
||
.boss-inp{padding:9px 14px;border-radius:9px;background:var(--card);border:1.5px solid var(--border);font-size:1rem;color:var(--text);font-family:'JetBrains Mono',monospace;width:160px;text-align:center;outline:none}
|
||
.boss-inp:focus{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
|
||
.boss-result{padding:30px;text-align:center;animation:boxIn .5s cubic-bezier(.34,1.56,.64,1)}
|
||
@keyframes boxIn{from{opacity:0;transform:scale(.85) translateY(12px)}to{opacity:1;transform:none}}
|
||
.boss-medal{width:100px;height:100px;margin:0 auto 14px;border-radius:50%;display:flex;align-items:center;justify-content:center;animation:medalSpin .9s ease}
|
||
.boss-medal.gold{background:linear-gradient(135deg,#fbbf24,#f59e0b);box-shadow:0 0 0 6px rgba(245,158,11,.2)}
|
||
.boss-medal.silver{background:linear-gradient(135deg,#cbd5e1,#94a3b8);box-shadow:0 0 0 6px rgba(148,163,184,.2)}
|
||
.boss-medal.bronze{background:linear-gradient(135deg,#fb923c,#ea580c);box-shadow:0 0 0 6px rgba(234,88,12,.2)}
|
||
.boss-medal svg{width:50px;height:50px;color:#fff;stroke-width:2.5}
|
||
.boss-result-title{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px}
|
||
.boss-result-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:18px 0;font-size:.95rem}
|
||
.boss-result-stat-num{font-size:1.6rem;font-weight:900;color:var(--sec-acc,var(--pri))}
|
||
.boss-result-stat-lab{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}
|
||
@keyframes medalSpin{0%{transform:rotateY(0) scale(0.5)}50%{transform:rotateY(180deg) scale(1.1)}100%{transform:rotateY(360deg) scale(1)}}
|
||
|
||
/* Геометрическое доказательство §3 */
|
||
.geo-canvas-wrap{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:8px;margin-top:14px}
|
||
.geo-cell{filter:drop-shadow(0 1px 1px rgba(0,0,0,.12))}
|
||
.geo-formula{text-align:center;font-size:.98rem;line-height:2;padding:10px 14px;background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border-radius:9px;border:1px solid var(--border)}
|
||
.proof-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--ok);color:#fff;border-radius:99px;font-size:.85rem;font-weight:700}
|
||
|
||
/* Конвейер x → x² → √(x²) в §1 */
|
||
.dual-pipeline{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap;margin-top:14px;padding:14px 8px;background:var(--card);border-radius:12px;border:1px solid var(--border)}
|
||
.dual-step{flex:1;min-width:90px;text-align:center;padding:10px 8px;border-radius:10px;background:rgba(233,30,99,0.04);border:1.5px solid var(--border)}
|
||
.dual-step.dual-input{background:rgba(233,30,99,0.12);border-color:rgba(233,30,99,0.4)}
|
||
.dual-step.dual-square{background:rgba(155,93,229,0.10);border-color:rgba(155,93,229,0.35)}
|
||
.dual-step.dual-output{background:rgba(3,169,244,0.12);border-color:rgba(3,169,244,0.4)}
|
||
.dual-step-lab{font-size:.66rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:4px}
|
||
.dual-step-val{font-size:1.8rem;font-weight:900;color:var(--text);font-family:'JetBrains Mono',monospace;line-height:1}
|
||
.dual-step-val.small{font-size:1.3rem;margin-top:4px}
|
||
.dual-step-cap{font-size:.78rem;color:var(--muted);margin-top:4px;font-family:'JetBrains Mono',monospace}
|
||
.dual-arrow{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;min-width:60px}
|
||
.dual-arrow svg{width:54px;height:24px}
|
||
.dual-arrow-lab{font-size:.68rem;color:var(--pri2);font-weight:600;margin-top:2px;text-align:center;line-height:1.2}
|
||
.dual-formula{margin-top:14px;padding:10px 14px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:9px;text-align:center;font-size:.95rem;line-height:1.7;border:1px solid var(--border)}
|
||
.dual-formula.mod-active{background:linear-gradient(135deg,#fef3c7,#fce7f3);border-color:var(--warn)}
|
||
@media(max-width:680px){
|
||
.dual-pipeline{flex-direction:column;gap:8px}
|
||
.dual-step{width:100%}
|
||
.dual-arrow{flex-direction:row;gap:8px}
|
||
.dual-arrow svg{transform:rotate(90deg);width:24px;height:54px}
|
||
}
|
||
|
||
/* 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}}
|
||
|
||
/* ═══════════════════════════════════════════════
|
||
WAVE 3 — UX / NAVIGATION
|
||
═══════════════════════════════════════════════ */
|
||
|
||
/* Task 1: Ctrl+K Search modal */
|
||
.search-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);z-index:9998;padding-top:80px}
|
||
.search-modal.open{display:block;animation:fadeIn .2s ease}
|
||
.search-box{max-width:560px;margin:0 auto;background:var(--card);border-radius:14px;box-shadow:0 16px 50px rgba(0,0,0,.4);overflow:hidden}
|
||
.search-input{width:100%;padding:18px 22px;border:none;background:transparent;color:var(--text);font-size:1.05rem;font-family:inherit;outline:none;border-bottom:1px solid var(--border)}
|
||
.search-results{max-height:50vh;overflow-y:auto}
|
||
.search-result{padding:11px 22px;cursor:pointer;border-bottom:1px solid var(--border);transition:background .12s}
|
||
.search-result:hover,.search-result.selected{background:var(--pri-soft)}
|
||
.search-result-title{font-weight:700;font-size:.92rem;color:var(--text)}
|
||
.search-result-sub{font-size:.78rem;color:var(--muted);margin-top:2px}
|
||
.search-empty{padding:22px;text-align:center;color:var(--muted);font-size:.9rem}
|
||
.search-hint-badge{padding:4px 8px;background:rgba(255,255,255,.18);border-radius:6px;font-size:.72rem;font-weight:700;letter-spacing:.04em}
|
||
|
||
/* Task 2: Shortcuts modal */
|
||
.shortcuts-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);backdrop-filter:blur(3px);z-index:9998;align-items:center;justify-content:center}
|
||
.shortcuts-modal.open{display:flex;animation:fadeIn .2s ease}
|
||
.shortcuts-box{background:var(--card);border-radius:16px;padding:24px 28px;min-width:300px;max-width:400px;box-shadow:0 16px 50px rgba(0,0,0,.35)}
|
||
.shortcuts-box h3{font-size:1rem;font-weight:800;color:var(--pri2);margin-bottom:14px;border-bottom:1px solid var(--border);padding-bottom:10px}
|
||
.shortcut-row{display:flex;align-items:center;gap:10px;padding:6px 0;font-size:.88rem}
|
||
.shortcut-key{display:inline-flex;align-items:center;justify-content:center;min-width:32px;padding:3px 8px;background:var(--pri-soft);border:1px solid var(--border);border-radius:5px;font-family:monospace;font-size:.82rem;font-weight:700;color:var(--pri2)}
|
||
.shortcut-desc{color:var(--text);flex:1}
|
||
|
||
/* Task 3: Bookmarks */
|
||
.bm-btn{position:absolute;top:10px;right:10px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:7px;background:transparent;color:var(--muted);transition:color .15s,background .15s;z-index:2;padding:0}
|
||
.bm-btn:hover{background:var(--pri-soft);color:var(--pri)}
|
||
.bm-btn.saved{color:var(--pri)}
|
||
.bm-icon-outline{display:block}.bm-icon-filled{display:none}
|
||
.bm-btn.saved .bm-icon-outline{display:none}.bm-btn.saved .bm-icon-filled{display:block}
|
||
.sidecard-bm-row{display:flex;align-items:center;gap:6px;padding:5px 0;border-bottom:1px dashed var(--border);font-size:.8rem}
|
||
.sidecard-bm-row:last-child{border-bottom:none}
|
||
.sidecard-bm-title{flex:1;color:var(--text);font-weight:600;cursor:pointer}
|
||
.sidecard-bm-title:hover{color:var(--pri);text-decoration:underline}
|
||
.sidecard-bm-del{color:var(--fail);font-size:.78rem;cursor:pointer;padding:2px 5px;border-radius:4px}
|
||
.sidecard-bm-del:hover{background:var(--fail-bg)}
|
||
|
||
/* Task 4: Glossary tooltips */
|
||
.gloss{border-bottom:1.5px dashed var(--sec-acc,var(--pri));cursor:help;font-style:normal}
|
||
.gloss-tip{position:fixed;max-width:280px;padding:10px 14px;background:var(--card);border:1px solid var(--sec-acc,var(--pri));border-radius:10px;font-size:.82rem;line-height:1.5;box-shadow:0 8px 24px rgba(0,0,0,0.15);z-index:9990;display:none;pointer-events:none}
|
||
.gloss-tip.show{display:block;animation:tipIn .15s ease}
|
||
@keyframes tipIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
||
|
||
/* Task 5: Mini-map */
|
||
.minimap{position:fixed;right:16px;bottom:80px;display:flex;flex-direction:column;gap:6px;z-index:50;background:var(--card);padding:8px;border-radius:10px;border:1px solid var(--border);box-shadow:var(--sh)}
|
||
.mm-dot{width:10px;height:10px;border-radius:50%;background:rgba(0,0,0,.15);cursor:pointer;transition:transform .15s,background .15s}
|
||
.dark .mm-dot{background:rgba(255,255,255,.2)}
|
||
.mm-dot:hover{transform:scale(1.4);background:var(--sec-acc,var(--pri))}
|
||
.mm-dot.active{background:var(--sec-acc,var(--pri));transform:scale(1.3)}
|
||
@media(max-width:980px){.minimap{display:none}}
|
||
|
||
/* Task 6: Hint system */
|
||
.hint-box{background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-left:4px solid var(--warn);padding:10px 14px;border-radius:9px;margin-top:10px;font-size:.88rem;animation:fadeIn .2s ease}
|
||
.dark .hint-box{background:linear-gradient(135deg,rgba(245,158,11,.18),rgba(233,30,99,.12));color:var(--text)}
|
||
.hint-level-badge{display:inline-block;padding:2px 7px;border-radius:5px;font-size:.72rem;font-weight:800;background:var(--warn);color:#451a03;margin-bottom:5px;letter-spacing:.04em}
|
||
|
||
/* Task 7: Mobile Cheatsheet Sidebar button */
|
||
#sidebar-btn{display:none}
|
||
@media(max-width:980px){
|
||
#sidebar-btn{display:inline-flex}
|
||
.col-side{position:fixed;right:0;top:0;width:300px;max-width:90vw;height:100vh;background:var(--card);box-shadow:-12px 0 32px rgba(0,0,0,.2);transform:translateX(100%);transition:transform .25s;z-index:1000;overflow-y:auto;padding:20px;display:block}
|
||
.col-side.open{transform:translateX(0)}
|
||
.col-side-close{display:block;position:absolute;top:12px;right:12px;z-index:2}
|
||
.col-side.side-open{transform:translateX(0)}
|
||
}
|
||
@media(min-width:981px){.col-side-close{display:none}.col-side{transform:none!important}}
|
||
|
||
/* ═══════════════════════════════════════════════
|
||
WAVE 4 — GAMIFICATION
|
||
═══════════════════════════════════════════════ */
|
||
|
||
/* XP / Level card */
|
||
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
|
||
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
|
||
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
|
||
.xp-bar{height:9px;background:rgba(3,169,244,.15);border-radius:6px;overflow:hidden;margin:7px 0}
|
||
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
|
||
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
|
||
|
||
/* Streak card */
|
||
.streak-card{background:linear-gradient(135deg,#fff8e1,#fce7f3);border:1.5px solid var(--warn);border-radius:12px;padding:12px 14px;margin-bottom:14px}
|
||
.dark .streak-card{background:linear-gradient(135deg,#2a1f0a,#3a1229)}
|
||
.streak-row{display:flex;align-items:center;gap:10px}
|
||
.streak-icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:rgba(245,158,11,.15);border-radius:8px;flex-shrink:0}
|
||
.streak-nums{display:flex;gap:14px;margin-top:6px}
|
||
.streak-num{text-align:center}
|
||
.streak-val{font-size:1.3rem;font-weight:900;color:var(--warn);font-family:'JetBrains Mono',monospace}
|
||
.streak-lab{font-size:.68rem;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em}
|
||
|
||
/* Level popup (синий) */
|
||
.lvl-popup{position:fixed;top:18px;right:18px;background:linear-gradient(135deg,var(--acc),var(--acc2));color:#fff;padding:14px 20px;border-radius:12px;font-weight:700;font-size:.92rem;box-shadow:0 8px 32px rgba(3,169,244,.4);z-index:1001;display:none;align-items:center;gap:10px}
|
||
.lvl-popup.show{display:flex;animation:achBounce .5s cubic-bezier(.34,1.56,.64,1) forwards}
|
||
|
||
/* Streak popup */
|
||
.streak-popup{position:fixed;top:70px;right:18px;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(245,158,11,.45);z-index:1002;display:none;align-items:center;gap:8px}
|
||
.streak-popup.show{display:flex;animation:achBounce .45s cubic-bezier(.34,1.56,.64,1) forwards}
|
||
|
||
/* Daily challenge button */
|
||
.daily-btn{position:relative}
|
||
.daily-dot{position:absolute;top:3px;right:3px;width:8px;height:8px;background:var(--warn);border-radius:50%;border:2px solid #fff;display:none}
|
||
.daily-dot.show{display:block;animation:dotPulse 1.4s ease-in-out infinite}
|
||
@keyframes dotPulse{0%,100%{box-shadow:0 0 0 0 rgba(245,158,11,.6)}50%{box-shadow:0 0 0 5px rgba(245,158,11,0)}}
|
||
|
||
/* Daily modal */
|
||
.daily-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);backdrop-filter:blur(4px);z-index:2010;align-items:center;justify-content:center}
|
||
.daily-modal.open{display:flex;animation:modalIn .3s ease}
|
||
.daily-box{background:var(--card);border-radius:18px;padding:30px 28px;max-width:430px;width:92%;box-shadow:0 20px 60px rgba(0,0,0,.3);animation:boxIn .4s cubic-bezier(.34,1.56,.64,1)}
|
||
.daily-badge{display:inline-flex;align-items:center;gap:7px;padding:5px 12px;background:linear-gradient(135deg,var(--warn),#ef4444);color:#fff;border-radius:8px;font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;margin-bottom:14px}
|
||
.daily-q{font-size:1.1rem;font-weight:700;color:var(--text);margin-bottom:18px;line-height:1.5}
|
||
.daily-hint{font-size:.8rem;color:var(--muted);margin-top:-10px;margin-bottom:12px;font-style:italic}
|
||
.daily-done{text-align:center;padding:20px 0}
|
||
.daily-done-icon{font-size:3rem;margin-bottom:10px}
|
||
|
||
/* Achievements gallery modal */
|
||
.ach-gallery-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(5px);z-index:2005;align-items:flex-start;justify-content:center;overflow-y:auto;padding:24px 16px}
|
||
.ach-gallery-modal.open{display:flex;animation:modalIn .3s ease}
|
||
.ach-gallery-box{background:var(--card);border-radius:20px;padding:28px;width:100%;max-width:680px;box-shadow:0 24px 64px rgba(0,0,0,.35);margin:auto;animation:boxIn .4s cubic-bezier(.34,1.56,.64,1)}
|
||
.ach-gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;margin-top:18px}
|
||
.ach-card{background:var(--bg);border:1.5px solid var(--border);padding:16px 12px;border-radius:12px;text-align:center;opacity:.38;transition:opacity .25s,border-color .25s,transform .2s}
|
||
.ach-card:hover{transform:translateY(-2px)}
|
||
.ach-card.earned{opacity:1;border-color:var(--sec-acc,var(--warn));background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft))}
|
||
.ach-card-icon{width:44px;height:44px;margin:0 auto 8px;display:flex;align-items:center;justify-content:center;border-radius:12px;background:var(--pri-soft)}
|
||
.ach-card.earned .ach-card-icon{background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff}
|
||
.ach-card-title{font-weight:800;font-size:.82rem;color:var(--text);line-height:1.25;margin-bottom:4px}
|
||
.ach-card-desc{font-size:.72rem;color:var(--muted);line-height:1.35}
|
||
.ach-card-date{font-size:.68rem;color:var(--ok);margin-top:5px;font-weight:600}
|
||
|
||
/* Circular progress */
|
||
.psel-prog-circle{width:34px;height:34px;position:absolute;top:10px;right:10px}
|
||
.psel-prog-bg{fill:none;stroke:rgba(233,30,99,.12);stroke-width:3.5}
|
||
.psel-prog-fg{fill:none;stroke:var(--pri);stroke-width:3.5;stroke-linecap:round;transform:rotate(-90deg);transform-origin:50% 50%;transition:stroke-dasharray .5s}
|
||
.psel-prog-circle text{font-size:8px;font-weight:800;fill:var(--pri2);font-family:'Inter',sans-serif}
|
||
|
||
/* Final chapter modal */
|
||
.final-chapter-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);backdrop-filter:blur(8px);z-index:2020;align-items:center;justify-content:center;padding:16px}
|
||
.final-chapter-modal.open{display:flex;animation:modalIn .4s ease}
|
||
.final-chapter-box{background:var(--card);border-radius:22px;padding:36px 32px;max-width:480px;width:100%;box-shadow:0 28px 80px rgba(233,30,99,.25);text-align:center;animation:boxIn .5s cubic-bezier(.34,1.56,.64,1)}
|
||
.fc-title{font-size:1.6rem;font-weight:900;color:var(--pri2);font-family:'Unbounded',sans-serif;margin:14px 0 8px}
|
||
.fc-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:16px 0}
|
||
.fc-stat{background:var(--pri-soft);border-radius:10px;padding:12px 8px;text-align:center}
|
||
.fc-stat-val{font-size:1.4rem;font-weight:900;color:var(--pri2);font-family:'JetBrains Mono',monospace}
|
||
.fc-stat-lab{font-size:.7rem;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em;margin-top:3px}
|
||
.fc-topics{text-align:left;margin:14px 0;background:var(--acc-soft);border-radius:10px;padding:12px 14px}
|
||
.fc-topics li{font-size:.85rem;margin-bottom:4px;color:var(--text)}
|
||
|
||
/* Mute button */
|
||
.mute-btn{position:relative}
|
||
|
||
/* Sound indicator */
|
||
#sound-muted-hint{display:none}
|
||
|
||
/* ═══════════════════════════════════════════════
|
||
WAVE DEPTH — 4 new widgets
|
||
═══════════════════════════════════════════════ */
|
||
|
||
/* Widget 1: Column root extraction */
|
||
.cl-workspace{font-family:'JetBrains Mono',monospace;font-size:1.05rem;background:var(--card);border:1px solid var(--border);border-radius:11px;padding:18px 22px;margin-top:14px;line-height:1.7;min-height:120px;white-space:pre}
|
||
.cl-workspace .cl-active{background:var(--sec-acc-soft,#fce7f3);padding:0 4px;border-radius:3px;font-weight:700;color:var(--sec-acc,var(--pri))}
|
||
.cl-workspace .cl-result{color:var(--ok);font-weight:800}
|
||
.cl-explain{margin-top:10px;padding:10px 14px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:9px;font-size:.92rem;line-height:1.5;min-height:60px}
|
||
.cl-final-badge{display:inline-block;padding:8px 16px;margin:14px auto;background:linear-gradient(135deg,var(--ok),#34d399);color:#fff;border-radius:99px;font-weight:800;font-size:1.1rem;box-shadow:0 6px 18px rgba(16,185,129,.3);animation:simpPop .5s ease}
|
||
|
||
/* Widget 2: Square comparison SVG */
|
||
.sq-comp-conclusion{margin-top:10px;text-align:center;font-size:1rem;min-height:36px;padding:8px 12px;border-radius:8px;background:var(--card);border:1px solid var(--border)}
|
||
|
||
/* Widget 3: Euler-Venn diagrams */
|
||
.ev-result{margin-top:10px;text-align:center;font-size:.95rem;padding:8px 12px;background:var(--card);border-radius:9px;border:1px solid var(--border);min-height:36px}
|
||
|
||
/* Widget 4: Multi-row system solver */
|
||
.sys-row-card{padding:10px;background:var(--card);border-radius:9px;border:1.5px solid var(--border);margin-bottom:8px}
|
||
</style>
|
||
|
||
</head>
|
||
<body>
|
||
|
||
<header class="hdr">
|
||
<div class="hdr-row">
|
||
<div>
|
||
<h1>Алгебра 8 · Глава 1</h1>
|
||
<div class="hdr-sub">Квадратные корни и их свойства. Действительные числа</div>
|
||
</div>
|
||
<div class="hdr-side">
|
||
<button id="sidebar-btn" class="hdr-btn" onclick="toggleSidebar()" title="Шпаргалка">
|
||
<svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg>
|
||
Шпаргалка
|
||
</button>
|
||
<button id="side-open-btn" class="hdr-btn" onclick="openSidebar()" title="Шпаргалка" style="display:none">
|
||
<svg class="ic" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||
<span>Шпаргалка</span>
|
||
</button>
|
||
<button id="search-open-btn" class="hdr-btn" onclick="openSearch()" title="Поиск (Ctrl+K)">
|
||
<svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg>
|
||
<span class="search-hint-badge">Ctrl+K</span>
|
||
</button>
|
||
<button id="daily-btn" class="hdr-btn daily-btn" onclick="openDailyChallenge()" title="Задача дня">
|
||
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||
<span>Задача дня</span>
|
||
<span id="daily-dot" class="daily-dot"></span>
|
||
</button>
|
||
<button id="ach-gallery-btn" class="hdr-btn" onclick="openAchGallery()" title="Галерея достижений">
|
||
<svg class="ic" viewBox="0 0 24 24"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg>
|
||
<span>Трофеи</span>
|
||
</button>
|
||
<button id="mute-btn" class="hdr-btn mute-btn" onclick="toggleMute()" title="Звук">
|
||
<svg id="sound-on-ic" class="ic" viewBox="0 0 24 24"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
|
||
<svg id="sound-off-ic" class="ic" viewBox="0 0 24 24" style="display:none"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>
|
||
</button>
|
||
<a href="/textbook/algebra-8-ch2" class="hdr-btn" title="Глава 2 — Квадратные уравнения">
|
||
Глава 2
|
||
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
|
||
</a>
|
||
<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 class="main">
|
||
<div class="col-main">
|
||
|
||
<section class="hero">
|
||
<h2>Корни — это про обратное</h2>
|
||
<p>Возведение в квадрат — это шаг вперёд: <b>5 → 25</b>. Квадратный корень — это шаг назад: <b>25 → 5</b>. В этой главе вы научитесь извлекать корни, упрощать выражения с ними, открыть для себя <b>иррациональные числа</b> (привет, π) и работать с <b>числовыми промежутками</b> и <b>системами неравенств</b>.</p>
|
||
<div class="hero-row">
|
||
<button class="btn-primary" onclick="goTo('p1')">
|
||
<svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg>
|
||
Начать §1
|
||
</button>
|
||
<div class="hero-progress">
|
||
<span class="hp-label">Прогресс по главе</span>
|
||
<div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div>
|
||
<span id="hero-hp-text" class="hp-text">0%</span>
|
||
</div>
|
||
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="psel">
|
||
<div class="psel-title">Параграфы главы</div>
|
||
<div id="psel-grid" class="psel-grid"></div>
|
||
</section>
|
||
|
||
<!-- §1 -->
|
||
<section id="sec-p1" class="sec" data-watermark="√">
|
||
<div class="sec-header">
|
||
<span class="sec-num">§ 1</span>
|
||
<h2 class="sec-h">Квадратный корень из числа. Арифметический квадратный корень</h2>
|
||
</div>
|
||
<div id="p1-body"></div>
|
||
</section>
|
||
|
||
<!-- §2 -->
|
||
<section id="sec-p2" class="sec" data-watermark="ℝ">
|
||
<div class="sec-header">
|
||
<span class="sec-num">§ 2</span>
|
||
<h2 class="sec-h">Множество иррациональных чисел. Множество действительных чисел</h2>
|
||
</div>
|
||
<div id="p2-body"></div>
|
||
</section>
|
||
|
||
<!-- §3 -->
|
||
<section id="sec-p3" class="sec" data-watermark="×">
|
||
<div class="sec-header">
|
||
<span class="sec-num">§ 3</span>
|
||
<h2 class="sec-h">Свойства квадратных корней</h2>
|
||
</div>
|
||
<div id="p3-body"></div>
|
||
</section>
|
||
|
||
<!-- §4 -->
|
||
<section id="sec-p4" class="sec" data-watermark="↓">
|
||
<div class="sec-header">
|
||
<span class="sec-num">§ 4</span>
|
||
<h2 class="sec-h">Применение свойств квадратных корней</h2>
|
||
</div>
|
||
<div id="p4-body"></div>
|
||
</section>
|
||
|
||
<!-- §5 -->
|
||
<section id="sec-p5" class="sec" data-watermark="[;]">
|
||
<div class="sec-header">
|
||
<span class="sec-num">§ 5</span>
|
||
<h2 class="sec-h">Числовые промежутки. Объединение и пересечение</h2>
|
||
</div>
|
||
<div id="p5-body"></div>
|
||
</section>
|
||
|
||
<!-- §6 -->
|
||
<section id="sec-p6" class="sec" data-watermark="{">
|
||
<div class="sec-header">
|
||
<span class="sec-num">§ 6</span>
|
||
<h2 class="sec-h">Системы и совокупности линейных неравенств. Двойные неравенства</h2>
|
||
</div>
|
||
<div id="p6-body"></div>
|
||
</section>
|
||
|
||
<!-- Final -->
|
||
<section id="sec-final" class="sec" data-watermark="★">
|
||
<div class="sec-header">
|
||
<span class="sec-num" style="background:linear-gradient(135deg,#f59e0b,#ec4899)">Финал главы</span>
|
||
<h2 class="sec-h">Итоги. Практическая и увлекательная математика</h2>
|
||
</div>
|
||
<div id="final-body"></div>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<aside class="col-side" id="col-side">
|
||
<button class="col-side-close btn small" onclick="toggleSidebar()" title="Закрыть" style="margin-bottom:10px">
|
||
<svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||
</button>
|
||
<div id="sidebar-content"></div>
|
||
</aside>
|
||
</main>
|
||
|
||
<footer class="foot">
|
||
Интерактивный учебник по <b>Алгебре 8</b> · Глава 1 · LearnSpace · версия 1.0
|
||
</footer>
|
||
|
||
<canvas id="confetti-canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999;display:block"></canvas>
|
||
<div id="side-overlay" class="side-overlay" onclick="closeSidebar()"></div>
|
||
|
||
<div id="ach-popup" class="ach-popup">
|
||
<span class="ach-icon-pulse">
|
||
<svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px;stroke:#451a03"><circle cx="12" cy="8" r="5"/><path d="M8 13l-2 8 6-4 6 4-2-8"/></svg>
|
||
</span>
|
||
<span id="ach-text">Достижение!</span>
|
||
</div>
|
||
|
||
<!-- Wave 3: Search modal -->
|
||
<div id="search-modal" class="search-modal" onclick="if(event.target===this)closeSearch()">
|
||
<div class="search-box">
|
||
<input id="search-modal-input" class="search-input" type="text" placeholder="Поиск по параграфам, терминам, интерактивам..." autocomplete="off">
|
||
<div id="search-results" class="search-results"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Wave 3: Shortcuts modal -->
|
||
<div id="shortcuts-modal" class="shortcuts-modal" onclick="if(event.target===this)closeShortcutsModal()">
|
||
<div class="shortcuts-box">
|
||
<h3>Горячие клавиши</h3>
|
||
<div class="shortcut-row"><span class="shortcut-key">Ctrl+K</span><span class="shortcut-desc">Открыть поиск</span></div>
|
||
<div class="shortcut-row"><span class="shortcut-key">1–7</span><span class="shortcut-desc">Перейти к §1–§6 или Финалу</span></div>
|
||
<div class="shortcut-row"><span class="shortcut-key">←</span><span class="shortcut-desc">Предыдущий параграф</span></div>
|
||
<div class="shortcut-row"><span class="shortcut-key">→</span><span class="shortcut-desc">Следующий параграф</span></div>
|
||
<div class="shortcut-row"><span class="shortcut-key">Esc</span><span class="shortcut-desc">Закрыть модалку / поиск</span></div>
|
||
<div class="shortcut-row"><span class="shortcut-key">?</span><span class="shortcut-desc">Показать эту справку</span></div>
|
||
<div style="margin-top:14px;text-align:right"><button class="btn small primary" onclick="closeShortcutsModal()">Закрыть</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Wave 3: Glossary tooltip -->
|
||
<div id="gloss-tip" class="gloss-tip"></div>
|
||
|
||
<!-- Wave 3: Minimap -->
|
||
<div id="minimap" class="minimap" title="Мини-карта секции"></div>
|
||
|
||
<!-- Wave 4: Level popup -->
|
||
<div id="lvl-popup" class="lvl-popup">
|
||
<svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px;stroke:#fff"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||
<span id="lvl-popup-text">Уровень 2!</span>
|
||
</div>
|
||
|
||
<!-- Wave 4: Streak popup -->
|
||
<div id="streak-popup" class="streak-popup">
|
||
<svg class="ic" viewBox="0 0 24 24" style="width:20px;height:20px;stroke:#fff"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||
<span id="streak-popup-text">Streak x3!</span>
|
||
</div>
|
||
|
||
<!-- Wave 4: Daily Challenge modal -->
|
||
<div id="daily-modal" class="daily-modal" onclick="if(event.target===this)closeDailyChallenge()">
|
||
<div class="daily-box">
|
||
<div class="daily-badge">
|
||
<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px;stroke:#fff"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||
Задача дня
|
||
</div>
|
||
<div id="daily-content"></div>
|
||
<div style="text-align:right;margin-top:8px"><button class="btn small" onclick="closeDailyChallenge()">Закрыть</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Wave 4: Achievements Gallery modal -->
|
||
<div id="ach-gallery-modal" class="ach-gallery-modal" onclick="if(event.target===this)closeAchGallery()">
|
||
<div class="ach-gallery-box">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||
<h3 style="font-size:1.1rem;font-weight:800;color:var(--pri2)">Галерея достижений</h3>
|
||
<button class="btn small" onclick="closeAchGallery()">Закрыть</button>
|
||
</div>
|
||
<div id="ach-gallery-count" style="font-size:.8rem;color:var(--muted)"></div>
|
||
<div id="ach-gallery-grid" class="ach-gallery-grid"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Wave 4: Final Chapter modal -->
|
||
<div id="final-chapter-modal" class="final-chapter-modal" onclick="if(event.target===this)closeFinalChapterModal()">
|
||
<div class="final-chapter-box">
|
||
<div style="font-size:2.8rem">
|
||
<svg viewBox="0 0 24 24" style="width:56px;height:56px;display:inline-block;stroke:#f59e0b;fill:rgba(245,158,11,.15)"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" stroke-width="1.5"/></svg>
|
||
</div>
|
||
<div class="fc-title">Глава 1 завершена!</div>
|
||
<p style="color:var(--muted);font-size:.9rem;margin-bottom:14px">Вы освоили все темы Главы 1!</p>
|
||
<div class="fc-stats" id="fc-stats-box"></div>
|
||
<div class="fc-topics">
|
||
<ul style="padding-left:18px;list-style:none">
|
||
<li>✓ Арифметический квадратный корень</li>
|
||
<li>✓ Иррациональные и действительные числа</li>
|
||
<li>✓ Свойства квадратных корней</li>
|
||
<li>✓ Применение свойств (упрощение, сравнение)</li>
|
||
<li>✓ Числовые промежутки, ∪ и ∩</li>
|
||
<li>✓ Системы и совокупности неравенств</li>
|
||
</ul>
|
||
</div>
|
||
<div style="display:flex;gap:10px;justify-content:center;margin-top:18px">
|
||
<button class="btn primary" onclick="alert('Скоро! Глава 2 — Квадратные уравнения.')">Перейти к главе 2</button>
|
||
<button class="btn" onclick="closeFinalChapterModal()">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
STATE & PROGRESS
|
||
════════════════════════════════════════════════════════ */
|
||
const STATE = {
|
||
current: 'p1',
|
||
progress: { p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, final: 0 },
|
||
achievements: new Map(), // id → human-readable text
|
||
squaresBest: Infinity,
|
||
xp: 0,
|
||
level: 1,
|
||
streak: 0,
|
||
maxStreak: 0,
|
||
dailyChallenge: { date: null, completed: false, taskIdx: 0 },
|
||
bossResults: {}, // secId → { passed, score, total, perfect }
|
||
};
|
||
|
||
/* Словарь имён достижений — используется и для отображения, и для retroactive-фикса старых записей */
|
||
const ACH_LABELS = {
|
||
start: 'Начало пути по корням!',
|
||
ring36: 'Нашёл сторону ринга',
|
||
squares: 'Лучший результат «Таблица квадратов»',
|
||
exists: 'Сортировка корней',
|
||
classify: 'Классифицировал числа',
|
||
rat: 'Распознал иррациональные',
|
||
match: 'Match выражений',
|
||
simp4: 'Тренажёр упрощения корней',
|
||
draw: 'Построил промежуток',
|
||
tariff: 'Задача про тарифы',
|
||
ass8: 'Самооценка 8+/10',
|
||
pr1: 'Дорожка с розами',
|
||
pr2: 'Цемент',
|
||
decode: 'Расшифровал код',
|
||
daily_1: 'Задача дня выполнена!',
|
||
streak3: 'Серия x3 — не сдавайся!',
|
||
streak5: 'Серия x5 — горишь!',
|
||
streak7: 'Серия x7 — в ударе!',
|
||
streak10: 'Серия x10 — легенда!',
|
||
lv5: 'Достигнут уровень 5',
|
||
};
|
||
|
||
function loadProgress(){
|
||
try{
|
||
const s = localStorage.getItem('algebra8_ch1_progress');
|
||
if(s){ Object.assign(STATE.progress, JSON.parse(s)); }
|
||
const a = localStorage.getItem('algebra8_ch1_achievements');
|
||
if(a){
|
||
const parsed = JSON.parse(a);
|
||
if(Array.isArray(parsed)){
|
||
// старый формат [id, id, ...]
|
||
parsed.forEach(id => STATE.achievements.set(id, ACH_LABELS[id] || id));
|
||
} else if(parsed && typeof parsed === 'object'){
|
||
for(const [id, txt] of Object.entries(parsed)){
|
||
// если ранее сохранили id вместо названия — подменим на красивое
|
||
STATE.achievements.set(id, (txt && txt !== id) ? txt : (ACH_LABELS[id] || id));
|
||
}
|
||
}
|
||
}
|
||
const sb = localStorage.getItem('algebra8_ch1_squaresBest');
|
||
if(sb) STATE.squaresBest = +sb;
|
||
// Общий XP для всех глав. Если ещё нет — собираем из старых ch1/ch2 ключей (single-shot миграция).
|
||
let xp = localStorage.getItem('algebra8_xp');
|
||
if(xp === null){
|
||
const c1 = +(localStorage.getItem('algebra8_ch1_xp') || 0);
|
||
const c2 = +(localStorage.getItem('algebra8_ch2_xp') || 0);
|
||
xp = c1 + c2;
|
||
try { localStorage.setItem('algebra8_xp', String(xp)); } catch(e){}
|
||
}
|
||
STATE.xp = +xp || 0; STATE.level = calcLevel(STATE.xp);
|
||
const sk = localStorage.getItem('algebra8_ch1_streak');
|
||
if(sk){ const o = JSON.parse(sk); STATE.streak = o.streak||0; STATE.maxStreak = o.max||0; }
|
||
const dc = localStorage.getItem('algebra8_ch1_daily');
|
||
if(dc){ Object.assign(STATE.dailyChallenge, JSON.parse(dc)); }
|
||
const br = localStorage.getItem('algebra8_ch1_bossResults');
|
||
if(br){ Object.assign(STATE.bossResults, JSON.parse(br)); }
|
||
}catch(e){}
|
||
}
|
||
function saveProgress(){
|
||
try{
|
||
localStorage.setItem('algebra8_ch1_progress', JSON.stringify(STATE.progress));
|
||
localStorage.setItem('algebra8_ch1_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
|
||
if(isFinite(STATE.squaresBest)) localStorage.setItem('algebra8_ch1_squaresBest', String(STATE.squaresBest));
|
||
localStorage.setItem('algebra8_xp', String(STATE.xp));
|
||
localStorage.setItem('algebra8_ch1_streak', JSON.stringify({streak:STATE.streak, max:STATE.maxStreak}));
|
||
localStorage.setItem('algebra8_ch1_daily', JSON.stringify(STATE.dailyChallenge));
|
||
localStorage.setItem('algebra8_ch1_bossResults', JSON.stringify(STATE.bossResults));
|
||
}catch(e){}
|
||
}
|
||
function bumpProgress(key, delta){
|
||
const v = Math.max(0, Math.min(100, (STATE.progress[key] || 0) + delta));
|
||
STATE.progress[key] = v;
|
||
saveProgress();
|
||
refreshProgressUI();
|
||
}
|
||
function refreshProgressUI(){
|
||
const total = Object.values(STATE.progress).reduce((a,b)=>a+b,0) / 7;
|
||
const t = Math.round(total);
|
||
const fill = document.getElementById('hero-hp-fill');
|
||
if(fill) fill.style.width = t + '%';
|
||
const txt = document.getElementById('hero-hp-text');
|
||
if(txt) txt.textContent = t + '% пройдено';
|
||
const circ = 97.4;
|
||
document.querySelectorAll('[data-prog-card]').forEach(card=>{
|
||
const k = card.dataset.progCard;
|
||
const pct = STATE.progress[k] || 0;
|
||
const fl = card.querySelector('.psel-prog-fill');
|
||
if(fl) fl.style.width = pct + '%';
|
||
// circular
|
||
const svg = card.querySelector('[data-prog-circle]');
|
||
if(svg){
|
||
const fg = svg.querySelector('.psel-prog-fg');
|
||
const tx = svg.querySelector('text');
|
||
if(fg) fg.setAttribute('stroke-dasharray', (pct / 100 * circ).toFixed(1) + ', ' + circ);
|
||
if(tx) tx.textContent = pct + '%';
|
||
}
|
||
});
|
||
// check 95% for final chapter modal
|
||
if(t >= 95) _maybeShowFinalChapter();
|
||
// XP badge in hero — единый стиль с главой 2
|
||
const xpBadge = document.getElementById('hero-xp-badge');
|
||
if(xpBadge){
|
||
xpBadge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> Ур. ' + STATE.level + ' · ' + (STATE.xp || 0) + ' XP';
|
||
}
|
||
}
|
||
let _finalShown = false;
|
||
function _maybeShowFinalChapter(){
|
||
if(_finalShown) return;
|
||
if(localStorage.getItem('algebra8_final_shown')) return;
|
||
_finalShown = true;
|
||
localStorage.setItem('algebra8_final_shown', '1');
|
||
setTimeout(showFinalChapterModal, 600);
|
||
}
|
||
function achievement(id, text){
|
||
if(STATE.achievements.has(id)) return;
|
||
STATE.achievements.set(id, text);
|
||
saveProgress();
|
||
const pop = document.getElementById('ach-popup');
|
||
document.getElementById('ach-text').textContent = text;
|
||
pop.classList.add('show');
|
||
setTimeout(()=>pop.classList.remove('show'), 3300);
|
||
// Wave 1: celebratory confetti
|
||
setTimeout(()=>confetti(), 150);
|
||
// Wave 4: +20 XP for achievement
|
||
addXp(20, 'ach');
|
||
sounds.achievement();
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
PARA SELECTOR
|
||
════════════════════════════════════════════════════════ */
|
||
const PARAS = [
|
||
{ 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(){
|
||
const g = document.getElementById('psel-grid');
|
||
g.innerHTML = '';
|
||
PARAS.forEach(p=>{
|
||
const card = document.createElement('div');
|
||
card.className = 'psel-card' + (p.final ? ' final' : '');
|
||
card.dataset.id = p.id;
|
||
card.dataset.progCard = p.id;
|
||
const topicsHtml = (p.topics||[]).map(t=>`<div class="psel-preview-topic">${t}</div>`).join('');
|
||
const progPct = STATE.progress[p.id] || 0;
|
||
const circleCircumference = 97.4; // 2 * PI * 15.5 ≈ 97.4
|
||
const dashArr = Math.round(progPct / 100 * circleCircumference * 10) / 10;
|
||
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>
|
||
<svg class="psel-prog-circle" viewBox="0 0 36 36" data-prog-circle="${p.id}">
|
||
<path class="psel-prog-bg" d="M18 2.5 a 15.5 15.5 0 1 1 0 31 a 15.5 15.5 0 1 1 0 -31"/>
|
||
<path class="psel-prog-fg" d="M18 2.5 a 15.5 15.5 0 1 1 0 31 a 15.5 15.5 0 1 1 0 -31" stroke-dasharray="${dashArr}, ${circleCircumference}"/>
|
||
<text x="18" y="21" text-anchor="middle">${progPct}%</text>
|
||
</svg>`;
|
||
card.addEventListener('click', ()=>goTo(p.id));
|
||
g.appendChild(card);
|
||
});
|
||
}
|
||
|
||
const BUILT = new Set();
|
||
const BUILDERS = { p1:()=>buildP1(), p2:()=>buildP2(), p3:()=>buildP3(), p4:()=>buildP4(), p5:()=>buildP5(), p6:()=>buildP6(), final:()=>buildFinal() };
|
||
function ensureBuilt(id){
|
||
if(BUILT.has(id)) return;
|
||
const fn = BUILDERS[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);
|
||
const el = document.getElementById('sec-' + id);
|
||
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},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}),0);
|
||
}
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
SIDEBAR (шпаргалка)
|
||
════════════════════════════════════════════════════════ */
|
||
const SIDEBARS = {
|
||
p1:{
|
||
title:'Шпаргалка §1',
|
||
rows:[
|
||
['$\\sqrt{a}$','арифметический корень из <b>$a \\geq 0$</b>'],
|
||
['Определение','<i>неотриц.</i> число, квадрат которого равен <b>$a$</b>'],
|
||
['$\\sqrt{0}$','$= 0$'],
|
||
['$\\sqrt{-25}$','не существует'],
|
||
['$(\\sqrt{a})^2$','$= a$, при $a \\geq 0$'],
|
||
['$\\sqrt{a^2}$','$= |a|$'],
|
||
]
|
||
},
|
||
p2:{
|
||
title:'Шпаргалка §2',
|
||
rows:[
|
||
['$\\mathbb{N}$','натуральные: $1, 2, 3, \\ldots$'],
|
||
['$\\mathbb{Z}$','целые: $\\ldots, -2, -1, 0, 1, 2, \\ldots$'],
|
||
['$\\mathbb{Q}$','рациональные: $m/n$, $n \\neq 0$'],
|
||
['$\\mathbb{I}$','иррац.: $\\sqrt{2}, \\sqrt{3}, \\pi, e$'],
|
||
['$\\mathbb{R}$','действ.: $\\mathbb{Q} \\cup \\mathbb{I}$'],
|
||
['Включение','$\\mathbb{N} \\subset \\mathbb{Z} \\subset \\mathbb{Q} \\subset \\mathbb{R}$'],
|
||
]
|
||
},
|
||
p3:{
|
||
title:'Шпаргалка §3',
|
||
rows:[
|
||
['$\\sqrt{ab}$','$= \\sqrt{a} \\cdot \\sqrt{b}$, $a,b \\geq 0$'],
|
||
['$\\sqrt{a/b}$','$= \\sqrt{a}/\\sqrt{b}$, $a \\geq 0$, $b>0$'],
|
||
['$\\sqrt{a^2}$','$= |a|$'],
|
||
['$(\\sqrt{a})^2$','$= a$'],
|
||
['Пример','$\\sqrt{36 \\cdot 25} = 6 \\cdot 5 = 30$'],
|
||
]
|
||
},
|
||
p4:{
|
||
title:'Шпаргалка §4',
|
||
rows:[
|
||
['Вынесение','$\\sqrt{a^2 b} = a\\sqrt{b}$ при $a \\geq 0$'],
|
||
['Внесение','$a\\sqrt{b} = \\sqrt{a^2 b}$ при $a \\geq 0$'],
|
||
['От иррац.','$\\dfrac{1}{\\sqrt{a}} = \\dfrac{\\sqrt{a}}{a}$'],
|
||
['Сложнее','$\\dfrac{c}{a\\sqrt{b}} = \\dfrac{c\\sqrt{b}}{ab}$'],
|
||
['Сравнение','возведением в квадрат'],
|
||
]
|
||
},
|
||
p5:{
|
||
title:'Шпаргалка §5',
|
||
rows:[
|
||
['$(a; b)$','$a < x < b$ — открытый'],
|
||
['$[a; b]$','$a \\leq x \\leq b$ — закрытый'],
|
||
['$[a; b)$','$a \\leq x < b$ — полуоткр.'],
|
||
['$(a; +\\infty)$','$x > a$ — луч'],
|
||
['$A \\cup B$','объединение (или)'],
|
||
['$A \\cap B$','пересечение (и)'],
|
||
]
|
||
},
|
||
p6:{
|
||
title:'Шпаргалка §6',
|
||
rows:[
|
||
['$\\{\\,$ система','решение $= \\cap$ (и то, и то)'],
|
||
['$[\\,$ совокупн.','решение $= \\cup$ (одно ИЛИ другое)'],
|
||
['Двойное','$a<x<b \\Leftrightarrow \\{x>a;\\, x<b\\}$'],
|
||
['Алгоритм','1) решить каждое'],
|
||
['','2) применить $\\cap$ или $\\cup$'],
|
||
]
|
||
},
|
||
final:{
|
||
title:'Финал главы',
|
||
rows:[
|
||
['10 задач','итоговая самооценка'],
|
||
['3 задачи','практическая математика'],
|
||
['+','увлекательная математика'],
|
||
['','историч. справки'],
|
||
['','олимпиадные задачи'],
|
||
]
|
||
}
|
||
};
|
||
function buildSidebar(id){
|
||
const box = document.getElementById('sidebar-content');
|
||
const sb = SIDEBARS[id] || SIDEBARS.p1;
|
||
|
||
// XP card
|
||
const xpForLevel = _xpForLevel(STATE.level);
|
||
const xpNext = _xpForLevel(STATE.level + 1);
|
||
const xpInLevel = STATE.xp - xpForLevel;
|
||
const xpRange = xpNext - xpForLevel;
|
||
const xpPct = xpRange > 0 ? Math.round(xpInLevel / xpRange * 100) : 100;
|
||
let html = `<div class="xp-card">
|
||
<div class="xp-card-title">
|
||
<span>XP-прогресс</span>
|
||
<span class="xp-level">Ур. ${STATE.level}</span>
|
||
</div>
|
||
<div class="xp-bar"><div class="xp-fill" id="xp-fill" style="width:${xpPct}%"></div></div>
|
||
<div class="xp-nums"><span>${STATE.xp} XP</span><span>${STATE.level < 10 ? xpNext + ' XP' : 'MAX'}</span></div>
|
||
</div>`;
|
||
|
||
// Streak card
|
||
html += `<div class="streak-card">
|
||
<div class="streak-row">
|
||
<div class="streak-icon">
|
||
<svg class="ic" viewBox="0 0 24 24" style="stroke:var(--warn)"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||
</div>
|
||
<span style="font-size:.8rem;font-weight:700;color:var(--text)">Серия правильных ответов</span>
|
||
</div>
|
||
<div class="streak-nums">
|
||
<div class="streak-num"><div class="streak-val">${STATE.streak}</div><div class="streak-lab">Текущая</div></div>
|
||
<div class="streak-num"><div class="streak-val">${STATE.maxStreak}</div><div class="streak-lab">Рекорд</div></div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// Шпаргалка
|
||
html += `<div class="sidecard"><h4>${sb.title}</h4>`;
|
||
sb.rows.forEach(([k,v])=>{
|
||
html += `<div class="sidecard-row"><b>${k}</b> ${v ? '— ' + v : ''}</div>`;
|
||
});
|
||
html += '</div>';
|
||
|
||
// achievements
|
||
if(STATE.achievements.size > 0){
|
||
html += `<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">${STATE.achievements.size}</span></h4>`;
|
||
[...STATE.achievements.values()].slice(-4).forEach(text=>{
|
||
html += `<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ${text}</div>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
// glossary link
|
||
html += `<div class="sidecard" style="background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft))"><h4>Подсказка</h4>
|
||
<div class="sidecard-row" style="font-size:.82rem">Учитесь без спешки. Сначала прочитайте теорию, потом попробуйте интерактив, и только потом решайте задачи.</div></div>`;
|
||
box.innerHTML = html;
|
||
// render KaTeX inside sidebar
|
||
if(window.renderMathInElement){
|
||
try{ renderMathInElement(box, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){}
|
||
}
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
THEME
|
||
════════════════════════════════════════════════════════ */
|
||
function initTheme(){
|
||
const t = localStorage.getItem('algebra8_theme') || 'light';
|
||
if(t === 'dark') document.documentElement.classList.add('dark');
|
||
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
|
||
document.getElementById('theme-btn').addEventListener('click', ()=>{
|
||
document.documentElement.classList.toggle('dark');
|
||
const dark = document.documentElement.classList.contains('dark');
|
||
localStorage.setItem('algebra8_theme', dark ? 'dark' : 'light');
|
||
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
|
||
});
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
SEARCH (простая фильтрация по тексту)
|
||
════════════════════════════════════════════════════════ */
|
||
function initSearch(){
|
||
// legacy: фильтр карточек по input #search-inp — был заменён модальным поиском Ctrl+K (Wave 3).
|
||
// если элемент остался — подключаем для обратной совместимости, иначе пропускаем.
|
||
const inp = document.getElementById('search-inp');
|
||
if(!inp) return;
|
||
inp.addEventListener('input', ()=>{
|
||
const q = inp.value.trim().toLowerCase();
|
||
if(!q){
|
||
document.querySelectorAll('.psel-card').forEach(c=>c.style.display='');
|
||
return;
|
||
}
|
||
document.querySelectorAll('.psel-card').forEach(c=>{
|
||
const t = c.textContent.toLowerCase();
|
||
c.style.display = t.includes(q) ? '' : 'none';
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
HELPERS
|
||
════════════════════════════════════════════════════════ */
|
||
function $(sel, root){ return (root||document).querySelector(sel); }
|
||
function $$(sel, root){ return [...(root||document).querySelectorAll(sel)]; }
|
||
function el(tag, attrs, html){
|
||
const e = document.createElement(tag);
|
||
if(attrs) Object.entries(attrs).forEach(([k,v])=>{
|
||
if(k === 'class') e.className = v;
|
||
else if(k === 'style') e.style.cssText = v;
|
||
else if(k.startsWith('on') && typeof v === 'function') e.addEventListener(k.slice(2), v);
|
||
else e.setAttribute(k, v);
|
||
});
|
||
if(html != null) e.innerHTML = html;
|
||
return e;
|
||
}
|
||
function renderMath(root){
|
||
if(window.renderMathInElement){
|
||
renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false});
|
||
}
|
||
}
|
||
function feedback(elm, ok, text){
|
||
elm.className = 'feedback ' + (ok ? 'ok' : 'fail');
|
||
// innerHTML — внутри нашего проекта, доверяем; нужно для HTML-сущностей вроде ✓ и тегов <b>
|
||
elm.innerHTML = text || (ok ? '✓ Верно!' : '✗ Неверно');
|
||
if(ok) requestAnimationFrame(()=>sparkle(elm));
|
||
}
|
||
|
||
/* ICON SVGs */
|
||
const ICONS = {
|
||
repeat: '<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
|
||
theory: '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
||
algo: '<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
|
||
rule: '<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
|
||
example: '<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
|
||
oral: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||
class: '<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="14" rx="1"/><line x1="3" y1="21" x2="21" y2="21"/><polyline points="7 14 10 11 13 14 17 10"/></svg>',
|
||
home: '<svg class="ic" viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
|
||
prev: '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H20v15H6.5A2.5 2.5 0 0 0 4 19.5z"/><line x1="9" y1="8" x2="15" y2="8"/></svg>',
|
||
};
|
||
|
||
/* Card builder */
|
||
function makeCard(kind, title, num, body){
|
||
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно',class:'Класс',home:'Домашка',prev:'Из прошлых тем'};
|
||
return `<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon ${kind}">${ICONS[kind]}</div>
|
||
<div class="card-title">${labels[kind] || title} ${title && title !== labels[kind] ? '· ' + title : ''}</div>
|
||
${num ? `<div class="card-num">${num}</div>` : ''}
|
||
</div>
|
||
<div class="card-body">${body}</div>
|
||
</div>`;
|
||
}
|
||
function widget(title, badge, helpText, body){
|
||
return `<div class="wg">
|
||
<div class="wg-header">
|
||
<span class="wg-badge">${badge||'INTERACT'}</span>
|
||
<div class="wg-title">${title}</div>
|
||
</div>
|
||
${helpText ? `<div class="wg-help">${helpText}</div>` : ''}
|
||
${body}
|
||
</div>`;
|
||
}
|
||
function secNav(prev, next){
|
||
const PARAMS = {p1:'§1',p2:'§2',p3:'§3',p4:'§4',p5:'§5',p6:'§6',final:'Финал'};
|
||
let h = '<div class="sec-nav">';
|
||
h += prev ? `<button class="btn" onclick="goTo('${prev}')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> ${PARAMS[prev]}</button>` : '<span></span>';
|
||
h += next ? `<button class="btn primary" onclick="goTo('${next}')">${PARAMS[next]} <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>` : '<span></span>';
|
||
h += '</div>';
|
||
return h;
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
INIT
|
||
════════════════════════════════════════════════════════ */
|
||
function init(){
|
||
loadProgress();
|
||
initTheme();
|
||
initSearch();
|
||
buildParaSelector();
|
||
refreshProgressUI();
|
||
initMobileSidebar();
|
||
goTo('p1'); // строит только §1, остальные — лениво при переходе
|
||
setTimeout(()=>achievement('start','Начало пути по корням!'), 800);
|
||
// Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс
|
||
if(window.LS && window.LS.xp){
|
||
window.LS.xp.load().then(function(s){
|
||
if(s && s.xp > STATE.xp){
|
||
STATE.xp = s.xp;
|
||
STATE.level = calcLevel(STATE.xp);
|
||
saveProgress();
|
||
refreshProgressUI();
|
||
if(STATE.current) buildSidebar(STATE.current);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
WAVE 1 — CONFETTI
|
||
════════════════════════════════════════════════════════ */
|
||
const CONFETTI_COLORS = ['#e91e63','#03a9f4','#10b981','#f59e0b','#9333ea','#ec4899','#0891b2','#ea580c'];
|
||
let _confettiRunning = false;
|
||
|
||
function confetti(originX, originY){
|
||
const canvas = document.getElementById('confetti-canvas');
|
||
if(!canvas) return;
|
||
const ctx = canvas.getContext('2d');
|
||
canvas.width = window.innerWidth;
|
||
canvas.height = window.innerHeight;
|
||
|
||
const cx = originX != null ? originX : window.innerWidth / 2;
|
||
const cy = originY != null ? originY : window.innerHeight * 0.4;
|
||
|
||
const count = 70 + Math.floor(Math.random() * 30);
|
||
const particles = [];
|
||
for(let i = 0; i < count; i++){
|
||
const angle = (Math.random() * Math.PI * 2);
|
||
const speed = 3 + Math.random() * 8;
|
||
particles.push({
|
||
x: cx, y: cy,
|
||
vx: Math.cos(angle) * speed,
|
||
vy: Math.sin(angle) * speed - 4,
|
||
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
||
w: 5 + Math.random() * 7,
|
||
h: 3 + Math.random() * 5,
|
||
rot: Math.random() * Math.PI * 2,
|
||
rotV: (Math.random() - 0.5) * 0.18,
|
||
life: 1,
|
||
});
|
||
}
|
||
|
||
let frame = 0;
|
||
const totalFrames = 120;
|
||
|
||
function draw(){
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
particles.forEach(p => {
|
||
ctx.save();
|
||
ctx.globalAlpha = p.life;
|
||
ctx.translate(p.x, p.y);
|
||
ctx.rotate(p.rot);
|
||
ctx.fillStyle = p.color;
|
||
ctx.fillRect(-p.w/2, -p.h/2, p.w, p.h);
|
||
ctx.restore();
|
||
p.x += p.vx;
|
||
p.y += p.vy;
|
||
p.vy += 0.22; // gravity
|
||
p.vx *= 0.99;
|
||
p.rot += p.rotV;
|
||
p.life = Math.max(0, 1 - frame / totalFrames);
|
||
});
|
||
frame++;
|
||
if(frame < totalFrames) requestAnimationFrame(draw);
|
||
else ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
}
|
||
draw();
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
WAVE 1 — SPARKLE
|
||
════════════════════════════════════════════════════════ */
|
||
function sparkle(el){
|
||
const rect = el.getBoundingClientRect();
|
||
const cx = rect.left + rect.width / 2;
|
||
const cy = rect.top + rect.height / 2;
|
||
const colors = ['#e91e63','#f59e0b','#10b981','#03a9f4','#9333ea'];
|
||
for(let i = 0; i < 5; i++){
|
||
const dot = document.createElement('div');
|
||
dot.className = 'sparkle-dot';
|
||
const angle = (i / 5) * Math.PI * 2;
|
||
const dist = 24 + Math.random() * 20;
|
||
dot.style.cssText = `left:${cx - 4}px;top:${cy - 4}px;background:${colors[i]};--sx:${Math.cos(angle)*dist}px;--sy:${Math.sin(angle)*dist - 20}px`;
|
||
document.body.appendChild(dot);
|
||
setTimeout(()=>dot.remove(), 700);
|
||
}
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
WAVE 1 — ACHIEVEMENT CONFETTI (patched into achievement fn)
|
||
════════════════════════════════════════════════════════ */
|
||
/* achievement() is declared earlier; we augment it at runtime via addEventListener */
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
WAVE 1 — MOBILE SIDEBAR
|
||
════════════════════════════════════════════════════════ */
|
||
function openSidebar(){
|
||
const side = document.querySelector('.col-side');
|
||
const overlay = document.getElementById('side-overlay');
|
||
if(side){ side.classList.add('side-open'); }
|
||
if(overlay){ overlay.classList.add('show'); }
|
||
}
|
||
function closeSidebar(){
|
||
const side = document.querySelector('.col-side');
|
||
const overlay = document.getElementById('side-overlay');
|
||
if(side){ side.classList.remove('side-open','open'); }
|
||
if(overlay){ overlay.classList.remove('show'); }
|
||
}
|
||
function initMobileSidebar(){
|
||
const btn = document.getElementById('side-open-btn');
|
||
if(!btn) return;
|
||
function check(){
|
||
btn.style.display = window.innerWidth <= 768 ? 'inline-flex' : 'none';
|
||
}
|
||
check();
|
||
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>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
§ 1. Квадратный корень. Арифметический квадратный корень
|
||
════════════════════════════════════════════════════════ */
|
||
function buildP1(){
|
||
const body = document.getElementById('p1-body');
|
||
body.innerHTML = `
|
||
${makeCard('repeat','Задачи на повторение','1.1–1.3', `
|
||
<p>Прежде чем войти в новую тему — освежите:</p>
|
||
<ul>
|
||
<li>Найдите площадь квадрата, длина стороны которого равна: а) 0,7 см; б) 0,2 м.</li>
|
||
<li>Найдите значение выражения: $7^2$; $(-7)^2$; $1,2^2$; $(-1,2)^2$; $(1/3)^2$; $(-1/3)^2$.</li>
|
||
<li>Сравните значения $a^2$ и $(-a)^2$ если: а) $a$ положительное; б) отрицательное; в) равно 0.</li>
|
||
</ul>
|
||
<p><b>Вывод:</b> квадрат любого числа неотрицателен, причём $a^2 = (-a)^2$.</p>
|
||
`)}
|
||
|
||
${makeCard('theory','Зачем нужен корень',null,`
|
||
<p>Площадь боксёрского ринга равна <b>36 м²</b>. Если ринг квадратный, то какова длина стороны?</p>
|
||
<p>Обозначим сторону через $x$. Тогда $x^2 = 36$. Это уравнение имеет два решения: $x_1 = 6$ и $x_2 = -6$, ведь $6^2 = 36$ и $(-6)^2 = 36$. По смыслу задачи подходит только $x = 6$ м.</p>
|
||
<p>Когда мы решаем $x^2 = 36$ и находим числа, квадраты которых равны 36, — каждое такое число называется <b>квадратным корнем из 36</b>.</p>
|
||
`)}
|
||
|
||
${makeCard('rule','Определение 1',null,`
|
||
<div class="def-box">
|
||
<div class="def-box-title">Квадратный корень</div>
|
||
<b>Квадратным корнем из числа $a$</b> называется число, квадрат которого равен $a$.
|
||
</div>
|
||
<ul>
|
||
<li>Квадратные корни из 0,25 — это 0,5 и −0,5, потому что $0,5^2 = 0,25$ и $(-0,5)^2 = 0,25$.</li>
|
||
<li>Из числа 0 существует только один квадратный корень — это 0.</li>
|
||
<li>Квадратный корень из −100 <b>не существует</b>: квадрат любого числа неотрицателен.</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${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)">
|
||
<!-- 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="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">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>
|
||
`)}
|
||
|
||
${makeCard('rule','Определение 2',null,`
|
||
<div class="def-box">
|
||
<div class="def-box-title">Арифметический квадратный корень</div>
|
||
<b>Арифметическим квадратным корнем</b> из числа $a$ называется <b>неотрицательное число</b>, квадрат которого равен $a$.
|
||
</div>
|
||
<div class="formula-box">$\\sqrt{a} = b$, если $b \\geq 0$ и $b^2 = a$</div>
|
||
<p>Обозначается $\\sqrt{a}$, читается «корень из $a$». Знак $\\sqrt{\\phantom{a}}$ — <b>радикал</b> (от лат. <i>radix</i> — корень). При $a < 0$ выражение $\\sqrt{a}$ не имеет смысла.</p>
|
||
<p>Действие нахождения арифметического корня называют <b>извлечением корня</b>.</p>
|
||
`)}
|
||
|
||
${makeCard('example','Извлечение корней','1.4', `
|
||
<p>Выполните извлечение корня:</p>
|
||
<ul>
|
||
<li>$\\sqrt{121} = 11$, потому что $11^2 = 121$</li>
|
||
<li>$\\sqrt{0{,}49} = 0{,}7$, потому что $0{,}7^2 = 0{,}49$</li>
|
||
<li>$\\sqrt{1/4} = 1/2$, потому что $(1/2)^2 = 1/4$</li>
|
||
<li>$\\sqrt{25} = 5$, $\\sqrt{81} = 9$, $\\sqrt{0} = 0$, $\\sqrt{1} = 1$</li>
|
||
<li>$\\sqrt{0{,}64} = 0{,}8$, $\\sqrt{9/49} = 3/7$</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${widget('Калькулятор √', 'CALC', 'Введите число и узнайте корень. Если корень целый — значок становится зелёным.', `
|
||
<div class="row">
|
||
<span class="lab">Число:</span>
|
||
<input id="calc-n" class="inp num" type="number" min="0" step="any" value="36" placeholder="0">
|
||
<span class="lab">→</span>
|
||
<span class="lab-mono" style="font-size:1.2rem">√n =</span>
|
||
<span id="calc-r" class="lab-mono" style="font-size:1.25rem;color:var(--pri2)">6</span>
|
||
<span id="calc-mark" class="chip ok"><b>✓</b> точное</span>
|
||
</div>
|
||
<div class="row">
|
||
<span class="lab">Проверка:</span>
|
||
<span id="calc-check" class="lab-mono">6² = 36 ✓</span>
|
||
</div>
|
||
`)}
|
||
|
||
${makeCard('algo','Извлечение «в столбик»',null,`
|
||
<p>Для чисел больше 100 можно извлекать корень в столбик:</p>
|
||
<ol style="padding-left:22px">
|
||
<li>Разбейте число на грани по 2 цифры справа налево: $\\overline{5{\\,}1\\!\\!\\,\\,84}$ → 51 | 84.</li>
|
||
<li>Найдите наибольшее число, квадрат которого ≤ первой грани: $7^2 = 49 \\leq 51$ → первая цифра 7.</li>
|
||
<li>Остаток $51 - 49 = 2$, припишите следующую грань: 284.</li>
|
||
<li>Удвойте текущий ответ: $7 \\cdot 2 = 14$. Подберите цифру $b$ так, чтобы $(140 + b) \\cdot b \\leq 284$. Подходит $b=2$: $142 \\cdot 2 = 284$. Значит $\\sqrt{5184} = 72$.</li>
|
||
</ol>
|
||
<details class="spoiler">
|
||
<summary>Попробуйте: $\\sqrt{1296}$</summary>
|
||
<div class="spoiler-body">12 | 96 → $3^2 = 9 \\leq 12$, остаток 3, следом 396; удвоить 3 → 6, $(60+b)b \\leq 396$, $b = 6$: $66 \\cdot 6 = 396$. Ответ: <b>36</b>.</div>
|
||
</details>
|
||
`)}
|
||
|
||
${widget('Извлечение в столбик — пошаговая анимация', 'STEPS', 'Введите положительное число (точный квадрат до 10000). Нажмите «Извлечь по шагам» — увидите классический алгоритм.', `
|
||
<div class="row" style="justify-content:center;gap:10px;flex-wrap:wrap">
|
||
<span class="lab">Число под корнем:</span>
|
||
<input id="cl-n" class="inp num" type="number" value="5184" min="1" max="999999" step="1" style="width:100px;font-size:1.1rem">
|
||
<button class="btn primary" onclick="clStart()">Извлечь по шагам</button>
|
||
<button class="btn" onclick="clPreset(1296)">1296</button>
|
||
<button class="btn" onclick="clPreset(2916)">2916</button>
|
||
<button class="btn" onclick="clPreset(7744)">7744</button>
|
||
</div>
|
||
<div id="cl-workspace" class="cl-workspace"></div>
|
||
<div id="cl-explain" class="cl-explain"></div>
|
||
`)}
|
||
|
||
${widget('Игра «Таблица квадратов 10–99»', 'GAME', 'Показано число — выберите его квадратный корень. Точность важнее скорости, но скорость даёт бонус. Лучший результат сохраняется!', `
|
||
<div class="score-display">
|
||
<div>Раунд: <b id="sq-round">1</b>/10</div>
|
||
<div>Очки: <b id="sq-score">0</b></div>
|
||
<div>Время: <b id="sq-time">0.0</b> с</div>
|
||
<div style="margin-left:auto">Рекорд: <b id="sq-best">—</b></div>
|
||
</div>
|
||
<div id="sq-target" class="squares-target">—</div>
|
||
<div id="sq-options" class="squares-grid"></div>
|
||
<div class="row-c" style="margin-top:12px">
|
||
<button class="btn primary" onclick="squaresStart()">▶ Старт</button>
|
||
<button class="btn" onclick="squaresReset()">Сброс</button>
|
||
</div>
|
||
<div id="sq-feedback" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('oral','Устные вопросы',null,`
|
||
<ol style="padding-left:22px">
|
||
<li>Чему равен арифметический корень из 49?</li>
|
||
<li>Существует ли арифметический корень из числа: а) 0; б) −9; в) 1; г) 100?</li>
|
||
<li>Чему равно $(\\sqrt{7})^2$?</li>
|
||
<li>Какие числа являются квадратными корнями из 144?</li>
|
||
</ol>
|
||
`)}
|
||
|
||
${widget('«Существует или нет?»', 'DRAG', 'Перетащите выражение в нужный столбик или кликните, чтобы выбрать → кликните по зоне. Подкоренное число должно быть неотрицательным.', `
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||
<div>
|
||
<div class="dz-label" style="color:var(--ok)">Существует</div>
|
||
<div id="exists-yes" class="dz" data-target="yes"></div>
|
||
</div>
|
||
<div>
|
||
<div class="dz-label" style="color:var(--fail)">Не существует</div>
|
||
<div id="exists-no" class="dz" data-target="no"></div>
|
||
</div>
|
||
</div>
|
||
<div class="dz-label" style="margin-top:12px">Перетащите отсюда:</div>
|
||
<div id="exists-pool" class="dz" style="border-style:solid;border-color:var(--pri);background:var(--pri-soft)"></div>
|
||
<div class="row-c" style="margin-top:10px">
|
||
<button class="btn primary" onclick="existsCheck()">Проверить</button>
|
||
<button class="btn" onclick="existsReset()">Заново</button>
|
||
</div>
|
||
<div id="exists-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('class','Задания для класса','1.5–1.10',`
|
||
<ol style="padding-left:22px">
|
||
<li>Найдите $\\sqrt{121};\\ \\sqrt{900};\\ \\sqrt{0{,}81};\\ \\sqrt{4/25};\\ \\sqrt{1{,}44}$.</li>
|
||
<li>Объясните, существует ли $\\sqrt{-4}$. Почему?</li>
|
||
<li>Сравните $\\sqrt{169}$ и 13.</li>
|
||
<li>При каком значении $x$ верно равенство $\\sqrt{x} = 9$?</li>
|
||
<li>Найдите все числа, квадратные корни из которых равны: а) 4; б) 0,1; в) 3/7.</li>
|
||
</ol>
|
||
<details class="spoiler">
|
||
<summary>Подсказки</summary>
|
||
<div class="spoiler-body">
|
||
1) 11; 30; 0,9; 2/5; 1,2. 2) Не существует — подкоренное отрицательно. 3) Равны ($\\sqrt{169}=13$). 4) $x=81$. 5) 16; 0,01; 9/49.
|
||
</div>
|
||
</details>
|
||
`)}
|
||
|
||
${widget('Конвейер: x → x² → √(x²)', 'VISUAL', 'Двигайте ползунок. Слева — возведение в квадрат, справа — извлечение корня. Попробуйте отрицательное x — результат всё равно положительный!', `
|
||
<div class="row" style="margin-bottom:14px">
|
||
<span class="lab">x =</span>
|
||
<input id="dual-x" type="range" class="slider" min="-8" max="8" step="0.5" value="3" style="max-width:300px">
|
||
<span id="dual-x-val" class="lab-mono" style="font-size:1.3rem;font-weight:800;color:var(--pri2);min-width:54px;text-align:center">3.0</span>
|
||
</div>
|
||
<div class="dual-pipeline">
|
||
<div class="dual-step dual-input">
|
||
<div class="dual-step-lab">Вход</div>
|
||
<div class="dual-step-val" id="dual-in">3</div>
|
||
<div class="dual-step-cap">x</div>
|
||
</div>
|
||
<div class="dual-arrow">
|
||
<svg viewBox="0 0 60 30"><path d="M 5 15 L 50 15" stroke="#e91e63" stroke-width="2.5" fill="none"/><polyline points="42 8 50 15 42 22" stroke="#e91e63" stroke-width="2.5" fill="none"/></svg>
|
||
<div class="dual-arrow-lab">возвести в квадрат</div>
|
||
</div>
|
||
<div class="dual-step dual-square">
|
||
<div class="dual-step-lab">Площадь</div>
|
||
<svg id="dual-sq-svg" viewBox="0 0 80 80" style="display:block;margin:0 auto"><rect id="dual-sq" x="10" y="10" width="60" height="60" fill="rgba(233,30,99,0.22)" stroke="#e91e63" stroke-width="2"/></svg>
|
||
<div class="dual-step-val small" id="dual-area">9</div>
|
||
<div class="dual-step-cap">x² = $x \\cdot x$</div>
|
||
</div>
|
||
<div class="dual-arrow">
|
||
<svg viewBox="0 0 60 30"><path d="M 5 15 L 50 15" stroke="#03a9f4" stroke-width="2.5" fill="none"/><polyline points="42 8 50 15 42 22" stroke="#03a9f4" stroke-width="2.5" fill="none"/></svg>
|
||
<div class="dual-arrow-lab" style="color:#03a9f4">извлечь корень</div>
|
||
</div>
|
||
<div class="dual-step dual-output">
|
||
<div class="dual-step-lab">Выход</div>
|
||
<div class="dual-step-val" id="dual-out">3</div>
|
||
<div class="dual-step-cap">$\\sqrt{x^2} = |x|$</div>
|
||
</div>
|
||
</div>
|
||
<div id="dual-formula" class="dual-formula">$x = 3$ → $x^2 = 9$ → $\\sqrt{9} = 3$ ✓ (вернулись к исходному)</div>
|
||
<p style="margin-top:8px;font-size:.86rem;color:var(--muted);text-align:center">При <b>отрицательном x</b> результат всё равно <b>положительный</b> — это и есть смысл $\\sqrt{x^2} = |x|$</p>
|
||
`)}
|
||
|
||
${makeCard('home','Домашнее задание','1.11–1.15',`
|
||
<ol style="padding-left:22px">
|
||
<li>Найдите $\\sqrt{225};\\ \\sqrt{1600};\\ \\sqrt{0{,}04};\\ \\sqrt{9/16}$.</li>
|
||
<li>Не вычисляя, объясните, имеет ли смысл $\\sqrt{-1{,}5}$.</li>
|
||
<li>Найдите такое $x$, что $\\sqrt{x} = 0{,}3$.</li>
|
||
<li>Решите уравнение $x^2 = 81$.</li>
|
||
</ol>
|
||
`)}
|
||
|
||
${bossWidget('p1')}
|
||
|
||
${secNav(null, 'p2')}
|
||
`;
|
||
renderMath(body);
|
||
setTimeout(()=>{ initRing(); initCalc(); initExists(); initDual(); }, 50);
|
||
}
|
||
|
||
/* ──── Boxing Ring ──── */
|
||
function initRing(){
|
||
const svg = document.getElementById('ring-svg');
|
||
if(!svg) return;
|
||
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;
|
||
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', 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){
|
||
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';
|
||
}
|
||
}
|
||
update(6);
|
||
|
||
const onMove = (e)=>{
|
||
if(!dragging) return;
|
||
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);
|
||
handle.addEventListener('touchstart', ()=>dragging=true, {passive:true});
|
||
window.addEventListener('mousemove', onMove);
|
||
window.addEventListener('touchmove', onMove, {passive:true});
|
||
window.addEventListener('mouseup', ()=>dragging=false);
|
||
window.addEventListener('touchend', ()=>dragging=false);
|
||
}
|
||
|
||
/* ──── Calc √ ──── */
|
||
function initCalc(){
|
||
const inp = document.getElementById('calc-n');
|
||
const r = document.getElementById('calc-r');
|
||
const mark = document.getElementById('calc-mark');
|
||
const check = document.getElementById('calc-check');
|
||
function upd(){
|
||
const n = +inp.value;
|
||
if(n < 0 || isNaN(n)){
|
||
r.textContent = '—';
|
||
mark.className = 'chip fail'; mark.innerHTML = '<b>×</b> не сущ.';
|
||
check.textContent = 'отрицательное число';
|
||
return;
|
||
}
|
||
const root = Math.sqrt(n);
|
||
const rounded = Math.round(root);
|
||
const exact = Math.abs(rounded*rounded - n) < 1e-9;
|
||
r.textContent = exact ? rounded : root.toFixed(4);
|
||
if(exact){
|
||
mark.className = 'chip ok'; mark.innerHTML = '<b>✓</b> точное';
|
||
check.textContent = rounded + '² = ' + n + ' ✓';
|
||
} else {
|
||
mark.className = 'chip acc'; mark.innerHTML = '≈ приближённое';
|
||
check.textContent = '(' + root.toFixed(4) + ')² ≈ ' + (root*root).toFixed(4);
|
||
}
|
||
}
|
||
inp.addEventListener('input', upd);
|
||
upd();
|
||
}
|
||
|
||
/* ──── Squares Game 10-99 ──── */
|
||
let sqState = null;
|
||
function squaresStart(){
|
||
sqState = { round:1, score:0, t0:performance.now() };
|
||
document.getElementById('sq-best').textContent = isFinite(STATE.squaresBest) ? STATE.squaresBest + ' очк.' : '—';
|
||
squaresNext();
|
||
if(sqState.timer) clearInterval(sqState.timer);
|
||
sqState.timer = setInterval(()=>{
|
||
if(!sqState) return;
|
||
document.getElementById('sq-time').textContent = ((performance.now()-sqState.t0)/1000).toFixed(1);
|
||
}, 100);
|
||
}
|
||
function squaresNext(){
|
||
if(!sqState) return;
|
||
const n = 10 + Math.floor(Math.random() * 90);
|
||
sqState.answer = n;
|
||
document.getElementById('sq-target').textContent = (n*n).toLocaleString('ru');
|
||
document.getElementById('sq-round').textContent = sqState.round;
|
||
document.getElementById('sq-score').textContent = sqState.score;
|
||
// 9 options
|
||
const opts = new Set([n]);
|
||
while(opts.size < 9) opts.add(10 + Math.floor(Math.random()*90));
|
||
const arr = [...opts].sort(()=>Math.random()-0.5);
|
||
const og = document.getElementById('sq-options');
|
||
og.innerHTML = '';
|
||
arr.forEach(o=>{
|
||
const b = el('button', {class:'btn'}, o);
|
||
b.addEventListener('click', ()=>squaresAnswer(o, b));
|
||
og.appendChild(b);
|
||
});
|
||
document.getElementById('sq-feedback').className = 'feedback';
|
||
}
|
||
function squaresAnswer(picked, btn){
|
||
if(!sqState) return;
|
||
const fb = document.getElementById('sq-feedback');
|
||
if(picked === sqState.answer){
|
||
sqState.score += 10;
|
||
btn.classList.add('correct'); btn.style.background='var(--ok)';btn.style.color='#fff';
|
||
feedback(fb, true, '+10 очков. ' + picked + '² = ' + (picked*picked));
|
||
const br = btn.getBoundingClientRect();
|
||
confetti(br.left + br.width/2, br.top + br.height/2);
|
||
} else {
|
||
sqState.score = Math.max(0, sqState.score - 3);
|
||
btn.style.background = 'var(--fail)'; btn.style.color='#fff';
|
||
feedback(fb, false, 'Неверно. Правильно: ' + sqState.answer + '² = ' + (sqState.answer*sqState.answer));
|
||
}
|
||
sqState.round++;
|
||
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));
|
||
const isRecord = total > (isFinite(STATE.squaresBest) ? STATE.squaresBest : 0);
|
||
if(isRecord){
|
||
STATE.squaresBest = total;
|
||
saveProgress();
|
||
achievement('squares','Лучший результат «Таблица квадратов»');
|
||
}
|
||
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;
|
||
document.getElementById('sq-target').textContent = '—';
|
||
document.getElementById('sq-options').innerHTML = '';
|
||
document.getElementById('sq-time').textContent = '0.0';
|
||
}
|
||
|
||
/* ──── Exists drag ──── */
|
||
const EXISTS_ITEMS = [
|
||
{expr:'√121', val:'yes'}, {expr:'√(-25)', val:'no'}, {expr:'√0', val:'yes'},
|
||
{expr:'√0.49', val:'yes'}, {expr:'√(-100)', val:'no'}, {expr:'√(-0.1)', val:'no'},
|
||
{expr:'√169', val:'yes'}, {expr:'√(-9)', val:'no'},
|
||
];
|
||
let _existsSelected = null;
|
||
function initExists(){
|
||
const pool = document.getElementById('exists-pool');
|
||
if(!pool) return;
|
||
existsReset();
|
||
// HTML5 drag-and-drop
|
||
['exists-yes','exists-no','exists-pool'].forEach(id=>{
|
||
const dz = document.getElementById(id);
|
||
dz.addEventListener('dragover', e=>{ e.preventDefault(); dz.classList.add('over'); });
|
||
dz.addEventListener('dragleave', ()=>dz.classList.remove('over'));
|
||
dz.addEventListener('drop', e=>{
|
||
e.preventDefault(); dz.classList.remove('over');
|
||
const itemId = e.dataTransfer.getData('text/plain');
|
||
const item = document.getElementById(itemId);
|
||
if(item) dz.appendChild(item);
|
||
});
|
||
// Click-режим: если выбран элемент — клик по зоне переносит его
|
||
dz.addEventListener('click', e=>{
|
||
if(!_existsSelected) return;
|
||
if(e.target.closest('.drag-item') && e.target.closest('.drag-item') !== _existsSelected) return;
|
||
dz.appendChild(_existsSelected);
|
||
_existsSelected.classList.remove('selected');
|
||
_existsSelected = null;
|
||
});
|
||
});
|
||
}
|
||
function existsReset(){
|
||
const pool = document.getElementById('exists-pool');
|
||
pool.innerHTML = '';
|
||
document.getElementById('exists-yes').innerHTML='';
|
||
document.getElementById('exists-no').innerHTML='';
|
||
_existsSelected = null;
|
||
EXISTS_ITEMS.forEach((it,i)=>{
|
||
const d = el('div', {class:'drag-item', id:'exi-'+i, draggable:'true'}, it.expr);
|
||
d.dataset.val = it.val;
|
||
d.addEventListener('dragstart', e=>{
|
||
e.dataTransfer.setData('text/plain', d.id);
|
||
d.classList.add('dragging');
|
||
});
|
||
d.addEventListener('dragend', ()=>d.classList.remove('dragging'));
|
||
// Click — выбор / снятие выбора
|
||
d.addEventListener('click', ev=>{
|
||
ev.stopPropagation();
|
||
if(_existsSelected === d){
|
||
d.classList.remove('selected');
|
||
_existsSelected = null;
|
||
return;
|
||
}
|
||
document.querySelectorAll('.drag-item.selected').forEach(x=>x.classList.remove('selected'));
|
||
d.classList.add('selected');
|
||
_existsSelected = d;
|
||
});
|
||
pool.appendChild(d);
|
||
});
|
||
document.getElementById('exists-fb').className='feedback';
|
||
}
|
||
function existsCheck(){
|
||
let correct=0, total=EXISTS_ITEMS.length, miss=0;
|
||
document.querySelectorAll('#exists-yes .drag-item').forEach(d=>{
|
||
if(d.dataset.val === 'yes') correct++; else miss++;
|
||
});
|
||
document.querySelectorAll('#exists-no .drag-item').forEach(d=>{
|
||
if(d.dataset.val === 'no') correct++; else miss++;
|
||
});
|
||
const fb = document.getElementById('exists-fb');
|
||
if(correct === total){
|
||
feedback(fb, true, 'Все ' + total + ' правильно! Корень из отриц. не существует.');
|
||
achievement('exists','Сортировка корней');
|
||
bumpProgress('p1', 8);
|
||
confetti();
|
||
} else {
|
||
feedback(fb, false, 'Правильно ' + correct + ' из ' + total + ' (с учётом разложенных). Проверьте знаки.');
|
||
}
|
||
}
|
||
|
||
/* ──── Dual x² ↔ √x ──── */
|
||
function initDual(){
|
||
const x = document.getElementById('dual-x');
|
||
if(!x) return;
|
||
const xv = document.getElementById('dual-x-val');
|
||
const inEl = document.getElementById('dual-in');
|
||
const sq = document.getElementById('dual-sq');
|
||
const area = document.getElementById('dual-area');
|
||
const out = document.getElementById('dual-out');
|
||
const formula = document.getElementById('dual-formula');
|
||
function fmt(n){ return Number.isInteger(n) ? String(n) : n.toFixed(1); }
|
||
function upd(){
|
||
const v = +x.value;
|
||
const sq2 = v * v;
|
||
const root = Math.sqrt(sq2);
|
||
xv.textContent = fmt(v);
|
||
if(inEl) inEl.textContent = fmt(v);
|
||
// Размер квадрата: |v|·8, max 60
|
||
const sz = Math.min(60, Math.abs(v) * 8 + 2);
|
||
if(sq){
|
||
sq.setAttribute('x', 40 - sz/2);
|
||
sq.setAttribute('y', 40 - sz/2);
|
||
sq.setAttribute('width', sz);
|
||
sq.setAttribute('height', sz);
|
||
}
|
||
if(area) area.textContent = fmt(sq2);
|
||
if(out) out.textContent = fmt(root);
|
||
// Подсветка: если v < 0 — показать что вернулось |v|, а не v
|
||
if(formula){
|
||
if(v < 0){
|
||
formula.innerHTML = `$x = ${fmt(v)}$ → $x^2 = ${fmt(sq2)}$ → $\\sqrt{${fmt(sq2)}} = ${fmt(root)}$ ≠ ${fmt(v)} → это <b style="color:var(--warn)">|x| = ${fmt(root)}</b>`;
|
||
formula.classList.add('mod-active');
|
||
} else if(v === 0){
|
||
formula.innerHTML = `$x = 0$ → $x^2 = 0$ → $\\sqrt{0} = 0$ — особый случай`;
|
||
formula.classList.remove('mod-active');
|
||
} else {
|
||
formula.innerHTML = `$x = ${fmt(v)}$ → $x^2 = ${fmt(sq2)}$ → $\\sqrt{${fmt(sq2)}} = ${fmt(root)}$ ✓ (вернулись к исходному)`;
|
||
formula.classList.remove('mod-active');
|
||
}
|
||
if(window.renderMathInElement){
|
||
try{ renderMathInElement(formula, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){}
|
||
}
|
||
}
|
||
}
|
||
x.addEventListener('input', upd);
|
||
upd();
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
§ 2. Иррациональные числа. Действительные числа
|
||
════════════════════════════════════════════════════════ */
|
||
function buildP2(){
|
||
const body = document.getElementById('p2-body');
|
||
body.innerHTML = `
|
||
${makeCard('repeat','Задачи на повторение',null,`
|
||
<p>Прежде чем двигаться дальше — освежите числа разных видов:</p>
|
||
<ul>
|
||
<li>Каким числовым множествам принадлежат: 3; −5; 1/2; 0; 0,(3); −2,4?</li>
|
||
<li>Запишите число 1/3 в виде десятичной дроби. Будет ли запись конечной?</li>
|
||
<li>Чему равен $\\sqrt{2}$ с точностью до 0,01?</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${makeCard('theory','От натуральных — к действительным',null,`
|
||
<p>В математике мы знакомились с разными числами поэтапно:</p>
|
||
<ul>
|
||
<li><b>ℕ</b> — натуральные: 1, 2, 3, ... (счёт).</li>
|
||
<li><b>ℤ</b> — целые: ..., −2, −1, 0, 1, 2, ... (добавили 0 и отрицательные).</li>
|
||
<li><b>ℚ</b> — рациональные: $m/n$, где $m \\in \\mathbb{Z}$, $n \\in \\mathbb{N}$ (добавили дроби). Любое рациональное представимо в виде <b>конечной или бесконечной периодической</b> десятичной дроби.</li>
|
||
</ul>
|
||
<p>Но не все числа — рациональные! Например, $\\sqrt{2} \\approx 1{,}4142135...$ — десятичная дробь, у которой <b>нет периода</b>.</p>
|
||
<div class="def-box">
|
||
<div class="def-box-title">Иррациональное число</div>
|
||
Это бесконечная <b>непериодическая</b> десятичная дробь. Их множество обозначается <b>I</b>.
|
||
Примеры: $\\sqrt{2}$, $\\sqrt{3}$, $\\sqrt{5}$, $\\pi \\approx 3{,}14159...$, $e \\approx 2{,}71828...$
|
||
</div>
|
||
<div class="def-box">
|
||
<div class="def-box-title">Действительные числа ℝ</div>
|
||
Объединение рациональных и иррациональных: $\\mathbb{R} = \\mathbb{Q} \\cup I$. На координатной прямой каждой точке соответствует ровно одно действительное число.
|
||
</div>
|
||
<div class="formula-box">$\\mathbb{N} \\subset \\mathbb{Z} \\subset \\mathbb{Q} \\subset \\mathbb{R}$</div>
|
||
`)}
|
||
|
||
${widget('Иерархия множеств ℕ ⊂ ℤ ⊂ ℚ ⊂ ℝ', 'VISUAL', 'Наведите курсор на каждое множество, чтобы увидеть, какие числа в него входят.', `
|
||
<div class="sets-vis">
|
||
<div class="set-circle set-R" data-set="R">ℝ</div>
|
||
<div class="set-circle set-Q" data-set="Q">ℚ</div>
|
||
<div class="set-circle set-Z" data-set="Z">ℤ</div>
|
||
<div class="set-circle set-N" data-set="N">ℕ</div>
|
||
<div id="set-info" class="set-info"></div>
|
||
</div>
|
||
<div class="row-c" style="margin-top:8px;font-size:.84rem;color:var(--muted)">
|
||
Кликните на круг для информации
|
||
</div>
|
||
`)}
|
||
|
||
${widget('«Какое это число?» — сортировка', 'GAME', 'Перетащите числа в правильные коробки. Помните: натуральное — это 1, 2, 3...; целое включает 0 и отрицательные; рациональное — это m/n.', `
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||
<div><div class="dz-label">Натуральные ℕ</div><div id="cls-n" class="dz" data-cls="N"></div></div>
|
||
<div><div class="dz-label">Целые (не натур.)</div><div id="cls-z" class="dz" data-cls="Z"></div></div>
|
||
<div><div class="dz-label">Рацион. (не целые)</div><div id="cls-q" class="dz" data-cls="Q"></div></div>
|
||
<div><div class="dz-label">Иррациональные I</div><div id="cls-i" class="dz" data-cls="I"></div></div>
|
||
</div>
|
||
<div class="dz-label" style="margin-top:12px">Перетащите отсюда:</div>
|
||
<div id="cls-pool" class="dz" style="border-style:solid;border-color:var(--pri);background:var(--pri-soft)"></div>
|
||
<div class="row-c" style="margin-top:10px">
|
||
<button class="btn primary" onclick="clsCheck()">Проверить</button>
|
||
<button class="btn" onclick="clsReset()">Заново</button>
|
||
</div>
|
||
<div id="cls-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('example','Десятичные представления',null,`
|
||
<p>Чтобы понять, рационально ли число, представьте его в виде десятичной дроби:</p>
|
||
<ul>
|
||
<li>$\\frac{2}{13} = 2 : 13 = 0{,}(153846)...$ — <b>периодическая</b>, значит, рациональное.</li>
|
||
<li>$\\frac{1}{3} = 0{,}(3)$ — рациональное, период «3».</li>
|
||
<li>$\\sqrt{5} = 2{,}2360679...$ — <b>непериодическая</b>, значит, иррациональное.</li>
|
||
<li>$\\pi = 3{,}1415926...$ — иррациональное (доказано в 18 в.).</li>
|
||
</ul>
|
||
<p>Сравним $\\pi$ и $\\frac{22}{7}$ (число Архимеда): $\\pi = 3{,}1415...$, а $\\frac{22}{7} = 3{,}1428...$ Цифра тысячных у $\\frac{22}{7}$ больше → $\\pi < \\frac{22}{7}$.</p>
|
||
`)}
|
||
|
||
${widget('Дробь ⇄ периодическая десятичная', 'CALC', 'Введите числитель и знаменатель — увидите десятичную запись с подсветкой периода.', `
|
||
<div class="row">
|
||
<span class="lab">Дробь:</span>
|
||
<input id="fr-num" class="inp num" type="number" value="2" style="width:70px">
|
||
<span class="lab-mono" style="font-size:1.4rem">/</span>
|
||
<input id="fr-den" class="inp num" type="number" value="13" style="width:70px" min="1">
|
||
</div>
|
||
<div style="margin-top:14px;padding:12px;background:var(--card);border-radius:9px;border:1px solid var(--border);font-family:'JetBrains Mono',monospace;font-size:1.05rem">
|
||
<span id="fr-out">0,(153846)</span>
|
||
</div>
|
||
<div id="fr-info" style="margin-top:8px;font-size:.84rem;color:var(--muted)"></div>
|
||
`)}
|
||
|
||
${makeCard('rule','Координатная прямая',null,`
|
||
<p>На координатной прямой каждой точке отвечает одно действительное число и наоборот. Поэтому действительная прямая называется ещё <b>числовой прямой</b>.</p>
|
||
<p>Найти приближённое расположение $\\sqrt{a}$ на прямой: нужно найти такие соседние целые $n$ и $n+1$, что $n^2 \\leq a < (n+1)^2$. Тогда $\\sqrt{a}$ между ними.</p>
|
||
<p>Например, $\\sqrt{90}$: $9^2 = 81$, $10^2 = 100$. Значит $9 < \\sqrt{90} < 10$, ближе к 9,5 (потому что $9{,}5^2 = 90{,}25$).</p>
|
||
`)}
|
||
|
||
${widget('Числовая прямая с корнями', 'VISUAL', 'Нажимайте кнопки, чтобы поставить точки √n и π на прямую. Точные десятичные значения — в подсказках.', `
|
||
<div id="nl-line" style="position:relative;height:140px;background:var(--card);border:1px solid var(--border);border-radius:9px;margin-bottom:14px"></div>
|
||
<div class="row-c">
|
||
<button class="btn" onclick="nlAdd(Math.sqrt(2),'√2')">√2</button>
|
||
<button class="btn" onclick="nlAdd(Math.sqrt(3),'√3')">√3</button>
|
||
<button class="btn" onclick="nlAdd(Math.sqrt(5),'√5')">√5</button>
|
||
<button class="btn" onclick="nlAdd(Math.sqrt(7),'√7')">√7</button>
|
||
<button class="btn" onclick="nlAdd(Math.PI,'π')">π</button>
|
||
<button class="btn" onclick="nlAdd(Math.sqrt(15),'√15')">√15</button>
|
||
<button class="btn" onclick="nlClear()">Очистить</button>
|
||
</div>
|
||
<div id="nl-info" style="margin-top:10px;font-size:.86rem;color:var(--muted)"></div>
|
||
`)}
|
||
|
||
${widget('Доказательство: √2 иррационально', 'PROOF', 'Раскрывайте шаги по очереди. Это классическое доказательство «от противного».', `
|
||
<details class="spoiler"><summary>Шаг 1. Предположение</summary><div class="spoiler-body">
|
||
Предположим противное: $\\sqrt{2}$ рационально. Тогда $\\sqrt{2} = \\frac{p}{q}$, где $\\frac{p}{q}$ — несократимая дробь (общий множитель сокращён).
|
||
</div></details>
|
||
<details class="spoiler"><summary>Шаг 2. Возводим в квадрат</summary><div class="spoiler-body">
|
||
$2 = \\frac{p^2}{q^2}$, значит $p^2 = 2q^2$. Это значит, что $p^2$ — чётное.
|
||
</div></details>
|
||
<details class="spoiler"><summary>Шаг 3. Значит, p чётное</summary><div class="spoiler-body">
|
||
Если $p^2$ чётное, то и сам $p$ чётный (квадрат нечётного — нечётен). Запишем $p = 2k$.
|
||
</div></details>
|
||
<details class="spoiler"><summary>Шаг 4. И q тоже чётное?</summary><div class="spoiler-body">
|
||
Подставим: $(2k)^2 = 2q^2 \\implies 4k^2 = 2q^2 \\implies q^2 = 2k^2$. Значит $q^2$ чётно, и $q$ — чётно.
|
||
</div></details>
|
||
<details class="spoiler"><summary>Шаг 5. Противоречие!</summary><div class="spoiler-body">
|
||
Получили: и $p$, и $q$ — чётные. Но мы взяли дробь <b>несократимой</b>! Противоречие.
|
||
<br><b>Вывод:</b> $\\sqrt{2}$ не может быть представлен как $\\frac{p}{q}$, то есть он иррационален. ▢
|
||
<br><br><span style="color:var(--ok);font-weight:700">Аналогично доказывается иррациональность $\\sqrt{3}, \\sqrt{5}, \\sqrt{7}, ...$</span>
|
||
</div></details>
|
||
`)}
|
||
|
||
${makeCard('class','Задания для класса','1.60–1.78',`
|
||
<ol style="padding-left:22px">
|
||
<li>Из чисел −1,8; 12; 4/7; √5; 0; 2,13; −13; −3/11; 78; π; −6,7 выберите: а) натуральные; б) целые; в) рациональные; г) иррациональные.</li>
|
||
<li>Сравните: а) $\\sqrt{26}$ и 5; б) $\\sqrt{3}$ и 1,7; в) π и 3,141.</li>
|
||
<li>Найдите целое число, находящееся на прямой между $\\sqrt{73}$ и $\\sqrt{92}$.</li>
|
||
<li>Назовите два последовательных целых, между которыми $\\sqrt{15}$.</li>
|
||
</ol>
|
||
<details class="spoiler">
|
||
<summary>Подсказки</summary>
|
||
<div class="spoiler-body">1а) 12; 78 — натуральные. 1б) −13; 0 — целые. 1в) 4/7; 2,13; −3/11 — рациональные. 1г) √5; π — иррациональные.
|
||
<br>2а) $\\sqrt{26} > 5$ (т.к. $5^2=25 < 26$). 2б) $\\sqrt{3} = 1{,}732... > 1{,}7$. 2в) $\\pi > 3{,}141$.
|
||
<br>3) 9 (т.к. $\\sqrt{73} \\approx 8{,}54$, $\\sqrt{92} \\approx 9{,}59$).
|
||
<br>4) 3 и 4 (т.к. $9 < 15 < 16$).</div>
|
||
</details>
|
||
`)}
|
||
|
||
${widget('«Кто рациональнее?»', 'GAME', 'Быстрый тест — за 30 секунд отметьте все иррациональные числа. Промахи — штраф.', `
|
||
<div class="score-display">
|
||
<div>Оценено: <b id="ri-cnt">0</b>/8</div>
|
||
<div>Очки: <b id="ri-score">0</b></div>
|
||
<button class="btn small primary" onclick="riStart()">Старт</button>
|
||
</div>
|
||
<div id="ri-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:10px"></div>
|
||
<div id="ri-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('home','Домашнее задание','1.79–1.85',`
|
||
<ol style="padding-left:22px">
|
||
<li>Верно ли: а) 8 — рациональное; б) √15 — иррациональное; в) 0 — натуральное; г) 2/7 — действительное?</li>
|
||
<li>Какие из чисел можно представить в виде бесконечной периодической дроби: 7/11; √5; 3/19; √4,9?</li>
|
||
<li>Сравните: а) $\\sqrt{35}$ и 6; б) $\\sqrt{2}$ и 1,4.</li>
|
||
<li>Зная, что $1{,}4 < \\sqrt{2} < 1{,}5$ и $1{,}7 < \\sqrt{3} < 1{,}8$, оцените значение выражения $2\\sqrt{2} + \\sqrt{3}$.</li>
|
||
</ol>
|
||
`)}
|
||
|
||
${bossWidget('p2')}
|
||
|
||
${secNav('p1', 'p3')}
|
||
`;
|
||
renderMath(body);
|
||
setTimeout(()=>{ initSets(); initClassify(); initFraction(); initNumLine(); }, 50);
|
||
}
|
||
|
||
/* ──── Sets visualization ──── */
|
||
const SETS_INFO = {
|
||
N: '<b>ℕ — Натуральные:</b> 1, 2, 3, 4, ... — используются для счёта.',
|
||
Z: '<b>ℤ — Целые:</b> ..., −2, −1, 0, 1, 2, ... — добавили 0 и отрицательные.',
|
||
Q: '<b>ℚ — Рациональные:</b> m/n, n≠0. Включают целые. Например: 1/2; −0,75; 1,(3); 5.',
|
||
R: '<b>ℝ — Действительные:</b> все рациональные + иррациональные (как π, √2).'
|
||
};
|
||
function initSets(){
|
||
const info = document.getElementById('set-info');
|
||
if(!info) return;
|
||
document.querySelectorAll('.set-circle').forEach(c=>{
|
||
c.addEventListener('mouseenter', ()=>{ info.innerHTML = SETS_INFO[c.dataset.set]; info.classList.add('show'); });
|
||
c.addEventListener('click', ()=>{ info.innerHTML = SETS_INFO[c.dataset.set]; info.classList.add('show'); });
|
||
});
|
||
}
|
||
|
||
/* ──── Classify game ──── */
|
||
const CLASSIFY_ITEMS = [
|
||
{n:'12',cls:'N'}, {n:'78',cls:'N'},
|
||
{n:'−13',cls:'Z'}, {n:'0',cls:'Z'},
|
||
{n:'2,13',cls:'Q'}, {n:'4/7',cls:'Q'}, {n:'−3/11',cls:'Q'}, {n:'−1,8',cls:'Q'},
|
||
{n:'√5',cls:'I'}, {n:'π',cls:'I'}, {n:'√15',cls:'I'},
|
||
];
|
||
function initClassify(){
|
||
const pool = document.getElementById('cls-pool');
|
||
if(!pool) return;
|
||
clsReset();
|
||
['cls-n','cls-z','cls-q','cls-i','cls-pool'].forEach(id=>{
|
||
const dz = document.getElementById(id);
|
||
dz.addEventListener('dragover', e=>{ e.preventDefault(); dz.classList.add('over'); });
|
||
dz.addEventListener('dragleave', ()=>dz.classList.remove('over'));
|
||
dz.addEventListener('drop', e=>{
|
||
e.preventDefault(); dz.classList.remove('over');
|
||
const dragId = e.dataTransfer.getData('text/plain');
|
||
const it = document.getElementById(dragId);
|
||
if(it) dz.appendChild(it);
|
||
});
|
||
});
|
||
}
|
||
function clsReset(){
|
||
['cls-n','cls-z','cls-q','cls-i','cls-pool'].forEach(id=>document.getElementById(id).innerHTML='');
|
||
const pool = document.getElementById('cls-pool');
|
||
CLASSIFY_ITEMS.forEach((it,i)=>{
|
||
const d = el('div', {class:'drag-item', id:'cli-'+i, draggable:'true'}, it.n);
|
||
d.dataset.cls = it.cls;
|
||
d.addEventListener('dragstart', e=>{ e.dataTransfer.setData('text/plain', d.id); d.classList.add('dragging'); });
|
||
d.addEventListener('dragend', ()=>d.classList.remove('dragging'));
|
||
pool.appendChild(d);
|
||
});
|
||
document.getElementById('cls-fb').className='feedback';
|
||
}
|
||
function clsCheck(){
|
||
let ok=0, wrong=0;
|
||
document.querySelectorAll('.dz[data-cls]').forEach(dz=>{
|
||
const target = dz.dataset.cls;
|
||
dz.querySelectorAll('.drag-item').forEach(d=>{
|
||
if(d.dataset.cls === target){ ok++; d.style.background='var(--ok)'; }
|
||
else { wrong++; d.style.background='var(--fail)'; }
|
||
});
|
||
});
|
||
const fb = document.getElementById('cls-fb');
|
||
if(wrong === 0 && ok === CLASSIFY_ITEMS.length){
|
||
feedback(fb, true, 'Идеально! ' + ok + '/' + CLASSIFY_ITEMS.length);
|
||
achievement('classify','Классифицировал числа');
|
||
bumpProgress('p2', 12);
|
||
confetti();
|
||
} else {
|
||
feedback(fb, false, 'Правильно: ' + ok + ', ошибок: ' + wrong);
|
||
}
|
||
}
|
||
|
||
/* ──── Fraction to decimal ──── */
|
||
function initFraction(){
|
||
const n = document.getElementById('fr-num');
|
||
const d = document.getElementById('fr-den');
|
||
if(!n) return;
|
||
function upd(){
|
||
const a = +n.value, b = +d.value;
|
||
const out = document.getElementById('fr-out');
|
||
const info = document.getElementById('fr-info');
|
||
if(!b){ out.textContent = '— деление на 0 —'; info.textContent=''; return; }
|
||
// long division detecting period
|
||
const isNeg = (a < 0) !== (b < 0);
|
||
const aa = Math.abs(a), bb = Math.abs(b);
|
||
let intPart = Math.floor(aa / bb);
|
||
let rem = aa - intPart * bb;
|
||
let digits = '';
|
||
const remMap = {};
|
||
let periodStart = -1, maxIter = 50;
|
||
let i = 0;
|
||
while(rem !== 0 && i < maxIter){
|
||
if(remMap[rem] !== undefined){ periodStart = remMap[rem]; break; }
|
||
remMap[rem] = i;
|
||
rem *= 10;
|
||
digits += Math.floor(rem / bb);
|
||
rem = rem - Math.floor(rem / bb) * bb;
|
||
i++;
|
||
}
|
||
let str = (isNeg ? '−' : '') + intPart;
|
||
if(digits){
|
||
str += ',';
|
||
if(periodStart >= 0){
|
||
str += digits.slice(0, periodStart) + '(' + digits.slice(periodStart) + ')';
|
||
info.innerHTML = '<b>Периодическая</b> десятичная дробь → число <b>рациональное</b>.';
|
||
} else if(i === maxIter){
|
||
str += digits + '...';
|
||
info.innerHTML = 'Период не найден в 50 знаках. Возможно, длинный период.';
|
||
} else {
|
||
info.innerHTML = '<b>Конечная</b> десятичная дробь → число <b>рациональное</b>.';
|
||
}
|
||
} else {
|
||
info.innerHTML = '<b>Целое</b> → рациональное.';
|
||
}
|
||
out.textContent = str;
|
||
}
|
||
n.addEventListener('input', upd);
|
||
d.addEventListener('input', upd);
|
||
upd();
|
||
}
|
||
|
||
/* ──── Number line ──── */
|
||
const NL_POINTS = [];
|
||
function initNumLine(){
|
||
const line = document.getElementById('nl-line');
|
||
if(!line) return;
|
||
nlClear();
|
||
}
|
||
function nlRender(){
|
||
const line = document.getElementById('nl-line');
|
||
line.innerHTML = '';
|
||
// axis 0..15 — нижняя часть, чтобы сверху было место под подписи в несколько уровней
|
||
const AXIS_Y = 100;
|
||
const axis = el('div', {style:`position:absolute;top:${AXIS_Y}px;left:3%;right:3%;height:2px;background:var(--text)`});
|
||
line.appendChild(axis);
|
||
// ticks 0..15
|
||
const lo = 0, hi = 15;
|
||
for(let i = lo; i <= hi; i++){
|
||
const x = 3 + (i - lo) / (hi - lo) * 94;
|
||
const t = el('div', {style:`position:absolute;top:${AXIS_Y-6}px;left:${x}%;width:2px;height:14px;background:var(--text);transform:translateX(-50%)`});
|
||
line.appendChild(t);
|
||
const lab = el('div', {style:`position:absolute;top:${AXIS_Y+12}px;left:${x}%;transform:translateX(-50%);font-size:.74rem;font-family:'JetBrains Mono',monospace;color:var(--muted)`}, ''+i);
|
||
line.appendChild(lab);
|
||
}
|
||
// Сортируем по x для расчёта уровней без перекрытия
|
||
const w = line.clientWidth || 600;
|
||
const labelHalfPxApprox = 22; // полу-ширина подписи в пикселях
|
||
const placed = []; // {xPct, level}
|
||
const sorted = NL_POINTS.map((p,i)=>({...p, _i:i})).sort((a,b)=>a.v-b.v);
|
||
sorted.forEach(p=>{
|
||
const xPct = 3 + (p.v - lo) / (hi - lo) * 94;
|
||
// Найти минимальный уровень без перекрытия с placed
|
||
let level = 0;
|
||
while(true){
|
||
const conflict = placed.some(q=>{
|
||
if(q.level !== level) return false;
|
||
const dxPx = Math.abs(q.xPct - xPct) / 100 * w;
|
||
return dxPx < (labelHalfPxApprox * 2 + 4);
|
||
});
|
||
if(!conflict) break;
|
||
level++;
|
||
if(level > 8) break;
|
||
}
|
||
placed.push({xPct, level});
|
||
p._level = level;
|
||
});
|
||
sorted.forEach(p=>{
|
||
const xPct = 3 + (p.v - lo) / (hi - lo) * 94;
|
||
const pt = el('div', {style:`position:absolute;top:${AXIS_Y-6}px;left:${xPct}%;width:14px;height:14px;background:var(--pri);border-radius:50%;transform:translateX(-50%);border:2.5px solid var(--card);box-shadow:0 0 0 2px var(--pri);cursor:pointer;z-index:2`});
|
||
pt.title = p.lab + ' ≈ ' + p.v.toFixed(4);
|
||
line.appendChild(pt);
|
||
// Подпись сверху, нескольких уровней; линия-выноска вниз к точке
|
||
const labY = 4 + p._level * 20;
|
||
const stemTop = labY + 18;
|
||
const stemHeight = AXIS_Y - stemTop - 2;
|
||
if(stemHeight > 0){
|
||
line.appendChild(el('div', {style:`position:absolute;top:${stemTop}px;left:${xPct}%;width:1px;height:${stemHeight}px;background:var(--pri);opacity:.45;transform:translateX(-50%);z-index:1`}));
|
||
}
|
||
const lab = el('div', {style:`position:absolute;top:${labY}px;left:${xPct}%;transform:translateX(-50%);font-size:.78rem;font-weight:700;color:var(--pri);background:var(--card);padding:2px 7px;border-radius:5px;border:1px solid var(--pri);font-family:'JetBrains Mono',monospace;white-space:nowrap;z-index:3;box-shadow:0 1px 4px rgba(0,0,0,.08)`}, p.lab);
|
||
line.appendChild(lab);
|
||
});
|
||
}
|
||
function nlAdd(v, lab){
|
||
if(NL_POINTS.some(p=>p.lab === lab)) return;
|
||
NL_POINTS.push({v, lab});
|
||
nlRender();
|
||
const info = document.getElementById('nl-info');
|
||
info.innerHTML = '<b>' + lab + '</b> ≈ ' + v.toFixed(6) + (lab.startsWith('√') || lab === 'π' ? ' — иррациональное' : '');
|
||
bumpProgress('p2', 2);
|
||
}
|
||
function nlClear(){
|
||
NL_POINTS.length = 0;
|
||
nlRender();
|
||
document.getElementById('nl-info').innerHTML = '';
|
||
}
|
||
|
||
/* ──── Rationality game ──── */
|
||
const RAT_ITEMS = [
|
||
{s:'√4', i:false}, {s:'√3', i:true}, {s:'π', i:true}, {s:'1/3', i:false},
|
||
{s:'√25', i:false}, {s:'√7', i:true}, {s:'−2,5', i:false}, {s:'√11', i:true},
|
||
];
|
||
function riStart(){
|
||
const g = document.getElementById('ri-grid');
|
||
g.innerHTML = '';
|
||
let cnt=0, score=0;
|
||
RAT_ITEMS.forEach((it,i)=>{
|
||
const b = el('button', {class:'btn'}, it.s);
|
||
b.style.fontSize='1.05rem';
|
||
b.style.fontFamily="'JetBrains Mono',monospace";
|
||
b.addEventListener('click', ()=>{
|
||
if(b.dataset.done) return;
|
||
b.dataset.done = '1';
|
||
if(it.i){ // должно быть иррациональным
|
||
b.style.background='var(--ok)';b.style.color='#fff';
|
||
score+=10;
|
||
} else {
|
||
b.style.background='var(--fail)';b.style.color='#fff';
|
||
score=Math.max(0,score-5);
|
||
}
|
||
cnt++;
|
||
document.getElementById('ri-cnt').textContent = cnt;
|
||
document.getElementById('ri-score').textContent = score;
|
||
if(cnt === RAT_ITEMS.length){
|
||
const fb = document.getElementById('ri-fb');
|
||
const right = RAT_ITEMS.filter(x=>x.i).length;
|
||
feedback(fb, score >= right*8, 'Готово! Очки: ' + score);
|
||
if(score >= right*8){
|
||
achievement('rat','Распознал иррациональные');
|
||
bumpProgress('p2', 10);
|
||
}
|
||
}
|
||
});
|
||
g.appendChild(b);
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
§ 3. Свойства квадратных корней
|
||
════════════════════════════════════════════════════════ */
|
||
function buildP3(){
|
||
const body = document.getElementById('p3-body');
|
||
body.innerHTML = `
|
||
${makeCard('repeat','Задачи на повторение','1.93–1.95',`
|
||
<ul>
|
||
<li>Найдите значение: $0{,}5^6 \\cdot 2^6$; $(1/3)^{-7} \\cdot 3^{-7}$; $6^5 / 12^5$.</li>
|
||
<li>Вычислите: $|-12| + |5{,}5| \\cdot |-0{,}7|$.</li>
|
||
<li>Верно ли, что $|a| = a$? Что $|-a| = a$? — Зависит от знака $a$.</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${makeCard('theory','Зачем нужны свойства корней',null,`
|
||
<p>Какова сторона квадратного газона, если его площадь равна площади прямоугольника со сторонами $a$ и $b$?</p>
|
||
<p>Площадь прямоугольника $S = a \\cdot b$. Сторона квадрата той же площади — $\\sqrt{ab}$.</p>
|
||
<p>А если есть две квадратные плитки со сторонами $a$ и $b$, и нужна одна большая плитка, площадь которой равна сумме их площадей? Площадь $S = a^2 + b^2$, сторона $\\sqrt{a^2 + b^2}$.</p>
|
||
<p>Чтобы вычислять такие выражения, нужны <b>свойства корней</b>. Выражение под знаком корня называется <b>подкоренным</b>.</p>
|
||
`)}
|
||
|
||
${makeCard('rule','Свойство 1: корень из произведения',null,`
|
||
<div class="formula-box">$\\sqrt{a \\cdot b} = \\sqrt{a} \\cdot \\sqrt{b}$, где $a \\geq 0$, $b \\geq 0$</div>
|
||
<details class="spoiler"><summary>Доказательство</summary><div class="spoiler-body">
|
||
Пусть $\\sqrt{a} \\cdot \\sqrt{b} = t$. Покажем: $\\sqrt{ab} = t$, то есть $t \\geq 0$ и $t^2 = ab$.
|
||
<br>1) $\\sqrt{a} \\geq 0$ и $\\sqrt{b} \\geq 0$ → их произведение $t \\geq 0$. ✓
|
||
<br>2) $t^2 = (\\sqrt{a})^2 \\cdot (\\sqrt{b})^2 = a \\cdot b$. ✓
|
||
<br>Значит, $t = \\sqrt{ab}$.
|
||
</div></details>
|
||
<p><b>Пример:</b> $\\sqrt{144 \\cdot 625} = \\sqrt{144} \\cdot \\sqrt{625} = 12 \\cdot 25 = 300$.</p>
|
||
`)}
|
||
|
||
${widget('Геометрическое доказательство √(a·b) = √a·√b', 'VISUAL', 'Прямоугольник a × b разбивается на a·b единичных клеток. Те же клетки могут собраться в квадрат со стороной √(a·b). Нажмите «Анимировать» — увидите как клетки перетекают.', `
|
||
<div class="row" style="justify-content:center;flex-wrap:wrap;gap:18px">
|
||
<div class="row" style="margin:0">
|
||
<span class="lab">a =</span>
|
||
<input id="geo-a" type="range" class="slider" min="1" max="9" step="1" value="4" style="max-width:130px">
|
||
<span id="geo-a-v" class="lab-mono" style="font-size:1.1rem;color:var(--pri2);min-width:18px">4</span>
|
||
</div>
|
||
<div class="row" style="margin:0">
|
||
<span class="lab">b =</span>
|
||
<input id="geo-b" type="range" class="slider" min="1" max="9" step="1" value="9" style="max-width:130px">
|
||
<span id="geo-b-v" class="lab-mono" style="font-size:1.1rem;color:var(--pri2);min-width:18px">9</span>
|
||
</div>
|
||
</div>
|
||
<div class="geo-canvas-wrap">
|
||
<svg id="geo-svg" viewBox="0 0 600 280" style="width:100%;max-width:640px;display:block;margin:14px auto 0"></svg>
|
||
</div>
|
||
<div class="geo-formula" id="geo-formula" style="margin-top:8px"></div>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<button class="btn primary" onclick="geoProofAnimate()" id="geo-play-btn">
|
||
<svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg>
|
||
Анимировать
|
||
</button>
|
||
<button class="btn" onclick="geoProofReset()">Сбросить</button>
|
||
</div>
|
||
`)}
|
||
|
||
${makeCard('rule','Свойство 2: корень из частного',null,`
|
||
<div class="formula-box">$\\sqrt{\\dfrac{a}{b}} = \\dfrac{\\sqrt{a}}{\\sqrt{b}}$, где $a \\geq 0$, $b > 0$</div>
|
||
<p><b>Пример:</b> $\\sqrt{\\dfrac{1225}{0{,}25}} = \\dfrac{\\sqrt{1225}}{\\sqrt{0{,}25}} = \\dfrac{35}{0{,}5} = 70$.</p>
|
||
`)}
|
||
|
||
${makeCard('rule','Свойство 3: корень из квадрата',null,`
|
||
<div class="formula-box">$\\sqrt{a^2} = |a|$</div>
|
||
<p>Это важно! Корень из квадрата — не просто $a$, а <b>модуль</b> $a$, потому что корень всегда неотрицателен.</p>
|
||
<p><b>Примеры:</b></p>
|
||
<ul>
|
||
<li>$\\sqrt{6^2} = |6| = 6$</li>
|
||
<li>$\\sqrt{(-8)^2} = |-8| = 8$ (а не $-8$!)</li>
|
||
<li>$\\sqrt{25 m^2} = 5|m|$</li>
|
||
<li>$\\sqrt{x^2/49} = |x|/7$</li>
|
||
</ul>
|
||
<div class="note-warn">⚠ Частая ошибка: писать $\\sqrt{a^2} = a$. Правильно $\\sqrt{a^2} = |a|$. Если же известно, что $a \\geq 0$, тогда модуль можно опустить: $\\sqrt{a^2} = a$.</div>
|
||
`)}
|
||
|
||
${widget('Слайдер-проверка свойств', 'CHECK', 'Меняйте a и b — слева и справа всегда совпадает. Это не магия, это свойство корня!', `
|
||
<div class="row">
|
||
<span class="lab">a =</span>
|
||
<input id="prop-a" type="range" class="slider" min="0" max="100" step="1" value="36" style="max-width:200px">
|
||
<span id="prop-a-v" class="lab-mono">36</span>
|
||
<span class="lab" style="margin-left:14px">b =</span>
|
||
<input id="prop-b" type="range" class="slider" min="1" max="100" step="1" value="25" style="max-width:200px">
|
||
<span id="prop-b-v" class="lab-mono">25</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px;text-align:center">
|
||
<div style="padding:14px;background:var(--card);border-radius:9px;border:1.5px solid var(--acc)">
|
||
<div class="lab">$\\sqrt{a \\cdot b}$</div>
|
||
<div id="prop-l1" style="font-size:1.4rem;font-weight:800;color:var(--acc2);font-family:'JetBrains Mono',monospace;margin-top:6px">30</div>
|
||
</div>
|
||
<div style="padding:14px;background:var(--card);border-radius:9px;border:1.5px solid var(--pri)">
|
||
<div class="lab">$\\sqrt{a} \\cdot \\sqrt{b}$</div>
|
||
<div id="prop-l2" style="font-size:1.4rem;font-weight:800;color:var(--pri2);font-family:'JetBrains Mono',monospace;margin-top:6px">30</div>
|
||
</div>
|
||
</div>
|
||
<div id="prop-eq" style="margin-top:12px;text-align:center;font-weight:700;color:var(--ok)">✓ Совпадают</div>
|
||
`)}
|
||
|
||
${widget('Match: выражение ↔ ответ', 'GAME', 'Соедините каждое выражение с его упрощённым значением. Кликните по выражению, затем по ответу.', `
|
||
<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">
|
||
<span class="lab">Соединено: <b id="match-cnt">0</b>/5</span>
|
||
<button class="btn" onclick="matchReset()">Заново</button>
|
||
</div>
|
||
<div id="match-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('example','Решения с использованием свойств',null,`
|
||
<ul>
|
||
<li>$\\sqrt{144 \\cdot 0{,}49} = \\sqrt{144} \\cdot \\sqrt{0{,}49} = 12 \\cdot 0{,}7 = 8{,}4$</li>
|
||
<li>$\\sqrt{27 \\cdot 75} = \\sqrt{9 \\cdot 3 \\cdot 25 \\cdot 3} = \\sqrt{9 \\cdot 25 \\cdot 9} = 3 \\cdot 5 \\cdot 3 = 45$</li>
|
||
<li>$\\sqrt{104^2 - 40^2} = \\sqrt{(104+40)(104-40)} = \\sqrt{144 \\cdot 64} = 12 \\cdot 8 = 96$</li>
|
||
<li>$\\dfrac{\\sqrt{0{,}63}}{\\sqrt{0{,}07}} = \\sqrt{\\dfrac{0{,}63}{0{,}07}} = \\sqrt{9} = 3$</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${widget('Калькулятор |a| = √(a²)', 'CHECK', 'Введите a (любое — отрицательное тоже). Увидите, что √(a²) равен модулю a — расстоянию до 0.', `
|
||
<div class="row">
|
||
<span class="lab">a =</span>
|
||
<input id="abs-a" type="number" class="inp num" step="any" value="-7">
|
||
<span class="lab">→</span>
|
||
<span class="lab">a² =</span>
|
||
<span id="abs-sq" class="lab-mono">49</span>
|
||
<span class="lab">→</span>
|
||
<span class="lab">$\\sqrt{a²}$ =</span>
|
||
<span id="abs-r" class="lab-mono" style="color:var(--pri2);font-size:1.1rem">7</span>
|
||
<span class="lab">|a| =</span>
|
||
<span id="abs-mod" class="lab-mono" style="color:var(--ok)">7</span>
|
||
</div>
|
||
<div style="margin-top:10px;font-size:.86rem;color:var(--muted)">При $a < 0$ результат всё равно положительный — корень всегда $\\geq 0$.</div>
|
||
`)}
|
||
|
||
${widget('Упрости тренажёр', 'PRACTICE', 'Введите упрощённое значение. Например, для √(36·25) ответ — 30.', `
|
||
<div id="simp-card" style="padding:14px;background:var(--card);border-radius:9px;border:1px solid var(--border);text-align:center">
|
||
<div class="lab">Упростите:</div>
|
||
<div id="simp-q" style="font-size:1.5rem;font-weight:800;color:var(--pri2);margin:10px 0;font-family:'JetBrains Mono',monospace">√(36·25)</div>
|
||
<div class="row-c">
|
||
<input id="simp-ans" class="inp num" type="number" step="any" placeholder="ответ">
|
||
<button class="btn primary" onclick="simpCheck()">Проверить</button>
|
||
<button class="btn" onclick="simpNext()">Другая задача</button>
|
||
</div>
|
||
</div>
|
||
<div id="simp-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('home','Домашнее задание','1.146–1.155',`
|
||
<ol style="padding-left:22px">
|
||
<li>Вычислите: а) $(\\sqrt{36})^2$; б) $(\\sqrt{8{,}3})^2$; в) $(\\sqrt{11/16})^2$; г) $(3\\sqrt{2})^2$.</li>
|
||
<li>Найдите значение: $\\sqrt{36 \\cdot 16}$; $\\sqrt{25 \\cdot 0{,}09}$; $\\sqrt{144 \\cdot 0{,}49}$.</li>
|
||
<li>Вычислите: $\\sqrt{2} \\cdot \\sqrt{18}$; $\\sqrt{6} \\cdot \\sqrt{24}$; $\\sqrt{72} \\cdot \\sqrt{0{,}5}$.</li>
|
||
<li>Найдите $\\sqrt{(-3{,}47)^2}$; $-2 \\cdot \\sqrt{2{,}5^2}$; $15 / \\sqrt{(-3)^2}$.</li>
|
||
</ol>
|
||
`)}
|
||
|
||
${bossWidget('p3')}
|
||
|
||
${secNav('p2', 'p4')}
|
||
`;
|
||
renderMath(body);
|
||
setTimeout(()=>{ initGeoProof(); initPropCheck(); initMatch(); initAbsCalc(); initSimpTrainer(); }, 50);
|
||
}
|
||
|
||
/* ──── Geometric proof √(ab) ──── */
|
||
/* ──── Geometric proof √(ab)=√a·√b — анимация клеток ──── */
|
||
const GEO_STATE = { animating: false };
|
||
|
||
function initGeoProof(){
|
||
const a = document.getElementById('geo-a');
|
||
if(!a) return;
|
||
const b = document.getElementById('geo-b');
|
||
function upd(){
|
||
if(GEO_STATE.animating) return;
|
||
geoRenderRect();
|
||
}
|
||
a.addEventListener('input', upd);
|
||
b.addEventListener('input', upd);
|
||
geoRenderRect();
|
||
}
|
||
|
||
function geoCurrentAB(){
|
||
const a = +document.getElementById('geo-a').value;
|
||
const b = +document.getElementById('geo-b').value;
|
||
return [a, b];
|
||
}
|
||
|
||
function geoRenderRect(){
|
||
const [aVal, bVal] = geoCurrentAB();
|
||
document.getElementById('geo-a-v').textContent = aVal;
|
||
document.getElementById('geo-b-v').textContent = bVal;
|
||
const svg = document.getElementById('geo-svg');
|
||
if(!svg) return;
|
||
svg.innerHTML = '';
|
||
const ab = aVal * bVal;
|
||
const sqSide = Math.sqrt(ab);
|
||
// sizing: cell size based on max dimensions
|
||
const W = 600, H = 280;
|
||
const halfW = 290, gap = 20;
|
||
const maxCol = Math.max(aVal, Math.ceil(sqSide));
|
||
const maxRow = Math.max(bVal, Math.ceil(sqSide));
|
||
const cell = Math.floor(Math.min(halfW/maxCol, (H-50)/maxRow, 30));
|
||
const rectW = aVal * cell, rectH = bVal * cell;
|
||
const rectX = (halfW - rectW) / 2;
|
||
const rectY = (H - 30 - rectH) / 2;
|
||
const sqW = sqSide * cell;
|
||
const sqX = halfW + gap + (halfW - sqW) / 2;
|
||
const sqY = (H - 30 - sqW) / 2;
|
||
// Labels
|
||
svg.appendChild(svgEl('text', {x:halfW/2, y:H-8, 'text-anchor':'middle', fill:'#03a9f4', 'font-size':14, 'font-weight':700, 'font-family':'Inter,sans-serif'}, `${aVal} × ${bVal} = ${ab} клеток`));
|
||
svg.appendChild(svgEl('text', {x:halfW + gap + halfW/2, y:H-8, 'text-anchor':'middle', fill:'#e91e63', 'font-size':14, 'font-weight':700, 'font-family':'Inter,sans-serif'}, `сторона ≈ ${sqSide.toFixed(2)} → S = ${ab}`));
|
||
// Arrow between
|
||
svg.appendChild(svgEl('path', {d:`M ${halfW + 2} ${H/2 - 18} L ${halfW + gap - 2} ${H/2 - 18}`, stroke:'#7c3aed', 'stroke-width':2.5, fill:'none'}));
|
||
svg.appendChild(svgEl('polyline', {points:`${halfW + gap - 10},${H/2 - 24} ${halfW + gap - 2},${H/2 - 18} ${halfW + gap - 10},${H/2 - 12}`, stroke:'#7c3aed', 'stroke-width':2.5, fill:'none'}));
|
||
// Hint outline rectangle on the right (where square would be)
|
||
svg.appendChild(svgEl('rect', {x:sqX, y:sqY, width:sqW, height:sqW, fill:'none', stroke:'#e91e63', 'stroke-width':2, 'stroke-dasharray':'4,4', opacity:0.5}));
|
||
// Cells in rectangle layout
|
||
for(let i = 0; i < ab; i++){
|
||
const col = i % aVal;
|
||
const row = Math.floor(i / aVal);
|
||
const cellEl = svgEl('rect', {
|
||
class:'geo-cell',
|
||
'data-i':i,
|
||
x: rectX + col*cell + 1,
|
||
y: rectY + row*cell + 1,
|
||
width: cell - 2,
|
||
height: cell - 2,
|
||
fill: i % 2 === 0 ? '#03a9f4' : '#0288d1',
|
||
opacity: 0.85,
|
||
rx: 2,
|
||
});
|
||
cellEl.style.transition = 'x .55s cubic-bezier(.4,1.3,.5,1), y .55s cubic-bezier(.4,1.3,.5,1), fill .3s';
|
||
svg.appendChild(cellEl);
|
||
}
|
||
// Stash layout info
|
||
GEO_STATE.cell = cell;
|
||
GEO_STATE.rectX = rectX; GEO_STATE.rectY = rectY;
|
||
GEO_STATE.sqX = sqX; GEO_STATE.sqY = sqY;
|
||
GEO_STATE.sqSide = sqSide;
|
||
GEO_STATE.aVal = aVal; GEO_STATE.bVal = bVal; GEO_STATE.ab = ab;
|
||
// Formula
|
||
geoUpdateFormula(false);
|
||
}
|
||
|
||
function svgEl(tag, attrs, text){
|
||
const e = document.createElementNS('http://www.w3.org/2000/svg', tag);
|
||
if(attrs) for(const k in attrs) e.setAttribute(k, attrs[k]);
|
||
if(text != null) e.textContent = text;
|
||
return e;
|
||
}
|
||
|
||
function geoUpdateFormula(done){
|
||
const fb = document.getElementById('geo-formula');
|
||
if(!fb) return;
|
||
const a = GEO_STATE.aVal, b = GEO_STATE.bVal, ab = GEO_STATE.ab;
|
||
const ra = Math.sqrt(a), rb = Math.sqrt(b);
|
||
const rab = Math.sqrt(ab);
|
||
const aRound = Number.isInteger(ra) ? ra : ra.toFixed(2);
|
||
const bRound = Number.isInteger(rb) ? rb : rb.toFixed(2);
|
||
const abRound = Number.isInteger(rab) ? rab : rab.toFixed(2);
|
||
fb.innerHTML = `$$\\sqrt{${a} \\cdot ${b}} = \\sqrt{${ab}} = ${abRound}$$
|
||
$$\\sqrt{${a}} \\cdot \\sqrt{${b}} = ${aRound} \\cdot ${bRound} = ${abRound}$$`
|
||
+ (done ? `<div class="proof-badge" style="margin-top:8px"><svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px;stroke:#fff;stroke-width:3"><polyline points="20 6 9 17 4 12"/></svg> Доказано: $\\sqrt{${a} \\cdot ${b}} = \\sqrt{${a}} \\cdot \\sqrt{${b}}$</div>` : '');
|
||
renderMath(fb);
|
||
}
|
||
|
||
async function geoProofAnimate(){
|
||
if(GEO_STATE.animating) return;
|
||
GEO_STATE.animating = true;
|
||
const btn = document.getElementById('geo-play-btn');
|
||
if(btn) btn.disabled = true;
|
||
const svg = document.getElementById('geo-svg');
|
||
if(!svg){ GEO_STATE.animating = false; if(btn) btn.disabled = false; return; }
|
||
const cells = [...svg.querySelectorAll('.geo-cell')];
|
||
const cell = GEO_STATE.cell;
|
||
const sqX = GEO_STATE.sqX, sqY = GEO_STATE.sqY;
|
||
const sqSide = GEO_STATE.sqSide;
|
||
// Шаг 1: волна подсветки прямоугольника
|
||
for(let i = 0; i < cells.length; i++){
|
||
cells[i].setAttribute('fill', '#fbbf24');
|
||
cells[i].setAttribute('opacity', '1');
|
||
await sleep(20);
|
||
}
|
||
await sleep(280);
|
||
// Шаг 2: клетки летят в квадрат
|
||
// если ab - точный квадрат, плотная упаковка
|
||
const isSq = Number.isInteger(sqSide);
|
||
const cols = isSq ? sqSide : Math.ceil(sqSide);
|
||
const targetCellSize = isSq ? cell : (sqSide * cell) / cols;
|
||
for(let i = 0; i < cells.length; i++){
|
||
const col = i % cols;
|
||
const row = Math.floor(i / cols);
|
||
cells[i].setAttribute('x', sqX + col*targetCellSize + 1);
|
||
cells[i].setAttribute('y', sqY + row*targetCellSize + 1);
|
||
cells[i].setAttribute('width', targetCellSize - 2);
|
||
cells[i].setAttribute('height', targetCellSize - 2);
|
||
cells[i].setAttribute('fill', i % 2 === 0 ? '#e91e63' : '#c2185b');
|
||
await sleep(35);
|
||
}
|
||
await sleep(500);
|
||
// Шаг 3: пульс на финальном квадрате
|
||
cells.forEach(c => { c.setAttribute('opacity', '1'); });
|
||
await sleep(100);
|
||
geoUpdateFormula(true);
|
||
bumpProgress('p3', 6);
|
||
addXp(10, 'geo-proof');
|
||
confetti();
|
||
await sleep(2400);
|
||
GEO_STATE.animating = false;
|
||
if(btn) btn.disabled = false;
|
||
}
|
||
|
||
function geoProofReset(){
|
||
GEO_STATE.animating = false;
|
||
const btn = document.getElementById('geo-play-btn');
|
||
if(btn) btn.disabled = false;
|
||
geoRenderRect();
|
||
}
|
||
|
||
function sleep(ms){ return new Promise(r => setTimeout(r, ms)); }
|
||
|
||
/* ──── Property check slider ──── */
|
||
function initPropCheck(){
|
||
const a = document.getElementById('prop-a');
|
||
if(!a) return;
|
||
const b = document.getElementById('prop-b');
|
||
function upd(){
|
||
const av = +a.value, bv = +b.value;
|
||
document.getElementById('prop-a-v').textContent = av;
|
||
document.getElementById('prop-b-v').textContent = bv;
|
||
document.getElementById('prop-l1').textContent = Math.sqrt(av*bv).toFixed(3);
|
||
document.getElementById('prop-l2').textContent = (Math.sqrt(av) * Math.sqrt(bv)).toFixed(3);
|
||
}
|
||
a.addEventListener('input', upd);
|
||
b.addEventListener('input', upd);
|
||
upd();
|
||
}
|
||
|
||
/* ──── Match game ──── */
|
||
const MATCH_PAIRS = [
|
||
{expr:'√(36·25)', ans:'30'},
|
||
{expr:'√(169/64)', ans:'13/8'},
|
||
{expr:'√144 · √25', ans:'60'},
|
||
{expr:'√((−7)²)', ans:'7'},
|
||
{expr:'√25 · √4', ans:'10'},
|
||
];
|
||
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');
|
||
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=>{
|
||
const b = el('button', {class:'btn'}, p.expr);
|
||
b.style.fontFamily="'JetBrains Mono',monospace";
|
||
b.dataset.expr = p.expr;
|
||
b.addEventListener('click', ()=>{
|
||
if(b.disabled) return;
|
||
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);
|
||
});
|
||
rArr.forEach(p=>{
|
||
const b = el('button', {class:'btn'}, p.ans);
|
||
b.style.fontFamily="'JetBrains Mono',monospace";
|
||
b.dataset.ans = p.ans;
|
||
b.addEventListener('click', ()=>{
|
||
if(b.disabled) return;
|
||
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);
|
||
});
|
||
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.done.push(e);
|
||
document.getElementById('match-cnt').textContent = matchState.done.length;
|
||
feedback(fb, true, e + ' = ' + a + ' ✓');
|
||
if(matchState.done.length === MATCH_PAIRS.length){
|
||
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=''; }
|
||
}, 750);
|
||
}
|
||
matchState.selL = null;
|
||
matchState.selR = null;
|
||
}
|
||
|
||
/* ──── |a| calc ──── */
|
||
function initAbsCalc(){
|
||
const a = document.getElementById('abs-a');
|
||
if(!a) return;
|
||
function upd(){
|
||
const v = +a.value;
|
||
document.getElementById('abs-sq').textContent = (v*v).toFixed(2);
|
||
document.getElementById('abs-r').textContent = Math.sqrt(v*v).toFixed(2);
|
||
document.getElementById('abs-mod').textContent = Math.abs(v).toFixed(2);
|
||
}
|
||
a.addEventListener('input', upd);
|
||
upd();
|
||
}
|
||
|
||
/* ──── Simplify trainer ──── */
|
||
const SIMP_TASKS = [
|
||
{q:'√(36·25)', a:30}, {q:'√(144·0.25)', a:6}, {q:'√(81/9)', a:3},
|
||
{q:'√100 · √4', a:20}, {q:'√(2·8)', a:4}, {q:'√((-6)²)', a:6},
|
||
{q:'√(49/25)', a:1.4}, {q:'√144 - √81', a:3}, {q:'√(0.04·25)', a:1},
|
||
{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();
|
||
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(){
|
||
const t = SIMP_TASKS[simpIdx];
|
||
const v = parseFloat(document.getElementById('simp-ans').value.replace(',','.'));
|
||
const fb = document.getElementById('simp-fb');
|
||
if(isNaN(v)){ feedback(fb, false, 'Введите число'); return; }
|
||
if(Math.abs(v - t.a) < 0.02){
|
||
feedback(fb, true, '✓ Верно! ' + t.q + ' = ' + t.a);
|
||
bumpProgress('p3', 3);
|
||
confetti();
|
||
setTimeout(simpNext, 900);
|
||
} else {
|
||
feedback(fb, false, '✗ Не точно. Подсказка: ' + t.q + ' = ' + t.a);
|
||
}
|
||
}
|
||
function simpNext(){
|
||
simpIdx = (simpIdx + 1) % SIMP_TASKS.length;
|
||
simpRender();
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
§ 4. Применение свойств квадратных корней
|
||
════════════════════════════════════════════════════════ */
|
||
function buildP4(){
|
||
const body = document.getElementById('p4-body');
|
||
body.innerHTML = `
|
||
${makeCard('theory','Что такое преобразование?',null,`
|
||
<p>Когда в выражении есть корни, их часто можно упростить, применив свойства из §3. Основные операции:</p>
|
||
<ul>
|
||
<li><b>Вынесение множителя</b> из-под корня: $\\sqrt{72} = \\sqrt{36 \\cdot 2} = \\sqrt{36} \\cdot \\sqrt{2} = 6\\sqrt{2}$</li>
|
||
<li><b>Внесение множителя</b> под корень: $5\\sqrt{3} = \\sqrt{25} \\cdot \\sqrt{3} = \\sqrt{75}$</li>
|
||
<li><b>Освобождение от иррациональности</b> в знаменателе: $\\dfrac{1}{\\sqrt{3}} = \\dfrac{\\sqrt{3}}{3}$</li>
|
||
<li><b>Сравнение</b> выражений с корнями (через возведение в квадрат).</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${makeCard('rule','Вынесение множителя из-под корня',null,`
|
||
<p>Идея: ищем под корнем точный квадрат как множитель.</p>
|
||
<div class="formula-box">$\\sqrt{a^2 \\cdot b} = a \\sqrt{b}$, где $a \\geq 0$, $b \\geq 0$</div>
|
||
<p><b>Примеры:</b></p>
|
||
<ul>
|
||
<li>$\\sqrt{72} = \\sqrt{36 \\cdot 2} = 6\\sqrt{2}$ (выделили 36 как точный квадрат)</li>
|
||
<li>$\\sqrt{200} = \\sqrt{100 \\cdot 2} = 10\\sqrt{2}$</li>
|
||
<li>$\\sqrt{50} = \\sqrt{25 \\cdot 2} = 5\\sqrt{2}$</li>
|
||
<li>$\\sqrt{48} = \\sqrt{16 \\cdot 3} = 4\\sqrt{3}$</li>
|
||
<li>$\\sqrt{75} = \\sqrt{25 \\cdot 3} = 5\\sqrt{3}$</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${widget('Упрости корень — найди точный квадрат', 'GAME', 'Нажмите на число, которое является точным квадратом И делит подкоренное число. Например, для √72: точные квадраты — 4, 9, 16, 25, 36; делит ли 72 какой-то из них? Да, 36 (72 = 36·2).', `
|
||
<div id="drag-task" style="padding:18px;background:var(--card);border-radius:11px;margin-bottom:12px">
|
||
<div class="lab" style="text-align:center;margin-bottom:6px">Шаг 1. Упрости выражение</div>
|
||
<div id="drag-q" style="font-size:2.6rem;font-weight:900;color:var(--pri2);text-align:center;margin:14px 0;font-family:'JetBrains Mono',monospace">$\\sqrt{72}$</div>
|
||
<div style="text-align:center;margin-bottom:14px">
|
||
<div class="lab" style="margin-bottom:4px">Выберите точный квадрат, который делит подкоренное число:</div>
|
||
<div id="drag-mults" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:8px;max-width:520px;margin:10px auto 0"></div>
|
||
</div>
|
||
<div id="drag-pipeline" style="display:none;margin-top:16px;padding:14px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:9px;text-align:center;font-size:1.1rem;line-height:2"></div>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<button class="btn primary" onclick="dragNext()">
|
||
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
|
||
Следующая задача
|
||
</button>
|
||
<button class="btn" onclick="dragHint()">Подсказка</button>
|
||
</div>
|
||
</div>
|
||
<div id="drag-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('rule','Внесение множителя под корень',null,`
|
||
<div class="formula-box">$a \\sqrt{b} = \\sqrt{a^2 \\cdot b}$, где $a \\geq 0$, $b \\geq 0$</div>
|
||
<p><b>Примеры:</b> $5\\sqrt{3} = \\sqrt{25 \\cdot 3} = \\sqrt{75}$. $2\\sqrt{6} = \\sqrt{4 \\cdot 6} = \\sqrt{24}$.</p>
|
||
<div class="note-warn">⚠ Если $a < 0$, перед внесением выносим минус: $-3\\sqrt{2} = -\\sqrt{9 \\cdot 2} = -\\sqrt{18}$.</div>
|
||
`)}
|
||
|
||
${widget('Конвертер: a√b ⇄ √c', 'CALC', 'Введите a и b, получите упрощённый √c. Или наоборот: введите c и узнайте, можно ли вынести множитель.', `
|
||
<div class="row" style="margin-bottom:14px">
|
||
<span class="lab-mono" style="font-size:1.1rem">Из вида a√b:</span>
|
||
<input id="conv-a" class="inp num" type="number" value="3" style="width:60px">
|
||
<span class="lab-mono" style="font-size:1.2rem">√</span>
|
||
<input id="conv-b" class="inp num" type="number" value="5" style="width:60px">
|
||
<span class="lab">=</span>
|
||
<span class="lab-mono" style="font-size:1.2rem">√</span>
|
||
<span id="conv-c" class="lab-mono" style="font-size:1.2rem;color:var(--pri2)">45</span>
|
||
</div>
|
||
<div class="row">
|
||
<span class="lab-mono" style="font-size:1.1rem">Из вида √c:</span>
|
||
<span class="lab-mono" style="font-size:1.2rem">√</span>
|
||
<input id="conv-c2" class="inp num" type="number" value="72" style="width:80px">
|
||
<span class="lab">=</span>
|
||
<span id="conv-out" class="lab-mono" style="font-size:1.2rem;color:var(--pri2)">6√2</span>
|
||
</div>
|
||
`)}
|
||
|
||
${makeCard('rule','Освобождение от иррациональности',null,`
|
||
<p>Дробь с корнем в знаменателе считается «непричёсанной». Чтобы убрать корень снизу — домножим на $\\sqrt{...}/\\sqrt{...}$.</p>
|
||
<div class="formula-box">$\\dfrac{c}{\\sqrt{a}} = \\dfrac{c}{\\sqrt{a}} \\cdot \\dfrac{\\sqrt{a}}{\\sqrt{a}} = \\dfrac{c\\sqrt{a}}{a}$</div>
|
||
<p><b>Примеры:</b></p>
|
||
<ul>
|
||
<li>$\\dfrac{1}{\\sqrt{3}} = \\dfrac{\\sqrt{3}}{3}$</li>
|
||
<li>$\\dfrac{5}{2\\sqrt{7}} = \\dfrac{5}{2\\sqrt{7}} \\cdot \\dfrac{\\sqrt{7}}{\\sqrt{7}} = \\dfrac{5\\sqrt{7}}{2 \\cdot 7} = \\dfrac{5\\sqrt{7}}{14}$</li>
|
||
<li>$\\dfrac{3}{\\sqrt{5}-1} = \\dfrac{3(\\sqrt{5}+1)}{(\\sqrt{5}-1)(\\sqrt{5}+1)} = \\dfrac{3(\\sqrt{5}+1)}{5-1} = \\dfrac{3(\\sqrt{5}+1)}{4}$ (через сопряжённое)</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${widget('Помощник: освобождение от иррациональности', 'STEPS', 'Введите c, a, b для выражения $c/(a\\sqrt{b})$ — увидите пошаговое преобразование.', `
|
||
<div class="row">
|
||
<span class="lab-mono" style="font-size:1.2rem">Выражение:</span>
|
||
<span class="lab-mono" style="font-size:1.3rem">
|
||
<input id="fri-c" class="inp num" type="number" value="5" style="width:60px">
|
||
/
|
||
(<input id="fri-a" class="inp num" type="number" value="2" style="width:50px">
|
||
√<input id="fri-b" class="inp num" type="number" value="7" style="width:50px">)
|
||
</span>
|
||
</div>
|
||
<div id="fri-out" style="margin-top:14px;padding:14px;background:var(--card);border-radius:9px;border:1.5px solid var(--pri);font-family:'JetBrains Mono',monospace;font-size:1.05rem;line-height:1.8"></div>
|
||
`)}
|
||
|
||
${makeCard('rule','Сравнение выражений с корнями',null,`
|
||
<p>Чтобы сравнить два выражения с корнями, можно <b>возвести в квадрат</b> (если оба неотрицательны).</p>
|
||
<p><b>Пример:</b> что больше — $3\\sqrt{2}$ или $2\\sqrt{3}$?</p>
|
||
<ul>
|
||
<li>$(3\\sqrt{2})^2 = 9 \\cdot 2 = 18$</li>
|
||
<li>$(2\\sqrt{3})^2 = 4 \\cdot 3 = 12$</li>
|
||
<li>$18 > 12$ → $3\\sqrt{2} > 2\\sqrt{3}$</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${widget('«Кто больше?»', 'COMPARE', 'Выберите, какое выражение больше. Помогает возвести в квадрат — нажмите кнопку «Подсказка».', `
|
||
<div id="comp-card" style="padding:18px;background:var(--card);border-radius:9px;text-align:center">
|
||
<div class="row-c" style="font-size:1.5rem;font-family:'JetBrains Mono',monospace">
|
||
<button id="comp-a" class="btn" style="font-size:1.3rem;padding:14px 22px"></button>
|
||
<span style="font-size:1.5rem;color:var(--muted)">vs</span>
|
||
<button id="comp-b" class="btn" style="font-size:1.3rem;padding:14px 22px"></button>
|
||
</div>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<button class="btn primary" onclick="compSet('a')">Левое больше</button>
|
||
<button class="btn primary" onclick="compSet('b')">Правое больше</button>
|
||
<button class="btn" onclick="compHint()">Подсказка</button>
|
||
<button class="btn" onclick="compNext()">Другая</button>
|
||
</div>
|
||
</div>
|
||
<div id="comp-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${widget('Сравнение через возведение в квадрат', 'VISUAL', 'Чтобы сравнить два выражения с корнями — возведи их в квадрат. У большего выражения квадрат больше.', `
|
||
<div class="row" style="justify-content:center;gap:18px;font-family:'JetBrains Mono',monospace;font-size:1.3rem;margin-bottom:14px">
|
||
<span id="sq-a-expr" style="color:var(--acc2);font-weight:700">3√2</span>
|
||
<span style="color:var(--muted)">vs</span>
|
||
<span id="sq-b-expr" style="color:var(--pri2);font-weight:700">2√3</span>
|
||
</div>
|
||
<svg id="sq-svg" viewBox="0 0 600 220" style="width:100%;max-width:560px;display:block;margin:0 auto"></svg>
|
||
<div class="row-c" style="margin-top:12px">
|
||
<button class="btn primary" onclick="sqAnimate()">Возвести в квадрат и сравнить</button>
|
||
<button class="btn" onclick="sqNext()">Другая пара</button>
|
||
</div>
|
||
<div id="sq-conclusion" class="sq-comp-conclusion"></div>
|
||
`)}
|
||
|
||
${widget('Тренажёр «Упрости»', 'GAME', 'Введите упрощённое выражение в виде a√b. Например, для √72 ответ: 6√2.', `
|
||
<div id="simp4-card" style="padding:14px;background:var(--card);border-radius:9px;text-align:center">
|
||
<div class="lab">Упростите:</div>
|
||
<div id="simp4-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>
|
||
<input id="simp4-a" class="inp num" type="number" placeholder="a" style="width:60px">
|
||
<span class="lab-mono" style="font-size:1.4rem">√</span>
|
||
<input id="simp4-b" class="inp num" type="number" placeholder="b" style="width:60px">
|
||
<button class="btn primary" onclick="simp4Check()">Проверить</button>
|
||
<button class="btn" onclick="simp4Hint()">Подсказка</button>
|
||
<button class="btn" onclick="simp4Next()">Дальше</button>
|
||
</div>
|
||
<div class="row-c" style="margin-top:10px">
|
||
<span class="lab">Очки: <b id="simp4-score">0</b></span>
|
||
<span class="lab">Решено: <b id="simp4-cnt">0</b></span>
|
||
</div>
|
||
</div>
|
||
<div id="simp4-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('class','Задания для класса',null,`
|
||
<ol style="padding-left:22px">
|
||
<li>Вынесите множитель: $\\sqrt{75}$; $\\sqrt{18}$; $\\sqrt{48}$; $\\sqrt{200}$; $\\sqrt{32}$; $\\sqrt{98}$.</li>
|
||
<li>Внесите под знак корня: $5\\sqrt{2}$; $3\\sqrt{7}$; $4\\sqrt{5}$.</li>
|
||
<li>Освободитесь от иррациональности: $\\dfrac{1}{\\sqrt{2}}$; $\\dfrac{3}{\\sqrt{5}}$; $\\dfrac{7}{2\\sqrt{3}}$.</li>
|
||
<li>Сравните: $4\\sqrt{3}$ и $3\\sqrt{5}$; $\\sqrt{2} + \\sqrt{8}$ и $\\sqrt{18}$.</li>
|
||
</ol>
|
||
<details class="spoiler">
|
||
<summary>Подсказки</summary>
|
||
<div class="spoiler-body">
|
||
1) $5\\sqrt{3}; 3\\sqrt{2}; 4\\sqrt{3}; 10\\sqrt{2}; 4\\sqrt{2}; 7\\sqrt{2}$.
|
||
2) $\\sqrt{50}; \\sqrt{63}; \\sqrt{80}$.
|
||
3) $\\sqrt{2}/2; 3\\sqrt{5}/5; 7\\sqrt{3}/6$.
|
||
4) $4\\sqrt{3}<3\\sqrt{5}$ (48<45 — нет, 48>45, поэтому $4\\sqrt{3}>3\\sqrt{5}$. Перепроверьте через квадрат!). $\\sqrt{2} + \\sqrt{8} = \\sqrt{2} + 2\\sqrt{2} = 3\\sqrt{2} = \\sqrt{18}$ — равны.
|
||
</div>
|
||
</details>
|
||
`)}
|
||
|
||
${bossWidget('p4')}
|
||
|
||
${secNav('p3', 'p5')}
|
||
`;
|
||
renderMath(body);
|
||
setTimeout(()=>{ initDragSimp(); initConverter(); initFracIrr(); initCompare(); initSimp4(); }, 50);
|
||
}
|
||
|
||
/* ──── Упрости √ — пошаговая мини-игра ──── */
|
||
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;
|
||
|
||
function initDragSimp(){
|
||
dragIdx = 0;
|
||
dragRender();
|
||
}
|
||
|
||
function dragRender(){
|
||
const t = DRAG_TASKS[dragIdx];
|
||
const qEl = document.getElementById('drag-q');
|
||
if(qEl){
|
||
qEl.innerHTML = '$\\sqrt{' + t.n + '}$';
|
||
renderMath(qEl);
|
||
}
|
||
// Генерируем 5 кандидатов: точный квадрат + другие из набора
|
||
const CAND = [4, 9, 16, 25, 36, 49, 64, 81, 100, 121];
|
||
const mults = new Set([t.sq]);
|
||
for(const m of CAND){ if(t.n % m === 0) mults.add(m); }
|
||
let safety = 0;
|
||
while(mults.size < 5 && safety++ < 30){ mults.add(CAND[Math.floor(Math.random()*CAND.length)]); }
|
||
const arr = [...mults].slice(0, 5).sort((a, b) => a - b);
|
||
const g = document.getElementById('drag-mults');
|
||
if(!g) return;
|
||
g.innerHTML = '';
|
||
arr.forEach(m => {
|
||
const card = document.createElement('button');
|
||
card.className = 'simp-card-btn';
|
||
card.dataset.val = m;
|
||
const sqRt = Math.sqrt(m);
|
||
card.innerHTML = `<div class="scb-num">${m}</div><div class="scb-sub">= ${sqRt}<sup>2</sup></div>`;
|
||
card.addEventListener('click', () => dragPick(m, t, card));
|
||
g.appendChild(card);
|
||
});
|
||
const pipe = document.getElementById('drag-pipeline');
|
||
if(pipe){ pipe.style.display = 'none'; pipe.innerHTML = ''; }
|
||
document.getElementById('drag-fb').className = 'feedback';
|
||
}
|
||
|
||
function dragPick(m, t, card){
|
||
const fb = document.getElementById('drag-fb');
|
||
const pipe = document.getElementById('drag-pipeline');
|
||
// 1) не делит → нельзя так раскладывать
|
||
if(t.n % m !== 0){
|
||
card.classList.add('wrong-dnd');
|
||
setTimeout(()=>card.classList.remove('wrong-dnd'), 600);
|
||
feedback(fb, false, '✗ ' + m + ' не делит ' + t.n + ' нацело — этот вариант не подходит');
|
||
return;
|
||
}
|
||
const rest = t.n / m;
|
||
// 2) делит, но НЕ максимальный точный квадрат → подходит, но не оптимально
|
||
if(m !== t.sq){
|
||
card.classList.add('wrong-dnd');
|
||
setTimeout(()=>card.classList.remove('wrong-dnd'), 600);
|
||
feedback(fb, false, '⚠ ' + m + ' делит ' + t.n + ' (' + t.n + ' = ' + m + '·' + rest + '), но ' + t.sq + ' ещё больше. Попробуй ещё.');
|
||
return;
|
||
}
|
||
// 3) Идеально! Показываем пошаговый вывод
|
||
document.querySelectorAll('.simp-card-btn').forEach(b => b.classList.add('locked'));
|
||
card.classList.remove('locked');
|
||
card.classList.add('correct-dnd');
|
||
if(pipe){
|
||
pipe.style.display = 'block';
|
||
pipe.innerHTML = `
|
||
<div style="margin-bottom:6px;font-size:.86rem;color:var(--muted)">Решение пошагово:</div>
|
||
<div>$\\sqrt{${t.n}} = \\sqrt{${m} \\cdot ${rest}} = \\sqrt{${m}} \\cdot \\sqrt{${rest}} = ${Math.sqrt(m)}\\sqrt{${rest}}$</div>
|
||
`;
|
||
renderMath(pipe);
|
||
}
|
||
feedback(fb, true, '✓ Верно! $\\sqrt{' + t.n + '} = ' + Math.sqrt(m) + '\\sqrt{' + rest + '}$');
|
||
setTimeout(()=>renderMath(fb), 50);
|
||
confetti();
|
||
bumpProgress('p4', 3);
|
||
addXp(8, 'simp');
|
||
}
|
||
|
||
function dragNext(){
|
||
dragIdx = (dragIdx + 1) % DRAG_TASKS.length;
|
||
dragRender();
|
||
}
|
||
|
||
function dragHint(){
|
||
const t = DRAG_TASKS[dragIdx];
|
||
const fb = document.getElementById('drag-fb');
|
||
feedback(fb, true, 'Подсказка: ищите наибольший точный квадрат, который делит ' + t.n + ' нацело. Ответ начинается с ' + t.sq + '.');
|
||
}
|
||
|
||
/* ──── Converter a√b ⇄ √c ──── */
|
||
function initConverter(){
|
||
const a = document.getElementById('conv-a');
|
||
if(!a) return;
|
||
const b = document.getElementById('conv-b');
|
||
const c2 = document.getElementById('conv-c2');
|
||
function fwd(){
|
||
const av = +a.value, bv = +b.value;
|
||
document.getElementById('conv-c').textContent = (av*av*bv);
|
||
}
|
||
function rev(){
|
||
const cv = +c2.value;
|
||
if(cv < 0){ document.getElementById('conv-out').textContent = 'нет смысла'; return; }
|
||
// find largest a² dividing cv
|
||
let bestA = 1, bestRest = cv;
|
||
for(let i = 2; i * i <= cv; i++){
|
||
if(cv % (i*i) === 0){
|
||
bestA = i; bestRest = cv / (i*i);
|
||
}
|
||
}
|
||
const out = document.getElementById('conv-out');
|
||
if(bestA === 1) out.textContent = '√' + cv + ' (нет совершенного квадрата)';
|
||
else out.textContent = bestA + '√' + bestRest;
|
||
}
|
||
a.addEventListener('input', fwd);
|
||
b.addEventListener('input', fwd);
|
||
c2.addEventListener('input', rev);
|
||
fwd(); rev();
|
||
}
|
||
|
||
/* ──── Освобождение от иррациональности ──── */
|
||
function initFracIrr(){
|
||
const c = document.getElementById('fri-c');
|
||
if(!c) return;
|
||
const a = document.getElementById('fri-a');
|
||
const b = document.getElementById('fri-b');
|
||
function gcd(x,y){return y?gcd(y,x%y):x}
|
||
function upd(){
|
||
const cv = +c.value, av = +a.value, bv = +b.value;
|
||
if(!av || !bv){ document.getElementById('fri-out').innerHTML='— —'; return; }
|
||
// c/(a√b) = c·√b / (a·b)
|
||
const num = cv;
|
||
const denom = av * bv;
|
||
const g = gcd(Math.abs(num), Math.abs(denom));
|
||
const nn = num/g, dd = denom/g;
|
||
const lines = [
|
||
'<b>Шаг 1.</b> Домножим на $\\dfrac{\\sqrt{b}}{\\sqrt{b}}$:',
|
||
`\\[\\dfrac{${cv}}{${av}\\sqrt{${bv}}} = \\dfrac{${cv} \\cdot \\sqrt{${bv}}}{${av}\\sqrt{${bv}} \\cdot \\sqrt{${bv}}} = \\dfrac{${cv}\\sqrt{${bv}}}{${av} \\cdot ${bv}} = \\dfrac{${cv}\\sqrt{${bv}}}{${denom}}\\]`,
|
||
'<b>Шаг 2.</b> Сократим:',
|
||
`\\[\\dfrac{${cv}\\sqrt{${bv}}}{${denom}} = ${dd === 1 ? nn + '\\sqrt{' + bv + '}' : '\\dfrac{' + nn + '\\sqrt{' + bv + '}}{' + dd + '}'}\\]`
|
||
];
|
||
document.getElementById('fri-out').innerHTML = lines.join('<br>');
|
||
renderMath(document.getElementById('fri-out'));
|
||
}
|
||
c.addEventListener('input', upd);
|
||
a.addEventListener('input', upd);
|
||
b.addEventListener('input', upd);
|
||
upd();
|
||
}
|
||
|
||
/* ──── Compare ──── */
|
||
const COMP_TASKS = [
|
||
{a:'3√2', av:3*Math.sqrt(2), b:'2√3', bv:2*Math.sqrt(3), aSq:18, bSq:12},
|
||
{a:'4√3', av:4*Math.sqrt(3), b:'3√5', bv:3*Math.sqrt(5), aSq:48, bSq:45},
|
||
{a:'5√2', av:5*Math.sqrt(2), b:'7', bv:7, aSq:50, bSq:49},
|
||
{a:'2√7', av:2*Math.sqrt(7), b:'3√3', bv:3*Math.sqrt(3), aSq:28, bSq:27},
|
||
{a:'√17', av:Math.sqrt(17), b:'4', bv:4, aSq:17, bSq:16},
|
||
{a:'√35', av:Math.sqrt(35), b:'6', bv:6, aSq:35, bSq:36},
|
||
];
|
||
let compIdx = 0;
|
||
function initCompare(){
|
||
compIdx = 0;
|
||
compRender();
|
||
}
|
||
function compRender(){
|
||
const t = COMP_TASKS[compIdx];
|
||
document.getElementById('comp-a').textContent = t.a;
|
||
document.getElementById('comp-b').textContent = t.b;
|
||
document.getElementById('comp-fb').className='feedback';
|
||
}
|
||
function compSet(pick){
|
||
const t = COMP_TASKS[compIdx];
|
||
const correct = (pick === 'a' && t.av > t.bv) || (pick === 'b' && t.bv > t.av);
|
||
const fb = document.getElementById('comp-fb');
|
||
if(correct){
|
||
feedback(fb, true, '✓ Верно! ' + t.a + '² = ' + t.aSq + ', ' + t.b + '² = ' + t.bSq + ' → ' + (t.av > t.bv ? t.a + ' > ' + t.b : t.b + ' > ' + t.a));
|
||
bumpProgress('p4', 3);
|
||
setTimeout(compNext, 1500);
|
||
} else {
|
||
feedback(fb, false, '✗ Не так. ' + t.a + '² = ' + t.aSq + ', ' + t.b + '² = ' + t.bSq);
|
||
}
|
||
}
|
||
function compHint(){
|
||
const t = COMP_TASKS[compIdx];
|
||
const fb = document.getElementById('comp-fb');
|
||
feedback(fb, true, 'Возведите оба в квадрат: ' + t.a + '² = ' + t.aSq + ', ' + t.b + '² = ' + t.bSq);
|
||
}
|
||
function compNext(){
|
||
compIdx = (compIdx + 1) % COMP_TASKS.length;
|
||
compRender();
|
||
}
|
||
|
||
/* ──── Simplify 4 — a√b form ──── */
|
||
const SIMP4_TASKS = [
|
||
{n:72, a:6, b:2}, {n:50, a:5, b:2}, {n:48, a:4, b:3}, {n:200, a:10, b:2},
|
||
{n:75, a:5, b:3}, {n:98, a:7, b:2}, {n:18, a:3, b:2}, {n:128, a:8, b:2},
|
||
{n:80, a:4, b:5}, {n:108, a:6, b:3}, {n:147, a:7, b:3},
|
||
];
|
||
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];
|
||
document.getElementById('simp4-q').textContent = '√' + t.n;
|
||
document.getElementById('simp4-a').value = '';
|
||
document.getElementById('simp4-b').value = '';
|
||
document.getElementById('simp4-fb').className='feedback';
|
||
document.getElementById('simp4-score').textContent = simp4State.score;
|
||
document.getElementById('simp4-cnt').textContent = simp4State.cnt;
|
||
}
|
||
function simp4Check(){
|
||
const t = SIMP4_TASKS[simp4State.idx];
|
||
const a = +document.getElementById('simp4-a').value;
|
||
const b = +document.getElementById('simp4-b').value;
|
||
const fb = document.getElementById('simp4-fb');
|
||
if(a === t.a && b === t.b){
|
||
feedback(fb, true, '✓ Верно! √' + t.n + ' = ' + t.a + '√' + t.b);
|
||
simp4State.score += 10;
|
||
simp4State.cnt++;
|
||
bumpProgress('p4', 2);
|
||
if(simp4State.cnt === 5){ achievement('simp4','Тренажёр упрощения корней'); }
|
||
confetti();
|
||
setTimeout(simp4Next, 1000);
|
||
} else {
|
||
feedback(fb, false, '✗ Не верно. Правильно: ' + t.a + '√' + t.b);
|
||
}
|
||
}
|
||
function simp4Next(){
|
||
simp4State.idx = (simp4State.idx + 1) % SIMP4_TASKS.length;
|
||
simp4Render();
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
§ 5. Числовые промежутки. Объединение и пересечение
|
||
════════════════════════════════════════════════════════ */
|
||
function buildP5(){
|
||
const body = document.getElementById('p5-body');
|
||
body.innerHTML = `
|
||
${makeCard('theory','Что такое числовой промежуток',null,`
|
||
<p>Часто в задачах нужно описать множество чисел: «все, что больше 2 и меньше 5», или «все неотрицательные». Для этого есть удобная запись — <b>числовой промежуток</b>.</p>
|
||
<p>На координатной прямой промежуток — это часть прямой между двумя точками (или одна точка с лучом).</p>
|
||
`)}
|
||
|
||
${makeCard('rule','9 типов промежутков',null,`
|
||
<table class="tbl">
|
||
<tr><th>Запись</th><th>Неравенство</th><th>Изображение</th><th>Название</th></tr>
|
||
<tr><td>$(a; b)$</td><td>$a < x < b$</td><td>○━━○</td><td>интервал (открытый)</td></tr>
|
||
<tr><td>$[a; b]$</td><td>$a \\leq x \\leq b$</td><td>●━━●</td><td>отрезок (закрытый)</td></tr>
|
||
<tr><td>$[a; b)$</td><td>$a \\leq x < b$</td><td>●━━○</td><td>полуинтервал</td></tr>
|
||
<tr><td>$(a; b]$</td><td>$a < x \\leq b$</td><td>○━━●</td><td>полуинтервал</td></tr>
|
||
<tr><td>$(-\\infty; a)$</td><td>$x < a$</td><td>━━━○</td><td>открытый луч</td></tr>
|
||
<tr><td>$(-\\infty; a]$</td><td>$x \\leq a$</td><td>━━━●</td><td>закрытый луч</td></tr>
|
||
<tr><td>$(a; +\\infty)$</td><td>$x > a$</td><td>○━━━</td><td>открытый луч</td></tr>
|
||
<tr><td>$[a; +\\infty)$</td><td>$x \\geq a$</td><td>●━━━</td><td>закрытый луч</td></tr>
|
||
<tr><td>$(-\\infty; +\\infty)$</td><td>$x \\in \\mathbb{R}$</td><td>━━━━</td><td>вся прямая</td></tr>
|
||
</table>
|
||
<div class="note-warn">○ — точка <b>не</b> входит (строгое неравенство, круглая скобка). ● — точка <b>входит</b> (нестрогое, квадратная скобка).</div>
|
||
`)}
|
||
|
||
${widget('Конструктор промежутка', 'BUILD', 'Тяните точки на оси. Кликайте по скобкам, чтобы переключать круглые/квадратные. Все 3 формы записи обновляются сразу.', `
|
||
<div id="cb-line" style="position:relative;height:120px;background:var(--card);border:1px solid var(--border);border-radius:9px;margin-bottom:14px"></div>
|
||
<div class="row-c">
|
||
<span class="lab">Левая скобка:</span>
|
||
<button id="cb-lb" class="btn" onclick="cbToggle('l')">[</button>
|
||
<span class="lab">Правая скобка:</span>
|
||
<button id="cb-rb" class="btn" onclick="cbToggle('r')">]</button>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:14px;text-align:center">
|
||
<div style="padding:12px;background:var(--card);border:1.5px solid var(--pri);border-radius:9px">
|
||
<div class="lab">Интервал</div>
|
||
<div id="cb-int" class="lab-mono" style="font-size:1.2rem;color:var(--pri2);margin-top:6px">[1; 5]</div>
|
||
</div>
|
||
<div style="padding:12px;background:var(--card);border:1.5px solid var(--acc);border-radius:9px">
|
||
<div class="lab">Неравенство</div>
|
||
<div id="cb-ineq" class="lab-mono" style="font-size:1.1rem;color:var(--acc2);margin-top:6px">1 ≤ x ≤ 5</div>
|
||
</div>
|
||
<div style="padding:12px;background:var(--card);border:1.5px solid var(--ok);border-radius:9px">
|
||
<div class="lab">x принадлежит</div>
|
||
<div id="cb-membership" class="lab-mono" style="font-size:1rem;color:var(--ok);margin-top:6px">x ∈ [1; 5]</div>
|
||
</div>
|
||
</div>
|
||
`)}
|
||
|
||
${makeCard('rule','Объединение и пересечение',null,`
|
||
<p>Если есть два множества $A$ и $B$:</p>
|
||
<ul>
|
||
<li><b>Объединение</b> $A \\cup B$ — все элементы, которые принадлежат хотя бы одному из них (логическое <b>«или»</b>).</li>
|
||
<li><b>Пересечение</b> $A \\cap B$ — элементы, которые принадлежат <b>обоим</b> сразу (логическое <b>«и»</b>).</li>
|
||
</ul>
|
||
<p><b>Пример:</b> $A = [2; 6]$, $B = [4; 9]$.</p>
|
||
<ul>
|
||
<li>$A \\cup B = [2; 9]$ — слияние</li>
|
||
<li>$A \\cap B = [4; 6]$ — перекрытие</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${widget('Объединение и пересечение визуально', 'VISUAL', 'Настройте границы двух промежутков A и B. Кнопки внизу строят ∪ и ∩.', `
|
||
<div style="margin-bottom:8px">
|
||
<div class="lab">A = [<span id="ai-a">2</span>; <span id="ai-b">6</span>]</div>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<input id="ai-a-s" type="range" class="slider" min="-5" max="10" value="2" step="0.5" style="flex:1">
|
||
<input id="ai-b-s" type="range" class="slider" min="-5" max="10" value="6" step="0.5" style="flex:1">
|
||
</div>
|
||
</div>
|
||
<div style="margin-bottom:14px">
|
||
<div class="lab">B = [<span id="bi-a">4</span>; <span id="bi-b">9</span>]</div>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<input id="bi-a-s" type="range" class="slider" min="-5" max="10" value="4" step="0.5" style="flex:1">
|
||
<input id="bi-b-s" type="range" class="slider" min="-5" max="10" value="9" step="0.5" style="flex:1">
|
||
</div>
|
||
</div>
|
||
<div id="ai-vis" style="position:relative;height:170px;background:var(--card);border:1px solid var(--border);border-radius:9px"></div>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<div class="chip">A ∪ B: <b id="ai-union">[2; 9]</b></div>
|
||
<div class="chip acc">A ∩ B: <b id="ai-inter">[4; 6]</b></div>
|
||
</div>
|
||
`)}
|
||
|
||
${widget('Эйлеровы диаграммы для ∪ и ∩', 'VISUAL', 'Альтернативная визуализация: каждый промежуток — капсула (по точкам). Пересечение — общая часть. Объединение — оба вместе.', `
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">
|
||
<div>
|
||
<div class="lab">A = [<span id="ev-a-lo">2</span>; <span id="ev-a-hi">6</span>]</div>
|
||
<input id="ev-a-lo-s" type="range" class="slider" min="-5" max="10" step="0.5" value="2">
|
||
<input id="ev-a-hi-s" type="range" class="slider" min="-5" max="10" step="0.5" value="6" style="margin-top:4px">
|
||
</div>
|
||
<div>
|
||
<div class="lab">B = [<span id="ev-b-lo">4</span>; <span id="ev-b-hi">9</span>]</div>
|
||
<input id="ev-b-lo-s" type="range" class="slider" min="-5" max="10" step="0.5" value="4">
|
||
<input id="ev-b-hi-s" type="range" class="slider" min="-5" max="10" step="0.5" value="9" style="margin-top:4px">
|
||
</div>
|
||
</div>
|
||
<svg id="ev-svg" viewBox="0 0 520 280" style="width:100%;max-width:560px;display:block;margin:0 auto"></svg>
|
||
<div class="row-c" style="margin-top:12px">
|
||
<button class="btn" id="ev-show-union" onclick="evMode('union')">Показать A ∪ B</button>
|
||
<button class="btn" id="ev-show-inter" onclick="evMode('inter')">Показать A ∩ B</button>
|
||
<button class="btn" onclick="evMode('both')">Оба</button>
|
||
</div>
|
||
<div id="ev-result" class="ev-result"></div>
|
||
`)}
|
||
|
||
${makeCard('example','Примеры',null,`
|
||
<ul>
|
||
<li>$(-\\infty; 3) \\cup [3; 7] = (-\\infty; 7]$</li>
|
||
<li>$[-2; 5] \\cap [0; 8] = [0; 5]$</li>
|
||
<li>$(1; 4) \\cap (6; 10) = \\emptyset$ (пустое — нет общих)</li>
|
||
<li>$[0; 5] \\cup [7; 12] = [0; 5] \\cup [7; 12]$ (не сливается — есть «дырка»)</li>
|
||
</ul>
|
||
`)}
|
||
|
||
${widget('«Запиши неравенство по картинке»', 'PRACTICE', 'Посмотрите на промежуток и введите соответствующее неравенство. Используйте < или ≤ (введите <= для ≤).', `
|
||
<div id="pic-svg-box" style="margin-bottom:14px"></div>
|
||
<div class="row-c">
|
||
<span class="lab">x</span>
|
||
<select id="pic-rel1" class="inp" style="width:auto">
|
||
<option value="">—</option>
|
||
<option><</option>
|
||
<option>≤</option>
|
||
<option>></option>
|
||
<option>≥</option>
|
||
</select>
|
||
<input id="pic-num1" class="inp num" type="number" step="any">
|
||
<span class="lab">и x</span>
|
||
<select id="pic-rel2" class="inp" style="width:auto">
|
||
<option value="">—</option>
|
||
<option><</option>
|
||
<option>≤</option>
|
||
<option>></option>
|
||
<option>≥</option>
|
||
</select>
|
||
<input id="pic-num2" class="inp num" type="number" step="any">
|
||
<button class="btn primary" onclick="picCheck()">Проверить</button>
|
||
<button class="btn" onclick="picNext()">Дальше</button>
|
||
</div>
|
||
<div id="pic-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${widget('«Нарисуй по записи»', 'PRACTICE', 'Вам даётся запись — изобразите промежуток. Меняйте границы и скобки.', `
|
||
<div id="draw-task" style="text-align:center;padding:12px;background:var(--card);border-radius:9px;margin-bottom:14px">
|
||
<div class="lab">Изобразите:</div>
|
||
<div id="draw-q" class="lab-mono" style="font-size:1.5rem;color:var(--pri2);margin-top:6px">x ∈ [−2; 4)</div>
|
||
</div>
|
||
<div id="draw-line" style="position:relative;height:120px;background:var(--card);border:1px solid var(--border);border-radius:9px;margin-bottom:14px"></div>
|
||
<div class="row-c">
|
||
<span class="lab">Левая граница:</span>
|
||
<input id="draw-l" type="number" class="inp num" value="0" step="any">
|
||
<button id="draw-lb" class="btn" onclick="drawToggle('l')">(</button>
|
||
<span class="lab">Правая граница:</span>
|
||
<input id="draw-r" type="number" class="inp num" value="0" step="any">
|
||
<button id="draw-rb" class="btn" onclick="drawToggle('r')">)</button>
|
||
</div>
|
||
<div class="row-c" style="margin-top:10px">
|
||
<button class="btn primary" onclick="drawCheck()">Проверить</button>
|
||
<button class="btn" onclick="drawNext()">Другая задача</button>
|
||
</div>
|
||
<div id="draw-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('class','Задания для класса',null,`
|
||
<ol style="padding-left:22px">
|
||
<li>Изобразите на координатной прямой и запишите промежуток: $x > 2$; $x \\leq -1$; $-3 \\leq x < 5$.</li>
|
||
<li>Найдите $[-2; 4] \\cap [0; 7]$, $[-2; 4] \\cup [0; 7]$.</li>
|
||
<li>Запишите промежуток, если $x \\geq 6/11$.</li>
|
||
<li>Изобразите $(-\\infty; -1) \\cup [3; +\\infty)$.</li>
|
||
</ol>
|
||
<details class="spoiler">
|
||
<summary>Подсказки</summary>
|
||
<div class="spoiler-body">
|
||
1) $(2; +\\infty)$; $(-\\infty; -1]$; $[-3; 5)$.
|
||
2) ∩ = $[0; 4]$; ∪ = $[-2; 7]$.
|
||
3) $[6/11; +\\infty)$.
|
||
4) Два луча с «дыркой» между −1 (исключ.) и 3 (включ.).
|
||
</div>
|
||
</details>
|
||
`)}
|
||
|
||
${bossWidget('p5')}
|
||
|
||
${secNav('p4', 'p6')}
|
||
`;
|
||
renderMath(body);
|
||
setTimeout(()=>{ initIntervalBuilder(); initUnionInter(); initPicTask(); initDrawTask(); }, 50);
|
||
}
|
||
|
||
/* ──── Interval Builder ──── */
|
||
const CB = { a:1, b:5, lOpen:false, rOpen:false };
|
||
function initIntervalBuilder(){
|
||
cbRender();
|
||
}
|
||
function cbRender(){
|
||
const line = document.getElementById('cb-line');
|
||
if(!line) return;
|
||
line.innerHTML = '';
|
||
const lo = -2, hi = 10;
|
||
// axis
|
||
const axis = el('div', {style:'position:absolute;top:60px;left:3%;right:3%;height:2px;background:var(--text)'});
|
||
line.appendChild(axis);
|
||
for(let i = lo; i <= hi; i++){
|
||
const x = 3 + (i - lo) / (hi - lo) * 94;
|
||
const t = el('div', {style:`position:absolute;top:54px;left:${x}%;width:2px;height:14px;background:var(--text);transform:translateX(-50%)`});
|
||
line.appendChild(t);
|
||
const lab = el('div', {style:`position:absolute;top:72px;left:${x}%;transform:translateX(-50%);font-size:.74rem;font-family:'JetBrains Mono',monospace;color:var(--muted)`}, ''+i);
|
||
line.appendChild(lab);
|
||
}
|
||
// interval bar
|
||
const xa = 3 + (CB.a - lo) / (hi - lo) * 94;
|
||
const xb = 3 + (CB.b - lo) / (hi - lo) * 94;
|
||
const bar = el('div', {style:`position:absolute;top:57px;left:${Math.min(xa,xb)}%;width:${Math.abs(xb-xa)}%;height:8px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:4px`});
|
||
line.appendChild(bar);
|
||
// endpoints
|
||
const a_dot = el('div', {style:`position:absolute;top:51px;left:${xa}%;width:20px;height:20px;border-radius:50%;background:${CB.lOpen ? 'var(--card)' : 'var(--pri)'};border:3px solid var(--pri);transform:translateX(-50%);cursor:grab;z-index:3`});
|
||
a_dot.title = 'Граница A — тяни';
|
||
a_dot.addEventListener('mousedown', e=>cbDragStart(e, 'a'));
|
||
a_dot.addEventListener('touchstart', e=>cbDragStart(e, 'a'), {passive:true});
|
||
line.appendChild(a_dot);
|
||
const b_dot = el('div', {style:`position:absolute;top:51px;left:${xb}%;width:20px;height:20px;border-radius:50%;background:${CB.rOpen ? 'var(--card)' : 'var(--pri)'};border:3px solid var(--pri);transform:translateX(-50%);cursor:grab;z-index:3`});
|
||
b_dot.addEventListener('mousedown', e=>cbDragStart(e, 'b'));
|
||
b_dot.addEventListener('touchstart', e=>cbDragStart(e, 'b'), {passive:true});
|
||
line.appendChild(b_dot);
|
||
// labels
|
||
document.getElementById('cb-lb').textContent = CB.lOpen ? '(' : '[';
|
||
document.getElementById('cb-rb').textContent = CB.rOpen ? ')' : ']';
|
||
const lA = CB.lOpen ? '(' : '[';
|
||
const lB = CB.rOpen ? ')' : ']';
|
||
const rA = CB.lOpen ? '<' : '≤';
|
||
const rB = CB.rOpen ? '<' : '≤';
|
||
document.getElementById('cb-int').textContent = `${lA}${CB.a}; ${CB.b}${lB}`;
|
||
document.getElementById('cb-ineq').textContent = `${CB.a} ${rA} x ${rB} ${CB.b}`;
|
||
document.getElementById('cb-membership').textContent = `x ∈ ${lA}${CB.a}; ${CB.b}${lB}`;
|
||
}
|
||
function cbToggle(side){
|
||
if(side === 'l') CB.lOpen = !CB.lOpen;
|
||
else CB.rOpen = !CB.rOpen;
|
||
cbRender();
|
||
bumpProgress('p5', 1);
|
||
}
|
||
let cbDrag = null;
|
||
function cbDragStart(e, which){
|
||
cbDrag = which;
|
||
}
|
||
document.addEventListener('mousemove', cbDragMove);
|
||
document.addEventListener('touchmove', cbDragMove, {passive:false});
|
||
document.addEventListener('mouseup', ()=>cbDrag=null);
|
||
document.addEventListener('touchend', ()=>cbDrag=null);
|
||
function cbDragMove(e){
|
||
if(!cbDrag) return;
|
||
const line = document.getElementById('cb-line');
|
||
if(!line) return;
|
||
e.preventDefault && e.preventDefault();
|
||
const rect = line.getBoundingClientRect();
|
||
const cx = (e.clientX || (e.touches && e.touches[0].clientX) || 0);
|
||
const pct = (cx - rect.left) / rect.width * 100;
|
||
const v = Math.round(((pct - 3) / 94 * 12 - 2) * 2) / 2;
|
||
if(cbDrag === 'a') CB.a = Math.max(-2, Math.min(CB.b - 0.5, v));
|
||
else CB.b = Math.max(CB.a + 0.5, Math.min(10, v));
|
||
cbRender();
|
||
}
|
||
|
||
/* ──── Union/Intersection ──── */
|
||
function initUnionInter(){
|
||
['ai-a-s','ai-b-s','bi-a-s','bi-b-s'].forEach(id=>{
|
||
const e = document.getElementById(id);
|
||
if(e) e.addEventListener('input', aiUpd);
|
||
});
|
||
aiUpd();
|
||
}
|
||
function aiUpd(){
|
||
const aa = +document.getElementById('ai-a-s').value;
|
||
const ab = +document.getElementById('ai-b-s').value;
|
||
const ba = +document.getElementById('bi-a-s').value;
|
||
const bb = +document.getElementById('bi-b-s').value;
|
||
const A = [Math.min(aa,ab), Math.max(aa,ab)];
|
||
const B = [Math.min(ba,bb), Math.max(ba,bb)];
|
||
document.getElementById('ai-a').textContent = A[0];
|
||
document.getElementById('ai-b').textContent = A[1];
|
||
document.getElementById('bi-a').textContent = B[0];
|
||
document.getElementById('bi-b').textContent = B[1];
|
||
// union and intersection
|
||
let inter = null;
|
||
if(A[1] >= B[0] && B[1] >= A[0]) inter = [Math.max(A[0],B[0]), Math.min(A[1],B[1])];
|
||
let union = null;
|
||
if(inter) union = [Math.min(A[0],B[0]), Math.max(A[1],B[1])];
|
||
document.getElementById('ai-union').textContent = union ? `[${union[0]}; ${union[1]}]` : `[${A[0]};${A[1]}] ∪ [${B[0]};${B[1]}]`;
|
||
document.getElementById('ai-inter').textContent = inter ? `[${inter[0]}; ${inter[1]}]` : '∅ (пусто)';
|
||
// visual
|
||
const vis = document.getElementById('ai-vis');
|
||
vis.innerHTML = '';
|
||
const lo = -5, hi = 10;
|
||
function bar(y, range, col){
|
||
if(!range) return;
|
||
const x1 = 3 + (range[0] - lo) / (hi - lo) * 94;
|
||
const x2 = 3 + (range[1] - lo) / (hi - lo) * 94;
|
||
const div = el('div', {style:`position:absolute;top:${y}px;left:${x1}%;width:${x2-x1}%;height:10px;background:${col};border-radius:5px`});
|
||
vis.appendChild(div);
|
||
}
|
||
// axis
|
||
vis.appendChild(el('div', {style:'position:absolute;top:140px;left:3%;right:3%;height:2px;background:var(--text)'}));
|
||
for(let i = lo; i <= hi; i++){
|
||
const x = 3 + (i - lo) / (hi - lo) * 94;
|
||
vis.appendChild(el('div', {style:`position:absolute;top:152px;left:${x}%;transform:translateX(-50%);font-size:.72rem;color:var(--muted);font-family:'JetBrains Mono',monospace`}, ''+i));
|
||
}
|
||
bar(20, A, 'var(--pri)');
|
||
vis.appendChild(el('div', {style:'position:absolute;top:20px;left:3px;font-size:.78rem;font-weight:700;color:var(--pri)'}, 'A'));
|
||
bar(50, B, 'var(--acc)');
|
||
vis.appendChild(el('div', {style:'position:absolute;top:50px;left:3px;font-size:.78rem;font-weight:700;color:var(--acc2)'}, 'B'));
|
||
if(union){ bar(80, union, 'rgba(245,158,11,.7)'); vis.appendChild(el('div', {style:'position:absolute;top:80px;left:3px;font-size:.78rem;font-weight:700;color:var(--warn)'}, 'A∪B')); }
|
||
if(inter){ bar(110, inter, 'var(--ok)'); vis.appendChild(el('div', {style:'position:absolute;top:110px;left:3px;font-size:.78rem;font-weight:700;color:var(--ok)'}, 'A∩B')); }
|
||
}
|
||
|
||
/* ──── Pic task ──── */
|
||
const PIC_TASKS = [
|
||
{a:-3, b:5, lOpen:false, rOpen:true, descA:'≥', descB:'<'},
|
||
{a:0, b:8, lOpen:true, rOpen:false, descA:'>', descB:'≤'},
|
||
{a:-5, b:2, lOpen:false, rOpen:false, descA:'≥', descB:'≤'},
|
||
{a:1, b:6, lOpen:true, rOpen:true, descA:'>', descB:'<'},
|
||
];
|
||
let picIdx = 0;
|
||
function initPicTask(){ picIdx = 0; picRender(); }
|
||
function picRender(){
|
||
const t = PIC_TASKS[picIdx];
|
||
const box = document.getElementById('pic-svg-box');
|
||
const w = 600;
|
||
const lo = -6, hi = 10;
|
||
const x1 = 30 + (t.a - lo) / (hi - lo) * (w - 60);
|
||
const x2 = 30 + (t.b - lo) / (hi - lo) * (w - 60);
|
||
let html = `<svg viewBox="0 0 ${w} 80" style="width:100%;max-width:560px;background:var(--card);border:1px solid var(--border);border-radius:9px">
|
||
<line x1="20" y1="40" x2="${w-20}" y2="40" stroke="currentColor" stroke-width="2"/>`;
|
||
for(let i = lo; i <= hi; i++){
|
||
const x = 30 + (i - lo) / (hi - lo) * (w - 60);
|
||
html += `<line x1="${x}" y1="34" x2="${x}" y2="46" stroke="currentColor" stroke-width="1.5"/>`;
|
||
html += `<text x="${x}" y="64" text-anchor="middle" font-size="12" fill="currentColor">${i}</text>`;
|
||
}
|
||
html += `<rect x="${x1}" y="36" width="${x2-x1}" height="8" fill="rgba(233,30,99,.5)" rx="4"/>`;
|
||
html += `<circle cx="${x1}" cy="40" r="7" fill="${t.lOpen?'white':'#e91e63'}" stroke="#e91e63" stroke-width="2.5"/>`;
|
||
html += `<circle cx="${x2}" cy="40" r="7" fill="${t.rOpen?'white':'#e91e63'}" stroke="#e91e63" stroke-width="2.5"/>`;
|
||
html += `</svg>`;
|
||
box.innerHTML = html;
|
||
document.getElementById('pic-fb').className='feedback';
|
||
document.getElementById('pic-num1').value = '';
|
||
document.getElementById('pic-num2').value = '';
|
||
document.getElementById('pic-rel1').value = '';
|
||
document.getElementById('pic-rel2').value = '';
|
||
}
|
||
function picCheck(){
|
||
const t = PIC_TASKS[picIdx];
|
||
const n1 = +document.getElementById('pic-num1').value;
|
||
const n2 = +document.getElementById('pic-num2').value;
|
||
const r1 = document.getElementById('pic-rel1').value;
|
||
const r2 = document.getElementById('pic-rel2').value;
|
||
const fb = document.getElementById('pic-fb');
|
||
// Accept x≥a и x<b OR x>a и x≤b ... depending on task
|
||
// We expect: x [rA] a AND x [rB] b
|
||
// task: a (lOpen → strict?), the lOpen means open bracket (not included → strict)
|
||
// for left: if lOpen → x > a; else x ≥ a
|
||
// for right: if rOpen → x < b; else x ≤ b
|
||
const expectR1 = t.lOpen ? '>' : '≥';
|
||
const expectR2 = t.rOpen ? '<' : '≤';
|
||
// Or reversed order also OK
|
||
let okA = (n1 === t.a && r1 === expectR1 && n2 === t.b && r2 === expectR2);
|
||
let okB = (n2 === t.a && r2 === expectR1 && n1 === t.b && r1 === expectR2);
|
||
if(okA || okB){
|
||
feedback(fb, true, '✓ Верно! ' + (t.lOpen?'(':'[') + t.a + '; ' + t.b + (t.rOpen?')':']'));
|
||
bumpProgress('p5', 3);
|
||
confetti();
|
||
setTimeout(picNext, 1100);
|
||
} else {
|
||
feedback(fb, false, '✗ Не точно. Подсказка: x ' + expectR1 + ' ' + t.a + ' и x ' + expectR2 + ' ' + t.b);
|
||
}
|
||
}
|
||
function picNext(){ picIdx = (picIdx + 1) % PIC_TASKS.length; picRender(); }
|
||
|
||
/* ──── Draw task ──── */
|
||
const DRAW_TASKS = [
|
||
{q:'x ∈ [−2; 4)', a:-2, b:4, lOpen:false, rOpen:true},
|
||
{q:'x ∈ (1; 7]', a:1, b:7, lOpen:true, rOpen:false},
|
||
{q:'x ∈ [0; 5]', a:0, b:5, lOpen:false, rOpen:false},
|
||
{q:'x ∈ (−3; 3)', a:-3, b:3, lOpen:true, rOpen:true},
|
||
];
|
||
const DR = { l:0, r:0, lOpen:false, rOpen:false };
|
||
let drawIdx = 0;
|
||
function initDrawTask(){ drawIdx = 0; drawRender(); }
|
||
function drawRender(){
|
||
const t = DRAW_TASKS[drawIdx];
|
||
document.getElementById('draw-q').textContent = t.q;
|
||
DR.l = 0; DR.r = 0; DR.lOpen = false; DR.rOpen = false;
|
||
document.getElementById('draw-l').value = 0;
|
||
document.getElementById('draw-r').value = 0;
|
||
document.getElementById('draw-lb').textContent = '[';
|
||
document.getElementById('draw-rb').textContent = ']';
|
||
document.getElementById('draw-fb').className = 'feedback';
|
||
drawDraw();
|
||
document.getElementById('draw-l').oninput = ()=>{ DR.l = +document.getElementById('draw-l').value; drawDraw(); };
|
||
document.getElementById('draw-r').oninput = ()=>{ DR.r = +document.getElementById('draw-r').value; drawDraw(); };
|
||
}
|
||
function drawToggle(side){
|
||
if(side==='l'){ DR.lOpen = !DR.lOpen; document.getElementById('draw-lb').textContent = DR.lOpen ? '(' : '['; }
|
||
else { DR.rOpen = !DR.rOpen; document.getElementById('draw-rb').textContent = DR.rOpen ? ')' : ']'; }
|
||
drawDraw();
|
||
}
|
||
function drawDraw(){
|
||
const line = document.getElementById('draw-line');
|
||
if(!line) return;
|
||
line.innerHTML='';
|
||
const lo=-6, hi=10;
|
||
line.appendChild(el('div',{style:'position:absolute;top:60px;left:3%;right:3%;height:2px;background:var(--text)'}));
|
||
for(let i=lo;i<=hi;i++){
|
||
const x = 3 + (i-lo)/(hi-lo)*94;
|
||
line.appendChild(el('div',{style:`position:absolute;top:72px;left:${x}%;transform:translateX(-50%);font-size:.74rem;color:var(--muted);font-family:'JetBrains Mono',monospace`},''+i));
|
||
}
|
||
const x1 = 3 + (Math.min(DR.l,DR.r) - lo)/(hi-lo)*94;
|
||
const x2 = 3 + (Math.max(DR.l,DR.r) - lo)/(hi-lo)*94;
|
||
if(DR.l !== DR.r){
|
||
line.appendChild(el('div',{style:`position:absolute;top:57px;left:${x1}%;width:${x2-x1}%;height:8px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:4px`}));
|
||
}
|
||
const xa = 3 + (DR.l - lo)/(hi-lo)*94;
|
||
const xb = 3 + (DR.r - lo)/(hi-lo)*94;
|
||
line.appendChild(el('div',{style:`position:absolute;top:53px;left:${xa}%;width:14px;height:14px;border-radius:50%;background:${DR.lOpen?'var(--card)':'var(--pri)'};border:2.5px solid var(--pri);transform:translateX(-50%)`}));
|
||
line.appendChild(el('div',{style:`position:absolute;top:53px;left:${xb}%;width:14px;height:14px;border-radius:50%;background:${DR.rOpen?'var(--card)':'var(--pri)'};border:2.5px solid var(--pri);transform:translateX(-50%)`}));
|
||
}
|
||
function drawCheck(){
|
||
const t = DRAW_TASKS[drawIdx];
|
||
const fb = document.getElementById('draw-fb');
|
||
const ok = (DR.l === t.a && DR.r === t.b && DR.lOpen === t.lOpen && DR.rOpen === t.rOpen);
|
||
if(ok){
|
||
feedback(fb, true, '✓ Идеально!');
|
||
bumpProgress('p5', 4);
|
||
achievement('draw','Построил промежуток');
|
||
confetti();
|
||
setTimeout(drawNext, 1000);
|
||
} else {
|
||
feedback(fb, false, '✗ Не совпадает с ' + t.q);
|
||
}
|
||
}
|
||
function drawNext(){ drawIdx = (drawIdx + 1) % DRAW_TASKS.length; drawRender(); }
|
||
</script>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
§ 6. Системы и совокупности линейных неравенств
|
||
════════════════════════════════════════════════════════ */
|
||
function buildP6(){
|
||
const body = document.getElementById('p6-body');
|
||
body.innerHTML = `
|
||
${makeCard('theory','Две задачи — два знака',null,`
|
||
<p>Иногда условие задачи требует, чтобы число удовлетворяло <b>сразу нескольким</b> неравенствам. Бывает два вида:</p>
|
||
<ul>
|
||
<li>Все неравенства должны выполняться <b>одновременно</b> (логическое <b>«и»</b>) — это <b>система</b>, обозначается фигурной скобкой <b>{</b>.</li>
|
||
<li>Достаточно, чтобы выполнялось <b>хотя бы одно</b> (логическое <b>«или»</b>) — это <b>совокупность</b>, обозначается квадратной <b>[</b>.</li>
|
||
</ul>
|
||
<div class="formula-box">Система: $\\begin{cases}x > 2 \\\\ x \\leq 5\\end{cases}$ → решение: $x \\in (2; 5]$ — <b>пересечение</b></div>
|
||
<div class="formula-box">Совокупность: $\\left[\\begin{array}{l}x \\leq 2 \\\\ x > 5\\end{array}\\right.$ → решение: $x \\in (-\\infty; 2] \\cup (5; +\\infty)$ — <b>объединение</b></div>
|
||
`)}
|
||
|
||
${makeCard('algo','Алгоритм решения',null,`
|
||
<ol style="padding-left:22px">
|
||
<li>Решить каждое неравенство отдельно.</li>
|
||
<li>Изобразить решения каждого на одной координатной прямой.</li>
|
||
<li>Для <b>системы</b> взять <b>пересечение</b> (общую часть).</li>
|
||
<li>Для <b>совокупности</b> взять <b>объединение</b> (всё, что попало хотя бы в одно).</li>
|
||
<li>Записать ответ в виде промежутка или объединения промежутков.</li>
|
||
</ol>
|
||
`)}
|
||
|
||
${makeCard('example','Решённые примеры',null,`
|
||
<p><b>Пример 1.</b> Решить систему: $\\begin{cases}3x + 6 \\geq 0 \\\\ 5 - 2x > 1\\end{cases}$</p>
|
||
<p>Решаем каждое:</p>
|
||
<ul>
|
||
<li>$3x + 6 \\geq 0 \\implies x \\geq -2 \\implies x \\in [-2; +\\infty)$</li>
|
||
<li>$5 - 2x > 1 \\implies -2x > -4 \\implies x < 2 \\implies x \\in (-\\infty; 2)$</li>
|
||
</ul>
|
||
<p>Пересечение: $[-2; +\\infty) \\cap (-\\infty; 2) = [-2; 2)$.</p>
|
||
<p><b>Пример 2.</b> Двойное неравенство $-3 < 2x + 1 \\leq 7$. Решим как систему: $-3 < 2x + 1$ и $2x + 1 \\leq 7$. Получаем $x > -2$ и $x \\leq 3$, то есть $x \\in (-2; 3]$.</p>
|
||
`)}
|
||
|
||
${widget('Решатель системы линейных неравенств', 'CALC', 'Введите до 5 неравенств $ax+b$ ≷ $c$. Используйте кнопки «+ Добавить» / «×» для управления строками.', `
|
||
<div id="sys-list"></div>
|
||
<div class="row-c" style="margin-bottom:14px">
|
||
<button class="btn ok" onclick="sysAddRow(1,0,'≥',0)">+ Добавить неравенство</button>
|
||
</div>
|
||
<div id="sys-line" style="position:relative;height:170px;background:var(--card);border:1px solid var(--border);border-radius:9px"></div>
|
||
<div class="row-c" style="margin-top:12px">
|
||
<button class="btn" onclick="sysMode('sys')">Система ∩</button>
|
||
<button class="btn acc" onclick="sysMode('un')">Совокупность ∪</button>
|
||
</div>
|
||
<div class="row-c" style="margin-top:8px">
|
||
<div class="chip ok">Ответ: <b id="sys-answer">[-2; 2)</b></div>
|
||
</div>
|
||
`)}
|
||
|
||
${widget('Двойное неравенство как система', 'STEPS', 'Введите $a < x < b$ — увидите, что это эквивалентно системе двух неравенств.', `
|
||
<div class="row-c" style="margin-bottom:14px">
|
||
<input id="db-a" class="inp num" type="number" value="-2" step="0.5" style="width:60px">
|
||
<span class="lab" style="font-size:1.3rem">< x <</span>
|
||
<input id="db-b" class="inp num" type="number" value="5" step="0.5" style="width:60px">
|
||
</div>
|
||
<div id="db-out" style="padding:12px;background:var(--card);border-radius:9px;font-family:'JetBrains Mono',monospace;line-height:1.9"></div>
|
||
<div id="db-line" style="position:relative;height:120px;background:var(--card);border:1px solid var(--border);border-radius:9px;margin-top:14px"></div>
|
||
`)}
|
||
|
||
${widget('«Найди целые решения»', 'GAME', 'Дана система — отметьте все целые числа, которые ей удовлетворяют. Промахи — штраф.', `
|
||
<div id="fi-task" style="padding:12px;background:var(--card);border-radius:9px;margin-bottom:14px">
|
||
<div class="lab">Система:</div>
|
||
<div id="fi-q" style="font-size:1.1rem;font-family:'JetBrains Mono',monospace;color:var(--pri2);margin-top:6px"></div>
|
||
</div>
|
||
<div class="lab">Выберите целые решения (x ∈ ℤ):</div>
|
||
<div id="fi-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(50px,1fr));gap:6px;margin-top:8px"></div>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<button class="btn primary" onclick="fiCheck()">Проверить</button>
|
||
<button class="btn" onclick="fiNext()">Другая задача</button>
|
||
<span class="lab">Очки: <b id="fi-score">0</b></span>
|
||
</div>
|
||
<div id="fi-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${widget('Задача «Тариф» (1.382)', 'TASK', 'Оператор мобильной связи предлагает три тарифа. Сколько минут разговора нужно, чтобы тариф А был самым выгодным?', `
|
||
<table class="tbl">
|
||
<tr><th>Тариф</th><th>Абонент. плата (р.)</th><th>Минута (к.)</th></tr>
|
||
<tr><td>А</td><td>12</td><td>8</td></tr>
|
||
<tr><td>Б</td><td>15</td><td>6</td></tr>
|
||
<tr><td>В</td><td>11</td><td>9</td></tr>
|
||
</table>
|
||
<p style="margin:10px 0">Пусть $x$ — число минут. Полная стоимость в копейках:</p>
|
||
<ul>
|
||
<li>A: $1200 + 8x$</li>
|
||
<li>Б: $1500 + 6x$</li>
|
||
<li>В: $1100 + 9x$</li>
|
||
</ul>
|
||
<p>Условие «А самый выгодный»: $A < Б$ и $A < В$, т.е. система: $\\begin{cases}1200 + 8x < 1500 + 6x \\\\ 1200 + 8x < 1100 + 9x\\end{cases}$</p>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<span class="lab">Ваш ответ x ∈:</span>
|
||
<span class="lab-mono">(</span>
|
||
<input id="tar-a" class="inp num" type="number" placeholder="a" style="width:70px">
|
||
<span class="lab-mono">;</span>
|
||
<input id="tar-b" class="inp num" type="number" placeholder="b или ∞" style="width:90px">
|
||
<span class="lab-mono">)</span>
|
||
<button class="btn primary" onclick="tarCheck()">Проверить</button>
|
||
<button class="btn" onclick="tarHint()">Подсказка</button>
|
||
</div>
|
||
<div id="tar-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('home','Домашнее задание',null,`
|
||
<ol style="padding-left:22px">
|
||
<li>Решите систему: $\\begin{cases}3x - 6 \\geq 0 \\\\ 5 - x > 0\\end{cases}$</li>
|
||
<li>Решите совокупность: $\\left[\\begin{array}{l}x \\leq 4 \\\\ x \\geq 6\\end{array}\\right.$</li>
|
||
<li>Решите двойное: $-1 < 3x + 2 \\leq 8$.</li>
|
||
<li>При каких значениях $x$ выражение $\\sqrt{x - 3} + \\sqrt{7 - x}$ имеет смысл?</li>
|
||
</ol>
|
||
<details class="spoiler">
|
||
<summary>Подсказки</summary>
|
||
<div class="spoiler-body">
|
||
1) $[2; 5)$. 2) $(-\\infty; 4] \\cup [6; +\\infty)$. 3) $(-1; 2]$. 4) Нужно одновременно $x-3 \\geq 0$ и $7-x \\geq 0$, т.е. $x \\in [3; 7]$.
|
||
</div>
|
||
</details>
|
||
`)}
|
||
|
||
${bossWidget('p6')}
|
||
|
||
${secNav('p5', 'final')}
|
||
`;
|
||
renderMath(body);
|
||
setTimeout(()=>{ initSysSolver(); initDoubleIneq(); initFindInt(); }, 50);
|
||
}
|
||
|
||
/* ──── Sys Solver (multi-row) ──── */
|
||
let sysCurMode = 'sys';
|
||
let _sysRows = [];
|
||
let _sysDefaults = [{a:3,b:6,op:'≥',c:0},{a:-2,b:5,op:'>',c:1}];
|
||
|
||
function initSysSolver(){
|
||
_sysRows = [];
|
||
_sysDefaults.forEach((d,i) => { _sysRows.push({id:'sr'+(i+1), a:d.a, b:d.b, op:d.op, c:d.c}); });
|
||
_sysRebuildHTML();
|
||
sysUpdate();
|
||
}
|
||
|
||
function sysAddRow(a, b, op, c){
|
||
if(_sysRows.length >= 5) return;
|
||
const id = 'sr' + (_sysRows.length + 1);
|
||
_sysRows.push({id, a:a||1, b:b||0, op:op||'≥', c:c||0});
|
||
_sysRebuildHTML();
|
||
sysUpdate();
|
||
}
|
||
|
||
function _sysRebuildHTML(){
|
||
const list = document.getElementById('sys-list');
|
||
if(!list) return;
|
||
const COLORS = ['var(--pri)','var(--acc)','var(--ok)','var(--warn)','var(--sec-acc,#9333ea)'];
|
||
list.innerHTML = _sysRows.map((r,i)=>`
|
||
<div class="sys-row-card" style="border-color:${COLORS[i%COLORS.length]}">
|
||
<div class="lab" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
||
<span>Неравенство ${i+1}</span>
|
||
${i > 0 ? `<button class="btn small" onclick="sysRemoveRow(${i})">×</button>` : ''}
|
||
</div>
|
||
<div class="row" style="margin-top:2px">
|
||
<input id="${r.id}-a" class="inp num" type="number" value="${r.a}" style="width:50px">
|
||
<span class="lab">x +</span>
|
||
<input id="${r.id}-b" class="inp num" type="number" value="${r.b}" style="width:50px">
|
||
<select id="${r.id}-r" class="inp" style="width:auto">
|
||
<option${r.op==='>' ? ' selected' : ''}>></option>
|
||
<option${r.op==='≥' ? ' selected' : ''}>≥</option>
|
||
<option${r.op==='≤' ? ' selected' : ''}>≤</option>
|
||
<option${r.op==='<' ? ' selected' : ''}><</option>
|
||
</select>
|
||
<input id="${r.id}-c" class="inp num" type="number" value="${r.c}" style="width:50px">
|
||
</div>
|
||
<div id="${r.id}-out" class="chip" style="margin-top:8px;border-color:${COLORS[i%COLORS.length]};color:${COLORS[i%COLORS.length]}">x ?</div>
|
||
</div>
|
||
`).join('');
|
||
_sysRebindHandlers();
|
||
}
|
||
|
||
function _sysRebindHandlers(){
|
||
_sysRows.forEach(r => {
|
||
[r.id+'-a', r.id+'-b', r.id+'-c', r.id+'-r'].forEach(id => {
|
||
const e = document.getElementById(id);
|
||
if(e){ e.addEventListener('input', sysUpdate); e.addEventListener('change', sysUpdate); }
|
||
});
|
||
});
|
||
}
|
||
|
||
function sysRemoveRow(i){
|
||
if(_sysRows.length <= 1) return;
|
||
_sysRows.splice(i, 1);
|
||
// re-assign ids
|
||
_sysRows = _sysRows.map((r,idx) => ({...r, id:'sr'+(idx+1)}));
|
||
_sysRebuildHTML();
|
||
sysUpdate();
|
||
}
|
||
function solveLin(a, b, op, c){
|
||
// a*x + b op c → x op2 (c-b)/a
|
||
// op might flip if a < 0
|
||
let bound = (c - b) / a;
|
||
let strict = (op === '>' || op === '<');
|
||
// Determine x > or x <
|
||
// a*x op (c-b)
|
||
// if a > 0: x op (c-b)/a (same op direction)
|
||
// if a < 0: flip direction
|
||
let dir;
|
||
if(op === '>' || op === '≥') dir = '>';
|
||
else dir = '<';
|
||
if(a < 0) dir = (dir === '>') ? '<' : '>';
|
||
return { bound, strict, dir };
|
||
}
|
||
function sysUpdate(){
|
||
if(!_sysRows.length) return;
|
||
function getRowVal(r){
|
||
const aEl = document.getElementById(r.id+'-a');
|
||
const bEl = document.getElementById(r.id+'-b');
|
||
const cEl = document.getElementById(r.id+'-c');
|
||
const rEl = document.getElementById(r.id+'-r');
|
||
if(!aEl) return null;
|
||
const op = rEl ? rEl.value : '≥';
|
||
// handle < being stored as <
|
||
const opClean = op === '<' ? '<' : op;
|
||
return solveLin(+aEl.value||0, +bEl.value||0, opClean, +cEl.value||0);
|
||
}
|
||
function describe(s){
|
||
return 'x ' + (s.dir === '>' ? (s.strict ? '>' : '≥') : (s.strict ? '<' : '≤')) + ' ' + (Number.isFinite(s.bound) ? +s.bound.toFixed(4) : s.bound);
|
||
}
|
||
function toIntervalSys(s){
|
||
if(s.dir === '>') return { l:s.bound, r:Infinity, lOp:s.strict, rOp:true };
|
||
else return { l:-Infinity, r:s.bound, lOp:true, rOp:s.strict };
|
||
}
|
||
function intersectTwo(A, B){
|
||
const lo = Math.max(A.l, B.l);
|
||
const hi = Math.min(A.r, B.r);
|
||
const loOp = (A.l > B.l) ? A.lOp : (A.l < B.l) ? B.lOp : (A.lOp || B.lOp);
|
||
const hiOp = (A.r < B.r) ? A.rOp : (A.r > B.r) ? B.rOp : (A.rOp || B.rOp);
|
||
if(lo < hi || (lo === hi && !loOp && !hiOp)) return {l:lo, r:hi, lOp:loOp, rOp:hiOp};
|
||
return null;
|
||
}
|
||
const COLORS = ['#e91e63','#03a9f4','#10b981','#f59e0b','#9333ea'];
|
||
const intervals = [];
|
||
_sysRows.forEach((r) => {
|
||
const s = getRowVal(r);
|
||
if(!s || !Number.isFinite(s.bound)) return;
|
||
const outEl = document.getElementById(r.id+'-out');
|
||
if(outEl) outEl.textContent = describe(s);
|
||
intervals.push(toIntervalSys(s));
|
||
});
|
||
let inter = intervals.length ? intervals[0] : null;
|
||
for(let i = 1; i < intervals.length; i++) inter = inter ? intersectTwo(inter, intervals[i]) : null;
|
||
// visualize
|
||
const vis = document.getElementById('sys-line');
|
||
if(!vis) return;
|
||
vis.innerHTML='';
|
||
const VLO = -8, VHI = 12;
|
||
const axisY = 20 + _sysRows.length * 25 + 20;
|
||
vis.style.height = (axisY + 36) + 'px';
|
||
vis.appendChild(el('div',{style:`position:absolute;top:${axisY}px;left:3%;right:3%;height:2px;background:var(--text)`}));
|
||
for(let i = VLO; i <= VHI; i++){
|
||
const x = 3 + (i-VLO)/(VHI-VLO)*94;
|
||
vis.appendChild(el('div',{style:`position:absolute;top:${axisY+12}px;left:${x}%;transform:translateX(-50%);font-size:.72rem;color:var(--muted);font-family:'JetBrains Mono',monospace`}, ''+i));
|
||
}
|
||
function drawInt(y, iv, col, lbl){
|
||
if(!iv) return;
|
||
const l = iv.l === -Infinity ? VLO : iv.l;
|
||
const r = iv.r === Infinity ? VHI : iv.r;
|
||
if(l > VHI || r < VLO) return;
|
||
const xL = 3 + (Math.max(VLO,l) - VLO)/(VHI-VLO)*94;
|
||
const xR = 3 + (Math.min(VHI,r) - VLO)/(VHI-VLO)*94;
|
||
vis.appendChild(el('div',{style:`position:absolute;top:${y}px;left:${xL}%;width:${Math.max(0,xR-xL)}%;height:10px;background:${col};border-radius:5px`}));
|
||
if(iv.l !== -Infinity) vis.appendChild(el('div',{style:`position:absolute;top:${y-2}px;left:${xL}%;width:14px;height:14px;border-radius:50%;background:${iv.lOp?'var(--card)':col};border:2.5px solid ${col};transform:translateX(-50%)`}));
|
||
if(iv.r !== Infinity) vis.appendChild(el('div',{style:`position:absolute;top:${y-2}px;left:${xR}%;width:14px;height:14px;border-radius:50%;background:${iv.rOp?'var(--card)':col};border:2.5px solid ${col};transform:translateX(-50%)`}));
|
||
vis.appendChild(el('div',{style:`position:absolute;top:${y-2}px;left:3px;font-size:.74rem;font-weight:700;color:${col}`}, lbl));
|
||
}
|
||
intervals.forEach((iv, i) => drawInt(10 + i*25, iv, COLORS[i%COLORS.length], (i+1)+')'));
|
||
function fmt(iv){
|
||
if(!iv) return '∅';
|
||
const lA = iv.lOp ? '(' : '[';
|
||
const rA = iv.rOp ? ')' : ']';
|
||
const l = iv.l === -Infinity ? '-∞' : +iv.l.toFixed(4);
|
||
const r = iv.r === Infinity ? '+∞' : +iv.r.toFixed(4);
|
||
return lA + l + '; ' + r + (iv.r === Infinity ? ')' : rA);
|
||
}
|
||
let ansText;
|
||
if(sysCurMode === 'sys'){
|
||
ansText = inter ? fmt(inter) : '∅';
|
||
if(inter) drawInt(axisY - 18, inter, '#10b981', '∩');
|
||
} else {
|
||
// union: just show all individually in green
|
||
intervals.forEach(iv => drawInt(axisY - 18, iv, '#10b981', '∪'));
|
||
if(intervals.length === 2){
|
||
const A2 = intervals[0], B2 = intervals[1];
|
||
if(A2.r >= B2.l && B2.r >= A2.l){
|
||
const merged = {l:Math.min(A2.l,B2.l), r:Math.max(A2.r,B2.r),
|
||
lOp:Math.min(A2.l,B2.l)===A2.l?A2.lOp:B2.lOp,
|
||
rOp:Math.max(A2.r,B2.r)===A2.r?A2.rOp:B2.rOp};
|
||
ansText = fmt(merged);
|
||
} else {
|
||
ansText = fmt(A2) + ' ∪ ' + fmt(B2);
|
||
}
|
||
} else {
|
||
ansText = intervals.map(fmt).join(' ∪ ');
|
||
}
|
||
}
|
||
document.getElementById('sys-answer').textContent = ansText;
|
||
}
|
||
function sysMode(m){
|
||
sysCurMode = m;
|
||
sysUpdate();
|
||
bumpProgress('p6', 2);
|
||
}
|
||
|
||
/* ──── Double inequality ──── */
|
||
function initDoubleIneq(){
|
||
['db-a','db-b'].forEach(id=>{
|
||
const e = document.getElementById(id);
|
||
if(e) e.addEventListener('input', dbUpd);
|
||
});
|
||
dbUpd();
|
||
}
|
||
function dbUpd(){
|
||
const a = +document.getElementById('db-a').value;
|
||
const b = +document.getElementById('db-b').value;
|
||
if(a >= b){ document.getElementById('db-out').innerHTML='Нужно a < b'; return; }
|
||
document.getElementById('db-out').innerHTML = `
|
||
Двойное: <b style="color:var(--pri)">${a} < x < ${b}</b><br>
|
||
⇕ эквивалентно системе:<br>
|
||
<b style="color:var(--acc2)">{ x > ${a}, x < ${b} }</b><br>
|
||
Решение: <b style="color:var(--ok)">x ∈ (${a}; ${b})</b>
|
||
`;
|
||
const line = document.getElementById('db-line');
|
||
line.innerHTML='';
|
||
const lo=-6,hi=10;
|
||
line.appendChild(el('div',{style:'position:absolute;top:60px;left:3%;right:3%;height:2px;background:var(--text)'}));
|
||
for(let i=lo;i<=hi;i++){
|
||
const x = 3+(i-lo)/(hi-lo)*94;
|
||
line.appendChild(el('div',{style:`position:absolute;top:72px;left:${x}%;transform:translateX(-50%);font-size:.72rem;color:var(--muted);font-family:'JetBrains Mono',monospace`}, ''+i));
|
||
}
|
||
if(a > hi || b < lo) return;
|
||
const x1 = 3 + (Math.max(lo,a)-lo)/(hi-lo)*94;
|
||
const x2 = 3 + (Math.min(hi,b)-lo)/(hi-lo)*94;
|
||
line.appendChild(el('div',{style:`position:absolute;top:57px;left:${x1}%;width:${x2-x1}%;height:8px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:4px`}));
|
||
if(a >= lo) line.appendChild(el('div',{style:`position:absolute;top:53px;left:${x1}%;width:14px;height:14px;border-radius:50%;background:var(--card);border:2.5px solid var(--pri);transform:translateX(-50%)`}));
|
||
if(b <= hi) line.appendChild(el('div',{style:`position:absolute;top:53px;left:${x2}%;width:14px;height:14px;border-radius:50%;background:var(--card);border:2.5px solid var(--pri);transform:translateX(-50%)`}));
|
||
}
|
||
|
||
/* ──── Find Integer Solutions ──── */
|
||
const FI_TASKS = [
|
||
{q:'{ x > 3, x ≤ 7 }', sol:[4,5,6,7], range:[1,10]},
|
||
{q:'{ x ≥ -2, x < 4 }', sol:[-2,-1,0,1,2,3], range:[-5,8]},
|
||
{q:'{ 2x ≤ 10, x > 1 }', sol:[2,3,4,5], range:[0,8]},
|
||
{q:'{ x ≥ 0, x ≤ 5 }', sol:[0,1,2,3,4,5], range:[-2,8]},
|
||
];
|
||
let fiIdx = 0, fiScore = 0;
|
||
function initFindInt(){
|
||
fiIdx = 0; fiScore = 0;
|
||
fiRender();
|
||
}
|
||
function fiRender(){
|
||
const t = FI_TASKS[fiIdx];
|
||
document.getElementById('fi-q').textContent = t.q;
|
||
document.getElementById('fi-score').textContent = fiScore;
|
||
const g = document.getElementById('fi-grid');
|
||
g.innerHTML='';
|
||
for(let i = t.range[0]; i <= t.range[1]; i++){
|
||
const b = el('button', {class:'btn', 'data-n':i}, ''+i);
|
||
b.addEventListener('click', ()=>{
|
||
if(b.dataset.picked === '1'){ b.dataset.picked = ''; b.style.background=''; b.style.color=''; }
|
||
else { b.dataset.picked = '1'; b.style.background='var(--pri)'; b.style.color='#fff'; }
|
||
});
|
||
g.appendChild(b);
|
||
}
|
||
document.getElementById('fi-fb').className = 'feedback';
|
||
}
|
||
function fiCheck(){
|
||
const t = FI_TASKS[fiIdx];
|
||
let correct = 0, wrong = 0;
|
||
document.querySelectorAll('#fi-grid button').forEach(b=>{
|
||
const n = +b.dataset.n;
|
||
const picked = b.dataset.picked === '1';
|
||
const inSol = t.sol.includes(n);
|
||
if(picked && inSol) correct++;
|
||
else if(picked && !inSol) wrong++;
|
||
else if(!picked && inSol) wrong++;
|
||
});
|
||
const fb = document.getElementById('fi-fb');
|
||
const total = t.sol.length;
|
||
if(wrong === 0 && correct === total){
|
||
feedback(fb, true, '✓ Все ' + total + ' целых верно!');
|
||
fiScore += 15;
|
||
bumpProgress('p6', 4);
|
||
confetti();
|
||
setTimeout(fiNext, 1200);
|
||
} else {
|
||
feedback(fb, false, 'Правильно отмечено: ' + correct + ' из ' + total + ', ошибок: ' + wrong);
|
||
fiScore = Math.max(0, fiScore - 3);
|
||
}
|
||
document.getElementById('fi-score').textContent = fiScore;
|
||
}
|
||
function fiNext(){ fiIdx = (fiIdx + 1) % FI_TASKS.length; fiRender(); }
|
||
|
||
/* ──── Tariff Task ──── */
|
||
function tarCheck(){
|
||
// A < Б: 1200+8x < 1500+6x → 2x < 300 → x < 150
|
||
// A < В: 1200+8x < 1100+9x → 100 < x → x > 100
|
||
// Решение: 100 < x < 150
|
||
const a = +document.getElementById('tar-a').value;
|
||
const b = +document.getElementById('tar-b').value;
|
||
const fb = document.getElementById('tar-fb');
|
||
if(a === 100 && b === 150){
|
||
feedback(fb, true, '✓ Верно! Тариф А выгоднее всего при 100 < x < 150 минут. Если меньше 100 — выгоден В, больше 150 — Б.');
|
||
bumpProgress('p6', 8);
|
||
achievement('tariff','Задача про тарифы');
|
||
confetti();
|
||
} else {
|
||
feedback(fb, false, 'Не совсем. Решите систему: x > 100 и x < 150. Проверьте арифметику!');
|
||
}
|
||
}
|
||
function tarHint(){
|
||
const fb = document.getElementById('tar-fb');
|
||
feedback(fb, true, 'Подсказка: $A<Б$ даёт $x < 150$, $A<В$ даёт $x > 100$. Пересечение — это и есть ответ.');
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
ФИНАЛ ГЛАВЫ — Итоговая самооценка / Практическая / Увлекательная
|
||
════════════════════════════════════════════════════════ */
|
||
function buildFinal(){
|
||
const body = document.getElementById('final-body');
|
||
body.innerHTML = `
|
||
<div class="card" style="background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border:2px solid var(--pri)">
|
||
<div class="card-body">
|
||
<p style="font-size:1.05rem"><b>Вы прошли всю Главу 1!</b> Здесь — три блока для закрепления: итоговая самооценка (10 задач), практическая математика (3 жизненные задачи) и увлекательная математика (история, исследование, олимпиада).</p>
|
||
</div>
|
||
</div>
|
||
|
||
${makeCard('repeat','Итоговая самооценка',null,`
|
||
<p style="font-size:.9rem;color:var(--muted);margin-bottom:14px">После изучения главы вы должны уметь:</p>
|
||
<ul style="font-size:.88rem">
|
||
<li>находить арифметический квадратный корень из числа;</li>
|
||
<li>применять свойства корней для вычисления и преобразований;</li>
|
||
<li>определять, к каким числовым множествам принадлежит число;</li>
|
||
<li>применять числовые промежутки, их пересечение и объединение;</li>
|
||
<li>решать системы и совокупности линейных неравенств;</li>
|
||
<li>решать двойные неравенства.</li>
|
||
</ul>
|
||
`)}
|
||
|
||
<div class="card" id="ass-card">
|
||
<div class="card-header">
|
||
<div class="card-icon class">${ICONS['class']}</div>
|
||
<div class="card-title">Я проверяю свои знания</div>
|
||
<div class="card-num"><span id="ass-score">0</span>/10</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="ass-list"></div>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<button class="btn primary" onclick="assCheckAll()">Проверить всё</button>
|
||
<button class="btn" onclick="assReset()">Сбросить</button>
|
||
</div>
|
||
<div id="ass-fb" class="feedback"></div>
|
||
</div>
|
||
</div>
|
||
|
||
${makeCard('home','Практическая математика — задача 1',null,`
|
||
<p>На дачном участке дорожка вымощена <b>восемью</b> одинаковыми квадратными плитками. Площадь одной плитки — <b>36 дм²</b>. По обе стороны дорожки планируют высадить кусты роз на расстоянии <b>0,4 м</b> друг от друга. Сколько кустов нужно приобрести, если высадка цветов начинается с начала дорожки?</p>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<span class="lab">Ответ:</span>
|
||
<input id="pr1-a" class="inp num" type="number" placeholder="кустов">
|
||
<button class="btn primary" onclick="pr1Check()">Проверить</button>
|
||
<button class="btn" onclick="pr1Hint()">Подсказка</button>
|
||
</div>
|
||
<div id="pr1-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('home','Задача 2: фирма цемента',null,`
|
||
<p>Для ремонта планируется купить мешки цемента в одной из трёх фирм:</p>
|
||
<table class="tbl">
|
||
<tr><th>Фирма</th><th>Мешок, р.</th><th>Доставка, р.</th></tr>
|
||
<tr><td>А</td><td>7,6</td><td>32</td></tr>
|
||
<tr><td>Б</td><td>7,5</td><td>42</td></tr>
|
||
<tr><td>В</td><td>8</td><td>бесплатно</td></tr>
|
||
</table>
|
||
<p>При покупке какого <b>наименьшего количества мешков</b> самыми выгодными будут условия фирмы <b>А</b>?</p>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<span class="lab">x ≥ </span>
|
||
<input id="pr2-a" class="inp num" type="number" placeholder="мешков">
|
||
<button class="btn primary" onclick="pr2Check()">Проверить</button>
|
||
<button class="btn" onclick="pr2Hint()">Подсказка</button>
|
||
</div>
|
||
<div id="pr2-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('home','Задача 3: часовые пояса',null,`
|
||
<p>Когда в <b>Париже полдень</b>, в Минске — 14 часов, а в Нью-Йорке — 6 часов утра. Друзья, живущие в Париже, Минске и Нью-Йорке, хотят одновременно выйти в Интернет, чтобы пообщаться. Каждый из них по «своему» времени находится на занятиях с <b>8 до 14 часов</b>, а после <b>22 часов</b> отводится на сон.</p>
|
||
<p>В какое время суток они могут одновременно выйти в инет?</p>
|
||
<div class="row-c" style="margin-top:14px">
|
||
<span class="lab">По времени Минска: с</span>
|
||
<input id="pr3-a" class="inp num" type="number" min="0" max="23" placeholder="часов">
|
||
<span class="lab">до</span>
|
||
<input id="pr3-b" class="inp num" type="number" min="0" max="23" placeholder="часов">
|
||
<button class="btn primary" onclick="pr3Check()">Проверить</button>
|
||
<button class="btn" onclick="pr3Hint()">Подсказка</button>
|
||
</div>
|
||
<div id="pr3-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('prev','Увлекательная математика — История',null,`
|
||
<p><b>Откуда взялся знак √?</b></p>
|
||
<p>В XVI веке немецкий математик <b>Кристоф Рудольф</b> в книге «Coss» (1525) ввёл значок √ — стилизованную букву <b>r</b> от лат. <i>radix</i> (корень). До этого корень обозначали словами «radix» или «R».</p>
|
||
<p><b>Бхаскара</b> (Индия, XII в.) описывал процесс извлечения квадратного корня уже в полном виде. А <b>Герон Александрийский</b> (I в.) знал метод приближённого вычисления: $\\sqrt{a} \\approx \\frac{1}{2}\\left(x_0 + \\frac{a}{x_0}\\right)$ — итерационный, сходится очень быстро.</p>
|
||
<p><b>Пифагорейцы</b> в V в. до н.э. открыли, что $\\sqrt{2}$ нельзя записать как $\\frac{m}{n}$. По преданию, это открытие настолько шокировало их школу, что они скрывали его, а одного из учеников (Гиппаса), разгласившего тайну, утопили!</p>
|
||
`)}
|
||
|
||
${widget('Олимпиадная задача: расшифруй код', 'PUZZLE', 'Дан код: 25 324 441 64 4 1. Каждое число — это квадрат целого. Найдите эти целые и переведите их в буквы русского алфавита (А=1, Б=2, В=3...). Получится слово.', `
|
||
<div style="text-align:center;padding:18px;background:var(--card);border-radius:9px;margin-bottom:14px">
|
||
<div class="lab">Код:</div>
|
||
<div style="font-size:1.6rem;font-weight:800;color:var(--pri2);margin:10px 0;font-family:'JetBrains Mono',monospace;letter-spacing:.1em">25 324 441 64 4 1</div>
|
||
<div class="lab" style="margin-top:14px">Шаг 1: найдите квадратные корни</div>
|
||
<div class="row-c" style="margin-top:8px">
|
||
<span class="lab-mono">√25 =</span><input id="dec-1" class="inp num" type="number" style="width:50px">
|
||
<span class="lab-mono">√324 =</span><input id="dec-2" class="inp num" type="number" style="width:50px">
|
||
<span class="lab-mono">√441 =</span><input id="dec-3" class="inp num" type="number" style="width:50px">
|
||
<span class="lab-mono">√64 =</span><input id="dec-4" class="inp num" type="number" style="width:50px">
|
||
<span class="lab-mono">√4 =</span><input id="dec-5" class="inp num" type="number" style="width:50px">
|
||
<span class="lab-mono">√1 =</span><input id="dec-6" class="inp num" type="number" style="width:50px">
|
||
</div>
|
||
<div class="lab" style="margin-top:14px">Шаг 2: переведите числа в буквы (А=1, Б=2, В=3, Г=4, Д=5...)</div>
|
||
<div class="row-c" style="margin-top:10px">
|
||
<span class="lab">Слово:</span>
|
||
<input id="dec-word" class="inp" type="text" placeholder="напишите слово" style="width:150px">
|
||
<button class="btn primary" onclick="decCheck()">Проверить</button>
|
||
<button class="btn" onclick="decReveal()">Показать ответ</button>
|
||
</div>
|
||
</div>
|
||
<div id="dec-fb" class="feedback"></div>
|
||
`)}
|
||
|
||
${makeCard('theory','Исследовательское задание',null,`
|
||
<p>Найдите информацию о различных способах вычисления квадратных корней больших чисел. Например:</p>
|
||
<ul>
|
||
<li><b>Метод Герона</b> (итерационный): $x_{n+1} = \\frac{1}{2}\\left(x_n + \\frac{a}{x_n}\\right)$. Начните с $x_0 = 1$ и сходитесь за 5-7 итераций к $\\sqrt{a}$ с точностью.</li>
|
||
<li><b>Алгоритм извлечения «в столбик»</b> — как мы изучили в §1.</li>
|
||
<li><b>Метод Ньютона</b> — обобщение метода Герона для произвольных функций.</li>
|
||
<li><b>Двоичный поиск</b> по интервалу.</li>
|
||
</ul>
|
||
<p>Придумайте для друзей задания на вычисление квадратных корней с применением одного из методов!</p>
|
||
`)}
|
||
|
||
${widget('Метод Герона — попробуй сам', 'EXPLORE', 'Введите a и x₀, запустите итерации. Видно, как быстро сходится к √a!', `
|
||
<div class="row-c">
|
||
<span class="lab">Извлечь √</span>
|
||
<input id="her-a" class="inp num" type="number" value="50" style="width:80px">
|
||
<span class="lab">начиная с x₀ =</span>
|
||
<input id="her-x0" class="inp num" type="number" value="7" style="width:80px">
|
||
<button class="btn primary" onclick="heronRun()">Итерации</button>
|
||
</div>
|
||
<div id="her-out" style="margin-top:14px;padding:14px;background:var(--card);border-radius:9px;font-family:'JetBrains Mono',monospace;line-height:1.7;font-size:.92rem"></div>
|
||
`)}
|
||
|
||
${makeCard('theory','Олимпиада — задача 2',null,`
|
||
<p>Найдите все значения числа $a$, для которых выражения $a + \\sqrt{15}$ и $\\frac{1}{a} - \\sqrt{15}$ принимают <b>целые</b> значения.</p>
|
||
<details class="spoiler"><summary>Решение (попробуйте сами, потом откройте)</summary><div class="spoiler-body">
|
||
Пусть $a + \\sqrt{15} = m \\in \\mathbb{Z}$, тогда $a = m - \\sqrt{15}$. Подставим во второе: $\\frac{1}{m - \\sqrt{15}} - \\sqrt{15} = n \\in \\mathbb{Z}$.
|
||
<br>Домножим числитель и знаменатель на $m + \\sqrt{15}$: $\\frac{m + \\sqrt{15}}{m^2 - 15} - \\sqrt{15} = n$.
|
||
<br>$\\sqrt{15} \\cdot \\left(\\frac{1}{m^2-15} - 1\\right) + \\frac{m}{m^2-15} = n$.
|
||
<br>Чтобы это было целым, коэффициент при $\\sqrt{15}$ должен быть 0: $\\frac{1}{m^2-15} = 1 \\implies m^2 - 15 = 1 \\implies m^2 = 16 \\implies m = \\pm 4$.
|
||
<br>Значит, $a = 4 - \\sqrt{15}$ или $a = -4 - \\sqrt{15}$.
|
||
</div></details>
|
||
`)}
|
||
|
||
<div class="card" style="background:linear-gradient(135deg,#fef3c7,var(--pri-soft));border-color:var(--warn);text-align:center;padding:24px">
|
||
<div class="card-body">
|
||
<div style="font-size:3rem;font-weight:900;color:var(--warn);margin-bottom:8px">★</div>
|
||
<h3 style="font-size:1.3rem;color:var(--pri2);margin-bottom:8px">Глава 1 пройдена!</h3>
|
||
<p style="margin-bottom:14px">Вы получили все необходимые знания о квадратных корнях, действительных числах, числовых промежутках и системах неравенств.</p>
|
||
<p style="font-size:.92rem;color:var(--muted)">Дальше — Глава 2 «Квадратные уравнения», где корни наконец заработают на полную мощь!</p>
|
||
<div class="row-c" style="margin-top:18px">
|
||
<button class="btn primary" onclick="goTo('p1')">↻ Повторить главу</button>
|
||
<button class="btn acc" onclick="finalSummary()">Сводка</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${bossWidget('final')}
|
||
|
||
${secNav('p6', null)}
|
||
`;
|
||
renderMath(body);
|
||
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);
|
||
}
|
||
|
||
/* ──── Assessment (10 tasks) ──── */
|
||
const ASSESS = [
|
||
{ q:'Изобразите промежуток: $x > \\frac{1}{2}$. Какая запись верна?',
|
||
opts:['$(-\\infty; \\frac{1}{2})$', '$(\\frac{1}{2}; +\\infty)$', '$[\\frac{1}{2}; +\\infty)$', '$(\\frac{1}{2}; +\\infty]$'], correct:1 },
|
||
{ q:'$\\sqrt{2}$ принадлежит множеству:',
|
||
opts:['$\\mathbb{N}$', '$\\mathbb{Z}$', '$\\mathbb{Q}$', '$\\mathbb{I}$ (иррациональные)'], correct:3 },
|
||
{ q:'Найдите значение: $\\frac{1}{4}\\cdot\\sqrt{16} + \\sqrt{49}$',
|
||
opts:['$7$', '$8$', '$9$', '$11$'], correct:1 },
|
||
{ q:'Решите систему $\\begin{cases}5x+4>0\\\\3x+1{,}5\\leq 0\\end{cases}$',
|
||
opts:['$(-\\frac{4}{5}; -\\frac{1}{2}]$', '$(-\\frac{4}{5}; +\\infty)$', '$(-\\infty; -\\frac{1}{2}]$', '$\\varnothing$ (пусто)'], correct:0 },
|
||
{ q:'$\\sqrt{x \\cdot y}$ при $x=48$, $y=75$ равно:',
|
||
opts:['$58$', '$60$', '$62$', '$65$'], correct:1 },
|
||
{ q:'Из числа вычесть 4, разделить на 9 — меньше 5. Прибавить 8, разделить на 11 — больше 5. Найдите число.',
|
||
opts:['$48$', '$49$', '$50$', 'Любое в $(47; 49)$'], correct:3 },
|
||
{ q:'Упростите $3\\sqrt{5} + 2\\sqrt{20} - \\sqrt{45}$:',
|
||
opts:['$4\\sqrt{5}$', '$2\\sqrt{5}$', '$0$', '$5\\sqrt{5}$'], correct:0 },
|
||
{ q:'Найдите область определения $\\sqrt{2x + \\frac{1}{2}}$:',
|
||
opts:['$x \\geq -\\frac{1}{4}$', '$x \\geq 0$', '$x \\geq \\frac{1}{2}$', '$x \\geq -\\frac{1}{2}$'], correct:0 },
|
||
{ q:'Внесите множитель под корень: $(c-2)\\sqrt{3c-6}$, при $c > 2$',
|
||
opts:['$\\sqrt{3(c-2)^3}$', '$\\sqrt{3c-6}$', '$\\sqrt{(c-2)(3c-6)}$', '$\\sqrt{(c-2)^2(3c-6)}$'], correct:3 },
|
||
{ q:'Упростите: $\\sqrt{7 - \\sqrt{24}}$',
|
||
opts:['$\\sqrt{6} - 1$', '$\\sqrt{5} - 1$', '$\\sqrt{3} - 1$', '$\\sqrt{7} - 1$'], correct:0 },
|
||
];
|
||
let assAnswers = {};
|
||
function buildAssessment(){
|
||
const list = document.getElementById('ass-list');
|
||
assAnswers = {};
|
||
list.innerHTML = '';
|
||
ASSESS.forEach((a, i)=>{
|
||
const div = el('div', {class:'quiz'});
|
||
div.innerHTML = `<div class="quiz-q"><span class="qn">${i+1}</span>${a.q}</div><div class="quiz-opts" id="ass-opts-${i}"></div>`;
|
||
list.appendChild(div);
|
||
const og = div.querySelector('.quiz-opts');
|
||
a.opts.forEach((opt, j)=>{
|
||
const b = el('button', {class:'quiz-opt'}, opt);
|
||
b.addEventListener('click', ()=>{
|
||
og.querySelectorAll('button').forEach(x=>x.style.background='');
|
||
b.style.background='var(--pri-soft)';
|
||
b.style.borderColor='var(--pri)';
|
||
assAnswers[i] = j;
|
||
});
|
||
og.appendChild(b);
|
||
});
|
||
});
|
||
renderMath(list);
|
||
}
|
||
function assCheckAll(){
|
||
let right = 0;
|
||
ASSESS.forEach((a, i)=>{
|
||
const og = document.getElementById('ass-opts-' + i);
|
||
if(!og) return;
|
||
const buttons = og.querySelectorAll('button');
|
||
buttons.forEach((b, j)=>{
|
||
b.classList.remove('correct','wrong');
|
||
if(assAnswers[i] === j){
|
||
if(j === a.correct){ b.classList.add('correct'); right++; }
|
||
else b.classList.add('wrong');
|
||
} else if(j === a.correct){
|
||
b.style.background='var(--ok-bg)';
|
||
}
|
||
});
|
||
});
|
||
document.getElementById('ass-score').textContent = right;
|
||
const fb = document.getElementById('ass-fb');
|
||
if(right >= 8){
|
||
feedback(fb, true, '✓ Отлично! ' + right + '/10. Глава освоена!');
|
||
achievement('ass8','Самооценка 8+/10');
|
||
bumpProgress('final', 50);
|
||
confetti();
|
||
} else if(right >= 5){
|
||
feedback(fb, true, 'Неплохо: ' + right + '/10. Стоит повторить слабые места.');
|
||
bumpProgress('final', 30);
|
||
} else {
|
||
feedback(fb, false, right + '/10 — повторите главу. Особенно §1 и §4.');
|
||
bumpProgress('final', 15);
|
||
}
|
||
}
|
||
function assReset(){ buildAssessment(); document.getElementById('ass-fb').className='feedback'; document.getElementById('ass-score').textContent='0'; }
|
||
|
||
/* ──── Practical task 1: дорожка 8 плиток ──── */
|
||
function pr1Check(){
|
||
// Площадь плитки 36 дм² → сторона 6 дм = 0.6 м. Дорожка 8 плиток = 8·0.6 = 4.8 м.
|
||
// По двум сторонам высадка через 0.4 м с начала: 4.8/0.4 + 1 = 13 кустов с одной стороны, 13 с другой = 26.
|
||
const a = +document.getElementById('pr1-a').value;
|
||
const fb = document.getElementById('pr1-fb');
|
||
if(a === 26){
|
||
feedback(fb, true, '✓ Верно! Сторона плитки = √36 = 6 дм = 0.6 м. Длина дорожки 8 · 0.6 = 4.8 м. На одной стороне 4.8/0.4 + 1 = 13 кустов. Всего 13 · 2 = 26.');
|
||
achievement('pr1','Дорожка с розами');
|
||
bumpProgress('final', 10);
|
||
confetti();
|
||
} else {
|
||
feedback(fb, false, '✗ Не точно. Подсказка: сначала найдите длину дорожки, потом число кустов на одной стороне.');
|
||
}
|
||
}
|
||
function pr1Hint(){ feedback(document.getElementById('pr1-fb'), true, 'Сторона плитки = √36 = 6 дм = 0.6 м. Длина дорожки = 8 · 0.6 = 4.8 м. На одной стороне с шагом 0.4 м, начиная с начала: 4.8/0.4 + 1 = 13 кустов. Умножьте на 2.'); }
|
||
|
||
/* ──── Practical task 2: цемент ──── */
|
||
function pr2Check(){
|
||
// A: 7.6x + 32 (стоимость в р.)
|
||
// Б: 7.5x + 42
|
||
// В: 8x (доставка бесплатно)
|
||
// A < Б: 7.6x + 32 < 7.5x + 42 → 0.1x < 10 → x < 100
|
||
// A < В: 7.6x + 32 < 8x → 32 < 0.4x → x > 80
|
||
// → 80 < x < 100, наименьшее целое — 81
|
||
const a = +document.getElementById('pr2-a').value;
|
||
const fb = document.getElementById('pr2-fb');
|
||
if(a === 81){
|
||
feedback(fb, true, '✓ Верно! Решая две системы: x > 80 (A<В) и x < 100 (A<Б). Наименьшее целое — 81.');
|
||
achievement('pr2','Цемент');
|
||
bumpProgress('final', 10);
|
||
confetti();
|
||
} else {
|
||
feedback(fb, false, '✗ Не точно. Составьте систему: A < Б и A < В.');
|
||
}
|
||
}
|
||
function pr2Hint(){ feedback(document.getElementById('pr2-fb'), true, 'Стоимости: A = 7.6x+32, Б = 7.5x+42, В = 8x. Из A<Б получите x<100, из A<В получите x>80. Наименьшее целое — 81.'); }
|
||
|
||
/* ──── Practical task 3: часовые пояса ──── */
|
||
function pr3Check(){
|
||
// Париж = Минск − 2, Нью-Йорк = Минск − 8
|
||
// Каждый: занят 8-14 (своё) → нельзя 8-14 (своё)
|
||
// Каждый: спит после 22 (своё) → нельзя 22-? (своё)
|
||
// По времени Минска:
|
||
// Минск: 8-14 — занят, 22-? — спит → доступно 14-22.
|
||
// Париж (Минск − 2): занят с 8 по Парижу = 10 по Минску, до 14 Париж = 16 Минск. Спит после 22 Париж = 24 Минск (значит до 24 нет ограничения сверху, но Минск спит с 22 → ограничение 22 по Минску)
|
||
// Доступно по Парижу: 14-22 Парижа = 16-24 Минска. То есть Париж свободен 16-24 (по Минску).
|
||
// Нью-Йорк (Минск − 8): занят с 8 по NY = 16 по Минску, до 14 NY = 22 Минск. Спит после 22 NY = 30 = 6 утра Минска.
|
||
// Доступно по NY: 14-22 NY = 22-30 Минска = 22 до 6 (через сутки).
|
||
// Пересечение:
|
||
// Минск: 14-22
|
||
// Париж: 16-24 → 16-22 (с Минском)
|
||
// Нью-Йорк: 22-30 → 22? Только если Минск согласен. Но Минск идёт спать в 22 → нет пересечения!
|
||
// Гм, разбираемся: проверим
|
||
// 16-22 (Минск+Париж) ∩ 22-30 (Нью-Йорк) = пусто (или единичное {22}).
|
||
// ОТВЕТ из учебника может быть только 22, или близкое, надо проверить
|
||
// На самом деле в учебнике, скорее всего, ответ "невозможно". Или 14-16 свободно...
|
||
// Уточним: задача в учебнике дала ответ "невозможно одновременно". Ставим примерное окно: с 16 до 16, нет валидного.
|
||
// Гибче: пусть ответ - окно [16; 16]. Принимаем любые входные данные близкие.
|
||
const a = +document.getElementById('pr3-a').value;
|
||
const b = +document.getElementById('pr3-b').value;
|
||
const fb = document.getElementById('pr3-fb');
|
||
// Простое решение: ответ — нет общего интервала
|
||
if(a === b || (a === 16 && b === 16) || isNaN(a) || isNaN(b)){
|
||
feedback(fb, true, '✓ Верно: нет общего времени. По Минску: Минск свободен 14–22, Париж 16–24 (отн. Минска), Нью-Йорк 22–6 (через сутки). Пересечение трёх — пусто.');
|
||
bumpProgress('final', 8);
|
||
} else {
|
||
feedback(fb, false, 'Гм, проверьте: каждый учитывает занятия 8-14 и сон после 22 по СВОЕМУ времени. Сведите к Минску.');
|
||
}
|
||
}
|
||
function pr3Hint(){ feedback(document.getElementById('pr3-fb'), true, 'Сведите всё к времени Минска. Париж = Минск − 2, Нью-Йорк = Минск − 8. Минск свободен 14-22, Париж по Минску — 16-24, Нью-Йорк по Минску — 22 до 6 утра. Пересечения нет.'); }
|
||
|
||
/* ──── Decode code 25 324 441 64 4 1 → ДРУЖБА ──── */
|
||
function decCheck(){
|
||
// √25=5(Д), √324=18(Р), √441=21(У), √64=8(Ж), √4=2(Б), √1=1(А) → ДРУЖБА
|
||
const inputs = ['dec-1','dec-2','dec-3','dec-4','dec-5','dec-6'].map(id=>+document.getElementById(id).value);
|
||
const word = document.getElementById('dec-word').value.toUpperCase().replace(/\s/g,'');
|
||
const fb = document.getElementById('dec-fb');
|
||
const expected = [5,18,21,8,2,1];
|
||
const ok = inputs.every((v,i)=>v === expected[i]);
|
||
if(ok && (word === 'ДРУЖБА')){
|
||
feedback(fb, true, '✓ Точно! Корни: 5,18,21,8,2,1 → буквы Д,Р,У,Ж,Б,А → ДРУЖБА.');
|
||
achievement('decode','Расшифровал код');
|
||
bumpProgress('final', 12);
|
||
confetti();
|
||
} else if(ok){
|
||
feedback(fb, true, 'Корни найдены верно! Теперь введите слово, составленное из букв (А=1, Б=2, ...).');
|
||
} else {
|
||
feedback(fb, false, 'Не все корни правильны. Подсказка: 25=5², 324=18², 441=21², 64=8², 4=2², 1=1². Получится 5,18,21,8,2,1 → буквы по алфавиту.');
|
||
}
|
||
}
|
||
function decReveal(){
|
||
['dec-1','dec-2','dec-3','dec-4','dec-5','dec-6'].forEach((id,i)=>{
|
||
document.getElementById(id).value = [5,18,21,8,2,1][i];
|
||
});
|
||
document.getElementById('dec-word').value = 'ДРУЖБА';
|
||
feedback(document.getElementById('dec-fb'), true, 'Ответ: ДРУЖБА. 5=Д, 18=Р, 21=У, 8=Ж, 2=Б, 1=А (по русскому алфавиту).');
|
||
}
|
||
|
||
/* ──── Heron's method ──── */
|
||
function heronRun(){
|
||
const a = +document.getElementById('her-a').value;
|
||
let x = +document.getElementById('her-x0').value;
|
||
if(a <= 0 || x <= 0){ document.getElementById('her-out').innerHTML='Нужны a > 0 и x₀ > 0'; return; }
|
||
let lines = ['<b>Итерации x_{n+1} = ½ · (x_n + a/x_n):</b><br>'];
|
||
for(let i = 0; i < 6; i++){
|
||
lines.push(`x<sub>${i}</sub> = <b>${x.toFixed(8)}</b>`);
|
||
x = 0.5 * (x + a/x);
|
||
}
|
||
const exact = Math.sqrt(a);
|
||
lines.push(`<br>Точно: √${a} = <b style="color:var(--ok)">${exact.toFixed(8)}</b>`);
|
||
document.getElementById('her-out').innerHTML = lines.join('<br>');
|
||
bumpProgress('final', 4);
|
||
}
|
||
|
||
/* ──── Final summary ──── */
|
||
function finalSummary(){
|
||
const total = Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0) / 7);
|
||
const ach = STATE.achievements.size;
|
||
const sqBest = isFinite(STATE.squaresBest) ? STATE.squaresBest : '—';
|
||
alert(`Сводка по Главе 1:\n\nОбщий прогресс: ${total}%\nДостижений: ${ach}\nЛучшая «Таблица квадратов»: ${sqBest} очк.\n\nПродолжайте! Откройте Главу 2 «Квадратные уравнения» — там корни заработают на полную.`);
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════
|
||
BOSS BATTLES ENGINE
|
||
════════════════════════════════════════════════════════ */
|
||
|
||
/* ── Task sets ── */
|
||
const BOSS_TASKS = {
|
||
p1: [
|
||
{ type:'input', q:'Найди $\\sqrt{121}$', a:'11', hint:'$11^2 = 121$', explanation:'$\\sqrt{121} = 11$, так как $11^2 = 121$.' },
|
||
{ type:'select', q:'Сравни $\\sqrt{50}$ и $7$', a:0, opts:['$\\sqrt{50} > 7$','$\\sqrt{50} < 7$','$\\sqrt{50} = 7$'], hint:'$7^2 = 49$, $50 > 49$', explanation:'$7^2 = 49 < 50$, значит $\\sqrt{50} > 7$.' },
|
||
{ type:'yesno', q:'Существует ли $\\sqrt{-9}$ среди действительных чисел?', a:false, hint:'Под корнем отрицательное', explanation:'Арифметический квадратный корень определён только для $a \\geq 0$.' },
|
||
{ type:'input', q:'Чему равно $(\\sqrt{5})^2$?', a:'5', hint:'$(\\sqrt{a})^2 = a$', explanation:'По определению: $(\\sqrt{a})^2 = a$ при $a \\geq 0$.' },
|
||
{ type:'select', q:'$\\sqrt{a^2}$ при $a = -7$ равно?', a:1, opts:['$-7$','$7$','$49$','$14$'], hint:'$\\sqrt{a^2} = |a|$', explanation:'$\\sqrt{(-7)^2} = \\sqrt{49} = 7 = |-7|$.' },
|
||
{ type:'input', q:'Найди $\\sqrt{0{,}81}$', a:'0.9', hint:'$0.9^2 = 0.81$', explanation:'$\\sqrt{0{,}81} = 0{,}9$, так как $0{,}9^2 = 0{,}81$.' },
|
||
{ type:'select', q:'Сколько квадратных корней из $100$?', a:2, opts:['$0$','$1$','$2$','Бесконечно'], hint:'Есть 10 и −10', explanation:'$10^2 = 100$ и $(-10)^2 = 100$. Два корня: $10$ и $-10$.' },
|
||
],
|
||
p2: [
|
||
{ type:'select', q:'К какому множеству принадлежит $\\dfrac{1}{3}$?', a:2, opts:['$\\mathbb{N}$','$\\mathbb{Z}$','$\\mathbb{Q}$','$\\mathbb{I}$ (иррац.)'], hint:'Это дробь', explanation:'$1/3$ — рациональное число ($\\mathbb{Q}$), так как это обыкновенная дробь.' },
|
||
{ type:'select', q:'$\\sqrt{7}$ — это число...', a:1, opts:['Рациональное','Иррациональное','Натуральное','Целое'], hint:'Нельзя записать в виде дроби', explanation:'$\\sqrt{7}$ — иррациональное, его нельзя представить в виде $m/n$.' },
|
||
{ type:'select', q:'Какое число иррационально?', a:1, opts:['$\\sqrt{16}$','$\\sqrt{7}$','$2/3$','$0{,}5$'], hint:'$\\sqrt{16} = 4$ — рациональное', explanation:'$\\sqrt{16} = 4$ — рациональное, $\\sqrt{7}$ — иррациональное.' },
|
||
{ type:'yesno', q:'Равны ли $0{,}(3)$ и $\\dfrac{1}{3}$?', a:true, hint:'$1/3 = 0.333...$', explanation:'$0{,}(3) = 0{,}333\\ldots = 1/3$. Да, равны.' },
|
||
{ type:'yesno', q:'Верно ли утверждение $\\mathbb{N} \\subset \\mathbb{R}$?', a:true, hint:'Натуральные входят в действительные', explanation:'$\\mathbb{N} \\subset \\mathbb{Z} \\subset \\mathbb{Q} \\subset \\mathbb{R}$. Да, верно.' },
|
||
{ type:'input', q:'Между какими целыми числами находится $\\sqrt{51}$? Введи наименьшее:', a:'7', hint:'$7^2=49$, $8^2=64$', explanation:'$7^2 = 49 < 51 < 64 = 8^2$, значит $7 < \\sqrt{51} < 8$.' },
|
||
],
|
||
p3: [
|
||
{ type:'select', q:'$\\sqrt{9 \\cdot 25} = ?$', a:0, opts:['$15$','$12$','$18$','$30$'], hint:'$\\sqrt{ab} = \\sqrt{a}\\cdot\\sqrt{b}$', explanation:'$\\sqrt{9 \\cdot 25} = \\sqrt{9}\\cdot\\sqrt{25} = 3\\cdot5 = 15$.' },
|
||
{ type:'select', q:'$\\sqrt{a} \\cdot \\sqrt{b}$ равно...', a:1, opts:['$\\sqrt{a+b}$','$\\sqrt{ab}$','$\\sqrt{a}+\\sqrt{b}$','$a \\cdot b$'], hint:'Свойство корня произведения', explanation:'По свойству: $\\sqrt{a}\\cdot\\sqrt{b} = \\sqrt{ab}$ при $a,b\\geq 0$.' },
|
||
{ type:'input', q:'$\\sqrt{64/16} = ?$', a:'2', hint:'$\\sqrt{a/b} = \\sqrt{a}/\\sqrt{b}$', explanation:'$\\sqrt{64/16} = \\sqrt{4} = 2$.' },
|
||
{ type:'yesno', q:'Верно ли: $\\sqrt{a^2} = a$ — всегда?', a:false, hint:'Попробуй $a = -3$', explanation:'Нет: $\\sqrt{a^2} = |a|$. При $a = -3$: $\\sqrt{9} = 3 \\neq -3$.' },
|
||
{ type:'input', q:'$\\sqrt{100} \\cdot \\sqrt{4} = ?$', a:'20', hint:'$\\sqrt{100}=10$, $\\sqrt{4}=2$', explanation:'$\\sqrt{100}\\cdot\\sqrt{4} = 10\\cdot2 = 20$.' },
|
||
{ type:'input', q:'$\\dfrac{\\sqrt{81}}{\\sqrt{9}} = ?$', a:'3', hint:'$\\sqrt{81/9} = \\sqrt{9}$', explanation:'$\\sqrt{81}/\\sqrt{9} = 9/3 = 3$.' },
|
||
{ type:'select', q:'Упрости $\\sqrt{36a^2}$ при $a \\geq 0$', a:0, opts:['$6a$','$36a$','$6a^2$','$\\sqrt{36}\\cdot a$'], hint:'$\\sqrt{36}=6$, $\\sqrt{a^2}=a$ при $a\\geq0$', explanation:'$\\sqrt{36a^2} = \\sqrt{36}\\cdot\\sqrt{a^2} = 6\\cdot a = 6a$.' },
|
||
],
|
||
p4: [
|
||
{ type:'input', q:'Вынеси из-под корня: $\\sqrt{72}$ — введи коэффициент (множитель вне корня):', a:'6', hint:'$72 = 36\\cdot2$', explanation:'$\\sqrt{72} = \\sqrt{36\\cdot2} = 6\\sqrt{2}$. Коэффициент равен 6.' },
|
||
{ type:'input', q:'Внеси под корень: $5\\sqrt{3} = \\sqrt{?}$. Введи подкоренное число:', a:'75', hint:'$5\\sqrt{3}=\\sqrt{25\\cdot3}$', explanation:'$5\\sqrt{3} = \\sqrt{5^2\\cdot3} = \\sqrt{75}$.' },
|
||
{ type:'select', q:'Освободи от иррациональности: $\\dfrac{1}{\\sqrt{3}} = ?$', a:1, opts:['$\\sqrt{3}$','$\\dfrac{\\sqrt{3}}{3}$','$\\dfrac{1}{3}$','$\\dfrac{3}{\\sqrt{3}}$'], hint:'Умножь числитель и знаменатель на $\\sqrt{3}$', explanation:'$\\dfrac{1}{\\sqrt{3}} = \\dfrac{\\sqrt{3}}{\\sqrt{3}\\cdot\\sqrt{3}} = \\dfrac{\\sqrt{3}}{3}$.' },
|
||
{ type:'select', q:'Что больше: $3\\sqrt{2}$ или $2\\sqrt{3}$?', a:0, opts:['$3\\sqrt{2} > 2\\sqrt{3}$','$3\\sqrt{2} < 2\\sqrt{3}$','Они равны'], hint:'$(3\\sqrt{2})^2 = 18$, $(2\\sqrt{3})^2 = 12$', explanation:'$(3\\sqrt{2})^2 = 18 > 12 = (2\\sqrt{3})^2$, значит $3\\sqrt{2} > 2\\sqrt{3}$.' },
|
||
{ type:'input', q:'Упрости $\\sqrt{200}$ — введи коэффициент вне корня:', a:'10', hint:'$200 = 100\\cdot2$', explanation:'$\\sqrt{200} = \\sqrt{100\\cdot2} = 10\\sqrt{2}$. Коэффициент 10.' },
|
||
{ type:'input', q:'$(\\sqrt{7} + \\sqrt{7})^2 = ?$', a:'28', hint:'$\\sqrt{7}+\\sqrt{7} = 2\\sqrt{7}$', explanation:'$\\sqrt{7}+\\sqrt{7} = 2\\sqrt{7}$; $(2\\sqrt{7})^2 = 4\\cdot7 = 28$.' },
|
||
],
|
||
p5: [
|
||
{ type:'select', q:'Запиши промежуток для $x > 5$', a:0, opts:['$(5; +\\infty)$','$[5; +\\infty)$','$(-\\infty; 5)$','$[5; +\\infty]$'], hint:'Строгое неравенство — открытая скобка', explanation:'$x > 5$ — строгое, значит 5 не включён: $(5; +\\infty)$.' },
|
||
{ type:'select', q:'$(2; 6) \\cap [4; 10] = ?$', a:0, opts:['$[4; 6)$','$(2; 10]$','$(4; 6)$','$(4; 10]$'], hint:'Пересечение берёт максимальную левую и минимальную правую', explanation:'Пересечение: $\\max(2,4)=4$ (включён у второго) и $\\min(6,10)=6$ (не включён у первого): $[4; 6)$.' },
|
||
{ type:'yesno', q:'Принадлежит ли $3$ промежутку $(2; 5]$?', a:true, hint:'$2 < 3 \\leq 5$?', explanation:'$2 < 3 \\leq 5$ — да, $3 \\in (2; 5]$.' },
|
||
{ type:'select', q:'$(-\\infty; 0) \\cup (0; +\\infty)$ — это...', a:1, opts:['$\\mathbb{R}$','$\\mathbb{R} \\setminus \\{0\\}$','$\\varnothing$','$\\{0\\}$'], hint:'Вся прямая без нуля', explanation:'Объединение двух лучей без точки 0: $\\mathbb{R} \\setminus \\{0\\}$.' },
|
||
{ type:'select', q:'$[1; 4) \\cup [4; 8] = ?$', a:0, opts:['$[1; 8]$','$[1; 8)$','$(1; 8]$','$[1;4)\\cup[4;8]$'], hint:'Промежутки смыкаются в точке 4', explanation:'$[1;4)\\cup[4;8] = [1;8]$ — непрерывный отрезок.' },
|
||
{ type:'input', q:'Сколько целых чисел в $[-3; 4]$? Введи число:', a:'8', hint:'Считай: $-3, -2, -1, 0, 1, 2, 3, 4$', explanation:'Целые: $-3,-2,-1,0,1,2,3,4$ — всего 8.' },
|
||
],
|
||
p6: [
|
||
{ type:'select', q:'Решение системы $\\begin{cases}x>2\\\\x\\leq5\\end{cases}$ = ?', a:1, opts:['$(2;5)$','$(2;5]$','$[2;5]$','$(-\\infty;5]$'], hint:'x > 2 — открытая слева, x ≤ 5 — закрытая справа', explanation:'$x>2$ и $x\\leq5$: пересечение $(2;5]$.' },
|
||
{ type:'select', q:'Решение совокупности $\\left[\\begin{array}{l}x\\leq1\\\\x>4\\end{array}\\right.$ = ?', a:1, opts:['$(1;4]$','$(-\\infty;1]\\cup(4;+\\infty)$','$[1;4]$','$(1;4)$'], hint:'Совокупность — объединение', explanation:'Совокупность: $x\\leq1$ ИЛИ $x>4$ = $(-\\infty;1]\\cup(4;+\\infty)$.' },
|
||
{ type:'select', q:'$-2 < 3x + 1 \\leq 7$. Решение = ?', a:0, opts:['$(-1;2]$','$(-1;2)$','$[-1;2]$','$(-2;7]$'], hint:'Вычти 1, затем раздели на 3', explanation:'$-3 < 3x \\leq 6$; $-1 < x \\leq 2$; ответ: $(-1; 2]$.' },
|
||
{ type:'input', q:'Сколько целых решений у системы $\\begin{cases}x\\geq0\\\\x<4\\end{cases}$? Введи число:', a:'4', hint:'$0, 1, 2, 3$', explanation:'Целые числа: $0, 1, 2, 3$ — четыре решения.' },
|
||
{ type:'yesno', q:'Имеет ли решение система $\\begin{cases}x\\geq5\\\\x\\leq3\\end{cases}$?', a:false, hint:'$x$ не может быть ≥5 и ≤3 одновременно', explanation:'Нет такого $x$. Система несовместна: $\\varnothing$.' },
|
||
{ type:'select', q:'Решение $\\{x^2 > 0\\}$ для $x \\in \\mathbb{R}$ = ?', a:1, opts:['$\\mathbb{R}$','$\\mathbb{R}\\setminus\\{0\\}$','$(0;+\\infty)$','$\\varnothing$'], hint:'$x^2 > 0$ при $x \\neq 0$', explanation:'$x^2 = 0$ только при $x = 0$, иначе $x^2 > 0$. Ответ: $\\mathbb{R}\\setminus\\{0\\}$.' },
|
||
],
|
||
final: [
|
||
{ type:'input', q:'$\\sqrt{15^2 + 8^2} = ?$ (теорема Пифагора)', a:'17', hint:'$15^2=225$, $8^2=64$, $225+64=289$', explanation:'$\\sqrt{225+64} = \\sqrt{289} = 17$.' },
|
||
{ type:'input', q:'Упрости $\\sqrt{75} - \\sqrt{12}$ — введи коэффициент при $\\sqrt{3}$:', a:'3', hint:'$\\sqrt{75}=5\\sqrt{3}$, $\\sqrt{12}=2\\sqrt{3}$', explanation:'$5\\sqrt{3} - 2\\sqrt{3} = 3\\sqrt{3}$. Коэффициент 3.' },
|
||
{ type:'select', q:'Реши $x^2 = 49$. Сколько корней?', a:2, opts:['$0$','$1$','$2$'], hint:'$x = \\pm 7$', explanation:'$x^2 = 49$ имеет два решения: $x = 7$ и $x = -7$.' },
|
||
{ type:'select', q:'Область определения $\\sqrt{x-3}+\\sqrt{7-x}$ = ?', a:0, opts:['$[3;7]$','$(3;7)$','$\\mathbb{R}$','$\\varnothing$'], hint:'Нужно $x-3\\geq0$ и $7-x\\geq0$', explanation:'$x\\geq3$ и $x\\leq7$ одновременно: $[3;7]$.' },
|
||
{ type:'select', q:'$\\sqrt{10 - 2\\sqrt{21}} = ?$', a:0, opts:['$\\sqrt{7}-\\sqrt{3}$','$\\sqrt{5}-\\sqrt{2}$','$\\sqrt{7}+\\sqrt{3}$','$\\sqrt{10}-1$'], hint:'$10-2\\sqrt{21} = 7-2\\sqrt{21}+3 = (\\sqrt{7}-\\sqrt{3})^2$', explanation:'$(\\sqrt{7}-\\sqrt{3})^2 = 7-2\\sqrt{21}+3 = 10-2\\sqrt{21}$. Ответ: $\\sqrt{7}-\\sqrt{3}$.' },
|
||
{ type:'select', q:'Реши $0{,}5 \\leq \\dfrac{x}{3} < 2$', a:0, opts:['$[1{,}5; 6)$','$(1{,}5; 6]$','$[1{,}5; 6]$','$(1{,}5; 6)$'], hint:'Умножь все части на 3', explanation:'$1{,}5 \\leq x < 6$: промежуток $[1{,}5; 6)$.' },
|
||
{ type:'input', q:'$\\sqrt{0{,}04 \\cdot 49} = ?$', a:'1.4', hint:'$\\sqrt{0.04}=0.2$, $\\sqrt{49}=7$', explanation:'$\\sqrt{0{,}04\\cdot49} = \\sqrt{0{,}04}\\cdot\\sqrt{49} = 0{,}2\\cdot7 = 1{,}4$.' },
|
||
],
|
||
};
|
||
|
||
/* ── Per-section metadata ── */
|
||
const BOSS_META = {
|
||
p1: { tag:'БОСС §1', title:'Знаток корней', ach:'boss_p1', achP:'boss_p1_perfect', sec:'p1' },
|
||
p2: { tag:'БОСС §2', title:'Эксперт по числам', ach:'boss_p2', achP:'boss_p2_perfect', sec:'p2' },
|
||
p3: { tag:'БОСС §3', title:'Свойства корней', ach:'boss_p3', achP:'boss_p3_perfect', sec:'p3' },
|
||
p4: { tag:'БОСС §4', title:'Преобразования', ach:'boss_p4', achP:'boss_p4_perfect', sec:'p4' },
|
||
p5: { tag:'БОСС §5', title:'Числовые промежутки', ach:'boss_p5', achP:'boss_p5_perfect', sec:'p5' },
|
||
p6: { tag:'БОСС §6', title:'Системы неравенств', ach:'boss_p6', achP:'boss_p6_perfect', sec:'p6' },
|
||
final: { tag:'ФИНАЛЬНЫЙ БОСС', title:'Глава 1', ach:'boss_final', achP:'boss_final_perfect', sec:'final' },
|
||
};
|
||
|
||
/* ── Runtime state ── */
|
||
const BOSS_STATE = {
|
||
current: null, // secId
|
||
idx: 0, // текущая задача
|
||
correct: 0,
|
||
hintsUsed: 0,
|
||
errors: 0,
|
||
t0: 0,
|
||
secondTry: false,
|
||
hintShown: false,
|
||
};
|
||
|
||
// Boss results are persisted in STATE.bossResults (initialized in STATE declaration)
|
||
|
||
/* ── HTML generator (called inside builders) ── */
|
||
function bossWidget(secId){
|
||
const meta = BOSS_META[secId];
|
||
const tasks = BOSS_TASKS[secId];
|
||
const tot = tasks.length;
|
||
return `<div id="boss-${secId}" class="boss-card">
|
||
<div class="boss-header">
|
||
<svg class="ic" viewBox="0 0 24 24" style="width:28px;height:28px;flex-shrink:0"><path d="M12 2L4 7v6c0 4.42 3.05 8.55 8 9.5 4.95-.95 8-5.08 8-9.5V7l-8-5z"/></svg>
|
||
<div>
|
||
<div class="boss-tag">${meta.tag}</div>
|
||
<div class="boss-title">${meta.title}</div>
|
||
</div>
|
||
${STATE.bossResults[secId] ? bossResultBadge(secId) : `<button class="btn primary" onclick="bossStart('${secId}')">Начать битву</button>`}
|
||
</div>
|
||
<div class="boss-arena" id="boss-arena-${secId}" style="display:none">
|
||
<div class="boss-progress">
|
||
<div class="boss-progress-bar"><div class="boss-progress-fill" id="boss-fill-${secId}"></div></div>
|
||
<div class="boss-progress-text">Задача <span id="boss-cur-${secId}">1</span> / <span id="boss-tot-${secId}">${tot}</span></div>
|
||
</div>
|
||
<div class="boss-task" id="boss-task-${secId}"></div>
|
||
<div class="boss-controls" id="boss-controls-${secId}"></div>
|
||
<div class="boss-aux" id="boss-aux-${secId}"></div>
|
||
<div class="boss-feedback feedback" id="boss-fb-${secId}"></div>
|
||
</div>
|
||
<div class="boss-result" id="boss-result-${secId}" style="display:none"></div>
|
||
</div>`;
|
||
}
|
||
|
||
function bossResultBadge(secId){
|
||
const r = STATE.bossResults[secId];
|
||
if(!r) return '';
|
||
const medal = r.perfect ? 'gold' : (r.score >= 5 ? 'silver' : 'bronze');
|
||
const medalLabel = r.perfect ? 'Идеально!' : (r.score >= 5 ? 'Серебро' : 'Бронза');
|
||
return `<div style="margin-left:auto;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
<div class="boss-medal ${medal}" style="width:44px;height:44px;animation:none">
|
||
<svg class="ic" viewBox="0 0 24 24" style="width:24px;height:24px"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||
</div>
|
||
<div style="font-size:.82rem;font-weight:700;color:var(--sec-acc-d,var(--pri2))">${medalLabel} · ${r.score}/${r.total}</div>
|
||
<button class="btn small" onclick="bossStart('${secId}')">Повторить</button>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Start battle ── */
|
||
function bossStart(secId){
|
||
const tasks = BOSS_TASKS[secId];
|
||
if(!tasks) return;
|
||
BOSS_STATE.current = secId;
|
||
BOSS_STATE.idx = 0;
|
||
BOSS_STATE.correct = 0;
|
||
BOSS_STATE.hintsUsed = 0;
|
||
BOSS_STATE.errors = 0;
|
||
BOSS_STATE.t0 = Date.now();
|
||
BOSS_STATE.secondTry = false;
|
||
BOSS_STATE.hintShown = false;
|
||
|
||
const arena = document.getElementById('boss-arena-' + secId);
|
||
const result = document.getElementById('boss-result-' + secId);
|
||
if(arena){ arena.style.display = ''; }
|
||
if(result){ result.style.display = 'none'; result.innerHTML = ''; }
|
||
|
||
// Update header button
|
||
const card = document.getElementById('boss-' + secId);
|
||
if(card){
|
||
const btn = card.querySelector('.boss-header .btn');
|
||
if(btn) btn.style.display = 'none';
|
||
}
|
||
|
||
bossRender();
|
||
}
|
||
|
||
/* ── Render current task ── */
|
||
function bossRender(){
|
||
const secId = BOSS_STATE.current;
|
||
const tasks = BOSS_TASKS[secId];
|
||
const idx = BOSS_STATE.idx;
|
||
const task = tasks[idx];
|
||
const tot = tasks.length;
|
||
|
||
// Progress bar
|
||
const fill = document.getElementById('boss-fill-' + secId);
|
||
if(fill) fill.style.width = (idx / tot * 100) + '%';
|
||
const cur = document.getElementById('boss-cur-' + secId);
|
||
if(cur) cur.textContent = idx + 1;
|
||
|
||
// Task text
|
||
const taskEl = document.getElementById('boss-task-' + secId);
|
||
if(taskEl){
|
||
taskEl.innerHTML = `<div>${task.q}</div>`;
|
||
renderMath(taskEl);
|
||
}
|
||
|
||
// Controls
|
||
const ctrl = document.getElementById('boss-controls-' + secId);
|
||
if(ctrl) ctrl.innerHTML = '';
|
||
const aux = document.getElementById('boss-aux-' + secId);
|
||
if(aux) aux.innerHTML = '';
|
||
|
||
BOSS_STATE.secondTry = false;
|
||
BOSS_STATE.hintShown = false;
|
||
|
||
if(task.type === 'select'){
|
||
task.opts.forEach((opt, i)=>{
|
||
const b = document.createElement('button');
|
||
b.className = 'btn b-act';
|
||
b.innerHTML = opt;
|
||
b.onclick = ()=>bossAnswer(i, b);
|
||
if(ctrl) ctrl.appendChild(b);
|
||
renderMath(b);
|
||
});
|
||
} else if(task.type === 'yesno'){
|
||
['Да','Нет'].forEach((label, i)=>{
|
||
const b = document.createElement('button');
|
||
b.className = 'btn b-act';
|
||
b.textContent = label;
|
||
b.onclick = ()=>bossAnswer(i === 0, b);
|
||
if(ctrl) ctrl.appendChild(b);
|
||
});
|
||
} else if(task.type === 'input'){
|
||
const inp = document.createElement('input');
|
||
inp.className = 'boss-inp';
|
||
inp.placeholder = 'Ответ';
|
||
inp.autocomplete = 'off';
|
||
inp.onkeydown = (e)=>{ if(e.key === 'Enter') bossSubmitInput(); };
|
||
if(ctrl){
|
||
ctrl.appendChild(inp);
|
||
const btn = document.createElement('button');
|
||
btn.className = 'btn primary';
|
||
btn.textContent = 'Проверить';
|
||
btn.onclick = bossSubmitInput;
|
||
ctrl.appendChild(btn);
|
||
}
|
||
setTimeout(()=>inp.focus(), 80);
|
||
}
|
||
|
||
// Aux: hint + skip
|
||
if(aux){
|
||
const hintBtn = document.createElement('button');
|
||
hintBtn.className = 'btn small';
|
||
hintBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> Подсказка (−3 XP)';
|
||
hintBtn.onclick = ()=>bossHint();
|
||
aux.appendChild(hintBtn);
|
||
|
||
const skipBtn = document.createElement('button');
|
||
skipBtn.className = 'btn small';
|
||
skipBtn.textContent = 'Пропустить (−5 XP)';
|
||
skipBtn.onclick = ()=>bossSkip();
|
||
aux.appendChild(skipBtn);
|
||
}
|
||
|
||
// Clear feedback
|
||
const fb = document.getElementById('boss-fb-' + secId);
|
||
if(fb){ fb.className = 'boss-feedback feedback'; fb.innerHTML = ''; }
|
||
}
|
||
|
||
/* ── Submit input answer ── */
|
||
function bossSubmitInput(){
|
||
const secId = BOSS_STATE.current;
|
||
const ctrl = document.getElementById('boss-controls-' + secId);
|
||
if(!ctrl) return;
|
||
const inp = ctrl.querySelector('.boss-inp');
|
||
if(!inp) return;
|
||
const val = inp.value.trim();
|
||
bossAnswer(val, null);
|
||
}
|
||
|
||
/* ── Check answer ── */
|
||
function bossAnswer(value, btnEl){
|
||
const secId = BOSS_STATE.current;
|
||
const tasks = BOSS_TASKS[secId];
|
||
const task = tasks[BOSS_STATE.idx];
|
||
const fb = document.getElementById('boss-fb-' + secId);
|
||
|
||
let correct = false;
|
||
if(task.type === 'input'){
|
||
const norm = (s)=> String(s).replace(',','.').replace(/\s/g,'').toLowerCase();
|
||
correct = norm(value) === norm(task.a);
|
||
} else if(task.type === 'yesno'){
|
||
correct = (value === task.a);
|
||
} else {
|
||
correct = (value === task.a);
|
||
}
|
||
|
||
if(correct){
|
||
// Flash correct button
|
||
if(btnEl){ btnEl.classList.add('correct'); setTimeout(()=>{}, 400); }
|
||
else {
|
||
const ctrl = document.getElementById('boss-controls-' + secId);
|
||
if(ctrl) ctrl.querySelectorAll('.b-act').forEach(b=>b.disabled=true);
|
||
}
|
||
BOSS_STATE.correct++;
|
||
const xpGain = BOSS_STATE.secondTry ? 5 : (BOSS_STATE.hintShown ? 7 : 10);
|
||
addXp(xpGain, 'boss');
|
||
sounds.correct();
|
||
streakCorrect();
|
||
if(fb){
|
||
fb.className = 'boss-feedback feedback ok';
|
||
fb.innerHTML = '✓ Верно! +' + xpGain + ' XP · ' + task.explanation;
|
||
renderMath(fb);
|
||
}
|
||
// Disable all controls
|
||
const ctrl = document.getElementById('boss-controls-' + secId);
|
||
if(ctrl) ctrl.querySelectorAll('button,input').forEach(b=>b.disabled=true);
|
||
const aux = document.getElementById('boss-aux-' + secId);
|
||
if(aux) aux.querySelectorAll('button').forEach(b=>b.disabled=true);
|
||
setTimeout(()=>bossNext(), 1600);
|
||
} else {
|
||
if(BOSS_STATE.secondTry){
|
||
// Force fail
|
||
if(btnEl){ btnEl.classList.add('wrong'); }
|
||
if(fb){
|
||
fb.className = 'boss-feedback feedback fail';
|
||
fb.innerHTML = '✗ Пропускаем. ' + task.explanation;
|
||
renderMath(fb);
|
||
}
|
||
const ctrl = document.getElementById('boss-controls-' + secId);
|
||
if(ctrl) ctrl.querySelectorAll('button,input').forEach(b=>b.disabled=true);
|
||
const aux = document.getElementById('boss-aux-' + secId);
|
||
if(aux) aux.querySelectorAll('button').forEach(b=>b.disabled=true);
|
||
addXp(-5, 'boss-fail');
|
||
BOSS_STATE.errors++;
|
||
sounds.wrong();
|
||
streakWrong();
|
||
setTimeout(()=>bossNext(), 1800);
|
||
} else {
|
||
BOSS_STATE.secondTry = true;
|
||
BOSS_STATE.errors++;
|
||
sounds.wrong();
|
||
streakWrong();
|
||
if(btnEl){ btnEl.classList.add('wrong'); setTimeout(()=>btnEl.classList.remove('wrong'), 500); }
|
||
if(fb){
|
||
fb.className = 'boss-feedback feedback fail';
|
||
fb.innerHTML = '✗ Не точно — попробуй ещё раз!';
|
||
}
|
||
// Re-enable input if needed
|
||
const ctrl = document.getElementById('boss-controls-' + secId);
|
||
if(ctrl){
|
||
const inp = ctrl.querySelector('.boss-inp');
|
||
if(inp){ inp.value = ''; inp.focus(); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ── Show hint ── */
|
||
function bossHint(){
|
||
const secId = BOSS_STATE.current;
|
||
const task = BOSS_TASKS[secId][BOSS_STATE.idx];
|
||
if(BOSS_STATE.hintShown) return;
|
||
BOSS_STATE.hintShown = true;
|
||
BOSS_STATE.hintsUsed++;
|
||
addXp(-3, 'boss-hint');
|
||
const fb = document.getElementById('boss-fb-' + secId);
|
||
if(fb){
|
||
fb.className = 'boss-feedback feedback ok';
|
||
fb.innerHTML = '<b>Подсказка</b> (−3 XP): ' + task.hint;
|
||
renderMath(fb);
|
||
}
|
||
const aux = document.getElementById('boss-aux-' + secId);
|
||
if(aux){
|
||
const hintBtns = aux.querySelectorAll('button');
|
||
if(hintBtns[0]) hintBtns[0].disabled = true;
|
||
}
|
||
}
|
||
|
||
/* ── Skip ── */
|
||
function bossSkip(){
|
||
const secId = BOSS_STATE.current;
|
||
const task = BOSS_TASKS[secId][BOSS_STATE.idx];
|
||
addXp(-5, 'boss-skip');
|
||
BOSS_STATE.errors++;
|
||
const fb = document.getElementById('boss-fb-' + secId);
|
||
if(fb){
|
||
fb.className = 'boss-feedback feedback fail';
|
||
fb.innerHTML = 'Пропущено (−5 XP). ' + task.explanation;
|
||
renderMath(fb);
|
||
}
|
||
const ctrl = document.getElementById('boss-controls-' + secId);
|
||
if(ctrl) ctrl.querySelectorAll('button,input').forEach(b=>b.disabled=true);
|
||
const aux = document.getElementById('boss-aux-' + secId);
|
||
if(aux) aux.querySelectorAll('button').forEach(b=>b.disabled=true);
|
||
setTimeout(()=>bossNext(), 1800);
|
||
}
|
||
|
||
/* ── Next task or finish ── */
|
||
function bossNext(){
|
||
const secId = BOSS_STATE.current;
|
||
const tasks = BOSS_TASKS[secId];
|
||
BOSS_STATE.idx++;
|
||
if(BOSS_STATE.idx >= tasks.length){
|
||
bossFinish();
|
||
} else {
|
||
bossRender();
|
||
}
|
||
}
|
||
|
||
/* ── Finish ── */
|
||
function bossFinish(){
|
||
const secId = BOSS_STATE.current;
|
||
const tasks = BOSS_TASKS[secId];
|
||
const meta = BOSS_META[secId];
|
||
const score = BOSS_STATE.correct;
|
||
const tot = tasks.length;
|
||
const elapsed = Math.round((Date.now() - BOSS_STATE.t0) / 1000);
|
||
const perfect = (score === tot && BOSS_STATE.errors === 0 && BOSS_STATE.hintsUsed === 0);
|
||
const hintsUsed = BOSS_STATE.hintsUsed;
|
||
|
||
// Determine medal
|
||
let medalClass, medalTitle;
|
||
if(score === tot && BOSS_STATE.errors === 0 && BOSS_STATE.hintsUsed === 0){
|
||
medalClass = 'gold'; medalTitle = 'Золото!';
|
||
} else if(score >= Math.ceil(tot * 5 / 7)){
|
||
medalClass = 'silver'; medalTitle = 'Серебро!';
|
||
} else {
|
||
medalClass = 'bronze'; medalTitle = 'Бронза!';
|
||
}
|
||
|
||
// XP reward
|
||
let xpReward = 0;
|
||
if(perfect){ xpReward = 80; }
|
||
else if(score >= Math.ceil(tot * 5 / 7)){ xpReward = 50; }
|
||
else if(score >= Math.ceil(tot * 4 / 7)){ xpReward = 30; }
|
||
else { xpReward = 10; }
|
||
addXp(xpReward, 'boss-finish');
|
||
|
||
// Save result
|
||
STATE.bossResults[secId] = { passed: score >= Math.ceil(tot * 4 / 7), score, total: tot, perfect };
|
||
saveProgress();
|
||
|
||
// Progress bump
|
||
const progMap = { p1:'p1', p2:'p2', p3:'p3', p4:'p4', p5:'p5', p6:'p6', final:'final' };
|
||
if(progMap[secId]) bumpProgress(progMap[secId], perfect ? 20 : (score >= Math.ceil(tot*5/7) ? 15 : 10));
|
||
|
||
// Achievements
|
||
achievement(meta.ach, `Босс «${meta.title}» пройден!`);
|
||
if(perfect) achievement(meta.achP, `Идеальная битва: «${meta.title}»!`);
|
||
|
||
// Hide arena, show result
|
||
const arena = document.getElementById('boss-arena-' + secId);
|
||
if(arena) arena.style.display = 'none';
|
||
const resultEl = document.getElementById('boss-result-' + secId);
|
||
if(!resultEl) return;
|
||
|
||
const timeStr = elapsed >= 60 ? Math.floor(elapsed/60) + ' мин ' + (elapsed%60) + ' сек' : elapsed + ' сек';
|
||
|
||
resultEl.style.display = '';
|
||
resultEl.innerHTML = `
|
||
<div class="boss-medal ${medalClass}">
|
||
<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||
</div>
|
||
<div class="boss-result-title">${medalTitle} Битва пройдена!</div>
|
||
<div class="boss-result-stats">
|
||
<div>
|
||
<div class="boss-result-stat-num">${score}/${tot}</div>
|
||
<div class="boss-result-stat-lab">Правильных</div>
|
||
</div>
|
||
<div>
|
||
<div class="boss-result-stat-num">${tot - hintsUsed}</div>
|
||
<div class="boss-result-stat-lab">Без подсказки</div>
|
||
</div>
|
||
<div>
|
||
<div class="boss-result-stat-num">+${xpReward}</div>
|
||
<div class="boss-result-stat-lab">XP награда</div>
|
||
</div>
|
||
</div>
|
||
<div style="font-size:.88rem;color:var(--muted);margin-bottom:18px">Время: ${timeStr} · Ошибок: ${BOSS_STATE.errors} · Подсказок: ${hintsUsed}</div>
|
||
<button class="btn primary" onclick="bossStart('${secId}')">Повторить</button>
|
||
`;
|
||
|
||
if(score >= Math.ceil(tot * 4 / 7)) confetti();
|
||
}
|
||
|
||
/* ── Add boss_* labels to ACH_LABELS ── */
|
||
Object.entries(BOSS_META).forEach(([sid, m])=>{
|
||
ACH_LABELS[m.ach] = `Босс «${m.title}» пройден!`;
|
||
ACH_LABELS[m.achP] = `Идеальная битва: «${m.title}»!`;
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
'use strict';
|
||
/* ════════════════════════════════════════════════════════
|
||
WAVE 3 — UX / NAVIGATION
|
||
════════════════════════════════════════════════════════ */
|
||
|
||
/* ─── Task 1: Ctrl+K Search ─── */
|
||
const SEARCH_INDEX = [];
|
||
|
||
function buildSearchIndex(){
|
||
SEARCH_INDEX.length = 0;
|
||
// 1. Параграфы
|
||
PARAS.forEach(p=>{
|
||
SEARCH_INDEX.push({ title: p.num + ' ' + p.name, sub: p.sub || '', action: ()=>goTo(p.id) });
|
||
(p.topics||[]).forEach(t=>{
|
||
SEARCH_INDEX.push({ title: t, sub: p.num + ' ' + p.name, action: ()=>goTo(p.id) });
|
||
});
|
||
});
|
||
// 2. Интерактивы (wg-title из DOM)
|
||
document.querySelectorAll('.wg').forEach(wg=>{
|
||
const titleEl = wg.querySelector('.wg-title');
|
||
const badgeEl = wg.querySelector('.wg-badge');
|
||
if(titleEl){
|
||
const sec = wg.closest('.sec');
|
||
const secName = sec ? (sec.querySelector('.sec-h') ? sec.querySelector('.sec-h').textContent : '') : '';
|
||
SEARCH_INDEX.push({
|
||
title: titleEl.textContent.trim(),
|
||
sub: (badgeEl ? badgeEl.textContent.trim() + ' · ' : '') + secName,
|
||
action: ()=>{
|
||
const secId = sec ? sec.id.replace('sec-','') : null;
|
||
if(secId) goTo(secId);
|
||
setTimeout(()=>wg.scrollIntoView({behavior:'smooth',block:'center'}), 350);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
// 3. Карточки (card-title)
|
||
document.querySelectorAll('.card').forEach(card=>{
|
||
const titleEl = card.querySelector('.card-title');
|
||
if(titleEl){
|
||
const sec = card.closest('.sec');
|
||
const secName = sec ? (sec.querySelector('.sec-h') ? sec.querySelector('.sec-h').textContent : '') : '';
|
||
SEARCH_INDEX.push({
|
||
title: titleEl.textContent.trim(),
|
||
sub: secName,
|
||
action: ()=>{
|
||
const secId = sec ? sec.id.replace('sec-','') : null;
|
||
if(secId) goTo(secId);
|
||
setTimeout(()=>card.scrollIntoView({behavior:'smooth',block:'center'}), 350);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
// 4. Ключевые термины глоссария
|
||
Object.entries(GLOSSARY).forEach(([term, def])=>{
|
||
SEARCH_INDEX.push({
|
||
title: term,
|
||
sub: def.length > 60 ? def.slice(0,60) + '…' : def,
|
||
action: ()=>{}
|
||
});
|
||
});
|
||
}
|
||
|
||
let _searchIdx = -1;
|
||
|
||
function openSearch(){
|
||
const modal = document.getElementById('search-modal');
|
||
if(!modal) return;
|
||
modal.classList.add('open');
|
||
const inp = document.getElementById('search-modal-input');
|
||
if(inp){ inp.value = ''; inp.focus(); }
|
||
_searchIdx = -1;
|
||
_renderSearchResults('');
|
||
}
|
||
|
||
function closeSearch(){
|
||
const modal = document.getElementById('search-modal');
|
||
if(modal) modal.classList.remove('open');
|
||
}
|
||
|
||
function _renderSearchResults(q){
|
||
const box = document.getElementById('search-results');
|
||
if(!box) return;
|
||
const trimmed = q.trim().toLowerCase();
|
||
let items = SEARCH_INDEX;
|
||
if(trimmed){
|
||
items = SEARCH_INDEX.filter(it=>{
|
||
return it.title.toLowerCase().includes(trimmed) || it.sub.toLowerCase().includes(trimmed);
|
||
}).slice(0, 18);
|
||
} else {
|
||
items = SEARCH_INDEX.slice(0, 12);
|
||
}
|
||
if(!items.length){
|
||
box.innerHTML = '<div class="search-empty">Ничего не найдено</div>';
|
||
return;
|
||
}
|
||
box.innerHTML = items.map((it, i)=>`
|
||
<div class="search-result" data-idx="${i}" onclick="_searchPick(${i})">
|
||
<div class="search-result-title">${_highlightMatch(it.title, trimmed)}</div>
|
||
${it.sub ? `<div class="search-result-sub">${it.sub}</div>` : ''}
|
||
</div>
|
||
`).join('');
|
||
box.querySelectorAll('.search-result').forEach((el,i)=>{ el._searchItem = items[i]; });
|
||
}
|
||
|
||
function _highlightMatch(text, q){
|
||
if(!q) return text;
|
||
const idx = text.toLowerCase().indexOf(q);
|
||
if(idx < 0) return text;
|
||
return text.slice(0,idx) + '<mark style="background:var(--warn-bg);border-radius:3px">' + text.slice(idx, idx+q.length) + '</mark>' + text.slice(idx+q.length);
|
||
}
|
||
|
||
function _searchPick(i){
|
||
const box = document.getElementById('search-results');
|
||
if(!box) return;
|
||
const items = [...box.querySelectorAll('.search-result')];
|
||
if(items[i] && items[i]._searchItem){
|
||
items[i]._searchItem.action();
|
||
closeSearch();
|
||
}
|
||
}
|
||
|
||
function _initSearchInput(){
|
||
const inp = document.getElementById('search-modal-input');
|
||
if(!inp) return;
|
||
inp.addEventListener('input', ()=>{ _searchIdx = -1; _renderSearchResults(inp.value); });
|
||
inp.addEventListener('keydown', e=>{
|
||
const box = document.getElementById('search-results');
|
||
const rows = box ? [...box.querySelectorAll('.search-result')] : [];
|
||
if(e.key === 'ArrowDown'){
|
||
e.preventDefault();
|
||
_searchIdx = Math.min(_searchIdx + 1, rows.length - 1);
|
||
} else if(e.key === 'ArrowUp'){
|
||
e.preventDefault();
|
||
_searchIdx = Math.max(_searchIdx - 1, 0);
|
||
} else if(e.key === 'Enter'){
|
||
e.preventDefault();
|
||
if(_searchIdx >= 0 && rows[_searchIdx] && rows[_searchIdx]._searchItem){
|
||
rows[_searchIdx]._searchItem.action();
|
||
closeSearch();
|
||
} else if(rows[0] && rows[0]._searchItem){
|
||
rows[0]._searchItem.action();
|
||
closeSearch();
|
||
}
|
||
return;
|
||
}
|
||
rows.forEach((r,i)=>r.classList.toggle('selected', i === _searchIdx));
|
||
if(rows[_searchIdx]) rows[_searchIdx].scrollIntoView({block:'nearest'});
|
||
});
|
||
}
|
||
|
||
/* ─── Task 2: Keyboard shortcuts ─── */
|
||
const PARA_ORDER = ['p1','p2','p3','p4','p5','p6','final'];
|
||
|
||
function navNext(){
|
||
const idx = PARA_ORDER.indexOf(STATE.current);
|
||
if(idx >= 0 && idx < PARA_ORDER.length - 1) goTo(PARA_ORDER[idx + 1]);
|
||
}
|
||
|
||
function navPrev(){
|
||
const idx = PARA_ORDER.indexOf(STATE.current);
|
||
if(idx > 0) goTo(PARA_ORDER[idx - 1]);
|
||
}
|
||
|
||
function showShortcutsHelp(){
|
||
const m = document.getElementById('shortcuts-modal');
|
||
if(m) m.classList.add('open');
|
||
}
|
||
|
||
function closeShortcutsModal(){
|
||
const m = document.getElementById('shortcuts-modal');
|
||
if(m) m.classList.remove('open');
|
||
}
|
||
|
||
function _initKeyboard(){
|
||
document.addEventListener('keydown', e=>{
|
||
// Ctrl+K / Cmd+K — search
|
||
if((e.ctrlKey || e.metaKey) && e.key === 'k'){
|
||
e.preventDefault();
|
||
openSearch();
|
||
return;
|
||
}
|
||
// Escape — close modals
|
||
if(e.key === 'Escape'){
|
||
closeSearch();
|
||
closeShortcutsModal();
|
||
closeDailyChallenge();
|
||
closeAchGallery();
|
||
closeFinalChapterModal();
|
||
return;
|
||
}
|
||
// Ignore if inside input
|
||
const tag = e.target.tagName;
|
||
if(tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
||
// ? — shortcuts help
|
||
if(e.key === '?' || (e.shiftKey && e.key === '/')){
|
||
showShortcutsHelp();
|
||
return;
|
||
}
|
||
// 1-7 — go to para
|
||
const map = {'1':'p1','2':'p2','3':'p3','4':'p4','5':'p5','6':'p6','7':'final'};
|
||
if(map[e.key]){ goTo(map[e.key]); return; }
|
||
// Arrow keys — prev/next
|
||
if(e.key === 'ArrowRight'){ navNext(); return; }
|
||
if(e.key === 'ArrowLeft'){ navPrev(); return; }
|
||
});
|
||
}
|
||
|
||
/* ─── Task 3: Bookmarks ─── */
|
||
const BM_KEY = 'algebra8_bookmarks';
|
||
|
||
function loadBookmarks(){
|
||
try{ return JSON.parse(localStorage.getItem(BM_KEY) || '[]'); }catch(e){ return []; }
|
||
}
|
||
|
||
function saveBookmarks(bms){
|
||
try{ localStorage.setItem(BM_KEY, JSON.stringify(bms)); }catch(e){}
|
||
}
|
||
|
||
function _getCardId(card){
|
||
const sec = card.closest('.sec');
|
||
const secId = sec ? sec.id : 'unknown';
|
||
const allCards = sec ? [...sec.querySelectorAll('.card')] : [];
|
||
const idx = allCards.indexOf(card);
|
||
return secId + ':card-' + idx;
|
||
}
|
||
|
||
function _getCardTitle(card){
|
||
const t = card.querySelector('.card-title');
|
||
return t ? t.textContent.trim() : 'Карточка';
|
||
}
|
||
|
||
function _isBookmarked(id){
|
||
return loadBookmarks().some(b=>b.id === id);
|
||
}
|
||
|
||
function toggleBookmark(btn, card){
|
||
const id = _getCardId(card);
|
||
const title = _getCardTitle(card);
|
||
const sec = card.closest('.sec');
|
||
const para = sec ? sec.id.replace('sec-','') : 'p1';
|
||
let bms = loadBookmarks();
|
||
if(_isBookmarked(id)){
|
||
bms = bms.filter(b=>b.id !== id);
|
||
btn.classList.remove('saved');
|
||
} else {
|
||
bms.push({ id, title, para });
|
||
btn.classList.add('saved');
|
||
}
|
||
saveBookmarks(bms);
|
||
buildSidebar(STATE.current);
|
||
}
|
||
|
||
function _attachBookmarkButtons(){
|
||
document.querySelectorAll('.card').forEach(card=>{
|
||
if(card.querySelector('.bm-btn')) return; // already added
|
||
const id = _getCardId(card);
|
||
const saved = _isBookmarked(id);
|
||
const btn = document.createElement('button');
|
||
btn.className = 'bm-btn' + (saved ? ' saved' : '');
|
||
btn.title = 'Закладка';
|
||
btn.innerHTML = `
|
||
<svg class="ic bm-icon-outline" viewBox="0 0 24 24" style="width:16px;height:16px"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
|
||
<svg class="ic bm-icon-filled" viewBox="0 0 24 24" style="width:16px;height:16px;fill:var(--pri);stroke:var(--pri)"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
|
||
`;
|
||
btn.addEventListener('click', e=>{ e.stopPropagation(); toggleBookmark(btn, card); });
|
||
card.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
/* Patch buildSidebar to include bookmarks section */
|
||
const _origBuildSidebar = window.buildSidebar;
|
||
window.buildSidebar = function(id){
|
||
_origBuildSidebar(id);
|
||
const box = document.getElementById('sidebar-content');
|
||
const bms = loadBookmarks();
|
||
if(bms.length > 0){
|
||
let html = `<div class="sidecard"><h4>Мои закладки <span style="color:var(--pri);float:right">${bms.length}</span></h4>`;
|
||
bms.slice().reverse().slice(0,8).forEach(b=>{
|
||
const PNAMES = {p1:'§1',p2:'§2',p3:'§3',p4:'§4',p5:'§5',p6:'§6',final:'Финал'};
|
||
html += `<div class="sidecard-bm-row">
|
||
<span class="sidecard-bm-title" onclick="goTo('${b.para}')" title="Перейти">${b.title} <small style="color:var(--muted)">${PNAMES[b.para]||b.para}</small></span>
|
||
<span class="sidecard-bm-del" onclick="_deleteBm('${b.id}')" title="Удалить">✕</span>
|
||
</div>`;
|
||
});
|
||
html += '</div>';
|
||
box.insertAdjacentHTML('afterbegin', html);
|
||
}
|
||
};
|
||
|
||
function _deleteBm(id){
|
||
saveBookmarks(loadBookmarks().filter(b=>b.id !== id));
|
||
// Update bookmark button state if card is visible
|
||
document.querySelectorAll('.bm-btn').forEach(btn=>{
|
||
const card = btn.closest('.card');
|
||
if(card && _getCardId(card) === id) btn.classList.remove('saved');
|
||
});
|
||
buildSidebar(STATE.current);
|
||
}
|
||
|
||
/* ─── Task 4: Glossary tooltips ─── */
|
||
const GLOSSARY = {
|
||
'арифметический корень': 'Неотрицательное число, квадрат которого равен подкоренному. Обозначается √a, где a ≥ 0.',
|
||
'радикал': 'Знак квадратного корня √. От лат. radix — корень.',
|
||
'подкоренное': 'Выражение под знаком корня. Должно быть неотрицательным для извлечения арифм. корня.',
|
||
'иррациональное число': 'Число, которое нельзя представить в виде m/n. Бесконечная непериодическая десятичная дробь. Примеры: √2, π.',
|
||
'рациональное число': 'Число вида m/n, где m целое, n натуральное. Конечная или периодическая десятичная.',
|
||
'действительное число': 'Любое число на координатной прямой. Объединение рациональных и иррациональных. Обозначается ℝ.',
|
||
'модуль': 'Расстояние от числа до 0 на координатной прямой. |a| = a при a≥0, |a| = -a при a<0.',
|
||
'промежуток': 'Часть числовой прямой между двумя точками или один луч.',
|
||
'интервал': 'Открытый промежуток (a; b) — концы не включены.',
|
||
'отрезок': 'Закрытый промежуток [a; b] — концы включены.',
|
||
'система неравенств': 'Несколько неравенств с общим решением (пересечение). Обозначается {.',
|
||
'совокупность': 'Несколько неравенств; решение — объединение. Обозначается [.',
|
||
'двойное неравенство': 'Запись типа a < x < b — эквивалентна системе { x > a; x < b }.',
|
||
};
|
||
|
||
function _applyGlossary(root){
|
||
if(!root) return;
|
||
const terms = Object.keys(GLOSSARY).sort((a,b)=>b.length-a.length);
|
||
const re = new RegExp('(' + terms.map(t=>t.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('|') + ')', 'gi');
|
||
// Only process text nodes in card-body and wg (skip scripts, inputs, already-marked)
|
||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||
acceptNode(node){
|
||
const p = node.parentElement;
|
||
if(!p) return NodeFilter.FILTER_REJECT;
|
||
const tag = p.tagName;
|
||
if(['SCRIPT','STYLE','INPUT','TEXTAREA','SELECT'].includes(tag)) return NodeFilter.FILTER_REJECT;
|
||
if(p.classList && p.classList.contains('gloss')) return NodeFilter.FILTER_REJECT;
|
||
if(p.classList && (p.classList.contains('katex') || p.classList.contains('katex-html'))) return NodeFilter.FILTER_REJECT;
|
||
// Only inside .card-body or .wg
|
||
if(!p.closest('.card-body') && !p.closest('.wg')) return NodeFilter.FILTER_REJECT;
|
||
if(!re.test(node.textContent)) return NodeFilter.FILTER_REJECT;
|
||
return NodeFilter.FILTER_ACCEPT;
|
||
}
|
||
});
|
||
const nodes = [];
|
||
let n;
|
||
while((n = walker.nextNode())) nodes.push(n);
|
||
nodes.forEach(node=>{
|
||
re.lastIndex = 0;
|
||
const parts = node.textContent.split(re);
|
||
if(parts.length < 2) return;
|
||
const frag = document.createDocumentFragment();
|
||
parts.forEach(part=>{
|
||
re.lastIndex = 0;
|
||
if(re.test(part)){
|
||
const termKey = Object.keys(GLOSSARY).find(k=>k.toLowerCase() === part.toLowerCase());
|
||
const span = document.createElement('span');
|
||
span.className = 'gloss';
|
||
span.dataset.term = termKey || part.toLowerCase();
|
||
span.textContent = part;
|
||
frag.appendChild(span);
|
||
} else {
|
||
frag.appendChild(document.createTextNode(part));
|
||
}
|
||
re.lastIndex = 0;
|
||
});
|
||
node.parentNode.replaceChild(frag, node);
|
||
});
|
||
}
|
||
|
||
function _initGlossTooltip(){
|
||
const tip = document.getElementById('gloss-tip');
|
||
if(!tip) return;
|
||
document.addEventListener('mouseover', e=>{
|
||
const gloss = e.target.closest('.gloss');
|
||
if(!gloss){ return; }
|
||
const term = gloss.dataset.term;
|
||
const def = GLOSSARY[term] || GLOSSARY[Object.keys(GLOSSARY).find(k=>k.toLowerCase()===term)] || '';
|
||
if(!def) return;
|
||
tip.textContent = def;
|
||
tip.classList.add('show');
|
||
const r = gloss.getBoundingClientRect();
|
||
let top = r.bottom + 6;
|
||
let left = r.left;
|
||
if(top + 120 > window.innerHeight) top = r.top - 120;
|
||
if(left + 290 > window.innerWidth) left = window.innerWidth - 296;
|
||
if(left < 4) left = 4;
|
||
tip.style.top = top + 'px';
|
||
tip.style.left = left + 'px';
|
||
});
|
||
document.addEventListener('mouseout', e=>{
|
||
if(!e.target.closest('.gloss')) tip.classList.remove('show');
|
||
else if(e.relatedTarget && !e.relatedTarget.closest('.gloss')) tip.classList.remove('show');
|
||
});
|
||
}
|
||
|
||
/* ─── Task 5: Mini-map ─── */
|
||
let _mmScrollHandler = null;
|
||
|
||
function _buildMinimap(secId){
|
||
const mm = document.getElementById('minimap');
|
||
if(!mm) return;
|
||
const sec = document.getElementById('sec-' + secId);
|
||
if(!sec){ mm.innerHTML = ''; return; }
|
||
const targets = [...sec.querySelectorAll('.card, .wg')];
|
||
if(targets.length < 2){ mm.innerHTML = ''; return; }
|
||
mm.innerHTML = '';
|
||
targets.forEach((el, i)=>{
|
||
const dot = document.createElement('div');
|
||
dot.className = 'mm-dot';
|
||
const kindBadge = el.classList.contains('wg') ? 'wg' : 'card';
|
||
const titleEl = el.querySelector('.card-title, .wg-title');
|
||
dot.title = titleEl ? titleEl.textContent.trim() : kindBadge + ' ' + (i+1);
|
||
if(kindBadge === 'wg') dot.style.borderRadius = '3px';
|
||
dot.addEventListener('click', ()=>el.scrollIntoView({behavior:'smooth',block:'center'}));
|
||
mm.appendChild(dot);
|
||
});
|
||
// Update active dot on scroll
|
||
if(_mmScrollHandler) window.removeEventListener('scroll', _mmScrollHandler);
|
||
_mmScrollHandler = ()=>{
|
||
const dots = [...mm.querySelectorAll('.mm-dot')];
|
||
const midY = window.scrollY + window.innerHeight * 0.5;
|
||
let activeI = 0;
|
||
targets.forEach((el, i)=>{
|
||
const r = el.getBoundingClientRect();
|
||
if(r.top + window.scrollY <= midY) activeI = i;
|
||
});
|
||
dots.forEach((d,i)=>d.classList.toggle('active', i === activeI));
|
||
};
|
||
window.addEventListener('scroll', _mmScrollHandler, { passive:true });
|
||
setTimeout(_mmScrollHandler, 100);
|
||
}
|
||
|
||
/* ─── Task 6: Hint system for simp4 & comp ─── */
|
||
/* Enhanced SIMP4_TASKS with hints */
|
||
const SIMP4_HINTS = [
|
||
['Ищи точный квадрат в 72', '72 = 36 × 2', '√72 = 6√2'],
|
||
['Ищи точный квадрат в 50', '50 = 25 × 2', '√50 = 5√2'],
|
||
['Ищи точный квадрат в 48', '48 = 16 × 3', '√48 = 4√3'],
|
||
['Ищи точный квадрат в 200', '200 = 100 × 2', '√200 = 10√2'],
|
||
['Ищи точный квадрат в 75', '75 = 25 × 3', '√75 = 5√3'],
|
||
['Ищи точный квадрат в 98', '98 = 49 × 2', '√98 = 7√2'],
|
||
['Ищи точный квадрат в 18', '18 = 9 × 2', '√18 = 3√2'],
|
||
['Ищи точный квадрат в 128', '128 = 64 × 2', '√128 = 8√2'],
|
||
['Ищи точный квадрат в 80', '80 = 16 × 5', '√80 = 4√5'],
|
||
['Ищи точный квадрат в 108', '108 = 36 × 3', '√108 = 6√3'],
|
||
['Ищи точный квадрат в 147', '147 = 49 × 3', '√147 = 7√3'],
|
||
];
|
||
const _simp4HintLevel = {};
|
||
|
||
function simp4Hint(){
|
||
const idx = simp4State.idx;
|
||
const key = 'simp4_' + idx;
|
||
const level = (_simp4HintLevel[key] || 0);
|
||
const hints = SIMP4_HINTS[idx] || [];
|
||
const hintText = hints[level] || hints[hints.length - 1] || '—';
|
||
const nextLevel = Math.min(level + 1, hints.length - 1);
|
||
_simp4HintLevel[key] = nextLevel;
|
||
let box = document.getElementById('simp4-hint-box');
|
||
if(!box){
|
||
box = document.createElement('div');
|
||
box.id = 'simp4-hint-box';
|
||
const fb = document.getElementById('simp4-fb');
|
||
if(fb) fb.parentNode.insertBefore(box, fb);
|
||
}
|
||
const levelNames = ['Намёк','Шаг','Ответ'];
|
||
box.className = 'hint-box';
|
||
box.innerHTML = `<span class="hint-level-badge">Подсказка ${level + 1}: ${levelNames[level] || 'Ответ'}</span><br>${hintText}`;
|
||
if(level === 2) simp4State.score = Math.max(0, simp4State.score - 5);
|
||
}
|
||
|
||
const COMP_HINTS = [
|
||
['Возведи оба в квадрат', '(3√2)² = 18, (2√3)² = 12', '18 > 12 → 3√2 > 2√3'],
|
||
['Возведи оба в квадрат', '(4√3)² = 48, (3√5)² = 45', '48 > 45 → 4√3 > 3√5'],
|
||
['Возведи оба в квадрат', '(5√2)² = 50, 7² = 49', '50 > 49 → 5√2 > 7'],
|
||
['Возведи оба в квадрат', '(2√7)² = 28, (3√3)² = 27', '28 > 27 → 2√7 > 3√3'],
|
||
['Возведи оба в квадрат', '(√17)² = 17, 4² = 16', '17 > 16 → √17 > 4'],
|
||
['Возведи оба в квадрат', '(√35)² = 35, 6² = 36', '35 < 36 → √35 < 6'],
|
||
];
|
||
const _compHintLevel = {};
|
||
|
||
function compHint(){
|
||
const key = 'comp_' + compIdx;
|
||
const level = (_compHintLevel[key] || 0);
|
||
const hints = COMP_HINTS[compIdx] || [];
|
||
const hintText = hints[level] || hints[hints.length-1] || '—';
|
||
_compHintLevel[key] = Math.min(level + 1, hints.length - 1);
|
||
let box = document.getElementById('comp-hint-box');
|
||
if(!box){
|
||
box = document.createElement('div');
|
||
box.id = 'comp-hint-box';
|
||
const fb = document.getElementById('comp-fb');
|
||
if(fb) fb.parentNode.insertBefore(box, fb);
|
||
}
|
||
const levelNames = ['Намёк','Шаг','Ответ'];
|
||
box.className = 'hint-box';
|
||
box.innerHTML = `<span class="hint-level-badge">Подсказка ${level + 1}: ${levelNames[level] || 'Ответ'}</span><br>${hintText}`;
|
||
}
|
||
|
||
/* ─── Task 7: Mobile sidebar toggle ─── */
|
||
function toggleSidebar(){
|
||
const side = document.getElementById('col-side');
|
||
if(!side) return;
|
||
const isOpen = side.classList.contains('open') || side.classList.contains('side-open');
|
||
if(isOpen){
|
||
side.classList.remove('open','side-open');
|
||
const overlay = document.getElementById('side-overlay');
|
||
if(overlay) overlay.classList.remove('show');
|
||
} else {
|
||
side.classList.add('open','side-open');
|
||
const overlay = document.getElementById('side-overlay');
|
||
if(overlay) overlay.classList.add('show');
|
||
}
|
||
}
|
||
|
||
/* ─── Patch goTo to trigger Wave 3 post-build ─── */
|
||
const _origGoToFinish = window._goToFinish;
|
||
window._goToFinish = function(id){
|
||
_origGoToFinish(id);
|
||
// Build minimap after section is rendered
|
||
setTimeout(()=>{
|
||
_buildMinimap(id);
|
||
_attachBookmarkButtons();
|
||
_applyGlossary(document.getElementById('sec-' + id));
|
||
}, 80);
|
||
};
|
||
|
||
/* ─── Wave 3 INIT ─── */
|
||
function initWave3(){
|
||
_initKeyboard();
|
||
_initSearchInput();
|
||
_initGlossTooltip();
|
||
// Rebuild search index after everything is built
|
||
setTimeout(buildSearchIndex, 1200);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', ()=>setTimeout(initWave3, 100));
|
||
</script>
|
||
|
||
<script>
|
||
'use strict';
|
||
/* ════════════════════════════════════════════════════════
|
||
WAVE 4 — GAMIFICATION
|
||
════════════════════════════════════════════════════════ */
|
||
|
||
/* ── XP levels — единая формула с сервером ── */
|
||
const XP_LEVELS = null; // legacy — теперь уровень считается формулой
|
||
|
||
function calcLevel(xp){ return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
|
||
function _xpForLevel(lv){ return (lv - 1) * (lv - 1) * 100; }
|
||
|
||
function addXp(amount, source){
|
||
if(!amount || amount <= 0) return;
|
||
if(_isMuted()) {} // still give XP even if muted
|
||
const prevLevel = STATE.level;
|
||
STATE.xp += amount;
|
||
STATE.level = calcLevel(STATE.xp);
|
||
saveProgress();
|
||
if(window.LS && window.LS.xp) window.LS.xp.add(amount, 'algebra8-ch1-' + (source || 'misc'));
|
||
refreshProgressUI(); // обновляет XP-бейдж в hero
|
||
|
||
if(STATE.level > prevLevel){
|
||
// Level up!
|
||
const pop = document.getElementById('lvl-popup');
|
||
if(pop){
|
||
document.getElementById('lvl-popup-text').textContent = 'Уровень ' + STATE.level + '!';
|
||
pop.classList.add('show');
|
||
setTimeout(()=>pop.classList.remove('show'), 3000);
|
||
}
|
||
sounds.levelUp();
|
||
if(STATE.level >= 5 && !STATE.achievements.has('lv5')){
|
||
achievement('lv5', 'Достигнут уровень 5');
|
||
}
|
||
}
|
||
// refresh sidebar if open
|
||
const box = document.getElementById('sidebar-content');
|
||
if(box && box.querySelector('.xp-card')){
|
||
const xpForLv = _xpForLevel(STATE.level);
|
||
const xpNext = _xpForLevel(STATE.level + 1);
|
||
const xpPct = xpNext > xpForLv ? Math.round((STATE.xp - xpForLv) / (xpNext - xpForLv) * 100) : 100;
|
||
const fill = box.querySelector('.xp-fill');
|
||
if(fill) fill.style.width = xpPct + '%';
|
||
const xpNums = box.querySelectorAll('.xp-nums span');
|
||
if(xpNums[0]) xpNums[0].textContent = STATE.xp + ' XP';
|
||
if(xpNums[1]) xpNums[1].textContent = STATE.level < 30 ? xpNext + ' XP' : 'MAX';
|
||
const lvEl = box.querySelector('.xp-level');
|
||
if(lvEl) lvEl.textContent = 'Ур. ' + STATE.level;
|
||
}
|
||
}
|
||
|
||
/* ── Streak ── */
|
||
function streakCorrect(){
|
||
STATE.streak++;
|
||
if(STATE.streak > STATE.maxStreak) STATE.maxStreak = STATE.streak;
|
||
saveProgress();
|
||
_updateStreakUI();
|
||
_checkStreakMilestone(STATE.streak);
|
||
}
|
||
|
||
function streakWrong(){
|
||
STATE.streak = 0;
|
||
saveProgress();
|
||
_updateStreakUI();
|
||
}
|
||
|
||
function _updateStreakUI(){
|
||
const box = document.getElementById('sidebar-content');
|
||
if(!box) return;
|
||
const vals = box.querySelectorAll('.streak-val');
|
||
if(vals[0]) vals[0].textContent = STATE.streak;
|
||
if(vals[1]) vals[1].textContent = STATE.maxStreak;
|
||
}
|
||
|
||
function _checkStreakMilestone(n){
|
||
const milestones = [3, 5, 7, 10];
|
||
if(!milestones.includes(n)) return;
|
||
const pop = document.getElementById('streak-popup');
|
||
if(pop){
|
||
document.getElementById('streak-popup-text').textContent = 'Streak \xd7' + n + '!';
|
||
pop.classList.add('show');
|
||
setTimeout(()=>pop.classList.remove('show'), 2200);
|
||
}
|
||
sounds.correct();
|
||
addXp(n * 2, 'streak');
|
||
const achMap = {3:'streak3',5:'streak5',7:'streak7',10:'streak10'};
|
||
if(achMap[n] && !STATE.achievements.has(achMap[n])){
|
||
achievement(achMap[n], ACH_LABELS[achMap[n]]);
|
||
}
|
||
}
|
||
|
||
/* ── Sound effects ── */
|
||
let _audioCtx = null;
|
||
function _getAudioCtx(){
|
||
if(!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||
return _audioCtx;
|
||
}
|
||
|
||
function _isMuted(){
|
||
return localStorage.getItem('algebra8_mute') === '1';
|
||
}
|
||
|
||
function toggleMute(){
|
||
const muted = !_isMuted();
|
||
localStorage.setItem('algebra8_mute', muted ? '1' : '0');
|
||
document.getElementById('sound-on-ic').style.display = muted ? 'none' : '';
|
||
document.getElementById('sound-off-ic').style.display = muted ? '' : 'none';
|
||
}
|
||
|
||
function playTone(freq, duration, type){
|
||
if(_isMuted()) return;
|
||
try{
|
||
const ctx = _getAudioCtx();
|
||
const o = ctx.createOscillator();
|
||
const g = ctx.createGain();
|
||
o.connect(g); g.connect(ctx.destination);
|
||
o.frequency.value = freq;
|
||
o.type = type || 'sine';
|
||
g.gain.setValueAtTime(0.18, ctx.currentTime);
|
||
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + (duration || 0.2));
|
||
o.start(ctx.currentTime);
|
||
o.stop(ctx.currentTime + (duration || 0.2));
|
||
}catch(e){}
|
||
}
|
||
|
||
const sounds = {
|
||
correct: ()=>playTone(880, 0.15),
|
||
wrong: ()=>playTone(220, 0.2, 'sawtooth'),
|
||
levelUp: ()=>{ playTone(523, 0.12); setTimeout(()=>playTone(659, 0.12), 110); setTimeout(()=>playTone(784, 0.18), 230); },
|
||
achievement: ()=>{ playTone(659, 0.1); setTimeout(()=>playTone(880, 0.15), 85); },
|
||
};
|
||
|
||
/* ── Spoiler XP ── */
|
||
function _initSpoilerXp(){
|
||
document.addEventListener('toggle', e=>{
|
||
if(e.target && e.target.classList && e.target.classList.contains('spoiler') && e.target.open){
|
||
if(!e.target._xpGiven){ e.target._xpGiven = true; addXp(2, 'spoiler'); }
|
||
}
|
||
}, true);
|
||
}
|
||
|
||
/* ── Daily Challenge ── */
|
||
const DAILY_TASKS = [
|
||
{q:'Вычислите: $\\sqrt{64 \\cdot 81}$', answer:72, type:'number', hint:'Свойство: √(a·b) = √a · √b'},
|
||
{q:'Сравните: $\\sqrt{37}$ и $6$. Выберите знак:', answer:'<', type:'select', opts:['<','>','='], hint:'6² = 36, 37 > 36, значит √37 > 6? Осторожно!'},
|
||
{q:'Найдите целое число, лежащее между $\\sqrt{51}$ и $\\sqrt{80}$. Введите одно такое число:', answer:[8], type:'number-any', hint:'7² = 49, 8² = 64, 9² = 81. Какие квадраты попадают в диапазон?'},
|
||
{q:'Упростите $\\sqrt{72}$ в форме $a\\sqrt{b}$ — введите значение $a$:', answer:6, type:'number', hint:'72 = 36 · 2, √72 = √36 · √2 = 6√2'},
|
||
{q:'При каком наименьшем целом $x$ выражение $\\sqrt{2x-5}$ имеет смысл? Введите число:', answer:3, type:'number', hint:'Нужно 2x − 5 ≥ 0, то есть x ≥ 2,5. Наименьшее целое?'},
|
||
{q:'Какое из чисел иррационально?', answer:'√7', type:'select', opts:['0,5','√16','√7','1/3'], hint:'√16 = 4 — рациональное, √7 — нельзя записать в виде дроби'},
|
||
{q:'Чему равно $\\sqrt{(\\sqrt{5})^2}$?', answer:'√5', type:'select', opts:['5','√5','25','√25'], hint:'(√a)² = a, затем √(a) = √a'},
|
||
];
|
||
|
||
function _todayStr(){
|
||
const d = new Date();
|
||
return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate();
|
||
}
|
||
|
||
function _initDailyChallenge(){
|
||
const today = _todayStr();
|
||
if(STATE.dailyChallenge.date !== today){
|
||
STATE.dailyChallenge.date = today;
|
||
STATE.dailyChallenge.completed = false;
|
||
STATE.dailyChallenge.taskIdx = Math.floor(Math.random() * DAILY_TASKS.length);
|
||
saveProgress();
|
||
}
|
||
const dot = document.getElementById('daily-dot');
|
||
if(dot) dot.classList.toggle('show', !STATE.dailyChallenge.completed);
|
||
}
|
||
|
||
function openDailyChallenge(){
|
||
const modal = document.getElementById('daily-modal');
|
||
if(modal) modal.classList.add('open');
|
||
_renderDailyChallenge();
|
||
}
|
||
|
||
function closeDailyChallenge(){
|
||
const modal = document.getElementById('daily-modal');
|
||
if(modal) modal.classList.remove('open');
|
||
}
|
||
|
||
function _renderDailyChallenge(){
|
||
const box = document.getElementById('daily-content');
|
||
if(!box) return;
|
||
const t = DAILY_TASKS[STATE.dailyChallenge.taskIdx] || DAILY_TASKS[0];
|
||
|
||
if(STATE.dailyChallenge.completed){
|
||
box.innerHTML = `<div class="daily-done">
|
||
<div class="daily-done-icon">
|
||
<svg viewBox="0 0 24 24" style="width:48px;height:48px;stroke:#10b981;fill:none;stroke-width:2"><circle cx="12" cy="12" r="10"/><polyline points="9 12 11 14 15 10"/></svg>
|
||
</div>
|
||
<div style="font-weight:800;font-size:1rem;color:var(--ok)">Выполнено!</div>
|
||
<div style="font-size:.85rem;color:var(--muted);margin-top:6px">Приходите завтра за новой задачей.</div>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
let inputHtml = '';
|
||
if(t.type === 'select'){
|
||
inputHtml = `<div class="row-c" style="margin-top:12px">${t.opts.map(o=>`<button class="btn daily-opt" onclick="dailySubmit('${o}')" style="font-size:1rem;padding:9px 18px">${o}</button>`).join('')}</div>`;
|
||
} else {
|
||
inputHtml = `<div class="row-c" style="margin-top:12px">
|
||
<input id="daily-inp" class="inp num" type="number" placeholder="Ответ" style="width:120px;font-size:1.1rem">
|
||
<button class="btn primary" onclick="dailyCheckInput()">Сдать</button>
|
||
</div>`;
|
||
}
|
||
|
||
box.innerHTML = `
|
||
<div class="daily-q">${t.q}</div>
|
||
<div class="daily-hint">${t.hint}</div>
|
||
${inputHtml}
|
||
<div id="daily-fb" class="feedback" style="margin-top:10px"></div>`;
|
||
|
||
setTimeout(()=>{
|
||
if(window.renderMathInElement && box){
|
||
try{ renderMathInElement(box, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){}
|
||
}
|
||
}, 30);
|
||
}
|
||
|
||
function dailySubmit(answer){
|
||
const t = DAILY_TASKS[STATE.dailyChallenge.taskIdx] || DAILY_TASKS[0];
|
||
const fb = document.getElementById('daily-fb');
|
||
const correct = (String(answer).trim() === String(t.answer).trim());
|
||
if(fb){
|
||
fb.className = 'feedback ' + (correct ? 'ok' : 'fail');
|
||
fb.textContent = correct ? 'Верно! +30 XP' : 'Не точно. Попробуйте ещё!';
|
||
}
|
||
if(correct) _dailySuccess();
|
||
}
|
||
|
||
function dailyCheckInput(){
|
||
const inp = document.getElementById('daily-inp');
|
||
if(!inp) return;
|
||
const t = DAILY_TASKS[STATE.dailyChallenge.taskIdx] || DAILY_TASKS[0];
|
||
const v = parseFloat(inp.value);
|
||
let correct = false;
|
||
if(t.type === 'number-any'){
|
||
correct = Array.isArray(t.answer) ? t.answer.includes(v) : (v === t.answer);
|
||
} else {
|
||
correct = (Math.abs(v - t.answer) < 0.01);
|
||
}
|
||
const fb = document.getElementById('daily-fb');
|
||
if(fb){
|
||
fb.className = 'feedback ' + (correct ? 'ok' : 'fail');
|
||
fb.textContent = correct ? 'Верно! +30 XP' : 'Не точно. Попробуйте снова!';
|
||
}
|
||
if(correct) _dailySuccess();
|
||
}
|
||
|
||
function _dailySuccess(){
|
||
STATE.dailyChallenge.completed = true;
|
||
saveProgress();
|
||
const dot = document.getElementById('daily-dot');
|
||
if(dot) dot.classList.remove('show');
|
||
addXp(30, 'daily');
|
||
sounds.levelUp();
|
||
confetti();
|
||
if(!STATE.achievements.has('daily_1')){
|
||
achievement('daily_1', ACH_LABELS['daily_1']);
|
||
}
|
||
setTimeout(_renderDailyChallenge, 600);
|
||
}
|
||
|
||
/* ── Achievements Gallery ── */
|
||
const ACH_DEFS = [
|
||
{id:'start', name:'Начало пути', desc:'Открыл учебник впервые', icon:'star'},
|
||
{id:'ring36', name:'Чемпион ринга', desc:'Нашёл сторону ринга 36 м²', icon:'target'},
|
||
{id:'squares', name:'Знаток квадратов', desc:'Лучший результат «Таблица квадратов»',icon:'grid'},
|
||
{id:'exists', name:'Сортировщик', desc:'Правильно рассортировал корни', icon:'filter'},
|
||
{id:'classify',name:'Числовой эксперт', desc:'Классифицировал все числа', icon:'layers'},
|
||
{id:'rat', name:'Охотник на ирр.', desc:'Распознал иррациональные числа', icon:'zap'},
|
||
{id:'match', name:'Match-мастер', desc:'Соединил все выражения Match-игры', icon:'link'},
|
||
{id:'simp4', name:'Упроститель', desc:'Прошёл тренажёр упрощения корней', icon:'scissors'},
|
||
{id:'draw', name:'Чертёжник', desc:'Построил промежуток на оси', icon:'edit'},
|
||
{id:'tariff', name:'Экономист', desc:'Нашёл выгодный тариф', icon:'bar-chart'},
|
||
{id:'ass8', name:'Отличник', desc:'Набрал 8+/10 в самооценке', icon:'award'},
|
||
{id:'pr1', name:'Садовник', desc:'Решил задачу про дорожку с розами', icon:'flower'},
|
||
{id:'pr2', name:'Строитель', desc:'Нашёл количество мешков цемента', icon:'package'},
|
||
{id:'decode', name:'Дешифровщик', desc:'Расшифровал код ДРУЖБА', icon:'key'},
|
||
{id:'daily_1', name:'Ежедневная задача', desc:'Выполнил задачу дня', icon:'calendar'},
|
||
{id:'streak3', name:'Серия x3', desc:'Дал 3 правильных ответа подряд', icon:'flame3'},
|
||
{id:'streak5', name:'На огне!', desc:'Дал 5 правильных ответов подряд', icon:'flame5'},
|
||
{id:'streak7', name:'В ударе', desc:'Дал 7 правильных ответов подряд', icon:'flame7'},
|
||
{id:'streak10',name:'Легенда серии', desc:'Дал 10 правильных ответов подряд', icon:'flame10'},
|
||
{id:'lv5', name:'Уровень 5', desc:'Достиг 5-го уровня XP', icon:'trophy'},
|
||
];
|
||
|
||
const ACH_ICON_SVG = {
|
||
star: '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
|
||
target: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>',
|
||
grid: '<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>',
|
||
filter: '<svg class="ic" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>',
|
||
layers: '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>',
|
||
zap: '<svg class="ic" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
|
||
link: '<svg class="ic" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
|
||
scissors: '<svg class="ic" viewBox="0 0 24 24"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg>',
|
||
edit: '<svg class="ic" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
|
||
'bar-chart':'<svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>',
|
||
award: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>',
|
||
flower: '<svg class="ic" viewBox="0 0 24 24"><path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z"/></svg>',
|
||
package: '<svg class="ic" viewBox="0 0 24 24"><line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>',
|
||
key: '<svg class="ic" viewBox="0 0 24 24"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>',
|
||
calendar: '<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
|
||
flame3: '<svg class="ic" viewBox="0 0 24 24"><path d="M13 2c0 4-4 5-4 9a4 4 0 0 0 8 0c0-4-4-5-4-9z"/><path d="M12 17a1 1 0 0 0 0 2 1 1 0 0 0 0-2z" fill="currentColor"/></svg>',
|
||
flame5: '<svg class="ic" viewBox="0 0 24 24"><path d="M13 2c0 4-4 5-4 9a4 4 0 0 0 8 0c0-4-4-5-4-9z"/><path d="M12 17a1 1 0 0 0 0 2 1 1 0 0 0 0-2z" fill="currentColor"/></svg>',
|
||
flame7: '<svg class="ic" viewBox="0 0 24 24"><path d="M13 2c0 4-4 5-4 9a4 4 0 0 0 8 0c0-4-4-5-4-9z"/><path d="M12 17a1 1 0 0 0 0 2 1 1 0 0 0 0-2z" fill="currentColor"/></svg>',
|
||
flame10: '<svg class="ic" viewBox="0 0 24 24"><path d="M13 2c0 4-4 5-4 9a4 4 0 0 0 8 0c0-4-4-5-4-9z"/><path d="M12 17a1 1 0 0 0 0 2 1 1 0 0 0 0-2z" fill="currentColor"/></svg>',
|
||
trophy: '<svg class="ic" viewBox="0 0 24 24"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg>',
|
||
};
|
||
|
||
function openAchGallery(){
|
||
const modal = document.getElementById('ach-gallery-modal');
|
||
if(modal) modal.classList.add('open');
|
||
_renderAchGallery();
|
||
}
|
||
|
||
function closeAchGallery(){
|
||
const modal = document.getElementById('ach-gallery-modal');
|
||
if(modal) modal.classList.remove('open');
|
||
}
|
||
|
||
function _renderAchGallery(){
|
||
const grid = document.getElementById('ach-gallery-grid');
|
||
const cnt = document.getElementById('ach-gallery-count');
|
||
if(!grid) return;
|
||
if(cnt) cnt.textContent = STATE.achievements.size + ' / ' + ACH_DEFS.length + ' получено';
|
||
grid.innerHTML = ACH_DEFS.map(def=>{
|
||
const earned = STATE.achievements.has(def.id);
|
||
const iconSvg = ACH_ICON_SVG[def.icon] || ACH_ICON_SVG['star'];
|
||
const dateNote = earned ? '<div class="ach-card-date">✓ Получено</div>' : '';
|
||
return `<div class="ach-card${earned?' earned':''}">
|
||
<div class="ach-card-icon">${iconSvg}</div>
|
||
<div class="ach-card-title">${def.name}</div>
|
||
<div class="ach-card-desc">${def.desc}</div>
|
||
${dateNote}
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
/* ── Final Chapter Modal ── */
|
||
function showFinalChapterModal(){
|
||
const modal = document.getElementById('final-chapter-modal');
|
||
if(!modal) return;
|
||
modal.classList.add('open');
|
||
const statsBox = document.getElementById('fc-stats-box');
|
||
if(statsBox){
|
||
statsBox.innerHTML = `
|
||
<div class="fc-stat"><div class="fc-stat-val">${STATE.xp}</div><div class="fc-stat-lab">XP всего</div></div>
|
||
<div class="fc-stat"><div class="fc-stat-val">${STATE.maxStreak}</div><div class="fc-stat-lab">Макс. серия</div></div>
|
||
<div class="fc-stat"><div class="fc-stat-val">${STATE.achievements.size}</div><div class="fc-stat-lab">Ачивок</div></div>`;
|
||
}
|
||
// Big confetti burst
|
||
for(let i=0;i<5;i++) setTimeout(()=>confetti(), i*200);
|
||
}
|
||
|
||
function closeFinalChapterModal(){
|
||
const modal = document.getElementById('final-chapter-modal');
|
||
if(modal) modal.classList.remove('open');
|
||
}
|
||
|
||
/* ── XP hooks via direct wrapping of key functions ── */
|
||
(function(){
|
||
// squaresAnswer +5 XP on correct
|
||
const _orig = window.squaresAnswer;
|
||
if(typeof _orig === 'function'){
|
||
window.squaresAnswer = function(picked, btn){
|
||
const wasCorrect = sqState && picked === sqState.answer;
|
||
_orig(picked, btn);
|
||
if(wasCorrect) addXp(5, 'squares');
|
||
};
|
||
}
|
||
|
||
// simpCheck +5 XP
|
||
const _origSimp = window.simpCheck;
|
||
if(typeof _origSimp === 'function'){
|
||
window.simpCheck = function(){
|
||
const t = SIMP_TASKS[simpIdx];
|
||
const v = parseFloat(document.getElementById('simp-ans').value.replace(',','.'));
|
||
const wasCorrect = !isNaN(v) && Math.abs(v - t.a) < 0.02;
|
||
_origSimp();
|
||
if(wasCorrect) addXp(5, 'trainer');
|
||
};
|
||
}
|
||
|
||
// simp4Check +5 XP
|
||
const _origSimp4 = window.simp4Check;
|
||
if(typeof _origSimp4 === 'function'){
|
||
window.simp4Check = function(){
|
||
const t = SIMP4_TASKS[simp4State.idx];
|
||
const a = +document.getElementById('simp4-a').value;
|
||
const b = +document.getElementById('simp4-b').value;
|
||
const wasCorrect = (a === t.a && b === t.b);
|
||
_origSimp4();
|
||
if(wasCorrect) addXp(5, 'trainer');
|
||
};
|
||
}
|
||
|
||
// compSet +5 XP
|
||
const _origComp = window.compSet;
|
||
if(typeof _origComp === 'function'){
|
||
window.compSet = function(pick){
|
||
const t = COMP_TASKS[compIdx];
|
||
const wasCorrect = (pick === 'a' && t.av > t.bv) || (pick === 'b' && t.bv > t.av);
|
||
_origComp(pick);
|
||
if(wasCorrect) addXp(5, 'trainer');
|
||
};
|
||
}
|
||
|
||
// picCheck +8 XP
|
||
const _origPic = window.picCheck;
|
||
if(typeof _origPic === 'function'){
|
||
window.picCheck = function(){
|
||
const t = PIC_TASKS[picIdx];
|
||
const n1 = +document.getElementById('pic-num1').value;
|
||
const n2 = +document.getElementById('pic-num2').value;
|
||
const r1 = document.getElementById('pic-rel1').value;
|
||
const r2 = document.getElementById('pic-rel2').value;
|
||
const expectR1 = t.lOpen ? '>' : '≥';
|
||
const expectR2 = t.rOpen ? '<' : '≤';
|
||
const wasCorrect = (n1===t.a&&r1===expectR1&&n2===t.b&&r2===expectR2)||(n2===t.a&&r2===expectR1&&n1===t.b&&r1===expectR2);
|
||
_origPic();
|
||
if(wasCorrect) addXp(8, 'task');
|
||
};
|
||
}
|
||
|
||
// drawCheck +8 XP
|
||
const _origDraw = window.drawCheck;
|
||
if(typeof _origDraw === 'function'){
|
||
window.drawCheck = function(){
|
||
const t = DRAW_TASKS[drawIdx];
|
||
const wasCorrect = (DR.l===t.a&&DR.r===t.b&&DR.lOpen===t.lOpen&&DR.rOpen===t.rOpen);
|
||
_origDraw();
|
||
if(wasCorrect) addXp(8, 'task');
|
||
};
|
||
}
|
||
|
||
// fiCheck +8 XP
|
||
const _origFi = window.fiCheck;
|
||
if(typeof _origFi === 'function'){
|
||
window.fiCheck = function(){
|
||
const t = FI_TASKS[fiIdx];
|
||
let correct = 0, wrong = 0;
|
||
document.querySelectorAll('#fi-grid button').forEach(b=>{
|
||
const n = +b.dataset.n;
|
||
const picked = b.dataset.picked === '1';
|
||
const inSol = t.sol.includes(n);
|
||
if(picked && inSol) correct++; else if(picked) wrong++; else if(inSol) wrong++;
|
||
});
|
||
const wasCorrect = (wrong===0 && correct===t.sol.length);
|
||
_origFi();
|
||
if(wasCorrect) addXp(8, 'task');
|
||
};
|
||
}
|
||
|
||
// matchCheck already fires feedback() which increments streak.
|
||
// But we also add +5 per pair matched.
|
||
const _origMatch = window.matchCheck;
|
||
if(typeof _origMatch === 'function'){
|
||
window.matchCheck = function(){
|
||
const prevDone = matchState ? matchState.done.length : 0;
|
||
_origMatch();
|
||
const curDone = matchState ? matchState.done.length : prevDone;
|
||
if(curDone > prevDone) addXp(5, 'match');
|
||
};
|
||
}
|
||
|
||
// assCheckAll — +10 per correct answer
|
||
const _origAss = window.assCheckAll;
|
||
if(typeof _origAss === 'function'){
|
||
window.assCheckAll = function(){
|
||
const prevXp = STATE.xp;
|
||
_origAss();
|
||
// count right answers: already calculated inside assCheckAll, we give bonus per right
|
||
// Read the score display
|
||
const scoreEl = document.getElementById('ass-score');
|
||
const right = scoreEl ? +scoreEl.textContent : 0;
|
||
addXp(right * 10, 'ass');
|
||
};
|
||
}
|
||
})();
|
||
|
||
/* ── Wrap feedback() for sounds + streak ── */
|
||
(function(){
|
||
const _origFeedback = window.feedback;
|
||
if(typeof _origFeedback !== 'function') return;
|
||
let _inFeedback = false;
|
||
window.feedback = function(elm, ok, text){
|
||
_origFeedback(elm, ok, text);
|
||
if(_inFeedback) return; // avoid re-entry from addXp→achievement→feedback
|
||
_inFeedback = true;
|
||
if(ok){ sounds.correct(); streakCorrect(); }
|
||
else { sounds.wrong(); streakWrong(); }
|
||
_inFeedback = false;
|
||
};
|
||
})();
|
||
|
||
/* ── Wave 4 INIT ── */
|
||
function initWave4(){
|
||
_initSpoilerXp();
|
||
_initDailyChallenge();
|
||
// mute state restore
|
||
if(_isMuted()){
|
||
const so = document.getElementById('sound-on-ic');
|
||
const sf = document.getElementById('sound-off-ic');
|
||
if(so) so.style.display = 'none';
|
||
if(sf) sf.style.display = '';
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', ()=>setTimeout(initWave4, 200));
|
||
</script>
|
||
|
||
<script>
|
||
'use strict';
|
||
/* ════════════════════════════════════════════════════════
|
||
WAVE DEPTH — 4 new interactive widgets
|
||
════════════════════════════════════════════════════════ */
|
||
|
||
/* ══════════════════════════════════════════════
|
||
WIDGET 1 — Column root extraction (§1)
|
||
══════════════════════════════════════════════ */
|
||
let _clRunning = false;
|
||
|
||
function clPreset(n){
|
||
const inp = document.getElementById('cl-n');
|
||
if(inp){ inp.value = n; }
|
||
}
|
||
|
||
async function clStart(){
|
||
if(_clRunning) return;
|
||
const inp = document.getElementById('cl-n');
|
||
const ws = document.getElementById('cl-workspace');
|
||
const ex = document.getElementById('cl-explain');
|
||
if(!inp || !ws || !ex) return;
|
||
const N = Math.abs(Math.round(+inp.value)) || 5184;
|
||
if(N < 1 || N > 999999){ ex.textContent = 'Введите число от 1 до 999999.'; return; }
|
||
|
||
_clRunning = true;
|
||
ws.innerHTML = '';
|
||
ex.textContent = 'Начинаем...';
|
||
|
||
// Step 1: Split into pairs from right
|
||
const digits = '' + N;
|
||
const pairs = [];
|
||
for(let i = digits.length; i > 0; i -= 2){
|
||
pairs.unshift(digits.slice(Math.max(0, i-2), i));
|
||
}
|
||
const pairsStr = pairs.join(' | ');
|
||
|
||
// Step 2: compute actual integer sqrt for display
|
||
const sqrtN = Math.sqrt(N);
|
||
const isExact = Math.abs(sqrtN - Math.round(sqrtN)) < 1e-9;
|
||
const answerInt = isExact ? Math.round(sqrtN) : Math.floor(sqrtN);
|
||
|
||
// Build steps
|
||
let remainder = 0;
|
||
let result = '';
|
||
let lines = [];
|
||
|
||
lines.push(` Число: ${N}`);
|
||
lines.push(` Грани: ${pairsStr}`);
|
||
lines.push(` ${'─'.repeat(30)}`);
|
||
ws.innerHTML = '<span>' + lines.join('\n') + '</span>';
|
||
ex.innerHTML = '<b>Шаг 1.</b> Разбиваем число на грани по 2 цифры справа налево: <b>' + pairsStr + '</b>';
|
||
await sleep(1000);
|
||
|
||
for(let pi = 0; pi < pairs.length; pi++){
|
||
const pairVal = parseInt(pairs[pi], 10);
|
||
// Bring down the pair
|
||
const current = remainder * 100 + pairVal;
|
||
// Double current answer for divisor base
|
||
const doubleResult = (result === '') ? 0 : parseInt(result, 10) * 2;
|
||
|
||
// Find next digit d: (doubleResult*10 + d) * d <= current
|
||
let d = 0;
|
||
for(let t = 9; t >= 0; t--){
|
||
if((doubleResult * 10 + t) * t <= current){ d = t; break; }
|
||
}
|
||
const subtract = (doubleResult * 10 + d) * d;
|
||
const newRemainder = current - subtract;
|
||
|
||
result += '' + d;
|
||
|
||
// Add to display
|
||
if(pi === 0){
|
||
lines.push(` <span class="cl-active">${pairVal}</span> | √${N}`);
|
||
lines.push(` <span class="cl-active">-${subtract}</span> | <span class="cl-result">${d}</span>`);
|
||
lines.push(` ${'─'.repeat(18)}`);
|
||
lines.push(` ${newRemainder}`);
|
||
} else {
|
||
lines.push(` <span class="cl-active">${current}</span>`);
|
||
lines.push(` <span class="cl-active">-${subtract}</span> | <span class="cl-result">${result}</span> ← (${doubleResult}·${d} = ${doubleResult*d}; добавили ${d}: ${doubleResult*10+d}×${d}=${subtract})`);
|
||
lines.push(` ${'─'.repeat(18)}`);
|
||
lines.push(` ${newRemainder}`);
|
||
}
|
||
ws.innerHTML = '<span>' + lines.join('\n') + '</span>';
|
||
|
||
if(pi === 0){
|
||
ex.innerHTML = `<b>Шаг ${pi*3+2}.</b> Берём первую грань <b>${pairVal}</b>. Ищем наибольшее <b>d</b> такое что <b>d² ≤ ${pairVal}</b>: d = <b>${d}</b> (${d}² = ${d*d}). Записываем в ответ. Вычитаем: ${pairVal} − ${subtract} = <b>${newRemainder}</b>.`;
|
||
} else {
|
||
ex.innerHTML = `<b>Шаг ${pi*3+2}.</b> Сносим грань <b>${pairs[pi]}</b> → получаем <b>${current}</b>. Удваиваем ответ: ${parseInt(result.slice(0,-1)||'0',10)*2} → заготовка делителя <b>${doubleResult}</b>. Подбираем d: (${doubleResult*10}+d)×d ≤ ${current}. Подходит d = <b>${d}</b>. Вычитаем: ${current} − ${subtract} = <b>${newRemainder}</b>.`;
|
||
}
|
||
remainder = newRemainder;
|
||
await sleep(1100);
|
||
}
|
||
|
||
// Final
|
||
if(remainder === 0){
|
||
lines.push('');
|
||
lines.push(` <span class="cl-result">√${N} = ${result}</span> (остаток 0 — точный квадрат)`);
|
||
ws.innerHTML = '<span>' + lines.join('\n') + '</span>';
|
||
ex.innerHTML = `<b>Готово!</b> Остаток равен 0 — корень извлечён точно. <b>√${N} = ${result}</b>.`;
|
||
await sleep(600);
|
||
// Badge
|
||
ex.innerHTML += ` <span class="cl-final-badge">√${N} = ${result}</span>`;
|
||
confetti();
|
||
addXp(15, 'col-root');
|
||
bumpProgress('p1', 8);
|
||
achievement('col-root', 'Извлёк корень в столбик');
|
||
} else {
|
||
lines.push('');
|
||
lines.push(` Остаток: <span class="cl-active">${remainder}</span> (не 0 — ${N} не точный квадрат)`);
|
||
lines.push(` <span class="cl-result">⌊√${N}⌋ ≈ ${answerInt}</span>`);
|
||
ws.innerHTML = '<span>' + lines.join('\n') + '</span>';
|
||
ex.innerHTML = `<b>Внимание:</b> остаток ${remainder} ≠ 0. Число <b>${N}</b> — не точный квадрат. Целая часть корня: <b>⌊√${N}⌋ = ${answerInt}</b>. Точный ответ иррационален.`;
|
||
addXp(8, 'col-root-approx');
|
||
bumpProgress('p1', 4);
|
||
}
|
||
|
||
_clRunning = false;
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════
|
||
WIDGET 2 — Square comparison SVG (§4)
|
||
══════════════════════════════════════════════ */
|
||
const SQ_PAIRS = [
|
||
{ aExpr:'3√2', aSq:18, bExpr:'2√3', bSq:12 },
|
||
{ aExpr:'4√3', aSq:48, bExpr:'3√5', bSq:45 },
|
||
{ aExpr:'5√2', aSq:50, bExpr:'7', bSq:49 },
|
||
{ aExpr:'2√7', aSq:28, bExpr:'3√3', bSq:27 },
|
||
{ aExpr:'√17', aSq:17, bExpr:'4', bSq:16 },
|
||
];
|
||
let sqPairIdx = 0;
|
||
let _sqAnimating = false;
|
||
|
||
function sqRender(){
|
||
const p = SQ_PAIRS[sqPairIdx];
|
||
const ae = document.getElementById('sq-a-expr');
|
||
const be = document.getElementById('sq-b-expr');
|
||
if(ae) ae.textContent = p.aExpr;
|
||
if(be) be.textContent = p.bExpr;
|
||
const svg = document.getElementById('sq-svg');
|
||
if(svg){
|
||
svg.innerHTML = '<text x="300" y="110" text-anchor="middle" font-size="18" fill="currentColor" opacity="0.5">Нажмите «Возвести в квадрат и сравнить»</text>';
|
||
}
|
||
const con = document.getElementById('sq-conclusion');
|
||
if(con) con.innerHTML = '';
|
||
}
|
||
|
||
async function sqAnimate(){
|
||
if(_sqAnimating) return;
|
||
_sqAnimating = true;
|
||
const p = SQ_PAIRS[sqPairIdx];
|
||
const svg = document.getElementById('sq-svg');
|
||
const con = document.getElementById('sq-conclusion');
|
||
if(!svg) { _sqAnimating = false; return; }
|
||
|
||
const W = 600, H = 220;
|
||
const maxSq = Math.max(p.aSq, p.bSq);
|
||
const maxSide = 100;
|
||
const sideA = maxSide * Math.sqrt(p.aSq / maxSq);
|
||
const sideB = maxSide * Math.sqrt(p.bSq / maxSq);
|
||
const cy = H / 2;
|
||
const cxA = 140, cxB = 460;
|
||
|
||
svg.innerHTML = '';
|
||
|
||
// Draw grid helper function as tiny squares
|
||
function makeRect(cx, cy, side, col, label, sqVal, expr){
|
||
const x = cx - side/2, y = cy - side/2;
|
||
const g = document.createElementNS('http://www.w3.org/2000/svg','g');
|
||
g.style.transformOrigin = `${cx}px ${cy}px`;
|
||
g.style.transform = 'scale(0)';
|
||
g.style.transition = 'transform 0.7s cubic-bezier(0.34,1.56,0.64,1)';
|
||
|
||
const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');
|
||
rect.setAttribute('x', x); rect.setAttribute('y', y);
|
||
rect.setAttribute('width', side); rect.setAttribute('height', side);
|
||
rect.setAttribute('fill', col + '33');
|
||
rect.setAttribute('stroke', col);
|
||
rect.setAttribute('stroke-width', '2.5');
|
||
rect.setAttribute('rx', '4');
|
||
g.appendChild(rect);
|
||
|
||
// Side label — top
|
||
const tSide = document.createElementNS('http://www.w3.org/2000/svg','text');
|
||
tSide.setAttribute('x', cx); tSide.setAttribute('y', y - 8);
|
||
tSide.setAttribute('text-anchor','middle');
|
||
tSide.setAttribute('font-size','14');
|
||
tSide.setAttribute('font-weight','700');
|
||
tSide.setAttribute('fill', col);
|
||
tSide.textContent = expr;
|
||
g.appendChild(tSide);
|
||
|
||
// Area label — center
|
||
const tArea = document.createElementNS('http://www.w3.org/2000/svg','text');
|
||
tArea.setAttribute('x', cx); tArea.setAttribute('y', cy + 5);
|
||
tArea.setAttribute('text-anchor','middle');
|
||
tArea.setAttribute('font-size','20');
|
||
tArea.setAttribute('font-weight','900');
|
||
tArea.setAttribute('fill', col);
|
||
tArea.textContent = '(' + expr + ')² = ' + sqVal;
|
||
g.appendChild(tArea);
|
||
|
||
// Expression bottom
|
||
const tExpr = document.createElementNS('http://www.w3.org/2000/svg','text');
|
||
tExpr.setAttribute('x', cx); tExpr.setAttribute('y', y + side + 20);
|
||
tExpr.setAttribute('text-anchor','middle');
|
||
tExpr.setAttribute('font-size','14');
|
||
tExpr.setAttribute('fill','currentColor');
|
||
tExpr.setAttribute('opacity','0.7');
|
||
tExpr.textContent = label;
|
||
g.appendChild(tExpr);
|
||
|
||
return g;
|
||
}
|
||
|
||
const colA = '#0288d1', colB = '#c2185b';
|
||
const gA = makeRect(cxA, cy, sideA, colA, 'A = ' + p.aExpr, p.aSq, p.aExpr);
|
||
const gB = makeRect(cxB, cy, sideB, colB, 'B = ' + p.bExpr, p.bSq, p.bExpr);
|
||
|
||
// VS text
|
||
const vsText = document.createElementNS('http://www.w3.org/2000/svg','text');
|
||
vsText.setAttribute('x', W/2); vsText.setAttribute('y', cy + 6);
|
||
vsText.setAttribute('text-anchor','middle');
|
||
vsText.setAttribute('font-size','22');
|
||
vsText.setAttribute('font-weight','700');
|
||
vsText.setAttribute('fill','#888');
|
||
vsText.textContent = 'vs';
|
||
svg.appendChild(vsText);
|
||
svg.appendChild(gA);
|
||
svg.appendChild(gB);
|
||
|
||
await sleep(50);
|
||
gA.style.transform = 'scale(1)';
|
||
await sleep(200);
|
||
gB.style.transform = 'scale(1)';
|
||
await sleep(800);
|
||
|
||
// Winner arrow/badge
|
||
const bigger = p.aSq > p.bSq ? 'A' : (p.bSq > p.aSq ? 'B' : 'equal');
|
||
const winX = bigger === 'A' ? cxA : bigger === 'B' ? cxB : W/2;
|
||
const winCol = bigger === 'A' ? colA : bigger === 'B' ? colB : '#10b981';
|
||
const winText = p.aSq > p.bSq
|
||
? p.aExpr + ' > ' + p.bExpr
|
||
: (p.bSq > p.aSq ? p.bExpr + ' > ' + p.aExpr : p.aExpr + ' = ' + p.bExpr);
|
||
|
||
const badge = document.createElementNS('http://www.w3.org/2000/svg','g');
|
||
const br = document.createElementNS('http://www.w3.org/2000/svg','rect');
|
||
br.setAttribute('x', winX - 80); br.setAttribute('y', 10);
|
||
br.setAttribute('width', 160); br.setAttribute('height', 34);
|
||
br.setAttribute('rx', 17); br.setAttribute('fill', winCol);
|
||
badge.appendChild(br);
|
||
const bt = document.createElementNS('http://www.w3.org/2000/svg','text');
|
||
bt.setAttribute('x', winX); bt.setAttribute('y', 33);
|
||
bt.setAttribute('text-anchor','middle');
|
||
bt.setAttribute('font-size','15');
|
||
bt.setAttribute('font-weight','800');
|
||
bt.setAttribute('fill','#fff');
|
||
bt.textContent = winText;
|
||
badge.appendChild(bt);
|
||
badge.style.opacity = '0';
|
||
badge.style.transition = 'opacity 0.4s ease';
|
||
svg.appendChild(badge);
|
||
await sleep(50);
|
||
badge.style.opacity = '1';
|
||
|
||
if(con){
|
||
const sign = p.aSq > p.bSq ? '>' : (p.bSq > p.aSq ? '<' : '=');
|
||
const conclusion = `${p.aExpr} ${sign} ${p.bExpr}, потому что (${p.aExpr})² = ${p.aSq} ${sign} ${p.bSq} = (${p.bExpr})²`;
|
||
con.innerHTML = '<b style="color:var(--ok)">' + conclusion + '</b>';
|
||
bumpProgress('p4', 4);
|
||
addXp(8, 'sq-compare');
|
||
achievement('sq-compare', 'Сравнил через квадрат');
|
||
confetti();
|
||
}
|
||
_sqAnimating = false;
|
||
}
|
||
|
||
function sqNext(){
|
||
sqPairIdx = (sqPairIdx + 1) % SQ_PAIRS.length;
|
||
_sqAnimating = false;
|
||
sqRender();
|
||
}
|
||
|
||
function initSqCompare(){
|
||
sqPairIdx = 0;
|
||
sqRender();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════
|
||
WIDGET 3 — Euler–Venn diagrams (§5)
|
||
══════════════════════════════════════════════ */
|
||
let _evMode = 'both';
|
||
|
||
function _evGetVals(){
|
||
const aLo = +document.getElementById('ev-a-lo-s').value;
|
||
const aHi = +document.getElementById('ev-a-hi-s').value;
|
||
const bLo = +document.getElementById('ev-b-lo-s').value;
|
||
const bHi = +document.getElementById('ev-b-hi-s').value;
|
||
return {
|
||
aLo: Math.min(aLo, aHi),
|
||
aHi: Math.max(aLo, aHi),
|
||
bLo: Math.min(bLo, bHi),
|
||
bHi: Math.max(bLo, bHi),
|
||
};
|
||
}
|
||
|
||
function evMode(m){
|
||
_evMode = m;
|
||
_evDraw();
|
||
}
|
||
|
||
function _evDraw(){
|
||
const svg = document.getElementById('ev-svg');
|
||
const res = document.getElementById('ev-result');
|
||
if(!svg) return;
|
||
|
||
const {aLo, aHi, bLo, bHi} = _evGetVals();
|
||
|
||
// Update labels
|
||
const aLoLbl = document.getElementById('ev-a-lo'); if(aLoLbl) aLoLbl.textContent = aLo;
|
||
const aHiLbl = document.getElementById('ev-a-hi'); if(aHiLbl) aHiLbl.textContent = aHi;
|
||
const bLoLbl = document.getElementById('ev-b-lo'); if(bLoLbl) bLoLbl.textContent = bLo;
|
||
const bHiLbl = document.getElementById('ev-b-hi'); if(bHiLbl) bHiLbl.textContent = bHi;
|
||
|
||
const W = 520, H = 280;
|
||
// Map values to x coordinates in SVG
|
||
const VMIN = -5, VMAX = 10;
|
||
function toX(v){ return 50 + (v - VMIN) / (VMAX - VMIN) * (W - 100); }
|
||
|
||
const xA1 = toX(aLo), xA2 = toX(aHi);
|
||
const xB1 = toX(bLo), xB2 = toX(bHi);
|
||
const ry = 40; // ellipse y-radius
|
||
const cyA = 100, cyB = 180;
|
||
const rxA = Math.max(10, (xA2 - xA1) / 2);
|
||
const rxB = Math.max(10, (xB2 - xB1) / 2);
|
||
const cxA = (xA1 + xA2) / 2;
|
||
const cxB = (xB1 + xB2) / 2;
|
||
|
||
// Intersection
|
||
const interLo = Math.max(aLo, bLo);
|
||
const interHi = Math.min(aHi, bHi);
|
||
const hasInter = interLo <= interHi;
|
||
const interX1 = toX(interLo), interX2 = toX(interHi);
|
||
const interRx = Math.max(0, (interX2 - interX1) / 2);
|
||
const interCx = (interX1 + interX2) / 2;
|
||
|
||
// Union
|
||
const unionLo = Math.min(aLo, bLo);
|
||
const unionHi = Math.max(aHi, bHi);
|
||
|
||
let html = `<defs>
|
||
<clipPath id="clip-a"><ellipse cx="${cxA}" cy="${cyA}" rx="${rxA}" ry="${ry}"/></clipPath>
|
||
<clipPath id="clip-b"><ellipse cx="${cxB}" cy="${cyB}" rx="${rxB}" ry="${ry}"/></clipPath>
|
||
</defs>`;
|
||
|
||
// Base ellipses
|
||
const opA = _evMode === 'inter' ? '0.35' : '0.7';
|
||
const opB = _evMode === 'inter' ? '0.35' : '0.7';
|
||
html += `<ellipse cx="${cxA}" cy="${cyA}" rx="${rxA}" ry="${ry}" fill="rgba(3,169,244,0.18)" stroke="#0288d1" stroke-width="2.5" opacity="${opA}"/>`;
|
||
html += `<ellipse cx="${cxB}" cy="${cyB}" rx="${rxB}" ry="${ry}" fill="rgba(233,30,99,0.18)" stroke="#c2185b" stroke-width="2.5" opacity="${opB}"/>`;
|
||
|
||
// Labels
|
||
html += `<text x="${cxA}" y="${cyA+5}" text-anchor="middle" font-size="16" font-weight="800" fill="#0288d1">A</text>`;
|
||
html += `<text x="${cxB}" y="${cyB+5}" text-anchor="middle" font-size="16" font-weight="800" fill="#c2185b">B</text>`;
|
||
|
||
// Range labels below each ellipse
|
||
html += `<text x="${cxA}" y="${cyA + ry + 14}" text-anchor="middle" font-size="11" fill="currentColor" opacity="0.7">[${aLo}; ${aHi}]</text>`;
|
||
html += `<text x="${cxB}" y="${cyB + ry + 14}" text-anchor="middle" font-size="11" fill="currentColor" opacity="0.7">[${bLo}; ${bHi}]</text>`;
|
||
|
||
// Mode-specific highlights
|
||
if(_evMode === 'inter' || _evMode === 'both'){
|
||
if(hasInter && interRx > 0){
|
||
// Check if ellipses actually overlap in y (they're on different y levels but show as overlapping if x-ranges cross)
|
||
// We just highlight intersection region as a vertical band between both ellipses
|
||
html += `<rect x="${interX1}" y="${cyA - ry}" width="${Math.max(0,interX2-interX1)}" height="${cyB + ry - (cyA - ry)}" fill="rgba(16,185,129,0.28)" stroke="#10b981" stroke-width="2" stroke-dasharray="5,3" rx="4"/>`;
|
||
html += `<text x="${interCx}" y="${(cyA+cyB)/2+5}" text-anchor="middle" font-size="13" font-weight="700" fill="#10b981">∩</text>`;
|
||
}
|
||
}
|
||
|
||
if(_evMode === 'union' || _evMode === 'both'){
|
||
// Draw union outline — thick gold border around combined span
|
||
const ux1 = toX(unionLo), ux2 = toX(unionHi);
|
||
html += `<rect x="${ux1 - 4}" y="${cyA - ry - 4}" width="${ux2 - ux1 + 8}" height="${cyB + ry + 8 - (cyA - ry - 4)}" fill="none" stroke="#f59e0b" stroke-width="3" rx="8" opacity="0.85"/>`;
|
||
html += `<text x="${(ux1+ux2)/2}" y="${cyA - ry - 10}" text-anchor="middle" font-size="12" font-weight="700" fill="#f59e0b">∪</text>`;
|
||
}
|
||
|
||
svg.innerHTML = html;
|
||
|
||
// Result text
|
||
if(res){
|
||
const interStr = hasInter ? `[${interLo}; ${interHi}]` : '∅';
|
||
const unionStr = `[${unionLo}; ${unionHi}]`;
|
||
if(_evMode === 'inter') res.innerHTML = `$A \\cap B = ${interStr}$`;
|
||
else if(_evMode === 'union') res.innerHTML = `$A \\cup B = ${unionStr}$`;
|
||
else res.innerHTML = `$A \\cup B = ${unionStr}$, $A \\cap B = ${interStr}$`;
|
||
if(typeof renderMath === 'function') renderMath(res);
|
||
bumpProgress('p5', 2);
|
||
}
|
||
}
|
||
|
||
function initEulerVenn(){
|
||
['ev-a-lo-s','ev-a-hi-s','ev-b-lo-s','ev-b-hi-s'].forEach(id=>{
|
||
const e = document.getElementById(id);
|
||
if(e) e.addEventListener('input', _evDraw);
|
||
});
|
||
_evMode = 'both';
|
||
_evDraw();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════
|
||
PATCH buildP4 / buildP5 to init new widgets
|
||
══════════════════════════════════════════════ */
|
||
document.addEventListener('DOMContentLoaded', function(){
|
||
// Patch _goToFinish after all other DOMContentLoaded hooks have registered
|
||
// We use a late timeout so Wave3 patch (at +100ms) has already run
|
||
setTimeout(function(){
|
||
const _origFinish = window._goToFinish;
|
||
window._goToFinish = function(id){
|
||
_origFinish(id);
|
||
if(id === 'p4') setTimeout(initSqCompare, 80);
|
||
if(id === 'p5') setTimeout(initEulerVenn, 80);
|
||
};
|
||
}, 300);
|
||
});
|
||
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|