网页动效制作指南
概述
网页动效的失败通常有两种形式:它不运行,或者运行得很差。两者都有具体且可查找的原因。猜测缓动曲线是最后才做的事,而不是第一步。
核心原则:在调整动画之前,先证明它正在你期望的元素上运行。
有关固定和擦除场景的设计,请参见 choreographing-scroll-stories。此技能关注的是机制:让动效运行并运行良好。
动画完全不运行时
按此顺序检查。前三种情况几乎涵盖了所有问题。
| 症状 | 原因 | 检查方法 |
|---|---|---|
| 什么都不动,元素可见 | 选择器匹配零个元素 | 在控制台运行 document.querySelectorAll(sel).length |
| 什么都不动,元素不可见 | 揭示元素设置了 opacity: 0 但触发从未发生 | 查看元素的计算 opacity 值 |
| 淡入淡出从未发生 | 两个图层都停留在 opacity: 1;只有“开”状态规则存在 | 必须有一个定义引入层为 0 的初始状态规则 |
| 加载时立即触发 | 触发器在布局稳定前就执行 | 在 load 事件后运行,而非 DOMContentLoaded,当图片会影响页面大小时尤需注意 |
| 触发一次后不再触发 | once: true 且元素重新进入视口 | 这是否符合预期? |
| 在隐藏标签页或无头面板中不运行 | 当 document.hidden 时时钟暂停 | 强制运行:gsap.ticker.lagSmoothing(0); gsap.ticker.tick() |
| 触发器偏移几百像素 | 图片在触发器测量之后加载 | 在加载和调整大小时调用 ScrollTrigger.refresh()(200 毫秒防抖) |
淡入淡出的情况值得详细说明,因为它看起来在代码审查中是正确的:
/* 仅此一项无效。 */
.tile.is-swapped .layer.is-alt { opacity: 1 }
/* 初始状态才是使其成为动画的关键。 */
.layer.is-alt { opacity: 0 }
.tile.is-swapped .layer.is-alt { opacity: 1 }
首屏入场与环境微动效
- 标题应快速可见。 将入场动画裁剪至标题在大约 1.1 秒内显示。一个 3.8 秒的入场动画就是加载屏幕。
- 图片通过淡出某个单元格并淡入另一个单元格来切换。它们从不滑动。 每张照片是一个独立元素,绝不会在屏幕上出现两次,也绝不会在副本淡入的同时淡出。
- 环境变化应缓慢且不规则。 每 1.8 到 3.4 秒更换一个图块,这会让网格看起来是活的,而单个图块变化缓慢。不要为了加快每个图块的变化速度而缩短间隔。
- 按轮次随机排列。 每个图块在重复之前都会变化一次,且每次选择都来自最近三次中较远的一半。
- 暂停无人可见的内容。 环境循环会在屏幕外和隐藏标签页中停止,并在它们原来的位置恢复。
- 入场动画不会在每次返回时重播。 入场动画稳定后,除了环境层之外,其他都不再动。
滚动揭示
- 仅对加载时实际位于首屏下方的元素设置
opacity: 0。全部隐藏意味着任何首屏元素都不可见,直到可能永远不会发生的滚动发生。 - 在
top 88%处揭示,而非完全交叉。等待元素完全进入视口意味着当读者已经看到它时,它才开始动画。 - 兄弟元素之间交错延迟,组件内部不延迟。卡片之间 60-90 毫秒,卡片内部为 0。
- 使用每组的单个观察者进行批处理,而不是每个元素对应一个观察者。
视频
自动播放的背景视频有一个普遍问题:视频在屏幕外播放,观察者会暂停它,而当读者到达时,循环已经进行了 15 秒,开场节拍已经丢失。
完全移除 autoplay 属性。
在初始化时调用 pause() 和 load()。
仅在首次交叉时:currentTime = 0,然后 play()。
在低于 992px 宽度或低于 1.5 设备像素比时,提供 1x 文件,并在视频解码前提供海报帧。主页的重度视频也是带宽消耗大户:要审计它。
卡顿
静止照片上的缓慢漂移是最常见的原因。每帧移动小于约 1 像素的 Ken Burns 效果会在每帧重新采样图像,看起来是闪烁而非运动。
- 要么移动足够多成为运动,要么根本不移动。
- 对于渲染视频中的静态帧,直接逐字节复制编码帧,而不是重新编码完全相同的帧。否则会产生漂移。
- 检查循环接缝。如果第 0 帧和最后一帧不同,循环会在每个周期明显跳跃。测量它们之间的平均绝对差值。
- 不要双重平滑。一个平滑滚动库加上一个较大的
scrub值会导致页面滞后于滚轮。使用平滑滚动库时,请使用scrub: 0.2。
粘性定位
CSS 网格内的 position: sticky 是相对于网格区域解析,而不是整个容器。一个只与其内容同高的粘性项目没有可移动空间,会立即释放。
.grid { align-items: stretch } /* 区域现在为全高 */
.col { position: static; height: 100% }
.inner { position: sticky; top: 10rem }
回退方案
每个动画都要有三个回退方案,写在动效规范中并经过测试:
- 缩减动效。
prefers-reduced-motion: reduce和一个?static查询会渲染堆叠的、完成的状态:没有任何固定、没有任何隐藏、每个场景都处于其结束状态。也要覆盖你添加的每个 CSStransition。列出动画选择器但忘记对容器设置过渡,会导致颜色仍然在滑动。 - 无脚本。 所有内容保持可见。如果脚本从未启动,大约 3.5 秒后会添加一个类,CSS 会揭示其下的所有内容。
- 手机端。 固定场景会堆叠。说明这一点并进行测试。
验证
不要仅凭代码报告动画正常工作。在页面中确认:
- 每个动画组的选择器计数非零
- 在任何滚动之前,首屏元素的计算
opacity为 1 - 视频
currentTime在首次可见时低于 0.5 秒 - 缩减动效模拟使页面对读者完全可读且无动效
- 无脚本状态下所有内容保持可见
- 不使用
width、height、top或left动画;使用transform和opacity - 环境循环在屏幕外时不进行任何样式更改
警示信号
- 在确认选择器匹配之前调整时长或缓动
- 没有对应初始状态规则的
.is-active规则 transition: all- 因为感觉“段落空洞”而添加的动画
- 图片滑动
- 同一张图片在屏幕上出现两次