static-demo / smooth-demo.html
duqing2026's picture
备份
990e7fd
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>平滑流式文本演示</title>
<style>
#output {
line-height: 1.6;
font-family: sans-serif;
white-space: pre-wrap;
border: 1px solid #ccc;
padding: 20px;
max-width: 600px;
}
.latest {
position: relative;
display: inline-block;
}
.latest::after {
content: "";
position: absolute;
inset: 0;
background: rgba(19, 2, 2, 0.08);
border-radius: 2px;
pointer-events: none;
}
.cursor { display: inline-block; width: 2px; height: 1.2em; background: #007bff; margin-left: 2px; vertical-align: middle;
animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: 0; } }
</style>
</head>
<body>
<div id="output"></div>
<script>
const outputEl = document.getElementById('output');
let textQueue = []; // 待显示的文字队列
let isRendering = false;
// 模拟从 API 获取流式数据
async function mockApiStream() {
const text = "Gemini 的自然感来源于前端的平滑步进控制。我们不直接渲染 API 推送的每一个字节,而是通过一个内部队列进行缓冲,并利用 requestAnimationFrame 配合固定的步长进行‘匀速消费’。这种做法能有效屏蔽网络不稳带来的突跳感,让文字像打字机一样优雅匀称地呈现。如果你给我一个具体网站/页面类型(静态还是 JS 渲染、是否需要登录、要哪些字段),我可以按你的场景把“翻页 + 去重 + 保存格式 + 稳定性策略”写成一份更贴近实战的方案。如果你给我一个具体网站/页面类型(静态还是 JS 渲染、是否需要登录、要哪些字段),我可以按你的场景把“翻页 + 去重 + 保存格式 + 稳定性策略”写成一份更贴近实战的方案。Gemini 的自然感来源于前端的平滑步进控制。我们不直接渲染 API 推送的每一个字节,而是通过一个内部队列进行缓冲,并利用 requestAnimationFrame 配合固定的步长进行‘匀速消费’。这种做法能有效屏蔽网络不稳带来的突跳感,让文字像打字机一样优雅匀称地呈现。如果你给我一个具体网站/页面类型(静态还是 JS 渲染、是否需要登录、要哪些字段),我可以按你的场景把“翻页 + 去重 + 保存格式 + 稳定性策略”写成一份更贴近实战的方案。如果你给我一个具体网站/页面类型(静态还是 JS 渲染、是否需要登录、要哪些字段),我可以按你的场景把“翻页 + 去重 + 保存格式 + 稳定性策略”写成一份更贴近实战的方案。";
for (let char of text) {
textQueue.push(char); // 生产者:存入队列
await new Promise(r => setTimeout(r, Math.random() * 100)); // 模拟不稳定的网络延迟
}
}
// 平滑渲染引擎
function startSmoothRender() {
if (isRendering) return;
isRendering = true;
let renderStartAt = null;
let latestSpan = null;
function renderStep() {
if (textQueue.length > 0) {
// 消费者:每次取出第一个字符
const char = textQueue.shift();
const span = document.createElement('span');
span.textContent = char;
outputEl.appendChild(span);
if (latestSpan) {
latestSpan.classList.remove('latest');
}
span.classList.add('latest');
latestSpan = span;
if (renderStartAt === null) {
renderStartAt = performance.now();
}
// 这里的延时决定了“缓慢自然”的语感速度
const elapsedMs = performance.now() - renderStartAt;
const normalDelayMs = 150;
const fastDelayMs = 10;
const delayMs = elapsedMs >= 4000 && elapsedMs < 10000 ? fastDelayMs : normalDelayMs;
setTimeout(() => requestAnimationFrame(renderStep), delayMs);
} else {
requestAnimationFrame(renderStep);
}
}
requestAnimationFrame(renderStep);
}
// 初始化
mockApiStream();
startSmoothRender();
</script>
</body>
</html>