You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为元素添加平滑动画时旋转行为异常的问题排查与解决

问题描述

实现跟随鼠标移动的箭头元素,添加平滑动画后出现异常:当旋转值达到180deg或-180deg时,箭头会反向滚动。尝试使用GSAP仍存在该问题,将动画时间设为0.01s时异常消失,但动画效果也不可见。

问题原因

Math.atan2返回的角度范围是-180°到180°,当鼠标从箭头右侧移动到左侧(或反之)时,角度会从179°跳变到-179°(或反向),此时CSS过渡会选择最长的旋转路径(358°反向旋转),而非最短路径(2°正向旋转),导致箭头反向滚动的视觉异常。

修复方案

核心思路是计算角度的最短旋转路径,每次调整目标角度到与当前角度差值最小的等价角度(通过加减360°实现),确保过渡始终走最短路径。

修改后的代码如下:

JavaScript

document.addEventListener('mousemove', function(event) {
  const mouseX = event.clientX;
  const mouseY = event.clientY;
  const arrows = document.querySelectorAll('.cta-link-icon');

  arrows.forEach(arrow => {
    const arrowRect = arrow.getBoundingClientRect();
    const arrowCenterX = arrowRect.left + arrow.offsetWidth / 2;
    const arrowCenterY = arrowRect.top + arrow.offsetHeight / 2;
    
    // 计算目标角度
    let targetAngle = Math.atan2(mouseY - arrowCenterY, mouseX - arrowCenterX) * (180 / Math.PI);
    
    // 获取当前存储的角度,初始为0
    let currentAngle = parseFloat(arrow.dataset.currentAngle) || 0;
    
    // 计算角度差并调整到-180°~180°范围,确保走最短路径
    let angleDiff = targetAngle - currentAngle;
    angleDiff = (angleDiff + 360) % 360;
    if (angleDiff > 180) {
      angleDiff -= 360;
    }
    
    // 得到调整后的目标角度
    const adjustedTargetAngle = currentAngle + angleDiff;
    
    // 更新当前角度到元素的dataset中
    arrow.dataset.currentAngle = adjustedTargetAngle;
    
    // 应用旋转
    arrow.style.transform = `rotate(${adjustedTargetAngle}deg)`;
  });
});

CSS

调整过渡时间为合适的值(比如0.1s),保证平滑效果:

html, body {
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.cta-link-icon {
  width: 50px;
  height: 50px;
  display: flex;
  justify-content: center;
  align-items: center;
  pointer-events: none;
  transition: transform 0.1s ease-out; /* 调整过渡时间和缓动函数 */
}

HTML

无需修改,保持原结构即可:

<div class="cta-link-icon">
<svg width="42" height="36" viewBox="0 0 42 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.5435 1.33321L41.2935 17.0832C41.4179 17.2017 41.517 17.3442 41.5847 17.5022C41.6524 17.6601 41.6873 17.8301 41.6873 18.002C41.6873 18.1738 41.6524 18.3438 41.5847 18.5017C41.517 18.6597 41.4179 18.8022 41.2935 18.9207L25.5435 34.6707C25.2998 34.9144 24.9694 35.0513 24.6248 35.0513C24.2802 35.0513 23.9497 34.9144 23.706 34.6707C23.4623 34.427 23.3254 34.0966 23.3254 33.752C23.3254 33.4074 23.4623 33.0769 23.706 32.8332L37.2029 19.3145L1.87476 19.3145C1.52666 19.3145 1.19282 19.1762 0.946678 18.93C0.700537 18.6839 0.562256 18.3501 0.562256 18.002C0.562256 17.6539 0.700537 17.32 0.946678 17.0739C1.19282 16.8277 1.52666 16.6895 1.87476 16.6895L37.2029 16.6895L23.706 3.17071C23.4623 2.92704 23.3254 2.59656 23.3254 2.25196C23.3254 1.90736 23.4623 1.57688 23.706 1.33321C23.9497 1.08954 24.2802 0.952651 24.6248 0.952651C24.9694 0.952651 25.2998 1.08954 25.5435 1.33321Z" fill="currentColor"></path>
</svg></div>
其他实现方式
  • GSAP实现:利用GSAP的shortRotation配置,自动处理最短旋转路径,实现更精细的动画控制:
    document.addEventListener('mousemove', (e) => {
      const arrows = document.querySelectorAll('.cta-link-icon');
      arrows.forEach(arrow => {
        const rect = arrow.getBoundingClientRect();
        const centerX = rect.left + rect.width/2;
        const centerY = rect.top + rect.height/2;
        const angle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * 180/Math.PI;
        
        gsap.to(arrow, {
          rotate: angle,
          duration: 0.1,
          ease: 'power1.out',
          shortRotation: true
        });
      });
    });
    
  • CSS变量+过渡:将角度存储在CSS变量中,通过JS更新变量,依赖CSS过渡实现平滑:
    document.addEventListener('mousemove', (e) => {
      const arrows = document.querySelectorAll('.cta-link-icon');
      arrows.forEach(arrow => {
        const rect = arrow.getBoundingClientRect();
        const centerX = rect.left + rect.width/2;
        const centerY = rect.top + rect.height/2;
        let targetAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * 180/Math.PI;
        
        let currentAngle = parseFloat(getComputedStyle(arrow).getPropertyValue('--angle')) || 0;
        let angleDiff = targetAngle - currentAngle;
        angleDiff = (angleDiff + 360) % 360;
        if (angleDiff > 180) angleDiff -= 360;
        const adjustedAngle = currentAngle + angleDiff;
        
        arrow.style.setProperty('--angle', `${adjustedAngle}deg`);
      });
    });
    
    CSS中添加:
    .cta-link-icon {
      --angle: 0deg;
      transform: rotate(var(--angle));
      transition: transform 0.1s ease-out;
      /* 其他样式不变 */
    }
    
  • requestAnimationFrame手动平滑:通过帧循环控制角度的增量,实现自定义的平滑效果:
    const arrows = document.querySelectorAll('.cta-link-icon');
    const arrowStates = Array.from(arrows).map(() => ({ currentAngle: 0 }));
    
    function updateArrows(e) {
      arrows.forEach((arrow, idx) => {
        const rect = arrow.getBoundingClientRect();
        const centerX = rect.left + rect.width/2;
        const centerY = rect.top + rect.height/2;
        const targetAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * 180/Math.PI;
        
        const state = arrowStates[idx];
        const angleDiff = targetAngle - state.currentAngle;
        const normalizedDiff = (angleDiff + 360) % 360;
        const finalDiff = normalizedDiff > 180 ? normalizedDiff - 360 : normalizedDiff;
        
        // 每次帧更新角度的一部分,实现平滑
        state.currentAngle += finalDiff * 0.1; // 0.1是缓动系数,值越小越平滑
        arrow.style.transform = `rotate(${state.currentAngle}deg)`;
      });
    }
    
    document.addEventListener('mousemove', (e) => {
      requestAnimationFrame(() => updateArrows(e));
    });
    

内容的提问来源于stack exchange,提问作者Andriy Trofimchuk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 06:44:54