Spaces:
Running
Running
| <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> | |