Spaces:
Sleeping
Sleeping
| <html lang="vi"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>PoolCoach — Gợi ý cú đánh 9-ball</title> | |
| <link rel="stylesheet" href="style.css"> | |
| </head> | |
| <body> | |
| <header> | |
| <!-- badge phiên bàn (QR, bàn giao 12) nằm trong header: hiện được cả | |
| desktop lẫn mobile mà không bao giờ đè lên canvas. Class RIÊNG | |
| .session-badge — không mượn class có event binding (bài học .btn-play). --> | |
| <h1>🎱 PoolCoach <span id="session-badge" class="session-badge hidden"></span></h1> | |
| <p>Kéo-thả bi theo bàn thật (mét) → <b>Gợi ý cú đánh</b> → <b>Đánh cú này</b> để chơi tiếp</p> | |
| </header> | |
| <main> | |
| <div id="table-wrap"> | |
| <canvas id="table"></canvas> | |
| <div id="tooltip" class="hidden"></div> | |
| </div> | |
| <aside id="panel"> | |
| <button id="sheet-close" class="sheet-close" aria-label="Đóng">✕</button> | |
| <div id="status" class="banner hidden"></div> | |
| <!-- A2b (feedback Danh 13/08 #2): Analyzer là tab ĐẦU + MẶC ĐỊNH khi mở | |
| app — tính năng chính của luận văn; tab Gợi ý lùi sau nhưng đầy đủ | |
| chức năng. HTML tĩnh phản ánh đúng trạng thái boot (Analyzer active, | |
| #analyzer hiện, .controls/.legend ẩn) khớp S.anMode=true trong | |
| app.js — MỘT nguồn sự thật cho trạng thái đầu, không có flash tab. | |
| Deep-link ?qr= (phiên bàn) vẫn vào thẳng tab Gợi ý — app.js xử lý. | |
| Tab Bài tập (F3) + Analyzer là LỚP THÊM: thoát tab là bàn + luồng | |
| gợi ý trả lại nguyên trạng (app.js chụp/khôi phục S.balls). --> | |
| <div class="tabs"> | |
| <button id="tab-analyzer" class="tab active">Analyzer</button> | |
| <button id="tab-suggest" class="tab">Gợi ý</button> | |
| <button id="tab-drills" class="tab">Bài tập</button> | |
| </div> | |
| <!-- Broadcast Analyzer (bàn giao 24, đa cú từ A1): upload video 1..N cú | |
| → click 4 pocket trên frame đầu (overlay #an-overlay dưới cùng | |
| trang) → máy tự cắt cú → bảng danh sách cú. Mọi id/class .an-* là | |
| MỚI — không mượn class có event binding (bài học .btn-play 04/08). | |
| Luồng 1-cú GỠ ở A2b (feedback Danh 13/08): video 1 cú là trường | |
| hợp con của luồng đa cú — MỘT đường vào duy nhất. --> | |
| <section id="analyzer"> | |
| <div class="an-intro">Tải lên video 9-ball (<b>1 hoặc nhiều cú</b>, | |
| một góc máy full-table ổn định, ≤ 20 phút, MP4/MOV) — máy tự tách | |
| từng cú đánh, đo quỹ đạo, V0, hướng φ, spin và các va chạm.</div> | |
| <!-- .an-cta chứ KHÔNG PHẢI .btn-go: .btn-go có binding recommend toàn | |
| cục + syncButtons ép label — mượn là nút này gọi ké engine và | |
| mọc nút "Đánh cú này" giữa tab Analyzer (bug smoke BG25). --> | |
| <button id="anv-pick" class="an-cta">🎬 Phân tích video (1 hoặc nhiều cú)</button> | |
| <div id="anv-result" class="hidden"> | |
| <!-- Danh sách cú (A1) — lát A2 bọc vào #anv-list để trang chi tiết | |
| cú (#and-detail, §3.2 design) thế chỗ khi click một hàng. | |
| Mọi id/class .and-* là MỚI (bài học .btn-play). --> | |
| <div id="anv-list"> | |
| <div id="anv-progressline" class="anv-progressline"></div> | |
| <div id="anv-warnings"></div> | |
| <!-- Xuất toàn danh sách cú (A2 phần 5): CSV UTF-8 BOM (Excel | |
| không vỡ tiếng Việt) + JSON — cùng nguồn với bảng. Href do | |
| JS điền theo video đang xem. --> | |
| <div class="btn-row anv-export hidden"> | |
| <a id="anv-csv" class="btn-mini and-link" download>⬇ Xuất CSV</a> | |
| <a id="anv-json" class="btn-mini and-link" download>⬇ Xuất JSON</a> | |
| </div> | |
| <div class="anv-tablewrap"> | |
| <table id="anv-table" class="anv-table"> | |
| <thead><tr> | |
| <th>#</th><th>Cú</th><th>V0</th><th>φ</th> | |
| <th>Spin (a/b)</th><th>Tin cậy</th><th>RMSE</th> | |
| <th>Trạng thái</th> | |
| </tr></thead> | |
| <tbody id="anv-tbody"></tbody> | |
| </table> | |
| </div> | |
| <div class="an-note">Click một hàng <b>đã xong</b> để mở trang | |
| chi tiết cú (overlay + resim chồng + bảng thông số; JSON thô | |
| có link trong đó). V0/φ/spin lấy từ cột model (ShotNet) khi có | |
| — nhãn "bi" là tốc độ bi đo giải tích, hai thước khác nhau. | |
| Cột RMSE = khớp resim pooltool ↔ track quan sát (mm, bộ params | |
| model ShotNet trừ khi ghi khác). Cú hàng xám là cú máy không | |
| phân tích được, kèm lý do. Cú xám | |
| "⚑ nghi không phải cú đánh" là van hiển thị từ tín hiệu cú tự | |
| khai (V0 / motion_start / coverage) — không bị lọc bỏ, vẫn | |
| click xem được.</div> | |
| </div> | |
| <!-- Trang chi tiết cú (lát A2 phần 4, §3.2 design): overlay.mp4 + | |
| bàn metric quan sát (nét liền, vẽ trên canvas bàn) vs resim | |
| (nét đứt) + bảng 2 cột + nút "Đánh lại cú" (animateShot() sẵn | |
| có — KHÔNG animation mới). --> | |
| <div id="and-detail" class="hidden"> | |
| <div class="btn-row"> | |
| <button id="and-back" class="btn-mini">← Danh sách cú</button> | |
| <a id="and-json" class="btn-mini and-link" target="_blank">JSON thô</a> | |
| <a id="and-download" class="btn-mini and-link hidden">⬇ Tải mp4</a> | |
| </div> | |
| <h3 id="and-title"></h3> | |
| <video id="and-video" class="and-video hidden" controls playsinline></video> | |
| <div id="and-video-missing" class="an-note hidden">Cú này chưa có | |
| overlay.mp4 — kết quả thuộc lần chạy trước A2 (clip đã xoá, | |
| không render hồi tố được) hoặc render lỗi (xem cảnh báo).</div> | |
| <!-- A2b phần 4 (Danh chốt 13/08 tối): "Đánh lại cú" animate theo | |
| quỹ đạo QUAN SÁT — cú thật sống lại, luôn đúng bất kể model. --> | |
| <button id="and-replay" class="an-cta" disabled>▶ Đánh lại cú (quỹ đạo quan sát)</button> | |
| <!-- Widget thông tin cú khi đánh lại (§3.2): bi cái + chấm điểm | |
| đầu cơ (a/b) + thanh lực V0 — tái dụng MARKUP + CSS style-only | |
| (.viz*) của #shotviz tab gợi ý; id + class tương tác RIÊNG | |
| (.and-spin-dot / .and-power-bar) vì .spin-dot/.power-bar bị | |
| initPowerBar + updateShotViz quét toàn cục (bài học .btn-play). | |
| Số từ bộ SHOTNET — analytic không có a/b; nhãn bộ ghi thẳng. --> | |
| <div id="and-shotviz" class="viz hidden"> | |
| <div class="viz-spin"> | |
| <svg id="and-spin-ball" viewBox="0 0 100 100" aria-label="điểm chạm bi cái (model)"> | |
| <defs> | |
| <radialGradient id="and-cuegrad" cx="35%" cy="30%" r="80%"> | |
| <stop offset="0%" stop-color="#ffffff"/> | |
| <stop offset="100%" stop-color="#c9c9c9"/> | |
| </radialGradient> | |
| </defs> | |
| <circle cx="50" cy="50" r="46" fill="url(#and-cuegrad)" stroke="#1c1c1c" stroke-width="1.6"/> | |
| <line x1="50" y1="7" x2="50" y2="93" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/> | |
| <line x1="7" y1="50" x2="93" y2="50" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/> | |
| <circle class="and-spin-dot" cx="50" cy="50" r="6.5" fill="#e02020" stroke="#7a0000" stroke-width="1.3"/> | |
| </svg> | |
| <div class="viz-cap" id="and-spin-cap"></div> | |
| </div> | |
| <div class="viz-power"> | |
| <div class="viz-lbl">Lực <b id="and-power-val"></b></div> | |
| <div id="and-power-bar" class="and-power-bar"></div> | |
| <div class="viz-scale"><span>0.5</span><span>m/s</span><span>8.0</span></div> | |
| <div class="viz-cap" id="and-viz-label"></div> | |
| </div> | |
| </div> | |
| <div id="and-suspect" class="an-warn hidden"></div> | |
| <!-- Lớp resim thành toggle, MẶC ĐỊNH TẮT (path quan sát ↔ resim | |
| đang lệch V0-thang, hiện cả hai thì rối — feedback #3). Bật | |
| mặc định lại = một hằng AND_RESIM_DEFAULT_ON trong app.js. --> | |
| <label id="and-resim-row" class="and-toggle hidden"> | |
| <input type="checkbox" id="and-resim-toggle"> | |
| So với mô phỏng (vẽ nét đứt resim + RMSE) | |
| </label> | |
| <div id="and-resim" class="an-note hidden"></div> | |
| <div id="and-warnings"></div> | |
| <table id="and-metrics" class="an-metrics"></table> | |
| <div id="and-caveat" class="an-caveat hidden">Model train thuần mô | |
| phỏng (P2), chưa tinh chỉnh trên video thật.</div> | |
| <div class="an-note">Bàn bên trái: <b>nét liền tô màu theo thời | |
| gian</b> = track quan sát; bi có số/màu theo BallID khi kết quả | |
| có nhận diện (kết quả cũ hiện bi xám). "Đánh lại cú" chạy lại | |
| chính quỹ đạo quan sát đó bằng animation của app gợi ý. Bật | |
| "So với mô phỏng" mới vẽ <b>nét đứt trắng</b> = quỹ đạo resim | |
| từ params suy được (bộ ghi ở dòng RMSE).</div> | |
| </div> | |
| </div> | |
| </section> | |
| <section id="drills" class="hidden"> | |
| <!-- khu "Tiến bộ" (bàn giao 12): chỉ hiện khi có phiên bàn + DB — | |
| tổng hợp pass/fail theo drill của ĐÚNG phiên này. Không phiên / | |
| 503 → ẩn khu, hiện dòng nhắc. Class riêng .drill-progress. --> | |
| <div id="drill-progress" class="drill-progress hidden"> | |
| <b>Tiến bộ phiên này</b> | |
| <div id="drill-progress-body"></div> | |
| </div> | |
| <div id="drill-progress-hint" class="drill-note hidden">Vào bàn bằng | |
| mã QR để lưu và xem tiến bộ tập.</div> | |
| <div id="drill-list"></div> | |
| <div id="drill-detail" class="hidden"> | |
| <div class="btn-row"> | |
| <button id="drill-back" class="btn-mini">← Danh sách</button> | |
| <button id="drill-reset" class="btn-mini">↺ Xếp lại bi</button> | |
| </div> | |
| <h3 id="drill-title"></h3> | |
| <div id="drill-goal"></div> | |
| <div class="row drill-round"> | |
| <span class="lbl">Vòng tập</span> | |
| <b id="drill-count"></b> | |
| </div> | |
| <div class="btn-row"> | |
| <!-- class RIÊNG .btn-pass (không phải .btn-play): init cũ bind | |
| playShot cho mọi .btn-play — dùng ké class đó là bấm "Đạt" | |
| kích luôn animation cú gợi ý (bug bắt được trong smoke 04/08) --> | |
| <button id="drill-pass" class="btn-pass">✔ Đạt</button> | |
| <button id="drill-fail" class="btn-fail">✘ Trượt</button> | |
| </div> | |
| <div id="drill-summary" class="hidden"></div> | |
| <button id="drill-retry" class="btn-mini hidden">↻ Tập vòng mới</button> | |
| <div id="drill-note" class="drill-note hidden">Chế độ demo — tiến bộ | |
| không được lưu (server chưa có cơ sở dữ liệu).</div> | |
| <button id="drill-suggest" class="btn-go" disabled>Xem gợi ý</button> | |
| </div> | |
| </section> | |
| <section class="controls hidden"> | |
| <!-- Bộ chọn engine GỠ 31/07/2026 (BRIEF việc B): app chỉ còn MỘT đường | |
| chọn cú — ZonePlanner V2 (tiêu chí design §8). Server tự chạy nó, | |
| không có gì để chọn. Thời gian thật của từng lần gợi ý vẫn hiện ở | |
| #searchinfo, đo bởi chính server. --> | |
| <div class="row"> | |
| <span class="lbl">Bi trên bàn</span> | |
| <span id="panel-target" class="target-badge">→ bi 1</span> | |
| </div> | |
| <div class="row" id="ball-chips"> | |
| <button class="chip" data-bid="1">1</button> | |
| <button class="chip" data-bid="2">2</button> | |
| <button class="chip" data-bid="3">3</button> | |
| <button class="chip" data-bid="4">4</button> | |
| <button class="chip" data-bid="5">5</button> | |
| <button class="chip" data-bid="6">6</button> | |
| <button class="chip" data-bid="7">7</button> | |
| <button class="chip" data-bid="8">8</button> | |
| <button class="chip" data-bid="9">9</button> | |
| </div> | |
| <div class="row btn-row"> | |
| <button id="btn-preset" class="btn-mini">Bàn mẫu</button> | |
| <button id="btn-undo" class="btn-mini" disabled>↩ Undo</button> | |
| <button id="btn-tv" class="btn-mini">📺 Ghép TV</button> | |
| </div> | |
| <!-- Scan ảnh (F2 MVP, bàn giao 18): id riêng + class style-only — | |
| không mượn class có event binding (bài học .btn-play 04/08) --> | |
| <div class="row btn-row"> | |
| <button id="btn-scan" class="btn-mini" disabled>📷 Scan ảnh bàn</button> | |
| </div> | |
| <!-- form Ghép TV (bàn giao 15): class RIÊNG .tv-pair — không mượn | |
| class có event binding toàn cục (bài học .btn-play 04/08) --> | |
| <div id="tv-pair" class="tv-pair hidden"> | |
| <input id="tv-code" inputmode="numeric" autocomplete="one-time-code" | |
| maxlength="6" placeholder="Mã 6 số trên TV"> | |
| <button id="tv-pair-go">Ghép</button> | |
| </div> | |
| <button id="btn-go" class="btn-go" disabled>Gợi ý cú đánh</button> | |
| <!-- auto-play để NGOÀI #result: #result ẩn tới khi bấm Gợi ý lần đầu, | |
| mà "tự chơi hết ván" lại là nút muốn bấm ngay lúc mở trang --> | |
| <button id="btn-auto" class="btn-mini btn-auto" disabled>▶ Tự chơi hết ván</button> | |
| </section> | |
| <section id="result" class="hidden"> | |
| <div id="shotviz" class="viz hidden"> | |
| <div class="viz-spin"> | |
| <svg id="spin-ball" viewBox="0 0 100 100" aria-label="điểm chạm bi cái"> | |
| <defs> | |
| <radialGradient id="cuegrad" cx="35%" cy="30%" r="80%"> | |
| <stop offset="0%" stop-color="#ffffff"/> | |
| <stop offset="100%" stop-color="#c9c9c9"/> | |
| </radialGradient> | |
| </defs> | |
| <circle cx="50" cy="50" r="46" fill="url(#cuegrad)" stroke="#1c1c1c" stroke-width="1.6"/> | |
| <line x1="50" y1="7" x2="50" y2="93" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/> | |
| <line x1="7" y1="50" x2="93" y2="50" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/> | |
| <circle class="spin-dot" cx="50" cy="50" r="6.5" fill="#e02020" stroke="#7a0000" stroke-width="1.3"/> | |
| </svg> | |
| <div class="viz-cap" id="spin-cap"></div> | |
| </div> | |
| <div class="viz-power"> | |
| <div class="viz-lbl">Lực <b id="power-val"></b></div> | |
| <div id="power-bar" class="power-bar"></div> | |
| <div class="viz-scale"><span>0.5</span><span>m/s</span><span>4.0</span></div> | |
| </div> | |
| </div> | |
| <pre id="describe"></pre> | |
| <div class="btn-row play-row"> | |
| <button id="btn-play" class="btn-play" disabled>Đánh cú này</button> | |
| </div> | |
| <div id="alts"></div> | |
| <div id="searchinfo"></div> | |
| </section> | |
| <section class="legend hidden"> | |
| <span><i class="ring target"></i> bi mục tiêu (số nhỏ nhất)</span> | |
| <span><i class="ring"></i> lỗ đích</span> | |
| <span><i class="ring lowconf"></i> bi scan chưa chắc — kiểm lại</span> | |
| <span>Tắt bi bằng ô số 1-9 để dựng bàn giữa ván</span> | |
| </section> | |
| </aside> | |
| </main> | |
| <div id="backdrop"></div> | |
| <!-- Scan ảnh (F2 MVP, bàn giao 18): overlay riêng, mọi class .scan-* là | |
| class MỚI (bài học .btn-play — không mượn class có event binding). | |
| Ảnh đã resize hiện trên canvas riêng; người dùng chấm 4 GÓC MẶT NỈ theo | |
| thứ tự #scan-hint chỉ dẫn từng bước; góc lưu sessionStorage — ảnh sau | |
| chụp cùng khung hình thì khỏi chấm lại (vẫn có nút Chấm lại góc). --> | |
| <div id="scan-overlay" class="scan-overlay hidden"> | |
| <div class="scan-box"> | |
| <div class="scan-head"> | |
| <b>📷 Scan ảnh bàn</b> | |
| <button id="scan-close" class="scan-x" aria-label="Đóng">✕</button> | |
| </div> | |
| <div id="scan-hint" class="scan-hint"></div> | |
| <canvas id="scan-canvas"></canvas> | |
| <div class="btn-row scan-actions"> | |
| <button id="scan-redo" class="btn-mini">↺ Chấm lại góc</button> | |
| <!-- .an-cta chứ KHÔNG PHẢI .btn-go (BG26, cùng bug 2 nút Analyzer | |
| 9fa4c18): .btn-go có binding recommend() toàn cục + syncButtons | |
| ép label/disabled — mượn là "Quét thế bi" gọi ké engine chạy nền | |
| và giành label với syncScanUi. --> | |
| <button id="scan-go" class="an-cta scan-go" disabled>Quét thế bi</button> | |
| </div> | |
| </div> | |
| </div> | |
| <input type="file" id="scan-file" accept="image/*" class="hidden"> | |
| <!-- Analyzer (bàn giao 24): overlay chấm 4 POCKET GÓC trên frame ĐẦU của | |
| chính clip vừa chọn (FE tự trích bằng <video> — không round-trip server). | |
| Container mượn class STYLE-ONLY .scan-overlay/.scan-box (không có event | |
| binding nào bind theo các class đó — bài học .btn-play chỉ cấm mượn class | |
| CÓ binding); mọi phần tử tương tác mang id .an-* riêng. --> | |
| <div id="an-overlay" class="scan-overlay hidden"> | |
| <div class="scan-box"> | |
| <div class="scan-head"> | |
| <b>🎬 Phân tích video — 4 pocket góc</b> | |
| <button id="an-close" class="scan-x" aria-label="Đóng">✕</button> | |
| </div> | |
| <div id="an-hint" class="scan-hint"></div> | |
| <!-- BG31: chuẩn ĐỘ CAO điểm click — bù độ cao tâm bi (worker) tính cho | |
| mặt phẳng VẢI; click cao hơn (mép gỗ/miệng lỗ, z_c ≈ +21mm phiên | |
| pilot) làm bù thành quá tay — BG30 đã đo tác hại. Note TĨNH, class | |
| .an-note MỚI style-only (bài học .btn-play: không mượn class có | |
| binding). Hình = mặt cắt ngang băng: gỗ + miệng lỗ trên cao, băng | |
| cao su nghiêng xuống, mũi nose chạm mặt vải là điểm ĐÚNG. --> | |
| <div id="an-note" class="an-note"> | |
| <svg viewBox="0 0 168 62" aria-hidden="true"> | |
| <rect x="2" y="46" width="164" height="8" rx="1" fill="#2e7d46"/> | |
| <polygon points="2,16 46,16 76,46 2,46" fill="#1f5c33"/> | |
| <rect x="2" y="6" width="44" height="10" rx="2" fill="#8a5a2b"/> | |
| <ellipse cx="15" cy="11" rx="8" ry="4.5" fill="#141414"/> | |
| <text x="28" y="14" fill="#ff6b6b" font-size="10.5">✗ miệng lỗ</text> | |
| <circle cx="76" cy="46" r="5.5" fill="none" stroke="#39d353" | |
| stroke-width="2"/> | |
| <text x="86" y="50" fill="#39d353" font-size="10.5">✓ mép nose vải</text> | |
| </svg> | |
| <span>Chấm đúng <b>mép nose vải</b> — nơi mũi băng cao su gặp mặt vải; | |
| <b>KHÔNG</b> chấm miệng lỗ trên mặt gỗ.</span> | |
| </div> | |
| <canvas id="an-canvas"></canvas> | |
| <!-- Chỉ hiện ở bản deploy CPU (health.mode == "local") — JS bật/tắt. --> | |
| <div id="an-cpu-warn" class="an-note hidden">⏳ <b>Server này phân tích | |
| bằng CPU</b> (bản demo online, không GPU) nên chậm — đo được: | |
| <b>clip 5,5 giây mất ~26 giây</b>, một rack 5 phút ước | |
| <b>10–12 phút</b>. Bảng cú <b>hiện dần từng cú</b> nên xem được ngay | |
| khi cú đầu xong; đóng tab cũng không mất kết quả, mở lại link là còn. | |
| Muốn xem ngay thì bảng đang có sẵn <b>video mẫu đã phân tích</b>.</div> | |
| <div class="btn-row scan-actions"> | |
| <button id="an-redo" class="btn-mini">↺ Chấm lại</button> | |
| <!-- Xoay nhãn (BRIEF 14/08 việc A): máy đoán CẶP BĂNG NGẮN bằng | |
| heuristic bất đối xứng độ dài (poolcoach_cv/autocorner) — đoán sai | |
| thì xoay ①②③④ một nhịp là xong, khỏi chấm lại 4 điểm. --> | |
| <button id="an-rotate" class="btn-mini" disabled>⟳ Xoay nhãn</button> | |
| <!-- .an-cta thay .btn-go — cùng lý do #anv-pick (binding recommend + | |
| syncButtons ép label/disabled đè lên syncAnUi). --> | |
| <button id="an-go" class="an-cta scan-go" disabled>Phân tích</button> | |
| </div> | |
| </div> | |
| </div> | |
| <input type="file" id="an-file" accept="video/mp4,video/quicktime,.mp4,.mov" class="hidden"> | |
| <!-- quick bar mobile: options + spin/lực tóm tắt + nút Gợi ý, không cần scroll. | |
| Quickbar nằm NGOÀI #panel (container 3 tab) — cố ý, vì nó phải nổi cố định | |
| đáy màn hình kể cả khi sheet đóng. Hệ quả: setTab() KHÔNG ẩn nó theo tab, | |
| nên phần thuộc tab Gợi ý trong đây phải được ẩn TAY (bug Danh 14/08: mở | |
| app trên điện thoại vào thẳng tab Analyzer mà đáy vẫn là UI gợi ý). | |
| Chia đôi rạch ròi: | |
| .qb-settings = ĐIỀU HƯỚNG, luôn hiện — trên mobile #panel là | |
| display:none tới khi "Chi tiết ▴" mở sheet, ẩn hàng này | |
| là nhốt người dùng trong tab hiện tại, không còn lối ra; | |
| #qb-target + .qb-main = UI tab GỢI Ý (badge bi mục tiêu, chấm đầu cơ, | |
| thanh lực, nút Gợi ý/Đánh) — ẩn khi ở tab Analyzer. | |
| Trạng thái tĩnh dưới đây khớp boot (Analyzer active), đúng nếp | |
| .controls/.legend ở trên: MỘT nguồn sự thật, không flash tab. --> | |
| <nav id="quickbar"> | |
| <div class="qb-row qb-settings"> | |
| <span id="qb-target" class="target-badge hidden">→ bi 1</span> | |
| <!-- lối vào tab Bài tập trên mobile (BRIEF 04/08: chỉ thêm lối vào, | |
| không đụng gì khác của quickbar) --> | |
| <button id="qb-drills">🎯 Bài tập</button> | |
| <button id="qb-detail">Chi tiết ▴</button> | |
| </div> | |
| <div class="qb-row qb-main hidden"> | |
| <svg id="qb-spin" viewBox="0 0 100 100" aria-label="điểm chạm bi cái"> | |
| <defs> | |
| <radialGradient id="qb-cuegrad" cx="35%" cy="30%" r="80%"> | |
| <stop offset="0%" stop-color="#ffffff"/> | |
| <stop offset="100%" stop-color="#c9c9c9"/> | |
| </radialGradient> | |
| </defs> | |
| <circle cx="50" cy="50" r="46" fill="url(#qb-cuegrad)" stroke="#1c1c1c" stroke-width="1.6"/> | |
| <line x1="50" y1="7" x2="50" y2="93" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/> | |
| <line x1="7" y1="50" x2="93" y2="50" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/> | |
| <circle class="spin-dot" cx="50" cy="50" r="6.5" fill="#e02020" stroke="#7a0000" stroke-width="1.3"/> | |
| </svg> | |
| <div class="qb-mid"> | |
| <div class="power-bar qb-power"></div> | |
| <div id="qb-info">Đặt bi rồi bấm Gợi ý</div> | |
| </div> | |
| <button id="qb-go" class="btn-go" disabled>Gợi ý</button> | |
| <button id="qb-play" class="btn-play" disabled>Đánh</button> | |
| </div> | |
| <!-- Hàng quickbar của tab ANALYZER (14/08, Danh yêu cầu) — đối xứng với | |
| .qb-main ở trên: thông số cú đang mở + nút đánh lại, ngay đáy màn hình. | |
| Vì sao cần: trên mobile #panel là bottom sheet CHE BÀN, nên bấm "Đánh | |
| lại cú" trong sheet là animate sau lưng người dùng; hàng này cho thấy | |
| số trong lúc bi chạy trên bàn (đúng cách tab Gợi ý vẫn làm). | |
| Chỉ hiện khi ĐANG mở một cú (ANV.detail) ở tab Analyzer — ở màn danh | |
| sách thì không có gì để hiện. | |
| Class tương tác RIÊNG .qban-*; chấm spin + thanh lực CỐ Ý mang class | |
| .and-spin-dot/.and-power-bar của widget trang chi tiết để initPowerBar | |
| và fillAndShotviz nuôi CẢ HAI từ một nguồn số — không mượn | |
| .spin-dot/.power-bar/.btn-play/.btn-go (bị updateShotViz + syncButtons | |
| quét toàn cục, bài học .btn-play 04/08). --> | |
| <div class="qb-row qb-an hidden"> | |
| <svg id="qban-spin" viewBox="0 0 100 100" aria-label="điểm chạm bi cái (model)"> | |
| <defs> | |
| <radialGradient id="qban-cuegrad" cx="35%" cy="30%" r="80%"> | |
| <stop offset="0%" stop-color="#ffffff"/> | |
| <stop offset="100%" stop-color="#c9c9c9"/> | |
| </radialGradient> | |
| </defs> | |
| <circle cx="50" cy="50" r="46" fill="url(#qban-cuegrad)" stroke="#1c1c1c" stroke-width="1.6"/> | |
| <line x1="50" y1="7" x2="50" y2="93" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/> | |
| <line x1="7" y1="50" x2="93" y2="50" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/> | |
| <circle class="and-spin-dot" cx="50" cy="50" r="6.5" fill="#e02020" stroke="#7a0000" stroke-width="1.3"/> | |
| </svg> | |
| <div class="qb-mid"> | |
| <div class="and-power-bar qb-power qban-bar"></div> | |
| <div id="qban-info"></div> | |
| </div> | |
| <button id="qban-replay" class="an-cta qban-replay" disabled>▶ Đánh lại</button> | |
| </div> | |
| </nav> | |
| <div id="toast" class="hidden"></div> | |
| <script src="app.js"></script> | |
| </body> | |
| </html> | |