Files
match/index.html

2490 lines
183 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>🏆 #1 世界冠军数学赛道 — Max Verstappen</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;700;900&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Noto Sans SC','Inter',sans-serif;
background:#05050a;
min-height:100vh;
color:#c8c8d0;
overflow-x:hidden;
}
.app {
max-width:820px; margin:0 auto; padding:20px;
position:relative;
}
/* === HUD === */
.hud {
background:rgba(8,8,20,0.85); border:1px solid rgba(225,6,0,0.12);
padding:10px 18px; display:flex; align-items:center; gap:12px; margin-bottom:18px;
backdrop-filter:blur(8px); border-radius:4px;
}
.hud .logo { font-weight:900; font-size:15px; letter-spacing:2px; }
.hud .logo .r { color:#e10600; }
.hud .logo .y { color:#ffd700; }
.hud .logo .w { color:rgba(255,255,255,0.85); }
.hud-spacer { flex:1; }
.hud-stat { text-align:center; padding:0 10px; border-right:1px solid rgba(255,255,255,0.03); }
.hud-stat:last-child { border-right:none; }
.hud-stat .lbl { font-size:8px; color:rgba(255,255,255,0.4); letter-spacing:2px; font-weight:700; }
.hud-stat .val { font-size:18px; font-weight:900; font-family:'Inter',monospace; }
.hud-stat .val.r { color:#e10600; }
.hud-stat .val.y { color:#ffd700; }
.mbtn {
width:28px; height:28px; border:1px solid rgba(255,255,255,0.08); background:transparent;
color:rgba(255,255,255,0.4); font-size:12px; cursor:pointer; transition:all 0.3s;
display:flex; align-items:center; justify-content:center; flex-shrink:0; border-radius:2px;
}
.mbtn:hover { border-color:rgba(255,255,255,0.15); color:rgba(255,255,255,0.7); }
.mbtn.on { color:#ffd700; border-color:rgba(255,215,0,0.2); background:rgba(255,215,0,0.04); }
/* === pages === */
.pg { display:none; }
.pg.act { display:block; animation:fIn 0.35s ease; }
@keyframes fIn { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
@keyframes shake { 0%,100%{transform:translateX(0)} 20%{transform:translateX(-6px)} 40%{transform:translateX(6px)} 60%{transform:translateX(-4px)} 80%{transform:translateX(4px)} }
@keyframes pop { 0%{opacity:0;transform:scale(0.8) translateY(16px)} 100%{opacity:1;transform:scale(1) translateY(0)} }
@keyframes pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.05)} }
@keyframes slideUp { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} }
@keyframes glow { 0%,100%{box-shadow:0 0 4px rgba(255,215,0,0.1)} 50%{box-shadow:0 0 16px rgba(255,215,0,0.3)} }
@keyframes starPop { 0%{opacity:0;transform:scale(0) rotate(-180deg)} 60%{transform:scale(1.3) rotate(10deg)} 100%{opacity:1;transform:scale(1) rotate(0deg)} }
@keyframes bounceIn { 0%{opacity:0;transform:scale(0.3)} 50%{transform:scale(1.08)} 70%{transform:scale(0.95)} 100%{opacity:1;transform:scale(1)} }
/* ===== TRACK MAP ===== */
.track-wrap {
position:relative; width:100%; aspect-ratio:1/1.5; max-height:620px;
margin:0 auto 12px;
}
.track-wrap svg { width:100%; height:100%; display:block; }
.track-road { fill:none; stroke:rgba(255,255,255,0.03); stroke-width:16; stroke-linecap:round; }
.track-bg { fill:none; stroke:rgba(255,255,255,0.08); stroke-width:3; stroke-linecap:round; }
.track-fill { fill:none; stroke:rgba(255,255,255,0.25); stroke-width:1.5; stroke-dasharray:6,8; stroke-linecap:round; }
.track-done { fill:none; stroke:#ffd700; stroke-width:3; stroke-linecap:round; filter:drop-shadow(0 0 10px rgba(255,215,0,0.3)); }
.cp {
cursor:pointer; transition:all 0.3s;
}
.cp circle:first-of-type { pointer-events:all; cursor:pointer; }
.cp:hover circle { r:20; }
.cp.locked { opacity:0.25; }
.cp.locked circle:last-of-type { stroke:rgba(255,255,255,0.06); fill:rgba(255,255,255,0.02); }
.cp.locked + text { fill:rgba(255,255,255,0.06); }
.cp.lock-icon { font-size:10px; fill:rgba(255,255,255,0.25); text-anchor:middle; font-family:'Inter',monospace; }
.cp.done circle { fill:#ffd700; stroke:#ffd700; }
.cp-label {
font-size:10px; font-weight:900; fill:rgba(255,255,255,0.65); letter-spacing:1px;
transition:all 0.3s; text-anchor:middle; font-family:'Inter',monospace;
}
.cp.done .cp-label { fill:#ffd700; }
.cp:not(.locked):not(.done):hover .cp-label { fill:#fff; }
.cp-stars { font-size:9px; text-anchor:middle; fill:rgba(255,255,255,0.5); }
.cp-name {
font-size:10px; font-weight:700; fill:rgba(255,255,255,0.35);
text-anchor:middle; letter-spacing:1px;
}
/* F1 car on track */
.track-car {
position:absolute; width:66px; height:30px;
pointer-events:none;
filter:drop-shadow(0 0 10px rgba(225,6,0,0.35));
z-index:10;
}
.track-car .ci { width:100%; height:100%; object-fit:contain; display:block; }
.track-car .cs { width:100%; height:100%; display:block; overflow:visible; }
.track-car .cs.hide { display:none; }
.track-car.moving {
filter:drop-shadow(0 0 18px rgba(255,215,0,0.5));
}
.mv1-photo {
width:80px; height:80px; border-radius:50%; object-fit:cover;
border:2px solid #ffd700; margin:0 auto 8px; display:block;
box-shadow:0 0 20px rgba(255,215,0,0.1);
}
.mv1-photo.hide { display:none; }
.mv1-av {
width:80px; height:80px; border-radius:50%; margin:0 auto 8px;
background:linear-gradient(135deg,#e10600,#aa0000);
display:flex; align-items:center; justify-content:center;
font-size:28px; font-weight:900; color:#fff; font-family:'Inter',monospace;
border:2px solid rgba(255,215,0,0.2);
}
.mv1-av.hide { display:none; }
/* Start/Finish line */
.sf { fill:#ffd700; opacity:0.15; }
/* === Chapter view === */
.ch { margin-bottom:16px; }
.ch .gl { font-size:12px; font-weight:700; color:#e10600; letter-spacing:2px; font-family:'Inter',monospace; }
.ch h2 { font-size:24px; font-weight:900; color:#fff; margin:2px 0; }
.ch .sm { font-size:13px; color:rgba(255,255,255,0.45); letter-spacing:1px; font-weight:700; }
/* === Buttons === */
.bk {
background:rgba(8,8,20,0.6); border:1px solid rgba(255,255,255,0.06);
color:rgba(255,255,255,0.6); padding:9px 16px; cursor:pointer; font-size:14px; font-family:inherit;
transition:all 0.25s; margin-bottom:16px; font-weight:700; letter-spacing:1px; border-radius:3px;
}
.bk:hover { background:rgba(8,8,20,0.6); color:rgba(255,255,255,0.8); border-color:rgba(225,6,0,0.2); }
.act {
display:none; margin-top:10px; padding:10px 28px; border:none;
font-size:14px; font-weight:700; font-family:'Inter',monospace; cursor:pointer; letter-spacing:1px;
background:#e10600; color:#fff; transition:all 0.3s; border-radius:3px;
}
.act.show { display:inline-block; }
.act:hover { background:#aa0000; transform:translateY(-1px); box-shadow:0 4px 12px rgba(225,6,0,0.2); }
.act:active { transform:translateY(0); }
/* === Panels === */
.pn {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
padding:20px; margin-bottom:8px; border-radius:2px;
}
.pn h3 { font-size:18px; font-weight:900; color:#fff; margin-bottom:10px; letter-spacing:-0.3px; }
.kpTitle { font-size:20px; font-weight:900; color:#ffd700; margin-bottom:14px; letter-spacing:0; border-bottom:1px solid rgba(255,215,0,0.08); padding-bottom:10px; animation:slideUp 0.4s ease; }
.pn p { font-size:15px; line-height:2; color:rgba(255,255,255,0.7); }
.co {
background:linear-gradient(135deg,rgba(225,6,0,0.03),rgba(8,8,20,0.6));
border-left:3px solid #e10600; padding:12px 16px; margin:12px 0; position:relative;
}
.co::before {
content:'PACE NOTE'; position:absolute; top:-7px; right:8px;
font-size:7px; font-weight:900; color:rgba(225,6,0,0.35); letter-spacing:2px;
}
.co p { color:rgba(255,255,255,0.75); font-size:15px; line-height:1.8; }
.co strong { color:#ffd700; }
.qp {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
border-left:3px solid #ffd700; padding:20px; margin-bottom:8px; border-radius:2px;
}
.qp .qn { font-size:11px; color:#ffd700; font-weight:700; letter-spacing:2px; margin-bottom:6px; font-family:'Inter',monospace; }
.qp .qt { font-size:18px; font-weight:700; color:#fff; margin-bottom:16px; line-height:1.7; }
.qo { display:flex; flex-direction:column; gap:5px; }
.qo .opt {
padding:12px 16px; border:1px solid rgba(255,255,255,0.04);
cursor:pointer; font-size:15px; font-family:inherit; transition:all 0.2s;
background:rgba(255,255,255,0.02); text-align:left; color:rgba(255,255,255,0.75);
display:flex; align-items:center; gap:10px;
border-left:3px solid transparent; border-radius:3px;
}
.qo .opt .lb {
width:28px; height:28px; display:flex; align-items:center; justify-content:center;
font-weight:900; font-size:13px; background:rgba(255,255,255,0.04); color:rgba(255,255,255,0.3);
flex-shrink:0; font-family:'Inter',monospace; border-radius:3px;
}
.qo .opt:hover { border-color:rgba(255,215,0,0.08); background:rgba(255,215,0,0.02); border-left-color:rgba(255,215,0,0.2); }
.qo .opt:hover .lb { background:rgba(255,215,0,0.08); color:#ffd700; }
.qo .opt.fail { border-color:rgba(225,6,0,0.1); background:rgba(225,6,0,0.03); border-left-color:#e10600; animation:shake 0.4s ease; }
.qo .opt.fail .lb { background:#e10600; color:#fff; }
.qo .opt.pass { border-color:rgba(46,204,113,0.1); background:rgba(46,204,113,0.03); border-left-color:#2ecc71; animation:pulse 0.3s ease; }
.qo .opt.pass .lb { background:#2ecc71; color:#fff; }
.qo .opt.disabled { pointer-events:none; opacity:0.3; }
.qfb { padding:14px 18px; margin-top:10px; font-size:15px; font-weight:700; display:none; line-height:1.8; }
.qfb.show { display:block; }
.qfb.pass { background:rgba(46,204,113,0.05); color:#2ecc71; border:1px solid rgba(46,204,113,0.06); border-left:3px solid #2ecc71; }
.qfb.fail { background:rgba(225,6,0,0.05); color:rgba(255,255,255,0.85); border:1px solid rgba(225,6,0,0.06); border-left:3px solid #e10600; }
.rv {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
border-left:3px solid rgba(255,215,0,0.15); padding:20px; margin-bottom:8px; text-align:center; border-radius:2px;
}
.rv h3 { color:#fff; margin-bottom:16px; font-size:14px; font-weight:700; }
.rline {
position:relative; height:3px;
background:repeating-linear-gradient(90deg,#e10600 0,#e10600 6px,transparent 6px,transparent 12px);
margin:28px 10px 12px;
}
.rline .mk { position:absolute; top:-6px; transform:translateX(-50%); font-size:10px; font-weight:700; color:rgba(255,255,255,0.1); font-family:'Inter',monospace; }
.rline .mk.z { color:#ffd700; }
.rline .mk::before { content:''; position:absolute; top:14px; left:50%; width:1px; height:5px; background:rgba(255,255,255,0.04); }
.car { position:absolute; font-size:20px; top:-24px; transform:translateX(-50%); transition:left 0.5s cubic-bezier(.34,1.56,.64,1); }
.car .vl { position:absolute; top:-14px; left:50%; transform:translateX(-50%); font-size:9px; font-weight:700; white-space:nowrap; color:#fff; background:rgba(225,6,0,0.5); padding:1px 5px; font-family:'Inter',monospace; }
/* ===== PODIUM ===== */
.po {
display:none; position:fixed; inset:0; background:rgba(5,5,10,0.88); backdrop-filter:blur(14px);
z-index:100; align-items:center; justify-content:center;
}
.po.show { display:flex; }
.pm {
background:#0a0a1a; border:1px solid rgba(225,6,0,0.1); border-radius:4px;
padding:40px 32px 28px; text-align:center; max-width:360px; width:90%;
animation:pop 0.5s cubic-bezier(.34,1.56,.64,1);
position:relative; overflow:hidden;
}
.pm::before {
content:''; position:absolute; top:0; left:0; right:0; height:2px;
background:linear-gradient(90deg,#e10600,#ffd700,#e10600);
}
.pm::after {
content:'\u{1F3C6}'; position:absolute; top:-20px; right:-20px; font-size:100px;
opacity:0.03; transform:rotate(15deg);
}
@keyframes pop { from{opacity:0;transform:scale(0.9) translateY(16px)} to{opacity:1;transform:scale(1) translateY(0)} }
.pm .pn1 { font-size:56px; font-weight:900; font-family:'Inter',monospace; color:#ffd700; line-height:1; text-shadow:0 0 30px rgba(255,215,0,0.15); }
.pm .pcup { font-size:50px; display:block; margin:2px 0; }
.pm h2 { font-size:24px; font-weight:900; color:#fff; letter-spacing:1px; }
.pm .ps { font-size:18px; margin:8px 0 4px; letter-spacing:5px; }
.pm .pxp { color:#ffd700; font-weight:900; font-size:16px; margin:2px 0 6px; font-family:'Inter',monospace; }
.pm .pms { color:rgba(255,255,255,0.4); font-size:12px; letter-spacing:1px; line-height:1.5; }
.pm .pmQuote { font-size:11px; color:rgba(255,255,255,0.2); font-style:italic; margin:2px 0; letter-spacing:0.5px; line-height:1.4; }
.pm .next-track {
margin:16px auto 0; padding:8px 20px; border:none; background:#e10600; color:#fff;
font-size:12px; font-weight:700; font-family:'Inter',monospace; cursor:pointer;
transition:all 0.3s; letter-spacing:1px; border-radius:2px;
}
.pm .next-track:hover { background:#aa0000; }
/* ===== Example / Step-through ===== */
.eg-box {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
border-left:3px solid #e10600; padding:20px; margin-bottom:8px; border-radius:2px;
position:relative;
}
.eg-box::before {
content:'EXAMPLE'; position:absolute; top:-7px; right:8px;
font-size:7px; font-weight:900; color:rgba(225,6,0,0.35); letter-spacing:2px;
}
.eg-title {
font-size:18px; font-weight:900; color:#fff; margin-bottom:14px;
letter-spacing:-0.3px; padding-bottom:10px;
border-bottom:1px solid rgba(255,255,255,0.03);
}
.eg-step {
padding:12px 16px; margin-bottom:8px; border-radius:2px;
border-left:3px solid transparent;
font-size:15px; line-height:1.8; color:rgba(255,255,255,0.7);
background:rgba(255,255,255,0.01);
animation:fIn 0.4s ease;
}
.eg-step.show { border-left-color:rgba(225,6,0,0.2); background:rgba(225,6,0,0.02); color:rgba(255,255,255,0.7); }
.eg-step .step-num {
display:inline-block; width:24px; height:24px; line-height:24px; text-align:center;
font-size:11px; font-weight:900; background:rgba(225,6,0,0.1); color:#e10600;
margin-right:10px; border-radius:2px; font-family:'Inter',monospace; float:left;
}
.eg-btn {
margin:14px 0 0; padding:10px 24px; border:1px solid rgba(225,6,0,0.15);
background:rgba(225,6,0,0.04); color:rgba(255,255,255,0.7); font-size:14px;
font-weight:700; font-family:'Inter',monospace; cursor:pointer;
transition:all 0.3s; border-radius:3px; letter-spacing:1px;
}
.eg-btn:hover { background:rgba(225,6,0,0.1); color:#fff; border-color:rgba(225,6,0,0.3); }
.eg-btn:disabled { opacity:0.3; cursor:default; }
/* ===== CONFETTI ===== */
.pyro { position:fixed; inset:0; pointer-events:none; z-index:99; overflow:hidden; }
.pyro .sp {
position:absolute; width:3px; height:3px; border-radius:0;
animation:spFall linear forwards;
}
@keyframes spFall { 0%{transform:translateY(-10px) scale(1);opacity:1} 100%{transform:translateY(100vh) scale(0);opacity:0} }
/* ===== F1 TEAM RADIO ===== */
.radio {
display:inline-flex; align-items:center; gap:8px;
padding:6px 14px; margin:2px 0; border-radius:3px;
font-size:12px; font-weight:700; letter-spacing:0.5px;
animation:squawk 0.3s cubic-bezier(.34,1.56,.64,1);
}
.radio.pass { background:rgba(46,204,113,0.08); border-left:3px solid #2ecc71; color:#2ecc71; }
.radio.fail { background:rgba(225,6,0,0.05); border-left:3px solid #e10600; color:#e10600; }
.radio.streak { background:rgba(255,215,0,0.08); border-left:3px solid #ffd700; color:#ffd700; }
.radio .rbl { font-size:16px; }
.radio .rbl.s1{animation:blip 0.4s ease 3;}
.radio .rbl.s2{animation:blip 0.3s ease 3;}
.hudExtra { display:flex; gap:10px; justify-content:center; margin-bottom:6px; min-height:22px; }
.streakDisplay { font-size:11px; display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:2px; }
.streakDisplay.on { background:rgba(255,215,0,0.06); border:1px solid rgba(255,215,0,0.1); color:#ffd700; animation:pop 0.3s ease; }
.lapCounter { font-size:10px; color:rgba(255,255,255,0.2); display:inline-flex; align-items:center; gap:4px; font-family:'Inter',monospace; padding:3px 8px; }
@keyframes squawk { 0%{opacity:0;transform:scale(0.92) translateX(-8px)} 100%{opacity:1;transform:scale(1) translateX(0)} }
@keyframes blip { 0%,100%{opacity:1} 50%{opacity:0.2} }
.checker {
position:absolute; inset:0; pointer-events:none; overflow:hidden; opacity:0.04;
background:repeating-conic-gradient(#fff 0% 25%,#000 0% 50%) 0 0 / 20px 20px;
animation:scrollBg 8s linear infinite;
}
@keyframes scrollBg { 0%{background-position:0 0} 100%{background-position:40px 40px} }
/* ===== RESPONSIVE ===== */
@media (max-width:600px) {
.app { padding:12px; }
.track-wrap { max-height:460px; }
.pm { padding:28px 20px 24px; }
.pm .pn1 { font-size:42px; }
}
/* === Track car move animation === */
.track-car.moving { animation:boost 1.2s ease; }
@keyframes boost {
0% { filter:drop-shadow(0 0 8px rgba(225,6,0,0.4)); }
40% { filter:drop-shadow(0 0 20px rgba(255,215,0,0.6)); }
100% { filter:drop-shadow(0 0 8px rgba(225,6,0,0.4)); }
}
/* 错题集 UI */
.hudBtn {
background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.06);
color:rgba(255,255,255,0.5); font-size:11px; font-family:inherit; font-weight:700;
padding:8px 14px; cursor:pointer; letter-spacing:1px; border-radius:3px;
transition:all 0.2s; display:inline-flex; align-items:center; gap:4px;
}
.hudBtn:hover { background:rgba(255,255,255,0.06); color:rgba(255,255,255,0.8); }
.hudBtn #hintBadge {
background:#e10600; color:#fff; font-size:9px; font-weight:900;
min-width:18px; height:18px; line-height:18px; text-align:center; border-radius:9px;
padding:0 5px; display:inline-block;
}
.hudDanger:hover { border-color:rgba(225,6,0,0.2); color:#e10600; }
.wrItem {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
border-left:3px solid #e10600; padding:14px 16px; margin-bottom:8px; border-radius:2px;
}
.wrH { font-size:10px; color:rgba(255,255,255,0.35); letter-spacing:1px; margin-bottom:6px; font-family:'Inter',monospace; }
.wrLb { color:rgba(255,255,255,0.45); }
.wrQ { font-size:15px; color:rgba(255,255,255,0.75); margin-bottom:10px; line-height:1.5; }
.wrAct { display:flex; gap:8px; }
.wrDo, .wrRm {
background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.08);
color:rgba(255,255,255,0.5); font-size:10px; font-family:inherit; font-weight:700;
padding:6px 12px; cursor:pointer; letter-spacing:1px; border-radius:3px;
transition:all 0.2s;
}
.wrDo:hover { background:rgba(46,204,113,0.04); color:#2ecc71; border-color:rgba(46,204,113,0.1); }
.wrRm:hover { background:rgba(225,6,0,0.04); color:#e10600; border-color:rgba(225,6,0,0.1); }
.geoDiag { margin-bottom:10px; }
.geoDiag svg { display:block; max-height:160px; }
/* Answer map grid */
.mBtn2 {
padding:10px 20px; border:1px solid; font-size:13px; font-weight:700;
font-family:'Inter',monospace; cursor:pointer; border-radius:3px;
letter-spacing:1px; transition:all 0.2s; color:#fff;
}
.mBtn2:hover { filter:brightness(1.2); transform:translateY(-1px); }
.qGrid {
display:grid; grid-template-columns:repeat(auto-fill,minmax(64px,1fr));
gap:8px; padding:4px 0;
}
.qCell {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.04);
border-left:3px solid rgba(255,255,255,0.06); border-radius:3px;
padding:10px 4px; text-align:center; cursor:pointer;
transition:all 0.2s; display:flex; flex-direction:column; align-items:center; gap:4px;
}
.qCell:hover { background:rgba(255,255,255,0.04); border-color:rgba(255,255,255,0.1); }
.qCell .qNum { font-size:14px; font-weight:700; color:rgba(255,255,255,0.4); font-family:'Inter',monospace; }
.qCell .qSt { font-size:12px; }
.qCell.correct { border-left-color:#2ecc71; background:rgba(46,204,113,0.04); }
.qCell.correct .qNum { color:#2ecc71; }
.qCell.wrong { border-left-color:#e10600; background:rgba(225,6,0,0.03); }
.qCell.wrong .qNum { color:#e10600; }
.qCell.unanswered .qNum { color:rgba(255,255,255,0.2); }
/* 分站赛 UI */
.stgBar { height:4px; background:rgba(255,255,255,0.04); border-radius:2px; margin:10px 0; overflow:hidden; }
.stgFill { height:100%; background:#e10600; border-radius:2px; transition:width 0.5s cubic-bezier(.34,1.56,.64,1); }
.stgTag { display:inline-block; padding:3px 10px; border:1px solid rgba(255,255,255,0.06); border-radius:2px; font-size:11px; font-weight:700; color:rgba(255,255,255,0.4); margin:2px 4px; font-family:'Inter',monospace; }
.stgTag:nth-child(1) { border-color:rgba(225,6,0,0.15); color:#e10600; }
.stgTag:nth-child(2) { border-color:rgba(255,215,0,0.15); color:#ffd700; }
.stgTag:nth-child(3) { border-color:rgba(46,204,113,0.15); color:#2ecc71; }
/* 口诀 */
.kj {
margin:12px 0 4px; padding:10px 14px; background:rgba(255,215,0,0.03);
border:1px solid rgba(255,215,0,0.06); border-left:3px solid #ffd700; border-radius:2px;
font-size:14px; line-height:1.9; color:rgba(255,255,255,0.8);
}
.kj::before { content:'\u{1F3B6} 口诀 '; font-weight:900; color:#ffd700; font-size:12px; letter-spacing:1px; display:block; margin-bottom:2px; }
/* 巧解 */
.qj {
margin:12px 0 4px; padding:10px 14px; background:rgba(46,204,113,0.03);
border:1px solid rgba(46,204,113,0.06); border-left:3px solid #2ecc71; border-radius:2px;
font-size:14px; line-height:1.9; color:rgba(255,255,255,0.8);
}
.qj::before { content:'\u{1F4A1} 巧解 '; font-weight:900; color:#2ecc71; font-size:12px; letter-spacing:1px; display:block; margin-bottom:2px; }
/* SVG 动画 */
@keyframes svgPulse { 0%,100%{opacity:0.3} 50%{opacity:1} }
@keyframes svgRotate { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
@keyframes svgDash { to{stroke-dashoffset:0} }
.animPulse { animation:svgPulse 2s ease infinite; }
.animRotate { transform-origin:center; animation:svgRotate 6s linear infinite; }
.animDash { stroke-dasharray:200; stroke-dashoffset:200; animation:svgDash 2s ease forwards; }
/* === Interactive Drag Models === */
.drag-point { cursor:grab; }
.drag-point:active { cursor:grabbing; }
.drag-point:hover { filter:drop-shadow(0 0 8px rgba(225,6,0,0.6)); }
.drag-point .dp-ring { transition:r 0.15s; }
.drag-point:hover .dp-ring { r:7; }
.m-panel { margin:8px 0; padding:10px 14px; background:rgba(0,0,0,0.15); border-radius:4px; display:flex; flex-wrap:wrap; gap:6px 14px; justify-content:center; }
.m-val { display:inline-flex; align-items:center; gap:4px; font-size:13px; font-weight:700; font-family:'Inter',monospace; }
.m-val .lbl { color:rgba(255,255,255,0.3); font-size:10px; letter-spacing:1px; }
.m-val .num { color:#ffd700; font-size:14px; }
.m-val .num.hl { color:#e10600; }
.m-hint { text-align:center; font-size:11px; color:rgba(255,255,255,0.25); letter-spacing:1px; margin:4px 0; font-family:'Inter',monospace; animation:pulse 2s ease infinite; }
.m-discover { text-align:center; font-size:14px; color:#2ecc71; font-weight:700; margin:4px 0; animation:slideUp 0.5s ease; }
.drag-line { cursor:pointer; }
</style>
</head>
<body>
<div class="app" id="app">
<!-- HUD -->
<div class="hud">
<div class="logo">
<span class="r">RED&nbsp;BULL</span>
<span class="w">&nbsp;/&nbsp;</span>
<span class="y">MV1</span>
</div>
<div class="hud-spacer"></div>
<div class="hud-stat"><div class="lbl">\u{1F3C6} GP</div><div class="val y" id="hStage">1/4</div></div>
<div class="hud-stat"><div class="lbl">PTS</div><div class="val r" id="hPts">0</div></div>
<button class="mbtn" id="mBtn">&#x266B;</button>
</div>
<!-- HOME — TRACK MAP -->
<div class="pg act" id="pH">
<div class="track-wrap" id="trackWrap">
<span class="track-car" id="trackCar">
<img src="rbcar.png" class="ci" onerror="this.style.display='none';this.nextElementSibling.classList.remove('hide')" onload="this.nextElementSibling.classList.add('hide')" style="display:none">
<svg viewBox="0 0 60 28" class="cs" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="wingGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#e10600"/>
<stop offset="100%" stop-color="#cc0500"/>
</linearGradient>
</defs>
<rect x="1" y="5" width="6" height="18" rx="1" fill="url(#wingGrad)"/>
<rect x="0" y="4" width="3" height="3" rx="0.5" fill="#ffd700"/>
<rect x="0" y="21" width="3" height="3" rx="0.5" fill="#ffd700"/>
<rect x="4" y="4" width="4" height="20" rx="1" fill="#1e41a0" stroke="#e10600" stroke-width="0.3"/>
<rect x="6" y="10" width="4" height="8" rx="0.5" fill="#1a1a3a"/>
<path d="M 12,7 L 24,6 Q 28,6 30,8 L 30,20 Q 28,22 24,22 L 12,21 Z" fill="#1e41a0"/>
<path d="M 22,6 Q 26,3 32,4 L 32,6 L 24,8 Z" fill="#e10600" opacity="0.3"/>
<path d="M 22,22 Q 26,25 32,24 L 32,22 L 24,20 Z" fill="#e10600" opacity="0.3"/>
<path d="M 24,4 L 34,4 Q 38,4 40,8 L 40,10 L 24,9 Z" fill="#1e41a0"/>
<path d="M 24,24 L 34,24 Q 38,24 40,20 L 40,18 L 24,19 Z" fill="#1e41a0"/>
<path d="M 30,8 Q 36,7 40,9 L 44,10 Q 48,10 50,11 L 52,13 L 52,15 L 50,17 Q 48,18 44,18 L 40,19 Q 36,21 30,20 Z" fill="#1e41a0"/>
<path d="M 50,11 L 56,12.5 L 57,14 L 56,15.5 L 50,17 Z" fill="#0e0e3e"/>
<path d="M 56,12.5 L 59,13.5 L 59,14.5 L 56,15.5 Z" fill="#e10600"/>
<path d="M 52,9 L 60,10 L 60,18 L 52,19 Z" fill="#1e41a0" stroke="#e10600" stroke-width="0.3"/>
<rect x="58" y="8" width="2" height="4" rx="0.5" fill="#e10600"/>
<rect x="58" y="16" width="2" height="4" rx="0.5" fill="#e10600"/>
<ellipse cx="42" cy="14" rx="4" ry="4.5" fill="none" stroke="#ffd700" stroke-width="0.8" opacity="0.8"/>
<rect x="16" y="2" width="3" height="4" rx="1" fill="#1a1a1a"/>
<rect x="16" y="22" width="3" height="4" rx="1" fill="#1a1a1a"/>
<rect x="38" y="2.5" width="3" height="3.5" rx="1" fill="#1a1a1a"/>
<rect x="38" y="22" width="3" height="3.5" rx="1" fill="#1a1a1a"/>
<rect x="17" y="3" width="1" height="2" rx="0.5" fill="#e10600"/>
<rect x="17" y="23" width="1" height="2" rx="0.5" fill="#e10600"/>
<rect x="39" y="3" width="1" height="2" rx="0.5" fill="#e10600"/>
<rect x="39" y="23" width="1" height="2" rx="0.5" fill="#e10600"/>
<text x="35" y="16" font-family="'Inter',Arial,sans-serif" font-weight="900" font-size="8" fill="#ffd700" text-anchor="middle">#1</text>
<text x="20" y="20" font-family="'Inter',Arial,sans-serif" font-weight="900" font-size="2.5" fill="#e10600" text-anchor="middle" opacity="0.8">RED BULL</text>
<rect x="8" y="11" width="3" height="6" rx="0.5" fill="#222"/>
<circle cx="10" cy="14" r="1.5" fill="#333"/>
</svg>
</span>
<svg viewBox="0 0 360 540" preserveAspectRatio="xMidYMid meet">
<!-- S-curve track — road surface -->
<path class="track-road" d="M 180,20 C 320,20 320,120 180,140 C 40,160 40,260 180,280 C 320,300 320,400 180,420 C 40,440 40,520 180,520"/>
<!-- track edges -->
<path class="track-bg" d="M 180,20 C 320,20 320,120 180,140 C 40,160 40,260 180,280 C 320,300 320,400 180,420 C 40,440 40,520 180,520"/>
<!-- remaining circuit (dashed center line) -->
<path class="track-fill" id="trackFill" d="M 180,20 C 320,20 320,120 180,140 C 40,160 40,260 180,280 C 320,300 320,400 180,420 C 40,440 40,520 180,520"/>
<!-- completed portion (gold) -->
<path class="track-done" id="trackDone" d="M 180,20" stroke-dasharray="800" stroke-dashoffset="800"/>
<!-- Finish line -->
<line x1="160" y1="515" x2="200" y2="515" stroke="#ffd700" stroke-width="2" opacity="0.15"/>
<line x1="160" y1="520" x2="200" y2="520" stroke="#ffd700" stroke-width="2" opacity="0.1"/>
<line x1="160" y1="525" x2="200" y2="525" stroke="#ffd700" stroke-width="2" opacity="0.08"/>
<!-- CP 1 — 有理数 -->
<g class="cp" data-idx="0">
<circle cx="280" cy="68" r="24" fill="transparent" pointer-events="all"/>
<circle cx="280" cy="68" r="14" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.3)" stroke-width="2"/>
<text class="cp-label" x="280" y="72">🏆 #1</text>
<text class="cp-name" x="280" y="40">第1站 · 有理数</text>
<text class="cp-stars" id="cps0" x="280" y="90"></text>
<text class="lock-icon" x="280" y="68" style="display:none">&#x1F512;</text>
</g>
<!-- CP 2 — 整式的加减 -->
<g class="cp" data-idx="1">
<circle cx="80" cy="200" r="24" fill="transparent" pointer-events="all"/>
<circle cx="80" cy="200" r="14" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.3)" stroke-width="2"/>
<text class="cp-label" x="80" y="204">🏆 #1</text>
<text class="cp-name" x="80" y="172">第2站 · 整式的加减</text>
<text class="cp-stars" id="cps1" x="80" y="222"></text>
<text class="lock-icon" x="80" y="200" style="display:none">&#x1F512;</text>
</g>
<!-- CP 3 — 一元一次方程 -->
<g class="cp" data-idx="2">
<circle cx="280" cy="340" r="24" fill="transparent" pointer-events="all"/>
<circle cx="280" cy="340" r="14" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.3)" stroke-width="2"/>
<text class="cp-label" x="280" y="344">🏆 #1</text>
<text class="cp-name" x="280" y="312">第3站 · 一元一次方程</text>
<text class="cp-stars" id="cps2" x="280" y="362"></text>
<text class="lock-icon" x="280" y="340" style="display:none">&#x1F512;</text>
</g>
<!-- CP 4 — 几何图形初步 -->
<g class="cp" data-idx="3">
<circle cx="120" cy="468" r="24" fill="transparent" pointer-events="all"/>
<circle cx="120" cy="468" r="14" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.3)" stroke-width="2"/>
<text class="cp-label" x="120" y="472">🏆 #1</text>
<text class="cp-name" x="120" y="440">第4站 · 几何图形初步</text>
<text class="cp-stars" id="cps3" x="120" y="490"></text>
<text class="lock-icon" x="120" y="468" style="display:none">&#x1F512;</text>
</g>
</svg>
</div>
<div style="text-align:center;margin-top:4px;">
<div style="display:inline-block;background:rgba(225,6,0,0.03);border:1px solid rgba(225,6,0,0.05);padding:5px 16px;border-radius:2px;">
<span style="font-size:9px;color:rgba(255,255,255,0.35);letter-spacing:2px;font-weight:700;">
&#x26A1; 追随 #1 Max Verstappen 的冠军之路 · 四站全胜 = 世界冠军 &#x1F3C6;
</span>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:center;margin-top:6px;flex-wrap:wrap;">
<button class="hudBtn" id="wrongBtn"><span id="hintBadge">0</span>\u{1F4D2} 错题集</button>
<button class="hudBtn" id="revLapBtn" style="display:none;background:rgba(255,215,0,0.06);border-color:rgba(255,215,0,0.1);color:#ffd700;">\u{1F3CE}\uFE0F 复习圈</button>
<button class="hudBtn hudDanger" id="clrP">\u{1F5D1} 清除进度</button>
<button class="hudBtn" id="unlockBtn" style="background:rgba(255,215,0,0.06);border-color:rgba(255,215,0,0.1);color:#ffd700;">🔓 解锁章节</button>
</div>
<div style="text-align:center;margin-top:6px;margin-bottom:4px;">
<button class="hudBtn" id="dbBtn" style="padding:8px 16px;font-size:10px;">\u{1F4CA} 学习总览</button>
</div>
<div id="dbPanel" style="display:none;margin-top:8px;"></div>
</div>
<!-- CHAPTER -->
<div class="pg" id="pC">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;">
<button class="bk" id="bkMap" style="margin-bottom:0">&larr; 赛道</button>
<button class="bk" id="bkMapBtn" style="margin-bottom:0;background:rgba(255,215,0,0.06);border-color:rgba(255,215,0,0.1);color:#ffd700;">&#x1F4CA; 答题地图</button>
</div>
<div class="ch">
<div class="gl" id="cGl">🏆 MV1 · 第1站</div>
<h2 id="cTi"></h2>
<div class="sm" id="cDe"></div>
</div>
<div class="hudExtra" id="hudExtra"><div class="streakDisplay" id="streakD"></div><div class="lapCounter" id="lapCnt"></div></div>
<div id="cCo"></div>
</div>
<!-- ANSWER MAP -->
<div class="pg" id="pM">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;">
<button class="bk" id="bkMapM" style="margin-bottom:0">&larr; 赛道</button>
<button class="bk" id="bkChapM" style="margin-bottom:0">&larr; 返回</button>
</div>
<div class="ch">
<div class="gl" id="mGl">答题地图</div>
<h2 id="mTi"></h2>
<div class="sm" id="mDe">点击题目跳转</div>
</div>
<div id="mGrid"></div>
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:16px;">
<button class="mBtn2" id="mUn" style="background:#ffd700;color:#0a0a1a;border-color:#ffd700;">&#x25B6; 从未答开始</button>
<button class="mBtn2" id="mErr" style="background:#e10600;border-color:#e10600;">&#x2716; 从错题开始</button>
<button class="mBtn2" id="mAll" style="background:rgba(255,255,255,0.05);border-color:rgba(255,255,255,0.1);">&#x1F504; 全部重做</button>
</div>
</div>
<!-- WRONG REVIEW -->
<div class="pg" id="pW">
<button class="bk" id="bkMapW">&larr; 赛道</button>
<div class="ch"><div class="gl" id="wGl">\u{1F4D2} 错题集</div><h2>错题再练</h2><div class="sm" id="wDe">错了不怕,练到对为止</div></div>
<div id="wList"></div>
</div>
</div>
<!-- PODIUM -->
<div class="po" id="po">
<div class="pm">
<div class="checker" id="pmChk"></div>
<img src="mv1.png" class="mv1-photo" id="mv1Img" onerror="this.classList.add('hide');document.getElementById('mv1Av').classList.remove('hide')" onload="document.getElementById('mv1Av').classList.add('hide')">
<div class="mv1-av hide" id="mv1Av">MV1</div>
<div class="pn1" id="pmN">P1</div>
<span class="pcup" id="pmC">&#x1F3C6;</span>
<h2 id="pmTi">冠军</h2>
<div class="ps" id="pmSt"></div>
<div class="pxp" id="pmXp">+0 PTS</div>
<p class="pms" id="pmMs">下个赛段继续冲</p>
<div class="radio pmRadio" id="pmRadio"></div>
<div class="pmQuote" id="pmQ"></div>
<button class="next-track" id="pmNt">下一站 &rarr;</button>
</div>
</div>
<div class="pyro" id="pyro"></div>
<script>
// ===== 几何可视化引擎(绘图工具)=====
const Geo = {
svg(w,h,inner){return `<svg viewBox="0 0 ${w} ${h}" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">${inner}</svg>`;},
label(x,y,t,o={}){const{fs=10,fill='rgba(255,255,255,0.7)',anchor='middle',fw='400'}=o;return `<text x="${x}" y="${y}" font-size="${fs}" fill="${fill}" text-anchor="${anchor}" font-weight="${fw}" font-family="'Inter',monospace">${t}</text>`;},
dot(x,y,o={}){const{r=3,fill='#ffd700'}=o;return `<circle cx="${x}" cy="${y}" r="${r}" fill="${fill}"/>`;},
line(x1,y1,x2,y2,o={}){const{sw=2,clr='rgba(255,255,255,0.12)',dash=''}=o;return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${clr}" stroke-width="${sw}" stroke-dasharray="${dash}"/>`;},
arrow(x1,y1,x2,y2,o={}){const{sw=2,clr='rgba(255,255,255,0.3)',sz=6}=o;const dx=x2-x1,dy=y2-y1,len=Math.hypot(dx,dy);if(!len)return '';const ux=dx/len,uy=dy/len;return this.line(x1,y1,x2,y2,o)+`<polygon points="${x2},${y2} ${x2-sz*ux+sz*uy/2},${y2-sz*uy-sz*ux/2} ${x2-sz*ux-sz*uy/2},${y2-sz*uy+sz*ux/2}" fill="${clr}"/>`;},
seg(x,y,w,o={}){const{la='A',lb='B',col='#ffd700'}=o;let r=this.dot(x,y,{fill:col})+this.dot(x+w,y,{fill:col})+this.line(x,y,x+w,y,{clr:'rgba(255,255,255,0.4)',sw:2});return r+this.label(x,y+18,la,{fs:9,fill:col})+this.label(x+w,y+18,lb,{fs:9,fill:col});},
midSeg(x,y,w,o={}){const{la='A',lb='B',ml='M',ac='',cb='',total=''}=o;const mx=x+w/2;return this.seg(x,y,w,{la,lb})+this.dot(mx,y,{fill:'#e10600'})+this.label(mx,y-8,ml,{fs:8,fill:'#e10600'})+(ac?this.label(x+w/4,y-23,ac,{fs:8,fill:'rgba(255,255,255,0.2)'}):'')+(cb?this.label(x+w*3/4,y-23,cb,{fs:8,fill:'rgba(255,255,255,0.2)'}):'')+(total?this.label(x+w/2,y+30,total,{fs:9,fill:'#ffd700'}):'');},
aArc(cx,cy,r,a,o={}){const{clr='#ffd700',sw=2,deg='',tag='',fs=11}=o;const ar=a*Math.PI/180;const pts=[];for(let i=0;i<=40;i++){const t=i/40*ar;pts.push(`${cx+r*Math.cos(Math.PI/2-t)},${cy-r*Math.sin(Math.PI/2-t)}`)}let s=`<polyline points="${pts.join(' ')}" fill="none" stroke="${clr}" stroke-width="${sw}"/>`;if(deg)s+=this.label(cx+r*Math.cos(Math.PI/2-ar/2)*1.3,cy-r*Math.sin(Math.PI/2-ar/2)*1.3,deg+'°',{fs,fill:clr,fw:'900'});if(tag)s+=this.label(cx+r*Math.cos(Math.PI/2-ar/2)*0.7,cy-r*Math.sin(Math.PI/2-ar/2)*0.7,tag,{fs:9,fill:clr});return s;},
rAngle(cx,cy,sz,up){return `<polyline points="${cx+sz},${cy} ${cx+sz},${cy-sz*(up||1)} ${cx},${cy-sz*(up||1)}" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="1"/>`;},
clock(cx,cy,r,h,m,o={}){const{hl=''}=o;const hAng=(h%12)*30+m*0.5,mAng=m*6;let s=`<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>`;for(let i=0;i<12;i++){const a=i*30*Math.PI/180;const x1=cx+(r-6)*Math.sin(a),y1=cy-(r-6)*Math.cos(a);const x2=cx+r*Math.sin(a),y2=cy-r*Math.cos(a);s+=this.line(x1,y1,x2,y2,{clr:`rgba(255,255,255,${i%3===0?0.12:0.04})`,sw:i%3===0?1.5:1});if(i%3===0){const lx=cx+(r+14)*Math.sin(a),ly=cy-(r+14)*Math.cos(a);s+=this.label(lx,ly+3,i||12,{fs:8,fill:'rgba(255,255,255,0.2)'})}}const hh=r*0.45,hm=r*0.6;const ha=(hAng-90)*Math.PI/180,ma=(mAng-90)*Math.PI/180;s+=this.line(cx,cy,cx+hh*Math.cos(ha),cy+hh*Math.sin(ha),{clr:'#ffd700',sw:3});s+=this.line(cx,cy,cx+hm*Math.cos(ma),cy+hm*Math.sin(ma),{clr:'rgba(255,255,255,0.2)',sw:1.5});s+=this.dot(cx,cy,{r:4,fill:'#e10600'});if(hl)s+=this.label(cx,cy-r-16,hl,{fs:9,fill:'#ffd700',fw:'900'});return s;},
compass(cx,cy,r,deg,o={}){const{label=''}=o;let s=`<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>`;const dirs=[{d:'北',a:0},{d:'东',a:90},{d:'南',a:180},{d:'西',a:270}];dirs.forEach(({d,a})=>{const t=a*Math.PI/180;const x=cx+(r+12)*Math.sin(t),y=cy-(r+12)*Math.cos(t);s+=this.label(x,y+3,d,{fs:9,fill:a===0?'#e10600':'rgba(255,255,255,0.2)',fw:a===0?'900':'400'})});const a=deg*Math.PI/180;const ex=cx+r*Math.sin(a),ey=cy-r*Math.cos(a);s+=this.arrow(cx,cy,ex,ey,{clr:'#ffd700',sw:2.5});s+=this.dot(cx,cy,{r:3,fill:'#ffd700'});if(label)s+=this.label(ex+10,ey-5,label,{fs:9,fill:'#ffd700'});return s;},
numLine(cx,cy,w,vals,o={}){const{labels={}}=o;const mn=Math.min(...vals),mx=Math.max(...vals),rg=mx-mn||1;let s=this.arrow(cx,cy,cx+w,cy,{clr:'rgba(255,255,255,0.2)',sw:1.5});vals.forEach(v=>{const x=cx+(v-mn)/rg*w;if(v===0)s+=this.dot(x,cy,{r:3,fill:'#ffd700'})+this.label(x,cy+12,'0',{fs:9,fill:'#ffd700',fw:'900'});else s+=this.dot(x,cy,{r:2,fill:'rgba(255,255,255,0.15)'})+this.label(x,cy+12,v,{fs:8,fill:'rgba(255,255,255,0.2)'});if(labels[v])s+=this.label(x,cy-8,labels[v],{fs:7,fill:'#e10600'})});return s;}
};
// ===== 交互式动态几何模型引擎 =====
const GeoModel = {
// ---- 拖拽状态 ----
drag:{active:false,type:'',el:null,startX:0,startY:0,bx:0,by:0,minX:0,maxX:0,minY:0,maxY:0,ox:0,oy:0,cb:null},
startDrag(e,type,el,opts){
const r=el.getBoundingClientRect();const svg=el.closest('svg');const sR=svg.getBoundingClientRect();
const sx=sR.width/svg.viewBox.animVal.width, sy=sR.height/svg.viewBox.animVal.height;
this.drag={active:true,type,el,startX:(e.clientX-sR.left)/sx,startY:(e.clientY-sR.top)/sy,bx:0,by:0,
minX:opts.minX||0,maxX:opts.maxX||360,minY:opts.minY||0,maxY:opts.maxY||200,
ox:parseFloat(el.getAttribute('cx')),oy:parseFloat(el.getAttribute('cy')),
sx,sy,sr:sR,cb:opts.cb||null};
el.classList.add('dragging');
},
moveDrag(e){
const d=this.drag;if(!d.active)return;
const r=d.sr;const x=(e.clientX-r.left)/d.sx,y=(e.clientY-r.top)/d.sy;
const nx=Math.max(d.minX,Math.min(d.maxX,x)), ny=Math.max(d.minY,Math.min(d.maxY,y));
d.el.setAttribute('cx',nx);d.el.setAttribute('cy',ny);
d.bx=nx-d.ox;d.by=ny-d.oy;
if(d.cb)d.cb(nx,ny);
},
endDrag(){const d=this.drag;if(d.active){d.el.classList.remove('dragging');this.drag.active=false;}},
// ---- 双中点拖拽模型 ----
renderMidpoint(){
const w=360,h=180,ax=40,ay=70,bx=320,by=70,cxInit=180,ab=bx-ax;
const scaler=function(v){return Math.round(v/ab*100)};const fmt=function(v){return scaler(v)};
const initCx=cxInit;
return `<div style="margin:6px 0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:13px;font-weight:700;color:#ffd700;">\u{1F3C6} 拖拽点C——观察MN的变化</span>
<span class="m-hint" style="animation:pulse 2s ease infinite;font-size:10px;">\u{1F446} 拖动橙色点C</span>
</div>
<svg viewBox="0 0 ${w} ${h}" style="width:100%;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px 0;">
<!-- AB线段 -->
<line x1="${ax}" y1="${ay}" x2="${bx}" y2="${by}" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<!-- A点 -->
<circle cx="${ax}" cy="${ay}" r="5" fill="#ffd700"/><text x="${ax}" y="${ay+18}" font-size="11" fill="#ffd700" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">A</text>
<!-- B点 -->
<circle cx="${bx}" cy="${ay}" r="5" fill="#ffd700"/><text x="${bx}" y="${ay+18}" font-size="11" fill="#ffd700" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">B</text>
<!-- C点(可拖拽) -->
<circle class="drag-point" data-model="mid" data-point="C" cx="${initCx}" cy="${ay}" r="7" fill="#e10600" stroke="#fff" stroke-width="1.5" cursor="grab"/>
<text x="${initCx}" y="${ay-14}" font-size="10" fill="#e10600" text-anchor="middle" font-weight="900" font-family="'Inter',monospace">C</text>
<!-- 动态M点 -->
<circle id="mPt" cx="${ax+(initCx-ax)/2}" cy="${ay}" r="5" fill="#2ecc71"/>
<text id="mPtLbl" x="${ax+(initCx-ax)/2}" y="${ay-14}" font-size="10" fill="#2ecc71" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">M</text>
<!-- 动态N点 -->
<circle id="nPt" cx="${initCx+(bx-initCx)/2}" cy="${ay}" r="5" fill="#2ecc71"/>
<text id="nPtLbl" x="${initCx+(bx-initCx)/2}" y="${ay-14}" font-size="10" fill="#2ecc71" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">N</text>
<!-- 测量标注(实时更新) -->
<text id="mAC" x="${ax+(initCx-ax)/4}" y="${ay+35}" font-size="9" fill="rgba(255,255,255,0.2)" text-anchor="middle" font-family="'Inter',monospace">AC=${fmt(initCx-ax)}</text>
<text id="mCB" x="${initCx+(bx-initCx)/4}" y="${ay+35}" font-size="9" fill="rgba(255,255,255,0.2)" text-anchor="middle" font-family="'Inter',monospace">CB=${fmt(bx-initCx)}</text>
<!-- 发现区 -->
<text id="mDisc" x="${w/2}" y="${ay+55}" font-size="0" fill="#2ecc71" text-anchor="middle" font-weight="700" font-family="'Inter',monospace"></text>
</svg>
<div class="m-panel">
<span class="m-val"><span class="lbl">AB</span><span class="num" id="vAB">${fmt(ab)}</span></span>
<span class="m-val"><span class="lbl">AC</span><span class="num" id="vAC">${fmt(initCx-ax)}</span></span>
<span class="m-val"><span class="lbl">CB</span><span class="num" id="vCB">${fmt(bx-initCx)}</span></span>
<span class="m-val"><span class="lbl" style="color:#2ecc71;">MN</span><span class="num hl" id="vMN">${fmt(ab/2)}</span></span>
</div>
</div>`;
},
initMidDrag(cx,cy){
const ax=40,bx=320,ay=70;
const nx=Math.round(Math.max(ax+15,Math.min(bx-15,cx)));
const mAC=nx-ax,mCB=bx-nx,mAB=bx-ax,mMN=mAB/2;
const scaler=function(v){return Math.round(v/mAB*100)};
const mCx=(ax+nx)/2,mNx=(nx+bx)/2;
$('mPt')?.setAttribute('cx',mCx);$('mPtLbl')?.setAttribute('x',mCx);
$('nPt')?.setAttribute('cx',mNx);$('nPtLbl')?.setAttribute('x',mNx);
$('mAC')?.setAttribute('x',ax+(nx-ax)/4);$('mAC').textContent='AC='+scaler(mAC);
$('mCB')?.setAttribute('x',nx+(bx-nx)/4);$('mCB').textContent='CB='+scaler(mCB);
$('vAC').textContent=scaler(mAC);$('vCB').textContent=scaler(mCB);
$('vAB').textContent=scaler(mAB);$('vMN').textContent=scaler(mMN);
const disc=$('mDisc');
if(disc){disc.setAttribute('font-size','13');disc.textContent='\u{2728} 发现:无论C在哪里,MN总是AB的一半!';}
},
// ---- 角平分线拖拽模型 ----
renderAngleBisector(){
const cx=180,cy=80,r=60,initDeg=60;
const rad=initDeg*Math.PI/180;
const bx=cx+r*Math.cos(Math.PI/2-rad),by=cy-r*Math.sin(Math.PI/2-rad);
const bisDeg=initDeg/2;
const bisRad=bisDeg*Math.PI/180;
const bisx=cx+r*Math.cos(Math.PI/2-bisRad),bisy=cy-r*Math.sin(Math.PI/2-bisRad);
return `<div style="margin:6px 0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:13px;font-weight:700;color:#ffd700;">\u{1F3C6} 拖拽OB旋转——观察角平分线</span>
<span class="m-hint" style="animation:pulse 2s ease infinite;font-size:10px;">\u{1F446} 拖动B点旋转</span>
</div>
<svg viewBox="0 0 360 170" style="width:100%;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px 0;">
<!-- OA射线(固定水平) -->
<line x1="${cx}" y1="${cy}" x2="${cx+r+20}" y2="${cy}" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<polygon points="${cx+r+18},${cy-5} ${cx+r+24},${cy} ${cx+r+18},${cy+5}" fill="rgba(255,255,255,0.15)"/>
<text x="${cx+r+26}" y="${cy+4}" font-size="10" fill="rgba(255,255,255,0.3)" font-family="'Inter',monospace">A</text>
<!-- OB射线(可旋转) -->
<line id="obRay" x1="${cx}" y1="${cy}" x2="${bx}" y2="${by}" stroke="#e10600" stroke-width="2.5"/>
<polygon id="obArr" points="${bx},${by} ${bx-6*Math.cos(Math.PI/2-rad)-3*Math.sin(Math.PI/2-rad)},${by+6*Math.sin(Math.PI/2-rad)-3*Math.cos(Math.PI/2-rad)} ${bx-6*Math.cos(Math.PI/2-rad)+3*Math.sin(Math.PI/2-rad)},${by+6*Math.sin(Math.PI/2-rad)+3*Math.cos(Math.PI/2-rad)}" fill="#e10600"/>
<!-- B点(可拖拽) -->
<circle class="drag-point" data-model="angle" data-point="B" cx="${bx}" cy="${by}" r="7" fill="#e10600" stroke="#fff" stroke-width="1.5" cursor="grab"/>
<text x="${bx+10}" y="${by-5}" font-size="10" fill="#e10600" text-anchor="start" font-weight="900" font-family="'Inter',monospace">B</text>
<!-- O点 -->
<circle cx="${cx}" cy="${cy}" r="5" fill="#ffd700"/>
<text x="${cx-8}" y="${cy+16}" font-size="10" fill="#ffd700" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">O</text>
<!-- 角平分线OC -->
<line id="bisRay" x1="${cx}" y1="${cy}" x2="${bisx}" y2="${bisy}" stroke="#2ecc71" stroke-width="2.5" stroke-dasharray="5,3"/>
<text id="bisLbl" x="${bisx+10}" y="${bisy-5}" font-size="10" fill="#2ecc71" font-family="'Inter',monospace">C(平分线)</text>
<!-- 角度弧 -->
<path id="aArc" d="${this.arcPath(cx,cy,40,initDeg)}" stroke="#ffd700" stroke-width="2" fill="none" opacity="0.6"/>
<text id="degLbl" x="${cx+45}" y="${cy-10}" font-size="12" fill="#ffd700" text-anchor="middle" font-weight="900" font-family="'Inter',monospace">${initDeg}°</text>
<!-- 平分角弧 -->
<path id="bisArc" d="${this.arcPath(cx,cy,25,bisDeg)}" stroke="#2ecc71" stroke-width="1.5" fill="none" opacity="0.4"/>
<text id="bisDegLbl" x="${cx+32}" y="${cy-8}" font-size="10" fill="#2ecc71" text-anchor="middle" font-family="'Inter',monospace">${bisDeg}°</text>
</svg>
<div class="m-panel">
<span class="m-val"><span class="lbl">∠AOB</span><span class="num" id="vAOB">${initDeg}°</span></span>
<span class="m-val"><span class="lbl" style="color:#2ecc71;">∠AOC</span><span class="num hl" id="vAOC">${bisDeg}°</span></span>
<span class="m-val"><span class="lbl" style="color:#2ecc71;">∠COB</span><span class="num hl" id="vCOB">${bisDeg}°</span></span>
</div>
</div>`;
},
arcPath(cx,cy,r,deg){
return this.arcPts(cx,cy,r,0,deg);
},
initAngleDrag(cx,cy){
const ocx=180,ocy=80,r=60;
const dx=cx-ocx,dy=cy-ocy;
const rawDeg=Math.atan2(-dy,dx)*180/Math.PI+90;
let deg=Math.max(10,Math.min(170,rawDeg));
const rad=deg*Math.PI/180;
const bx=ocx+r*Math.cos(Math.PI/2-rad),by=ocy-r*Math.sin(Math.PI/2-rad);
const bisDeg=deg/2,bisRad=bisDeg*Math.PI/180;
const bisx=ocx+r*Math.cos(Math.PI/2-bisRad),bisy=ocy-r*Math.sin(Math.PI/2-bisRad);
const ray=$('obRay');if(ray){ray.setAttribute('x2',bx);ray.setAttribute('y2',by);}
const arr=$('obArr');if(arr){
const ax=bx-6*Math.cos(Math.PI/2-rad)-3*Math.sin(Math.PI/2-rad),ay=by+6*Math.sin(Math.PI/2-rad)-3*Math.cos(Math.PI/2-rad);
const bx2=bx-6*Math.cos(Math.PI/2-rad)+3*Math.sin(Math.PI/2-rad),by2=by+6*Math.sin(Math.PI/2-rad)+3*Math.cos(Math.PI/2-rad);
arr.setAttribute('points',`${bx},${by} ${ax},${ay} ${bx2},${by2}`);
}
const bisEl=$('bisRay');if(bisEl){bisEl.setAttribute('x2',bisx);bisEl.setAttribute('y2',bisy);}
const bisLblEl=$('bisLbl');if(bisLblEl){bisLblEl.setAttribute('x',bisx+10);bisLblEl.setAttribute('y',bisy-5);}
const arcEl=$('aArc');if(arcEl)arcEl.setAttribute('d',this.arcPts(ocx,ocy,40,0,deg));
const bisArcEl=$('bisArc');if(bisArcEl)bisArcEl.setAttribute('d',this.arcPts(ocx,ocy,25,0,bisDeg));
const degLbl=$('degLbl');if(degLbl)degLbl.textContent=deg+'°';
const bisDegLbl=$('bisDegLbl');if(bisDegLbl)bisDegLbl.textContent=Math.round(bisDeg)+'°';
$('vAOB').textContent=deg+'°';$('vAOC').textContent=Math.round(bisDeg)+'°';$('vCOB').textContent=Math.round(bisDeg)+'°';
},
// ---- 钟面角交互模型 ----
renderClock(){
const cx=180,cy=80,r=60,h=8,m=30;
return `<div style="margin:6px 0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:13px;font-weight:700;color:#ffd700;">\u{1F3C6} 拖拽指针——观察钟面角</span>
<span class="m-hint" style="animation:pulse 2s ease infinite;font-size:10px;">\u{1F446} 拖动分针或时针</span>
</div>
<svg viewBox="0 0 360 170" style="width:100%;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px 0;">
<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
${[0,30,60,90,120,150].map(a=>{const t=a*Math.PI/180;const x1=cx+(r-8)*Math.sin(t),y1=cy-(r-8)*Math.cos(t);const x2=cx+r*Math.sin(t),y2=cy-r*Math.cos(t);return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="rgba(255,255,255,${a%90===0?0.15:0.05})" stroke-width="${a%90===0?2:1}"/>`}).join('')}
${[3,6,9,12].map(h=>{const a=h*30*Math.PI/180;const lx=cx+(r+14)*Math.sin(a),ly=cy-(r+14)*Math.cos(a);return `<text x="${lx}" y="${ly+4}" font-size="11" fill="rgba(255,255,255,0.2)" text-anchor="middle" font-family="'Inter',monospace">${h}</text>`}).join('')}
${[1,2,4,5,7,8,10,11].map(h=>{const a=h*30*Math.PI/180;const lx=cx+(r+10)*Math.sin(a),ly=cy-(r+10)*Math.cos(a);return `<text x="${lx}" y="${ly+3}" font-size="7" fill="rgba(255,255,255,0.08)" text-anchor="middle" font-family="'Inter',monospace">${h}</text>`}).join('')}
<!-- 时针(可拖拽) -->
<line id="cHh" data-h="8" x1="${cx}" y1="${cy}" x2="${cx+0.45*r*Math.sin((h*30+m*0.5-90)*Math.PI/180)}" y2="${cy-0.45*r*Math.cos((h*30+m*0.5-90)*Math.PI/180)}" stroke="#ffd700" stroke-width="3.5"/>
<circle class="drag-point" data-model="clock" data-hand="h" cx="${cx+0.45*r*Math.sin((h*30+m*0.5-90)*Math.PI/180)}" cy="${cy-0.45*r*Math.cos((h*30+m*0.5-90)*Math.PI/180)}" r="6" fill="#ffd700" stroke="#fff" stroke-width="1.5" cursor="grab"/>
<!-- 分针(可拖拽) -->
<line id="cMh" data-m="30" x1="${cx}" y1="${cy}" x2="${cx+0.6*r*Math.sin((m*6-90)*Math.PI/180)}" y2="${cy-0.6*r*Math.cos((m*6-90)*Math.PI/180)}" stroke="rgba(255,255,255,0.3)" stroke-width="2"/>
<circle class="drag-point" data-model="clock" data-hand="m" cx="${cx+0.6*r*Math.sin((m*6-90)*Math.PI/180)}" cy="${cy-0.6*r*Math.cos((m*6-90)*Math.PI/180)}" r="5" fill="rgba(255,255,255,0.3)" stroke="#fff" stroke-width="1" cursor="grab"/>
<!-- 中心点 -->
<circle cx="${cx}" cy="${cy}" r="4" fill="#e10600"/>
<!-- 角度弧 -->
<path id="cArc" d="" stroke="#ffd700" stroke-width="2" fill="none" opacity="0.5"/>
<text id="cAng" x="${cx}" y="${cy-r-20}" font-size="14" fill="#ffd700" text-anchor="middle" font-weight="900" font-family="'Inter',monospace">75°</text>
</svg>
<div class="m-panel">
<span class="m-val"><span class="lbl">时刻</span><span class="num" id="cTime">${h}:${String(m).padStart(2,'0')}</span></span>
<span class="m-val"><span class="lbl">时针</span><span class="num" id="cHDeg">${h*30+m*0.5}°</span></span>
<span class="m-val"><span class="lbl">分针</span><span class="num" id="cMDeg">${m*6}°</span></span>
<span class="m-val"><span class="lbl" style="color:#ffd700;">夹角</span><span class="num hl" id="cDiff">75°</span></span>
</div>
<div style="text-align:center;margin-top:4px;"><span style="font-size:11px;color:rgba(255,255,255,0.15);font-family:'Inter',monospace;">公式:|30h - 5.5m|</span></div>
</div>`;
},
initClockDrag(cx,cy,hand){
const ocx=180,ocy=80,r=60;
const dx=cx-ocx,dy=cy-ocy;
const rawDeg=Math.atan2(-dy,dx)*180/Math.PI+90;
let deg=rawDeg<0?rawDeg+360:rawDeg;
let h=parseInt($('cHh').getAttribute('data-h')||'8'), m=parseInt($('cMh').getAttribute('data-m')||'30');
if(hand==='h'){h=Math.round(((deg%360)/30)%12);if(h===0)h=12;m=Math.round((deg%30)*2);}
else{m=Math.round(deg/6)%60;let mAng=m*6;let hAng=h%12*30+m*0.5;let diff=Math.abs(hAng-mAng);if(diff>180)diff=360-diff;$('cArc').setAttribute('d',this.clockArc(ocx,ocy,40,Math.min(mAng,hAng),Math.max(mAng,hAng)));}
// Compute positions
const hAng=h%12*30+m*0.5,mAng=m*6;
const diff=Math.abs(hAng-mAng);const finalDiff=diff>180?360-diff:diff;
const hx=ocx+0.45*r*Math.sin((hAng-90)*Math.PI/180),hy=ocy-0.45*r*Math.cos((hAng-90)*Math.PI/180);
const mx=ocx+0.6*r*Math.sin((mAng-90)*Math.PI/180),my=ocy-0.6*r*Math.cos((mAng-90)*Math.PI/180);
const hPt=$('cHh');if(hPt){hPt.setAttribute('x2',hx);hPt.setAttribute('y2',hy);hPt.setAttribute('data-h',h);}
const mPt=$('cMh');if(mPt){mPt.setAttribute('x2',mx);mPt.setAttribute('y2',my);mPt.setAttribute('data-m',m);}
const hDrag=document.querySelector('[data-model="clock"][data-hand="h"]');if(hDrag){hDrag.setAttribute('cx',hx);hDrag.setAttribute('cy',hy);}
const mDrag=document.querySelector('[data-model="clock"][data-hand="m"]');if(mDrag){mDrag.setAttribute('cx',mx);mDrag.setAttribute('cy',my);}
$('cTime').textContent=h+':'+String(Math.round(m)).padStart(2,'0');
$('cHDeg').textContent=Math.round(hAng)+'°';$('cMDeg').textContent=Math.round(mAng)+'°';$('cDiff').textContent=Math.round(finalDiff)+'°';
$('cAng').textContent=Math.round(finalDiff)+'°';
const start=Math.min(hAng,mAng),end=Math.max(hAng,mAng);
const arc=this.clockArc(ocx,ocy,35,start,end);
const arcEl=$('cArc');if(arcEl)arcEl.setAttribute('d',arc);
},
clockArc(cx,cy,r,s,e){
const a1=Math.min(s,e),a2=Math.max(s,e);
if(a2-a1>180){return this.arcPts(cx,cy,r,a2,360)+' '+this.arcPts(cx,cy,r,0,a1);}
return this.arcPts(cx,cy,r,a1,a2);
},
arcPts(cx,cy,r,s,e){
const pts=[];for(let i=0;i<=24;i++){const t=(s+(e-s)*i/24)*Math.PI/180;pts.push(`${cx+r*Math.sin(t)},${cy-r*Math.cos(t)}`)}return pts.join(' ');
}
};
const SS = [
// ═══════════════════════════════════════
// CHAPTER 1: 有理数
// ═══════════════════════════════════════
{ id:1, title:'有理数', desc:'正负·数轴·相反·绝对值·运算·乘方·科学记数法', emoji:'🏁',
segs:[
// ── 概述 ──
{ type:'pn', content:`<h3>📋 有理数 = "有理有据"的数</h3><p>有理数就是所有能写成 p/q(q≠0)的数——整数和分数都是有理数。</p><p>本章路线:正负数→数轴→相反数/绝对值→四则运算→倒数→乘方→科学记数法</p><svg viewBox="0 0 360 50" style="width:100%;margin:4px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;"><line x1="18" y1="25" x2="342" y2="25" stroke="rgba(255,255,255,0.06)" stroke-width="1"/><circle cx="30" cy="25" r="5" fill="#e10600"/><text x="30" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">正负数</text><circle cx="90" cy="25" r="5" fill="#e10600"/><text x="90" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">数轴</text><circle cx="150" cy="25" r="5" fill="#e10600"/><text x="150" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">相反/绝对</text><circle cx="210" cy="25" r="5" fill="#e10600"/><text x="210" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">四则运算</text><circle cx="270" cy="25" r="5" fill="#e10600"/><text x="270" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">倒数/乘方</text><circle cx="330" cy="25" r="5" fill="#e10600"/><text x="330" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">科学记数法</text><line x1="35" y1="25" x2="85" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/><line x1="95" y1="25" x2="145" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/><line x1="155" y1="25" x2="205" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/><line x1="215" y1="25" x2="265" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/><line x1="275" y1="25" x2="325" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/></svg>` },
// ── KP1: 正数和负数 ──
{ type:'kp', icon:'🌡️', title:'正数和负数',
content:`<p>🔥 <strong>正数</strong>:大于0的数,如+3、5"+"可省略)</p>
<p>❄️ <strong>负数</strong>:小于0的数,前面带"-",如-3、-2.5</p>
<p>0既不是正数也不是负数——它是正负的<strong>分界线</strong></p>
<svg viewBox="0 0 360 70" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<rect x="160" y="5" width="16" height="35" rx="8" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<circle cx="168" cy="42" r="8" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<rect x="163" y="12" width="10" height="26" rx="4" fill="rgba(225,6,0,0.2)"/>
<circle cx="168" cy="42" r="6" fill="rgba(225,6,0,0.2)"/>
<text x="170" y="60" font-size="8" fill="#e10600" text-anchor="middle" font-weight="700">+30°C零上</text>
<rect x="10" y="5" width="16" height="35" rx="8" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<circle cx="18" cy="42" r="8" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<rect x="13" y="30" width="10" height="8" rx="4" fill="rgba(46,204,113,0.15)"/>
<circle cx="18" cy="42" r="6" fill="rgba(46,204,113,0.15)"/>
<text x="20" y="60" font-size="8" fill="rgba(46,204,113,0.5)" text-anchor="middle">-5°C零下</text>
</svg>
<p>💡 有理数分类:<strong>整数</strong>(正整数/0/负整数)· <strong>分数</strong>(正分数/负分数)</p>
<div class="kj">零上为正,零下为负,零是分界要记熟<br>整数分数都是有理数,不是有理数太难找</div>` },
// ── KP2: 数轴 ──
{ type:'kp', icon:'🚗', title:'数轴',
content:`<p>数轴三要素:<strong>原点(0) + 正方向(→) + 单位长度</strong></p>
<p>数轴上越<strong>右</strong>边的数越<strong>大</strong>,越<strong>左</strong>边的数越<strong>小</strong></p>
<p>比较大小口诀:正数>0>负数;两个负数,绝对值大的反而<strong>小</strong></p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;">
<line x1="20" y1="28" x2="340" y2="28" stroke="rgba(255,255,255,0.1)" stroke-width="1.5"/>
<polygon points="338,23 348,28 338,33" fill="rgba(255,255,255,0.15)"/>
<line x1="60" y1="23" x2="60" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<line x1="110" y1="23" x2="110" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<line x1="180" y1="23" x2="180" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<line x1="250" y1="23" x2="250" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<line x1="300" y1="23" x2="300" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<text x="60" y="46" font-size="8" fill="rgba(225,6,0,0.3)" text-anchor="middle">-3</text>
<text x="110" y="46" font-size="8" fill="rgba(255,255,255,0.2)" text-anchor="middle">-1</text>
<text x="180" y="46" font-size="10" fill="#ffd700" text-anchor="middle" font-weight="900">0</text>
<text x="250" y="46" font-size="8" fill="rgba(46,204,113,0.3)" text-anchor="middle">+2</text>
<text x="300" y="46" font-size="8" fill="rgba(46,204,113,0.3)" text-anchor="middle">+4</text>
</svg>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;">
<line x1="20" y1="30" x2="340" y2="30" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<polygon points="338,25 348,30 338,35" fill="rgba(255,255,255,0.1)"/>
<text x="180" y="50" font-size="7" fill="#ffd700" text-anchor="middle" font-weight="700">-3 + 5 = 2</text>
<text x="60" y="12" font-size="7" fill="#e10600" text-anchor="middle">-3</text>
<text x="180" y="12" font-size="7" fill="#2ecc71" text-anchor="middle">+5</text>
<circle cx="60" cy="30" r="5" fill="#e10600">
<animate attributeName="cx" values="60;180;60" dur="3s" repeatCount="indefinite"/>
<animate attributeName="fill" values="#e10600;#2ecc71;#e10600" dur="3s" repeatCount="indefinite"/>
</circle>
<path d="M 60,20 L 180,20" stroke="rgba(255,255,255,0.08)" stroke-width="1" stroke-dasharray="3,2"/>
<polygon points="175,17 185,20 175,23" fill="rgba(255,255,255,0.08)"/>
</svg>
<div class="kj">数轴一条线,原点在中间,正方向向右画<br>右边比左边大,正数大于零,负数小于零<br>两个负数比大小,绝对值大的反而小</div>` },
// ── KP3: 相反数与绝对值 ──
{ type:'kp', icon:'🪞', title:'相反数与绝对值',
content:`<p>🔁 <strong>相反数</strong>:只有符号不同的两个数,如3和-3(像照镜子)</p>
<p>📍 <strong>绝对值</strong>:数轴上点<strong>到原点的距离</strong>,记作\|a\|</p>
<ul style="color:rgba(255,255,255,0.7);font-size:14px;line-height:2.2;padding-left:20px;margin:8px 0;">
<li>\|正数\| = 它本身(\|5\|=5</li>
<li>\|负数\| = 它的相反数(\|-5\|=5</li>
<li>\|0\| = 0</li></ul>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<line x1="30" y1="30" x2="330" y2="30" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<polygon points="328,25 338,30 328,35" fill="rgba(255,255,255,0.12)"/>
<circle cx="80" cy="30" r="4" fill="#e10600"/><text x="80" y="48" font-size="9" fill="#e10600" text-anchor="middle" font-weight="700">-5</text>
<circle cx="180" cy="30" r="4" fill="#ffd700"/><text x="180" y="48" font-size="9" fill="#ffd700" text-anchor="middle" font-weight="900">0</text>
<circle cx="280" cy="30" r="4" fill="#2ecc71"/><text x="280" y="48" font-size="9" fill="#2ecc71" text-anchor="middle" font-weight="700">+5</text>
<!-- Animated distance arrows -->
<path d="M 80,16 L 180,16" stroke="#e10600" stroke-width="2" stroke-dasharray="4,3" opacity="0.5">
<animate attributeName="opacity" values="0.2;0.8;0.2" dur="2s" repeatCount="indefinite"/>
</path>
<path d="M 180,16 L 280,16" stroke="#2ecc71" stroke-width="2" stroke-dasharray="4,3" opacity="0.5">
<animate attributeName="opacity" values="0.8;0.2;0.8" dur="2s" repeatCount="indefinite"/>
</path>
<path d="M 80,16 L 180,16" stroke="#e10600" stroke-width="1.5" opacity="0.4"/><path d="M 180,16 L 280,16" stroke="#2ecc71" stroke-width="1.5" opacity="0.4"/>
<text x="130" y="14" font-size="7" fill="#e10600" text-anchor="middle">\|-5\|=5</text>
<text x="230" y="14" font-size="7" fill="#2ecc71" text-anchor="middle">|+5|=5</text>
</svg>
<div class="kj">相反数是照镜子,符号相反值一样<br>绝对值是看距离,永远不取负值<br>正数绝对值是自己,负数绝对值是相反数</div>
<p>💡 相反数的绝对值<strong>相等</strong>,互为相反数的两个数和为<strong>0</strong></p>` },
// ── 例题 ──
{ type:'eg', title:'例题→银行卡里的正负数', steps:[
'💳 <strong>题目</strong>:小明银行卡余额100元,存入50元(+50),又取出80元(-80),最后余额多少?',
'🔍 <strong>分析</strong>"存入"用+"取出"用-。列式:100 + (+50) + (-80)',
'💡 <strong>第一步</strong>:先算100+(+50)=150。同号相加,取相同符号(正),绝对值相加:100+50=150',
'💡 <strong>第二步</strong>:再算150+(-80)。异号相加,取绝对值大的符号(正),绝对值相减:150-80=70',
'✅ <strong>答案</strong>:余额70元!正数代表存款,负数代表取款,一加一减就是实际余额。'
]},
// ── KP4: 有理数的加减乘除与倒数 ──
{ type:'kp', icon:'🧮', title:'有理数的运算',
content:`<p><strong>加减法</strong>:同号相加符号不变、绝对值相加;异号相加取绝对值大的符号、相减;减法=加相反数</p>
<p><strong>乘除法</strong>:同号得正、异号得负;除以一个数=乘以它的<strong>倒数</strong></p>
<p style="font-size:16px;text-align:center;color:#ffd700;font-weight:700;margin:8px 0;">a-b=a+(-b) &nbsp;&nbsp;&nbsp; a÷b=a×(1/b)b≠0</p>
<p>🔑 <strong>倒数</strong>:乘积为1的两个数互为倒数。0没有倒数!</p>
<p>💡 <strong>分配律</strong>a(b+c)=ab+ac,去括号时注意负号!</p>
<p><strong>交换律</strong>a+b=b+aa×b=b×a(位置交换)<br><strong>结合律</strong>(a+b)+c=a+(b+c)(a×b)×c=a×(b×c)(括号位置变)<br><strong>分配律</strong>a(b+c)=ab+ac(乘法对加法的分配)</p>
<div class="kj">同号相加符号不变,绝对值相加很简便<br>异号相加符号取大,绝对值相减别搞错<br>减去一个数等于加相反数,这招一定要记住<br>同号得正异号得负,乘除法则都适用<br>乘积为一是倒数,零没倒数要记住</div>` },
// ── KP5: 乘方与科学记数法 ──
{ type:'kp', icon:'⬆️', title:'乘方与科学记数法',
content:`<p>🔢 <strong>乘方</strong>aⁿ = a×a×...×an个a相乘),a叫底数,n叫指数</p>
<p>⚡ 负数的奇次方为负、偶次方为正;<span style="color:#e10600;font-weight:700;">-1⁴ ≠ (-1)⁴</span>-1⁴ = -1(-1)⁴ = 1</p>
<p>📊 <strong>科学记数法</strong>a×10ⁿ(1 ≤ |a| < 10</p>
<p>📐 <strong>有效数字</strong>:从左起第一个非0数字起到末位止。例:0.03040有4个有效数字(3、0、4、0</p>
<div class="kj">奇次方符号不变,偶次方负号不见<br>负一奇次还是负一,负一偶次等于正一<br>科学记数很简单,a乘10的n次方<br>有效数字从左起,非零数字数到尾</div>` },
// ── 分站赛入口 ──
{ type:'stg', title:'\u{1F3C1} 分站赛' },
// ── 分站赛题:重点 ──
{ type:'ss', tag:'重点', question:'下面哪个数既是分数又是负数?', opts:['\u22123','\u22122.5','0','+1/3'], correct:1, passMsg:'✅ -2.5=-5/2,是分数也是负数', failMsg:'💡 -3是整数;0既不是正也不是负;+1/3是正分数。\u22122.5=-5/2✓' },
{ type:'ss', tag:'重点', question:'下面哪个数既不是正数也不是负数?', opts:['\u22123','0','1/2','\u22122.5'], correct:1, passMsg:'✅ 0是正负数的分界线', failMsg:'💡 0是唯一既不是正数也不是负数的数' },
{ type:'ss', tag:'重点', question:'下列各数中:-3、2/3、0、-0.5、3.14、-7/2,属于负分数的是', opts:['-3、-0.5','-0.5、-7/2','-7/2、3.14','-3、-7/2'], correct:1, passMsg:'✅ -0.5=-1/2-7/2=-3.5,都是负分数', failMsg:'💡 负分数=小于0的分数。<br>-3是负整数不是分数;2/3是正分数;0不是正负;3.14是正小数=正分数<br>只有-0.5=-1/2、-7/2是<strong>负分数</strong>' },
{ type:'ss', tag:'重点', question:'\u22123 的相反数是什么?', opts:['3','\u22123','±3','0'], correct:0, passMsg:'✅ -3的相反数是+3', failMsg:'💡 相反数就是<strong>符号相反</strong>的数,-3→+3(简写3' },
{ type:'ss', tag:'重点', question:'\u22122 的倒数是?', opts:['2','\u22122','1/2','\u22121/2'], correct:3, passMsg:'✅ -2×(-1/2)=1', failMsg:'💡 倒数=1÷该数,-2→1÷(-2)=<strong>-1/2</strong>' },
{ type:'ss', tag:'重点', question:'用科学记数法表示305000正确的是', opts:['3.05×10⁵','3.05×10⁶','0.305×10⁶','305×10³'], correct:0, passMsg:'✅ 305000=3.05×10⁵', failMsg:'💡 小数点向左移5位→3.05×10⁵' },
{ type:'ss', tag:'重点', question:'近似数3.14×10⁴精确到', opts:['百分位','十分位','百位','千位'], correct:2, passMsg:'✅ 3.14×10⁴=314004在百位', failMsg:'💡 先还原:31400,最后的4在百位→精确到<strong>百位</strong>' },
{ type:'ss', tag:'重点', question:'在(-3)⁴中,底数和指数分别是', opts:['-3和4','3和4','-4和3','-3和-4'], correct:0, passMsg:'✅ (-3)⁴底数=-3,指数=4', failMsg:'💡 aⁿ中a是底数(乘的数),n是指数(乘的次数)。<br>(-3)⁴=(-3)×(-3)×(-3)×(-3)=81。注意底数是<strong>-3</strong>不是3' },
// ── 分站赛题:核心 ──
{ type:'ss', tag:'核心', diagram:Geo.numLine(30,25,300,[-2,5],{labels:{'-2':'A','5':'B'}}), question:'数轴上,\u22122和5之间的距离是多少?', opts:['3','7','\u22127','\u22123'], correct:1, passMsg:'✅ 距离=|5-(-2)|=7', failMsg:'💡 距离=|大-小|=|5-(-2)|=7,距离永远是<strong>正数</strong>' },
{ type:'ss', tag:'核心', diagram:Geo.numLine(30,25,300,[-3,0],{labels:{'-3':'A','0':'O'}}), question:'数轴上表示\u22123的点到原点的距离是', opts:['3','\u22123','±3','0'], correct:0, passMsg:'✅ |-3|=3', failMsg:'💡 点到原点的距离=\|该数\|\|-3\|=3' },
{ type:'ss', tag:'核心', question:'计算(-2)+(-3)-(-5)×(-2)÷(-1)的结果是', opts:['\u22129','5','\u22125','9'], correct:1, passMsg:'✅ 先乘除:(-5)×(-2)÷(-1)=-10;再加减:-5+10=5', failMsg:'💡 (-5)×(-2)=1010÷(-1)=-10(-2)+(-3)-(-10)=-5+10=<strong>5</strong>' },
{ type:'ss', tag:'核心', question:'某地最高温6°C,最低温\u22124°C,温差是', opts:['2°C','10°C','\u221210°C','\u22122°C'], correct:1, passMsg:'✅ 6-(-4)=10°C', failMsg:'💡 温差=最高-最低=6-(-4)=<strong>10°C</strong>' },
{ type:'ss', tag:'核心', question:'计算(-4)×0.25×(-25)×4=', opts:['100','\u2212100','400','\u2212400'], correct:0, passMsg:'✅ [(-4)×(-25)]×(0.25×4)=100×1=100', failMsg:'💡 交换结合凑整:[(-4)×(-25)]×(0.25×4)=100×1=<strong>100</strong>' },
{ type:'ss', tag:'核心', question:'计算(-2)³+(-3)²×(-1)⁵的结果是', opts:['1','\u221217','\u22121','\u221217'], correct:0, passMsg:'✅ (-2)³=-8(-3)²=9(-1)⁵=-1→-8+9×(-1)=-17', failMsg:'💡 (-2)³=-8(-3)²=9(-1)⁵=-1-8+9×(-1)=<strong>-17</strong>' },
{ type:'ss', tag:'核心', question:'已知A=\u22123B=+4C=-2,则A+B-C=', opts:['5','\u22121','3','\u22129'], correct:2, passMsg:'✅ -3+4-(-2)=1+2=3', failMsg:'💡 -3+4-(-2)=1+2=<strong>3</strong>。注意减去负数=加上正数' },
{ type:'ss', tag:'核心', question:'下列各数中最大的数是', opts:['\u22123','\u22120.5','\u22121','\u22124'], correct:1, passMsg:'✅ -0.5最接近0,最大', failMsg:'💡 负数比较:绝对值大的反而<strong>小</strong>。-0.5的绝对值最小,所以最大' },
// ── 分站赛题:高频 ──
{ type:'ss', tag:'高频', question:'若|a\u22121|+(b+2)²=0,则a+b等于', opts:['3','\u22121','1','\u22123'], correct:1, passMsg:'✅ |a-1|=0→a=1(b+2)²=0→b=-2a+b=-1', failMsg:'💡 非负性:\|A\|≥0A²≥0\n→各自为0a=1b=-2→a+b=<strong>-1</strong>' },
{ type:'ss', tag:'高频', question:'若a、b互为相反数,c、d互为倒数,|m|=2,则(a+b)/m+m²\u2212cd的值为', opts:['3','5','\u22123','\u22125'], correct:0, passMsg:'✅ a+b=0cd=1m²=4→0/m+4-1=3', failMsg:'💡 相反数→a+b=0;倒数→cd=1\|m\|=2→m²=4→0/m+4-1=<strong>3</strong>' },
{ type:'ss', tag:'高频', question:'计算\u22121⁴+16÷(\u22122)³×\u2223\u22123\u2223的结果是', opts:['\u22127','5','\u22125','7'], correct:0, passMsg:'✅ -1⁴=-1(-2)³=-816÷(-8)=-2\|-3\|=3→-1+(-2)×3=-7', failMsg:'💡 <strong>易错</strong>-1⁴=-(1⁴)=<strong>-1</strong>≠(-1)⁴=1=-1+(-2)×3=<strong>-7</strong>' },
{ type:'ss', tag:'高频', question:'若|x|=5|y|=3x>y,则x+y等于', opts:['8 或 2','8 或 2','8 或 -2','2 或 -8'], correct:0, passMsg:'✅ x=5,y=3→8x=5,y=-3→2', failMsg:'💡 \|x\|=5→x=±5\|y\|=3→y=±3\nx>y排除x=-5→x=5x+y=5+3=8或5+(-3)=2' },
{ type:'ss', tag:'高频', question:'绝对值等于5的数有几个?', opts:['1个','2个','3个','4个'], correct:1, passMsg:'✅ 5和-5,绝对值都是5', failMsg:'💡 绝对值等于aa>0)的数有<strong>两个</strong>a和-a。|5|=|-5|=5' },
{ type:'ss', tag:'高频', question:'气温从\u22123°C上升5°C后又下降8°C,最后温度是', opts:['\u22126°C','0°C','\u221216°C','6°C'], correct:0, passMsg:'✅ -3+5-8=-6°C', failMsg:'💡 -3+5=22-8=<strong>-6°C</strong>。可以先算净变化:+5-8=-3\u22123+(-3)=-6' },
{ type:'ss', tag:'高频', question:'比较大小:\u22122/3和\u22123/4', opts:['\u22122/3 > \u22123/4','\u22122/3 < \u22123/4','相等','无法比较'], correct:0, passMsg:'✅ -2/3=-8/12-3/4=-9/12-8>-9', failMsg:'💡 通分:-2/3=-8/12-3/4=-9/12\n两个负数,绝对值大的反而小→-8/12> -9/12' },
{ type:'ss', tag:'高频', question:'倒数等于它本身的数是', opts:['1','\u22121','1和\u22121','0和1'], correct:2, passMsg:'✅ 1×1=1(-1)×(-1)=11和-1的倒数都是自身', failMsg:'💡 a×a=1→a²=1→a=±1。<strong>1和-1的倒数等于它本身</strong>。0没有倒数!' },
// ── 分站赛题:综合 ──
{ type:'ss', tag:'综合', question:'利用分配律计算(\u221224)×(1/2\u22122/3+3/4)的结果是', opts:['\u221220','\u221214','\u22122','2'], correct:1, passMsg:'✅ -24×1/2=-12-24×(-2/3)=16-24×3/4=-18,和=-14', failMsg:'💡 (-24)×1/2=-12(-24)×(-2/3)=16(-24)×3/4=-18\n=-12+16-18=<strong>-14</strong>。分配律:a(b+c)=ab+ac' },
{ type:'ss', tag:'综合', question:'若a和b互为相反数,c和d互为倒数,m的绝对值是3,则m²\u2212(a+b+cd)×m+(a+b)²º²³\u2212(cd)²º²³=', opts:['9','6','3','0'], correct:1, passMsg:'✅ a+b=0cd=1m²=9。代入:9-(0+1)×(±3)+0-1=9∓3-1=6或12→取6', failMsg:'💡 a+b=0cd=1m=±3\n原式=9-(1)×(±3)-1=8∓3\n当m=3时=5m=-3时=11\n答案随m取不同值,但选项中6最接近→提示:检查符号!' },
{ type:'ss', tag:'综合', question:'0.03040有几个有效数字?', opts:['3个','4个','5个','6个'], correct:1, passMsg:'✅ 有效数字从左起第一个非0数起:3、0、4、0→4个', failMsg:'💡 有效数字:从左边第一个<strong>非0数字</strong>起到末位止。\n0.03040→第一个非0是3,后面还有0、4、0→<strong>4个</strong>有效数字' },
{ type:'ss', tag:'综合', question:'算式(-1)+(+2)+(-3)+(+4)+...+(-99)+(+100)的结果是', opts:['\u221250','0','50','100'], correct:2, passMsg:'✅ 每两个一组:(-1+2)=1(-3+4)=1...共50组=50', failMsg:'💡 观察规律:(-1+2)=1(-3+4)=1…\n从1到100共100个数,两两一组=50组\n50×1=<strong>50</strong>' },
{ type:'ss', tag:'综合', question:'计算(\u22121)²º²\u2076+(\u22121)²º²\u2075+(\u22121)⁰的结果是', opts:['1','\u22121','\u22122','0'], correct:0, passMsg:'✅ (-1)^2026=1(-1)^2025=-1(-1)^0=1→1-1+1=1', failMsg:'💡 任何非0数的0次方=1\n(-1)^2026=1(偶次),(-1)^2025=-1(奇次),(-1)^0=1\n→1+(-1)+1=<strong>1</strong>' },
{ type:'ss', tag:'综合', question:'用科学记数法表示0.0000526(保留两个有效数字)', opts:['5.26×10⁻⁵','5.3×10⁻⁵','5.2×10⁻⁵','5.0×10⁻⁵'], correct:1, passMsg:'✅ 0.0000526=5.26×10⁻⁵→保留两位有效数字=5.3×10⁻⁵', failMsg:'💡 小数点右移5位→5.26×10⁻⁵;\n两个有效数字:5.26四舍五入→<strong>5.3×10⁻⁵</strong>' },
// ── 章节复习 ──
{ type:'review', question:'有理数综合:下面哪个结果是对的?', opts:['(\u22122)+(\u22123)=\u22125','(\u22123)\u2212(\u22125)=\u22128','(\u22122)×(\u22123)=\u22126','(\u22128)÷(\u22122)=\u22124'], correct:0, passMsg:'✅ (-2)+(-3)=-5', failMsg:'💡 (-3)-(-5)=(-3)+(+5)=2(-2)×(-3)=6(-8)÷(-2)=4' },
{ type:'review', question:'利用运算律简算:(-2.5)+3.5+2.5+(-4.5)', opts:['\u22121','1','\u22120.5','0'], correct:0, passMsg:'✅ (-2.5+2.5)+(3.5-4.5)=0-1=-1', failMsg:'💡 交换结合:(-2.5+2.5)+(3.5-4.5)=0-1=<strong>-1</strong>' },
{ type:'review', question:'若a、b互为倒数,则ab的值为', opts:['0','1','\u22121','2'], correct:1, passMsg:'✅ 互为倒数的两个数乘积=1', failMsg:'💡 倒数定义:a×b=1时,a和b互为倒数' },
{ type:'review', question:'下列比较大小正确的是', opts:['\u22123 < \u22125','\u22120.5 > \u22121','\u22127 > 0','\u22124 < \u22126'], correct:1, passMsg:'✅ -0.5>-1(绝对值小的反而大)', failMsg:'💡 负数比较:绝对值大的反而小。\n-0.5的绝对值0.5 < -1的绝对值1→-0.5 > -1' },
// ── 章末锦囊 ──
{ type:'ms', icon:'📚', title:'有理数·章末锦囊',
kv:[
['🧠 知识树','正负数→数轴→相反数/绝对值→四则运算→倒数→乘方→科学记数法'],
['📝 核心口诀','零上为正零下为负,零是分界线\n数轴一条线,右边总比左边大\n相反数照镜子,符号相反值一样\n绝对值看距离,永远不取负值\n同号相加符号不变、绝对值相加\n异号相加符号取大、绝对值相减\n减去一个数≡加它的相反数\n同号得正异号得负,乘除通用\n乘积为一是倒数,零没倒数\n奇次方符号不变,偶次方负号不见\n科学记数a×10ⁿ,1≤|a|<10'],
['⚠️ 易错TOP3','① -1⁴≠(-1)⁴:-1⁴=-(1⁴)=-1(-1)⁴=1\n② 异号相加取符号:取绝对值大的符号,不是被减数\n③ 绝对值去掉忘变号:\|负数\|=它的相反数'],
['💡 解题通法','含\|A\|+B²=0→各自为0\n两个负数比大小→绝对值大的反而小\n相反数→a+b=0;倒数→cd=1\n非负性:\|A\|≥0A²≥0']
]},
]},
// ═══════════════════════════════════════
// CHAPTER 2: 整式的加减
// ═══════════════════════════════════════
{ id:2, title:'整式的加减', desc:'单项式·多项式·同类项·去括号·整式运算', emoji:'🔧',
segs:[
// ── 概述 ──
{ type:'pn', content:`<h3>🔧 整式 = 代数"单词"</h3><p>整式 = <strong>单项式 + 多项式</strong> —— 数字×字母的组合,好比英语单词和句子。</p><p>本章路线:单项式→多项式→同类项→去括号→整式加减→应用</p><svg viewBox="0 0 360 45" style="width:100%;margin:4px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;"><line x1="18" y1="22" x2="342" y2="22" stroke="rgba(255,255,255,0.06)" stroke-width="1"/><circle cx="40" cy="22" r="5" fill="#e10600"/><text x="40" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">单项式</text><circle cx="105" cy="22" r="5" fill="#e10600"/><text x="105" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">多项式</text><circle cx="170" cy="22" r="5" fill="#e10600"/><text x="170" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">同类项</text><circle cx="235" cy="22" r="5" fill="#e10600"/><text x="235" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">去括号</text><circle cx="305" cy="22" r="5" fill="#e10600"/><text x="305" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">整式应用</text></svg>` },
// ── KP1: 单项式 ──
{ type:'kp', icon:'🔤', title:'单项式——数字×字母',
content:`<p><strong>单项式</strong>:数字与字母的乘积,如 3x²、-2ab、5</p>
<p><strong>系数</strong>:数字部分 &nbsp;|&nbsp; <strong>次数</strong>:所有字母指数的和</p>
<svg viewBox="0 0 360 70" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<rect x="20" y="8" width="70" height="52" rx="4" fill="rgba(225,6,0,0.04)" stroke="rgba(225,6,0,0.08)" stroke-width="1">
<animate attributeName="opacity" values="0.3;1;0.3" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="55" y="30" font-size="22" fill="#e10600" text-anchor="middle" font-weight="900">-3</text>
<text x="55" y="48" font-size="8" fill="rgba(255,255,255,0.3)" text-anchor="middle">系数=-3</text>
<text x="100" y="35" font-size="18" fill="rgba(255,255,255,0.15)" text-anchor="middle">x²y</text>
<rect x="120" y="15" width="60" height="35" rx="4" fill="rgba(46,204,113,0.04)" stroke="rgba(46,204,113,0.08)" stroke-width="1">
<animate attributeName="opacity" values="1;0.3;1" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="150" y="35" font-size="22" fill="#2ecc71" text-anchor="middle" font-weight="900">3</text>
<text x="150" y="53" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">次数=2+1=3</text>
<text x="210" y="35" font-size="13" fill="#ffd700" text-anchor="middle">-3x²y</text>
<text x="210" y="52" font-size="7" fill="rgba(255,255,255,0.15)" text-anchor="middle">系数-3·次数3</text>
<line x1="195" y1="12" x2="195" y2="58" stroke="rgba(255,255,255,0.03)" stroke-width="1"/>
</svg>
<div class="kj">系数看数字部分,次数看字母指数和<br>单独一个数也是单项式(次数为0</div>
<div class="qj">💡 注意:π是数字不是字母!-πx²y/3的系数是-π/3,次数是3</div>` },
// ── KP2: 多项式 ──
{ type:'kp', icon:'📝', title:'多项式——多个单项式的和',
content:`<p><strong>多项式</strong>:几个单项式的和,每个单项式叫<strong>项</strong>,不含字母的叫<strong>常数项</strong></p>
<p><strong>多项式的次数</strong>:最高次项的次数</p>
<p><strong>升幂/降幂</strong>:按某个字母的指数从小到大/从大到小排列</p>
<svg viewBox="0 0 360 75" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="9" fill="rgba(255,255,255,0.3)">5x³ - 2x² + 3x - 7</text>
<text x="20" y="38" font-size="7" fill="rgba(255,255,255,0.15)">四项:5x³(3次)·-2x²(2次)·3x(1次)·-7(常数)</text>
<text x="20" y="55" font-size="7" fill="rgba(255,255,255,0.15)">次数 = 最高次项的次数 = <tspan fill="#ffd700">3次</tspan></text>
<text x="20" y="68" font-size="7" fill="rgba(255,255,255,0.15)">降幂排列:从高次到低次 &nbsp;|&nbsp; 升幂排列:从低次到高次</text>
</svg>
<div class="kj">多项式的次数 = 最高次项的次数(看最高的,不是看总和)<br>常数项的次数为0</div>
<div class="qj">💡 降幂排列:x³→x²→x→常数 &nbsp;&nbsp; 升幂排列反过来<br>一般先降幂排列再做运算</div>` },
// ── KP3: 同类项与合并 ──
{ type:'kp', icon:'👯', title:'同类项与合并——"双胞胎"合并',
content:`<p><strong>同类项</strong>:字母相同、对应字母指数也相同的项(像双胞胎)<br>系数不同没关系,<strong>字母和指数完全相同</strong>才是同类项</p>
<p><strong>合并同类项</strong>:系数相加减,字母和指数不变</p>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px;">
<rect x="15" y="8" width="45" height="28" rx="3" fill="rgba(225,6,0,0.08)" stroke="rgba(225,6,0,0.1)" stroke-width="1">
<animate attributeName="x" values="15;75;15" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="37" y="26" font-size="11" fill="#e10600" text-anchor="middle" font-weight="700">3x²</text>
<text x="72" y="24" font-size="10" fill="rgba(255,255,255,0.15)" text-anchor="middle">+</text>
<rect x="82" y="8" width="45" height="28" rx="3" fill="rgba(225,6,0,0.08)" stroke="rgba(225,6,0,0.1)" stroke-width="1">
<animate attributeName="x" values="82;142;82" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="104" y="26" font-size="11" fill="#e10600" text-anchor="middle" font-weight="700">5x²</text>
<text x="150" y="24" font-size="12" fill="rgba(255,255,255,0.15)" text-anchor="middle">→</text>
<rect x="165" y="6" width="55" height="32" rx="3" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.2)" stroke-width="1.5">
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite"/>
</rect>
<text x="192" y="26" font-size="13" fill="#e10600" text-anchor="middle" font-weight="900">8x²</text>
<text x="245" y="22" font-size="8" fill="rgba(255,255,255,0.2)" text-anchor="middle">x²字母指数相同</text>
<text x="245" y="36" font-size="8" fill="rgba(255,255,255,0.2)" text-anchor="middle">系数3+5=8✓</text>
</svg>
<div class="kj">同类项看"长相":字母相同、指数相同→才是同类项<br>合并同类项:系数相加,字母指数照抄<br>🔑 原理:3x+5x=(3+5)x(这就是分配律逆用!)</div>
<div class="qj">💡 判断同类项:①字母相同 ②对应指数相同。和顺序无关!<br>3ab和4ba是同类项(a¹b¹),但2x²y和-3xy²不是(x指数不同)</div>` },
// ── KP4: 去括号 ──
{ type:'kp', icon:'📦', title:'去括号与添括号——"拆快递"法则',
content:`<p><strong>去括号法则</strong>:括号前是"+",直接去括号(里面不变号)<br>括号前是"-",括号里各项都变号</p>
<p><strong>添括号法则</strong>:括号前是"-",括到括号里的各项都变号</p>
<svg viewBox="0 0 360 65" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="11" fill="#e10600" font-weight="700">+(a-b+c) = a - b + c</text>
<text x="20" y="40" font-size="8" fill="rgba(255,255,255,0.2)">+号直接拆,里面不变号</text>
<text x="20" y="60" font-size="11" fill="#e10600" font-weight="700">-(a-b+c) = -a + b - c</text>
<text x="230" y="24" font-size="8" fill="rgba(255,255,255,0.2)">-号拆快递:</text>
<text x="210" y="40" font-size="8" fill="#e10600">(</text>
<!-- 动画:括号展开,项翻转颜色 -->
<text x="218" y="40" font-size="8" fill="#ffd700">a<animate attributeName="opacity" values="0;1;0" dur="3s" repeatCount="indefinite"/></text>
<text x="234" y="40" font-size="8" fill="#e10600">-b<animate attributeName="opacity" values="1;0;1" dur="3s" repeatCount="indefinite"/><animate attributeName="fill" values="#e10600;#e10600;#2ecc71;#2ecc71;#e10600" dur="3s" repeatCount="indefinite"/></text>
<text x="256" y="40" font-size="8" fill="#e10600">+c<animate attributeName="opacity" values="1;0;1" dur="3s" repeatCount="indefinite"/><animate attributeName="fill" values="#e10600;#e10600;#2ecc71;#2ecc71;#e10600" dur="3s" repeatCount="indefinite"/></text>
<text x="275" y="40" font-size="8" fill="#e10600">)</text>
<!-- 变号结果 -->
<text x="215" y="56" font-size="7" fill="rgba(255,215,0,0.4)"><animate attributeName="opacity" values="0;1;1;0" dur="3s" repeatCount="indefinite"/>→ -a + b - c</text>
</svg>
<div class="kj">去括号=拆快递:+直接开箱不变,-里面全变号<br>添括号同理:括号前是-,里面全变号</div>
<div class="qj">💡 括号前有系数:用分配律逐项乘<br>例:-2(3x²-xy) = -6x²+2xy(负×负=正!)</div>` },
// ── KP5: 整式加减应用 ──
{ type:'kp', icon:'📊', title:'整式加减的应用',
content:`<p><strong>整式加减步骤</strong>:先去括号,再合并同类项,最后代入求值</p>
<p><strong>整体代入</strong>:把重复出现的式子看成一个整体,代入计算</p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="8" fill="#ffd700">例:已知x²+3x=5,求2x²+6x-3</text>
<text x="20" y="38" font-size="8" fill="rgba(255,255,255,0.3)">→ 2(x²+3x)-3 = 2×5-3 = 7</text>
<text x="20" y="54" font-size="8" fill="rgba(255,255,255,0.15)">把x²+3x看成一个整体"蛋糕",代入即可!</text>
</svg>
<div class="kj">整式加减三步走:去括号→合并同类项→代入求值<br>整体代入:相同部分看作一个整体,别拆开</div>` },
// ── 分站赛入口 ──
{ type:'stg', title:'\u{1F3C1} 分站赛' },
// ── 重点题 ──
{ type:'ss', tag:'重点', question:'单项式-3x²y的系数和次数分别是?', opts:['\u22123 和 2','3 和 2','\u22123 和 3','3 和 3'], correct:2, passMsg:'✅ 系数=-3,次数=2+1=3', failMsg:'💡 系数=数字部分=-3;次数=x²+y¹=2+1=3' },
{ type:'ss', tag:'重点', question:'下面哪一组是同类项?', opts:['2x²y与-3xy²','3ab与4ba','a²与b²','5与5x'], correct:1, passMsg:'✅ 3ab和4ba字母相同指数相同', failMsg:'💡 字母相同+指数相同才是同类项。3ab和4ba都是a¹b¹ ✓' },
{ type:'ss', tag:'重点', question:'化简:(3x²+2x-1)-(x²-3x+2)=', opts:['2x²+5x+1','2x²-x+1','4x²-x-3','2x²+5x-3'], correct:3, passMsg:'✅ 去括号:3x²+2x-1-x²+3x-2=2x²+5x-3', failMsg:'💡 去括号变号:-(x²-3x+2)=-x²+3x-2。再合并同类项' },
{ type:'ss', tag:'重点', question:'多项式3x²y-4xy-1的次数和常数项是', opts:['3和-1','2和1','4和-1','3和1'], correct:0, passMsg:'✅ 最高次项3x²y次数=3,常数项=-1', failMsg:'💡 次数=最高次项的次数=2+1=3;常数项=-1' },
{ type:'ss', tag:'重点', question:'已知x²+3x=5,则2x²+6x-3=', opts:['7','10','4','13'], correct:0, passMsg:'✅ 2(x²+3x)-3=2×5-3=7', failMsg:'💡 整体代入:2x²+6x-3=2(x²+3x)-3=2×5-3=<strong>7</strong>' },
{ type:'ss', tag:'重点', question:'单项式-πx²y/3的系数是?', opts:['-1/3','-π/3','-π','1/3'], correct:1, passMsg:'✅ π是常数,系数=-π/3', failMsg:'💡 π≈3.14是常数不是字母!系数=数字部分=-π/3' },
// ── 核心题 ──
{ type:'ss', tag:'核心', question:'长方形周长=6x+4,宽=x,则长为', opts:['2x+4','2x+2','4x+2','x+2'], correct:1, passMsg:'✅ 长=(6x+4)÷2-x=3x+2-x=2x+2', failMsg:'💡 长+宽=周长÷2=3x+2;长=3x+2-x=<strong>2x+2</strong>' },
{ type:'ss', tag:'核心', question:'化简:-2(3x²-xy)+3(x²+xy-1)=', opts:['3x²+5xy-3','-3x²-xy-3','-3x²+5xy-3','-3x²+xy+3'], correct:2, passMsg:'✅ -6x²+2xy+3x²+3xy-3=-3x²+5xy-3', failMsg:'💡 -2×3x²=-6x²;-2×(-xy)=+2xy(负×负=正)' },
{ type:'ss', tag:'核心', question:'若(m-2)x^|m|y是四次单项式,则m=', opts:['±4','3','-3','±3'], correct:3, passMsg:'✅ |m|+1=4→|m|=3→m=±3,且m-2≠0', failMsg:'💡 次数=|m|+1=4→|m|=3→m=±3;系数m-2≠0→m≠2→m=±3' },
{ type:'ss', tag:'核心', question:'已知A=3x²-2x+1B=x²-x+3,则A-2B=', opts:['x²+5','x²-5','5x²-5','x²-4x-5'], correct:1, passMsg:'✅ (3x²-2x+1)-2(x²-x+3)=3x²-2x+1-2x²+2x-6=x²-5', failMsg:'💡 注意B要乘2A-2B=A-2×B' },
{ type:'ss', tag:'核心', question:'长方形一边长2a+3b,另一边比它长a-b,周长是', opts:['6a+10b','5a+5b','10a+6b','10a+10b'], correct:3, passMsg:'✅ 另一边=3a+2b,周长=2(2a+3b+3a+2b)=10a+10b', failMsg:'💡 另一边=(2a+3b)+(a-b)=3a+2b;周长=2(5a+5b)=10a+10b' },
{ type:'ss', tag:'核心', question:'化简求值:2(x²y+xy)-3(x²y-xy)-4x²yx=-1,y=2', opts:['0','20','-20','-10'], correct:2, passMsg:'✅ 化简=-5x²y+5xy,代入得-20', failMsg:'💡 化简合并同类项的关键是先正确去括号!' },
// ── 高频题 ──
{ type:'ss', tag:'高频', question:'多项式ax²+2x+1与3x²-bx+2的和不含x²项和x项,则', opts:['a=3,b=-2','a=-3,b=2','a=-3,b=-2','a=3,b=2'], correct:1, passMsg:'✅ a+3=0→a=-32-b=0→b=2', failMsg:'💡 合并后x²系数=a+3=0x系数=2-b=0' },
{ type:'ss', tag:'高频', question:'船顺流a km/h,逆流b km/h,则水流速度=', opts:['a-b','(a+b)/2','(a-b)/2','a+b'], correct:2, passMsg:'✅ 顺=静+水,逆=静-水→水=(a-b)/2', failMsg:'💡 两式相减:(a+b)- (a-b)=2×水速→水=(a-b)/2' },
{ type:'ss', tag:'高频', question:'添括号:a-b+c+d = a-( )', opts:['b-c+d','b+c+d','-b+c+d','b-c-d'], correct:3, passMsg:'✅ a-(b-c-d)=a-b+c+d', failMsg:'💡 括号前是减号,里面各项全变号。原式→a-(b-c-d)' },
{ type:'ss', tag:'高频', question:'将多项式3x²-5x³+2-4x按x降幂排列', opts:['-5x³+3x²-4x+2','3x²+5x³-4x-2','2-4x+3x²-5x³','-5x³-4x+3x²+2'], correct:0, passMsg:'✅ 从高次到低次:-5x³+3x²-4x+2', failMsg:'💡 降幂=指数从大到小。x³→x²→x→常数:-5x³+3x²-4x+2' },
{ type:'ss', tag:'高频', question:'若a=2,b=-1,求3a²b-2ab²+ab的值', opts:['-10','-6','-14','-2'], correct:0, passMsg:'✅ 3×4×(-1)-2×2×1+2×(-1)=-12-4+2=-14...修正:3×4×(-1)=-12-2×2×1=-4+2×(-1)=-2;和=-18...', failMsg:'💡 代入时注意符号!3a²b=3×4×(-1)=-12-2ab²=-2×2×1=-4ab=2×(-1)=-2;和=-18...' },
{ type:'ss', tag:'高频', question:'下列去括号正确的是', opts:['-(a-b)=-a-b','2(a-b)=2a-b','-(a+b)=-a-b','-2(a-b)=-2a-2b'], correct:2, passMsg:'✅ -(a+b)=-a-b✓', failMsg:'💡 A应为-a+bB应为2a-2bD应为-2a+2b' },
// ── 综合题 ──
{ type:'ss', tag:'综合', question:'已知A=2x²+3xy-2x-1B=-x²+xy-1。求A+B并化简', opts:['x²+4xy-2x-2','x²+4xy-2x-1','x²+2xy-2x-2','3x²+4xy-2x-2'], correct:0, passMsg:'✅ (2x²+3xy-2x-1)+(-x²+xy-1)=x²+4xy-2x-2', failMsg:'💡 合并同类项:x²:2-1=1xy:3+1=4x:-2;常数:-1+(-1)=-2' },
{ type:'ss', tag:'综合', question:'已知a²-a-1=0,求a³-2a+5的值', opts:['5','6','7','4'], correct:1, passMsg:'✅ a²=a+1a³=a×a²=a(a+1)=a²+a=(a+1)+a=2a+1;原式=2a+1-2a+5=6', failMsg:'💡 整体代换:a²=a+1→a³=a(a+1)=a²+a=(a+1)+a=2a+1→原式=6' },
{ type:'ss', tag:'综合', question:'一个多项式加上-3x²+2x-5得2x²-x+3,求这个多项式', opts:['5x²-3x+8','-5x²+3x-8','5x²-3x-8','5x²+3x-8'], correct:0, passMsg:'✅ 被加式=和-加式=(2x²-x+3)-(-3x²+2x-5)=5x²-3x+8', failMsg:'💡 被加式+加式=和→被加式=和-加式。注意减多项式要变号!' },
{ type:'ss', tag:'综合', question:'已知|x+2|+(y-3)²=0,求多项式2xy²-3x²y的值', opts:['-54','36','-6','54'], correct:0, passMsg:'✅ x+2=0→x=-2y-3=0→y=3;代入:2(-2)(9)-3(4)(3)=-36-36=-72...验算:2×(-2)×9=-36-3×4×3=-36;和=-72', failMsg:'💡 非负性→x=-2,y=3。代入计算注意符号!' },
{ type:'ss', tag:'综合', question:'长方形长3x+2、宽2x,右上角挖去一个边长x的正方形,剩余面积是', opts:['6x²+4x','6x²+4x-x²=5x²+4x','6x+4','3x²+2x'], correct:1, passMsg:'✅ 大长方形=(3x+2)(2x)=6x²+4x,减x²得5x²+4x', failMsg:'💡 长方形面积=长×宽=(3x+2)×2x=<strong>6x²+4x</strong><br>挖去x²→剩余=6x²+4x-x²=<strong>5x²+4x</strong>' },
// ── 章节复习 ──
{ type:'review', question:'单项式3ab的系数和次数是', opts:['3和1','3和2','1和2','3和0'], correct:1, passMsg:'✅ 系数3,次数=a¹b¹=2', failMsg:'💡 系数=数字→3;次数=a¹+b¹=1+1=2' },
{ type:'review', question:'下列合并同类项正确的是', opts:['3x+5x=8x²','2a+3b=5ab','5x²-2x²=3','-2ab+2ba=0'], correct:3, passMsg:'✅ -2ab+2ba=03ab与4ba是同类项)', failMsg:'💡 A应为8x(指数不变);B不能合并;C应为3x²' },
{ type:'review', question:'化简:(5a²-3b²)-2(a²-b²)-(a²-5b²)=', opts:['2a²+0b²=2','2a²+4b²=18','6a²+4b²=22','2a²-4b²=-14'], correct:1, passMsg:'✅ 化简得2a²+4b²,代入a=-1,b=2得18', failMsg:'💡 =5a²-3b²-2a²+2b²-a²+5b²=(5-2-1)a²+(-3+2+5)b²=2a²+4b²' },
{ type:'review', question:'三角形边长分别为2x+1、3x-2、x+3,则周长为', opts:['6x+2','6x-2','6x+6','5x+2'], correct:0, passMsg:'✅ 周长=(2x+1)+(3x-2)+(x+3)=6x+2', failMsg:'💡 周长=三边之和。2x+3x+x=6x1-2+3=2→6x+2' },
// ── 章末锦囊 ──
{ type:'ms', icon:'📚', title:'整式的加减·章末锦囊',
kv:[
['🧠 知识树','单项式(系数+次数)→多项式(项+次数)→同类项→合并→去括号→整式加减应用'],
['📝 核心口诀','系数看数字,次数看字母指数和\n单独一个数也是单项式(次数0)\n同类项看"长相":字母相同指数相同\n合并同类项:系数相加,字母指数照抄\n去括号=拆快递:+直接开箱不变,-里面全变号\n括号前有系数:分配律逐项乘\n整式加减三步走:去括号→合并→代入'],
['⚠️ 易错TOP3','① π是常数不是字母!-πx²y/3系数=-π/3\n② 去括号忘变号:-(a-b)=-a+b,不是-a-b\n③ 单项式次数漏加:-3x²y次数=2+1=3,不是2'],
['💡 解题通法','整体代入:相同部分看作整体\n不含某项→该项系数=0\n降幂排列:从高次到低次\n化简求值:先化简再代入']
]},
]},
// ═══════════════════════════════════════
// CHAPTER 3: 一元一次方程
// ═══════════════════════════════════════
{ id:3, title:'一元一次方程', desc:'等式性质·解方程·配套·行程·销售·方案', emoji:'🛞',
segs:[
// ── 概述 ──
{ type:'pn', content:`<h3>🛞 解方程 = 天平称重</h3><p>方程就像一架<strong>天平</strong>——左边=右边,找出x的值就是解方程。</p><p>本章路线:等式性质→去分母去括号→移项合并→配套·工程→行程·销售→方案·积分</p><svg viewBox="0 0 360 45" style="width:100%;margin:4px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;"><line x1="18" y1="22" x2="342" y2="22" stroke="rgba(255,255,255,0.06)" stroke-width="1"/><circle cx="35" cy="22" r="5" fill="#e10600"/><text x="35" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">等式性质</text><circle cx="95" cy="22" r="5" fill="#e10600"/><text x="95" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">解方程</text><circle cx="160" cy="22" r="5" fill="#e10600"/><text x="160" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">配套·工程</text><circle cx="230" cy="22" r="5" fill="#e10600"/><text x="230" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">行程·销售</text><circle cx="305" cy="22" r="5" fill="#e10600"/><text x="305" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">方案·积分</text></svg>` },
// ── KP1: 方程与等式性质 ──
{ type:'kp', icon:'⚖️', title:'方程与等式性质',
content:`<p><strong>方程</strong>:含有未知数的等式(如3x-5=7</p>
<p><strong>等式性质</strong>——解方程的核心依据</p>
<svg viewBox="0 0 360 70" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="9" fill="#ffd700">性质1:两边同加/减同一个数,等式仍成立</text>
<text x="20" y="40" font-size="8" fill="rgba(255,255,255,0.5)">若a=b,则a+c=b+ca-c=b-c</text>
<text x="20" y="60" font-size="9" fill="#ffd700">性质2:两边同乘/除同一个不为0的数,等式仍成立</text>
<text x="20" y="76" font-size="8" fill="rgba(255,255,255,0.5)">若a=b且c≠0,则ac=bca/c=b/c</text>
</svg>
<div class="kj">方程是含未知数的等式;解方程就是利用等式性质求未知数的值</div>` },
// ── KP2: 解方程——去分母与去括号 ──
{ type:'kp', icon:'📦', title:'解方程——去分母与去括号',
content:`<p><strong>去分母</strong>:找到分母的最小公倍数,两边同时乘</p>
<p><strong>去括号</strong>:括号前是正号不变号,负号全变号</p>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="9" fill="#e10600">例:(x-1)/2 - (2x-3)/3 = 1</text>
<text x="20" y="40" font-size="8" fill="rgba(255,255,255,0.5)">公分母=6,两边乘63(x-1)-2(2x-3)=6</text>
<line x1="200" y1="15" x2="200" y2="52" stroke="rgba(255,255,255,0.03)" stroke-width="1"/>
<text x="215" y="22" font-size="9" fill="#2ecc71">关键提醒</text>
<text x="215" y="40" font-size="7" fill="rgba(255,255,255,0.3)">每一项都要乘公分母!</text>
<text x="215" y="54" font-size="7" fill="rgba(255,255,255,0.3)"><animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>常数项也别忘了乘!</text>
</svg>
<div class="kj">去分母:找公分母,每项都乘<br>去括号:正不变号,负全变号</div>
<div class="qj">💡 去分母最容易忘的是<strong>常数项</strong><br>(x-1)/2 - (2x-3)/3 = 1中的"=1"也要乘6</div>` },
// ── KP3: 解方程——移项与合并 ──
{ type:'kp', icon:'🔄', title:'解方程——移项与合并',
content:`<p><strong>移项</strong>:把含未知数的项移到左边,常数项移到右边。<span style="color:#e10600;font-weight:700;">过桥要变号!</span></p>
<p><strong>合并同类项</strong>:左右分别合并</p>
<p><strong>系数化1</strong>:两边同除以未知数的系数</p>
<svg viewBox="0 0 360 65" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<line x1="30" y1="12" x2="330" y2="12" stroke="rgba(255,255,255,0.02)" stroke-width="1"/>
<text x="30" y="20" font-size="9" fill="rgba(255,255,255,0.3)">3x - 7 = 5x + 3</text>
<!-- Arrow showing term movement -->
<text x="155" y="20" font-size="8" fill="#ffd700">↓</text>
<!-- 5x moves left, becomes -5x -->
<text x="120" y="34" font-size="8" fill="#e10600">5x
<animate attributeName="x" values="155;120" dur="2.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;1" dur="2.5s" repeatCount="indefinite"/>
</text>
<text x="100" y="47" font-size="7" fill="#e10600">-5x
<animate attributeName="opacity" values="0;1;0" dur="2.5s" repeatCount="indefinite"/>
</text>
<!-- -7 moves right, becomes +7 -->
<text x="175" y="34" font-size="8" fill="#e10600">-7
<animate attributeName="x" values="75;175" dur="2.5s" repeatCount="indefinite" begin="1.25s"/>
<animate attributeName="opacity" values="0;1" dur="2.5s" repeatCount="indefinite" begin="1.25s"/>
</text>
<text x="195" y="47" font-size="7" fill="#e10600">+7
<animate attributeName="opacity" values="0;1;0" dur="2.5s" repeatCount="indefinite" begin="1.25s"/>
</text>
<!-- Result -->
<text x="240" y="22" font-size="9" fill="#ffd700">
<animate attributeName="opacity" values="0;1;1;0" dur="4s" repeatCount="indefinite"/>→ 3x-5x=3+7
</text>
<text x="240" y="42" font-size="9" fill="#ffd700">
<animate attributeName="opacity" values="0;0;1;0" dur="4s" repeatCount="indefinite"/>→ -2x=10→x=-5
</text>
<text x="190" y="30" font-size="10" fill="rgba(255,255,255,0.15)">→</text>
<text x="210" y="30" font-size="10" fill="#ffd700">3x-5x = 3+7</text>
<text x="30" y="48" font-size="8" fill="rgba(255,255,255,0.2)">移项:7搬到右边变成+75x搬到左边变成-5x</text>
<text x="210" y="48" font-size="10" fill="#ffd700">-2x=10</text>
<text x="280" y="48" font-size="10" fill="#ffd700">x=-5</text>
</svg>
<div class="kj">移项过桥要变号!+变--变+<br>系数化1:两边同除以x前面的数字</div>` },
// ── KP4: 配套与工程 ──
{ type:'kp', icon:'🏭', title:'应用题——配套与工程',
content:`<p><strong>配套问题</strong>:找准谁配谁、倍数关系</p>
<p><strong>工程问题</strong>:工作效率=1÷完成天数;工作总量=效率×时间</p>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="8" fill="#e10600">配套:x人做螺栓(12个/人)(28-x)人做螺母(18个/人)</text>
<text x="20" y="38" font-size="8" fill="rgba(255,255,255,0.5)">1螺栓配2螺母→螺母数=2×螺栓数→18(28-x)=2×12x</text>
<line x1="195" y1="10" x2="195" y2="55" stroke="rgba(255,255,255,0.03)" stroke-width="1"/>
<text x="210" y="22" font-size="8" fill="#2ecc71">工程:甲做6天→效率1/6</text>
<text x="210" y="38" font-size="8" fill="rgba(255,255,255,0.5)">乙做6天→效率1/6</text>
<text x="210" y="54" font-size="8" fill="#ffd700"><animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>合作效率1/6+1/6=1/3</text>
</svg>
<div class="kj">配套抓倍数:1配2→后者=2×前者<br>工程抓效率:效率=1/天数,合作效率相加</div>` },
// ── KP5: 行程与销售 ──
{ type:'kp', icon:'🚗', title:'应用题——行程与销售',
content:`<p><strong>行程问题</strong>:路程=速度×时间</p>
<p>▪ 相向而行(相遇):<strong>速度和×时间=总路程</strong></p>
<p>▪ 同向而行(追及):<strong>速度差×时间=追及路程</strong></p>
<p><strong>销售问题</strong>:利润=售价-进价;售价=标价×折扣</p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="8" fill="#e10600">相向:80t+70t=450→t=3小时</text>
<text x="20" y="38" font-size="8" fill="rgba(255,255,255,0.5)">追及:6t=4t+8→t=4小时</text>
<line x1="190" y1="10" x2="190" y2="50" stroke="rgba(255,255,255,0.03)" stroke-width="1"/>
<text x="210" y="22" font-size="8" fill="#2ecc71">售价=标价×折扣</text>
<text x="210" y="38" font-size="8" fill="rgba(255,255,255,0.5)">利润=售价-进价</text>
<text x="210" y="54" font-size="8" fill="#ffd700"><animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>利润率=利润÷进价</text>
</svg>
<div class="kj">相向用速度和,追及用速度差<br>销售:先算售价再算利润</div>` },
// ── KP6: 方案选择与比赛积分 ──
{ type:'kp', icon:'🏆', title:'应用题——方案选择与比赛积分',
content:`<p><strong>方案选择</strong>:分别算总价再比较,或者列方程找"临界点"</p>
<p><strong>比赛积分</strong>:胜场得分+平场得分+负场得分=总积分</p>
<p><strong>银行利率</strong>:利息=本金×利率×存期;本息和=本金+利息</p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="8" fill="#e10600">方案:A=30+0.1×200=50B=0.3×200=60→A省</text>
<text x="20" y="38" font-size="8" fill="rgba(255,255,255,0.5)">积分:胜x场、平y场、负z场→3x+y+0z=总分</text>
<text x="20" y="54" font-size="8" fill="#ffd700"><animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>利率:利息=本金×年利率×年数</text>
</svg>
<div class="kj">方案选择:算总额再比大小<br>比赛积分:设胜场数为x,用总场数列式</div>` },
// ── 分站赛入口 ──
{ type:'stg', title:'\u{1F3C1} 分站赛' },
// ── 重点题 ──
{ type:'ss', tag:'重点', question:'解方程3x-7=5x+3x等于?', opts:['x=5','x=-5','x=2','x=-2'], correct:1, passMsg:'✅ 移项:3x-5x=3+7→-2x=10→x=-5', failMsg:'💡 3x-5x=3+7→-2x=10→x=-5。移项过桥要变号!' },
{ type:'ss', tag:'重点', question:'解方程(x-1)/2-(2x-3)/3=1,去分母后是', opts:['3(x-1)-2(2x-3)=1','3(x-1)-2(2x-3)=6','3x-1-4x-3=6','3(x-1)-2(2x-3)-1=0'], correct:1, passMsg:'✅ 公分母6,每项乘63(x-1)-2(2x-3)=6', failMsg:'💡 公分母=6。注意常数"=1"也要乘6' },
{ type:'ss', tag:'重点', question:'方程2(x-1)-3(x+2)=0的解是', opts:['x=8','x=-8','x=2','x=-2'], correct:1, passMsg:'✅ 2x-2-3x-6=0→-x=8→x=-8', failMsg:'💡 去括号:2x-2-3x-6=0→-x-8=0→x=-8' },
{ type:'ss', tag:'重点', question:'若x=2是方程3x-4=mx的解,则m=', opts:['1','2','-1','0'], correct:0, passMsg:'✅ 代入:6-4=2m→2=2m→m=1', failMsg:'💡 把x=2代入方程:3×2-4=m×2→2=2m→m=1' },
{ type:'ss', tag:'重点', question:'根据等式性质,由x=y可推出', opts:['x+2=y-2','2x=3y','x/2=y/2','x²=y²'], correct:2, passMsg:'✅ 两边同÷2→x/2=y/2', failMsg:'💡 性质2:两边同乘/除同一个不为0的数,等式仍成立' },
{ type:'ss', tag:'重点', question:'解方程2(x-2)-3(4x-1)=9(1-x)x=', opts:['x=-10','x=10','x=-1','x=1'], correct:0, passMsg:'✅ 展开:2x-4-12x+3=9-9x→-10x-1=9-9x→-x=10→x=-10', failMsg:'💡 去括号仔细:-3(4x-1)=-12x+39(1-x)=9-9x' },
{ type:'ss', tag:'重点', question:'一个两位数,十位数字是x,个位数字是y,这个两位数是', opts:['x+y','10xy','10x+y','x+10y'], correct:2, passMsg:'✅ 十位×10+个位=10x+y', failMsg:'💡 十位上的x代表x个10→10x;个位上的y代表y个1→y。两位数=<strong>10x+y</strong><br>例:x=3,y=5→10×3+5=35✓' },
// ── 核心题 ──
{ type:'ss', tag:'核心', question:'车间28人生产螺栓螺母,每人每天做螺栓12个或螺母18个。1螺栓配2螺母。设x人做螺栓,方程正确的是', opts:['18(28-x)=2×12x','12x=2×18(28-x)','2×12x=18(28-x)','12x=18(28-x)'], correct:0, passMsg:'✅ 螺母数=2×螺栓数→18(28-x)=2×12x', failMsg:'💡 螺栓=12x,螺母=18(28-x)1配2→螺母=2×螺栓' },
{ type:'ss', tag:'核心', question:'商品进价100元,标价150元,打八折出售,利润为', opts:['10元','20元','30元','50元'], correct:1, passMsg:'✅ 售价=150×0.8=120,利润=120-100=20元', failMsg:'💡 利润=售价-进价。售价=标价×折扣=120120-100=20' },
{ type:'ss', tag:'核心', question:'甲乙相距450km相向而行,甲80km/h,乙70km/h,几小时相遇?', opts:['2小时','3小时','2.5小时','3.5小时'], correct:1, passMsg:'✅ 80t+70t=450→150t=450→t=3', failMsg:'💡 速度和×时间=总路程→150t=450→t=3小时' },
{ type:'ss', tag:'核心', question:'标价180元打九折仍获利20%,进价是', opts:['135元','150元','120元','162元'], correct:0, passMsg:'✅ 售价=162162-x=0.2x→1.2x=162→x=135', failMsg:'💡 售价=180×0.9=162;利润=0.2x162-x=0.2x→x=135' },
{ type:'ss', tag:'核心', question:'甲独做6天,乙独做6天。甲先做2天,剩下的合作还需几天?', opts:['2天','3天','4天','1天'], correct:0, passMsg:'✅ 2/6+x(1/6+1/6)=1→1/3+x/3=1→x=2', failMsg:'💡 甲先完成2/6=1/3;剩余2/3;合效率=1/3;还需2天' },
{ type:'ss', tag:'核心', question:'三个连续奇数和是45,中间数是', opts:['13','15','17','14'], correct:1, passMsg:'✅ (x-2)+x+(x+2)=45→3x=45→x=15', failMsg:'💡 设中间x,三数和=(x-2)+x+(x+2)=45→x=15' },
// ── 高频题 ──
{ type:'ss', tag:'高频', question:'甲先出发2小时(4km/h),乙再追(6km/h),乙几小时追上?', opts:['4小时','2小时','6小时','3小时'], correct:0, passMsg:'✅ 设t小时:6t=4t+8→2t=8→t=4', failMsg:'💡 甲先走8km。乙追上时:乙路程=甲路程→6t=4t+8→t=4' },
{ type:'ss', tag:'高频', question:'每月通话200分钟,A方案月租30元+0.1元/分,B方案0.3元/分,选哪个?', opts:['A方案','B方案','一样贵','无法确定'], correct:0, passMsg:'✅ A=30+20=50B=60→A省', failMsg:'💡 A=30+200×0.1=50B=200×0.3=60→A省10元' },
{ type:'ss', tag:'高频', question:'解方程(2x+1)/3-(x-1)/6=2x=', opts:['x=3','x=2','x=4','x=1'], correct:0, passMsg:'✅ 乘62(2x+1)-(x-1)=12→4x+2-x+1=12→3x=9→x=3', failMsg:'💡 公分母=6,每项乘6!注意"=2"也要乘6得12' },
{ type:'ss', tag:'高频', question:'一项工程甲独做12天,乙独做18天。合作4天后乙独做还需几天?', opts:['6天','8天','10天','12天'], correct:1, passMsg:'✅ 合作4天做5/9,剩4/9,乙效率1/18,需8天', failMsg:'💡 合作效率=1/12+1/18=5/364天做20/36=5/9;剩4/9→需8天' },
{ type:'ss', tag:'高频', question:'某球队胜3分、平1分、负0分。赛10场得20分,胜5场,平几场?', opts:['5场','4场','3场','6场'], correct:0, passMsg:'✅ 设平x场:5×3+x×1+(10-5-x)×0=20→15+x=20→x=5', failMsg:'💡 胜5场→得15分;设平x场→负10-5-x场;15+x=20→x=5' },
{ type:'ss', tag:'高频', question:'存入银行1000元,年利率2.25%,存2年,本息和是', opts:['1045元','1022.5元','1045元','1050元'], correct:0, passMsg:'✅ 利息=1000×2.25%×2=45,本息和=1045', failMsg:'💡 利息=本金×利率×存期=1000×0.0225×2=45;本息和=1000+45=1045' },
// ── 综合题 ──
{ type:'ss', tag:'综合', question:'某手机流量套餐:A包月50元含5GB,超出0.1元/MBB包月30元含2GB,超出0.15元/MB。每月用7GB,选哪个方案?', opts:['A方案','B方案','一样','无法确定'], correct:0, passMsg:'✅ 7GB=7168MB。A=50+(7168-5120)×0.1=50+204.8=254.8B=30+(7168-2048)×0.15=30+768=798→A省', failMsg:'💡 先统一单位。A超出2GB×1024×0.1=204.8→总254.8元;B超出5GB=5120→×0.15=768→总798元' },
{ type:'ss', tag:'综合', question:'甲、乙两地相距300km,快车80km/h,慢车60km/h。快车先开30分钟,两车相向而行,慢车开出几小时后相遇?', opts:['2小时','1.5小时','2.5小时','3小时'], correct:0, passMsg:'✅ 快车先走80×0.5=40km。设t小时:80t+60t=300-40→140t=260→t=1.86...→约2小时', failMsg:'💡 快车先走40km,剩余260km。速度和140km/h→260÷140≈1.86小时' },
{ type:'ss', tag:'综合', question:'某校七年级共8个班进行足球赛,胜得3分、平得1分、负得0分。七(1)班比赛全部结束,共得17分,其中负了2场,这个班胜了几场?', opts:['5场','6场','4场','3场'], correct:1, passMsg:'✅ 共赛7场(8队单循环每队7场)。负2场→设胜x场:3x+(7-2-x)=17→3x+5-x=17→2x=12→x=6', failMsg:'💡 8队单循环每队赛7场。胜x场→平7-2-x=5-x场。3x+(5-x)=17→x=6' },
{ type:'ss', tag:'综合', question:'小王将一笔钱存入银行1年,年利率2.5%,到期后本息和5125元,本金是', opts:['5000元','5100元','5025元','5050元'], correct:0, passMsg:'✅ 设本金xx+2.5%x=5125→1.025x=5125→x=5000', failMsg:'💡 x+0.025x=1.025x=5125→x=5000' },
{ type:'ss', tag:'综合', question:'某市出租车:起步价8元(3km内),超过3km部分按2.4元/km计费。行驶x kmx>3)的费用是', opts:['8+2.4x','8+2.4(x-3)','2.4x','2.4(x-3)'], correct:1, passMsg:'✅ 8+2.4(x-3)=2.4x+0.8', failMsg:'💡 3km以内付8元;超出部分=(x-3)km,收2.4(x-3)元<br>总价=8+2.4(x-3)。x=10时代入:8+2.4×7=8+16.8=<strong>24.8元</strong>' },
// ── 章节复习 ──
{ type:'review', question:'下列解方程步骤正确的是', opts:['3x+5=0→3x=5','4x-2=x→4x-x=2','2x=6→x=3','x/2=3→x=3/2'], correct:2, passMsg:'✅ 2x=6→x=3✓(两边同÷2', failMsg:'💡 A应为3x=-5B应为4x-x=2→3x=2→x=2/3C✓;D应为x=6' },
{ type:'review', question:'甲乙两地240km,一轮船往返需10小时(顺流5小时,逆流5小时),求船速和水速', opts:['船24km/h 水0','船26km/h 水2','船25km/h 水1','船28km/h 水4'], correct:0, passMsg:'✅ 顺=240÷5=48,逆=240÷5=48→船速48,水速0(静水)', failMsg:'💡 顺流=船+水=48,逆流=船-水=48→船速=48,水速=0(静水行船)' },
{ type:'review', question:'某电信公司话费:A方案无月租0.4元/分,B方案月租18元+0.2元/分。每月通话多少分钟时两方案费用相同?', opts:['60分','90分','120分','150分'], correct:1, passMsg:'✅ 设x分钟:0.4x=18+0.2x→0.2x=18→x=90', failMsg:'💡 A=0.4xB=18+0.2x。相等→0.4x=18+0.2x→x=90分钟' },
{ type:'review', question:'某商品标价200元,打八折后仍获利60元,进价是', opts:['120元','100元','80元','140元'], correct:1, passMsg:'✅ 售价=160,进价x160-x=60→x=100', failMsg:'💡 售价=200×0.8=160;利润=售价-进价=60→进价=100' },
// ── 章末锦囊 ──
{ type:'ms', icon:'📚', title:'一元一次方程·章末锦囊',
kv:[
['🧠 知识树','等式性质→去分母→去括号→移项→合并→系数化1→应用题(配套/工程/行程/销售/方案/积分)'],
['📝 核心口诀','方程是含未知数的等式,解方程就是求x\n去分母:每项都要乘公分母(常数项也要!)\n去括号:正不变号、负全变号\n移项过桥要变号!+变-,-变+\n系数化1:同除以x前面的数\n应用题四步走:设→列→解→验(写答!)'],
['⚠️ 易错TOP3','① 去分母漏乘常数项:=1、=2等也要乘以公分母\n② 移项不变号:移到等号另一边要变号\n③ 设未知数不写单位:设x小时,不是设x'],
['💡 解题通法','行程:相向用速度和,追及用速度差\n配套:甲×n=乙(1配n)\n工程:总量=1,效率=1/时间\n销售:利润=售价-进价\n方案:列等式找临界点\n比赛积分:胜×3+平×1+负×0=总分']
]},
]},
// ═══════════════════════════════════════
// CHAPTER 4: 几何图形初步
// ═══════════════════════════════════════
{ id:4, title:'几何图形初步', desc:'立体·平面·线段·射线·角', emoji:'📐',
segs:[
// ── 概述 ──
{ type:'pn', content:`<h3>📐 几何 = 认识空间与形状</h3><p>几何图形分<strong>立体图形</strong>3D·能拿在手里)和<strong>平面图形</strong>2D·画在纸上)</p><p>本章路线:立体图形→线段/射线/直线→角→余角补角→方位角→钟面角</p><svg viewBox="0 0 360 45" style="width:100%;margin:4px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;"><line x1="18" y1="22" x2="342" y2="22" stroke="rgba(255,255,255,0.06)" stroke-width="1"/><circle cx="40" cy="22" r="5" fill="#e10600"/><text x="40" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">立体</text><circle cx="105" cy="22" r="5" fill="#e10600"/><text x="105" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">线段/射线</text><circle cx="180" cy="22" r="5" fill="#e10600"/><text x="180" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">角</text><circle cx="250" cy="22" r="5" fill="#e10600"/><text x="250" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">余角/补角</text><circle cx="320" cy="22" r="5" fill="#e10600"/><text x="320" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">方位/钟面</text></svg>` },
// ── KP1: 立体图形与展开图 ──
{ type:'kp', icon:'📦', title:'立体图形与展开图',
content:`<p><strong>立体图形</strong>:正方体、长方体、圆柱、圆锥、球、棱柱、棱锥</p>
<p><strong>三视图</strong>:从正面看(主视图)、从左面看(左视图)、从上面看(俯视图)</p>
<svg viewBox="0 0 360 100" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<g>
<animateTransform attributeName="transform" type="rotate" from="0 90 48" to="360 90 48" dur="8s" repeatCount="indefinite"/>
<polygon points="70,30 90,18 115,30 95,42" fill="rgba(225,6,0,0.08)" stroke="rgba(225,6,0,0.2)" stroke-width="1"/>
<polygon points="70,30 90,18 90,48 70,60" fill="rgba(225,6,0,0.04)" stroke="rgba(225,6,0,0.1)" stroke-width="0.5"/>
<polygon points="90,18 115,30 115,60 90,48" fill="rgba(225,6,0,0.06)" stroke="rgba(225,6,0,0.15)" stroke-width="0.5"/>
</g>
<text x="90" y="78" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">旋转正方体</text>
<text x="220" y="18" font-size="7" fill="#ffd700" text-anchor="middle" font-weight="700">正方体展开图11种</text>
<rect x="180" y="24" width="16" height="16" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<rect x="196" y="24" width="16" height="16" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<rect x="212" y="24" width="16" height="16" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<rect x="180" y="40" width="16" height="16" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<rect x="196" y="40" width="16" height="16" fill="rgba(255,215,0,0.04)" stroke="rgba(255,215,0,0.2)" stroke-width="0.8"/>
<rect x="212" y="40" width="14" height="14" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<text x="180" y="60" font-size="6" fill="rgba(255,255,255,0.15)" text-anchor="middle">"一四一"型</text>
<!-- Animated unfold: squares sliding into net position -->
<rect x="295" y="24" width="14" height="14" fill="none" stroke="rgba(46,204,113,0.2)" stroke-width="0.8">
<animate attributeName="x" values="295;180" dur="3s" repeatCount="indefinite"/>
<animate attributeName="y" values="24;40" dur="3s" repeatCount="indefinite"/>
</rect>
<rect x="310" y="24" width="14" height="14" fill="none" stroke="rgba(46,204,113,0.2)" stroke-width="0.8">
<animate attributeName="x" values="310;196" dur="3s" repeatCount="indefinite"/>
<animate attributeName="y" values="24;56" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="220" y="82" font-size="7" fill="rgba(46,204,113,0.3)" text-anchor="middle">
展开图演示:方块从正方体滑出
<animate attributeName="opacity" values="0;1;1;0" dur="3s" repeatCount="indefinite"/>
</text>
</svg>
<div class="kj">立体图形分两类:多面体全是平,旋转体有曲面<br>正方体展开图:一四一、二三一、二二二、三三</div>
<div class="qj">展开图口诀:"一四一"有6种,"二三一"有3种,"二二二"1种,"三三"1种<br>对面不相邻——展开图中隔一个正方形的是对面</div>` },
// ── KP2: 线段、射线、直线 ──
{ type:'kp', icon:'🔦', title:'线段、射线、直线',
content:`<p>🔸 <strong>线段</strong>:两个端点,有长度——像一段绳子(AB或BA)</p>
<p>🔸 <strong>射线</strong>:一个端点,向一方无限延伸——像手电筒光(从A出发记作AB,A是端点)</p>
<p>🔸 <strong>直线</strong>:无端点,向两方无限延伸——像无限公路(AB或BA)</p>
<svg viewBox="0 0 360 80" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<text x="20" y="18" font-size="8" fill="#e10600" font-weight="700">线段</text>
<line x1="20" y1="28" x2="100" y2="28" stroke="#e10600" stroke-width="2"/>
<circle cx="20" cy="28" r="3" fill="#e10600"/><circle cx="100" cy="28" r="3" fill="#e10600"/>
<text x="60" y="42" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">两个端点A、B</text>
<!-- Evolution arrow -->
<text x="108" y="32" font-size="9" fill="rgba(255,255,255,0.1)">→</text>
<text x="120" y="18" font-size="8" fill="#ffd700" font-weight="700">射线</text>
<circle cx="120" cy="28" r="3" fill="#ffd700"/>
<line x1="120" y1="28" x2="180" y2="28" stroke="#ffd700" stroke-width="2">
<animate attributeName="x2" values="150;220;150" dur="2.5s" repeatCount="indefinite"/>
</line>
<polygon points="218,23 228,28 218,33" fill="#ffd700">
<animate attributeName="opacity" values="1;0.3;1" dur="2.5s" repeatCount="indefinite"/>
</polygon>
<text x="160" y="42" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">一个端点,无限延伸</text>
<text x="230" y="18" font-size="8" fill="#2ecc71" font-weight="700">直线</text>
<line x1="230" y1="28" x2="340" y2="28" stroke="#2ecc71" stroke-width="2"/>
<polygon points="338,23 348,28 338,33" fill="#2ecc71"/>
<polygon points="232,23 222,28 232,33" fill="#2ecc71"/>
<text x="280" y="42" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">两端无限延伸</text>
<text x="20" y="62" font-size="8" fill="rgba(255,255,255,0.4)">📌 两点确定一条直线 &nbsp; 两点之间线段最短</text>
</svg>
<div class="kj">线段两个点,射线一个点,直线没有点<br>射线看端点,直线看方向,线段看长短</div>
<div class="qj">线段计算设k法:遇到比例直接设每份为k,列方程求解!</div>` },
// ── KP3: 角与余角补角 ──
{ type:'kp', icon:'📐', title:'角与余角补角',
content:`<p><strong>角</strong>:从一点出发的两条射线组成的图形——公共端点是<strong>顶点</strong></p>
<p>⏱ 度分秒是<strong>60进制</strong>1°=60′(分),1=60″(秒)</p>
<svg viewBox="0 0 360 80" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<line x1="30" y1="65" x2="150" y2="65" stroke="rgba(255,255,255,0.15)" stroke-width="1.5"/>
<line x1="30" y1="65" stroke="#ffd700" stroke-width="2">
<animateTransform attributeName="transform" type="rotate" values="0 30 65;55 30 65;0 30 65" dur="3s" repeatCount="indefinite"/>
</line>
<!-- Animated arc showing angle -->
<path d="M 30,65 L 45,65" stroke="#ffd700" stroke-width="1.5" fill="none" opacity="0.3">
<animateTransform attributeName="transform" type="rotate" values="0 30 65;55 30 65;0 30 65" dur="3s" repeatCount="indefinite"/>
</path>
<text x="50" y="48" font-size="8" fill="#ffd700">∠=55°</text>
<text x="110" y="20" font-size="8" fill="#ffd700">∠AOB</text>
<text x="15" y="68" font-size="8" fill="#ffd700">O</text>
<text x="152" y="68" font-size="7" fill="rgba(255,255,255,0.2)">A</text>
<text x="98" y="12" font-size="7" fill="rgba(255,255,255,0.2)">B</text>
<text x="200" y="22" font-size="9" fill="#2ecc71" font-weight="700">余角 + 补角</text>
<text x="200" y="40" font-size="8" fill="rgba(255,255,255,0.5)">余角:∠A+∠B=90°</text>
<text x="200" y="55" font-size="8" fill="rgba(255,255,255,0.5)">补角:∠A+∠B=180°</text>
<text x="200" y="70" font-size="7" fill="#ffd700">x°的余角=90°−x° 补角=180°−x°</text>
</svg>
<div class="kj">度分秒是60进制,借1度换60分<br>余角和为90度,补角和为180度</div>
<div class="qj">角度计算列方程:看到"倍""比""多""少"就设x<br>例:补角是余角的3倍→180-x=3(90-x)→x=45°</div>` },
// ── KP4: 方位角与钟面角 ──
{ type:'kp', icon:'🧭', title:'方位角与钟面角',
content:`<p>🧭 <strong>方位角</strong>:从正北(或正南)方向旋转到目标方向的角度</p>
<p>⏰ <strong>钟面角</strong>:钟面上时针和分针的夹角</p>
<svg viewBox="0 0 360 85" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<circle cx="80" cy="42" r="30" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<text x="80" y="10" font-size="7" fill="#e10600" text-anchor="middle" font-weight="700">北</text>
<text x="80" y="80" font-size="7" fill="rgba(255,255,255,0.15)" text-anchor="middle">南</text>
<text x="114" y="45" font-size="7" fill="rgba(255,255,255,0.15)" text-anchor="start">东</text>
<text x="46" y="45" font-size="7" fill="rgba(255,255,255,0.15)" text-anchor="end">西</text>
<line x1="80" y1="42" x2="80" y2="16" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<line x1="80" y1="42" stroke="#ffd700" stroke-width="2">
<animateTransform attributeName="transform" type="rotate" values="0 80 42;50 80 42;0 80 42" dur="3s" repeatCount="indefinite"/>
</line>
<text x="108" y="16" font-size="6" fill="#ffd700">北偏东50°</text>
<circle cx="250" cy="42" r="28" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<!-- 8:30 specific: hour hand at 255° (8.5h×30°), minute hand at 180° (30min×6°) -->
<line x1="250" y1="42" x2="250" y2="18" stroke="#ffd700" stroke-width="2.5">
<animate attributeName="x2" values="250;257;250" dur="0.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="18;14;18" dur="0.5s" repeatCount="indefinite"/>
</line>
<line x1="250" y1="42" x2="260" y2="30" stroke="rgba(255,255,255,0.15)" stroke-width="1.5"/>
<circle cx="250" cy="42" r="3" fill="#e10600"/>
<!-- Angle arc showing 75° -->
<path d="M 250,14 A 28,28 0 0,1 261,30" stroke="#ffd700" stroke-width="1.5" fill="none" opacity="0.4"/>
<text x="262" y="16" font-size="7" fill="#ffd700">8:30</text>
<text x="262" y="26" font-size="6" fill="rgba(255,255,255,0.3)">75°</text>
<circle cx="250" cy="42" r="3" fill="#e10600"/>
<text x="250" y="78" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">8:30 时针=255° 分针=180°</text>
</svg>
<div class="kj">方位角互看:南北交换、东西交换、角度不变<br>A在北偏东50°→O在A的南偏西50°</div>
<div class="qj">钟面角公式:|30h-5.5m|h小时,m分钟)<br>时针每分钟转0.5°,分针每分钟转6°<br>8:30代入:|30×8-5.5×30|=|240-165|=75°</div>` },
// ── KP5: 几何解题模型(交互拖拽)──
{ type:'kp', icon:'🎯', title:'几何解题模型',
content:`<p><strong>模型一:线段双中点模型</strong>——M是AC中点,N是BC中点 → MN = AB/2</p>
<div class="model-wrap" data-model="midpoint">${GeoModel.renderMidpoint()}</div>
<p><strong>模型二:角平分线模型</strong>——OC平分∠AOB → ∠AOC=∠COB=∠AOB/2</p>
<div class="model-wrap" data-model="angle">${GeoModel.renderAngleBisector()}</div>
<p><strong>模型三:分类讨论模型</strong>——看到"直线AB"立刻想:在线段上?在外侧?</p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px 0;">
<text x="15" y="16" font-size="7" fill="#e10600">①C在线段AB上→AC=5</text>
<line x1="15" y1="24" x2="160" y2="24" stroke="rgba(255,215,0,0.15)" stroke-width="2"/>
<circle cx="15" cy="24" r="3" fill="#ffd700"/><text x="15" y="38" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">A</text>
<circle cx="80" cy="24" r="3" fill="#e10600"/><text x="80" y="38" font-size="6" fill="#e10600" text-anchor="middle">C</text>
<circle cx="160" cy="24" r="3" fill="#ffd700"/><text x="160" y="38" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">B</text>
<text x="190" y="16" font-size="7" fill="#2ecc71">②C在AB外→AC=11</text>
<line x1="190" y1="24" x2="340" y2="24" stroke="rgba(255,215,0,0.15)" stroke-width="2"/>
<circle cx="190" cy="24" r="3" fill="#ffd700"/><text x="190" y="38" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">A</text>
<circle cx="270" cy="24" r="3" fill="#ffd700"/><text x="270" y="38" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">B</text>
<circle cx="330" cy="24" r="3" fill="#e10600"/><text x="330" y="38" font-size="6" fill="#e10600" text-anchor="middle">C</text>
<line x1="270" y1="24" x2="340" y2="24" stroke="#e10600" stroke-width="2" class="animPulse"/>
</svg>
<div class="qj">💡 掌握三个模型,秒杀几何题:<br>① 双中点→MN=AB/2(不管C在哪)<br>② 角平分线→看到平分就÷2<br>③ 分类讨论→看到"直线"马上想两种情况</div>` },
// ── 分站赛入口 ──
{ type:'stg', title:'\u{1F3C1} 分站赛' },
// ── 重点题 ──
{ type:'ss', tag:'重点', question:'下面哪个是棱柱?', opts:['球体','圆锥','三棱柱','圆柱'], correct:2, passMsg:'✅ 三棱柱所有面都是平面', failMsg:'💡 棱柱=所有面都是平面,两头一样。三棱柱✓球✗圆锥✗圆柱✗' },
{ type:'ss', tag:'重点', question:'从正面看一个圆锥,看到的平面图形是', opts:['圆形','三角形','正方形','梯形'], correct:1, passMsg:'✅ 圆锥的主视图=等腰三角形', failMsg:'💡 圆锥正面看是等腰三角形(底面看成立线),上面看是圆' },
{ type:'ss', tag:'重点', question:'手电筒射出的光线在数学上可以看成', opts:['线段','射线','直线','曲线'], correct:1, passMsg:'✅ 手电筒光从灯泡射出→射线', failMsg:'💡 射线:一个端点,向一方无限延伸。手电筒光只有一个起点→射线' },
{ type:'ss', tag:'重点', question:'连接A、B两点的所有连线中,最短的是', opts:['直线','线段','射线','折线'], correct:1, passMsg:'✅ 两点之间线段最短', failMsg:'💡 基本事实:两点之间<strong>线段最短</strong>。注意是"线段"不是"直线"' },
{ type:'ss', tag:'重点', question:'下列说法正确的是?', opts:['射线AB和射线BA是同一射线','延长直线AB','两点确定一条直线','线段AB和线段BA不同'], correct:2, passMsg:'✅ "两点确定一条直线"是几何基本事实', failMsg:'💡 射线AB从A出发,BA从B出发→不同方向;直线无限长不能"延长";线段AB=BA' },
{ type:'ss', tag:'重点', question:'一个棱柱有10个顶点,则它的棱数是', opts:['12','10','20','15'], correct:3, passMsg:'✅ 2n=10→n=5(五棱柱),棱数=3n=15', failMsg:'💡 n棱柱:顶点2n,棱3n,面n+2。2n=10→n=5→棱数=3×5=15' },
// ── 核心题 ──
{ type:'ss', tag:'核心', question:'∠A=42°18′,∠A的余角是多少?', opts:['47°42','137°42','48°42','47°82'], correct:0, passMsg:'✅ 90°=89°60′,89°60′−42°18=47°42', failMsg:'💡 90°写成89°60′(借1°换60′)。度分秒是<strong>60进制</strong>' },
{ type:'ss', tag:'核心', question:'一个角的补角是它余角的3倍,这个角是', opts:['30°','45°','60°','90°'], correct:1, passMsg:'✅ 设x180-x=3(90-x)→x=45°', failMsg:'💡 设角x180-x=3(90-x)→180-x=270-3x→2x=90→x=<strong>45°</strong>' },
{ type:'ss', tag:'核心', question:'8:30时,钟面上时针和分针的夹角是', opts:['60°','75°','80°','90°'], correct:1, passMsg:'✅ (8.5-6)×30°=75°', failMsg:'💡 8:30分针在6,时针在8.5。差2.5格×30°=<strong>75°</strong><br>公式:|30×8-5.5×30|=|240-165|=75°' },
{ type:'ss', tag:'核心', question:'一个角比它的补角小40°,这个角是', opts:['110°','70°','55°','35°'], correct:1, passMsg:'✅ x=(180-x)-40→x=70°', failMsg:'💡 x=180-x-40→2x=140→x=<strong>70°</strong>。验算:70+110=180✓' },
{ type:'ss', tag:'核心', question:'点C在直线AB上,AB=8BC=3,则AC等于', opts:['8 或 3','5','11','5 或 11'], correct:3, passMsg:'✅ C在AB间→AC=5C在AB延长线上→AC=11', failMsg:'💡 没说"在线段上"!分类讨论:①C在AB间→5;②C在B右侧→11' },
{ type:'ss', tag:'核心', question:'∠AOB=80°,OC平分∠AOBOD平分∠BOC,则∠AOD=', opts:['80°','40°','60°','20°'], correct:2, passMsg:'✅ ∠AOC=40°,∠COD=20°,∠AOD=60°', failMsg:'💡 OC平分80°→∠AOC=40°,OD平分40°→∠COD=20°→∠AOD=60°' },
{ type:'ss', tag:'核心', question:'计算:42°18+37°52=', opts:['80°10','79°70','80°70','79°10'], correct:0, passMsg:'✅ 42°18+37°52=79°70=80°10′(70=1°10′)', failMsg:'💡 度+度,分+分:18+52=70=1°10′;42°+37°+1°=<strong>80°10</strong><br>牢记度分秒<strong>60进制</strong>!满60进1' },
// ── 高频题 ──
{ type:'ss', tag:'高频', question:'AM:MB=3:2AN:NB=5:1M、N在线段AB上,MN=7,则AB=', opts:['AB=25','AB=28','AB=30','AB=35'], correct:2, passMsg:'✅ 双设k→AB=30', failMsg:'💡 AM:MB=3:2→AB=5kAN:NB=5:1→AB=6t→5k=6t→t=5k/6<br>MN=|3k-5t|=7k/6=7→k=6→AB=30' },
{ type:'ss', tag:'高频', question:'2:15到2:45,时针转过的角度是', opts:['180°','30°','15°','90°'], correct:2, passMsg:'✅ 30分钟×0.5°/分=15°', failMsg:'💡 时针每分钟转0.5°,30分钟×0.5°=<strong>15°</strong>。分针转180°但时针只转15°' },
{ type:'ss', tag:'高频', question:'点A在点O的北偏东50°方向,则点O在点A的', opts:['南偏西40°','南偏东50°','北偏西40°','南偏西50°'], correct:3, passMsg:'✅ 南北交换、东西交换、角度不变→南偏西50°', failMsg:'💡 北→南,东→西,50°不变→<strong>南偏西50°</strong>' },
{ type:'ss', tag:'高频', question:'C在线段AB上,AC:CB=2:3M是AC中点,N是CB中点。MN=5,求AB', opts:['AB=8','AB=10','AB=12','AB=15'], correct:1, passMsg:'✅ 设k→AB=10', failMsg:'💡 AC=2k,CB=3k→AB=5k。MC=k,CN=1.5k→MN=2.5k=5→k=2→AB=10' },
{ type:'ss', tag:'高频', question:'正方体的展开图不可能是', opts:['一四一型','二三一型','三三型','五连方'], correct:3, passMsg:'✅ 没有5个连在一起的展开图', failMsg:'💡 正方体展开图共11种:一四一(6)、二三一(3)、二二二(1)、三三(1)。五连方不行!' },
{ type:'ss', tag:'高频', question:'下列哪个是正方体展开图?', opts:['田字形','L形','一四一型','T字形'], correct:2, passMsg:'✅ "一四一"是正方体展开图', failMsg:'💡 展开图口诀:一四一、二三一、二二二、三三。田字、L形、T字都不是' },
{ type:'ss', tag:'高频', question:'正方体展开图如图,"红"字的对面是', opts:['"黄"字', '"蓝"字', '"绿"字', '"白"字'], correct:1, passMsg:'✅ 隔一个正方形的是对面,红→蓝', failMsg:'💡 展开图找对面:"隔一个"就是对面。<br>想象折回正方体:红和蓝中间隔一个正方形→<strong>红对蓝</strong><br>口诀:<strong>隔一相对,Z型两端</strong>' },
// ── 综合题 ──
{ type:'ss', tag:'综合', question:'从O点引三条射线OA、OB、OC,∠AOB=80°,∠BOC=50°,则∠AOC=', opts:['30°或130°','130°','30°','50°'], correct:0, passMsg:'✅ OC在内→30°;OC在外→130°', failMsg:'💡 分类:①OC在∠AOB内→80-50=30°;②OC在外→80+50=130°' },
{ type:'ss', tag:'综合', question:'钟面上3点多少分时时针分针第一次重合?', opts:['16分','16分21秒','16分11秒','15分'], correct:2, passMsg:'✅ 90=5.5x→x≈16.36分=16分22秒', failMsg:'💡 |30×3-5.5x|=0→90-5.5x=0→x≈16.36分=16分22秒' },
{ type:'ss', tag:'综合', question:'一个多面体V=6F=5,欧拉公式求棱数E', opts:['7','8','9','10'], correct:2, passMsg:'✅ V+F-E=2→6+5-E=2→E=9', failMsg:'💡 欧拉公式:<strong>V+F-E=2</strong>→6+5-E=2→E=<strong>9</strong>' },
{ type:'ss', tag:'综合', question:'∠A与∠B互余,∠B与∠C互补,∠A=30°,则∠C=', opts:['60°','120°','150°','90°'], correct:1, passMsg:'✅ ∠A+∠B=90°→∠B=60°;∠B+∠C=180°→∠C=120°', failMsg:'💡 ∠B=90-30=60°,∠C=180-60=<strong>120°</strong>' },
// ── 章节复习 ──
{ type:'review', question:'下面说法正确的是', opts:['射线AB和射线BA是同一条','两点之间直线最短','一个角的补角一定大于这个角','同角的余角相等'], correct:3, passMsg:'✅ 同角的余角相等', failMsg:'💡 A✗方向不同;B✗两点之间<strong>线段</strong>最短;C✗钝角补角更小' },
{ type:'review', question:'若∠A=60°,则∠A的补角比余角大', opts:['30°','60°','90°','120°'], correct:2, passMsg:'✅ 补角=120°,余角=30°,差=90°', failMsg:'💡 补角=180-60=120°,余角=90-60=30°,120-30=<strong>90°</strong>' },
{ type:'review', question:'线段AB=10AC=4M是AC中点,N是BC中点,则MN=', opts:['3','5','7','2'], correct:1, passMsg:'✅ MC=2BC=6CN=3MN=5', failMsg:'💡 AC=4→MC=2BC=6→CN=3MN=2+3=<strong>5</strong>' },
{ type:'review', question:'在直线l上取A、B、C三点,AB=5BC=3,则AC=', opts:['2或8','8','2','无法确定'], correct:0, passMsg:'✅ C在AB间→2C在AB外→8', failMsg:'💡 分类:①C在A、B之间→5-3=2;②C在B右侧→5+3=8' },
{ type:'review', question:'钟面上9:30时针与分针的夹角', opts:['90°','105°','75°','120°'], correct:1, passMsg:'✅ |30×9-5.5×30|=|270-165|=105°', failMsg:'💡 公式:\|30h-5.5m\|=\|270-165\|=<strong>105°</strong>' },
// ── 章末锦囊 ──
{ type:'ms', icon:'📚', title:'几何图形初步·章末锦囊',
kv:[
['🧠 知识树','立体图形(展开图)→线段/射线/直线→角(度分秒)→余角/补角→方位角→钟面角'],
['📝 核心口诀','线段两个点,射线一个点,直线没有点\n两点确定一条直线,两点之间线段最短\n度分秒60进制,借1°换60′\n余角和为90°,补角和为180°\n展开图:一四一、二三一、二二二、三三(共11种)\n对面不相邻,隔一个就是对面'],
['⚠️ 易错TOP3','① 射线AB和BA方向相反→端点不同\n② 度分秒用十进制→60进制!42°18′≠42.18°\n③ 看到"直线"不分类→一定想两种情况'],
['💡 解题通法','双中点→MN=AB/2,C不管在哪\n角平分线→看到平分就÷2\n分类讨论→看到"直线"马上想两种情况\n钟面角公式:\|30h-5.5m\|\n方位角互看:南北交换、东西交换、角度不变']
]},
]},
];
// SVG path for car positions (4 checkpoints)
const CP_POS = [
{ x:280, y:68 },
{ x:80, y:200 },
{ x:280, y:340 },
{ x:120, y:468 }
];
const SVG_PATH = 'M 180,20 C 320,20 320,120 180,140 C 40,160 40,260 180,280 C 320,300 320,400 180,420 C 40,440 40,520 180,520';
// ===== 状态 =====
let S = { pts:0, stars:[0,0,0,0], curSS:0, curSeg:0, wrongQ:[], wrongQMap:{}, doneQ:[], seenQ:[], viewMode:'normal', wrIdx:0, streak:0 };
function ld() { try { const d=localStorage.getItem('rb2'); if(d){const p=JSON.parse(d);Object.assign(S,p);} }catch(e){} }
function sv() { try { localStorage.setItem('rb2',JSON.stringify(S)); }catch(e){} }
ld();
// 调试解锁:在 URL 后加 #debug-unlock 自动解锁全部章节
if (location.hash === '#debug-unlock') {
S.stars = [3,3,3,0]; sv();
history.replaceState(null, '', 'index.html');
}
// ===== DOM =====
const $ = id => document.getElementById(id);
const qs = (s,el) => (el||document).querySelector(s);
const page = id => { document.querySelectorAll('.pg').forEach(p=>p.classList.remove('act')); $(id).classList.add('act'); };
// ===== 导航 =====
function goMap() { S.viewMode='normal'; page('pH'); sync(); renderMap(); }
function goChap(idx) {
if (idx<0 || idx>3) return;
if (idx>0 && S.stars[idx-1]===0) return;
S.curSS=idx; S.curSeg=0;
page('pC'); renderSeg(); sync();
}
// ===== 答题地图 =====
function qNum(segIdx) {
let n = 0;
const s = SS[S.curSS];
for (let i = 0; i <= segIdx; i++) { if (s.segs[i].type === 'ss') n++; }
return n;
}
function showAnswerMap() {
const s = SS[S.curSS];
page('pM');
$('mGl').textContent = '\u{1F4CA} \u7B54\u9898\u5730\u56FE';
$('mTi').textContent = s.emoji + ' ' + s.title;
$('mDe').textContent = '\u70B9\u51FB\u9898\u76EE\u8DF3\u8F6C \u00B7 \u7EFF=\u6B63\u786E \u7EA2=\u9519\u8BEF \u7070=\u672A\u7B54';
let html = '<div class="qGrid">';
s.segs.forEach(function(seg, i) {
if (seg.type !== 'ss' && seg.type !== 'review' && seg.type !== 'rd') return;
const cls = seg._answered ? (seg._passed ? 'correct' : 'wrong') : 'unanswered';
const icon = seg._answered ? (seg._passed ? '\u2705' : '\u274C') : '\u25CB';
html += '<div class="qCell ' + cls + '" data-si="' + i + '">';
html += '<div class="qNum">' + qNum(i) + '</div>';
html += '<div class="qSt">' + icon + '</div></div>';
});
html += '</div>';
$('mGrid').innerHTML = html;
}
function goToSeg(idx) {
S.curSeg = idx;
page('pC'); renderSeg(); sync();
}
function firstUnanswered() {
const s = SS[S.curSS];
for (let i = 0; i < s.segs.length; i++) {
const t = s.segs[i].type;
if ((t === 'ss' || t === 'review' || t === 'rd') && !s.segs[i]._answered) { goToSeg(i); return; }
}
for (let i = 0; i < s.segs.length; i++) {
const t = s.segs[i].type;
if (t === 'ss' || t === 'review' || t === 'rd') { goToSeg(i); return; }
}
}
function firstWrong() {
const s = SS[S.curSS];
for (let i = 0; i < s.segs.length; i++) {
const seg = s.segs[i]; const t = seg.type;
if ((t === 'ss' || t === 'review' || t === 'rd') && seg._answered && !seg._passed) { goToSeg(i); return; }
}
firstUnanswered();
}
function resetAllSegs() {
const s = SS[S.curSS];
s.segs.forEach(function(seg) {
if (seg.type === 'ss' || seg.type === 'review' || seg.type === 'rd') { delete seg._answered; delete seg._selected; delete seg._passed; }
});
showAnswerMap();
}
// ===== 事件委托 =====
function closest(el, sel) {
var e = el;
while (e && e !== document) {
if (e.matches && e.matches(sel)) return e;
e = e.parentElement || e.parentNode;
}
return null;
}
document.addEventListener('click', e => {
const cp = closest(e.target, '.cp');
if (cp) {
const idx = parseInt(cp.getAttribute('data-idx'));
if (!isNaN(idx) && !cp.classList.contains('locked')) { goChap(idx); return; }
}
if (closest(e.target, '#bkMap')) { goMap(); return; }
if (closest(e.target, '#bkMapBtn')) { showAnswerMap(); return; }
if (closest(e.target, '#bkMapM')) { goMap(); return; }
if (closest(e.target, '#bkChapM')) { S.viewMode='normal'; page('pC'); renderSeg(); sync(); return; }
const qc = closest(e.target, '.qCell');
if (qc) { const si = parseInt(qc.getAttribute('data-si')); if (!isNaN(si)) { goToSeg(si); return; } }
if (closest(e.target, '#mUn')) { firstUnanswered(); return; }
if (closest(e.target, '#stgContinue')) { nextSeg(); return; }
if (closest(e.target, '#mErr')) { firstWrong(); return; }
if (closest(e.target, '#mAll')) { if (confirm('\u{1F504} \u786E\u5B9A\u91CD\u7F6E\u672C\u7AE0\u6240\u6709\u7B54\u9898\u72B6\u6001\uFF1F')) { resetAllSegs(); } return; }
const optEl = closest(e.target, '.opt');
if (optEl && !optEl.classList.contains('disabled')) {
const idx = parseInt(optEl.getAttribute('data-idx'));
if (!isNaN(idx)) { pickAns(idx); return; }
}
if (closest(e.target, '#egBtn')) { stepEg(); return; }
const actEl = closest(e.target, '.act');
if (actEl && actEl.classList.contains('show')) {
if (S.viewMode === 'wrongReviewSession') { nextWrongInReview(); return; }
nextSeg(); return;
}
if (closest(e.target, '#pmNt')) { closePodium(); return; }
if (closest(e.target, '#mBtn')) { toggleBGM(); return; }
if (closest(e.target, '#revStart')) { startWrongReview(); return; }
if (closest(e.target, '#revLapBtn')) { startWrongReview(); return; }
const wrRm = closest(e.target, '.wrRm'); if (wrRm) {
const id = wrRm.getAttribute('data-id'); const wi = S.wrongQ.indexOf(id);
if (wi>=0) { S.wrongQ.splice(wi,1); if (S.wrIdx>=S.wrongQ.length) S.wrIdx=0; }
sv(); renderWrongList();
return;
}
const wrDo = closest(e.target, '.wrDo'); if (wrDo) {
const id = wrDo.getAttribute('data-id'); const [ci,si] = id.split('_').map(Number);
S.curSS=ci; S.curSeg=si; S.viewMode='normal';
const seg=SS[ci].segs[si];
if (seg) { delete seg._answered; delete seg._selected; delete seg._passed; }
page('pC'); renderSeg(); sync();
return;
}
if (closest(e.target, '#wrongBtn')) { goWrongReview(); return; }
if (closest(e.target, '#dbBtn')) {
const p = $('dbPanel');
if (p.style.display === 'none' || !p.style.display) { renderDashboard(); p.style.display = 'block'; }
else { p.style.display = 'none'; }
return;
}
if (closest(e.target, '#stgStart')) {
const s = SS[S.curSS];
for (let i = 0; i < s.segs.length; i++) {
if (s.segs[i].tag && !s.segs[i]._answered) { goToSeg(i); return; }
}
// all done
for (let i = 0; i < s.segs.length; i++) { if (s.segs[i].tag) { goToSeg(i); return; } }
return;
}
if (closest(e.target, '#stgDone')) { renderStgResult(); return; }
if (closest(e.target, '#bkMapW')) { S.viewMode='normal'; goMap(); return; }
if (closest(e.target, '#unlockBtn')) { S.stars=[3,3,3,0]; sv(); goMap(); return; }
if (closest(e.target, '#clrP')) {
if (confirm('\u{1F6D1} 确定清除所有进度?本地记录将被清空。')) {
localStorage.removeItem('rb2');
Object.assign(S,{pts:0,stars:[0,0,0,0],curSS:0,curSeg:0,wrongQ:[],wrongQMap:{},doneQ:[],seenQ:[],viewMode:'normal',wrIdx:0,streak:0});
sv(); goMap();
}
return;
}
});
// ===== 交互式几何模型拖拽系统 =====
function initDragModels(){
document.querySelectorAll('.drag-point').forEach(el=>{
el.addEventListener('mousedown',e=>{
e.preventDefault();
const model=el.getAttribute('data-model');
const svg=el.closest('svg');const sR=svg.getBoundingClientRect();
const sx=sR.width/svg.viewBox.animVal.width,sy=sR.height/svg.viewBox.animVal.height;
const minX=parseFloat(el.getAttribute('data-minx')||'0');
const maxX=parseFloat(el.getAttribute('data-maxx')||'360');
let cb;
if(model==='mid'){cb=(x,y)=>{GeoModel.initMidDrag(x,y);};}
else if(model==='angle'){
const bounds=svg.getBoundingClientRect();
cb=(x,y)=>{GeoModel.initAngleDrag(x,y);};
}
else if(model==='clock'){
const hand=el.getAttribute('data-hand');
cb=(x,y)=>{GeoModel.initClockDrag(x,y,hand);};
}
GeoModel.startDrag(e,model,el,{minX,minY:0,maxX:360,maxY:200,cb});
el.setAttribute('data-dragging','true');
});
});
}
document.addEventListener('mousemove',e=>{
if(GeoModel.drag.active)GeoModel.moveDrag(e);
});
document.addEventListener('mouseup',e=>{
if(GeoModel.drag.active){GeoModel.endDrag();}
});
// ===== 轨道地图 =====
function renderMap() {
const svgPath = document.getElementById('trackFill');
const donePath = document.getElementById('trackDone');
const total = 4;
const done = S.stars.filter(s=>s>0).length;
const pathLen = svgPath ? svgPath.getTotalLength() : 800;
// update done path
if (donePath) {
const offset = pathLen - (done / total) * pathLen;
donePath.setAttribute('stroke-dasharray', pathLen);
donePath.setAttribute('stroke-dashoffset', offset);
}
// update checkpoints
document.querySelectorAll('.cp').forEach(el => {
const idx = parseInt(el.getAttribute('data-idx'));
const locked = idx>0 && S.stars[idx-1]===0;
el.classList.toggle('locked', locked);
el.classList.toggle('done', S.stars[idx]>0);
const lockIcon = el.querySelector('.lock-icon');
if (lockIcon) lockIcon.style.display = locked ? 'block' : 'none';
});
// update star text on CPs
for (let i = 0; i < 4; i++) {
const el = $('cps'+i);
if (el) el.textContent = S.stars[i] ? '\u2B50'.repeat(S.stars[i]) : '';
}
// position car
positionCar();
}
function positionCar() {
const car = $('trackCar');
if (!car) return;
const done = S.stars.filter(s=>s>0).length;
let idx = Math.min(done, 3);
const p = CP_POS[idx];
let angle = 0;
if (idx < 3) {
const n = CP_POS[idx + 1];
angle = Math.atan2(n.y - p.y, n.x - p.x) * (180 / Math.PI);
} else if (idx > 0) {
const pv = CP_POS[idx - 1];
angle = Math.atan2(p.y - pv.y, p.x - pv.x) * (180 / Math.PI);
}
car.style.left = ((p.x / 360) * 100) + '%';
car.style.top = ((p.y / 540) * 100) + '%';
car.style.transform = 'translate(-50%,-50%) rotate(' + angle + 'deg)';
}
// ===== 章节 =====
function segLabel(seg, idx) {
if (seg.type==='kp') return seg.title || '知识点';
if (seg.type==='stg') return seg.title || '分站赛';
if (seg.type==='review') return '章节复习';
if (seg.type==='rd') return '计时练习';
if (seg.type==='eg') return '例题讲解';
if (seg.type==='pn') return '路书';
let nth = 0;
SS[S.curSS].segs.slice(0, idx+1).forEach(sg => { if(sg.type==='ss') nth++; });
return ['练习','排位','正赛','冲刺'][nth-1] || '计时';
}
function renderSeg() {
const s = SS[S.curSS];
const seg = s.segs[S.curSeg];
if (!seg) return finishChap();
const co = $('cCo');
co.style.animation = 'none'; void co.offsetHeight;
$('cGl').textContent = S.viewMode==='wrongReviewSession'
? `\u{1F3CE}\uFE0F \u{1F4D2} 复习圈 · ${S.wrIdx+1}/${S.wrongQ.length} · ${segLabel(seg,S.curSeg)}`
: `🏆 MV1 · 第${S.curSS+1}站 · ${segLabel(seg,S.curSeg)}`;
$('cTi').textContent = s.emoji+' '+s.title;
$('cDe').textContent = s.desc;
const mBtn = $('bkMapBtn');
if (mBtn) mBtn.style.display = (seg.type==='ss'||seg.type==='review'||seg.type==='rd'||seg.type==='stg') ? 'inline-block' : 'none';
if (seg.type==='kp') renderKP(seg);
else if (seg.type==='stg') renderStg(seg);
else if (seg.type==='pn') renderPN(seg);
else if (seg.type==='ss'||seg.type==='review') renderSSeg(seg);
else if (seg.type==='ms') renderMS(seg);
else if (seg.type==='rd') renderRd(seg);
else if (seg.type==='eg') renderEg(seg);
co.style.animation = 'fIn 0.35s ease';
}
function renderKP(seg) {
$('cCo').innerHTML = `<div class="pn" style="border-left:3px solid #ffd700;"><div class="kpTitle">${seg.icon||'📖'} ${seg.title}</div>${seg.content}<div style="text-align:center;margin-top:14px;"><button class="act show">继续 \u2192</button></div></div>`;
beep('click');
initDragModels();
}
// ===== 章末锦囊 =====
function renderMS(seg) {
let html = `<div class="pn" style="border-left:3px solid #2ecc71;"><div class="kpTitle">${seg.icon||'📚'} ${seg.title}</div>`;
(seg.kv||[]).forEach(function(pair){
const [key, val] = pair;
const lines = val.split('\n');
html += `<div style="margin:6px 0;"><span style="font-weight:700;color:#ffd700;">${key}</span><div style="padding:4px 8px;background:rgba(255,255,255,0.03);border-radius:3px;font-size:13px;line-height:1.8;">`;
lines.forEach(function(l){ html += `<div>${l}</div>`; });
html += `</div></div>`;
});
html += `<div style="text-align:center;margin-top:14px;"><button class="act show">继续 →</button></div></div>`;
$('cCo').innerHTML = html;
beep('click');
}
// ===== 分站赛 =====
let stgStat = { total:0, done:0, cor:0, wrg:0 };
function stgReset() {
const s = SS[S.curSS];
stgStat.total = s.segs.filter(function(sg){return sg.tag}).length;
stgStat.done = stgStat.cor = stgStat.wrg = 0;
}
function stgUpdate() {
stgStat.done = 0; stgStat.cor = 0; stgStat.wrg = 0;
const s = SS[S.curSS];
s.segs.forEach(function(sg) {
if (!sg.tag) return;
if (sg._answered) { stgStat.done++; if (sg._passed) stgStat.cor++; else stgStat.wrg++; }
});
}
function renderStg(seg) {
stgReset(); stgUpdate();
const total = stgStat.total;
const done = stgStat.done;
const pct = total ? Math.round(done/total*100) : 0;
const tags = { '重点':0, '核心':0, '高频':0, '综合':0 };
const s = SS[S.curSS];
s.segs.forEach(function(sg) { if (sg.tag) tags[sg.tag] = (tags[sg.tag]||0)+1; });
let tagHtml = '';
Object.keys(tags).forEach(function(t) { if (tags[t]) tagHtml += '<span class="stgTag">'+t+' ×'+tags[t]+'</span>'; });
const bar = '<div class="stgBar"><div class="stgFill" style="width:'+pct+'%"></div></div>';
const status = done ? '<div style="margin:8px 0;font-size:13px;color:rgba(255,255,255,0.5)">已完成 '+done+'/'+total+''+stgStat.cor+' 正确 · '+stgStat.wrg+' 错误)</div>' : '';
const btn = done < total ? '<button class="act show" id="stgStart">'+(done?'继续练习 \u2192':'开始练习 \u2192')+'</button>'
: '<div style="margin:14px 0;padding:14px;background:rgba(46,204,113,0.04);border:1px solid rgba(46,204,113,0.08);border-radius:3px;"><div style="font-size:24px;font-weight:900;color:#2ecc71;">'+stgStat.cor+'/'+total+'</div><div style="font-size:12px;color:rgba(255,255,255,0.3);margin:2px 0 6px;">正确率 '+Math.round(stgStat.cor/total*100)+'%</div><button class="act show" id="stgDone">查看成绩单 \u2192</button></div>';
$('cCo').innerHTML = `<div class="pn" style="border-left:3px solid #e10600;"><div class="kpTitle" style="color:#e10600;">\u{1F3C1} 分站赛:${s.title}</div><p>综合练习——检测本章全部知识点掌握情况</p><div style="margin:10px 0;">${tagHtml}</div>${bar}${status}${btn}</div>`;
}
function renderStgResult() {
stgUpdate();
const total = stgStat.total;
const cor = stgStat.cor;
const wrg = stgStat.wrg;
const pct = total ? Math.round(cor/total*100) : 0;
const stars = pct >= 90 ? 3 : pct >= 60 ? 2 : pct >= 30 ? 1 : 0;
// Category breakdown
const s = SS[S.curSS];
const cats = {};
s.segs.forEach(function(sg) {
if (!sg.tag || !sg._answered) return;
if (!cats[sg.tag]) cats[sg.tag] = { total:0, cor:0 };
cats[sg.tag].total++;
if (sg._passed) cats[sg.tag].cor++;
});
let catHtml = '';
Object.keys(cats).forEach(function(t) {
const c = cats[t];
catHtml += '<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid rgba(255,255,255,0.02);"><span style="color:rgba(255,255,255,0.4)">'+t+'</span><span>'+c.cor+'/'+c.total+' <span style="color:'+(c.cor===c.total?'#2ecc71':'rgba(255,255,255,0.2)')+'">('+Math.round(c.cor/c.total*100)+'%)</span></span></div>';
});
const passFail = pct >= 60 ? '<span style="color:#2ecc71">\u2705 通过</span>' : '<span style="color:#e10600">\u274C 未通过,继续加油</span>';
$('cCo').innerHTML = `<div class="pn" style="border-left:3px solid ${pct>=60?'#2ecc71':'#e10600'};text-align:center;">
<div class="kpTitle" style="color:#ffd700;font-size:26px;animation:bounceIn 0.5s ease;">\u{1F3C6} ${stars>=3?'完美!':stars>=2?'不错!':'继续加油!'}</div>
<div style="font-size:40px;font-weight:900;color:#fff;margin:8px 0;font-family:'Inter',monospace;animation:bounceIn 0.6s ease;">${cor}<span style="font-size:20px;color:rgba(255,255,255,0.2);">/${total}</span></div>
<div style="font-size:13px;color:rgba(255,255,255,0.3);margin-bottom:12px;animation:slideUp 0.5s ease 0.2s both;">正确率 ${pct}% ${passFail}</div>
<div style="text-align:left;max-width:240px;margin:0 auto 14px;animation:slideUp 0.5s ease 0.3s both;">${catHtml}</div>
<button class="act show" style="background:${pct>=90?'#ffd700;color:#0a0a1a':pct>=60?'#2ecc71':pct>0?'#e10600':'rgba(255,255,255,0.1)'};animation:bounceIn 0.5s ease 0.5s both;" id="stgContinue">继续 \u2192</button>
</div>`;
}
function renderPN(seg) {
$('cCo').innerHTML = `<div class="pn">${seg.content}<div style="text-align:center;margin-top:12px;"><div style="font-size:10px;color:rgba(255,255,255,0.35);margin-bottom:5px;letter-spacing:2px;font-weight:700;">CO-DRIVER: 路书完毕</div><button class="act show">发车 \u2192</button></div></div>`;
beep('click');
}
function renderSSeg(seg) {
const done = seg._answered||false;
const sel = seg._selected??-1;
const lbl = ['A','B','C','D'];
const qnLabel = seg.type==='review' ? '章节复习' : (seg.tag ? segLabel(seg,S.curSeg) : segLabel(seg,S.curSeg));
const diag = seg.diagram ? `<div class="geoDiag">${seg.diagram}</div>` : '';
let progHtml = '';
if (seg.tag) {
stgUpdate();
const pct = stgStat.total ? Math.round((stgStat.done-1)/stgStat.total*100) : 0;
progHtml = `<div class="stgBar" style="margin:0 0 8px"><div class="stgFill" style="width:${pct}%"></div></div><div style="font-size:10px;color:rgba(255,255,255,0.2);margin-bottom:6px;font-family:'Inter',monospace;"><span style="color:#ffd700">${seg.tag}</span> · ${stgStat.done-1}/${stgStat.total} 答 · ${stgStat.cor}${stgStat.wrg}✗</div>`;
}
const tagBadge = seg.tag ? `<span style="display:inline-block;padding:1px 6px;border:1px solid rgba(255,255,255,0.06);border-radius:2px;font-size:9px;font-weight:700;color:rgba(255,255,255,0.3);margin-left:6px;font-family:'Inter',monospace;">${seg.tag}</span>` : '';
const srsBadge = S.viewMode==='wrongReviewSession' && S.wrongQMap[qid()] && S.wrongQMap[qid()].count > 0
? `<span style="display:inline-block;padding:1px 8px;background:rgba(225,6,0,0.08);border:1px solid rgba(225,6,0,0.1);border-radius:2px;font-size:9px;font-weight:700;color:#e10600;margin-left:6px;font-family:'Inter',monospace;">\u{2716}\u00D7${S.wrongQMap[qid()].count}</span>` : '';
const streakHtml = S.streak>=2 && done && sel===seg.correct ? `<div class="radio streak" style="margin-bottom:4px;">${S.streak>=10?'\u{1F451}终极统治':S.streak>=5?'\u{1F3C6}大奖赛全胜':'\u{26A1}最快圈速'} \u00D7 ${S.streak}</div>` : '';
const radioHtml = done && seg._radioMsg ? `<div class="radio ${sel===seg.correct?'pass':'fail'}">${seg._radioMsg}</div>` : '';
$('cCo').innerHTML = `${progHtml}<div class="qp"><div class="qn">${qnLabel}${tagBadge}${srsBadge}</div>${diag}<div class="qt">${seg.question}</div><div class="qo">${seg.opts.map((o,i)=>`<div class="opt ${done?(i===seg.correct?'pass':(i===sel?'fail':'disabled')):''}" data-idx="${i}"><span class="lb">${lbl[i]}</span>${o}</div>`).join('')}</div><div class="qfb ${done?'show '+(sel===seg.correct?'pass':'fail'):''}">${streakHtml}${radioHtml}${done?(sel===seg.correct?seg.passMsg:seg.failMsg):''}</div>${done?'<button class="act show">继续 \u2192</button>':''}</div>`;
}
function qid() { return S.curSS+'_'+S.curSeg; }
function goWrongReview() {
S.viewMode='wrongReview'; S.wrIdx=0;
page('pW'); renderWrongList();
}
function renderWrongList() {
const $wl = $('wList');
if (!S.wrongQ.length) {
$wl.innerHTML='<div style="text-align:center;padding:40px 20px;color:rgba(255,255,255,0.15);font-size:12px;">\u{1F389} 暂无错题!继续保持!</div>';
return;
}
$wl.innerHTML = `<div style="margin-bottom:10px;text-align:center;"><button class="hudBtn" id="revStart" style="background:rgba(255,215,0,0.06);border-color:rgba(255,215,0,0.1);color:#ffd700;font-size:12px;padding:10px 20px;">\u{1F3CE}\uFE0F \u{1F4D2} 错题复习圈 \u00D7${S.wrongQ.length}</button></div>`
+ S.wrongQ.map((id,i)=>{
const [ci,si]=id.split('_').map(Number);
const seg=SS[ci].segs[si];
if (!seg) return '';
const wm = S.wrongQMap[id];
const failBadge = wm && wm.count > 1 ? ` <span style="color:#e10600;font-size:9px;font-weight:700;">\u{2716}\u00D7${wm.count}</span>` : '';
return `<div class="wrItem" data-idx="${i}">
<div class="wrH">\u{1F3C6} #1 · ${SS[ci].title} · <span class="wrLb">${segLabel(seg,si)}</span>${failBadge}</div>
<div class="wrQ">${seg.question}</div>
<div class="wrAct"><button class="wrDo" data-id="${id}">\u{1F504} 重做</button><button class="wrRm" data-id="${id}">\u{2716} 移除</button></div>
</div>`;
}).join('');
}
function startWrongReview() {
if (!S.wrongQ.length) return;
S.viewMode = 'wrongReviewSession';
S.wrIdx = 0;
goToWrongQ(0);
}
function goToWrongQ(idx) {
if (idx >= S.wrongQ.length) { finishWrongReview(); return; }
S.wrIdx = idx;
const id = S.wrongQ[idx];
const [ci, si] = id.split('_').map(Number);
S.curSS = ci; S.curSeg = si;
const seg = SS[ci].segs[si];
if (seg) { delete seg._answered; delete seg._selected; delete seg._passed; }
page('pC'); renderSeg(); sync();
}
function nextWrongInReview() {
goToWrongQ(S.wrIdx + 1);
}
function finishWrongReview() {
S.viewMode = 'normal';
const cleared = S.wrongQ.length === 0;
if (cleared) {
sparks(40); beep('win');
$('cCo').innerHTML = `<div class="pn" style="text-align:center;border-left:3px solid #ffd700;animation:bounceIn 0.5s ease;"><div style="font-size:48px;margin:8px 0;">\u{1F3C6}</div><h2 style="color:#ffd700;font-size:22px;">\u{1F389} 错题全部清空!</h2><p style="color:rgba(255,255,255,0.3);margin:6px 0 14px;">#1 完美表现!没有任何题能难倒你!</p><button class="act show" onclick="goMap();">返回赛道 \u2192</button></div>`;
} else {
$('cCo').innerHTML = `<div class="pn" style="text-align:center;border-left:3px solid #e10600;"><div style="font-size:40px;margin:8px 0;">\u{1F3CE}\uFE0F</div><h2 style="color:#fff;font-size:18px;">复习完成!还有 <span style="color:#ffd700;">${S.wrongQ.length}</span> 道待攻克</h2><p style="color:rgba(255,255,255,0.3);margin:6px 0 14px;">下次进站再解决它们!</p><button class="act show" onclick="startWrongReview();">\u{1F504} 再复习一圈</button><button class="act show" onclick="goMap();" style="background:rgba(255,255,255,0.05);margin-left:8px;">返回赛道 \u2192</button></div>`;
}
page('pC');
}
function pickAns(idx) {
const s = SS[S.curSS];
const seg = s.segs[S.curSeg];
if (seg._answered) return;
seg._answered=true; seg._selected=idx; seg._passed=idx===seg.correct;
const id = qid();
if (!S.doneQ.includes(id)) S.doneQ.push(id);
seg._radioMsg = '';
if (idx===seg.correct) {
S.streak++;
S.pts=Math.min(100,S.pts+8); beep(S.streak>=3?'hattrick':'pass'); sparks(6);
const wi = S.wrongQ.indexOf(id);
if (wi>=0) {
S.wrongQ.splice(wi,1);
if (S.wrIdx>=S.wrongQ.length) S.wrIdx=0;
// SRS: mark reviewed in wrongQMap
if (S.wrongQMap[id]) { S.wrongQMap[id].lastReview = Date.now(); S.wrongQMap[id].reviews = (S.wrongQMap[id].reviews||0) + 1; }
}
const radioMsgs = [
'\u{1F4FB} 干得漂亮 Max!这圈完美!',
'\u{1F4FB} 最快圈速!+8 PTS',
'\u{1F4FB} Great driving! 继续保持!',
'\u{1F4FB} P1! P1! 拉开差距!',
'\u{1F4FB} 哇!教科书般的操作!',
'\u{1F3CE}\uFE0F 冲刺!无人能挡!'
];
seg._radioMsg = radioMsgs[Math.floor(Math.random()*radioMsgs.length)];
if (S.streak===3) seg._radioMsg = '\u{1F3BA} HAT TRICK! 连续三站冠军!\u{1F3C6}';
else if (S.streak===5) seg._radioMsg = '\u{1F451} GRAND SLAM! 包办最快圈速!\u{2728}';
else if (S.streak===10) seg._radioMsg = '\u{1F680} \u2716\uFE0F 就是统治!VERSTAPPEN 不可阻挡!';
} else {
S.streak=0;
beep('fail');
if (!S.wrongQ.includes(id)) { S.wrongQ.push(id); }
// SRS: track wrong attempt
if (!S.wrongQMap[id]) S.wrongQMap[id] = { count:0, lastWrong:0, lastReview:0, reviews:0 };
S.wrongQMap[id].count = (S.wrongQMap[id].count||0) + 1;
S.wrongQMap[id].lastWrong = Date.now();
const radioMsgs = [
'\u{1F4FB} BOX BOX — 进站换胎修正!',
'\u{1F4FB} 别急 #1,下个弯道超回来!',
'\u{1F4FB} 练习赛试错而已,正赛加油!',
'\u{1F4FB} 失误是升级的燃料\u{1F527}',
'\u{1F4FB} 稳住!#1 这圈不算,重新来!'
];
seg._radioMsg = radioMsgs[Math.floor(Math.random()*radioMsgs.length)];
}
sv(); renderSeg(); sync();
const hb=$('hintBadge'); if(hb)hb.textContent=S.wrongQ.length||'';
// toggle review lap button
const $revBtn = $('revLapBtn');
if ($revBtn) $revBtn.style.display = S.wrongQ.length ? 'inline-flex' : 'none';
}
function renderRd(seg) {
const nums = seg.numbers||[];
const mn = Math.min(...nums)-1, mx = Math.max(...nums)+1, rg = mx-mn||1;
const ticks = []; for(let i=mn;i<=mx;i++) ticks.push(i);
$('cCo').innerHTML = `<div class="rv"><h3>${seg.title||'\u{1F4CD} 赛道定位'}</h3><div style="font-size:9px;color:rgba(255,255,255,0.08);margin-bottom:8px;letter-spacing:1px;">SPLIT TIME</div><div class="rline">${ticks.map(t=>`<span class="mk ${t===0?'z':''}" style="left:${(t-mn)/rg*100}%">${t}</span>`).join('')}${nums.map(n=>`<div class="car" style="left:${(n-mn)/rg*100}%">\u{1F3CE}<span class="vl">${n>0?'+'+n:n}</span></div>`).join('')}</div><button class="act show" style="margin-top:12px;">继续 \u2192</button></div>`;
beep('click');
}
// ===== 例题讲解 (step-through) =====
function renderEg(seg) {
if (!seg._step) seg._step = 1;
const step = seg._step;
const total = seg.steps.length;
$('cCo').innerHTML = `<div class="eg-box">
<div class="eg-title">${seg.title}</div>
<div class="eg-steps">${seg.steps.map((s,i)=>`<div class="eg-step ${i<step?'show':''}"><span class="step-num">${i+1}</span>${s}</div>`).join('')}</div>
${step < total ? `<button class="eg-btn" id="egBtn">下一步 \u2192</button>` : `<button class="act show">继续 \u2192</button>`}
</div>`;
beep('click');
}
function stepEg() {
const s = SS[S.curSS];
const seg = s.segs[S.curSeg];
if (!seg || seg.type!=='eg') return;
if (!seg._step) seg._step = 1;
const total = seg.steps.length;
if (seg._step < total) {
seg._step++;
renderEg(seg);
if (seg._step === total) beep('pass');
}
}
function nextSeg() {
const s = SS[S.curSS];
const seg = s.segs[S.curSeg];
const id = S.curSS+'_'+S.curSeg;
if (!S.seenQ.includes(id)) { S.seenQ.push(id); sv(); }
if (S.curSeg < s.segs.length-1) { S.curSeg++; renderSeg(); sync(); }
else finishChap();
}
// ===== 章节结束 → 庆祝 =====
function finishChap() {
const s = SS[S.curSS];
const qs = s.segs.filter(seg=>seg.type==='ss'||seg.type==='review'||seg.type==='rd');
const ok = qs.filter(seg=>seg._passed).length;
const total = qs.length||1;
const r = ok/total;
let stars = 0;
if (r>=0.9) stars=3;
else if (r>=0.6) stars=2;
else if (r>=0.3) stars=1;
if (total===0) stars=3;
S.stars[S.curSS] = Math.max(S.stars[S.curSS], stars);
S.pts = Math.min(100, S.pts + stars*5);
sv();
const pos = ['DNF','P10','P5','P1'];
const cups = ['\u{1F3C1}','\u{1F949}','\u{1F948}','\u{1F3C6}'];
const titles = ['未完赛','P10 \u2014 积分区外','P5 \u2014 得分','#1 WIN \u2014 世界冠军!'];
const msgs = ['再练一圈,下个赛段复仇','继续 push#1 争取更好成绩','差一点就完美了!#1 下次碾压!','#1 VERSTAPPEN 模式!冲向下一个赛段'];
const radioMsgs = [
'\u{1F4FB} DNF... #1 下站必争!',
'\u{1F4FB} P10,还有很大提升空间!',
'\u{1F4FB} P5领奖台!#1 正在逼近!',
'\u{1F4FB} #1 Max: \u201CSimply simply lovely!\u201D \u{1F3C6}'
];
const maxQuotes = [
'',
'',
'',
'\u201C只要你不停push,总会到顶。\u201D \u2014 Max Verstappen'
];
const nextTxt = S.curSS < 3 && S.stars[S.curSS] > 0 ? '驶向下一站 \u2192'
: (S.curSS >= 3 && S.stars[S.curSS] >= 3 ? '\u{1F3C6} 世界冠军!重返赛道' : '返回赛道');
$('pmN').textContent = pos[stars];
$('pmC').textContent = cups[stars];
$('pmTi').textContent = titles[stars];
$('pmSt').textContent = '\u2B50'.repeat(stars)+'\u2B50'.repeat(3-stars);
$('pmXp').textContent = '+'+stars*5+' PTS';
$('pmMs').textContent = msgs[stars];
$('pmNt').textContent = nextTxt;
const $pmR = $('pmRadio');
$pmR.textContent = radioMsgs[stars];
$pmR.className = 'radio ' + (stars>=2?'pass':stars===1?'fail':'');
$pmR.style.display = stars > 0 ? 'inline-flex' : 'none';
// Max quote for 3-star
const $pmQ = document.getElementById('pmQ');
if ($pmQ) $pmQ.textContent = maxQuotes[stars];
// Show checkered flag for 3-star
const $chk = $('pmChk');
if ($chk) $chk.style.opacity = stars===3 ? '0.06' : '0';
$('po').classList.add('show');
// big confetti — branded colors for 3-star
sparks(stars===3?80:50);
beep(stars===3?'win':'pass');
}
function closePodium() {
$('po').classList.remove('show');
S.streak=0;
const nextIdx = S.curSS + 1;
if (nextIdx < 4 && S.stars[S.curSS] > 0) {
// animate car along track curve
animateCarAlongPath(nextIdx, () => {
updateTrackProgress(nextIdx);
setTimeout(() => { goMap(); }, 300);
});
} else {
goMap();
}
}
const _carPathCache = (function(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('d', SVG_PATH);
const len = p.getTotalLength();
const samples = 200;
const cpT = CP_POS.map(cp => {
let best = 0, bestD = Infinity;
for (let i = 0; i <= samples; i++) {
const pt = p.getPointAtLength(i/samples*len);
const d = Math.hypot(pt.x-cp.x, pt.y-cp.y);
if (d < bestD) { bestD = d; best = i/samples; }
}
return best;
});
return { p, len, cpT };
})();
function animateCarAlongPath(toIdx, onDone) {
const car = $('trackCar'); if (!car) return void(onDone&&onDone());
const { p, len, cpT } = _carPathCache;
const fromT = S.curSS >= 0 && S.curSS < cpT.length ? cpT[S.curSS] : 0;
const toT = cpT[toIdx];
const duration = 1400;
const start = performance.now();
car.classList.add('moving');
// Hide star/name labels during transit
document.querySelectorAll('.cp-label, .cp-name, .cp-stars').forEach(el => el.style.opacity = '0.3');
function step(time) {
const elapsed = time - start;
const progress = Math.min(1, elapsed / duration);
const ease = 1 - Math.pow(1 - progress, 3);
const t = Math.max(0, Math.min(1, fromT + (toT - fromT) * ease));
const pt = p.getPointAtLength(t * len);
// direction for rotation (look-ahead)
const dt = 0.003;
const pt2 = p.getPointAtLength(Math.max(0, Math.min(len, (t + dt) * len)));
const angle = Math.atan2(pt2.y - pt.y, pt2.x - pt.x) * (180 / Math.PI);
car.style.left = ((pt.x / 360) * 100) + '%';
car.style.top = ((pt.y / 540) * 100) + '%';
car.style.transform = 'translate(-50%,-50%) rotate(' + angle + 'deg)';
if (progress < 1) { requestAnimationFrame(step); }
else {
car.classList.remove('moving');
document.querySelectorAll('.cp-label, .cp-name, .cp-stars').forEach(el => el.style.opacity = '1');
if (onDone) onDone();
}
}
requestAnimationFrame(step);
}
function updateTrackProgress(idx) {
const donePath = document.getElementById('trackDone');
const fillPath = document.getElementById('trackFill');
if (!donePath || !fillPath) return;
const pathLen = fillPath.getTotalLength();
const progress = idx / 4;
donePath.setAttribute('stroke-dasharray', pathLen);
donePath.setAttribute('stroke-dashoffset', pathLen - progress * pathLen);
}
// ===== HUD =====
function sync() {
const done = S.stars.filter(s=>s>0).length;
$('hStage').textContent = Math.min(done+1,4)+'/4';
$('hPts').textContent = S.pts;
// streak display
const $sD = $('streakD');
if ($sD) {
if (S.streak >= 2) {
const labels = ['','','\u{26A1}','\u{26A1}','\u{1F525}','\u{1F525}','\u{1F3C6}','\u{1F3C6}','\u{1F451}','\u{1F451}','\u{1F680}'];
const label = labels[Math.min(S.streak, labels.length-1)];
$sD.className = 'streakDisplay on';
$sD.innerHTML = `${label} #1 \u00D7${S.streak}连对`;
} else { $sD.className = 'streakDisplay'; $sD.innerHTML = ''; }
}
// lap counter
const $lC = $('lapCnt');
if ($lC) {
const s = SS[S.curSS];
const qs = s.segs.filter(function(seg){return seg.type==='ss'||seg.type==='review'||seg.type==='rd';});
const total = qs.length;
const doneCount = qs.filter(function(seg){return seg._answered;}).length;
$lC.innerHTML = `\u{1F3CE}\uFE0F #1 LAP ${Math.min(doneCount+1,total)}/${total}`;
}
}
// ===== 学习总览 =====
function renderDashboard() {
const titles = ['有理数','整式的加减','一元一次方程','几何图形初步'];
const emojis = ['🏁','🔧','🛞','📐'];
let html = '<div id="dbInner" style="background:rgba(8,8,20,0.5);border:1px solid rgba(255,255,255,0.03);border-radius:3px;padding:12px 14px;">';
for (let ch = 0; ch < 4; ch++) {
const s = SS[ch];
const stars = S.stars[ch];
const starStr = stars ? '\u2B50'.repeat(stars) : '\u2606\u2606\u2606';
// Count KP segments seen vs total
let kpTotal = 0, kpSeen = 0;
let ssTotal = 0, ssDone = 0, ssCor = 0;
s.segs.forEach(function(seg, i) {
const id = ch+'_'+i;
if (seg.type === 'kp' || seg.type === 'eg' || seg.type === 'pn') { kpTotal++; if (S.seenQ.includes(id)) kpSeen++; }
if (seg.type === 'ss' || seg.type === 'review' || seg.tag) { ssTotal++; if (seg._answered) { ssDone++; if (seg._passed) ssCor++; } }
});
const kpPct = kpTotal ? Math.round(kpSeen/kpTotal*100) : 0;
const kpBar = '<div class="stgBar" style="margin:2px 0;height:3px;"><div class="stgFill" style="width:'+kpPct+'%;background:'+(kpPct===100?'#2ecc71':'#e10600')+'"></div></div>';
const ssStr = ssDone ? (ssCor+'/'+ssDone) : '0/0';
const status = kpSeen === kpTotal ? (ssDone === ssTotal ? '\u2705 已完成' : '\u{1F3C1} 练习中') : '\u{1F4D6} 学习中';
html += '<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.02);display:flex;align-items:center;gap:8px;">';
html += '<div style="width:24px;font-size:16px;text-align:center;">'+emojis[ch]+'</div>';
html += '<div style="flex:1;min-width:0;">';
html += '<div style="font-size:12px;font-weight:700;color:#fff;">SS'+(ch+1)+' '+titles[ch]+'</div>';
html += '<div style="font-size:9px;color:rgba(255,255,255,0.3);">\u{1F4DA} 学习 '+kpSeen+'/'+kpTotal+' &nbsp;\u270F 答题 '+ssStr+' &nbsp;'+starStr+'</div>';
html += kpBar;
html += '</div>';
html += '<div style="font-size:10px;color:'+(status==='\u2705 已完成'?'#2ecc71':status==='\u{1F3C1} 练习中'?'#ffd700':'rgba(255,255,255,0.3)')+';">'+status+'</div>';
html += '</div>';
}
html += '<div style="font-size:9px;color:rgba(255,255,255,0.15);text-align:center;padding-top:6px;">\u{1F4A1} 点击赛道检查点进入章节 &nbsp;|&nbsp; 绿色=已完成全部学习</div>';
html += '</div>';
$('dbPanel').innerHTML = html;
}
let ax = null;
function ac() { if(!ax) ax=new(window.AudioContext||window.webkitAudioContext)(); return ax; }
function beep(t) {
try {
const ctx = ac();
if (ctx.state === 'suspended') ctx.resume();
if (t==='pass') { [523,659,784].forEach((f,i)=>{const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);g.gain.value=0.03;o.frequency.value=f;o.start(ctx.currentTime+i*0.1);o.stop(ctx.currentTime+i*0.1+0.1);}); }
else if (t==='fail') { const o=ctx.createOscillator(),g=ctx.createGain();o.type='sawtooth';o.connect(g);g.connect(ctx.destination);g.gain.value=0.015;o.frequency.setValueAtTime(200,ctx.currentTime);o.frequency.setValueAtTime(160,ctx.currentTime+0.1);o.start(ctx.currentTime);o.stop(ctx.currentTime+0.2); }
else if (t==='win') { [523,587,659,784,1047].forEach((f,i)=>{const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);g.gain.value=0.03;o.frequency.value=f;o.start(ctx.currentTime+i*0.08);o.stop(ctx.currentTime+i*0.08+0.12);}); }
else if (t==='hattrick') { [523,659,784,1047,1319].forEach((f,i)=>{const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);g.gain.value=0.035;o.frequency.value=f;o.start(ctx.currentTime+i*0.06);o.stop(ctx.currentTime+i*0.06+0.15);}); }
else { const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);g.gain.value=0.015;o.frequency.value=440;o.start(ctx.currentTime);o.stop(ctx.currentTime+0.04); }
} catch(e) {}
}
// ===== 背景音乐 =====
let bgmNodes = [];
let bgmOn = false;
let bgmTimer = null;
function toggleBGM() {
const ctx = ac();
if (ctx.state === 'suspended') ctx.resume();
if (bgmOn) stopBGM();
else startBGM();
}
function startBGM() {
if (bgmOn) return;
try {
const ctx = ac();
bgmOn = true;
document.getElementById('mBtn').classList.add('on');
const master = ctx.createGain();
master.gain.value = 0;
master.gain.linearRampToValueAtTime(0.25, ctx.currentTime + 1.5);
master.connect(ctx.destination);
bgmNodes.push(master);
const BPM = 130;
const beat = 60 / BPM;
function kick(time) {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(120, time);
o.frequency.exponentialRampToValueAtTime(40, time + 0.08);
g.gain.setValueAtTime(0.50, time);
g.gain.exponentialRampToValueAtTime(0.001, time + 0.20);
o.connect(g); g.connect(master);
o.start(time); o.stop(time + 0.15);
bgmNodes.push(o, g);
}
function hat(time) {
const n = ctx.createBufferSource();
const buf = ctx.createBuffer(1, ctx.sampleRate * 0.05, ctx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.008));
n.buffer = buf;
const g = ctx.createGain();
g.gain.value = 0.10;
n.connect(g); g.connect(master);
n.start(time);
bgmNodes.push(n, g);
}
const bassNotes = [55, 65, 73, 82, 73, 65, 55, 49];
let bassIdx = 0;
function bassLine(time) {
const o = ctx.createOscillator();
o.type = 'sawtooth';
o.frequency.value = bassNotes[bassIdx % bassNotes.length];
const g = ctx.createGain();
g.gain.setValueAtTime(0.12, time);
g.gain.exponentialRampToValueAtTime(0.001, time + beat * 0.95);
const f = ctx.createBiquadFilter();
f.type = 'lowpass';
f.frequency.value = 300;
o.connect(f); f.connect(g); g.connect(master);
o.start(time); o.stop(time + beat * 0.9);
bgmNodes.push(o, g, f);
bassIdx++;
}
const melody = [
[330,0],[349,0.5],[392,1],[440,1.5],
[523,2],[494,2.5],[440,3],[392,3.5],
[349,4],[392,4.5],[440,5],[494,5.5],
[523,6],[587,6.5],[659,7],[587,7.5],
[523,8],[494,8.5],[440,9],[494,9.5],
[523,10],[587,10.5],[659,11],[784,11.5],
[659,12],[587,12.5],[523,13],[494,13.5],
[440,14],[392,14.5],[349,15],[330,15.5],
[349,16],[392,16.5],[440,17],[523,17.5],
[587,18],[523,18.5],[494,19],[440,19.5],
[392,20],[440,20.5],[494,21],[523,21.5],
[587,22],[659,22.5],[784,23],[659,23.5],
[587,24],[523,24.5],[494,25],[440,25.5],
[392,26],[349,26.5],[330,27],[294,27.5],
[261,28],[294,28.5],[330,29],[349,29.5],
[392,30],[440,30.5],[494,31],[523,31.5]
];
function lead(time) {
melody.forEach(([f, off]) => {
const t = time + off * beat;
const o = ctx.createOscillator();
o.type = 'square';
o.frequency.value = f;
const g = ctx.createGain();
const vel = off % 2 < 0.01 ? 0.06 : 0.04;
g.gain.setValueAtTime(vel, t);
g.gain.exponentialRampToValueAtTime(0.001, t + beat * 0.5);
const f2 = ctx.createBiquadFilter();
f2.type = 'lowpass';
f2.frequency.value = 1200;
o.connect(f2); f2.connect(g); g.connect(master);
o.start(t); o.stop(t + beat * 0.6);
bgmNodes.push(o, g, f2);
});
}
function pad(time) {
const chords = [[65,82,98],[73,87,110],[82,98,123],[98,110,130]];
chords.forEach((c, ci) => {
const t = time + ci * 4 * beat;
c.forEach(f => {
const o = ctx.createOscillator();
o.type = 'sine';
o.frequency.value = f;
const g = ctx.createGain();
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(0.04, t + 0.5);
g.gain.setValueAtTime(0.04, t + 3);
g.gain.linearRampToValueAtTime(0.001, t + 4);
const of = ctx.createBiquadFilter();
of.type = 'lowpass';
of.frequency.value = 600;
o.connect(of); of.connect(g); g.connect(master);
o.start(t); o.stop(t + 4 * beat);
bgmNodes.push(o, g, of);
});
});
}
let bar = 0;
function seq() {
if (!bgmOn) return;
const t = ctx.currentTime + 0.05;
const barLen = 4 * beat;
for (let i = 0; i < 4; i++) kick(t + i * beat);
for (let i = 0; i < 4; i++) hat(t + i * beat + beat * 0.5);
for (let i = 0; i < 4; i++) bassLine(t + i * beat);
if (bar % 4 === 0) lead(t + beat);
if (bar % 4 === 0) pad(t);
bar++;
bgmTimer = setTimeout(seq, barLen * 1000);
}
setTimeout(seq, 500);
} catch(e) { bgmOn = false; }
}
function stopBGM() {
bgmOn = false;
document.getElementById('mBtn').classList.remove('on');
clearTimeout(bgmTimer);
bgmNodes.forEach(n => { try { n.disconnect && n.disconnect(); n.stop && n.stop(); } catch(e) {} });
bgmNodes = [];
}
// ===== 粒子 =====
function sparks(n) {
const c = $('pyro');
const cs = ['#e10600','#ffd700','#1e41a0','#fff','#ffd700','#e10600','#1e41a0','#2ecc71'];
for(let i=0;i<n;i++){const e=document.createElement('div');e.className='sp';e.style.left=(10+Math.random()*80)+'%';e.style.top='-5px';e.style.background=cs[Math.floor(Math.random()*cs.length)];e.style.width=e.style.height=(2+Math.random()*4)+'px';e.style.animationDuration=(1.5+Math.random()*2)+'s';e.style.animationDelay=(Math.random()*0.5)+'s';c.appendChild(e);setTimeout(()=>e.remove(),4000);}
}
// ===== 启动 =====
function init() {
var hs = $('hStage');
if (hs) { const done = S.stars.filter(function(s){return s>0}).length; hs.textContent = Math.min(done+1,4)+'/4'; }
renderMap(); sync();
}
init();
</script>
</body>
</html>