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

JS Web Animations API实现旋转缓动时出现异常行为

拖动旋转元素后Web Animations API缓动归位导致后续旋转错位问题

问题描述

实现拖动旋转HTML元素功能,预期行为:

  • 按下鼠标触发mousedown事件
  • 按住鼠标移动时触发mousemove,沿X轴移动控制元素正反旋转
  • 释放鼠标触发mouseup,元素缓动归位到预设角度(0、π/2、π、3π/2弧度)

直接设置transform:rotate时所有行为正常,但使用Web Animations API实现缓动归位后,首次操作正常,后续拖动旋转会出现元素错位;下方硬归位的元素则无此问题。

问题根源

  1. 旋转值读取逻辑错误:原getRotation函数仅从元素的style.transform中读取旋转值,但Web Animations API运行时,元素的实际旋转状态由动画控制,style.transform不会实时更新,导致后续拖动时读取的是旧值,计算新角度时出错。
  2. 字符串截取漏洞:原getRotation通过固定长度截取旋转数值,当数值小数位数变化时,会错误截断数值,导致角度读取不准确。
  3. 未处理动画冲突:拖动时未取消元素上正在运行的动画,动画状态和手动设置的transform相互干扰。

修复方案

1. 正确获取元素实际旋转角度

替换原getRotation函数,通过getComputedStyle获取元素的计算样式,解析矩阵值得到真实旋转角度(弧度),兼容直接设置样式和动画控制两种场景:

function getRotation(elem) {
  const computedStyle = window.getComputedStyle(elem);
  const transform = computedStyle.transform;
  if (!transform || transform === 'none') return 0;
  
  // 解析matrix/matrix3d获取旋转弧度
  const values = transform.split('(')[1].split(')')[0].split(',');
  const a = parseFloat(values[0]);
  const b = parseFloat(values[1]);
  return Math.atan2(b, a);
}

2. 拖动前取消正在运行的动画

在handleMouseMove中,开始拖动时先取消元素上所有未完成的动画,避免动画状态和手动拖动操作冲突:

function handleMouseMove(e){
  if(primaryMouseButtonDown === true)
  {
    for (var i = 0; i < squares.length; ++i)
    {
      var square = squares[i];
      // 取消所有正在运行的动画
      square.getAnimations().forEach(anim => anim.cancel());
      
      var oldRotation = getRotation(square);
      newRotation = (oldRotation + e.movementX/100) % (2*Math.PI);
      // 修正负角度,确保在0-2π区间
      if(newRotation < 0) newRotation += 2*Math.PI;
      square.style.transform = `rotate(${newRotation}rad)`;
    }
  }
}

3. 优化动画结束处理

动画结束后仅需调用commitStyles()同步状态,无需额外cancel(),避免样式状态回退:

if (square.parentElement.id=="animate-container")
{
  const animation = square.animate([
    {transform: `rotate(${currentRotation}rad)`},
    {transform: `rotate(${nearestRotation}rad)`}
  ],
  {
    fill: "forwards",
    duration: 1000,
    iterations: 1
  });

  animation.addEventListener("finish", () => {
    console.log("finished-square");
    animation.commitStyles();
  });
}

4. 优化角度匹配逻辑

考虑旋转的环形特性,计算最小角度差,避免跨0/2π边界时的错误匹配:

function getNearestAngle(currentAngle) {
  let nearest = angles[0];
  let minDiff = Infinity;
  
  angles.forEach(angle => {
    // 计算环形最小角度差
    let diff = Math.abs(currentAngle - angle);
    diff = Math.min(diff, 2*Math.PI - diff);
    if(diff < minDiff) {
      minDiff = diff;
      nearest = angle;
    }
  });
  
  return nearest;
}

完整修复代码

Array.prototype.max = function() {
  return Math.max.apply(null, this);
};

Array.prototype.min = function() {
  return Math.min.apply(null, this);
};

let animateContainer = document.getElementById("animate-container")
let doNotAnimateContainer = document.getElementById("do-not-animate-container")
createScene();

const angles = [0,Math.PI/2, Math.PI,3*Math.PI/2]
const squares = document.getElementsByClassName("square")

var primaryMouseButtonDown = false;

function setPrimaryButtonState(e) {
  var flags = e.buttons !== undefined ? e.buttons : e.which;
  primaryMouseButtonDown = (flags & 1) === 1;
}

function getRotation(elem) {
  const computedStyle = window.getComputedStyle(elem);
  const transform = computedStyle.transform;
  if (!transform || transform === 'none') return 0;
  
  const values = transform.split('(')[1].split(')')[0].split(',');
  const a = parseFloat(values[0]);
  const b = parseFloat(values[1]);
  return Math.atan2(b, a);
}

function getNearestAngle(currentAngle) {
  let nearest = angles[0];
  let minDiff = Infinity;
  
  angles.forEach(angle => {
    let diff = Math.abs(currentAngle - angle);
    diff = Math.min(diff, 2*Math.PI - diff);
    if(diff < minDiff) {
      minDiff = diff;
      nearest = angle;
    }
  });
  
  return nearest;
}

function createScene()
{
  for (var i = 0; i < 4; i++)
  {
    var square = document.createElement("div")
    square.className="square"
    square.classList.add("crossed")
    square.classList.add("prevent-select")
    square.style.transform=`rotate(${i*Math.PI/2}rad)`
    square.innerHTML = i
    animateContainer.appendChild(square)
  }

  for (var i = 0; i < 4; i++)
  {
    var square = document.createElement("div")
    square.className="square"
    square.classList.add("crossed")
    square.classList.add("prevent-select")
    square.style.transform=`rotate(${i*Math.PI/2}rad)`
    square.innerHTML = i
    doNotAnimateContainer.appendChild(square)
    }
}

document.addEventListener("mousedown", (e) => {setPrimaryButtonState(e)});
document.addEventListener("mousemove", (e) => {setPrimaryButtonState(e), handleMouseMove(e)});
document.addEventListener("mouseup", (e) => {setPrimaryButtonState(e), handleMouseUp(e)});

function handleMouseMove(e){
  if(primaryMouseButtonDown === true)
  {
    for (var i = 0; i < squares.length; ++i)
    {
      var square = squares[i];
      square.getAnimations().forEach(anim => anim.cancel());
      
      var oldRotation = getRotation(square);
      newRotation = (oldRotation + e.movementX/100) % (2*Math.PI);
      if(newRotation < 0) newRotation += 2*Math.PI;
      square.style.transform = `rotate(${newRotation}rad)`;
    }
  }
}

function handleMouseUp(e)
{
  for (var i = 0; i < squares.length; ++i)
  {
    var square = squares[i];
    var currentRotation = getRotation(square);
    var nearestRotation = getNearestAngle(currentRotation);
    
    if (square.parentElement.id=="animate-container")
    {
      const animation = square.animate([
        {transform: `rotate(${currentRotation}rad)`},
        {transform: `rotate(${nearestRotation}rad)`}
      ],
      {
        fill: "forwards",
        duration: 1000,
        iterations: 1
      });

      animation.addEventListener("finish", () => {
        console.log("finished-square");
        animation.commitStyles();
      });
    }
    else
    {
        square.style.transform = `rotate(${nearestRotation}rad)`;
    }
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 20:35:08