[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>专业高级提词器 (Turbo 零延迟算法版)</title>
<style>
:root {
--bg-color: #111111;
--text-color: #555555;
--highlight-color: #00ff66;
--panel-bg: rgba(30, 30, 30, 0.85);
--cue-top: 35%;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body, html {
width: 100%; height: 100%; overflow: hidden;
background-color: var(--bg-color);
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
}
#container { display: flex; width: 100%; height: 100%; position: relative; }
/* 左侧设置面板 */
#control-panel {
width: 320px; height: 100%; background: var(--panel-bg);
backdrop-filter: blur(10px); border-right: 1px solid #333;
padding: 20px; color: #fff; display: flex; flex-direction: column;
gap: 14px; z-index: 10; transition: transform 0.3s ease; overflow-y: auto;
}
#control-panel.hidden { transform: translateX(-320px); position: absolute; }
/* 右侧提词显示区域 */
#display-area { flex: 1; height: 100%; position: relative; overflow: hidden; display: flex; flex-direction: column; }
/* 视线对齐辅助线区域 */
#cue-zone {
position: absolute; top: var(--cue-top); left: 0; width: 100%; height: 80px; margin-top: -40px;
border-top: 1px dashed rgba(255,255,255,0.15); border-bottom: 1px dashed rgba(255,255,255,0.15);
background: rgba(255, 255, 255, 0.01); z-index: 5; pointer-events: none; display: flex; align-items: center;
}
#cue-line { width: 100%; height: 2px; background: linear-gradient(90deg, transparent, var(--highlight-color), transparent); position: relative; }
#cue-line::before, #cue-line::after { content: '►'; position: absolute; color: var(--highlight-color); font-size: 20px; top: -12px; }
#cue-line::before { left: 10px; }
#cue-line::after { right: 10px; transform: rotate(180deg); }
/* 滚动容器 */
#scroll-wrapper {
flex: 1; overflow-y: scroll; padding: 35vh 8% 65vh 8%;
/* 缩短过渡时间至 80ms,实现近乎瞬移的干脆感,消除粘滞感 */
scroll-behavior: smooth;
}
#scroll-wrapper.instant-mode { scroll-behavior: auto; }
#scroll-wrapper::-webkit-scrollbar { display: none; }
#scroll-wrapper { -ms-overflow-style: none; scrollbar-width: none; }
#text-content { display: flex; flex-direction: column; gap: 20px; }
/* 单行文本样式 */
.script-line {
font-size: 55px; line-height: 1.4; color: var(--text-color); font-weight: bold;
white-space: pre-wrap; word-break: break-all; transition: color 0.1s ease, transform 0.1s ease, opacity 0.1s ease;
opacity: 0.2; transform: scale(0.95); transform-origin: left center;
}
/* 文字过线高亮样式 */
.script-line.active { color: var(--highlight-color) !important; opacity: 1; transform: scale(1.02); }
/* 镜像翻转 */
.mirror-x { transform: scaleX(-1); }
.mirror-y { transform: scaleY(-1); }
.mirror-both { transform: scale(-1, -1); }
.mirror-x .script-line, .mirror-both .script-line { transform-origin: right center; }
/* UI 布局 */
.form-group { display: flex; flex-direction: column; gap: 6px; }
label { font-size: 12px; color: #aaa; }
textarea { width: 100%; height: 90px; background: #222; border: 1px solid #444; color: #fff; padding: 10px; border-radius: 4px; resize: none; font-size: 13px; }
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
button { background: #333; color: #fff; border: 1px solid #555; padding: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; transition: all 0.2s; font-size: 13px; }
button:hover { background: #444; border-color: var(--highlight-color); }
button.primary { background: var(--highlight-color); color: #000; border: none; }
input[type="range"] { width: 100%; accent-color: var(--highlight-color); }
select { background: #222; color: #fff; padding: 7px; border-radius: 4px; border: 1px solid #444; font-size: 13px; }
.value-display { float: right; color: var(--highlight-color); }
/* AI跟随高级面板 */
.ai-panel { background: rgba(0, 255, 102, 0.06); border: 1px solid rgba(0, 255, 102, 0.2); padding: 12px; border-radius: 6px; }
.ai-toggle-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.ai-status-dot { font-size: 12px; display: flex; align-items: center; gap: 4px; color: #aaa; }
.ai-status-dot::before { content: ''; display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #666; }
.ai-status-dot.active::before { background: #00ff66; box-shadow: 0 0 8px #00ff66; }
.ai-speech-preview { font-size: 11px; color: #888; background: #222; padding: 6px; border-radius: 4px; min-height: 28px; word-break: break-all; margin-top: 4px; max-height: 50px; overflow-y: auto; }
.color-picker-group { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.color-dot { width: 24px; height: 24px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; }
.color-dot.selected { border-color: #fff; transform: scale(1.1); }
.more-color-wrapper { position: relative; width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(45deg, red, purple); overflow: hidden; cursor: pointer;}
.more-color-wrapper input[type="color"] { position: absolute; top: -5px; left: -5px; width: 34px; height: 34px; opacity: 0; cursor: pointer; }
#toggle-panel-btn { position: absolute; left: 10px; top: 10px; z-index: 11; background: rgba(0,0,0,0.6); border: 1px solid #444; padding: 5px 10px; font-size: 12px; color: #fff; cursor: pointer; }
#status-bar { position: absolute; bottom: 10px; right: 20px; color: #666; font-size: 13px; z-index: 5; pointer-events: none; }
</style>
</head>
<body>
<div id="container">
<div id="control-panel">
<h4>高级定制提词器Turbo零延迟算法版<br />AI检测每句前几个字</h4>
<hr style="border-color: #333;">
<div class="form-group">
<label>提词文本内容</label>
<textarea id="text-input" placeholder="请输入或粘贴演讲稿...">欢迎体验 Turbo 零延迟算法版!
当前版本专门解决网络传输造成的滞后。
我们启用了激进的前端单字预判机制。
只要你读出某行开头的第一个字。
系统就会强行中断一切无用匹配。
直接把这一行推向屏幕中央。
不需要等整句话吐完。
快开口试一下它的敏捷度吧!</textarea>
</div>
<div class="form-group ai-panel">
<div class="ai-toggle-row">
<label style="color: #00ff66; font-weight: bold;">Turbo 语音跟随</label>
<button id="ai-mode-btn" style="padding: 4px 10px; background: #333; font-size: 12px; border-color: #444;">已关闭</button>
</div>
<div style="display: flex; justify-content: space-between;">
<span id="ai-dot" class="ai-status-dot">未启用监听</span>
</div>
<div id="speech-preview" class="ai-speech-preview">暂无语音输入...</div>
</div>
<div class="form-group">
<label>常规控制 (匀速模式)</label>
<div class="btn-row">
<button id="start-btn" class="primary">播放 (Space)</button>
<button id="reset-btn">重置 (Esc)</button>
</div>
</div>
<div class="form-group">
<label>每行强制字数 (个) <span id="max-chars-val" class="value-display">20</span></label>
<input type="range" id="max-chars-slider" min="5" max="40" value="20">
</div>
<div class="form-group" id="speed-group-box">
<label>常规滚动速度 (像素/秒) <span id="speed-val" class="value-display">45</span></label>
<input type="range" id="speed-slider" min="10" max="250" value="45">
</div>
<div class="form-group">
<label>文字大小 (px) <span id="font-val" class="value-display">55</span></label>
<input type="range" id="font-slider" min="24" max="100" value="55">
</div>
<div class="form-group">
<label>引导线高度位置 (%) <span id="line-pos-val" class="value-display">35</span></label>
<input type="range" id="line-pos-slider" min="15" max="80" value="35">
</div>
<div class="form-group">
<label>提词背景颜色</label>
<div class="color-picker-group" id="bg-color-group">
<div class="color-dot selected" style="background: #111111;" data-color="#111111"></div>
<div class="color-dot" style="background: #22252a;" data-color="#22252a"></div>
<div class="color-dot" style="background: #091124;" data-color="#091124"></div>
<div class="color-dot" style="background: #091a0f;" data-color="#091a0f"></div>
<div class="more-color-wrapper">
<input type="color" id="bg-color-more" value="#111111">
</div>
</div>
</div>
<div class="form-group">
<label>普通文字颜色</label>
<div class="color-picker-group" id="base-color-group">
<div class="color-dot selected" style="background: #555555;" data-color="#555555"></div>
<div class="color-dot" style="background: #888888;" data-color="#888888"></div>
<div class="color-dot" style="background: #ffffff;" data-color="#ffffff"></div>
<div class="color-dot" style="background: #224263;" data-color="#224263"></div>
<div class="more-color-wrapper">
<input type="color" id="base-color-more" value="#555555">
</div>
</div>
</div>
<div class="form-group">
<label>过线高亮颜色</label>
<div class="color-picker-group" id="highlight-color-group">
<div class="color-dot selected" style="background: #00ff66;" data-color="#00ff66"></div>
<div class="color-dot" style="background: #ff007f;" data-color="#ff007f"></div>
<div class="color-dot" style="background: #00e5ff;" data-color="#00e5ff"></div>
<div class="color-dot" style="background: #ffcc00;" data-color="#ffcc00"></div>
<div class="more-color-wrapper">
<input type="color" id="highlight-color-more" value="#00ff66">
</div>
</div>
</div>
<div class="form-group">
<label>文本镜像 (分光镜适配)</label>
<select id="mirror-select">
<option value="none">正常模式 (常规直视)</option>
<option value="x">水平镜像 (左右反转)</option>
<option value="y">垂直镜像 (上下反转)</option>
<option value="both">全镜像 (中心对称)</option>
</select>
</div>
<div class="form-group" style="margin-top: 5px;">
<button id="fullscreen-btn">进入全屏演讲模式</button>
</div>
</div>
<!-- 右侧提词器主体区域 -->
<div id="display-area">
<button id="toggle-panel-btn">隐藏菜单</button>
<div id="cue-zone"><div id="cue-line"></div></div>
<div id="scroll-wrapper"><div id="text-content"></div></div>
<div id="status-bar">模式: <span id="status-mode-text">常规滚动</span> | 状态: <span id="status-run">准备</span></div>
</div>
</div>
<script>
const textInput = document.getElementById('text-input');
const textContent = document.getElementById('text-content');
const scrollWrapper = document.getElementById('scroll-wrapper');
const startBtn = document.getElementById('start-btn');
const resetBtn = document.getElementById('reset-btn');
const speedSlider = document.getElementById('speed-slider');
const speedVal = document.getElementById('speed-val');
const fontSlider = document.getElementById('font-slider');
const fontVal = document.getElementById('font-val');
const maxCharsSlider = document.getElementById('max-chars-slider');
const maxCharsVal = document.getElementById('max-chars-val');
const mirrorSelect = document.getElementById('mirror-select');
const linePosSlider = document.getElementById('line-pos-slider');
const linePosVal = document.getElementById('line-pos-val');
const fullscreenBtn = document.getElementById('fullscreen-btn');
const controlPanel = document.getElementById('control-panel');
const togglePanelBtn = document.getElementById('toggle-panel-btn');
const statusRun = document.getElementById('status-run');
const statusModeText = document.getElementById('status-mode-text');
const aiModeBtn = document.getElementById('ai-mode-btn');
const aiDot = document.getElementById('ai-dot');
const speechPreview = document.getElementById('speech-preview');
const speedGroupBox = document.getElementById('speed-group-box');
let isPlaying = false;
let isHovered = false;
let speed = parseInt(speedSlider.value);
let lastTimestamp = 0;
let currentScrollY = 0;
let animationFrameId = null;
let observer = null;
let isAiMode = false;
let recognition = null;
let isUserStoppingVoice = false;
let parsedLinesArray = [];
// 【Turbo 核心变量】:引入历史锁步单向指针
let currentLinePointer = 0;
function autoSplitText(rawText) {
const paragraphs = rawText.split('\n');
const processedLines = [];
const maxCharsPerLine = parseInt(maxCharsSlider.value);
paragraphs.forEach(para => {
if (para.trim() === "") return;
let currentLine = "";
for (let i = 0; i < para.length; i++) {
const char = para[i];
currentLine += char;
const isPunctuation = ['。', '!', '?', ';', ',', '.', '!', '?', ';', ','].includes(char);
if ((isPunctuation && currentLine.length >= Math.max(5, maxCharsPerLine * 0.6)) || currentLine.length >= maxCharsPerLine) {
if (i + 1 < para.length && ['。', '!', '?', ';', ',', '、', '.', '!', '?', ';', ','].includes(para[i+1])) {
continue;
}
processedLines.push(currentLine.trim());
currentLine = "";
}
}
if (currentLine.trim() !== "") processedLines.push(currentLine.trim());
});
return processedLines;
}
function syncTextPositionAndObserver() {
const linePercent = parseInt(linePosSlider.value);
const fontSize = parseInt(fontSlider.value);
scrollWrapper.style.paddingTop = `calc(${linePercent}vh - ${fontSize * 0.6}px)`;
scrollWrapper.style.paddingBottom = `calc(${100 - linePercent}vh + 150px)`;
if (observer) observer.disconnect();
const topMargin = linePercent - 1;
const bottomMargin = 100 - linePercent - 3;
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio > 0.15) {
if(!isAiMode) {
document.querySelectorAll('.script-line').forEach(el => el.classList.remove('active'));
entry.target.classList.add('active');
}
}
});
}, {
root: scrollWrapper,
rootMargin: `-${topMargin}% 0% -${bottomMargin}% 0%`,
threshold: [0.01, 0.15, 0.5, 0.8, 1.0]
});
document.querySelectorAll('.script-line').forEach(line => observer.observe(line));
}
linePosSlider.addEventListener('input', (e) => {
const val = e.target.value;
linePosVal.innerText = val;
document.documentElement.style.setProperty('--cue-top', val + '%');
syncTextPositionAndObserver();
if (!isPlaying && !isAiMode) {
scrollWrapper.scrollTop = 0;
currentScrollY = 0;
}
});
function renderText() {
textContent.innerHTML = '';
parsedLinesArray = autoSplitText(textInput.value);
parsedLinesArray.forEach((lineText) => {
const lineDiv = document.createElement('div');
lineDiv.className = 'script-line';
lineDiv.innerText = lineText;
lineDiv.style.fontSize = fontSlider.value + 'px';
textContent.appendChild(lineDiv);
});
if(textContent.children.length > 0) textContent.children[0].classList.add('active');
syncTextPositionAndObserver();
}
maxCharsSlider.addEventListener('input', (e) => {
maxCharsVal.innerText = e.target.value;
renderText();
});
// 调色板
function setupColorPicker(groupId, moreInputId, cssVariableName, onUpdateCallback) {
const group = document.getElementById(groupId);
const dots = group.querySelectorAll('.color-dot');
const moreInput = document.getElementById(moreInputId);
dots.forEach(dot => {
dot.addEventListener('click', () => {
group.querySelector('.color-dot.selected')?.classList.remove('selected');
dot.classList.add('selected');
const color = dot.getAttribute('data-color');
document.documentElement.style.setProperty(cssVariableName, color);
if(onUpdateCallback) onUpdateCallback(color);
});
});
moreInput.addEventListener('input', (e) => {
group.querySelector('.color-dot.selected')?.classList.remove('selected');
const color = e.target.value;
document.documentElement.style.setProperty(cssVariableName, color);
if(onUpdateCallback) onUpdateCallback(color);
});
}
setupColorPicker('bg-color-group', 'bg-color-more', '--bg-color');
setupColorPicker('base-color-group', 'base-color-more', '--text-color');
setupColorPicker('highlight-color-group', 'highlight-color-more', '--highlight-color', (color) => {
if (!isPlaying) startBtn.style.background = color;
});
function scrollEngine(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
const elapsed = timestamp - lastTimestamp;
lastTimestamp = timestamp;
if (isPlaying && !isHovered && !isAiMode) {
currentScrollY += (speed / 1000) * elapsed;
scrollWrapper.scrollTop = currentScrollY;
const lines = document.querySelectorAll('.script-line');
if (lines.length > 0) {
const lastLine = lines[lines.length - 1];
const lastLineRect = lastLine.getBoundingClientRect();
const cueZoneRect = document.getElementById('cue-zone').getBoundingClientRect();
if (lastLineRect.bottom < cueZoneRect.top) stop();
}
}
if(!isAiMode) animationFrameId = requestAnimationFrame(scrollEngine);
}
function start() {
if (isAiMode) return;
if (!isPlaying) {
isPlaying = true;
startBtn.innerText = "暂停 (Space)";
startBtn.style.background = "#ff3333";
statusRun.innerText = "播放中";
lastTimestamp = 0;
currentScrollY = scrollWrapper.scrollTop;
animationFrameId = requestAnimationFrame(scrollEngine);
}
}
function stop() {
if (isPlaying) {
isPlaying = false;
startBtn.innerText = "播放 (Space)";
startBtn.style.background = "var(--highlight-color)";
statusRun.innerText = "已暂停";
if (animationFrameId) cancelAnimationFrame(animationFrameId);
}
}
function togglePlay() { isPlaying ? stop() : start(); }
function resetScroll() {
stop();
scrollWrapper.scrollTop = 0;
currentScrollY = 0;
currentLinePointer = 0;
statusRun.innerText = "准备";
document.querySelectorAll('.script-line').forEach((el, idx) => {
if(idx === 0) el.classList.add('active');
else el.classList.remove('active');
});
speechPreview.innerText = "暂无语音输入...";
}
// ================= 【深度整合:Turbo 级零延迟匹配引擎】 =================
function initVoiceRecognition() {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
alert("当前浏览器不支持原生语音识别。请使用 Chrome 或 Edge 浏览器。");
disableAiMode();
return;
}
recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true; // 抓取毫秒级未定型音频流
recognition.lang = 'zh-CN';
recognition.onstart = () => {
aiDot.innerText = "Turbo 级强行卡位聆听中...";
aiDot.classList.add('active');
statusRun.innerText = "语音跟随中";
};
recognition.onerror = (event) => {
console.warn("语音通道异常: ", event.error);
if (event.error === 'not-allowed') {
alert("麦克风授信被拒!");
disableAiMode();
}
};
recognition.onend = () => {
if (isAiMode && !isUserStoppingVoice) {
try { recognition.start(); } catch(e) {}
} else {
aiDot.innerText = "监听已结束";
aiDot.classList.remove('active');
}
};
recognition.onresult = (event) => {
let textBuffer = '';
for (let i = event.resultIndex; i < event.results.length; ++i) {
textBuffer += event.results[i][0].transcript;
}
const cleanSpeechText = textBuffer.replace(/[,。?!、.,?!]/g, "").trim();
if(!cleanSpeechText) return;
speechPreview.innerText = cleanSpeechText;
// 触发激进的前端单字预判与历史锁步算法
matchTextTurbo(cleanSpeechText);
};
}
function matchTextTurbo(speechText) {
const linesDOM = document.querySelectorAll('.script-line');
if(linesDOM.length === 0) return;
// 雷达扫描范围:当前指针位置往后看 4 行(丢弃久远的历史文本,减少 90% 无用计算)
const lookAheadLimit = Math.min(currentLinePointer + 4, linesDOM.length);
for (let i = currentLinePointer; i < lookAheadLimit; i++) {
const cleanLineText = parsedLinesArray[i].replace(/[,。?!、.,?!]/g, "");
// 加速策略 1:提取待命行的前 2 个字
const lineStartTokens = cleanLineText.slice(0, 2);
// 加速策略 2:提取实时语音最后吐出的 2~4 个字
const realTimeVoiceTail = speechText.slice(-4);
// 【首字强制唤醒条件】:新词尾部包含了下一行开头,或全串包含,或最后两字刚好咬合
if (
realTimeVoiceTail.includes(lineStartTokens) ||
speechText.includes(cleanLineText) ||
(lineStartTokens && speechText.slice(-2) === lineStartTokens)
) {
if (i !== currentLinePointer) {
// 锁步向前,彻底封闭过去的行
currentLinePointer = i;
// 更新高亮样式
linesDOM.forEach(el => el.classList.remove('active'));
linesDOM[i].classList.add('active');
// 毫秒级像素咬合滚动
const wrapperHeight = scrollWrapper.clientHeight;
const linePercent = parseInt(linePosSlider.value);
const cueZonePixelTop = (linePercent / 100) * wrapperHeight;
const lineOffsetTop = linesDOM[i].offsetTop;
const targetScrollTop = lineOffsetTop - cueZonePixelTop + (linesDOM[i].clientHeight / 2);
scrollWrapper.scrollTop = Math.max(0, targetScrollTop);
break;
}
}
}
}
function enableAiMode() {
isAiMode = true;
isUserStoppingVoice = false;
stop();
scrollWrapper.classList.remove('instant-mode');
aiModeBtn.innerText = "已开启";
aiModeBtn.style.background = "var(--highlight-color)";
aiModeBtn.style.color = "#000";
statusModeText.innerText = "Turbo语音跟随";
speedGroupBox.style.opacity = "0.3";
speedGroupBox.style.pointerEvents = "none";
startBtn.style.pointerEvents = "none";
startBtn.style.opacity = "0.4";
if (!recognition) initVoiceRecognition();
currentLinePointer = 0; // 开启时重置历史锁步指针
try { recognition.start(); } catch(e) {}
}
function disableAiMode() {
isAiMode = false;
isUserStoppingVoice = true;
scrollWrapper.classList.add('instant-mode');
aiModeBtn.innerText = "已关闭";
aiModeBtn.style.background = "#333";
aiModeBtn.style.color = "#fff";
aiDot.innerText = "未启用监听";
aiDot.classList.remove('active');
statusModeText.innerText = "常规滚动";
speedGroupBox.style.opacity = "1";
speedGroupBox.style.pointerEvents = "auto";
startBtn.style.pointerEvents = "auto";
startBtn.style.opacity = "1";
speechPreview.innerText = "暂无语音输入...";
if (recognition) { try { recognition.stop(); } catch(e) {} }
resetScroll();
}
aiModeBtn.addEventListener('click', () => {
if (!isAiMode) enableAiMode();
else disableAiMode();
});
// 基础控制绑定
textInput.addEventListener('input', renderText);
textInput.addEventListener('paste', () => setTimeout(renderText, 50));
startBtn.addEventListener('click', togglePlay);
resetBtn.addEventListener('click', resetScroll);
speedSlider.addEventListener('input', (e) => {
speed = parseInt(e.target.value);
speedVal.innerText = speed;
});
fontSlider.addEventListener('input', (e) => {
fontVal.innerText = e.target.value;
document.querySelectorAll('.script-line').forEach(el => el.style.fontSize = e.target.value + 'px');
setTimeout(syncTextPositionAndObserver, 150);
});
mirrorSelect.addEventListener('change', (e) => {
textContent.className = '';
if (e.target.value !== 'none') textContent.classList.add('mirror-' + e.target.value);
});
togglePanelBtn.addEventListener('click', () => {
controlPanel.classList.toggle('hidden');
togglePanelBtn.innerText = controlPanel.classList.contains('hidden') ? "显示菜单" : "隐藏菜单";
});
fullscreenBtn.addEventListener('click', () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => alert(`全屏失败: ${err.message}`));
} else {
document.exitFullscreen();
}
});
scrollWrapper.addEventListener('mouseenter', () => { isHovered = true; if(isPlaying && !isAiMode) statusRun.innerText = "悬停暂停"; });
scrollWrapper.addEventListener('mouseleave', () => { isHovered = false; if(isPlaying && !isAiMode) statusRun.innerText = "播放中"; });
window.addEventListener('keydown', (e) => {
if (document.activeElement === textInput) return;
if (e.code === 'Space') { e.preventDefault(); if(!isAiMode) togglePlay(); }
if (e.code === 'Escape') resetScroll();
});
// 初始化
renderText();
resetScroll();
</script>
</body>
</html>