滚动叙事
高端产品页的核心手法:滚动条就是时间轴。向下滚动,笔记本缓缓开盖、 参数逐条亮起;向上滚动,整个过程原样回溯——动画进度与滚动位置严格同步, 不抢滚动条的 control,也没有「播动画时页面停住」的割裂感。
Evoke UI 把它拆成两个形态,与 v-reveal 的触发型入场互补:
- 触发型(v-reveal):元素进视口 → 播一次。适合入场点缀;
- 进度驱动型(本页):元素在视口里的穿越程度 = 动画进度,双向可逆。适合产品叙事。
本站就在实战使用:首页的「一个官网的诞生」滚动带、企业官网案例 的「数据链路」折线(stroke-dashoffset 随滚动画出折线),都是 EvScrollScene 做的, 源码就在页面里。
EvScrollScene 场景
EvScrollScene 外层按 duration 拉出滚动长度,内层 sticky 钉在视口里; 进度经作用域插槽 { progress } 与 CSS 变量 --ev-scene-progress(0..1)双通道暴露。 纯 CSS 消费用 calc()——笔记本开合就是一行 transform:
继续向下滚动 ↓
开盖,即赴工作状态
12 亿色原彩屏 · 自适应 120Hz
厚度 11.5mm · 全天续航
<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,再绑回样式(本站首页「一个官网的诞生」与企业官网案例的 「数据链路」就是这个做法,源码即参考):
// 区段 [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:
一屏一个重点
proximity 模式只在靠近吸附位时轻推一下,中间浏览不受打扰。
原生滚动,无劫持
滚轮、触摸板惯性、键盘与读屏全部保持浏览器默认行为。
双向可逆
向上滚即回到上一栏,与触发型入场动画的分工见上。
<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 让位:
<EvSection eyebrow="Features" title="即时洞察" snap>
…
</EvSection>自定义场景
场景结构不满足时(视差层、多元素错拍),用 useScrollProgress 进度原语自行组装, 返回 { progress, reduced },几何语义与组件一致:
<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>