摘要
早先那些滚动驱动的炫酷动画,动不动就靠JS硬扛,结果到了移动端直接翻车,卡顿掉帧不说,有的特效甚至得忍痛割爱。最近发现一个用纯CSS实现的文字漩涡效果,核心就靠sibling-index()和scroll-timeline这俩新特性。每个字符都是独立个体,滚动时自动螺旋排列,从外向内渐隐,整个过程丝滑得不像话。关键是,这套玩法完全绕开了JS动画那种主线程拥堵的坑,以后那些花里胡哨的滚动交互,终于可以甩掉性能包袱了。
scroll-timeline是什么来头
scroll-timeline这玩意儿,相当于给CSS动画装了个滚动进度条。以前做滚动驱动的效果,得在JS里监听滚动事件,然后手动算百分比,再塞给元素改样式。现在好了,直接在CSS里声明animation-timeline: scroll(),动画就跟滚动条绑定上了,滚动多少,动画就走到哪,全程交给浏览器底层去优化。
字符拆分这步绕不过去
想让文字每个字都自己转圈圈,第一步肯定得把字符串拆成一个个独立的元素。这事儿要是纯手写HTML,碰上长文案直接劝退。所以用了一丢丢脚本,就干了三件事:
const el = document.querySelector(".vortex");
// 把空格换成特殊占位符,不然空格会被吞掉
el.innerHTML = el.innerHTML.replaceAll(/s/g, '⠀');
// 用GSAP的SplitText插件,把每个字符装进独立div
new SplitText(".title", { type: "chars", charsClass: "char" });这里有个坑得填:直接拆出来的空格会被忽略掉,导致排版稀碎。所以先偷偷把普通空格换成'⠀'这个特殊空格字符,SplitText见到它就会乖乖保留一个独立盒子。另外SplitText自带aria-label属性,屏幕阅读器依然能正常读出原文,这点相当贴心。
兄弟元素计数是关键配方
sibling-count()和sibling-index()这俩函数,在CSS里就像开了天眼,能知道当前元素在同级里排第几个,总共多少个。有了这俩数,就能给每个字符算出一套独一无二的样式值:
.char {
--radius: calc(10vh - (7vh/sibling-count() * sibling-index()));
--rotation: calc((360deg * 3/sibling-count()) * sibling-index());
transform: rotate(var(--rotation))
translateY(calc(-2.9 * var(--radius)))
scale(calc(.4 - (.25/(sibling-count()) * sibling-index())));
}这里的逻辑有点像发牌,第一个字符半径最大、尺寸最大、旋转角度最小,后面的逐个缩水。用数学公式表达就是起始值 - (递减量/总人数 * 当前序号),递减量设得越大,越靠后的字符缩得越狠,螺旋的收紧感就越强。
淡入效果也得按顺序来
光有螺旋还不够,得让字符随着滚动一个一个亮起来。这里用animation-range-start做了个滚动进度偏移:
.char {
animation-name: fade-in;
animation-range-start: calc(90%/var(sibling-count()) * var(--sibling-index()));
animation-fill-mode: forwards;
animation-timeline: scroll();
}animation-range-start相当于告诉浏览器“这个字符的动画从滚动到百分之多少才开始”。第一个字符从0%就开动,第二个得等滚动到90%/总数*2的位置才触发,第三个更靠后。滚动条一拖,字符就像排好队似的,一个接一个淡入。
父级旋转让整个漩涡动起来
所有字符都安排到位了,还差最后一步:让整个“漩涡”转起来。直接对.vortex这个容器下手:
.vortex {
position: fixed;
left: 50%;
height: 100vh;
animation-name: vortex;
animation-duration: 20s;
animation-fill-mode: forwards;
animation-timeline: scroll();
}父容器一旋转,里面每个字符的相对位置都跟着变,看起来就像整个人被吸进漩涡中心。这里animation-duration设了20秒只是个基准值,实际动画长度完全由滚动距离决定。
回退方案得留一手
sibling-index()这玩意儿,目前Firefox还没跟上节奏。所以在CodePen里或者实际项目里,总得给个Plan B。最简单的做法是检测浏览器是否支持,不支持的话就用JS模拟一遍。实在不行,录个屏丢张动图上去,至少让访客知道原版长啥样。毕竟移动端用户看到“这页面跑不动”的提示,体验分直接掉到地板。
