CHRONICLE:把滚动叙事做成一条可回退的时间线

从 8 个场景、12 个视口高度到 GSAP ScrollTrigger 与减少动态效果,记录一个滚动驱动叙事的实现取舍。

本文目录

CHRONICLE 是一个滚动驱动的交互叙事页面。它没有把“滚动”当成页面进度条,而是把 8 个场景组织成一条有边界、可回退、可重播的时间线。

这篇文章不讨论它最终好不好看,而是记录几个工程问题:场景之间如何衔接、动画和滚动如何解耦、以及为什么“能回退”比“能播放”难得多。

场景不是页面,而是状态

项目把体验拆成 Prelude、Welcome、Composer、Currently、Profile、Space、Destinations、Goodbye 等场景。每个场景有自己的背景、文字、角色或 WebGL 画布,滚动只负责触发边界。

一个关键选择是:滚动距离和动画时长分离。场景用 svh 定义滚动区间,动画时长则写在 sceneRegistry.ts 里。Composer 甚至预留了 200svh,其中 50svh 用于编辑,角色在这段时间里隐藏。

这带来一个直接结果:用户继续滚动,不会把动画“拖快”或“拖慢”;动画按自己的时间线播放,滚动只决定何时开始。

部分作用
src/app/固定视口舞台、场景、滚动区间、可访问性与输入门禁
src/scenes/场景内容与局部动画钩子,不直接监听滚动
src/transitions/角色、文字、传送门、内容交接与重启编排
src/features/Composer、文字冲击、星空/黑洞等独立能力
src/stores/当前场景、减少动态效果、Composer 就绪状态

可回退,才是真正的时间线

很多滚动页面只实现了“向下播放”。CHRONICLE 为每个边界准备了前进时间线,以及可选的返回时间线。Prelude 返回是 3.3 秒,Welcome 是 0.35 秒的简化边界,Profile 返回是 1.8 秒,传送门进出各 1.25 秒。

当边界触发后,播放会独立于滚动输入完成;主内容在过渡期间设为 inert,滚轮、触摸和滚动键被暂时阻止。到达目标场景后,文档才移动到该场景的阅读/编辑区间,输入恢复。

这解决了一个常见问题:用户快速滚动时,动画播放到一半又被新的滚动事件打断。这里用“过渡期间锁输入”换取了确定性。

Composer 与角色编排

Composer 是一个可编辑的节点图。它使用 React Flow,但禁用了滚轮缩放、背景平移和自动适配;只有节点和边可编辑,滚轮继续驱动页面叙事。

角色不是视频,而是注册表驱动的姿态层。DeepSeek、Grok、Gemini、Claude、GPT 等角色由 src/components/CharacterCast.tsx 和角色注册表管理。Scene 03 到 02 的拉拽动画使用 DS_PULL / GROK_PULL 姿态,并重复“用力”的过程。

这类页面最容易失控的地方是“每个场景都写一套动画”。项目把场景内容、过渡编排和共享能力拆开,至少让新增场景时不用复制整段滚动逻辑。

可访问性与降级

  • 非活动场景设为 inert,并从辅助技术中隐藏。
  • 阅读/编辑区间到达后,键盘焦点回到可用内容。
  • prefers-reduced-motion 下,边界简化、时长缩短到 0.35s。
  • 没有 WebGL 时保留 CSS 艺术与原生效果;WebGL 1 也能工作。
  • 首次访问的 Loading 有 6 秒超时兜底,不会因为资源失败卡死。

这些不是“加分项”。当一个页面把滚动、动画和输入都锁在一起时,降级路径就是可用性的一部分。

部署边界

项目用 Vite 构建,base: './',同一份产物既能部署在域名根路径,也能部署在 GitHub Pages 的子路径。dist/ 由 GitHub Actions 发布,不依赖仓库名硬编码。

这类叙事页面通常不需要服务端;真正的复杂度在客户端的时间线、资源预热和状态回收。CHRONICLE 把场景保持在固定视口中,非活动场景暂停效果,也是在控制长期运行的成本。

小结

CHRONICLE 值得看的地方不是“用了多少库”,而是它把滚动叙事拆成了可管理的几层:场景、边界、时间线、输入门禁和降级。只要其中一层没有边界,整个页面就会变成一次不可回退的演示。

如果你要做类似的滚动体验,先回答三个问题:动画能否独立完成?返回时能否恢复?减少动态效果时还剩下什么?这三个问题比选哪个动画库更重要。

讨论

评论由 GitHub Discussions 提供,点击后加载。未加载前不会产生第三方请求。