---
url: /guide/scroll.md
---
# 滚动叙事

高端产品页的核心手法：**滚动条就是时间轴**。向下滚动，笔记本缓缓开盖、
参数逐条亮起；向上滚动，整个过程原样回溯——动画进度与滚动位置严格同步，
不抢滚动条的 control，也没有「播动画时页面停住」的割裂感。

Evoke UI 把它拆成两个形态，与 [v-reveal](/guide/motion) 的触发型入场互补：

* **触发型**（v-reveal）：元素进视口 → 播一次。适合入场点缀；
* **进度驱动型**（本页）：元素在视口里的穿越程度 = 动画进度，双向可逆。适合产品叙事。

本站就在实战使用：首页的「[一个官网的诞生](/)」滚动带、[企业官网案例](/cases/corporate)
的「数据链路」折线（`stroke-dashoffset` 随滚动画出折线），都是 `EvScrollScene` 做的，
源码就在页面里。

## EvScrollScene 场景

`EvScrollScene` 外层按 `duration` 拉出滚动长度，内层 sticky 钉在视口里；
进度经作用域插槽 `{ progress }` 与 CSS 变量 `--ev-scene-progress`（0..1）双通道暴露。
纯 CSS 消费用 `calc()`——笔记本开合就是一行 transform：

```vue
<EvScrollScene :duration="260">
  <template #default>
    <div class="sl-lid">
      <!-- 闭合 -92° → 直立 0°，滚动条就是时间轴 -->
      <div class="sl-screen">…</div>
    </div>
  </template>
</EvScrollScene>

<style>
.sl-lid {
  transform-origin: 50% 100%;
  transform: rotateX(calc(-92deg + var(--ev-scene-progress) * 92deg));
}
.sl-screen {
  opacity: calc(var(--ev-scene-progress) * 1.4 - 0.25);
}
</style>
```

要点：

* 屏幕亮起用 `opacity: calc(var(--ev-scene-progress) * 1.4 - 0.25)`——calc 超界自动收敛到 0..1；
* 只动 `transform` 与 `opacity`（合成器层），滚动全程不掉帧；
* 用户系统开了「减少动态效果」时，组件自动把进度钉在终态 1，页面静态呈现开盖完成的样子，
  不需要宿主写任何判断；
* JS 消费（canvas 图片序列刷帧、`video.currentTime = progress × 时长`）走插槽值，
  与 CSS 变量同源同值。

进度本身是线性的。想要「缓动」的呼吸感，在消费端对进度做一次曲线映射即可——把区段
归一后过 easeOutCubic，再绑回样式（本站首页「一个官网的诞生」与企业官网案例的
「数据链路」就是这个做法，源码即参考）：

```js
// 区段 [a, b] 归一到 0..1，再过 easeOutCubic
const seg = (p, a, b) => Math.min(Math.max((p - a) / (b - a), 0), 1)
const easeOut = (t) => 1 - Math.pow(1 - t, 3)
// opacity: easeOut(seg(progress, 0.34, 0.62))
```

## 分步功能区

「滚一下进入下一个功能模块」用原生 **scroll-snap** 实现：吸附是浏览器原生行为，
温和可打断，滚轮、触摸板、键盘、读屏全都自然工作——不要做滚轮劫持式的整页翻页。

容器加 `ev-snap-y`（proximity 温和吸附）、每个分栏加 `ev-snap-start`：

```html
<div class="snap-demo ev-snap-y">
  <section class="snap-pane ev-snap-start">…</section>
  <section class="snap-pane ev-snap-start">…</section>
  <section class="snap-pane ev-snap-start">…</section>
</div>
```

整页官网的分步（每个 EvSection 吸到视口顶）不用手写：给区块加 `snap` prop 即可，
任一区块声明后页面滚动容器自动启用吸附；悬浮顶栏用 CSS `scroll-margin-top` 让位：

```vue
<EvSection eyebrow="Features" title="即时洞察" snap>
  …
</EvSection>
```

## 自定义场景

场景结构不满足时（视差层、多元素错拍），用 `useScrollProgress` 进度原语自行组装，
返回 `{ progress, reduced }`，几何语义与组件一致：

```vue
<script setup>
import { ref } from 'vue'
import { useScrollProgress } from '@wil-works/evoke-ui'

const stage = ref(null)
const { progress } = useScrollProgress(stage)
</script>

<template>
  <div ref="stage">
    <div :style="{ transform: `translateY(${progress * -40}px)` }">视差层</div>
  </div>
</template>
```
