🎵 舞蹈节拍表

BPM: ${bpm}  ·  总时长: ${fmtTime(duration)}  ·  共 ${phrases.length} 个八拍
${rows}
#12345678
由舞蹈拍子计数器生成  ·  Ctrl+P 可打印
`; const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([html],{type:'text/html'})); a.download = `节拍表_${bpm}BPM.html`; a.click(); URL.revokeObjectURL(a.href); }); // ================================================================ // rAF: beat overlay + metronome + AB loop + timeline // ================================================================ function rafLoop() { if (beatTimes.length) { const t = vid.currentTime, off = currentOffset; const flash = Math.min(FLASH_SEC, beatInterval*.45); let found = false, currentBeatIdx = -1; for (let i = 0; i < beatTimes.length; i++) { if (t >= beatTimes[i] && t < beatTimes[i]+flash) { beatNumEl.textContent = ((i+off)%8)+1; beatNumEl.classList.add('show'); found = true; currentBeatIdx = i; break; } } if (!found) beatNumEl.classList.remove('show'); // Trigger metronome click or voice count on each new beat if (currentBeatIdx !== lastMetronomeBeatIdx) { lastMetronomeBeatIdx = currentBeatIdx; if (currentBeatIdx >= 0 && !vid.paused) { const beatNum = ((currentBeatIdx + off) % 8) + 1; if (audioMode === 'tick') { playMetronomeTick(beatNum); } else if (audioMode === 'voice') { playVoiceCount(beatNum); } } } // 1-4 / 1-8 count loop enforcement enforceCountLoop(t); // AB loop enforcement if (loopEnabled && loopA!==null && loopB!==null && loopB>loopA && !vid.paused) { if (t >= loopB) vid.currentTime = loopA; } // Beat strip: move active indicator to current beat (persists between flash windows) if (bsMainCells.length) { let lo = 0, hi = beatTimes.length - 1, nearIdx = -1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (beatTimes[mid] <= t) { nearIdx = mid; lo = mid + 1; } else hi = mid - 1; } const bsPos = nearIdx >= 0 ? (nearIdx + off) % 8 : -1; if (bsPos !== lastBsActiveBeat) { if (lastBsActiveBeat >= 0 && bsMainCells[lastBsActiveBeat]) bsMainCells[lastBsActiveBeat].classList.remove('bs-active'); if (bsPos >= 0 && bsMainCells[bsPos]) bsMainCells[bsPos].classList.add('bs-active'); lastBsActiveBeat = bsPos; } } if (!vid.paused && !vid.ended) drawTimeline(); } // Sync custom controls if (!seekBar.matches(':active')) seekBar.value = vid.currentTime; if (vid.duration) vidTimeLbl.textContent = `${fmtTime(vid.currentTime)} / ${fmtTime(vid.duration)}`; requestAnimationFrame(rafLoop); } rafLoop();