跳到正文
笔记库
动效4 分钟成长中

滚动背景序列:让下一层把上一层「接」过去

三层背景共享一个进度,相邻两层在交接区间互补。第一版用三角波算透明度,画面越滚越黑——因为后画的图层会压住先画的。

有一种滚动效果是这样的:页面在滚,背景却像一段剪好的片子,一张画面缓缓被下一张接走,中间看不到切换点。

它不是视差——视差让内容分层移动;这里内容正常滚,动的只有背景。也不是滚动劫持——滚轮没有被接管,只是被读了一遍。

结构:一条轨道,一个舞台

┌─ 轨道  height = 层数 × screensPerLayer × 100svh ─┐
│                                                  │
│   ┌─ 舞台  position: sticky; top: 0 ──────────┐  │
│   │  层 0  img                                │  │
│   │  层 1  img                                │  │
│   │  层 2  img                                │  │
│   │  scrim / 文案                             │  │
│   └───────────────────────────────────────────┘  │
│                    ↑ 滚过去,舞台不动,只换层     │
└──────────────────────────────────────────────────┘

轨道高度决定这个效果能滚多久:三层各占一屏,总高 300svh,实际滚动距离是两屏。

进度怎么分给三层

const u = progress * (count - 1)          // 3 层 → u ∈ [0, 2]
const edge = Math.min(0.45, hold / 2)
 
for (let i = 0; i < count; i += 1) {
  const opacity = i === 0 ? 1 : smoothstep(i - 1 + edge, i - edge, u)
  // 层 i 在 u = i 时刚好满不透明
}

hold 是"到位之后稳一下"的比例:0 表示全程都在过渡,0.24 表示每层满不透明地停留一小段。smoothstep 让交接的两端都慢、中间快,所以两层的透明度是互补的——A 降多少,B 就升多少。

第一版是三角波,画面越滚越黑

最直觉的写法是给每层一个以自己为中心的三角波:离自己越近越不透明。三层各算各的,看起来完全合理。

截图直接打脸:滚动过程中整个画面在变暗,交接点最黑。

原因是 DOM 的叠放顺序。三个 <img> 是同一位置的兄弟节点,后画的压住先画的。当 A 的 opacity 是 0.3、B 是 0.7 时,屏幕上的合成不是 0.7B + 0.3A,而是:

0.7·B + 0.3·(0.3·A + 0.7·背景) = 0.7B + 0.09A + 0.21·背景

两层都变暗了,还有 21% 的背景透出来。所以正确模型只有一种:上层淡入,下层始终满不透明。

A 不透明 → B 从 0 升到 1 盖住 A → A 已经不需要参与混合

这样合成结果才等于 a·B + (1−a)·A,亮度守恒。这条现在写在组件注释里,防止以后被"优化"回三角波。

斜切只在过渡区间存在

split 模式给每层加一道对角裁切:

if (opacity > 0.002 && opacity < 0.998) {
  const percent = ((1 - opacity) * split).toFixed(2)
  el.style.clipPath = i % 2 === 0
    ? `polygon(0 ${percent}%, 100% 0, 100% 100%, 0 100%)`
    : `polygon(0 0, 100% ${percent}%, 100% 100%, 0 100%)`
} else {
  el.style.clipPath = 'none'
}

奇偶层反方向切,两张图在过渡中各缺一角,合起来才是"拼接"而不是"叠化"。

clip-path 会触发重绘,所以它只在 0 < opacity < 1 时写,静止帧一律 none。重绘成本被限制在交接的那一小段,其余时间只有合成。

配合 parallax(相邻层上下错位 56px)和轻微的 scale,画面看起来是在重组,不是整体淡入淡出。

三个会出错的地方

  1. 轨道高度用 JS 设 → 首帧不知道多高,先短后长推一下版面(CLS)。现在是 CSS calc(var(--sb-layers) * var(--sb-screens) * 100svh)。
  2. 首帧可见性写内联 style → 每帧都会被覆盖,父组件重渲染时还会把进度冲掉。现在由 CSS :first-child 保证第一层初始可见。
  3. 滚轮事件直接映射进度 → 滚轮是离散的,画面会一顿一顿。smoothing 做指数衰减,松手后还在往目标值收敛——衔接感有一半来自这里。

选图标准是「低频」,不是好看

第一版第三张用的是霓虹天桥:色彩很足,但画面里全是高频细节。两张高细节照片五五开叠加时,观感像信号串扰——不是不好看,是看不清。

换图之后的标准变成两条:

  • 色相跨度够大:暖橙 → 青绿 → 蓝天,交接时能看出是"两个世界";
  • 每张都有大片低频区域:天空、水面、墙面,给另一张留出可读的位置。

另外在交接中点额外压暗 scrimDip(默认 0.18),既让高细节叠加不刺眼,也保证压在图上的白字始终有对比度。

降级与位置

prefers-reduced-motion 下停在第一帧,滚动距离和版面高度都不变。不是把速度调成 0.01,也不是把整段删掉——版面跳动比动效本身更让人不适。

这个组件原本只放在 /craft 页尾。首页首屏已经有 DitherVeil,再挂一个常驻动效就违反了效果预算的第一条:一屏最多一个。

2026-11-05 从 /craft 撤下了:它要占三屏,放在一个「逛一圈看看有什么」的展品页里,读完设计系统还要再滚三段背景,节奏是断的。组件和素材都还在 src/components/effects/ 与 src/assets/ 里,要重新挂上去只是加一段 JSX。

接着读

留言

还没有人说话

读到这里,说两句。不用登录,昵称随便填。

不用登录
0 / 1200

文字原样显示,不支持 Markdown。发完可以删。