磁性吸附 + 3D 倾斜:让卡片像有生命一样回应你的光标
磁性吸附 + 3D 倾斜:让卡片像有生命一样回应你的光标
2小时前 3 阅读
  • 首页
  • /
  • 分享
  • /
  • 正文
  • 当光标靠近元素时,元素会产生一种"被吸引"的微妙偏移;鼠标在元素上移动时,卡片会像真实物体一样沿 X/Y 轴倾斜。两种效果叠加,让 UI 拥有物理质感和生命力。

    效果预览

    一、效果拆解

    这个交互由两层独立的物理效果叠加而成,理解它们的数学原理是写好代码的前提。

    1.1 磁性吸附(Magnetic Pull)

    现象:光标靠近元素时,元素会微微"迎向"光标移动;离开时缓慢复位。

    数学原理:以元素中心为原点,计算光标到原点的距离 dist

    power = 1 - dist / magnetRadius   (dist < magnetRadius 时有效)
    offsetX = distX * power * (magnetIntensity / magnetRadius)
    offsetY = distY * power * (magnetIntensity / magnetRadius)

    power 是一个 0~1 的衰减系数——光标越近,系数越大,偏移量越大。离开触发半径后用 lerp 平滑归零。

    用途场景

    • 导航菜单项:鼠标靠近时微微放大并上移
    • CTA 按钮:光标靠近时产生"吸附感",引导点击
    • 图片画廊:鼠标靠近缩略图时产生微妙的聚拢效果

    1.2 3D 倾斜(Tilt Effect)

    现象:鼠标在元素上移动时,元素沿 X 轴和 Y 轴旋转,产生真实的透视立体感。

    数学原理:同样以元素中心为原点,计算光标相对位移的归一化值:

    tiltX = (mouseY - centerY) / (height / 2) * tiltIntensity
    tiltY = (mouseX - centerX) / (width / 2) * tiltIntensity

    配合 CSS perspectiverotateX/rotateY,实现三维空间旋转。值域钳制在 -tiltIntensity ~ +tiltIntensity之间,防止倾斜过度。

    用途场景

    • 商品卡片:鼠标扫过时展示多角度立体感
    • 头像框:创造轻微的悬浮感
    • 数据仪表卡:让数字面板更有质感

    二、完整代码

    HTML 结构

    <div class="magnetic-wrapper">
      <div class="magnetic-card" id="magneticCard">
        <!-- 动态光泽层 -->
        <div class="card-glare" id="cardGlare"></div>
    
        <span class="card-tag">CSS Effect</span>
        <h2 class="card-title">Magnetic Cursor &amp; 3D Tilt</h2>
        <p class="card-subtitle">光标磁性吸附与三维倾斜叠加效果……</p>
        <div class="card-divider"></div>
    
        <div class="card-meta">
          <div class="card-avatar">FL</div>
          <div class="card-author-info">
            <div class="card-author">Flora</div>
            <div class="card-date">2026 · 03 · 19</div>
          </div>
        </div>
    
        <div class="card-tags">
          <span class="card-tag-item">JavaScript</span>
          <span class="card-tag-item">CSS 3D</span>
        </div>
    
        <div class="card-footer">
          <span class="card-stat"><strong>∞</strong> reads</span>
          <span class="card-btn">Read More →</span>
        </div>
      </div>
    </div>

    核心 CSS

    .magnetic-card {
      /* 保留 3D 空间,否则 rotateX/Y 不生效 */
      transform-style: preserve-3d;
    
      /* 告诉浏览器提前检测变换,用于 GPU 加速 */
      will-change: transform;
    
      /* 初始状态平滑过渡 */
      transition:
        transform 0.08s ease-out,
        box-shadow 0.3s ease;
    }
    
    /* 动态光泽:跟随光标位置实时更新背景 */
    .card-glare {
      background: radial-gradient(
        circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
        rgba(255, 255, 255, 0.12) 0%,
        transparent 60%
      );
      opacity: 0;
      transition: opacity 0.3s ease;
    }
    
    .magnetic-card:hover .card-glare {
      opacity: 1; /* 悬停时才显示光泽 */
    }
    
    /* 动态阴影:跟随倾斜方向偏移 */
    .magnetic-card {
      box-shadow:
        /* 基础阴影 */
        20px 20px 60px rgba(0, 0, 0, 0.5),
        /* 彩色氛围光(随偏移量动态变化) */
        var(--shadow-x, 0) var(--shadow-y, 0) 20px rgba(99, 102, 241, 0.15),
        /* 边框 */
        0 0 0 1px rgba(255, 255, 255, 0.05);
    }

    JavaScript 逻辑

    const CONFIG = {
      tiltIntensity: 15,        // 最大倾斜角度(度)
      magnetIntensity: 40,      // 磁性吸附最大偏移量(px)
      magnetRadius: 120,       // 触发磁性吸附的光标距离(px)
      resetSpeed: 0.08,        // 复位速度(越小越顺滑)
      tiltSpeed: 0.15,         // 倾斜跟随速度
    };
    
    let currentTiltX = 0, currentTiltY = 0;
    let targetTiltX = 0, targetTiltY = 0;
    let magnetOffsetX = 0, magnetOffsetY = 0;
    
    function lerp(start, end, factor) {
      return start + (end - start) * factor;
    }
    
    // 每帧更新卡片状态
    function updateCard() {
      // 倾斜:快速跟随鼠标
      currentTiltX = lerp(currentTiltX, targetTiltX, CONFIG.tiltSpeed);
      currentTiltY = lerp(currentTiltY, targetTiltY, CONFIG.tiltSpeed);
    
      // 磁性偏移:鼠标离开后缓慢归零
      magnetOffsetX = lerp(magnetOffsetX, 0, CONFIG.resetSpeed);
      magnetOffsetY = lerp(magnetOffsetY, 0, CONFIG.resetSpeed);
    
      // 复位完成后停止动画
      if (Math.abs(targetTiltX) < 0.01 && Math.abs(targetTiltY) < 0.01 &&
          Math.abs(magnetOffsetX) < 0.01 && Math.abs(magnetOffsetY) < 0.01) {
        return; // 动画停止,下次 mousemove 重新启动
      }
    
      card.style.transform =
        `translate(${magnetOffsetX}px, ${magnetOffsetY}px) ` +
        `perspective(1000px) ` +
        `rotateX(${-currentTiltY}deg) ` +
        `rotateY(${currentTiltX}deg)`;
    
      requestAnimationFrame(updateCard);
    }
    
    // 鼠标移动:计算两个效果的目标值
    card.addEventListener('mousemove', (e) => {
      const rect = card.getBoundingClientRect();
      const centerX = rect.left + rect.width / 2;
      const centerY = rect.top + rect.height / 2;
    
      // --- 3D 倾斜 ---
      const dx = (e.clientX - centerX) / (rect.width / 2);
      const dy = (e.clientY - centerY) / (rect.height / 2);
      targetTiltX = dy * CONFIG.tiltIntensity; // 上下 → rotateX
      targetTiltY = dx * CONFIG.tiltIntensity; // 左右 → rotateY
    
      // --- 磁性吸附 ---
      const distX = e.clientX - centerX;
      const distY = e.clientY - centerY;
      const dist = Math.hypot(distX, distY); // = √(dx² + dy²)
    
      if (dist < CONFIG.magnetRadius) {
        const power = 1 - dist / CONFIG.magnetRadius;
        magnetOffsetX = distX * power * (CONFIG.magnetIntensity / CONFIG.magnetRadius);
        magnetOffsetY = distY * power * (CONFIG.magnetIntensity / CONFIG.magnetRadius);
      }
    
      // --- 更新光泽层位置 ---
      const pctX = ((e.clientX - rect.left) / rect.width) * 100;
      const pctY = ((e.clientY - rect.top) / rect.height) * 100;
      glare.style.setProperty('--mouse-x', pctX + '%');
      glare.style.setProperty('--mouse-y', pctY + '%');
    });
    
    // 鼠标离开:复位
    card.addEventListener('mouseleave', () => {
      targetTiltX = targetTiltY = 0;
      magnetOffsetX = magnetOffsetY = 0;
    });
    
    // 鼠标进入:启动动画循环
    card.addEventListener('mouseenter', () => {
      cancelAnimationFrame(rafId);
      rafId = requestAnimationFrame(updateCard);
    });

    三、参数调优指南

    参数默认值调大效果调小效果
    tiltIntensity15°倾斜更夸张,3D 感更强轻微倾斜,更克制
    magnetIntensity40px吸附感更强,更"黏"几乎无吸附感
    magnetRadius120px更大范围触发吸附仅在卡片附近触发
    resetSpeed0.08快速弹回,干脆利落缓慢归位,丝滑感
    tiltSpeed0.15跟得更紧,响应更快跟随滞后,更柔和

    实际调优建议

    • 商品展示卡片tiltIntensity: 20~25,偏夸张,增强吸引力
    • 正式工具界面tiltIntensity: 8~12,resetSpeed: 0.12,克制冷静
    • 移动端触控:把 mousemove 换成 touchmove,倾斜 Intensity 减半

    四、性能优化要点

    4.1 will-change 的正确使用

    .magnetic-card {
      /* 在动画即将开始前添加,开始后保持,结束后移除 */
      will-change: transform;
    }

    不要在 CSS 中永久设置 will-change,只有在需要时才声明,否则会浪费 GPU 内存。

    4.2 requestAnimationFrame 替代 setInterval

    60fps 的定时更新逻辑用 requestAnimationFrame,它会在浏览器下一次重绘前执行,避免丢帧和卡顿。

    4.3 限制计算频率

    如果页面上有多张可交互卡片,可以对 mousemove 事件做节流(throttle),每 16ms(1帧)最多计算一次:

    let lastTime = 0;
    card.addEventListener('mousemove', (e) => {
      const now = performance.now();
      if (now - lastTime < 16) return;
      lastTime = now;
      // ...执行计算逻辑
    });

    五、扩展思路

    进阶 1:多元素联动
    在磁性吸附范围内同时影响其他元素(比如周围的图标也向光标靠拢),形成"涟漪"效果。

    进阶 2:倾斜引导注意力
    当卡片倾斜时,将高光区域(光泽层)引导到你想让用户注意的 CTA 按钮上,利用倾斜自然引导视线。

    进阶 3:3D 翻转
    mousemove 中检测移动方向(从左向右 vs 从右向左),配合 rotateY,可以做到让元素"转身"的微妙效果。


    原文链接:荣小站 xgr.cab
    代码仓库:所有代码可直接保存为 .html 文件在浏览器中运行预览
    1

    评语 (0)

    取消