JS Web Animations API实现旋转缓动时出现异常行为
拖动旋转元素后Web Animations API缓动归位导致后续旋转错位问题
问题描述
实现拖动旋转HTML元素功能,预期行为:
- 按下鼠标触发
mousedown事件 - 按住鼠标移动时触发
mousemove,沿X轴移动控制元素正反旋转 - 释放鼠标触发
mouseup,元素缓动归位到预设角度(0、π/2、π、3π/2弧度)
直接设置transform:rotate时所有行为正常,但使用Web Animations API实现缓动归位后,首次操作正常,后续拖动旋转会出现元素错位;下方硬归位的元素则无此问题。
问题根源
- 旋转值读取逻辑错误:原
getRotation函数仅从元素的style.transform中读取旋转值,但Web Animations API运行时,元素的实际旋转状态由动画控制,style.transform不会实时更新,导致后续拖动时读取的是旧值,计算新角度时出错。 - 字符串截取漏洞:原
getRotation通过固定长度截取旋转数值,当数值小数位数变化时,会错误截断数值,导致角度读取不准确。 - 未处理动画冲突:拖动时未取消元素上正在运行的动画,动画状态和手动设置的
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
相关产品推荐
相关产品推荐

