磁场按钮(rAF 插值磁吸跟随按钮动效源码)

CSSJS 许可:自研原创

磁场按钮源码:按钮被光标「磁吸」跟随,距离越近吸力越强,rAF 逐帧插值产生顺滑磁性拖拽感,附带感应光环可视化磁场范围,原生 JS 零依赖。

效果演示(真实可交互)

把鼠标移入演示区的虚线舞台,一个浅蓝色光环会跟随光标出现并放大,这就是「磁场」的可视化范围。三枚按钮(主色、描边、深色)会朝光标方向平滑移动:距离近的按钮被吸得明显,距离远的几乎不动,模拟真实磁力随距离衰减的规律。移出舞台后按钮带着惯性缓缓回弹归位,按钮内的文字还会反向漂移 35%,产生立体景深。与直接把位移写进 transform 的简易版不同,本组件用 requestAnimationFrame 每帧做插值逼近,位移曲线连续顺滑,观感接近物理弹簧。光环只做视觉提示,不拦截事件,指针穿过它不会有任何卡顿。按钮的位移是纯视觉反馈,实际点击命中区域仍是原位,因此磁吸效果不会影响表单提交或路由跳转的可靠性。多按钮之间吸力独立计算,互不干扰。磁吸适合用于主 CTA、导航入口这类需要被「注意到」的元素;配合文字反向漂移,按钮会显得有厚度和深度,比静态高亮更有记忆点,适合营销落地页的首屏引导。

鼠标进入区域后按钮被平滑「吸」向光标,移出后惯性回弹

完整代码

HTML:舞台容器包着光环与三枚按钮,按钮内部文字单独包一层用于反向漂移:

<div class="bm-field" id="bmField">
  <div class="bm-ring" id="bmRing"></div>
  <button class="bm-btn bm-primary"><span class="bm-text">磁场按钮</span></button>
  <button class="bm-btn bm-outline"><span class="bm-text">描边磁场</span></button>
  <button class="bm-btn bm-dark"><span class="bm-text">深色磁场</span></button>
</div>

CSS:光环默认缩小隐藏,指针进入后弹性放大浮现:

.bm-ring {
  position: absolute; width: 220px; height: 220px; border-radius: 50%;
  border: 2px dashed rgba(108,140,255,.5);
  transform: translate(-50%, -50%) scale(.4); opacity: 0;
  transition: transform .5s cubic-bezier(.34,1.2,.64,1), opacity .3s ease;
}
.bm-field.bm-on .bm-ring { opacity: 1; transform: translate(-50%, -50%) scale(1); }
.bm-btn { transition: box-shadow .25s ease; will-change: transform; }

原生 JS 核心:每个按钮维护「目标坐标」与「当前坐标」,rAF 每帧按 18% 逼近,形成磁性拖拽感:

const field = document.getElementById('bmField');
const ring = document.getElementById('bmRing');
const buttons = [...field.querySelectorAll('.bm-btn')];
const textEls = buttons.map((b) => b.querySelector('.bm-text'));
const state = buttons.map(() => ({ tx: 0, ty: 0, cx: 0, cy: 0 }));
let raf = null;

function tick() {
  let settled = true;
  buttons.forEach((btn, i) => {
    const s = state[i];
    s.cx += (s.tx - s.cx) * 0.18;                 // 每帧向目标逼近 18%
    s.cy += (s.ty - s.cy) * 0.18;
    if (Math.abs(s.tx - s.cx) > 0.1 || Math.abs(s.ty - s.cy) > 0.1) settled = false;
    btn.style.transform = `translate(${s.cx.toFixed(2)}px, ${s.cy.toFixed(2)}px)`;
    textEls[i].style.transform = `translate(${(s.cx * 0.35).toFixed(2)}px, ${(s.cy * 0.35).toFixed(2)}px)`;
  });
  raf = settled ? null : requestAnimationFrame(tick);   // 稳定后自动停帧省电
}

function onMove(e) {
  ring.style.left = e.clientX - field.getBoundingClientRect().left + 'px';
  ring.style.top = e.clientY - field.getBoundingClientRect().top + 'px';
  buttons.forEach((btn, i) => {
    const rect = btn.getBoundingClientRect();
    const dx = e.clientX - (rect.left + rect.width / 2);
    const dy = e.clientY - (rect.top + rect.height / 2);
    const dist = Math.hypot(dx, dy);
    const pull = Math.max(0, 1 - dist / 220) * 0.5;   // 距离越近吸力越强
    state[i].tx = dx * pull;
    state[i].ty = dy * pull;
  });
  if (!raf) raf = requestAnimationFrame(tick);
}
field.addEventListener('pointermove', onMove);
field.addEventListener('pointerleave', () => {
  state.forEach((s) => { s.tx = 0; s.ty = 0; });      // 归零目标,自然回弹
  if (!raf) raf = requestAnimationFrame(tick);
});

参数说明

参数默认值说明
磁场半径dist / 220220px 内才有吸力,调大范围更广
吸力系数pull * 0.5位移比例上限 50%,调大按钮「飞」得更明显
插值速率* 0.18每帧逼近比例,越小越粘滞、越大越跟手
文字反向系数* 0.35文字与按钮反向漂移的景深幅度
光环尺寸220px应与磁场半径一致,提示用户有效范围

兼容性与性能

常见问题

Q: 按钮被「吸」得太远脱离按钮语义? 磁吸位移只是视觉反馈,按钮实际占位未变,可点击区域仍是原位置;如担心误触,把 pull 上限从 0.5 降到 0.25。

Q: 多按钮会互相遮挡吗? 按钮默认不重叠;若间距太近,靠近光标时会互相挤压,建议保持 20px 以上间距或按需只磁吸主 CTA。

Q: 为什么我的版本没有惯性回弹? 惯性来自插值逼近:回弹时目标归零、当前坐标逐帧追赶,直接赋 translate(0,0) 就没有过渡。保留 tick 循环即可。

Q: 可以商用吗? 可以。本组件为粤喵自研原创代码,免费商用无需署名。

许可与来源

  • 许可证:自研原创 —— 粤喵自研原创,可免费用于个人与商业项目,无需署名。查看来源公示与许可清单
  • 来源:粤喵团队原创自研(从效果思路到代码实现均为独立完成,未直接复制第三方代码)
  • 首次发布:2026-09-01 · 最近更新:2026-09-01
  • 修改日志:2026-09-01 首次发布;2026-09-13 补充许可证与来源标注、复制按钮说明与法务页入口
  • 纠错:联系页面 / min@cactus.asia,2 个工作日内核实处理

常见问题

这个组件可以商用吗?

可以。本组件为粤喵自研原创,可免费用于个人和商业项目,无需署名。

浏览器兼容性如何?

支持所有现代浏览器(Chrome / Firefox / Safari / Edge 最新版本)。旧版 IE 不支持 CSS 动画相关特性。

怎么修改动画参数?

直接调整 CSS 中的 animation-duration(时长)、animation-timing-function(缓动曲线)等属性即可,代码中有注释说明。

✍️ 作者与审核