自定义弹簧鼠标光标:CSS变换顺序异常问题求助
自定义弹簧阻尼鼠标光标变换顺序异常排查
问题现象
我制作了一个带弹簧(spring)和阻尼(damping)效果的圆形自定义鼠标光标,支持以下功能:
- 跟随鼠标移动
- 根据鼠标速度实现拉伸挤压效果
- 基于鼠标速度方向的旋转效果
所有功能单独使用均正常,但组合使用时出现两种异常:
- 若变换顺序为
circle.style.transform =${translateElement} ${scaleElement} ${rotateElement};:功能可运行,但拉伸挤压是在旋转后应用于原坐标轴,视觉上始终上下挤压、左右拉伸,无法实现朝向鼠标移动方向拉伸的预期效果。 - 若变换顺序改为
circle.style.transform =${translateElement} ${rotateElement} ${scaleElement};:圆形光标完全不移动也不缩放,且控制台无报错。
已确认旋转功能本身生效,问题出在第二种变换顺序的后续步骤中。
问题根源
- 语法错误导致transform失效:
rotateElement的代码写法遗漏了闭合括号,rotate(${angle}deg应为rotate(${angle}deg),浏览器无法解析错误的transform语法,直接忽略了整个变换规则。 - 变换原点偏移:元素默认的
transform-origin是左上角,旋转后再缩放会导致元素偏离目标位置,即便语法修复后也会出现视觉错位。 - 初始transform冲突:CSS中默认设置了
transform: rotate(45deg);,会与JS中动态设置的transform叠加,导致变换逻辑混乱。
修复方案
1. 修正JS中的rotate语法错误
将rotateElement的定义改为:
const rotateElement = `rotate(${angle}deg)`;
2. 调整CSS的变换原点与初始transform
给.circle添加变换原点,并移除默认旋转:
.circle { --circle-size: 30px; position: fixed; top: calc(var(--circle-size) / 2 * -1); left: calc(var(--circle-size) / 2 * -1); pointer-events: none; box-sizing: border-box; width: var(--circle-size); height: var(--circle-size); background-color: #c1bb98; mix-blend-mode: difference; border-radius: 100%; transform-origin: center; /* 添加变换原点为中心 */ /* 移除默认的rotate(45deg) */ transition: opacity 0.15s ease-out; }
3. 确认正确的变换顺序
使用translate → rotate → scale的顺序,这样缩放会基于旋转后的局部坐标轴,实现朝向鼠标移动方向拉伸的效果。
修复后的完整代码
JavaScript
const circle = document.querySelector('.circle'); const mousePos = { x: 0, y: 0 }; const prevMousePos = { x: 0, y: 0 }; let mouseSpeed = 0; const circlePos = { x: 0, y: 0 }; const circleVelocity = { x: 0, y: 0 }; const spring = 0.025; const damping = 0.845; const maxSqueeze = 0.5; const maxStretch = 1.5; const maxMouseSpeed = 5; let lowSpeedStartTime = 0; let lowSpeedDuration = 0; const lowSpeedThreshold = 0.5; // 0.5 seconds document.addEventListener('mousemove', (e) => { const currentMousePos = { x: e.clientX, y: e.clientY }; const dx = currentMousePos.x - prevMousePos.x; const dy = currentMousePos.y - prevMousePos.y; // Calculate the speed as the magnitude of the velocity vector mouseSpeed = Math.sqrt(dx * dx + dy * dy); // Update previous mouse position prevMousePos.x = currentMousePos.x; prevMousePos.y = currentMousePos.y; // Update the current mouse position mousePos.x = currentMousePos.x; mousePos.y = currentMousePos.y; }); function animate() { const ax = (mousePos.x - circlePos.x) * spring; const ay = (mousePos.y - circlePos.y) * spring; circleVelocity.x += ax; circleVelocity.y += ay; circleVelocity.x *= damping; circleVelocity.y *= damping; circlePos.x += circleVelocity.x; circlePos.y += circleVelocity.y; // calculate rotation angle const angle = Math.atan2(circleVelocity.y, circleVelocity.x) * 180 / Math.PI; // Calculate the normalized mouse speed const normalizedSpeed = Math.min(mouseSpeed, maxMouseSpeed) / maxMouseSpeed; // Calculate stretch and squeeze factors const scaleFactor = 1 + (maxStretch - 1) * normalizedSpeed; const squeezeFactor = 1 - (1 - maxSqueeze) * normalizedSpeed; if (mouseSpeed <= 1) { if (lowSpeedStartTime === 0) { lowSpeedStartTime = performance.now(); } lowSpeedDuration = (performance.now() - lowSpeedStartTime) / 1000; // in seconds } else { lowSpeedStartTime = 0; lowSpeedDuration = 0; } // Apply squeeze/stretch effect let scaleX = scaleFactor; // Stretch let scaleY = squeezeFactor; // Squeeze if (lowSpeedDuration > lowSpeedThreshold) { scaleX = 1.0; scaleY = 1.0; } const translateElement = `translate(${circlePos.x}px, ${circlePos.y}px)`; const rotateElement = `rotate(${angle}deg)`; // 修复语法错误 const scaleElement = `scale(${scaleX}, ${scaleY})`; circle.style.transform = `${translateElement} ${rotateElement} ${scaleElement}`; requestAnimationFrame(animate); } animate(); document.addEventListener('mouseleave', () => { circle.style.opacity = '0'; // Make cursor disappear }); document.addEventListener('mouseenter', () => { circle.style.opacity = '1'; // Make cursor reappear when mouse enters the document again });
CSS
body { background-color: #1d1f22; } .circle { --circle-size: 30px; position: fixed; top: calc(var(--circle-size) / 2 * -1); left: calc(var(--circle-size) / 2 * -1); pointer-events: none; box-sizing: border-box; width: var(--circle-size); height: var(--circle-size); background-color: #c1bb98; mix-blend-mode: difference; border-radius: 100%; transform-origin: center; transition: opacity 0.15s ease-out; }
HTML
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="style.css"> <title>Document</title> </head> <body> <div class="circle"></div> <script src="script.js"></script> </body> </html>
内容的提问来源于stack exchange,提问作者Tughlok Siddiquee
相关产品推荐
相关产品推荐

