Joye Personal Blog

Back

动效 typinganimationperformance

逐字打字别硬跳:按帧点亮,每个字淡入

角色说话时一个字一个字出现的文字,别靠定时器让字从无到有地跳出来;按帧决定亮到哪,再给每个字一小段淡入,节奏马上就顺了。

来源 ↗
views

本站首页的 Jojo 会带你逛一圈:它飞到每个板块上方,在气泡里一句一句地介绍。做完以后我总觉得气泡里的字出得有点卡,一顿一顿的。速度本身没问题,问题出在字是怎么出现的。下面左右两边是同一段话、同样的速度,只是出字的方式不同。

同一段话、同样的速度,盯着正在出现的那几个字看。
✕ 定时器 + 整句替换

定时器每隔一段时间往句子里加一个字,新字从无到有只用一帧。定时器和屏幕刷新对不上,实际间隔在相邻的两个整帧之间来回跳(每字 42ms 时是 33ms 和 50ms),看着一顿一顿的。

✓ 按帧点亮 + 逐字淡入(本站)

嗨,我是 Jojo,欢迎来到 Joye 的博客。这段话是一个字一个字打出来的:先看左边,每个字都是一下子跳出来的;再看右边,每个字都是慢慢浮现出来的。把上面的速度往快调一点,或者往慢调一点,两边的差别会更明显。

每个字是独立的一小块,按经过的时间在每一帧算出该亮到哪,再用大约四个字的时间淡入(最少 160ms)。前后几个字的淡入叠在一起,节奏的不均匀被抹平了。

01让每个字有一小段淡入,时长是打字间隔的好几倍。前后几个字的淡入叠在一起,读起来是一条往前流动的边,而不是一下一下地跳。
02“现在该亮到第几个字”按经过的时间算,在每一帧开始时更新。定时器和屏幕刷新不同步,间隔会忽长忽短,页面一忙还会整体晚到。
03出一个字只改这个字自己的样式,别让整个组件跟着重新渲染。角色、按钮、位置计算都不用动,主线程才有空把每一帧画完。
盯着正在出现的那几个字看。拖动“速度”调快或调慢,两边的差别会更明显。

为什么这样更好#

  • 硬跳的字没有过渡。每个字在一帧之内从无到有,眼睛看到的是一连串小闪烁。字越多、越快,越像屏幕在抖。
  • 定时器的节拍是不均匀的。屏幕大约每 16.7ms 刷新一次,42ms 的定时器落不到整数帧上,实际间隔会在 33ms 和 50ms 之间来回跳;页面一忙,定时器还会整体晚到。人对这种忽快忽慢的节奏非常敏感。
  • 淡入把节拍抹平了。每个字的淡入时长大约是打字间隔的四倍(本站是 160ms),所以任何时刻都有三四个字处在半透明的状态。前面那条“正在变清楚”的边是连续往前走的,单个字早一帧晚一帧就看不出来了。
  • 按帧算位置,不按次数加一。每一帧开始时,按“已经过去多久”算出该亮到第几个字。画面更新和屏幕刷新对齐;就算某一帧被耽误了,下一帧也会直接追到正确的位置,而不是一个一个慢慢补。
  • 出一个字,只动这一个字。原来的写法每出一个字就让整个导览组件重新渲染一次,连 Jojo 本身都跟着重算。现在点亮一个字只是改它自己的样式,主线程才有空把 Jojo 的动作和气泡的移动都画完。

什么时候不该用#

  • 长文本、需要反复阅读的内容(文档、聊天记录、报错信息)不要逐字出现,直接整段显示。逐字只适合角色说一两句话这种场景。
  • 用户点“下一步”时,先把这句话一次性补全,别让人干等它打完。
  • 用户在系统里开了“减少动态效果”,就直接显示整句,不播打字过程。

出处#

来自本站 Jojo 的入场动画和“带我逛逛首页”导览。“视觉更新别用 setTimeout / setInterval,改用 requestAnimationFrame”是 Paul Lewis 在 web.dev 的 Optimize JavaScript execution ↗ 里的第一条建议,文章里还讲了为什么定时器回调会落在一帧的末尾、导致掉帧,值得一读。