SOP:逆向 + 复刻「滚动叙事页」(豆包 about 那一类)
场景:看到一个"整页像视频一样播放"的官网(豆包 about、Apple 产品页、各家 AI 大厂首页),要搞清它怎么做的,并自己做一个同机制的页面。 配套文件:同目录
scroll-stage.html(可直接双击打开的成品,单文件、零依赖)。 成文日期:2026-08-13。
#一、这类页面的真实结构(结论先行)
它不是一个视频。 它是:
一个 position:sticky 的 100vh 舞台(整页唯一可见容器)
+ 后面串着几个什么都不渲染、只负责撑高度的空 section
+ 一个 rAF 循环,把 scrollY 换算成每一幕的 progress(0→1)
+ progress 写成 CSS 变量,交给 CSS 去做位移/透明度/缩放滚动条走的是时间轴,画面本身一直钉在原地。视频只是贴在舞台上的素材片段,不是页面本体。
#二、逆向一个站的固定动作(20 分钟以内出结论)
先 curl 看是不是空壳
curl -sL "https://目标/页面" -o page.html && grep -cE "mp4|canvas|webgl" page.html现在的大厂官网基本都是 CSR,HTML 是空的 —— 拿不到东西不代表没东西,直接进下一步。
用无头浏览器渲染后再问 DOM(Playwright MCP / claude-in-chrome 都行)
[...document.querySelectorAll('video')].map(v => ({ src: v.currentSrc, w: v.videoWidth, loop: v.loop, autoplay: v.autoplay, cls: v.className, rect: v.getBoundingClientRect().toJSON() }))一次就能知道:几个视频、多大、是否 loop/autoplay、在页面什么位置。
查有没有用现成动画库
['gsap','ScrollTrigger','THREE','lottie','Lenis','ScrollMagic'].filter(k => window[k])空数组 = 自研引擎,那就得去扒 chunk。
找 sticky 舞台(判定是不是本 SOP 这套结构的关键)
[...document.querySelectorAll('*')] .filter(e => ['sticky','fixed'].includes(getComputedStyle(e).position)) .map(e => e.className + ' / ' + getComputedStyle(e).position)看到
sticky top:0且高度≈视口,同时document.body.scrollHeight上万 —— 石锤。扒 chunk 数关键 API(判断视频是"擦洗"还是"播放")
for k in currentTime IntersectionObserver requestAnimationFrame scrollY "@keyframes" animation-timeline; do echo -n "$k: "; grep -oE "$k" chunk.js | wc -l donecurrentTime被大量赋值 → 视频按滚动逐帧擦洗(scrub),做法更贵、更卡。- 只有
play()/pause()+ IntersectionObserver → 只是进视口播、出视口停。 animation-timeline有值 → 用了 CSS 原生滚动驱动动画。
量素材成本
curl -sI "视频URL" | grep -i content-length常常发现:技术只值几十 KB,35 MB 全是美术。这条结论对"要不要抄"最有用。
#豆包 about 的实测结果(2026-08 版)
- React SPA(rspack + Tailwind),HTML 空壳,逻辑在
about_page.*.js。 - 6 段 mp4,共约 35 MB:
opening(12.7M, autoplay) /part1(4.2M, loop) / writing / image / search / music 各 3.5–6M。 body.scrollHeight ≈ 14000px,舞台position:sticky; top:0。- 没有 GSAP / three.js / canvas / WebGL。自研 stage 引擎:rAF 读 scrollY → 每个 stage 的 progress + enter/leave 状态 → 发布订阅分发。
- 视频不擦洗:进视口 play,出视口 pause 并
currentTime = duration冻在最后一帧。 - 另有 Lottie 按 progress 走帧(
goToAndStop/seek),Safari 单独留了降级开关。
#三、复刻的最小骨架(照抄即可)
<main id="root">
<div class="stage" id="stage"> …所有画面层都在这里… </div>
<section class="beat" style="--h:180vh"></section> <!-- 只撑高度 -->
<section class="beat" style="--h:280vh"></section>
</main>.stage{ position:sticky; top:0; height:100vh; overflow:hidden }
.beat { height:var(--h) }
.layer{ position:absolute; inset:0; opacity:var(--a,0) } /* JS 只写 --a */// 唯一一个 scroll 监听,只标脏,不干活
addEventListener('scroll', () => { dirty = true }, { passive:true });
function frame(now){
if (dirty) { apply(); dirty = false } // 每帧最多算一次
requestAnimationFrame(frame);
}
function apply(){
const y = scrollY;
for (const b of beats) b.p = clamp((y - b.top) / b.height); // 0 → 1
layer.style.setProperty('--a', p.toFixed(3)); // 只写变量
}
// 幕内分镜:把 p 在 [a,b] 区间重映射成 0→1
const seg = (p,a,b) => clamp((p-a)/(b-a));分工铁律:JS 只负责算 progress 并写 CSS 变量,具体怎么动全部写在 CSS 里。好处是动画跑在合成层,不回流;调动画不用碰 JS。
#四、踩过的坑(都是实测炸过的)
| # | 坑 | 症状 | 解法 |
|---|---|---|---|
| 1 | body{overflow-x:hidden} |
sticky 舞台直接失效,滚一屏后画面就滚走了 | 改用 overflow-x:clip。hidden 会让 body 变成滚动容器,sticky 就不再相对视口 |
| 2 | sticky 舞台自己在文档流里占 100vh | 开头一整屏"死区":滚了半天所有 progress 还是 0 | 算 beat 起点时减掉舞台高度:b.top = rect.top + scrollY - stageH |
| 3 | 后台标签页 rAF 停摆 | 无头浏览器截图全黑 / HUD 数字不动,误以为代码坏了 | 先查 document.visibilityState,是 hidden 就不是 bug |
| 4 | 无头截图与真实布局对不上 | 截图里元素位置乱飞 | 别靠截图判断,直接读 getBoundingClientRect() 对数字 |
| 5 | canvas 不按 DPR 备份 | Retina 上糊成一片 | cv.width = rect.width * dpr + ctx.setTransform(dpr,0,0,dpr,0,0),且只在 resize 时算一次 |
| 6 | canvas 尺寸每帧读 rect | 每帧强制回流,掉帧 | 尺寸缓存进 WeakMap,绘制时只读缓存 |
| 7 | 缺 <meta charset="utf-8"> |
本地 python server 打开全是乱码 | 文件头补上,别指望服务器给 |
| 8 | 所有 canvas 一直画 | 风扇狂转 | 复刻原版逻辑:不在视口内就不画,最后一帧自然冻住 |
| 9 | 忘了 prefers-reduced-motion |
晕动症用户直接劝退 | 加媒体查询关掉位移与颗粒层 |
#五、要不要抄的判断
- 机制便宜:
scroll-stage.html整页 27 KB,零依赖,几十行核心 JS。 - 美术昂贵:原版 35 MB 视频素材才是真成本。
- 如果只是想要"滚动叙事感",用 canvas 程序化画面 / SVG / CSS 就够,别一上来就录视频。
- 真要视频:优先 play/pause,不要 scrub。逐帧擦洗要预加载整段、seek 抖动大、移动端经常直接崩。
#六、下次怎么用这份 SOP
新开会话,把这个文件夹整个拖进去,说:
按这份 SOP,逆向 <某个网址> 并复刻一个同机制的静态页。
Claude 会照第二节的 5 步出逆向结论,照第三节搭骨架,并绕开第四节那 9 个坑。
来源:沉淀/滚动叙事页复刻/SOP-滚动叙事页复刻.md(整理于 2026-08-18)