MacOS的Dock栏一直被视为桌面交互设计的典范:鼠标划过时,图标会按照距离远近呈现出波浪式的放大效果,就像水面被指尖轻轻划过一样自然流畅。移开鼠标后,图标又会带着一丝弹性回弹到原始大小,整个过程的物理感非常真实。这种效果看似复杂,其实核心逻辑只有两部分:一是根据鼠标位置与每个图标的距离动态计算缩放倍数,二是给动画过程加上合适的缓动函数模拟物理回弹。本文就用jQuery完整实现这套效果,并把每一处细节讲透。

一、效果原理拆解:波浪式放大是怎么发生的
首先要想清楚一个问题:为什么鼠标放在Dock栏中间时,整个区域的图标都会放大,而且是中间的放大得最厉害、两边的逐渐变小?答案就是距离衰减。我们把鼠标的横向坐标记为X,每个图标的中心横坐标记为Cx,两者之间的距离d越小,放大倍数就越大。用一个简单的衰减公式表示:scale = 1 + maxScale * (1 - d / influence),其中influence是影响半径,超出这个半径的图标保持原尺寸。
这个公式带来的视觉效果非常自然:鼠标移动时,每个图标到鼠标的距离都在实时变化,缩放比例也就跟着连续变化,所有图标一起动,就形成了波浪般的视觉过渡。需要注意的是,如果直接在mousemove事件里逐个设置transform,浏览器每帧都要触发重排,性能会受影响。更好的做法是只更新一个共享状态,然后用requestAnimationFrame统一驱动渲染,让缩放始终在浏览器的绘制帧内完成。
另外一个容易被忽略的细节是缩放的锚点。默认情况下CSS的transform-origin是元素中心,但Dock栏图标放大时应该以底部为锚点向上生长,否则图标会呈现出上下同时膨胀的奇怪样子。所以必须设置transform-origin: bottom center,让放大只发生在水平方向和垂直向上方向,这一点对还原MacOS的质感至关重要。
二、基础HTML结构与样式准备
先搭建页面骨架。Dock容器用flex布局水平排列图标,底部垫一层半透明背景,图标下方再各自放一个倒影元素。倒影的实现不依赖浏览器前缀属性,而是手动复制一份图标并垂直翻转,配合渐变遮罩做出真实的反射效果,这样兼容性更好。
<div class="dock">
<div class="dock-item">
<div class="icon">
<img src="icon1.png" alt="Finder"/>
<div class="reflection"><img src="icon1.png" alt=""/></div>
</div>
<span class="label">Finder</span>
</div>
<!-- 其余图标结构相同 -->
</div>对应的CSS部分,重点是倒影的翻转与遮罩,以及缩放锚点的设置。
.dock {
display: flex;
align-items: flex-end;
gap: 8px;
padding: 10px 16px;
background: rgba(255, 255, 255, 0.15);
border-radius: 18px;
backdrop-filter: blur(10px);
}
.icon {
position: relative;
transform-origin: bottom center; /* 关键:以底部为缩放锚点 */
transition: transform 0.15s ease-out;
}
.icon img { width: 56px; height: 56px; display: block; }
.reflection {
position: absolute;
top: 100%;
left: 0;
transform: scaleY(-1); /* 垂直翻转形成倒影 */
opacity: 0.35;
mask-image: linear-gradient(to bottom, rgba(0,0,0,0.5), transparent 60%);
-webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,0.5), transparent 60%);
pointer-events: none;
}这里的mask-image渐变是反射效果的灵魂,它让倒影从靠近图标处的不透明逐渐过渡到完全透明,模拟出光滑桌面上的镜面反射。倒影直接复用了同一个图片资源,不会增加额外的网络请求。由于倒影是图标元素的子节点,当父元素被放大时,倒影也会同步跟着放大和翻转,不需要单独处理。
三、用jQuery实现距离感应的动态缩放
接下来是核心的JavaScript逻辑。思路是:监听Dock容器的mousemove事件,遍历每个图标,计算其中心点与鼠标横坐标的距离,套用衰减公式得出缩放倍数,然后写入transform。当鼠标离开容器时,把所有图标恢复到1倍并触发弹性回弹。
$(function () {
var maxScale = 1.6; // 最大放大倍数
var influence = 120; // 影响半径,单位像素
var rafId = null;
var mouseX = null;
var $dock = $('.dock');
var $icons = $dock.find('.icon');
$dock.on('mousemove', function (e) {
mouseX = e.pageX;
if (!rafId) {
rafId = requestAnimationFrame(render);
}
});
$dock.on('mouseleave', function () {
mouseX = null;
if (!rafId) {
rafId = requestAnimationFrame(render);
}
});
function render() {
rafId = null;
$icons.each(function () {
var $icon = $(this);
var offset = $icon.offset();
var centerX = offset.left + $icon.width() / 2;
var scale = 1;
if (mouseX !== null) {
var d = Math.abs(mouseX - centerX);
if (d < influence) {
scale = 1 + (maxScale - 1) * (1 - d / influence);
}
}
// 保留两位小数,避免生成过长的transform字符串
$icon.css('transform', 'scale(' + scale.toFixed(3) + ')');
});
}
});这段代码有几个值得注意的地方。第一,mousemove只记录坐标,真正的DOM操作放在requestAnimationFrame回调里,这样即使鼠标移动事件触发得比屏幕刷新更频繁,也只会在每帧渲染一次,性能开销可控。第二,影响半径influence决定了波浪的宽度,值越大,被放大的图标越多、过渡越平缓;值越小则只有鼠标正下方的少数图标会动,视觉上更紧凑。第三,scale.toFixed(3)对数值做截断,可以减少浏览器解析transform字符串的负担。
还要处理一个边界情况:当图标本身被放大后,它的offset().left也会随之变化,如果直接用变化后的位置参与计算,会出现放大越算越大的抖动。解决办法有两种,一是在首次渲染时缓存所有图标的原始中心坐标,二是给图标外再包一层固定尺寸的容器,把缩放只作用于内层。推荐后者,结构更清晰:
<div class="dock-item"> <!-- 外层不缩放,坐标稳定 --> <div class="icon">...</div> <!-- 内层负责缩放 --> </div>
这样计算距离时取.dock-item的中心点,缩放时改.icon的transform,两个职责互不干扰,鼠标快速来回滑动也不会出现位置跳变。
四、弹性回弹动画的实现与调参
波浪放大是实时的、跟手的过程,而鼠标离开后的恢复则适合用一段带有过冲的弹性动画来收尾。原生jQuery的animate不支持transform,这里直接用jQuery UI的effect,或者更轻量的方式:自己写一个基于easeOutElastic缓动函数的补间动画。弹性缓动的公式是Math.pow(2, -10 * t) * Math.sin((t - p / 4) * (2 * Math.PI) / p) + 1,它会让数值先冲过目标值再震荡回落,正是那种果冻般的质感。
// 自定义弹性缓动的补间
function elasticTo($el, from, to, duration) {
var start = null;
var p = 0.35; // 震荡周期,越小抖得越快
function easeOutElastic(t) {
if (t === 0 || t === 1) return t;
return Math.pow(2, -10 * t) *
Math.sin((t - p / 4) * (2 * Math.PI) / p) + 1;
}
function step(timestamp) {
if (start === null) start = timestamp;
var t = Math.min((timestamp - start) / duration, 1);
var scale = from + (to - from) * easeOutElastic(t);
$el.css('transform', 'scale(' + scale.toFixed(3) + ')');
if (t < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
$dock.on('mouseleave', function () {
mouseX = null;
$icons.each(function () {
var $icon = $(this);
var current = $icon.data('scale') || 1;
// 给靠鼠标近的图标加一点延迟,形成错落的回弹节奏
elasticTo($icon, current, 1, 600);
});
});调参方面,duration控制在500到800毫秒之间手感最好,太短弹性出不来,太长则显得拖沓。p值(震荡周期)越小,回弹的抖动次数越多、越活泼;调大到0.5左右就变成了比较稳重的一次轻微过冲。如果你希望回弹像真实MacOS那样带一点错落感,可以给每个图标按其到容器中心的距离加一个递增的delay,让边缘的图标晚几十毫秒再开始回弹,波浪会呈现出漂亮的收拢节奏。
还有一个加分项是配合垂直位移。真实Dock栏中图标放大时除了变大,还会略微向上顶起, tooltips标签会淡入。可以在设置scale的同时加上translateY(-8px),标签文字用opacity过渡,鼠标悬停在某个图标上时淡入显示,鼠标移开后淡出,整体观感会立刻精致很多。
五、方案对比与几点优化建议
这个效果也可以用纯CSS配合:hover加transition实现简化版,但纯CSS无法感知相邻图标的距离,做不到波浪式的连带放大,只能实现单个图标自身的放大。而用JavaScript逐帧计算,才能实现那种一整排图标随鼠标位置连续起伏的效果,这是两种方案的本质区别。如果你的项目已经在用jQuery,本文的方案实现成本很低;如果追求极致性能,也可以把$(this).css换成原生的style.transform赋值,减少jQuery对象的创建开销。
在性能优化上,建议把所有图标的DOM引用在初始化时一次性取出存进数组,避免在每帧的render函数里反复调用.find();距离计算只依赖横坐标,不需要读取offsetTop,减少强制同步布局的风险。图标数量较多时,还可以考虑把influence半径随图标总数自适应调整,保证波浪始终覆盖合理的宽度范围。
最后,这套距离衰减加弹性缓动的思路并不局限于Dock栏,图片墙的悬停放大、导航菜单的磁性按钮、甚至Canvas粒子动画中的排斥效果,用的都是同一套数学模型。掌握了本文的核心逻辑,你可以把它灵活迁移到各种交互场景中,为页面增添真实的物理质感。
jQuery动画MacOS Dock效果弹性动画修改时间:2026-09-09 01:39:03