Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >打造一款会呼吸的滚动条

打造一款会呼吸的滚动条

原创
作者头像
繁依Fanyi
修改于 2025-06-30 16:59:32
修改于 2025-06-30 16:59:32
14600
代码可运行
举报
运行总次数:0
代码可运行

我一直觉得滚动条这个东西实在太容易被忽视了。它就待在页面的边缘,不声不响地陪我们翻页、滑动,看起来像个工具,却几乎从未被认真打磨过。大多数时候,我们花了很多精力去优化页面动画、微交互,甚至为每个按钮设计 hover 效果,唯独滚动条,几乎都用浏览器默认的样式。

有一次我在看一个音乐可视化网站,页面背景的动效特别炫,粒子飞舞、节奏同步,视觉冲击力特别强。那一刻我突然冒出了个想法:如果滚动条也能动起来,能呼吸、能流动,甚至像液体一样具有生命感,那是不是会更酷一点?

这个想法就这么诞生了。


事情是怎么开始的?

一开始我只是想着能不能让滚动条别那么无趣。比如在用户滚动的时候,滑块后面留下一点拖尾?或者发点光?再夸张点,像《星际穿越》里的虫洞拉伸那样来一发“超时空轨迹”?这个想法说大不大,说小也不算小,我把它记在了笔记里,慢慢琢磨。

最开始我用的是 ::-webkit-scrollbar 去改滚动条的样式。这个方案简单,很多教程里都有。但试了一圈发现兼容性太烂了。Firefox 完全不认,Safari 和 Edge 各有各的奇葩行为。你在 Chrome 上调得再漂亮,换个浏览器就直接打回原形。

所以我干脆一咬牙:那就自己画一个滚动条吧,用 canvas 来搞。


项目目标:不是改样式,而是做个“有生命的条”

这个项目我起了个名字,叫 FluidScroll。意思很直接:流动的滚动条。

我并不是单纯想做个样式好看的滚动条,我想让它“动”起来,能根据滚动速度做出反应,滑得快的时候拖尾长一点、闪一点颜色,慢的时候就温柔点,像水一样。我的目标是让它感觉像一个有温度的 UI,而不只是一个滑块。

技术栈方面,其实蛮简单:

  • canvas 自己画滑块和拖尾;
  • requestAnimationFrame 做高帧率动画;
  • 根据 scrollTop 变化计算滚动速度;
  • 用粒子系统来模拟流体轨迹;
  • 整个模块尽量封装、独立,方便别人直接用。

页面结构怎么搭?

HTML 结构非常清爽:

代码语言:html
AI代码解释
复制
<div id="scroll-wrapper">
  <canvas id="scrollbar"></canvas>
  <div id="content">
    <h1>🚀 滚动体验</h1>
    <p>拖动页面,体验流体滚动条。</p>
    <div class="filler"></div>
  </div>
</div>

滚动的区域是 #scroll-wrapper,canvas 是独立画布,固定在页面右边,宽度窄一点,高度占满全屏。真正的浏览器滚动条被我隐藏了,视觉上看到的都是 canvas 渲染出来的。

而且我特地加了 pointer-events: none,保证 canvas 不拦截任何点击或拖动事件。这个细节很重要,否则你点页面右边会发现啥都点不了。


怎么让它动起来?

核心思路其实不复杂:监听滚动事件,每次滚动时,根据滚动速度生成一堆粒子,然后画在 canvas 上。这些粒子会自己扩散、变淡、最后消失,就像一个拖尾。

每个粒子有以下几个属性:

代码语言:js
AI代码解释
复制
{
  x, y,       // 位置
  dx, dy,     // 移动方向
  alpha,      // 透明度
  radius,     // 大小
  color       // hsl颜色
}

动画主循环里,每一帧都更新这些粒子的位置和状态,再重新绘制整张 canvas:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function updateParticles() {
  particles.forEach(p => {
    p.x += p.dx;
    p.y += p.dy;
    p.alpha -= 0.02;
  });
  particles = particles.filter(p => p.alpha > 0);
}

只保留 alpha > 0 的粒子,这样它们会“自然死亡”,不会一直占内存。


滚动速度是怎么计算的?

这个逻辑就更简单了,纯粹靠 scrollTop 的差值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let velocity = scrollTop - lastScrollTop;
lastScrollTop = scrollTop;

速度越大,说明滚动越快,我就多生成一些粒子;滚得慢,就少生成点:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (let i = 0; i < Math.abs(velocity) * 0.5; i++) {
  particles.push(createParticle(yPos));
}

这个 yPos 是当前滚动条位置对应的 y 坐标。通过比例映射到 canvas 的高度即可。


来张图看看它的结构吧

在这里插入图片描述
在这里插入图片描述

UI 风格:极简深色 + 柔光拖尾

颜色我用了黑色底,粒子是 hsl 生成的彩色系,既丰富又不会太杂。因为色相是用随机的 Math.random() * 360 来搞的,所以每次滚动都会有微妙差异,视觉上很灵动。

整个 canvas 不加边框、不加阴影,只保留最核心的光点轨迹。这种视觉风格其实是参考了早期一些音频可视化工具,也有点像某些科幻 HUD 的感觉。


性能优化:不然真能卡死你

我一开始跑起来,发现滚动快的时候粒子太多了,画布卡得一塌糊涂。于是加了几重优化策略:

  1. 限制每帧最大粒子数,比如不超过 30 个;
  2. 粒子 fade out 后马上销毁,不保留;
  3. 用户停止滚动后就不再 requestAnimationFrame
  4. canvas 尺寸用 devicePixelRatio 适配 Retina 屏;

代码示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (particles.length === 0 && Math.abs(velocity) < 0.01) {
  return; // 不滚了,省资源
}
requestAnimationFrame(animate);

踩过的坑也不少

调这个东西其实也遇到不少问题:

  • 粒子消失太快:alpha 递减太快,有些粒子一出来就没了,后来我加了初始透明度随机值,问题解决;
  • 高 DPI 模糊:canvas 没适配高分屏,结果粒子都发虚,后来通过 ctx.scale() 解决;
  • scrollTop 获取不一致:不同容器的 scrollTop 源不一样,调了半天才弄清要用容器的 scroll 值。

最终打包成组件

后来我把整个逻辑封装成了一个类,可以直接挂载:

代码语言:js
AI代码解释
复制
const scrollFX = new FluidScrollbar('#scroll-wrapper')
scrollFX.init()

甚至还留了配置项:

代码语言:js
AI代码解释
复制
new FluidScrollbar('#scroll-wrapper', {
  colorMode: 'hsl',
  maxParticles: 40,
  blurEffect: true
});

模块化之后,这东西就能在别的项目里复用了,不用每次都复制一堆代码。


整体效果咋样?

说实话,做好之后我自己挺满意的。虽然只是一个滚动条,但它的细节、动效、跟手性都做到了我想要的程度。平常滚页面没啥感觉,现在看到那一串彩色粒子随着滚动拖出来,就像是在点亮页面一样。

它不会干扰用户操作,也不会喧宾夺主,却在每次滚动时,悄悄回应一下你的动作。

在这里插入图片描述
在这里插入图片描述

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验