660e7e2747
Until now the 'gamification' feature flag did nothing: it had no row in
app_settings, the admin couldn't toggle it, awardXP/awardCoins ignored
it, and the CSS only hid three dashboard widgets — XP bars in textbooks
stayed visible regardless.
Phase 1 closes every hole.
Backend (source of truth):
• migration 029 seeds feature_gamification_enabled=1
• new isGamificationEnabled() helper in gamification/_shared.js with a
30s cache + invalidateGamificationCache() for instant admin toggles
• awardXP / awardCoins / updateStreak / unlockAchievement /
checkAchievements all bail out when the flag is off
• /api/gamification/* and /api/shop/* (user routes) return 404 when
disabled; admin routes remain open so the switch itself is reachable
• adminController.updateFeatures gains 'gamification' in the allow-list
and invalidates the cache on flip
Frontend:
• LS.isGamificationEnabled() (synchronous, populated by loadFeatures)
so xp.js + applyCosmetics can bail without a round-trip
• xp.js load/add/flush become no-ops when the flag is off
• applyCosmetics skips the round-trip when off
• CSS .no-gamification rule expanded to cover .hero-xp-badge, .po-xp,
.xp-card, .xp-bar, #frames-section, and a universal [data-gamified]
hook for future blocks
Textbooks (Variant 2 of the plan):
• backend/scripts/wrap_textbook_xp.py — idempotent script that adds
data-gamified to 167 XP tags across 63 textbook files (chapters +
hubs, all subjects/grades). Single CSS rule now hides everything.
Verified end-to-end: with the flag off, awardXP/awardCoins write nothing;
flipping back restores normal behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2072 lines
145 KiB
HTML
2072 lines
145 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||
<meta http-equiv="Pragma" content="no-cache">
|
||
<meta http-equiv="Expires" content="0">
|
||
<title>Алгебра 9 · Глава 1 · Рациональные выражения</title>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<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>
|
||
<script src="/js/api.js" defer></script>
|
||
<script src="/js/xp.js" defer></script>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--bg:#fafafa; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
|
||
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
|
||
--pri:#d97706; --pri2:#b45309; --pri-soft:#fef3c7;
|
||
--acc:#f59e0b; --acc2:#d97706; --acc-soft:#fef9c3;
|
||
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
|
||
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
|
||
}
|
||
.dark{--bg:#0a0a0e; --card:#13120a; --card-soft:#18160a; --text:#fef9e7; --ink:#fef9e7; --muted:#a39070; --border:#2a2512}
|
||
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
|
||
button,input,select,textarea{font-family:inherit;font-size:inherit}
|
||
button{cursor:pointer;border:0;background:transparent;color:inherit}
|
||
a{color:inherit;text-decoration:none}
|
||
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
|
||
|
||
.hdr{position:relative;background:linear-gradient(110deg,#92400e 0%,#d97706 55%,#fbbf24 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(251,191,36,.2);min-height:130px}
|
||
.hdr::before{content:'ГЛАВА 1';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,235,180,.12);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-family:'Unbounded',sans-serif;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;flex-wrap:wrap}
|
||
.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;text-decoration:none}
|
||
.hdr-btn:hover{background:rgba(255,255,255,.24)}
|
||
|
||
.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{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;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
|
||
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
|
||
.hero::before{content:'A/B';position:absolute;right:0;top:-30px;font-size:clamp(2rem,12vw,8rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
|
||
.hero h2{font-family:'Unbounded',sans-serif;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}
|
||
.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(0,0,0,.18)}
|
||
.hero-progress{flex:1;min-width:200px;max-width:280px}
|
||
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
|
||
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
|
||
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
|
||
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
|
||
.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(0,0,0,.18);font-family:'Unbounded',sans-serif}
|
||
|
||
.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(180px,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}
|
||
.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));border-radius:13px 13px 0 0}
|
||
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
|
||
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
|
||
.psel-prog{height:4px;background:rgba(0,0,0,.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,#fef3c7)}
|
||
.psel-card.final .psel-num{color:var(--warn)}
|
||
|
||
.sec[id="sec-p1"]{ --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
|
||
.sec[id="sec-p2"]{ --sec-acc:#f59e0b; --sec-acc-d:#d97706; --sec-acc-soft:#fef9c3; }
|
||
.sec[id="sec-p3"]{ --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
|
||
.sec[id="sec-p4"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
|
||
.sec[id="sec-p5"]{ --sec-acc:#db2777; --sec-acc-d:#9d174d; --sec-acc-soft:#fce7f3; }
|
||
.sec[id="sec-final1"]{ --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
|
||
|
||
.sec{display:none;position:relative;animation:fadeIn .35s ease}
|
||
.sec.active{display:block}
|
||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||
.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,var(--pri-soft));line-height:1;pointer-events:none;user-select:none;z-index:0;opacity:.35}
|
||
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
|
||
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
|
||
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
|
||
|
||
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.04);position:relative;z-index:1;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(0,0,0,.08)}
|
||
.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 .ic{width:18px;height:18px}
|
||
.card-title{font-family:'Unbounded',sans-serif;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(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
|
||
.card-body{font-size:.94rem;line-height:1.65}
|
||
.card-body p{margin-bottom:8px}
|
||
.card-body p:last-child{margin-bottom:0}
|
||
|
||
.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(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
|
||
.btn:active{transform:scale(.96)}
|
||
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
|
||
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
|
||
|
||
.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)}
|
||
|
||
.wg{background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)));border:1.5px solid var(--sec-acc,var(--pri));border-radius:14px;padding:18px 20px;margin-bottom:18px;box-shadow:var(--sh2);position:relative;z-index:1}
|
||
.wg-header{display:flex;align-items:center;gap:8px;margin-bottom:14px}
|
||
.wg-badge{padding:4px 9px;background:var(--sec-acc,var(--pri));color:#fff;border-radius:6px;font-family:'Unbounded',sans-serif;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
|
||
.wg-title{font-family:'Unbounded',sans-serif;font-size:1.05rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));flex:1}
|
||
.wg-help{font-size:.88rem;color:var(--text);margin-bottom:12px;line-height:1.55;background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--sec-acc-soft,var(--pri-soft)));border-left:4px solid var(--warn,#f59e0b);padding:9px 14px;border-radius:9px}
|
||
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);transition:border-color .15s;font-family:'JetBrains Mono',monospace}
|
||
.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
|
||
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
|
||
.sliders{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:10px}
|
||
.sliders label{display:block;font-size:.92rem;color:var(--muted);background:var(--card);padding:8px 12px;border-radius:8px;border:1px solid var(--border);line-height:1.5}
|
||
.sliders label b{font-family:'JetBrains Mono',monospace;font-size:1.05rem;color:var(--sec-acc-d,var(--pri2));margin-left:4px}
|
||
.sliders label input[type="range"]{display:block;width:100%;margin-top:6px;accent-color:var(--sec-acc,var(--pri))}
|
||
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-bottom:12px}
|
||
.score-display b{color:var(--sec-acc-d,var(--pri2));font-size:1.15rem}
|
||
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
|
||
.spoiler summary{padding:8px 14px;background:var(--sec-acc-soft,var(--pri-soft));font-weight:700;cursor:pointer;font-size:.88rem;color:var(--sec-acc-d,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(--sec-acc,var(--pri));width:18px}
|
||
.spoiler[open] summary::before{content:'\2212'}
|
||
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
|
||
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px;transition:border-color .18s,background .18s}
|
||
.dnd-pool.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
|
||
.dnd-pool.col{flex-direction:column;align-items:stretch}
|
||
.dnd-pool.col .dnd-chip{width:auto}
|
||
.dnd-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--card);border:1.5px solid var(--border);border-radius:10px;cursor:grab;user-select:none;font-size:.92rem;line-height:1.4;transition:transform .12s,box-shadow .12s,border-color .12s;touch-action:none;max-width:100%}
|
||
.dnd-chip:hover{transform:translateY(-1px);border-color:var(--sec-acc,var(--pri));box-shadow:var(--sh)}
|
||
.dnd-chip:active{cursor:grabbing}
|
||
.dnd-chip.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px rgba(217,119,6,.22);transform:translateY(-1px)}
|
||
.dnd-chip.dragging{opacity:.28}
|
||
.dnd-chip.placed{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
|
||
.dnd-chip .dnd-x{padding:0 5px;color:var(--muted);font-weight:700;font-size:1.05rem;border-radius:4px;cursor:pointer}
|
||
.dnd-chip .dnd-x:hover{color:var(--bad,var(--fail));background:var(--fail-bg)}
|
||
.drop-box{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px;transition:border-color .15s,background .15s}
|
||
.drop-box:hover{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
|
||
.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em}
|
||
.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
|
||
.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
|
||
.dnd-hint{font-size:.78rem;color:var(--muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
|
||
.dnd-hint svg{width:14px;height:14px;flex-shrink:0}
|
||
|
||
.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-family:'Unbounded',sans-serif;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}
|
||
.sidecard-row:last-child{margin-bottom:0}
|
||
@media(max-width:980px){.col-side{position:static;max-height:none}}
|
||
|
||
.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(0,0,0,.10);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}
|
||
|
||
.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}
|
||
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
|
||
|
||
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
|
||
.ach-popup.show{display:flex}
|
||
|
||
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
|
||
.col-side-backdrop.show{display:block}
|
||
@media(max-width:980px){
|
||
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
|
||
.col-side.open{transform:none}
|
||
}
|
||
|
||
.gloss-term{border-bottom:1.5px dotted var(--sec-acc,var(--pri));cursor:help;color:var(--sec-acc-d,var(--pri2));font-weight:600;padding:0 1px}
|
||
.gloss-term:hover{background:var(--sec-acc-soft,var(--pri-soft));border-radius:3px}
|
||
.gloss-tip{position:fixed;max-width:320px;padding:11px 14px;background:var(--card);border:1.5px solid var(--sec-acc,var(--pri));border-radius:11px;font-size:.84rem;line-height:1.55;box-shadow:0 12px 32px rgba(0,0,0,.18);z-index:9994;display:none;pointer-events:none;color:var(--text)}
|
||
.gloss-tip.show{display:block}
|
||
.gloss-tip b{color:var(--sec-acc-d,var(--pri2));font-size:.92rem}
|
||
|
||
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
|
||
.search-modal.show{display:flex}
|
||
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
|
||
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
|
||
.search-results{flex:1;overflow-y:auto;padding:6px 0}
|
||
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
|
||
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
|
||
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
|
||
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
|
||
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
|
||
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
|
||
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
|
||
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="hdr">
|
||
<div class="hdr-row">
|
||
<div>
|
||
<h1>Алгебра 9 · Глава 1</h1>
|
||
<div class="hdr-sub">Рациональные дроби · ОДЗ · действия с дробями</div>
|
||
</div>
|
||
<div class="hdr-side">
|
||
<a href="/textbook/algebra-9" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К алгебре 9</a>
|
||
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
|
||
<button id="sidebar-btn" class="hdr-btn"><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="theme-btn" class="hdr-btn"><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>рациональные дроби</b> (выражения вида $\dfrac{P(x)}{Q(x)}$), их <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" data-gamified></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="psel">
|
||
<div class="psel-title">Параграфы главы</div>
|
||
<div id="psel-grid" class="psel-grid"></div>
|
||
</section>
|
||
|
||
<section id="sec-p1" class="sec" data-watermark="P/Q"><div class="sec-header"><span class="sec-num">§ 1</span><h2 class="sec-h">Рациональная дробь</h2></div><div id="p1-body"></div></section>
|
||
<section id="sec-p2" class="sec" data-watermark="k"><div class="sec-header"><span class="sec-num">§ 2</span><h2 class="sec-h">Основное свойство дроби</h2></div><div id="p2-body"></div></section>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<section id="sec-final1" class="sec" data-watermark="★"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#d97706,#f59e0b)">Финал главы</span><h2 class="sec-h">Итоги. 5 боссов главы 1</h2></div><div id="final1-body"></div></section>
|
||
|
||
</div>
|
||
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
|
||
<div class="col-side-backdrop" id="col-side-backdrop"></div>
|
||
</main>
|
||
|
||
<footer class="foot">Интерактивный учебник «Алгебра 9» · Глава 1 · Рациональные выражения · LearnSpace</footer>
|
||
|
||
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
|
||
<div id="gloss-tip" class="gloss-tip"></div>
|
||
<div id="search-modal" class="search-modal" role="dialog">
|
||
<div class="search-box">
|
||
<input type="text" id="search-input" class="search-input" placeholder="Поиск…" autocomplete="off">
|
||
<div id="search-results" class="search-results"></div>
|
||
<div class="search-foot"><span><kbd>↑↓</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
const STATE = { current:'p1', progress:{p1:0,p2:0,p3:0,p4:0,p5:0,final1:0}, achievements:new Map(), xp:0, level:1 };
|
||
const TOTAL_PARAS = 6;
|
||
const _TB_SLUG = 'algebra-9-ch1';
|
||
|
||
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
|
||
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
|
||
|
||
const ACH_LABELS = {
|
||
start:'Начало главы 1!',
|
||
p2_done:'Сокращение дробей освоено!',
|
||
p4_done:'Действия с дробями освоены!',
|
||
p5_done:'Преобразование выражений освоено!',
|
||
ch1_done:'Глава 1 пройдена!'
|
||
};
|
||
|
||
function loadProgress(){
|
||
try{
|
||
const s=localStorage.getItem('algebra9_ch1_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
|
||
const a=localStorage.getItem('algebra9_ch1_achievements');
|
||
if(a){ const p=JSON.parse(a); if(Array.isArray(p)) p.forEach(id=>STATE.achievements.set(id, ACH_LABELS[id]||id)); else if(p&&typeof p==='object'){ for(const[id,t] of Object.entries(p)) STATE.achievements.set(id,(t&&t!==id)?t:(ACH_LABELS[id]||id)); } }
|
||
STATE.xp=+(localStorage.getItem('algebra9_xp')||0); STATE.level=calcLevel(STATE.xp);
|
||
}catch(e){}
|
||
}
|
||
function saveProgress(){
|
||
try{
|
||
localStorage.setItem('algebra9_ch1_progress', JSON.stringify(STATE.progress));
|
||
localStorage.setItem('algebra9_ch1_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
|
||
localStorage.setItem('algebra9_xp', String(STATE.xp));
|
||
}catch(e){}
|
||
}
|
||
function bumpProgress(key, delta){
|
||
STATE.progress[key]=Math.max(0,Math.min(100,(STATE.progress[key]||0)+delta));
|
||
saveProgress(); refreshProgressUI();
|
||
if(STATE.progress[key]>=50) markParaRead(key);
|
||
}
|
||
|
||
const _markedRead=new Set();
|
||
let _pendingProgressBody=null, _progressTimer=null;
|
||
function _flushProgress(){
|
||
const body=_pendingProgressBody; _pendingProgressBody=null; if(!body) return;
|
||
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||
fetch('/api/textbooks/'+_TB_SLUG+'/progress',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tok},body:JSON.stringify(body),keepalive:true}).catch(()=>{});
|
||
}
|
||
function _queueProgress(patch){ _pendingProgressBody=Object.assign(_pendingProgressBody||{},patch); if(_progressTimer) clearTimeout(_progressTimer); _progressTimer=setTimeout(_flushProgress, 600); }
|
||
function markLastPara(id){ _queueProgress({last_para:id}); }
|
||
function markParaRead(id){ if(_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({mark_read:id}); }
|
||
window.addEventListener('beforeunload', _flushProgress);
|
||
function loadServerReadState(){
|
||
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
|
||
fetch('/api/textbooks/'+_TB_SLUG,{headers:{'Authorization':'Bearer '+tok}}).then(r=>r.ok?r.json():null).then(d=>{
|
||
if(!d||!d.progress) return;
|
||
(d.progress.read||[]).forEach(k=>{_markedRead.add(k); if((STATE.progress[k]||0)<50) STATE.progress[k]=100;});
|
||
saveProgress(); refreshProgressUI();
|
||
}).catch(()=>{});
|
||
}
|
||
|
||
function addXp(n,src){
|
||
if(!n) return;
|
||
const prev=STATE.level; STATE.xp=Math.max(0,(STATE.xp||0)+n); STATE.level=calcLevel(STATE.xp);
|
||
saveProgress(); refreshProgressUI();
|
||
if(window.LS&&window.LS.xp) window.LS.xp.add(n,'algebra9-ch1-'+(src||'misc'));
|
||
if(STATE.level>prev){
|
||
const pop=document.getElementById('ach-popup');
|
||
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
|
||
}
|
||
}
|
||
|
||
function refreshProgressUI(){
|
||
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
|
||
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
|
||
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
|
||
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
|
||
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 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
|
||
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
|
||
}
|
||
|
||
function achievement(id,text){
|
||
if(STATE.achievements.has(id)) return;
|
||
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
|
||
const pop=document.getElementById('ach-popup');
|
||
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
|
||
addXp(20,'ach-'+id);
|
||
}
|
||
|
||
const PARAS = [
|
||
{ id:'p1', num:'§ 1', name:'Рациональная дробь', sub:'ОДЗ выражения' },
|
||
{ id:'p2', num:'§ 2', name:'Основное свойство дроби', sub:'Сокращение' },
|
||
{ id:'p3', num:'§ 3', name:'Сложение и вычитание', sub:'Общий знаменатель' },
|
||
{ id:'p4', num:'§ 4', name:'Умножение и деление', sub:'×, ÷ дробей' },
|
||
{ id:'p5', num:'§ 5', name:'Преобразование выражений', sub:'Сложные дроби' },
|
||
{ id:'final1', num:'★', name:'Финал главы', sub:'Итоги · 5 боссов', final:true }
|
||
];
|
||
|
||
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;
|
||
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>';
|
||
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(), final1:()=>buildFinal1() };
|
||
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
|
||
function goTo(id){
|
||
STATE.current=id; ensureBuilt(id);
|
||
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
|
||
const el=document.getElementById('sec-'+id); if(el) el.classList.add('active');
|
||
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(()=>renderMath(el), 0);
|
||
markLastPara(id);
|
||
}
|
||
|
||
const SIDEBARS = {
|
||
p1:{title:'Шпаргалка \xA71',rows:[['Дробь','$\\dfrac{P(x)}{Q(x)}$, где $P, Q$ — многочлены'],['ОДЗ','$Q(x) \\ne 0$'],['Целое','частный случай при $Q = 1$']]},
|
||
p2:{title:'Шпаргалка \xA72',rows:[['Свойство','$\\dfrac{P \\cdot R}{Q \\cdot R} = \\dfrac{P}{Q}$ при $R \\ne 0$'],['Сокращение','делим числитель и знаменатель на общий множитель'],['Знак','$\\dfrac{-a}{-b} = \\dfrac{a}{b}$, $\\dfrac{-a}{b} = -\\dfrac{a}{b}$']]},
|
||
p3:{title:'Шпаргалка \xA73',rows:[['Одинак.знам.','$\\dfrac{a}{c} \\pm \\dfrac{b}{c} = \\dfrac{a \\pm b}{c}$'],['Разные знам.','приведи к общему знаменателю'],['НОЗ','наименьший общий знаменатель']]},
|
||
p4:{title:'Шпаргалка \xA74',rows:[['Умножение','$\\dfrac{a}{b} \\cdot \\dfrac{c}{d} = \\dfrac{ac}{bd}$'],['Деление','$\\dfrac{a}{b} : \\dfrac{c}{d} = \\dfrac{a}{b} \\cdot \\dfrac{d}{c}$'],['Степень','$\\left(\\dfrac{a}{b}\\right)^n = \\dfrac{a^n}{b^n}$']]},
|
||
p5:{title:'Шпаргалка \xA75',rows:[['Шаг 1','выпиши ОДЗ'],['Шаг 2','разложи на множители'],['Шаг 3','выполни действия по порядку'],['Шаг 4','сократи результат']]},
|
||
final1:{title:'Финал главы',rows:[['§§1–5','теория главы 1'],['Боссов','5'],['Награда','+100 XP']]}
|
||
};
|
||
|
||
const TIPS=[
|
||
{sec:'p1',html:'<b>ОДЗ</b> — это значения, при которых знаменатель $\\ne 0$. Всегда выписывай ОДЗ перед работой с дробью.'},
|
||
{sec:'p2',html:'Сокращение возможно после <b>разложения на множители</b> числителя и знаменателя.'},
|
||
{sec:'p3',html:'Для сложения дробей с разными знаменателями ищи <b>наименьший общий знаменатель</b>.'},
|
||
{sec:'p4',html:'$\\dfrac{a}{b} \\cdot \\dfrac{c}{d} = \\dfrac{ac}{bd}$, $\\dfrac{a}{b} : \\dfrac{c}{d} = \\dfrac{ad}{bc}$.'},
|
||
{sec:'p5',html:'Сложные выражения упрощай по действиям, не забывай об ОДЗ.'},
|
||
{sec:'final1',html:'5 боссов главы 1. Удачи!'}
|
||
];
|
||
|
||
function buildSidebar(id){
|
||
const box=document.getElementById('sidebar-content');
|
||
const sb=SIDEBARS[id]||SIDEBARS['p1'];
|
||
let html='';
|
||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></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>';
|
||
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
|
||
if(tip){
|
||
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
|
||
}
|
||
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>';
|
||
}
|
||
box.innerHTML=html;
|
||
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
|
||
}
|
||
|
||
function initTheme(){
|
||
const t=localStorage.getItem('algebra9_ch1_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('algebra9_ch1_theme', dark?'dark':'light');
|
||
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
|
||
});
|
||
}
|
||
|
||
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
|
||
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'✓ Верно!':'✗ Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
|
||
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
|
||
function ipow(base, exp){ let r=1; for(let i=0;i<Math.abs(exp);i++) r*=base; return exp<0 ? 1/r : r; }
|
||
function gcd(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ const t=b; b=a%b; a=t; } return a||1; }
|
||
function makeCard(kind, title, num, body){
|
||
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
|
||
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
|
||
}
|
||
function setupSorter(cfg){
|
||
const placed = {}; const pool = document.getElementById(cfg.poolId); const scope = document.querySelector(cfg.scopeSelector);
|
||
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
|
||
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
|
||
let armed = null;
|
||
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
|
||
function attach(elm,itId){ let ghost=null,dragging=false,sx=0,sy=0; elm.addEventListener('pointerdown',ev=>{ if(ev.button!==undefined&&ev.button!==0) return;
|
||
ev.preventDefault(); if(ev.target.classList&&ev.target.classList.contains('dnd-x')){ ev.stopPropagation(); if(placed[itId]){delete placed[itId];render();}else if(armed===itId){armed=null;render();} return; } sx=ev.clientX;sy=ev.clientY; const r=elm.getBoundingClientRect(); const ox=ev.clientX-r.left,oy=ev.clientY-r.top; try{elm.setPointerCapture(ev.pointerId);}catch(e){} function onMove(e){ const dx=e.clientX-sx,dy=e.clientY-sy; if(!dragging&&Math.hypot(dx,dy)>8){ dragging=true; ghost=elm.cloneNode(true); ghost.classList.remove('armed'); ghost.style.cssText='position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:'+r.width+'px;left:'+(e.clientX-ox)+'px;top:'+(e.clientY-oy)+'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if(dragging&&ghost){ ghost.style.left=(e.clientX-ox)+'px';ghost.style.top=(e.clientY-oy)+'px'; const under=document.elementsFromPoint(e.clientX,e.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); const tgt=under.find(n=>n.classList&&(n.classList.contains('drop-box')||n.classList.contains('dnd-pool'))); if(tgt)tgt.classList.add('over'); } } function onUp(e){ elm.removeEventListener('pointermove',onMove);elm.removeEventListener('pointerup',onUp);elm.removeEventListener('pointercancel',onUp);elm.classList.remove('dragging'); if(ghost){ghost.remove();ghost=null;} scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); if(dragging){ const under=document.elementsFromPoint(e.clientX,e.clientY); const box=under.find(n=>n.classList&&n.classList.contains('drop-box')); const pl=under.find(n=>n.classList&&n.classList.contains('dnd-pool')); if(box){const di=box.querySelector('[data-cat]');if(di){placed[itId]=di.dataset.cat;armed=null;render();return;}}else if(pl){delete placed[itId];armed=null;render();return;} }else{ if(placed[itId]){delete placed[itId];armed=null;render();}else{armed=(armed===itId)?null:itId;render();} } dragging=false; } elm.addEventListener('pointermove',onMove);elm.addEventListener('pointerup',onUp);elm.addEventListener('pointercancel',onUp); }); }
|
||
function attachBoxTaps(){ scope.querySelectorAll('.drop-box').forEach(box=>{ box.addEventListener('click',ev=>{ if(!armed)return; if(ev.target.closest('.dnd-chip'))return; const di=box.querySelector('[data-cat]'); if(di){placed[armed]=di.dataset.cat;armed=null;render();} }); }); }
|
||
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); cfg.cats.forEach(cat=>{const box=scope.querySelector('.drop-items[data-cat="'+cat+'"]');if(!box)return;box.innerHTML='';cfg.items.forEach(it=>{if(placed[it.id]!==cat)return;box.appendChild(buildChip(it,true));});}); if(window.renderMathInElement)try{renderMath(scope);}catch(_){} }
|
||
attachBoxTaps(); render();
|
||
return {placed,render,reset(){ for(const k in placed)delete placed[k];armed=null;render(); }};
|
||
}
|
||
|
||
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>',
|
||
};
|
||
|
||
function secNav(prev, next){
|
||
const NAMES={p1:'\xA71',p2:'\xA72',p3:'\xA73',p4:'\xA74',p5:'\xA75',final1:'Финал'};
|
||
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> '+NAMES[prev]+'</button>':'<span></span>';
|
||
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+NAMES[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;
|
||
}
|
||
|
||
function readButton(paraId){
|
||
return '<div style="margin-top:18px;display:flex;justify-content:center">'
|
||
+'<button class="btn primary" id="'+paraId+'-read-btn">'
|
||
+'<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>'
|
||
+' Я прочитал — '+(paraId.startsWith('final')?'финал':'\xA7'+paraId.replace('p',''))+' (+10 XP)'
|
||
+'</button></div>';
|
||
}
|
||
function wireReadBtn(paraId){
|
||
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
|
||
btn.addEventListener('click', ()=>{
|
||
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
|
||
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
|
||
if(paraId==='p2') achievement('p2_done');
|
||
if(paraId==='p4') achievement('p4_done');
|
||
if(paraId==='p5') achievement('p5_done');
|
||
if(paraId==='final1') achievement('ch1_done');
|
||
});
|
||
}
|
||
|
||
/* ===== STUB BUILDERS — наполнение в Phase 1+ ===== */
|
||
|
||
function buildP1(){
|
||
const box = document.getElementById('p1-body');
|
||
let html = '';
|
||
|
||
html += makeCard('theory', 'Определение рациональной дроби', '1.1', `
|
||
<p><b>Рациональной дробью</b> называется выражение вида $\\dfrac{P(x)}{Q(x)}$, где $P(x)$ и $Q(x)$ — многочлены, причём $Q(x) \\ne 0$.</p>
|
||
<p>Числитель $P(x)$ может быть любым многочленом (даже нулевым), а знаменатель $Q(x)$ — <b>не равен нулю</b>.</p>
|
||
<p>Примеры рациональных дробей:</p>
|
||
<ul style="padding-left:22px;line-height:1.9">
|
||
<li>$\\dfrac{x+1}{x-2}$ — простейшая дробь с линейными многочленами</li>
|
||
<li>$\\dfrac{3}{x^2+1}$ — числитель константа, знаменатель квадратный</li>
|
||
<li>$\\dfrac{2a^2-1}{a^2-9}$ — обе части квадратные многочлены</li>
|
||
</ul>
|
||
<details class="spoiler"><summary>А целые выражения — это дроби?</summary><div class="spoiler-body">
|
||
Да! Любой многочлен $P(x)$ — это частный случай рациональной дроби: $P(x) = \\dfrac{P(x)}{1}$. Поэтому рациональные дроби обобщают и целые выражения.
|
||
</div></details>`);
|
||
|
||
html += makeCard('rule', 'Область допустимых значений (ОДЗ)', '1.2', `
|
||
<p><b>ОДЗ</b> рациональной дроби $\\dfrac{P(x)}{Q(x)}$ — это все значения переменной, при которых <b>знаменатель не обращается в ноль</b>.</p>
|
||
<p>Алгоритм нахождения ОДЗ:</p>
|
||
<ol style="padding-left:22px;line-height:2">
|
||
<li>Записать уравнение $Q(x) = 0$.</li>
|
||
<li>Найти все его корни.</li>
|
||
<li>Исключить эти корни из множества действительных чисел.</li>
|
||
</ol>
|
||
<p>Запись: «$x \\ne$ ...» или «$x \\in \\mathbb{R}, x \\ne $...».</p>
|
||
<p>Если уравнение $Q(x) = 0$ <b>не имеет корней</b> (например, $x^2 + 1 = 0$), то ОДЗ — <b>все действительные числа</b>.</p>`);
|
||
|
||
html += makeCard('example', 'Примеры нахождения ОДЗ', '1.3', `
|
||
<p><b>а)</b> $\\dfrac{1}{x-3}$. Знаменатель: $x - 3 = 0 \\Rightarrow x = 3$. <b>ОДЗ: $x \\ne 3$.</b></p>
|
||
<p><b>б)</b> $\\dfrac{x+5}{x^2-4}$. Знаменатель: $x^2 - 4 = 0 \\Rightarrow (x-2)(x+2) = 0 \\Rightarrow x = \\pm 2$. <b>ОДЗ: $x \\ne 2, x \\ne -2$.</b></p>
|
||
<p><b>в)</b> $\\dfrac{2x}{(x-1)(x+7)}$. Знаменатель: $(x-1)(x+7) = 0 \\Rightarrow x = 1$ или $x = -7$. <b>ОДЗ: $x \\ne 1, x \\ne -7$.</b></p>`);
|
||
|
||
/* INTERACTIVE 1 — slider + SVG number line */
|
||
html += `<div class="wg" id="p1-iv1">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">ОДЗ на числовой прямой</div></div>
|
||
<div class="wg-help">Выбери дробь ползунком — увидишь её ОДЗ на числовой прямой и в текстовой форме.</div>
|
||
<div class="sliders">
|
||
<label>Дробь №<b id="p1-iv1-idx">1</b> / 5<input type="range" id="p1-iv1-sl" min="1" max="5" step="1" value="1"></label>
|
||
</div>
|
||
<div id="p1-iv1-formula" style="text-align:center;font-size:1.15rem;padding:10px;background:var(--card);border-radius:9px;margin-bottom:10px"></div>
|
||
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
|
||
<svg id="p1-iv1-svg" viewBox="0 0 600 100" style="width:100%;min-width:520px;height:auto;display:block"></svg>
|
||
</div>
|
||
<div id="p1-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:.95rem;text-align:center"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 2 — calc ОДЗ */
|
||
html += `<div class="wg" id="p1-iv2">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор ОДЗ дроби $\\dfrac{1}{ax+b}$</div></div>
|
||
<div class="wg-help">Введи целые коэффициенты $a$ и $b$, нажми «Найти ОДЗ» — посчитаем точку, исключённую из ОДЗ.</div>
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center;margin-bottom:10px">
|
||
<span style="font-family:'JetBrains Mono',monospace">$a$ =</span>
|
||
<input type="number" id="p1-iv2-a" class="tinp" style="width:80px;text-align:center" value="2">
|
||
<span style="font-family:'JetBrains Mono',monospace">$b$ =</span>
|
||
<input type="number" id="p1-iv2-b" class="tinp" style="width:80px;text-align:center" value="-6">
|
||
<button class="btn primary" id="p1-iv2-go">Найти ОДЗ</button>
|
||
</div>
|
||
<div id="p1-iv2-out" style="padding:12px 14px;background:var(--card);border-radius:9px;text-align:center;font-size:1rem;min-height:48px"></div>
|
||
<div class="feedback" id="p1-iv2-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 3 — quickfire quiz */
|
||
html += `<div class="wg" id="p1-iv3">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Входит ли в ОДЗ?</div></div>
|
||
<div class="wg-help">Дробь и значение $x_0$. Решай: входит ли $x_0$ в ОДЗ (т.е. не обращает ли знаменатель в 0)?</div>
|
||
<div class="score-display"><span>Задача <b id="p1-iv3-i">1</b> / 8</span><span>Очки: <b id="p1-iv3-s">0</b> / 8</span></div>
|
||
<div id="p1-iv3-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.15rem;text-align:center;margin-bottom:10px"></div>
|
||
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap">
|
||
<button class="btn primary" id="p1-iv3-yes" style="background:#10b981;border-color:#10b981">Да, входит</button>
|
||
<button class="btn primary" id="p1-iv3-no" style="background:#dc2626;border-color:#dc2626">Нет, не входит</button>
|
||
</div>
|
||
<div class="feedback" id="p1-iv3-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 4 — trainer */
|
||
html += `<div class="wg" id="p1-iv4">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр: сумма корней знаменателя</div></div>
|
||
<div class="wg-help">Для каждой дроби введи <b>сумму всех значений</b>, при которых знаменатель $= 0$ (т.е. сумму точек, исключённых из ОДЗ).</div>
|
||
<div class="score-display"><span>Задача <b id="p1-iv4-i">1</b> / 6</span><span>Очки: <b id="p1-iv4-s">0</b> / 6</span></div>
|
||
<div id="p1-iv4-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">
|
||
<span style="font-family:'JetBrains Mono',monospace;font-size:1rem">сумма корней =</span>
|
||
<input type="number" id="p1-iv4-ans" class="tinp" style="width:100px;text-align:center" step="1">
|
||
<button class="btn primary" id="p1-iv4-go">Проверить</button>
|
||
<button class="btn" id="p1-iv4-start">Заново</button>
|
||
</div>
|
||
<div class="feedback" id="p1-iv4-fb"></div>
|
||
</div>`;
|
||
|
||
html += secNav(null, 'p2');
|
||
html += readButton('p1');
|
||
|
||
box.innerHTML = html;
|
||
renderMath(box);
|
||
|
||
/* IV1 — slider + SVG */
|
||
(function(){
|
||
const FRACS = [
|
||
{ fr:'\\dfrac{1}{x-2}', roots:[2], text:'$x \\ne 2$' },
|
||
{ fr:'\\dfrac{x}{x^2-9}', roots:[-3,3], text:'$x \\ne -3,\\ x \\ne 3$' },
|
||
{ fr:'\\dfrac{3}{(x+1)(x-4)}', roots:[-1,4], text:'$x \\ne -1,\\ x \\ne 4$' },
|
||
{ fr:'\\dfrac{2x+1}{x^2-25}', roots:[-5,5], text:'$x \\ne -5,\\ x \\ne 5$' },
|
||
{ fr:'\\dfrac{1}{x^2+1}', roots:[], text:'$x \\in \\mathbb{R}$ — все действительные' },
|
||
];
|
||
const sl = document.getElementById('p1-iv1-sl');
|
||
const idx = document.getElementById('p1-iv1-idx');
|
||
const fEl = document.getElementById('p1-iv1-formula');
|
||
const svg = document.getElementById('p1-iv1-svg');
|
||
const out = document.getElementById('p1-iv1-out');
|
||
const seen = new Set();
|
||
function draw(){
|
||
const k = +sl.value; idx.textContent = k;
|
||
const cur = FRACS[k-1];
|
||
fEl.innerHTML = '$' + cur.fr + '$';
|
||
let s = '';
|
||
// grid
|
||
s += '<rect x="0" y="0" width="600" height="100" fill="none"/>';
|
||
for(let v=-10; v<=10; v++){
|
||
const x = 30 + (v+10) * 27;
|
||
s += '<line x1="'+x+'" y1="42" x2="'+x+'" y2="58" stroke="#cbd5e1" stroke-width="1"/>';
|
||
if(v%2===0){
|
||
s += '<text x="'+x+'" y="76" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#64748b">'+v+'</text>';
|
||
}
|
||
}
|
||
// axis
|
||
s += '<line x1="20" y1="50" x2="580" y2="50" stroke="#0f172a" stroke-width="2"/>';
|
||
s += '<polygon points="580,50 570,45 570,55" fill="#0f172a"/>';
|
||
s += '<text x="585" y="46" font-family="JetBrains Mono,monospace" font-size="13" fill="#0f172a">x</text>';
|
||
// excluded points
|
||
cur.roots.forEach(r=>{
|
||
const x = 30 + (r+10) * 27;
|
||
s += '<circle cx="'+x+'" cy="50" r="6" fill="#fff" stroke="#dc2626" stroke-width="2.5"/>';
|
||
s += '<text x="'+x+'" y="32" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" fill="#dc2626" font-weight="700">'+r+'</text>';
|
||
});
|
||
svg.innerHTML = s;
|
||
out.innerHTML = 'ОДЗ: ' + cur.text;
|
||
renderMath(fEl); renderMath(out);
|
||
seen.add(k);
|
||
if(seen.size === FRACS.length){ addXp(10,'p1-iv1'); bumpProgress('p1', 15); seen.clear(); seen.add('done'); }
|
||
}
|
||
sl.addEventListener('input', draw);
|
||
draw();
|
||
})();
|
||
|
||
/* IV2 — ОДЗ калькулятор */
|
||
(function(){
|
||
const go = document.getElementById('p1-iv2-go');
|
||
const aI = document.getElementById('p1-iv2-a');
|
||
const bI = document.getElementById('p1-iv2-b');
|
||
const out = document.getElementById('p1-iv2-out');
|
||
const fb = document.getElementById('p1-iv2-fb');
|
||
let solved = 0;
|
||
function showFormula(){ out.innerHTML = 'Дробь: $\\dfrac{1}{('+(aI.value||'a')+')x + ('+(bI.value||'b')+')}$'; renderMath(out); }
|
||
function calc(){
|
||
const a = parseInt(aI.value, 10), b = parseInt(bI.value, 10);
|
||
if(isNaN(a) || isNaN(b)){ feedback(fb, false, '✗ Введи целые числа $a$ и $b$.'); return; }
|
||
if(a === 0){
|
||
out.innerHTML = '<b>$a = 0$</b> — выражение не является рациональной дробью с переменной (знаменатель — константа $'+b+'$). Если ещё и $b = 0$, дробь не определена.';
|
||
feedback(fb, true, '✓ При $a = 0$ переменная в знаменателе исчезает.');
|
||
return;
|
||
}
|
||
// root: ax + b = 0 → x = -b/a
|
||
const num = -b, den = a;
|
||
const d = gcd(Math.abs(num), Math.abs(den));
|
||
let nn = num/d, dd = den/d;
|
||
if(dd < 0){ nn = -nn; dd = -dd; }
|
||
let xstr;
|
||
if(dd === 1) xstr = String(nn);
|
||
else xstr = '\\dfrac{'+nn+'}{'+dd+'}';
|
||
out.innerHTML = 'Знаменатель: $'+a+'x + ('+b+') = 0 \\Rightarrow x = '+xstr+'$.<br><b>ОДЗ: $x \\ne '+xstr+'$.</b>';
|
||
renderMath(out);
|
||
feedback(fb, true, '✓ ОДЗ найдена! +10 XP');
|
||
solved++;
|
||
if(solved === 1){ addXp(10,'p1-iv2'); bumpProgress('p1', 15); }
|
||
}
|
||
aI.addEventListener('input', showFormula); bI.addEventListener('input', showFormula);
|
||
go.addEventListener('click', calc);
|
||
showFormula();
|
||
})();
|
||
|
||
/* IV3 — quickfire */
|
||
(function(){
|
||
const Q = [
|
||
{ expr:'$\\dfrac{1}{x-3}, \\ x_0 = 5$', yes:true, why:'$5 - 3 = 2 \\ne 0$' },
|
||
{ expr:'$\\dfrac{1}{x-3}, \\ x_0 = 3$', yes:false, why:'$3 - 3 = 0$ — знаменатель обнуляется' },
|
||
{ expr:'$\\dfrac{x}{x^2-4}, \\ x_0 = 2$', yes:false, why:'$2^2 - 4 = 0$' },
|
||
{ expr:'$\\dfrac{x}{x^2-4}, \\ x_0 = 0$', yes:true, why:'$0^2 - 4 = -4 \\ne 0$' },
|
||
{ expr:'$\\dfrac{1}{(x+1)(x-5)}, \\ x_0 = -1$', yes:false, why:'$(-1+1)(-1-5) = 0$' },
|
||
{ expr:'$\\dfrac{2}{x^2+1}, \\ x_0 = -7$', yes:true, why:'$(-7)^2 + 1 = 50 \\ne 0$ (знаменатель всегда $>0$)' },
|
||
{ expr:'$\\dfrac{x+1}{x-x}, \\ x_0 = 2$', yes:false, why:'$x - x = 0$ всегда! Дробь не определена ни при каком $x$' },
|
||
{ expr:'$\\dfrac{3}{x^2-9}, \\ x_0 = 3$', yes:false, why:'$3^2 - 9 = 0$' },
|
||
];
|
||
let i = 0, score = 0;
|
||
function show(){
|
||
if(i >= Q.length){
|
||
document.getElementById('p1-iv3-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
|
||
if(score === Q.length){ addXp(15,'p1-iv3'); bumpProgress('p1', 25); }
|
||
else if(score >= Q.length - 2){ addXp(8,'p1-iv3'); bumpProgress('p1', 15); }
|
||
return;
|
||
}
|
||
document.getElementById('p1-iv3-i').textContent = (i+1);
|
||
document.getElementById('p1-iv3-s').textContent = score;
|
||
document.getElementById('p1-iv3-q').innerHTML = Q[i].expr;
|
||
renderMath(document.getElementById('p1-iv3-q'));
|
||
document.getElementById('p1-iv3-fb').style.display = 'none';
|
||
}
|
||
function answer(isYes){
|
||
if(i >= Q.length) return;
|
||
const fb = document.getElementById('p1-iv3-fb');
|
||
if(isYes === Q[i].yes){ score++; feedback(fb, true, '✓ Верно! '+Q[i].why+'. Дальше ▶'); }
|
||
else feedback(fb, false, '✗ Неверно. '+Q[i].why+'. Дальше ▶');
|
||
document.getElementById('p1-iv3-s').textContent = score;
|
||
i++;
|
||
setTimeout(show, 1000);
|
||
}
|
||
document.getElementById('p1-iv3-yes').addEventListener('click', ()=>answer(true));
|
||
document.getElementById('p1-iv3-no').addEventListener('click', ()=>answer(false));
|
||
show();
|
||
})();
|
||
|
||
/* IV4 — trainer */
|
||
(function(){
|
||
const Q = [
|
||
{ q:'$\\dfrac{1}{x-7}$', sum:7, hint:'корень $x = 7$' },
|
||
{ q:'$\\dfrac{x}{x^2-16}$', sum:0, hint:'корни $x = \\pm 4$, сумма $= 0$' },
|
||
{ q:'$\\dfrac{1}{(x-1)(x+9)}$', sum:-8, hint:'корни $x = 1, -9$, сумма $= -8$' },
|
||
{ q:'$\\dfrac{x^2}{x^2-25}$', sum:0, hint:'корни $x = \\pm 5$, сумма $= 0$' },
|
||
{ q:'$\\dfrac{2}{(x+3)^2}$', sum:-3, hint:'один корень $x = -3$' },
|
||
{ q:'$\\dfrac{1}{x^2-2x-15}$', sum:2, hint:'корни $x = 5, -3$, сумма $= 2$' },
|
||
];
|
||
let i = 0, score = 0;
|
||
function show(){
|
||
if(i >= Q.length){
|
||
document.getElementById('p1-iv4-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
|
||
if(score === Q.length){ addXp(15,'p1-iv4'); bumpProgress('p1', 25); }
|
||
else if(score >= 4){ addXp(8,'p1-iv4'); bumpProgress('p1', 15); }
|
||
return;
|
||
}
|
||
document.getElementById('p1-iv4-i').textContent = (i+1);
|
||
document.getElementById('p1-iv4-s').textContent = score;
|
||
document.getElementById('p1-iv4-q').innerHTML = 'Дробь: ' + Q[i].q;
|
||
document.getElementById('p1-iv4-ans').value = '';
|
||
renderMath(document.getElementById('p1-iv4-q'));
|
||
document.getElementById('p1-iv4-fb').style.display = 'none';
|
||
}
|
||
function go(){
|
||
if(i >= Q.length) return;
|
||
const fb = document.getElementById('p1-iv4-fb');
|
||
const ans = parseInt(document.getElementById('p1-iv4-ans').value, 10);
|
||
if(isNaN(ans)){ feedback(fb, false, '✗ Введи целое число.'); return; }
|
||
if(ans === Q[i].sum){ score++; feedback(fb, true, '✓ Верно! '+Q[i].hint+'. Дальше ▶'); }
|
||
else feedback(fb, false, '✗ Неверно. Сумма $= '+Q[i].sum+'$ ('+Q[i].hint+'). Дальше ▶');
|
||
document.getElementById('p1-iv4-s').textContent = score;
|
||
i++;
|
||
setTimeout(show, 1200);
|
||
}
|
||
document.getElementById('p1-iv4-go').addEventListener('click', go);
|
||
document.getElementById('p1-iv4-ans').addEventListener('keydown', e=>{ if(e.key === 'Enter') go(); });
|
||
document.getElementById('p1-iv4-start').addEventListener('click', ()=>{ i=0; score=0; show(); });
|
||
show();
|
||
})();
|
||
|
||
wireReadBtn('p1');
|
||
}
|
||
|
||
function buildP2(){
|
||
const box = document.getElementById('p2-body');
|
||
let html = '';
|
||
|
||
html += makeCard('theory', 'Основное свойство дроби', '2.1', `
|
||
<p><b>Основное свойство рациональной дроби:</b> если числитель и знаменатель дроби умножить или разделить на одно и то же отличное от нуля выражение, то значение дроби не изменится:</p>
|
||
\\[\\dfrac{A}{B} = \\dfrac{A \\cdot C}{B \\cdot C}, \\qquad \\dfrac{A \\cdot C}{B \\cdot C} = \\dfrac{A}{B}, \\quad B \\ne 0,\\ C \\ne 0.\\]
|
||
<p>Слева направо — это <b>умножение</b> на $C$ (приведение к общему знаменателю), справа налево — это <b>сокращение</b>.</p>
|
||
<details class="spoiler"><summary>Правило знаков</summary><div class="spoiler-body">
|
||
$\\dfrac{-a}{-b} = \\dfrac{a}{b}$ (минусы сокращаются), $\\dfrac{-a}{b} = -\\dfrac{a}{b} = \\dfrac{a}{-b}$ — минус можно «перекидывать».
|
||
</div></details>`);
|
||
|
||
html += makeCard('rule', 'Как сокращать дробь', '2.2', `
|
||
<p><b>Алгоритм сокращения:</b></p>
|
||
<ol style="padding-left:22px;line-height:2">
|
||
<li><b>Разложить</b> числитель и знаменатель на множители (вынести общий множитель, применить формулы сокращённого умножения).</li>
|
||
<li><b>Найти</b> общий множитель числителя и знаменателя.</li>
|
||
<li><b>Разделить</b> на него (вычеркнуть).</li>
|
||
</ol>
|
||
<p>Пример: $\\dfrac{12x^2}{18x} = \\dfrac{6x \\cdot 2x}{6x \\cdot 3} = \\dfrac{2x}{3}$ — общий множитель $6x$.</p>
|
||
<p style="background:var(--warn-bg);padding:8px 12px;border-radius:8px;border-left:3px solid var(--warn)"><b>Внимание:</b> сокращать можно только <b>множители</b>, а не слагаемые! $\\dfrac{a+b}{a} \\ne 1 + b$ — это ошибка.</p>`);
|
||
|
||
html += makeCard('example', 'Примеры сокращения', '2.3', `
|
||
<p><b>а)</b> $\\dfrac{a^2-b^2}{a+b} = \\dfrac{(a-b)(a+b)}{a+b} = a - b$ — применили формулу разности квадратов.</p>
|
||
<p><b>б)</b> $\\dfrac{x^2-9}{x^2+3x} = \\dfrac{(x-3)(x+3)}{x(x+3)} = \\dfrac{x-3}{x}$ — общий множитель $(x+3)$.</p>
|
||
<p><b>в)</b> $\\dfrac{6a^2b}{15ab^2} = \\dfrac{3ab \\cdot 2a}{3ab \\cdot 5b} = \\dfrac{2a}{5b}$ — общий множитель $3ab$.</p>`);
|
||
|
||
/* INTERACTIVE 1 — slider visualizer */
|
||
html += `<div class="wg" id="p2-iv1">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Сократить с подсветкой</div></div>
|
||
<div class="wg-help">Выбери задачу — увидишь дробь с подсвеченным общим множителем. Нажми «Показать сокращение», чтобы увидеть результат.</div>
|
||
<div class="sliders">
|
||
<label>Задача №<b id="p2-iv1-idx">1</b> / 5<input type="range" id="p2-iv1-sl" min="1" max="5" step="1" value="1"></label>
|
||
</div>
|
||
<div id="p2-iv1-before" style="text-align:center;font-size:1.25rem;padding:14px;background:var(--card);border-radius:9px;margin-bottom:10px;min-height:60px"></div>
|
||
<div style="display:flex;gap:10px;justify-content:center;margin-bottom:10px">
|
||
<button class="btn primary" id="p2-iv1-go">Показать сокращение</button>
|
||
<button class="btn" id="p2-iv1-hide">Скрыть</button>
|
||
</div>
|
||
<div id="p2-iv1-after" style="text-align:center;font-size:1.25rem;padding:14px;background:var(--sec-acc-soft);border-radius:9px;min-height:50px;display:none"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 2 — number GCD calc */
|
||
html += `<div class="wg" id="p2-iv2">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор сокращения числовой дроби</div></div>
|
||
<div class="wg-help">Введи целые числитель и знаменатель — посчитаем НОД и покажем сокращённую дробь.</div>
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center;margin-bottom:10px">
|
||
<span style="font-family:'JetBrains Mono',monospace">числитель =</span>
|
||
<input type="number" id="p2-iv2-num" class="tinp" style="width:90px;text-align:center" value="24">
|
||
<span style="font-family:'JetBrains Mono',monospace">знаменатель =</span>
|
||
<input type="number" id="p2-iv2-den" class="tinp" style="width:90px;text-align:center" value="36">
|
||
<button class="btn primary" id="p2-iv2-go">Сократить</button>
|
||
</div>
|
||
<div id="p2-iv2-out" style="padding:12px 14px;background:var(--card);border-radius:9px;text-align:center;font-size:1rem;min-height:50px"></div>
|
||
<div class="feedback" id="p2-iv2-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 3 — DnD sorter "Можно сократить / Уже не сокращается" */
|
||
html += `<div class="wg" id="p2-iv3">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Сократимо или нет?</div></div>
|
||
<div class="wg-help">Перетащи каждую дробь в нужный ящик. Шесть дробей — два варианта.</div>
|
||
<div class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11V6a3 3 0 0 1 6 0v5"/><path d="M9 11h6v8a4 4 0 0 1-8 0z"/></svg> 6 дробей — 2 ящика</div>
|
||
<div id="p2-iv3-pool"></div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin-top:8px">
|
||
<div class="drop-box"><h5 data-cat="yes">Можно сократить</h5><div class="drop-items" data-cat="yes"></div></div>
|
||
<div class="drop-box"><h5 data-cat="no">Уже не сокращается</h5><div class="drop-items" data-cat="no"></div></div>
|
||
</div>
|
||
<div class="actions"><button class="btn primary" id="p2-iv3-check">Проверить</button><button class="btn" id="p2-iv3-reset">Сначала</button></div>
|
||
<div class="feedback" id="p2-iv3-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 4 — trainer */
|
||
html += `<div class="wg" id="p2-iv4">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр сокращения</div></div>
|
||
<div class="wg-help">Сократи дробь и введи <b>числитель</b> результата (число или числовой коэффициент).</div>
|
||
<div class="score-display"><span>Задача <b id="p2-iv4-i">1</b> / 6</span><span>Очки: <b id="p2-iv4-s">0</b> / 6</span></div>
|
||
<div id="p2-iv4-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">
|
||
<span style="font-family:'JetBrains Mono',monospace">числитель =</span>
|
||
<input type="number" id="p2-iv4-ans" class="tinp" style="width:90px;text-align:center">
|
||
<button class="btn primary" id="p2-iv4-go">Проверить</button>
|
||
<button class="btn" id="p2-iv4-start">Заново</button>
|
||
</div>
|
||
<div class="feedback" id="p2-iv4-fb"></div>
|
||
</div>`;
|
||
|
||
html += secNav('p1', 'p3');
|
||
html += readButton('p2');
|
||
|
||
box.innerHTML = html;
|
||
renderMath(box);
|
||
|
||
/* IV1 */
|
||
(function(){
|
||
const TASKS = [
|
||
{ before:'\\dfrac{6x}{9}', after:'\\dfrac{2x}{3}', common:'общий множитель $3$' },
|
||
{ before:'\\dfrac{a^2-1}{a+1}', after:'a - 1', common:'разность квадратов: $a^2 - 1 = (a-1)(a+1)$' },
|
||
{ before:'\\dfrac{x^2-4}{x-2}', after:'x + 2', common:'разность квадратов: $x^2 - 4 = (x-2)(x+2)$' },
|
||
{ before:'\\dfrac{4ab^2}{6a^2 b}', after:'\\dfrac{2b}{3a}', common:'общий множитель $2ab$' },
|
||
{ before:'\\dfrac{x^2+5x}{x^2-25}', after:'\\dfrac{x}{x-5}', common:'$x^2+5x = x(x+5)$, $x^2-25 = (x-5)(x+5)$, сокращаем $(x+5)$' },
|
||
];
|
||
const sl = document.getElementById('p2-iv1-sl');
|
||
const idx = document.getElementById('p2-iv1-idx');
|
||
const bEl = document.getElementById('p2-iv1-before');
|
||
const aEl = document.getElementById('p2-iv1-after');
|
||
const go = document.getElementById('p2-iv1-go');
|
||
const hide = document.getElementById('p2-iv1-hide');
|
||
const seen = new Set();
|
||
function show(){
|
||
const k = +sl.value; idx.textContent = k;
|
||
const t = TASKS[k-1];
|
||
bEl.innerHTML = '$' + t.before + '$';
|
||
aEl.innerHTML = '<div style="font-size:.85rem;color:var(--muted);margin-bottom:6px">' + t.common + '</div>$' + t.before + ' \\;=\\; ' + t.after + '$';
|
||
aEl.style.display = 'none';
|
||
renderMath(bEl);
|
||
seen.add(k);
|
||
if(seen.size === TASKS.length && !seen.has('done')){ addXp(10,'p2-iv1'); bumpProgress('p2', 15); seen.add('done'); }
|
||
}
|
||
sl.addEventListener('input', show);
|
||
go.addEventListener('click', ()=>{ aEl.style.display = 'block'; renderMath(aEl); });
|
||
hide.addEventListener('click', ()=>{ aEl.style.display = 'none'; });
|
||
show();
|
||
})();
|
||
|
||
/* IV2 */
|
||
(function(){
|
||
const go = document.getElementById('p2-iv2-go');
|
||
const nI = document.getElementById('p2-iv2-num');
|
||
const dI = document.getElementById('p2-iv2-den');
|
||
const out = document.getElementById('p2-iv2-out');
|
||
const fb = document.getElementById('p2-iv2-fb');
|
||
let solved = 0;
|
||
function calc(){
|
||
const n = parseInt(nI.value, 10), d = parseInt(dI.value, 10);
|
||
if(isNaN(n) || isNaN(d)){ feedback(fb, false, '✗ Введи целые числа.'); return; }
|
||
if(d === 0){ feedback(fb, false, '✗ Знаменатель не может быть 0.'); out.innerHTML = ''; return; }
|
||
const g = gcd(n, d);
|
||
const n2 = n/g, d2 = d/g;
|
||
if(g === 1){
|
||
out.innerHTML = '$\\dfrac{'+n+'}{'+d+'}$ — НОД $= 1$, дробь уже несократима.';
|
||
} else {
|
||
out.innerHTML = '$\\dfrac{'+n+'}{'+d+'} = \\dfrac{'+n+':'+g+'}{'+d+':'+g+'} = \\dfrac{'+n2+'}{'+d2+'}$, где НОД $= '+g+'$.';
|
||
}
|
||
renderMath(out);
|
||
feedback(fb, true, '✓ Готово! +10 XP');
|
||
solved++;
|
||
if(solved === 1){ addXp(10,'p2-iv2'); bumpProgress('p2', 15); }
|
||
}
|
||
go.addEventListener('click', calc);
|
||
calc();
|
||
})();
|
||
|
||
/* IV3 — DnD sorter */
|
||
(function(){
|
||
const items = [
|
||
{ id:'i1', cat:'yes', html:'$\\dfrac{12}{18}$' },
|
||
{ id:'i2', cat:'no', html:'$\\dfrac{5}{7}$' },
|
||
{ id:'i3', cat:'yes', html:'$\\dfrac{a^2-b^2}{a-b}$' },
|
||
{ id:'i4', cat:'no', html:'$\\dfrac{x+1}{x-1}$' },
|
||
{ id:'i5', cat:'yes', html:'$\\dfrac{4xy}{6xz}$' },
|
||
{ id:'i6', cat:'no', html:'$\\dfrac{1}{x^2+1}$' },
|
||
];
|
||
const sorter = setupSorter({
|
||
poolId:'p2-iv3-pool',
|
||
scopeSelector:'#p2-iv3',
|
||
items: items,
|
||
cats:['yes','no'],
|
||
columnLayout:true,
|
||
});
|
||
document.getElementById('p2-iv3-check').addEventListener('click', ()=>{
|
||
const fb = document.getElementById('p2-iv3-fb');
|
||
const placedCount = items.filter(it => sorter.placed[it.id]).length;
|
||
const correct = items.filter(it => sorter.placed[it.id] === it.cat).length;
|
||
if(placedCount < items.length){ feedback(fb, false, '✗ Размести все 6 дробей.'); return; }
|
||
if(correct === items.length){ feedback(fb, true, '✓ Все 6 на месте! +15 XP'); addXp(15,'p2-iv3'); bumpProgress('p2', 25); }
|
||
else feedback(fb, false, '✗ Правильно ' + correct + ' из 6. Попробуй ещё.');
|
||
});
|
||
document.getElementById('p2-iv3-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p2-iv3-fb').style.display = 'none'; });
|
||
})();
|
||
|
||
/* IV4 — trainer */
|
||
(function(){
|
||
const Q = [
|
||
{ q:'$\\dfrac{8a}{12}$', n:2, res:'\\dfrac{2a}{3}', hint:'НОД(8,12) $= 4$' },
|
||
{ q:'$\\dfrac{15x^2}{25x}$', n:3, res:'\\dfrac{3x}{5}', hint:'НОД(15,25) $= 5$, сокращаем $x$' },
|
||
{ q:'$\\dfrac{x^2-25}{x-5}$', n:1, res:'x+5', hint:'$(x-5)(x+5) / (x-5) = x+5$' },
|
||
{ q:'$\\dfrac{a^2-9}{a+3}$', n:1, res:'a-3', hint:'$(a-3)(a+3) / (a+3) = a-3$' },
|
||
{ q:'$\\dfrac{14m^2 n}{21mn^2}$', n:2, res:'\\dfrac{2m}{3n}', hint:'НОД(14,21) $= 7$, сокращаем $mn$' },
|
||
{ q:'$\\dfrac{6(x-1)}{8(x-1)^2}$', n:3, res:'\\dfrac{3}{4(x-1)}', hint:'НОД(6,8) $= 2$, сокращаем $(x-1)$' },
|
||
];
|
||
let i = 0, score = 0;
|
||
function show(){
|
||
if(i >= Q.length){
|
||
document.getElementById('p2-iv4-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
|
||
if(score === Q.length){ addXp(15,'p2-iv4'); bumpProgress('p2', 25); }
|
||
else if(score >= 4){ addXp(8,'p2-iv4'); bumpProgress('p2', 15); }
|
||
return;
|
||
}
|
||
document.getElementById('p2-iv4-i').textContent = (i+1);
|
||
document.getElementById('p2-iv4-s').textContent = score;
|
||
document.getElementById('p2-iv4-q').innerHTML = 'Сократи: ' + Q[i].q;
|
||
document.getElementById('p2-iv4-ans').value = '';
|
||
renderMath(document.getElementById('p2-iv4-q'));
|
||
document.getElementById('p2-iv4-fb').style.display = 'none';
|
||
}
|
||
function go(){
|
||
if(i >= Q.length) return;
|
||
const fb = document.getElementById('p2-iv4-fb');
|
||
const ans = parseInt(document.getElementById('p2-iv4-ans').value, 10);
|
||
if(isNaN(ans)){ feedback(fb, false, '✗ Введи число.'); return; }
|
||
if(ans === Q[i].n){ score++; feedback(fb, true, '✓ Верно! $' + Q[i].res + '$ ('+Q[i].hint+'). Дальше ▶'); }
|
||
else feedback(fb, false, '✗ Неверно. Должно быть $' + Q[i].n + '$, ответ $' + Q[i].res + '$ ('+Q[i].hint+'). Дальше ▶');
|
||
document.getElementById('p2-iv4-s').textContent = score;
|
||
i++;
|
||
setTimeout(show, 1300);
|
||
}
|
||
document.getElementById('p2-iv4-go').addEventListener('click', go);
|
||
document.getElementById('p2-iv4-ans').addEventListener('keydown', e=>{ if(e.key === 'Enter') go(); });
|
||
document.getElementById('p2-iv4-start').addEventListener('click', ()=>{ i=0; score=0; show(); });
|
||
show();
|
||
})();
|
||
|
||
wireReadBtn('p2');
|
||
}
|
||
|
||
function buildP3(){
|
||
const box = document.getElementById('p3-body');
|
||
let html = '';
|
||
|
||
html += makeCard('theory', 'С одинаковыми знаменателями', '3.1', `
|
||
<p>Чтобы сложить (или вычесть) рациональные дроби с <b>одинаковыми знаменателями</b>, надо сложить (вычесть) их числители, а знаменатель оставить прежним:</p>
|
||
\\[\\dfrac{A}{C} + \\dfrac{B}{C} = \\dfrac{A+B}{C}, \\qquad \\dfrac{A}{C} - \\dfrac{B}{C} = \\dfrac{A-B}{C}, \\quad C \\ne 0.\\]
|
||
<p>Примеры:</p>
|
||
<ul style="padding-left:22px;line-height:1.9">
|
||
<li>$\\dfrac{x}{5} + \\dfrac{3}{5} = \\dfrac{x+3}{5}$</li>
|
||
<li>$\\dfrac{2a}{a-1} - \\dfrac{a+5}{a-1} = \\dfrac{2a - (a+5)}{a-1} = \\dfrac{a-5}{a-1}$ — не забывай менять знаки при вычитании!</li>
|
||
</ul>
|
||
<details class="spoiler"><summary>Часто допускаемая ошибка</summary><div class="spoiler-body">
|
||
При вычитании всегда заключай числитель вычитаемой дроби в <b>скобки</b>: $\\dfrac{A}{C} - \\dfrac{B}{C} = \\dfrac{A - (B)}{C}$, иначе можно «потерять» минус и получить $A - B$ вместо $A - B_1 + B_2$.
|
||
</div></details>`);
|
||
|
||
html += makeCard('rule', 'С разными знаменателями · Алгоритм', '3.2', `
|
||
<p>Если знаменатели <b>разные</b> — приводим дроби к общему знаменателю.</p>
|
||
<ol style="padding-left:22px;line-height:2">
|
||
<li><b>Разложить</b> каждый знаменатель на множители.</li>
|
||
<li><b>Найти НОЗ</b> — наименьший общий знаменатель: произведение всех различных множителей, взятых в <b>наивысших</b> степенях.</li>
|
||
<li><b>Привести</b> каждую дробь к НОЗ: умножить числитель и знаменатель на нужный <b>дополнительный множитель</b>.</li>
|
||
<li><b>Сложить/вычесть</b> числители (знаменатель — НОЗ).</li>
|
||
<li><b>Сократить</b> результат, если возможно.</li>
|
||
</ol>
|
||
<p>Пример НОЗ:</p>
|
||
<ul style="padding-left:22px;line-height:1.8">
|
||
<li>$\\dfrac{1}{2x}$ и $\\dfrac{1}{3x}$ → НОЗ $= 6x$ (НОК(2,3) $= 6$, $x$ один раз).</li>
|
||
<li>$\\dfrac{1}{x-2}$ и $\\dfrac{1}{x^2-4}$ → $x^2-4 = (x-2)(x+2)$, НОЗ $= (x-2)(x+2)$.</li>
|
||
</ul>`);
|
||
|
||
html += makeCard('example', 'Примеры пошагово', '3.3', `
|
||
<p><b>Пример 1.</b> $\\dfrac{1}{x-2} + \\dfrac{1}{x+2}$. НОЗ $= (x-2)(x+2) = x^2 - 4$.</p>
|
||
\\[\\dfrac{1}{x-2} + \\dfrac{1}{x+2} = \\dfrac{(x+2) + (x-2)}{(x-2)(x+2)} = \\dfrac{2x}{x^2-4}.\\]
|
||
<p><b>Пример 2.</b> $\\dfrac{2}{a} - \\dfrac{3}{a^2}$. НОЗ $= a^2$. Дополнительный множитель для $\\dfrac{2}{a}$ равен $a$:</p>
|
||
\\[\\dfrac{2}{a} - \\dfrac{3}{a^2} = \\dfrac{2a}{a^2} - \\dfrac{3}{a^2} = \\dfrac{2a - 3}{a^2}.\\]
|
||
<p><b>Пример 3 (с сокращением).</b> $\\dfrac{1}{x-1} - \\dfrac{1}{x+1} = \\dfrac{(x+1) - (x-1)}{(x-1)(x+1)} = \\dfrac{2}{x^2-1}.$</p>`);
|
||
|
||
/* INTERACTIVE 1 — Конструктор НОЗ */
|
||
html += `<div class="wg" id="p3-iv1">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Конструктор НОЗ</div></div>
|
||
<div class="wg-help">Выбери пару знаменателей — увидишь разложение каждого, НОЗ и дополнительные множители.</div>
|
||
<div class="sliders">
|
||
<label>Пара №<b id="p3-iv1-idx">1</b> / 5<input type="range" id="p3-iv1-sl" min="1" max="5" step="1" value="1"></label>
|
||
</div>
|
||
<div id="p3-iv1-pair" style="text-align:center;font-size:1.1rem;padding:12px;background:var(--card);border-radius:9px;margin-bottom:10px"></div>
|
||
<div style="display:flex;gap:10px;justify-content:center;margin-bottom:10px">
|
||
<button class="btn primary" id="p3-iv1-go">Найти НОЗ</button>
|
||
<button class="btn" id="p3-iv1-hide">Скрыть</button>
|
||
</div>
|
||
<div id="p3-iv1-out" style="padding:12px;background:var(--sec-acc-soft);border-radius:9px;font-size:1rem;min-height:50px;display:none"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 2 — Калькулятор сложения */
|
||
html += `<div class="wg" id="p3-iv2">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор $\\dfrac{A}{C} \\pm \\dfrac{B}{C}$</div></div>
|
||
<div class="wg-help">Введи целые числители $A$ и $B$, выбери знак и знаменатель $C$ — получишь результат.</div>
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:center;margin-bottom:10px">
|
||
<span>$A =$</span>
|
||
<input type="number" id="p3-iv2-a" class="tinp" style="width:80px;text-align:center" value="3">
|
||
<select id="p3-iv2-op" class="tinp" style="width:60px;text-align:center;font-size:1.1rem">
|
||
<option value="+">+</option>
|
||
<option value="-">−</option>
|
||
</select>
|
||
<span>$B =$</span>
|
||
<input type="number" id="p3-iv2-b" class="tinp" style="width:80px;text-align:center" value="5">
|
||
<span>над $C =$</span>
|
||
<select id="p3-iv2-c" class="tinp" style="width:90px;text-align:center">
|
||
<option value="x">x</option>
|
||
<option value="x+1">x+1</option>
|
||
<option value="x-2">x-2</option>
|
||
<option value="2">2</option>
|
||
<option value="5">5</option>
|
||
</select>
|
||
<button class="btn primary" id="p3-iv2-go">Вычислить</button>
|
||
</div>
|
||
<div id="p3-iv2-out" style="padding:12px;background:var(--card);border-radius:9px;text-align:center;font-size:1.05rem;min-height:50px"></div>
|
||
<div class="feedback" id="p3-iv2-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 3 — DnD-сортер: какой НОЗ? */
|
||
html += `<div class="wg" id="p3-iv3">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Какой НОЗ?</div></div>
|
||
<div class="wg-help">Перетащи каждую пару знаменателей в нужный ящик: тип НОЗ.</div>
|
||
<div class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11V6a3 3 0 0 1 6 0v5"/><path d="M9 11h6v8a4 4 0 0 1-8 0z"/></svg> 6 пар — 3 типа НОЗ</div>
|
||
<div id="p3-iv3-pool"></div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;margin-top:8px">
|
||
<div class="drop-box"><h5 data-cat="prod">Произведение знаменателей</h5><div class="drop-items" data-cat="prod"></div></div>
|
||
<div class="drop-box"><h5 data-cat="one">Один из знаменателей</h5><div class="drop-items" data-cat="one"></div></div>
|
||
<div class="drop-box"><h5 data-cat="num">Просто число</h5><div class="drop-items" data-cat="num"></div></div>
|
||
</div>
|
||
<div class="actions"><button class="btn primary" id="p3-iv3-check">Проверить</button><button class="btn" id="p3-iv3-reset">Сначала</button></div>
|
||
<div class="feedback" id="p3-iv3-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 4 — Тренажёр сложения/вычитания */
|
||
html += `<div class="wg" id="p3-iv4">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр сложения и вычитания</div></div>
|
||
<div class="wg-help">Реши пример и введи число, которое спрашивают в подсказке.</div>
|
||
<div class="score-display"><span>Задача <b id="p3-iv4-i">1</b> / 6</span><span>Очки: <b id="p3-iv4-s">0</b> / 6</span></div>
|
||
<div id="p3-iv4-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">
|
||
<span id="p3-iv4-prompt" style="font-family:'JetBrains Mono',monospace;font-size:.95rem">ответ =</span>
|
||
<input type="number" id="p3-iv4-ans" class="tinp" style="width:100px;text-align:center">
|
||
<button class="btn primary" id="p3-iv4-go">Проверить</button>
|
||
<button class="btn" id="p3-iv4-start">Заново</button>
|
||
</div>
|
||
<div class="feedback" id="p3-iv4-fb"></div>
|
||
</div>`;
|
||
|
||
html += secNav('p2', 'p4');
|
||
html += readButton('p3');
|
||
|
||
box.innerHTML = html;
|
||
renderMath(box);
|
||
|
||
/* IV1 — Конструктор НОЗ */
|
||
(function(){
|
||
const PAIRS = [
|
||
{ d1:'x', d2:'3x', fact1:'x', fact2:'3 \\cdot x', lcm:'3x', add1:'3', add2:'1' },
|
||
{ d1:'x-1', d2:'x+1', fact1:'(x-1)', fact2:'(x+1)', lcm:'(x-1)(x+1) = x^2 - 1', add1:'(x+1)', add2:'(x-1)' },
|
||
{ d1:'x-2', d2:'x^2-4', fact1:'(x-2)', fact2:'(x-2)(x+2)', lcm:'(x-2)(x+2)', add1:'(x+2)', add2:'1' },
|
||
{ d1:'2x', d2:'3x', fact1:'2 \\cdot x', fact2:'3 \\cdot x', lcm:'6x', add1:'3', add2:'2' },
|
||
{ d1:'x', d2:'x^2', fact1:'x', fact2:'x \\cdot x', lcm:'x^2', add1:'x', add2:'1' },
|
||
];
|
||
const sl = document.getElementById('p3-iv1-sl');
|
||
const idx = document.getElementById('p3-iv1-idx');
|
||
const pEl = document.getElementById('p3-iv1-pair');
|
||
const out = document.getElementById('p3-iv1-out');
|
||
const go = document.getElementById('p3-iv1-go');
|
||
const hide= document.getElementById('p3-iv1-hide');
|
||
const seen = new Set();
|
||
function show(){
|
||
const k = +sl.value; idx.textContent = k;
|
||
const p = PAIRS[k-1];
|
||
pEl.innerHTML = 'Знаменатели: $'+p.d1+'$ и $'+p.d2+'$';
|
||
out.innerHTML = '<div>Разложение: $'+p.d1+' = '+p.fact1+'$, $'+p.d2+' = '+p.fact2+'$</div>'
|
||
+ '<div style="margin-top:6px"><b>НОЗ $= '+p.lcm+'$</b></div>'
|
||
+ '<div style="margin-top:6px;font-size:.92rem;color:var(--muted)">Доп. множители: к $\\dfrac{?}{'+p.d1+'}$ → $'+p.add1+'$; к $\\dfrac{?}{'+p.d2+'}$ → $'+p.add2+'$</div>';
|
||
out.style.display = 'none';
|
||
renderMath(pEl);
|
||
seen.add(k);
|
||
if(seen.size === PAIRS.length && !seen.has('done')){ addXp(10,'p3-iv1'); bumpProgress('p3', 15); seen.add('done'); }
|
||
}
|
||
sl.addEventListener('input', show);
|
||
go.addEventListener('click', ()=>{ out.style.display = 'block'; renderMath(out); });
|
||
hide.addEventListener('click', ()=>{ out.style.display = 'none'; });
|
||
show();
|
||
})();
|
||
|
||
/* IV2 — Калькулятор A/C ± B/C */
|
||
(function(){
|
||
const aI = document.getElementById('p3-iv2-a');
|
||
const bI = document.getElementById('p3-iv2-b');
|
||
const opI= document.getElementById('p3-iv2-op');
|
||
const cI = document.getElementById('p3-iv2-c');
|
||
const out= document.getElementById('p3-iv2-out');
|
||
const fb = document.getElementById('p3-iv2-fb');
|
||
const go = document.getElementById('p3-iv2-go');
|
||
let solved = 0;
|
||
function calc(){
|
||
const a = parseInt(aI.value, 10), b = parseInt(bI.value, 10);
|
||
const op= opI.value;
|
||
const c = cI.value;
|
||
if(isNaN(a) || isNaN(b)){ feedback(fb, false, '✗ Введи целые числа $A$ и $B$.'); return; }
|
||
const sum = (op === '+') ? a + b : a - b;
|
||
const numericC = /^\d+$/.test(c);
|
||
let resHtml;
|
||
if(sum === 0){
|
||
resHtml = '$\\dfrac{'+a+'}{'+c+'} '+op+' \\dfrac{'+b+'}{'+c+'} = \\dfrac{0}{'+c+'} = 0$';
|
||
} else if(numericC){
|
||
const C = parseInt(c, 10);
|
||
const g = gcd(Math.abs(sum), C);
|
||
const n2 = sum/g, d2 = C/g;
|
||
if(d2 === 1) resHtml = '$\\dfrac{'+a+'}{'+c+'} '+op+' \\dfrac{'+b+'}{'+c+'} = \\dfrac{'+sum+'}{'+c+'} = '+n2+'$';
|
||
else if(g === 1) resHtml = '$\\dfrac{'+a+'}{'+c+'} '+op+' \\dfrac{'+b+'}{'+c+'} = \\dfrac{'+sum+'}{'+c+'}$';
|
||
else resHtml = '$\\dfrac{'+a+'}{'+c+'} '+op+' \\dfrac{'+b+'}{'+c+'} = \\dfrac{'+sum+'}{'+c+'} = \\dfrac{'+n2+'}{'+d2+'}$';
|
||
} else {
|
||
resHtml = '$\\dfrac{'+a+'}{'+c+'} '+op+' \\dfrac{'+b+'}{'+c+'} = \\dfrac{'+sum+'}{'+c+'}$';
|
||
}
|
||
out.innerHTML = resHtml;
|
||
renderMath(out);
|
||
feedback(fb, true, '✓ Готово! +10 XP');
|
||
solved++;
|
||
if(solved === 1){ addXp(10,'p3-iv2'); bumpProgress('p3', 15); }
|
||
}
|
||
go.addEventListener('click', calc);
|
||
calc();
|
||
})();
|
||
|
||
/* IV3 — DnD сортер НОЗ */
|
||
(function(){
|
||
const items = [
|
||
{ id:'s1', cat:'prod', html:'$x$ и $x+1$' },
|
||
{ id:'s2', cat:'one', html:'$x-3$ и $2(x-3)$' },
|
||
{ id:'s3', cat:'prod', html:'$x-1$ и $x+1$' },
|
||
{ id:'s4', cat:'num', html:'$4$ и $6$' },
|
||
{ id:'s5', cat:'one', html:'$x^2$ и $x^3$' },
|
||
{ id:'s6', cat:'one', html:'$x$ и $x^2-x$' },
|
||
];
|
||
const sorter = setupSorter({
|
||
poolId:'p3-iv3-pool',
|
||
scopeSelector:'#p3-iv3',
|
||
items: items,
|
||
cats:['prod','one','num'],
|
||
columnLayout:true,
|
||
});
|
||
document.getElementById('p3-iv3-check').addEventListener('click', ()=>{
|
||
const fb = document.getElementById('p3-iv3-fb');
|
||
const placedCount = items.filter(it => sorter.placed[it.id]).length;
|
||
const correct = items.filter(it => sorter.placed[it.id] === it.cat).length;
|
||
if(placedCount < items.length){ feedback(fb, false, '✗ Размести все 6 пар.'); return; }
|
||
if(correct === items.length){ feedback(fb, true, '✓ Все 6 на месте! +15 XP'); addXp(15,'p3-iv3'); bumpProgress('p3', 25); }
|
||
else feedback(fb, false, '✗ Правильно ' + correct + ' из 6. Попробуй ещё.');
|
||
});
|
||
document.getElementById('p3-iv3-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p3-iv3-fb').style.display = 'none'; });
|
||
})();
|
||
|
||
/* IV4 — Тренажёр */
|
||
(function(){
|
||
const Q = [
|
||
{ q:'$\\dfrac{3}{x} + \\dfrac{5}{x}$', ans:8, prompt:'числитель =', hint:'сложили $3 + 5 = 8$, знаменатель тот же', res:'\\dfrac{8}{x}' },
|
||
{ q:'$\\dfrac{7}{a} - \\dfrac{4}{a}$', ans:3, prompt:'числитель =', hint:'$7 - 4 = 3$', res:'\\dfrac{3}{a}' },
|
||
{ q:'$\\dfrac{1}{x-1} + \\dfrac{1}{x-1}$', ans:2, prompt:'числитель =', hint:'$1 + 1 = 2$', res:'\\dfrac{2}{x-1}' },
|
||
{ q:'$\\dfrac{x}{2} + \\dfrac{x}{3}$', ans:5, prompt:'коэф. при x =', hint:'НОЗ $= 6$: $\\dfrac{3x + 2x}{6} = \\dfrac{5x}{6}$', res:'\\dfrac{5x}{6}' },
|
||
{ q:'$\\dfrac{1}{x} - \\dfrac{1}{x^2}$', ans:2, prompt:'показатель x в знаменателе =', hint:'НОЗ $= x^2$, числитель $= x - 1$', res:'\\dfrac{x-1}{x^2}' },
|
||
{ q:'$\\dfrac{2}{x+1} + \\dfrac{3}{x+1}$', ans:5, prompt:'числитель =', hint:'$2 + 3 = 5$', res:'\\dfrac{5}{x+1}' },
|
||
];
|
||
let i = 0, score = 0;
|
||
function show(){
|
||
if(i >= Q.length){
|
||
document.getElementById('p3-iv4-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
|
||
document.getElementById('p3-iv4-prompt').textContent = 'ответ =';
|
||
if(score === Q.length){ addXp(15,'p3-iv4'); bumpProgress('p3', 25); }
|
||
else if(score >= 4){ addXp(8,'p3-iv4'); bumpProgress('p3', 15); }
|
||
return;
|
||
}
|
||
document.getElementById('p3-iv4-i').textContent = (i+1);
|
||
document.getElementById('p3-iv4-s').textContent = score;
|
||
document.getElementById('p3-iv4-q').innerHTML = 'Найди: ' + Q[i].q;
|
||
document.getElementById('p3-iv4-prompt').textContent = Q[i].prompt;
|
||
document.getElementById('p3-iv4-ans').value = '';
|
||
renderMath(document.getElementById('p3-iv4-q'));
|
||
document.getElementById('p3-iv4-fb').style.display = 'none';
|
||
}
|
||
function go(){
|
||
if(i >= Q.length) return;
|
||
const fb = document.getElementById('p3-iv4-fb');
|
||
const ans = parseInt(document.getElementById('p3-iv4-ans').value, 10);
|
||
if(isNaN(ans)){ feedback(fb, false, '✗ Введи целое число.'); return; }
|
||
if(ans === Q[i].ans){ score++; feedback(fb, true, '✓ Верно! Ответ: $' + Q[i].res + '$ ('+Q[i].hint+'). Дальше ▶'); }
|
||
else feedback(fb, false, '✗ Неверно. Должно быть $' + Q[i].ans + '$. Ответ: $' + Q[i].res + '$ ('+Q[i].hint+'). Дальше ▶');
|
||
document.getElementById('p3-iv4-s').textContent = score;
|
||
i++;
|
||
setTimeout(show, 1400);
|
||
}
|
||
document.getElementById('p3-iv4-go').addEventListener('click', go);
|
||
document.getElementById('p3-iv4-ans').addEventListener('keydown', e=>{ if(e.key === 'Enter') go(); });
|
||
document.getElementById('p3-iv4-start').addEventListener('click', ()=>{ i=0; score=0; show(); });
|
||
show();
|
||
})();
|
||
|
||
wireReadBtn('p3');
|
||
}
|
||
|
||
function buildP4(){
|
||
const box = document.getElementById('p4-body');
|
||
let html = '';
|
||
|
||
html += makeCard('theory', 'Умножение дробей', '4.1', `
|
||
<p>Чтобы <b>умножить</b> две рациональные дроби, надо перемножить их числители и перемножить знаменатели:</p>
|
||
\\[\\dfrac{A}{B} \\cdot \\dfrac{C}{D} = \\dfrac{A \\cdot C}{B \\cdot D}, \\quad B \\ne 0,\\ D \\ne 0.\\]
|
||
<p>Чаще удобно <b>сократить</b> общие множители <i>до</i> умножения — это упрощает вычисления.</p>
|
||
<p>Пример: $\\dfrac{2x}{3} \\cdot \\dfrac{5}{x^2} = \\dfrac{2x \\cdot 5}{3 \\cdot x^2} = \\dfrac{10x}{3x^2} = \\dfrac{10}{3x}$ — сократили $x$.</p>
|
||
<details class="spoiler"><summary>Можно ли «крест-накрест»?</summary><div class="spoiler-body">
|
||
При умножении сокращать можно <b>любой</b> множитель числителя с <b>любым</b> множителем знаменателя (даже «крест-накрест»): $\\dfrac{a}{b} \\cdot \\dfrac{b}{c} = \\dfrac{a}{c}$, $b$ ушло.
|
||
</div></details>`);
|
||
|
||
html += makeCard('rule', 'Деление дробей · Правило', '4.2', `
|
||
<p>Чтобы <b>разделить</b> одну рациональную дробь на другую, надо первую дробь умножить на <b>обратную</b> ко второй:</p>
|
||
\\[\\dfrac{A}{B} : \\dfrac{C}{D} = \\dfrac{A}{B} \\cdot \\dfrac{D}{C} = \\dfrac{A \\cdot D}{B \\cdot C}, \\quad B \\ne 0,\\ C \\ne 0,\\ D \\ne 0.\\]
|
||
<p><b>Обратная дробь</b> к $\\dfrac{C}{D}$ — это $\\dfrac{D}{C}$ (числитель и знаменатель меняются местами).</p>
|
||
<p>Условие $C \\ne 0$ возникает потому, что мы делим на дробь $\\dfrac{C}{D}$, а на ноль делить нельзя.</p>
|
||
<p style="background:var(--warn-bg);padding:8px 12px;border-radius:8px;border-left:3px solid var(--warn)"><b>Запомни:</b> деление = умножение на <i>перевёрнутую</i> дробь. После этого работаем как с обычным умножением.</p>`);
|
||
|
||
html += makeCard('example', 'Возведение дроби в степень', '4.3', `
|
||
<p>Чтобы возвести дробь в натуральную степень $n$, надо возвести в эту степень и числитель, и знаменатель:</p>
|
||
\\[\\left(\\dfrac{A}{B}\\right)^n = \\dfrac{A^n}{B^n}, \\quad B \\ne 0.\\]
|
||
<p>Примеры:</p>
|
||
<ul style="padding-left:22px;line-height:1.9">
|
||
<li>$\\left(\\dfrac{2}{x}\\right)^3 = \\dfrac{2^3}{x^3} = \\dfrac{8}{x^3}$</li>
|
||
<li>$\\left(\\dfrac{x-1}{x+1}\\right)^2 = \\dfrac{(x-1)^2}{(x+1)^2}$ — скобки обязательны!</li>
|
||
<li>$\\left(\\dfrac{3a}{b^2}\\right)^2 = \\dfrac{9a^2}{b^4}$</li>
|
||
</ul>`);
|
||
|
||
/* INTERACTIVE 1 — Умножение с подсветкой */
|
||
html += `<div class="wg" id="p4-iv1">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Умножение с подсветкой</div></div>
|
||
<div class="wg-help">Выбери задачу — увидишь произведение дробей. Нажми «Показать шаги», чтобы увидеть сокращение.</div>
|
||
<div class="sliders">
|
||
<label>Задача №<b id="p4-iv1-idx">1</b> / 5<input type="range" id="p4-iv1-sl" min="1" max="5" step="1" value="1"></label>
|
||
</div>
|
||
<div id="p4-iv1-before" style="text-align:center;font-size:1.2rem;padding:14px;background:var(--card);border-radius:9px;margin-bottom:10px;min-height:60px"></div>
|
||
<div style="display:flex;gap:10px;justify-content:center;margin-bottom:10px">
|
||
<button class="btn primary" id="p4-iv1-go">Показать шаги</button>
|
||
<button class="btn" id="p4-iv1-hide">Скрыть</button>
|
||
</div>
|
||
<div id="p4-iv1-after" style="text-align:center;font-size:1.1rem;padding:14px;background:var(--sec-acc-soft);border-radius:9px;min-height:50px;display:none"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 2 — Калькулятор деления a/b : c/d */
|
||
html += `<div class="wg" id="p4-iv2">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор деления $\\dfrac{a}{b} : \\dfrac{c}{d}$</div></div>
|
||
<div class="wg-help">Введи целые $a, b, c, d$ — увидишь преобразование к умножению и упрощённый результат.</div>
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:center;margin-bottom:10px">
|
||
<span>$a =$</span><input type="number" id="p4-iv2-a" class="tinp" style="width:70px;text-align:center" value="3">
|
||
<span>$b =$</span><input type="number" id="p4-iv2-b" class="tinp" style="width:70px;text-align:center" value="4">
|
||
<span>$c =$</span><input type="number" id="p4-iv2-c" class="tinp" style="width:70px;text-align:center" value="9">
|
||
<span>$d =$</span><input type="number" id="p4-iv2-d" class="tinp" style="width:70px;text-align:center" value="8">
|
||
<button class="btn primary" id="p4-iv2-go">Вычислить</button>
|
||
</div>
|
||
<div id="p4-iv2-out" style="padding:12px;background:var(--card);border-radius:9px;text-align:center;font-size:1rem;min-height:50px"></div>
|
||
<div class="feedback" id="p4-iv2-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 3 — Найди ошибку */
|
||
html += `<div class="wg" id="p4-iv3">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Верно или ошибка?</div></div>
|
||
<div class="wg-help">Реши каждое преобразование: верно оно или нет. 6 заданий.</div>
|
||
<div class="score-display"><span>Задача <b id="p4-iv3-i">1</b> / 6</span><span>Очки: <b id="p4-iv3-s">0</b> / 6</span></div>
|
||
<div id="p4-iv3-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>
|
||
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap">
|
||
<button class="btn primary" id="p4-iv3-ok" style="background:#10b981;border-color:#10b981">Верно</button>
|
||
<button class="btn primary" id="p4-iv3-err" style="background:#dc2626;border-color:#dc2626">Ошибка</button>
|
||
</div>
|
||
<div class="feedback" id="p4-iv3-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 4 — Тренажёр умножения и деления */
|
||
html += `<div class="wg" id="p4-iv4">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр умножения и деления</div></div>
|
||
<div class="wg-help">Вычисли и введи число, которое спрашивают в подсказке.</div>
|
||
<div class="score-display"><span>Задача <b id="p4-iv4-i">1</b> / 6</span><span>Очки: <b id="p4-iv4-s">0</b> / 6</span></div>
|
||
<div id="p4-iv4-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">
|
||
<span id="p4-iv4-prompt" style="font-family:'JetBrains Mono',monospace;font-size:.95rem">ответ =</span>
|
||
<input type="number" id="p4-iv4-ans" class="tinp" style="width:100px;text-align:center">
|
||
<button class="btn primary" id="p4-iv4-go">Проверить</button>
|
||
<button class="btn" id="p4-iv4-start">Заново</button>
|
||
</div>
|
||
<div class="feedback" id="p4-iv4-fb"></div>
|
||
</div>`;
|
||
|
||
html += secNav('p3', 'p5');
|
||
html += readButton('p4');
|
||
|
||
box.innerHTML = html;
|
||
renderMath(box);
|
||
|
||
/* IV1 — Умножение со скрытием шагов */
|
||
(function(){
|
||
const T = [
|
||
{ before:'\\dfrac{3}{x} \\cdot \\dfrac{x}{5}', steps:'\\dfrac{3 \\cdot x}{x \\cdot 5} = \\dfrac{3x}{5x}', after:'\\dfrac{3}{5}', note:'сократили $x$' },
|
||
{ before:'\\dfrac{2a}{b^2} \\cdot \\dfrac{b}{4}', steps:'\\dfrac{2a \\cdot b}{b^2 \\cdot 4} = \\dfrac{2ab}{4b^2}', after:'\\dfrac{a}{2b}', note:'сократили $2b$' },
|
||
{ before:'\\dfrac{x+1}{x-1} \\cdot \\dfrac{x-1}{x+2}', steps:'\\dfrac{(x+1)(x-1)}{(x-1)(x+2)}', after:'\\dfrac{x+1}{x+2}', note:'сократили $(x-1)$' },
|
||
{ before:'\\dfrac{6}{x^2} \\cdot \\dfrac{x}{2}', steps:'\\dfrac{6 \\cdot x}{x^2 \\cdot 2} = \\dfrac{6x}{2x^2}', after:'\\dfrac{3}{x}', note:'сократили $2x$' },
|
||
{ before:'\\dfrac{x-3}{4} \\cdot \\dfrac{8}{(x-3)^2}', steps:'\\dfrac{(x-3) \\cdot 8}{4 \\cdot (x-3)^2} = \\dfrac{8(x-3)}{4(x-3)^2}', after:'\\dfrac{2}{x-3}', note:'сократили $4(x-3)$' },
|
||
];
|
||
const sl = document.getElementById('p4-iv1-sl');
|
||
const idx = document.getElementById('p4-iv1-idx');
|
||
const bEl = document.getElementById('p4-iv1-before');
|
||
const aEl = document.getElementById('p4-iv1-after');
|
||
const go = document.getElementById('p4-iv1-go');
|
||
const hide = document.getElementById('p4-iv1-hide');
|
||
const seen = new Set();
|
||
function show(){
|
||
const k = +sl.value; idx.textContent = k;
|
||
const t = T[k-1];
|
||
bEl.innerHTML = '$' + t.before + '$';
|
||
aEl.innerHTML = '<div style="font-size:.9rem;color:var(--muted);margin-bottom:6px">'+t.note+'</div>$' + t.before + ' \\;=\\; ' + t.steps + ' \\;=\\; ' + t.after + '$';
|
||
aEl.style.display = 'none';
|
||
renderMath(bEl);
|
||
seen.add(k);
|
||
if(seen.size === T.length && !seen.has('done')){ addXp(10,'p4-iv1'); bumpProgress('p4', 15); seen.add('done'); }
|
||
}
|
||
sl.addEventListener('input', show);
|
||
go.addEventListener('click', ()=>{ aEl.style.display = 'block'; renderMath(aEl); });
|
||
hide.addEventListener('click', ()=>{ aEl.style.display = 'none'; });
|
||
show();
|
||
})();
|
||
|
||
/* IV2 — Деление калькулятор */
|
||
(function(){
|
||
const aI = document.getElementById('p4-iv2-a');
|
||
const bI = document.getElementById('p4-iv2-b');
|
||
const cI = document.getElementById('p4-iv2-c');
|
||
const dI = document.getElementById('p4-iv2-d');
|
||
const out= document.getElementById('p4-iv2-out');
|
||
const fb = document.getElementById('p4-iv2-fb');
|
||
const go = document.getElementById('p4-iv2-go');
|
||
let solved = 0;
|
||
function calc(){
|
||
const a = parseInt(aI.value,10), b = parseInt(bI.value,10),
|
||
c = parseInt(cI.value,10), d = parseInt(dI.value,10);
|
||
if([a,b,c,d].some(v => isNaN(v))){ feedback(fb, false, '✗ Введи 4 целых числа.'); return; }
|
||
if(b === 0 || c === 0 || d === 0){ feedback(fb, false, '✗ $b$, $c$, $d$ не должны быть равны $0$.'); return; }
|
||
const num = a * d;
|
||
const den = b * c;
|
||
const g = gcd(Math.abs(num), Math.abs(den));
|
||
let n2 = num/g, d2 = den/g;
|
||
if(d2 < 0){ n2 = -n2; d2 = -d2; }
|
||
let resHtml;
|
||
if(d2 === 1) resHtml = '$\\dfrac{'+a+'}{'+b+'} : \\dfrac{'+c+'}{'+d+'} = \\dfrac{'+a+'}{'+b+'} \\cdot \\dfrac{'+d+'}{'+c+'} = \\dfrac{'+num+'}{'+den+'} = '+n2+'$';
|
||
else if(g === 1) resHtml = '$\\dfrac{'+a+'}{'+b+'} : \\dfrac{'+c+'}{'+d+'} = \\dfrac{'+a+'}{'+b+'} \\cdot \\dfrac{'+d+'}{'+c+'} = \\dfrac{'+num+'}{'+den+'}$ (несократимо)';
|
||
else resHtml = '$\\dfrac{'+a+'}{'+b+'} : \\dfrac{'+c+'}{'+d+'} = \\dfrac{'+a+'}{'+b+'} \\cdot \\dfrac{'+d+'}{'+c+'} = \\dfrac{'+num+'}{'+den+'} = \\dfrac{'+n2+'}{'+d2+'}$';
|
||
out.innerHTML = resHtml;
|
||
renderMath(out);
|
||
feedback(fb, true, '✓ Готово! +10 XP');
|
||
solved++;
|
||
if(solved === 1){ addXp(10,'p4-iv2'); bumpProgress('p4', 15); }
|
||
}
|
||
go.addEventListener('click', calc);
|
||
calc();
|
||
})();
|
||
|
||
/* IV3 — Верно/Ошибка */
|
||
(function(){
|
||
const Q = [
|
||
{ expr:'$\\dfrac{x}{2} \\cdot \\dfrac{3}{y} = \\dfrac{3x}{2y}$', ok:true, why:'умножили числители и знаменатели' },
|
||
{ expr:'$\\dfrac{a}{b} : \\dfrac{c}{d} = \\dfrac{ad}{bc}$', ok:true, why:'это правило деления (умножение на обратную)' },
|
||
{ expr:'$\\dfrac{a}{b} \\cdot \\dfrac{c}{d} = \\dfrac{a+c}{b+d}$', ok:false, why:'при умножении числители <b>перемножаются</b>, а не складываются' },
|
||
{ expr:'$\\dfrac{2}{x} \\cdot \\dfrac{x}{2} = 1$', ok:true, why:'сократили $2x$ в числителе и $2x$ в знаменателе' },
|
||
{ expr:'$\\dfrac{a}{b} : \\dfrac{c}{d} = \\dfrac{a}{b} \\cdot \\dfrac{c}{d}$', ok:false, why:'нужно умножать на <b>обратную</b> дробь: $\\dfrac{d}{c}$, а не $\\dfrac{c}{d}$' },
|
||
{ expr:'$\\left(\\dfrac{2}{x}\\right)^2 = \\dfrac{2}{x^2}$', ok:false, why:'числитель тоже возводится в степень: правильно $\\dfrac{4}{x^2}$' },
|
||
];
|
||
let i = 0, score = 0;
|
||
function show(){
|
||
if(i >= Q.length){
|
||
document.getElementById('p4-iv3-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
|
||
if(score === Q.length){ addXp(15,'p4-iv3'); bumpProgress('p4', 25); }
|
||
else if(score >= Q.length - 2){ addXp(8,'p4-iv3'); bumpProgress('p4', 15); }
|
||
return;
|
||
}
|
||
document.getElementById('p4-iv3-i').textContent = (i+1);
|
||
document.getElementById('p4-iv3-s').textContent = score;
|
||
document.getElementById('p4-iv3-q').innerHTML = Q[i].expr;
|
||
renderMath(document.getElementById('p4-iv3-q'));
|
||
document.getElementById('p4-iv3-fb').style.display = 'none';
|
||
}
|
||
function answer(isOk){
|
||
if(i >= Q.length) return;
|
||
const fb = document.getElementById('p4-iv3-fb');
|
||
if(isOk === Q[i].ok){ score++; feedback(fb, true, '✓ Верно! '+Q[i].why+'. Дальше ▶'); }
|
||
else feedback(fb, false, '✗ Неверно. '+Q[i].why+'. Дальше ▶');
|
||
document.getElementById('p4-iv3-s').textContent = score;
|
||
i++;
|
||
setTimeout(show, 1300);
|
||
}
|
||
document.getElementById('p4-iv3-ok').addEventListener('click', ()=>answer(true));
|
||
document.getElementById('p4-iv3-err').addEventListener('click', ()=>answer(false));
|
||
show();
|
||
})();
|
||
|
||
/* IV4 — Тренажёр умножения/деления */
|
||
(function(){
|
||
const Q = [
|
||
{ q:'$\\dfrac{3}{4} \\cdot \\dfrac{8}{9}$', ans:2, prompt:'числитель =', hint:'$=\\dfrac{24}{36} = \\dfrac{2}{3}$', res:'\\dfrac{2}{3}' },
|
||
{ q:'$\\dfrac{x}{2} \\cdot \\dfrac{4}{x^2}$', ans:1, prompt:'показатель x в знам. =', hint:'$=\\dfrac{4x}{2x^2} = \\dfrac{2}{x}$', res:'\\dfrac{2}{x}' },
|
||
{ q:'$\\dfrac{6}{a} : \\dfrac{2}{a}$', ans:3, prompt:'число =', hint:'$=\\dfrac{6}{a} \\cdot \\dfrac{a}{2} = \\dfrac{6a}{2a} = 3$', res:'3' },
|
||
{ q:'$\\dfrac{5}{x+1} \\cdot \\dfrac{x+1}{10}$', ans:2, prompt:'знаменатель =', hint:'сократили $(x+1)$ и $5$: $\\dfrac{1}{2}$', res:'\\dfrac{1}{2}' },
|
||
{ q:'$\\left(\\dfrac{3}{x}\\right)^2$', ans:9, prompt:'числитель =', hint:'$=\\dfrac{3^2}{x^2} = \\dfrac{9}{x^2}$', res:'\\dfrac{9}{x^2}' },
|
||
{ q:'$\\dfrac{a^2}{b} : \\dfrac{a}{b^2}$', ans:1, prompt:'показатель a в ответе =', hint:'$=\\dfrac{a^2}{b} \\cdot \\dfrac{b^2}{a} = \\dfrac{a^2 b^2}{ab} = ab$ — у $a$ степень $1$', res:'ab' },
|
||
];
|
||
let i = 0, score = 0;
|
||
function show(){
|
||
if(i >= Q.length){
|
||
document.getElementById('p4-iv4-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
|
||
document.getElementById('p4-iv4-prompt').textContent = 'ответ =';
|
||
if(score === Q.length){ addXp(15,'p4-iv4'); bumpProgress('p4', 25); }
|
||
else if(score >= 4){ addXp(8,'p4-iv4'); bumpProgress('p4', 15); }
|
||
return;
|
||
}
|
||
document.getElementById('p4-iv4-i').textContent = (i+1);
|
||
document.getElementById('p4-iv4-s').textContent = score;
|
||
document.getElementById('p4-iv4-q').innerHTML = 'Вычисли: ' + Q[i].q;
|
||
document.getElementById('p4-iv4-prompt').textContent = Q[i].prompt;
|
||
document.getElementById('p4-iv4-ans').value = '';
|
||
renderMath(document.getElementById('p4-iv4-q'));
|
||
document.getElementById('p4-iv4-fb').style.display = 'none';
|
||
}
|
||
function go(){
|
||
if(i >= Q.length) return;
|
||
const fb = document.getElementById('p4-iv4-fb');
|
||
const ans = parseInt(document.getElementById('p4-iv4-ans').value, 10);
|
||
if(isNaN(ans)){ feedback(fb, false, '✗ Введи целое число.'); return; }
|
||
if(ans === Q[i].ans){ score++; feedback(fb, true, '✓ Верно! Ответ: $' + Q[i].res + '$ ('+Q[i].hint+'). Дальше ▶'); }
|
||
else feedback(fb, false, '✗ Неверно. Должно быть $' + Q[i].ans + '$. Ответ: $' + Q[i].res + '$ ('+Q[i].hint+'). Дальше ▶');
|
||
document.getElementById('p4-iv4-s').textContent = score;
|
||
i++;
|
||
setTimeout(show, 1400);
|
||
}
|
||
document.getElementById('p4-iv4-go').addEventListener('click', go);
|
||
document.getElementById('p4-iv4-ans').addEventListener('keydown', e=>{ if(e.key === 'Enter') go(); });
|
||
document.getElementById('p4-iv4-start').addEventListener('click', ()=>{ i=0; score=0; show(); });
|
||
show();
|
||
})();
|
||
|
||
wireReadBtn('p4');
|
||
}
|
||
|
||
function buildP5(){
|
||
const box = document.getElementById('p5-body');
|
||
let html = '';
|
||
|
||
html += makeCard('theory', 'Алгоритм многошагового упрощения', '5.1', `
|
||
<p>Сложное рациональное выражение упрощается по чёткому порядку действий:</p>
|
||
<ol style="padding-left:22px;line-height:2">
|
||
<li><b>Разложи</b> все числители и знаменатели на множители.</li>
|
||
<li>Выполни действия <b>в скобках</b> (сначала $+$ и $-$).</li>
|
||
<li>Выполни <b>умножение и деление</b> в порядке слева направо.</li>
|
||
<li><b>Сократи</b> результат.</li>
|
||
</ol>
|
||
<p style="background:var(--warn-bg);padding:8px 12px;border-radius:8px;border-left:3px solid var(--warn)"><b>Главное:</b> на каждом шаге следи за <b>ОДЗ</b> — никакой знаменатель не должен обращаться в ноль.</p>
|
||
<p>Помни также правила приоритета: сначала всё, что в скобках; затем умножение/деление; затем сложение/вычитание.</p>`);
|
||
|
||
html += makeCard('example', 'Пример: вся цепочка от начала до конца', '5.2', `
|
||
<p>Упростим выражение:</p>
|
||
\\[\\left( \\dfrac{1}{x-1} + \\dfrac{1}{x+1} \\right) \\cdot \\dfrac{x^2-1}{2x}.\\]
|
||
<p><b>Шаг 1 (скобка).</b> Общий знаменатель — $(x-1)(x+1)$:</p>
|
||
\\[\\dfrac{1}{x-1} + \\dfrac{1}{x+1} = \\dfrac{(x+1)+(x-1)}{(x-1)(x+1)} = \\dfrac{2x}{(x-1)(x+1)}.\\]
|
||
<p><b>Шаг 2 (умножение).</b> Разложим $x^2-1 = (x-1)(x+1)$:</p>
|
||
\\[\\dfrac{2x}{(x-1)(x+1)} \\cdot \\dfrac{(x-1)(x+1)}{2x}.\\]
|
||
<p><b>Шаг 3 (сокращение).</b> Сокращаем $2x$ и $(x-1)(x+1)$:</p>
|
||
\\[= 1.\\]
|
||
<p><b>ОДЗ:</b> $x \\ne 0,\\ x \\ne 1,\\ x \\ne -1$. На ОДЗ результат равен $1$.</p>`);
|
||
|
||
html += makeCard('rule', 'Полезные тождества для сокращений', '5.3', `
|
||
<p>Без этих формул многие дроби не упростить:</p>
|
||
<ul style="padding-left:22px;line-height:2">
|
||
<li><b>Разность квадратов:</b> $a^2 - b^2 = (a-b)(a+b)$</li>
|
||
<li><b>Квадрат суммы/разности:</b> $(a \\pm b)^2 = a^2 \\pm 2ab + b^2$</li>
|
||
<li><b>Сумма/разность кубов:</b> $a^3 \\pm b^3 = (a \\pm b)(a^2 \\mp ab + b^2)$</li>
|
||
<li><b>Вынесение общего множителя:</b> $ax \\pm ay = a(x \\pm y)$</li>
|
||
</ul>
|
||
<p>Перед сокращением всегда попробуй применить одно из этих тождеств — часто это единственный путь.</p>
|
||
<details class="spoiler"><summary>Подсказка: алгоритм поиска множителя</summary><div class="spoiler-body">
|
||
Если в числителе или знаменателе видишь два слагаемых — проверь: это разность квадратов? Если три слагаемых — может, полный квадрат? Если есть общий буквенный множитель — вынеси его. И только после этого сокращай.
|
||
</div></details>`);
|
||
|
||
/* INTERACTIVE 1 — Конвейер пошагового упрощения */
|
||
html += `<div class="wg" id="p5-iv1">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Конвейер упрощения</div></div>
|
||
<div class="wg-help">Выбери задачу и раскрывай шаги один за другим. После последнего шага — «Готово!». 5 задач.</div>
|
||
<div class="sliders">
|
||
<label>Задача №<b id="p5-iv1-idx">1</b> / 5<input type="range" id="p5-iv1-sl" min="1" max="5" step="1" value="1"></label>
|
||
</div>
|
||
<div id="p5-iv1-before" style="text-align:center;font-size:1.18rem;padding:14px;background:var(--card);border-radius:9px;margin-bottom:10px;min-height:60px"></div>
|
||
<div id="p5-iv1-steps" style="padding:0;background:transparent;margin-bottom:10px"></div>
|
||
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-bottom:10px">
|
||
<button class="btn primary" id="p5-iv1-next">Следующий шаг ▶</button>
|
||
<button class="btn" id="p5-iv1-reset">Сначала</button>
|
||
</div>
|
||
<div class="feedback" id="p5-iv1-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 2 — DnD: разложи числитель и знаменатель */
|
||
html += `<div class="wg" id="p5-iv2">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Сборщик: разложи и сократи</div></div>
|
||
<div class="wg-help">Дана дробь $\\dfrac{x^2-4}{x^2+4x+4}$. Перетащи правильные разложения в ящики «Числитель» и «Знаменатель». Лишние карточки оставь в пуле.</div>
|
||
<div class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11V6a3 3 0 0 1 6 0v5"/><path d="M9 11h6v8a4 4 0 0 1-8 0z"/></svg> Подсказка: $a^2-b^2$ и $(a+b)^2$</div>
|
||
<div id="p5-iv2-pool"></div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin-top:8px">
|
||
<div class="drop-box"><h5 data-cat="num">Числитель: $x^2-4$</h5><div class="drop-items" data-cat="num"></div></div>
|
||
<div class="drop-box"><h5 data-cat="den">Знаменатель: $x^2+4x+4$</h5><div class="drop-items" data-cat="den"></div></div>
|
||
</div>
|
||
<div class="actions"><button class="btn primary" id="p5-iv2-check">Проверить</button><button class="btn" id="p5-iv2-reset">Сначала</button></div>
|
||
<div id="p5-iv2-result" style="margin-top:10px;text-align:center;font-size:1.05rem;display:none;padding:10px;background:var(--sec-acc-soft);border-radius:9px"></div>
|
||
<div class="feedback" id="p5-iv2-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 3 — Найди ошибку (квикфайр) */
|
||
html += `<div class="wg" id="p5-iv3">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Найди ошибку в упрощении</div></div>
|
||
<div class="wg-help">6 цепочек преобразований. Реши: верно или ошибка?</div>
|
||
<div class="score-display"><span>Задача <b id="p5-iv3-i">1</b> / 6</span><span>Очки: <b id="p5-iv3-s">0</b> / 6</span></div>
|
||
<div id="p5-iv3-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px;min-height:54px"></div>
|
||
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap">
|
||
<button class="btn primary" id="p5-iv3-ok" style="background:#10b981;border-color:#10b981">Верно</button>
|
||
<button class="btn primary" id="p5-iv3-err" style="background:#dc2626;border-color:#dc2626">Ошибка</button>
|
||
</div>
|
||
<div class="feedback" id="p5-iv3-fb"></div>
|
||
</div>`;
|
||
|
||
/* INTERACTIVE 4 — Тренажёр многошагового упрощения */
|
||
html += `<div class="wg" id="p5-iv4">
|
||
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр многошагового упрощения</div></div>
|
||
<div class="wg-help">Упрости каждое выражение и введи число, которое спрашивают в подсказке.</div>
|
||
<div class="score-display"><span>Задача <b id="p5-iv4-i">1</b> / 5</span><span>Очки: <b id="p5-iv4-s">0</b> / 5</span></div>
|
||
<div id="p5-iv4-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center;min-height:60px"></div>
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">
|
||
<span id="p5-iv4-prompt" style="font-family:'JetBrains Mono',monospace;font-size:.95rem">ответ =</span>
|
||
<input type="number" id="p5-iv4-ans" class="tinp" style="width:110px;text-align:center">
|
||
<button class="btn primary" id="p5-iv4-go">Проверить</button>
|
||
<button class="btn" id="p5-iv4-start">Заново</button>
|
||
</div>
|
||
<div class="feedback" id="p5-iv4-fb"></div>
|
||
</div>`;
|
||
|
||
html += secNav('p4', 'final1');
|
||
html += readButton('p5');
|
||
|
||
box.innerHTML = html;
|
||
renderMath(box);
|
||
|
||
/* IV1 — Конвейер */
|
||
(function(){
|
||
const T = [
|
||
{
|
||
title:'\\dfrac{x^2-9}{x+3} : \\dfrac{x-3}{2}',
|
||
steps:[
|
||
'\\dfrac{x^2-9}{x+3} : \\dfrac{x-3}{2} = \\dfrac{(x-3)(x+3)}{x+3} \\cdot \\dfrac{2}{x-3}',
|
||
'= \\dfrac{(x-3)(x+3) \\cdot 2}{(x+3)(x-3)}',
|
||
'= 2'
|
||
],
|
||
notes:['разность квадратов и деление = умножение на обратную','перемножили','сократили $(x-3)(x+3)$']
|
||
},
|
||
{
|
||
title:'\\dfrac{1}{a} + \\dfrac{1}{a^2}',
|
||
steps:[
|
||
'\\dfrac{1}{a} + \\dfrac{1}{a^2} = \\dfrac{a}{a^2} + \\dfrac{1}{a^2}',
|
||
'= \\dfrac{a+1}{a^2}'
|
||
],
|
||
notes:['общий знаменатель $a^2$','сложили числители']
|
||
},
|
||
{
|
||
title:'\\dfrac{x^2-4}{x} \\cdot \\dfrac{1}{x+2}',
|
||
steps:[
|
||
'\\dfrac{x^2-4}{x} \\cdot \\dfrac{1}{x+2} = \\dfrac{(x-2)(x+2)}{x} \\cdot \\dfrac{1}{x+2}',
|
||
'= \\dfrac{(x-2)(x+2)}{x(x+2)}',
|
||
'= \\dfrac{x-2}{x}'
|
||
],
|
||
notes:['разложили $x^2-4$','перемножили','сократили $(x+2)$']
|
||
},
|
||
{
|
||
title:'\\dfrac{a}{a-b} - \\dfrac{b}{a-b}',
|
||
steps:[
|
||
'\\dfrac{a}{a-b} - \\dfrac{b}{a-b} = \\dfrac{a-b}{a-b}',
|
||
'= 1'
|
||
],
|
||
notes:['одинаковые знаменатели — вычли числители','сократили']
|
||
},
|
||
{
|
||
title:'\\dfrac{x+1}{x-1} : \\dfrac{x^2-1}{x-1}',
|
||
steps:[
|
||
'\\dfrac{x+1}{x-1} : \\dfrac{x^2-1}{x-1} = \\dfrac{x+1}{x-1} \\cdot \\dfrac{x-1}{(x-1)(x+1)}',
|
||
'= \\dfrac{(x+1)(x-1)}{(x-1)(x-1)(x+1)}',
|
||
'= \\dfrac{1}{x-1}'
|
||
],
|
||
notes:['обратная дробь + разложение $x^2-1$','перемножили','сократили $(x+1)(x-1)$']
|
||
},
|
||
];
|
||
const sl = document.getElementById('p5-iv1-sl');
|
||
const idx = document.getElementById('p5-iv1-idx');
|
||
const bEl = document.getElementById('p5-iv1-before');
|
||
const stEl = document.getElementById('p5-iv1-steps');
|
||
const next = document.getElementById('p5-iv1-next');
|
||
const rst = document.getElementById('p5-iv1-reset');
|
||
const fb = document.getElementById('p5-iv1-fb');
|
||
let step = 0;
|
||
const seen = new Set();
|
||
function show(){
|
||
const k = +sl.value; idx.textContent = k;
|
||
const t = T[k-1];
|
||
bEl.innerHTML = '$' + t.title + '$';
|
||
let out = '';
|
||
for(let i=0;i<step && i<t.steps.length;i++){
|
||
out += '<div style="padding:10px 14px;background:var(--card);border-radius:9px;margin-bottom:8px;border-left:3px solid var(--pri)"><div style="font-size:.85rem;color:var(--muted);margin-bottom:4px">Шаг '+(i+1)+': '+t.notes[i]+'</div><div style="text-align:center;font-size:1.05rem">$'+t.steps[i]+'$</div></div>';
|
||
}
|
||
stEl.innerHTML = out;
|
||
renderMath(bEl); renderMath(stEl);
|
||
if(step >= t.steps.length){
|
||
feedback(fb, true, '✓ Готово! Все шаги пройдены.');
|
||
seen.add(k);
|
||
if(seen.size === T.length && !seen.has('done')){ addXp(10,'p5-iv1'); bumpProgress('p5', 15); seen.add('done'); }
|
||
} else {
|
||
fb.style.display = 'none';
|
||
}
|
||
}
|
||
sl.addEventListener('input', ()=>{ step = 0; show(); });
|
||
next.addEventListener('click', ()=>{ const t = T[(+sl.value)-1]; if(step < t.steps.length){ step++; show(); } });
|
||
rst.addEventListener('click', ()=>{ step = 0; show(); });
|
||
show();
|
||
})();
|
||
|
||
/* IV2 — DnD: разложи числитель и знаменатель */
|
||
(function(){
|
||
const items = [
|
||
{ id:'s1', cat:'num', html:'$(x-2)(x+2)$' },
|
||
{ id:'s2', cat:'den', html:'$(x+2)^2$' },
|
||
{ id:'s3', cat:'trash', html:'$(x-2)^2$' },
|
||
{ id:'s4', cat:'trash', html:'$x^2-2$' },
|
||
{ id:'s5', cat:'num', html:'$(x+2)(x-2)$' },
|
||
{ id:'s6', cat:'trash', html:'$(x-4)(x+1)$' },
|
||
];
|
||
const sorter = setupSorter({
|
||
poolId:'p5-iv2-pool',
|
||
scopeSelector:'#p5-iv2',
|
||
items: items,
|
||
cats:['num','den'],
|
||
columnLayout:true,
|
||
});
|
||
let solved = false;
|
||
document.getElementById('p5-iv2-check').addEventListener('click', ()=>{
|
||
const fb = document.getElementById('p5-iv2-fb');
|
||
const result = document.getElementById('p5-iv2-result');
|
||
const placed = sorter.placed;
|
||
const inNum = items.filter(it => placed[it.id] === 'num');
|
||
const inDen = items.filter(it => placed[it.id] === 'den');
|
||
const trashPlaced = items.filter(it => it.cat === 'trash' && placed[it.id]);
|
||
if(inNum.length === 0 || inDen.length === 0){ feedback(fb, false, '✗ В каждый ящик нужно что-то положить.'); return; }
|
||
const numOk = inNum.every(it => it.cat === 'num');
|
||
const denOk = inDen.every(it => it.cat === 'den') && inDen.some(it => it.id === 's2');
|
||
if(numOk && denOk && trashPlaced.length === 0){
|
||
feedback(fb, true, '✓ Верно! Разложили правильно. +10 XP');
|
||
result.style.display = 'block';
|
||
result.innerHTML = 'После сокращения: $\\dfrac{x^2-4}{x^2+4x+4} = \\dfrac{(x-2)(x+2)}{(x+2)^2} = \\dfrac{x-2}{x+2}$';
|
||
renderMath(result);
|
||
if(!solved){ addXp(10,'p5-iv2'); bumpProgress('p5', 15); solved = true; }
|
||
} else if(trashPlaced.length > 0){
|
||
feedback(fb, false, '✗ Лишние карточки в ящиках — убери их в пул.');
|
||
} else {
|
||
feedback(fb, false, '✗ Не все разложения подходят. Подсказка: $x^2-4$ — разность квадратов, $x^2+4x+4 = (x+2)^2$.');
|
||
}
|
||
});
|
||
document.getElementById('p5-iv2-reset').addEventListener('click', ()=>{
|
||
sorter.reset();
|
||
document.getElementById('p5-iv2-fb').style.display = 'none';
|
||
document.getElementById('p5-iv2-result').style.display = 'none';
|
||
});
|
||
})();
|
||
|
||
/* IV3 — Найди ошибку */
|
||
(function(){
|
||
const Q = [
|
||
{ expr:'$\\dfrac{x+2}{x+3} = \\dfrac{2}{3}$', ok:false, why:'нельзя сокращать <b>слагаемые</b> — только общий <b>множитель</b>' },
|
||
{ expr:'$\\dfrac{(x+2)(x-1)}{(x+2)(x+3)} = \\dfrac{x-1}{x+3}$', ok:true, why:'сократили общий множитель $(x+2)$' },
|
||
{ expr:'$\\dfrac{x^2-9}{x-3} = x+3$', ok:true, why:'$\\dfrac{(x-3)(x+3)}{x-3} = x+3$' },
|
||
{ expr:'$\\dfrac{a^2+a}{a^2-a} = -1$', ok:false, why:'правильно $\\dfrac{a(a+1)}{a(a-1)} = \\dfrac{a+1}{a-1}$' },
|
||
{ expr:'$\\dfrac{2x-2}{x-1} = 2$', ok:true, why:'$\\dfrac{2(x-1)}{x-1} = 2$' },
|
||
{ expr:'$\\dfrac{x}{x+y} = \\dfrac{1}{1+y}$', ok:false, why:'нельзя сокращать $x$ со слагаемым $x$ в сумме $x+y$' },
|
||
];
|
||
let i = 0, score = 0;
|
||
function show(){
|
||
if(i >= Q.length){
|
||
document.getElementById('p5-iv3-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
|
||
if(score === Q.length){ addXp(15,'p5-iv3'); bumpProgress('p5', 25); }
|
||
else if(score >= Q.length - 1){ addXp(8,'p5-iv3'); bumpProgress('p5', 15); }
|
||
return;
|
||
}
|
||
document.getElementById('p5-iv3-i').textContent = (i+1);
|
||
document.getElementById('p5-iv3-s').textContent = score;
|
||
document.getElementById('p5-iv3-q').innerHTML = Q[i].expr;
|
||
renderMath(document.getElementById('p5-iv3-q'));
|
||
document.getElementById('p5-iv3-fb').style.display = 'none';
|
||
}
|
||
function answer(isOk){
|
||
if(i >= Q.length) return;
|
||
const fb = document.getElementById('p5-iv3-fb');
|
||
if(isOk === Q[i].ok){ score++; feedback(fb, true, '✓ Верно! '+Q[i].why+'. Дальше ▶'); }
|
||
else feedback(fb, false, '✗ Неверно. '+Q[i].why+'. Дальше ▶');
|
||
document.getElementById('p5-iv3-s').textContent = score;
|
||
i++;
|
||
setTimeout(show, 1500);
|
||
}
|
||
document.getElementById('p5-iv3-ok').addEventListener('click', ()=>answer(true));
|
||
document.getElementById('p5-iv3-err').addEventListener('click', ()=>answer(false));
|
||
show();
|
||
})();
|
||
|
||
/* IV4 — Тренажёр многошагового упрощения */
|
||
(function(){
|
||
const Q = [
|
||
{ q:'$\\dfrac{a-b}{a+b} + \\dfrac{2b}{a+b}$', ans:1, prompt:'значение =', hint:'$=\\dfrac{a-b+2b}{a+b} = \\dfrac{a+b}{a+b} = 1$', res:'1' },
|
||
{ q:'$\\dfrac{x^2-25}{x+5}$', ans:-5, prompt:'свободный член результата =', hint:'$=\\dfrac{(x-5)(x+5)}{x+5} = x-5$ — свободный член $-5$', res:'x-5' },
|
||
{ q:'$\\dfrac{2}{x-3} - \\dfrac{2}{x+3}$', ans:12, prompt:'числитель результата =', hint:'$=\\dfrac{2(x+3)-2(x-3)}{(x-3)(x+3)} = \\dfrac{12}{x^2-9}$', res:'\\dfrac{12}{x^2-9}' },
|
||
{ q:'$\\dfrac{a^2-1}{a+1} \\cdot \\dfrac{1}{a-1}$', ans:1, prompt:'значение =', hint:'$=\\dfrac{(a-1)(a+1)}{a+1} \\cdot \\dfrac{1}{a-1} = 1$', res:'1' },
|
||
{ q:'$\\dfrac{x}{x^2-1} : \\dfrac{x^2}{x-1}$', ans:-1, prompt:'сумма корней знаменателя $x(x+1)$ =', hint:'$=\\dfrac{x}{(x-1)(x+1)} \\cdot \\dfrac{x-1}{x^2} = \\dfrac{1}{x(x+1)}$. Корни $0$ и $-1$, сумма $-1$.', res:'\\dfrac{1}{x(x+1)}' },
|
||
];
|
||
let i = 0, score = 0;
|
||
function show(){
|
||
if(i >= Q.length){
|
||
document.getElementById('p5-iv4-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
|
||
document.getElementById('p5-iv4-prompt').textContent = 'ответ =';
|
||
if(score === Q.length){ addXp(15,'p5-iv4'); bumpProgress('p5', 25); }
|
||
else if(score >= 4){ addXp(8,'p5-iv4'); bumpProgress('p5', 15); }
|
||
return;
|
||
}
|
||
document.getElementById('p5-iv4-i').textContent = (i+1);
|
||
document.getElementById('p5-iv4-s').textContent = score;
|
||
document.getElementById('p5-iv4-q').innerHTML = 'Упрости: ' + Q[i].q;
|
||
document.getElementById('p5-iv4-prompt').textContent = Q[i].prompt;
|
||
document.getElementById('p5-iv4-ans').value = '';
|
||
renderMath(document.getElementById('p5-iv4-q'));
|
||
document.getElementById('p5-iv4-fb').style.display = 'none';
|
||
}
|
||
function go(){
|
||
if(i >= Q.length) return;
|
||
const fb = document.getElementById('p5-iv4-fb');
|
||
const ans = parseInt(document.getElementById('p5-iv4-ans').value, 10);
|
||
if(isNaN(ans)){ feedback(fb, false, '✗ Введи целое число.'); return; }
|
||
if(ans === Q[i].ans){ score++; feedback(fb, true, '✓ Верно! Ответ: $' + Q[i].res + '$ ('+Q[i].hint+'). Дальше ▶'); }
|
||
else feedback(fb, false, '✗ Неверно. Должно быть $' + Q[i].ans + '$. Ответ: $' + Q[i].res + '$ ('+Q[i].hint+'). Дальше ▶');
|
||
document.getElementById('p5-iv4-s').textContent = score;
|
||
i++;
|
||
setTimeout(show, 1600);
|
||
}
|
||
document.getElementById('p5-iv4-go').addEventListener('click', go);
|
||
document.getElementById('p5-iv4-ans').addEventListener('keydown', e=>{ if(e.key === 'Enter') go(); });
|
||
document.getElementById('p5-iv4-start').addEventListener('click', ()=>{ i=0; score=0; show(); });
|
||
show();
|
||
})();
|
||
|
||
wireReadBtn('p5');
|
||
}
|
||
|
||
function buildFinal1(){
|
||
const box = document.getElementById('final1-body');
|
||
let html = '';
|
||
|
||
/* Часть А — Шпаргалка главы (5 mini-карточек) */
|
||
html += `<div class="card">
|
||
<div class="card-header">
|
||
<span class="card-icon theory">${ICONS.theory}</span>
|
||
<span class="card-title">Шпаргалка главы 1</span>
|
||
<span class="card-num">Итог</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<p>Все ключевые правила главы — в одном месте. Просмотри перед боссами!</p>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-top:10px">
|
||
<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
|
||
<div style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);margin-bottom:6px;font-size:.92rem">§ 1 · ОДЗ</div>
|
||
<div style="font-size:.95rem">$\\dfrac{P(x)}{Q(x)},\\ Q(x) \\ne 0$. Решаем $Q(x)=0$ и исключаем корни.</div>
|
||
</div>
|
||
<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
|
||
<div style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);margin-bottom:6px;font-size:.92rem">§ 2 · Сокращение</div>
|
||
<div style="font-size:.95rem">$\\dfrac{AC}{BC} = \\dfrac{A}{B}$. Только общий <b>множитель</b>, не слагаемое!</div>
|
||
</div>
|
||
<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
|
||
<div style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);margin-bottom:6px;font-size:.92rem">§ 3 · Сложение</div>
|
||
<div style="font-size:.95rem">Одинаковые знам. — числители $\\pm$. Разные — приводим к НОЗ.</div>
|
||
</div>
|
||
<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
|
||
<div style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);margin-bottom:6px;font-size:.92rem">§ 4 · Умножение и деление</div>
|
||
<div style="font-size:.95rem">$\\dfrac{A}{B} \\cdot \\dfrac{C}{D} = \\dfrac{AC}{BD}$. Деление — умножение на обратную.</div>
|
||
</div>
|
||
<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
|
||
<div style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);margin-bottom:6px;font-size:.92rem">§ 5 · Преобразование</div>
|
||
<div style="font-size:.95rem">Разложи → скобки → $\\cdot$ и $:$ слева направо → сократи.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
/* Часть Б — 5 боссов */
|
||
html += `<div class="card">
|
||
<div class="card-header">
|
||
<span class="card-icon rule">${ICONS.rule}</span>
|
||
<span class="card-title">Боссы главы 1</span>
|
||
<span class="card-num">5</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<p>5 интегрированных задач. Каждая комбинирует несколько тем. За каждого побеждённого босса — <b>+10 XP</b>. Победишь всех — <b>+50 XP бонус</b> и ачивка «Магистр рациональных дробей»!</p>
|
||
</div>
|
||
</div>`;
|
||
|
||
html += '<div id="ch1-bosses-container"></div>';
|
||
|
||
html += `<div style="margin-top:18px;padding:18px 20px;background:linear-gradient(135deg,var(--pri-soft),var(--sec-acc-soft));border-radius:14px;border:1.5px solid var(--pri);text-align:center" id="ch1-final-summary">
|
||
<div style="font-family:'Unbounded',sans-serif;font-weight:800;color:var(--pri2);font-size:1.1rem;margin-bottom:6px">Прогресс по боссам</div>
|
||
<div id="ch1-boss-overall" style="font-size:.95rem;color:var(--text);margin-bottom:10px">0 / 5 боссов побеждено</div>
|
||
<div style="height:12px;background:var(--card);border-radius:8px;overflow:hidden;border:1px solid var(--border)">
|
||
<div id="ch1-boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,#0891b2,#22d3ee);transition:width .35s"></div>
|
||
</div>
|
||
<div id="ch1-final-reward" style="margin-top:14px;display:none;padding:14px;background:var(--card);border-radius:11px;border:2px solid #f59e0b">
|
||
<div style="font-family:'Unbounded',sans-serif;font-weight:800;color:#92400e;font-size:1.05rem;margin-bottom:6px">Магистр рациональных дробей</div>
|
||
<div style="font-size:.92rem;margin-bottom:10px">Глава 1 пройдена! Все 5 боссов повержены. +50 XP бонус.</div>
|
||
<a class="btn primary" href="/textbook/algebra-9-ch2" style="text-decoration:none">Дальше: Глава 2 <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>
|
||
</div>
|
||
</div>`;
|
||
|
||
html += secNav('p5', null);
|
||
|
||
box.innerHTML = html;
|
||
renderMath(box);
|
||
|
||
/* Боссы */
|
||
const BOSSES = [
|
||
{
|
||
n:1, color:'#10b981',
|
||
title:'Огр Сокращения',
|
||
tag:'§ 1 + § 2',
|
||
q:'Найди ОДЗ и сократи: $\\dfrac{x^2-16}{x^2-x-12}$. После сокращения получится $\\dfrac{x+4}{x+?}$. Введи число в знаменателе.',
|
||
ans:3,
|
||
hint:'$x^2-16 = (x-4)(x+4)$; $x^2-x-12 = (x-4)(x+3)$. Сократи $(x-4)$ — останется $\\dfrac{x+4}{x+3}$.'
|
||
},
|
||
{
|
||
n:2, color:'#0891b2',
|
||
title:'Дракон Сложения',
|
||
tag:'§ 3 + § 2',
|
||
q:'Упрости: $\\dfrac{1}{x-2} + \\dfrac{1}{x+2} - \\dfrac{4}{x^2-4}$. Результат имеет вид $\\dfrac{?}{x+2}$. Введи числитель.',
|
||
ans:2,
|
||
hint:'Общий знаменатель $x^2-4 = (x-2)(x+2)$. Числитель: $(x+2)+(x-2)-4 = 2x-4 = 2(x-2)$. После сокращения $(x-2)$: $\\dfrac{2}{x+2}$.'
|
||
},
|
||
{
|
||
n:3, color:'#7c3aed',
|
||
title:'Гидра Умножения',
|
||
tag:'§ 4 + § 5',
|
||
q:'Упрости: $\\dfrac{x^2-9}{x+5} \\cdot \\dfrac{x^2+10x+25}{x-3}$. Результат — квадратный трёхчлен $x^2+\\Box x+15$. Введи коэффициент при $x$.',
|
||
ans:8,
|
||
hint:'$(x^2-9)(x+5)^2 / ((x+5)(x-3)) = (x-3)(x+3) \\cdot (x+5)/(x-3) = (x+3)(x+5) = x^2+8x+15$.'
|
||
},
|
||
{
|
||
n:4, color:'#dc2626',
|
||
title:'Тёмный Лорд Деления',
|
||
tag:'§ 4 + § 1',
|
||
q:'Реши: $\\dfrac{x+1}{x^2-9} : \\dfrac{x+1}{x-3}$. Результат имеет вид $\\dfrac{1}{x+?}$. Введи число.',
|
||
ans:3,
|
||
hint:'ОДЗ: $x \\ne \\pm 3,\\ x \\ne -1$. $\\dfrac{x+1}{(x-3)(x+3)} \\cdot \\dfrac{x-3}{x+1} = \\dfrac{1}{x+3}$.'
|
||
},
|
||
{
|
||
n:5, color:'#f59e0b',
|
||
title:'Мастер Синтеза',
|
||
tag:'§ 5 — итог',
|
||
q:'Найди значение: $\\left(\\dfrac{1}{a-b} - \\dfrac{1}{a+b}\\right) : \\dfrac{2b}{a^2-b^2}$ при $a=5,\\ b=3$. Введи число.',
|
||
ans:1,
|
||
hint:'$\\dfrac{(a+b)-(a-b)}{(a-b)(a+b)} : \\dfrac{2b}{a^2-b^2} = \\dfrac{2b}{a^2-b^2} \\cdot \\dfrac{a^2-b^2}{2b} = 1$ — независимо от $a, b$.'
|
||
},
|
||
];
|
||
|
||
const cont = document.getElementById('ch1-bosses-container');
|
||
const STATE_KEY = 'algebra9_ch1_bosses';
|
||
const BOSS_STATE = (function(){
|
||
try{ const s = localStorage.getItem(STATE_KEY); if(s) return JSON.parse(s); }catch(e){}
|
||
return BOSSES.map(()=>({defeated:false}));
|
||
})();
|
||
function saveBosses(){ try{ localStorage.setItem(STATE_KEY, JSON.stringify(BOSS_STATE)); }catch(e){} }
|
||
|
||
cont.innerHTML = BOSSES.map((b, idx)=>{
|
||
return '<div class="boss-card" id="boss-'+b.n+'-card" style="padding:16px;background:var(--card);border-radius:12px;border:2px solid '+b.color+';margin-bottom:14px">'
|
||
+'<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap">'
|
||
+'<svg viewBox="0 0 24 24" fill="none" stroke="'+b.color+'" stroke-width="2.2" style="width:28px;height:28px;flex-shrink:0"><polygon points="12,2 22,20 2,20"/></svg>'
|
||
+'<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:'+b.color+';font-size:1.05rem">Босс '+b.n+': '+b.title+'</div>'
|
||
+'<div style="margin-left:auto;font-size:.78rem;color:var(--muted);padding:3px 8px;background:var(--sec-acc-soft);border-radius:6px">'+b.tag+'</div>'
|
||
+'</div>'
|
||
+'<div id="boss-'+b.n+'-q" style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:1rem;line-height:1.5;margin-bottom:10px">'+b.q+'</div>'
|
||
+'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
|
||
+'<span style="font-family:\'JetBrains Mono\',monospace;font-size:.92rem">ответ =</span>'
|
||
+'<input type="number" id="boss-'+b.n+'-ans" class="tinp" style="width:120px;text-align:center" placeholder="число">'
|
||
+'<button class="btn primary" id="boss-'+b.n+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атаковать</button>'
|
||
+'<button class="btn" id="boss-'+b.n+'-hint">Подсказка</button>'
|
||
+'</div>'
|
||
+'<div class="feedback" id="boss-'+b.n+'-fb"></div>'
|
||
+'</div>';
|
||
}).join('');
|
||
renderMath(cont);
|
||
|
||
function refreshOverall(){
|
||
const won = BOSS_STATE.filter(s => s.defeated).length;
|
||
const txt = document.getElementById('ch1-boss-overall');
|
||
const fill = document.getElementById('ch1-boss-overall-fill');
|
||
if(txt) txt.textContent = won + ' / ' + BOSSES.length + ' боссов побеждено';
|
||
if(fill) fill.style.width = (won * 100 / BOSSES.length) + '%';
|
||
if(won >= BOSSES.length){
|
||
const reward = document.getElementById('ch1-final-reward');
|
||
if(reward && reward.style.display === 'none'){
|
||
reward.style.display = 'block';
|
||
if(!STATE.achievements.has('ch1_done')){
|
||
achievement('ch1_done','Магистр рациональных дробей');
|
||
addXp(50, 'ch1-bonus');
|
||
bumpProgress('final1', 30);
|
||
if(window.confetti){ try{ confetti(); }catch(e){} }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
BOSSES.forEach((b, idx)=>{
|
||
const card = document.getElementById('boss-'+b.n+'-card');
|
||
const goBtn = document.getElementById('boss-'+b.n+'-go');
|
||
const hintBtn = document.getElementById('boss-'+b.n+'-hint');
|
||
const ansInp = document.getElementById('boss-'+b.n+'-ans');
|
||
if(BOSS_STATE[idx].defeated){
|
||
card.style.background = 'linear-gradient(135deg,var(--sec-acc-soft),var(--pri-soft))';
|
||
goBtn.disabled = true; goBtn.style.opacity = .55; goBtn.textContent = '✓ Повержен';
|
||
ansInp.disabled = true;
|
||
}
|
||
goBtn.addEventListener('click', ()=>{
|
||
if(BOSS_STATE[idx].defeated) return;
|
||
const fb = document.getElementById('boss-'+b.n+'-fb');
|
||
const val = parseInt(ansInp.value, 10);
|
||
if(isNaN(val)){ feedback(fb, false, '✗ Введи целое число.'); return; }
|
||
if(val === b.ans){
|
||
BOSS_STATE[idx].defeated = true; saveBosses();
|
||
feedback(fb, true, '✓ Босс '+b.n+' повержен! +10 XP. '+b.hint);
|
||
addXp(10, 'boss-ch1-'+b.n);
|
||
bumpProgress('final1', 18);
|
||
goBtn.disabled = true; goBtn.style.opacity = .55; goBtn.textContent = '✓ Повержен';
|
||
ansInp.disabled = true;
|
||
card.style.background = 'linear-gradient(135deg,var(--sec-acc-soft),var(--pri-soft))';
|
||
refreshOverall();
|
||
} else {
|
||
feedback(fb, false, '✗ Промах. Попробуй ещё. Подсказка доступна.');
|
||
}
|
||
});
|
||
hintBtn.addEventListener('click', ()=>{
|
||
const fb = document.getElementById('boss-'+b.n+'-fb');
|
||
fb.className = 'feedback ok';
|
||
fb.innerHTML = '<b>Подсказка:</b> '+b.hint;
|
||
fb.style.display = 'block';
|
||
fb.style.background = 'var(--warn-bg)';
|
||
fb.style.color = '#92400e';
|
||
fb.style.borderLeftColor = 'var(--warn)';
|
||
renderMath(fb);
|
||
});
|
||
ansInp.addEventListener('keydown', e=>{ if(e.key === 'Enter') goBtn.click(); });
|
||
});
|
||
|
||
refreshOverall();
|
||
}
|
||
|
||
/* ===== Search ===== */
|
||
const SEARCH_INDEX = (function(){
|
||
const arr=[];
|
||
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
|
||
return arr;
|
||
})();
|
||
function initSearch(){
|
||
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
|
||
if(!modal||!inp||!out) return;
|
||
let cur=0,rows=[];
|
||
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
|
||
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
|
||
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'…':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
|
||
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
|
||
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
|
||
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
|
||
function close(){ modal.classList.remove('show'); }
|
||
btn&&btn.addEventListener('click',open);
|
||
modal.addEventListener('click',e=>{if(e.target===modal)close();});
|
||
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
|
||
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
|
||
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||
}
|
||
|
||
function initSidebarToggle(){
|
||
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
|
||
if(!side||!btn) return;
|
||
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
|
||
back.addEventListener('click',close);
|
||
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
|
||
}
|
||
|
||
function init(){
|
||
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo('p1');
|
||
setTimeout(()=>achievement('start'), 600);
|
||
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);
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|