Scroll驱动的文字漩涡,只用CSS就能实现?看看sibling-index()怎么把性能短板给补上

2,057字
9–13 分钟
in

摘要

目录

早先那些滚动驱动的炫酷动画,动不动就靠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模拟一遍。实在不行,录个屏丢张动图上去,至少让访客知道原版长啥样。毕竟移动端用户看到“这页面跑不动”的提示,体验分直接掉到地板。