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

React中如何实现元素跟随鼠标的平滑缓动滑动效果?

实现图片鼠标跟随的平滑滑动效果

问题背景

现有React组件实现了两个带图片的面板,支持滚轮缩放,缩放后图片会跟随鼠标移动。但尝试用CSS transition实现平滑跟随效果时,画面卡顿,只有鼠标停止时变换才会更新,需要实现带有轻微滞后或动态速度的平滑滑动效果。

解决方案一:用requestAnimationFrame做渐进式过渡

核心思路是拆分目标状态和当前过渡状态,通过逐帧计算的方式让当前位置逼近目标位置,避免React状态频繁更新导致的卡顿。

修改后的完整代码:

import React, { useState, useRef, useEffect } from 'react'
import './App.css'
import PaintingPanel, {PaintingPanelProps, PaintingPosition} from './PaintingPanel';

const App: React.FC = () => {
  // 存储最终目标位置,仅在鼠标/滚轮操作时更新
  const [targetPosition, setTargetPosition] = useState<PaintingPosition>({
    zoomRatio: 1,
    xFraction: 0,
    yFraction: 0
  })
  // 存储当前过渡中的位置,不触发组件重渲染
  const currentPosition = useRef<PaintingPosition>({...targetPosition})
  const animationFrameRef = useRef<number>()

  const paintingName = 'test';

  const zoomPainting = (e: WheelEvent) => {
    const minZoomRatio = 1.0;
    const maxZoomRatio = 2.4;
    
    const newTargetPosition = {...targetPosition}
    const nextZoomRatio = newTargetPosition.zoomRatio + -e.deltaY/1000;
    newTargetPosition.zoomRatio = Math.min(Math.max(nextZoomRatio, minZoomRatio), maxZoomRatio)
    setTargetPosition(newTargetPosition)
  }

  function createFollowMouseOnPainting(panelId: string) {
    return (e: MouseEvent) => {
      const paintingPanel: HTMLDivElement = document.getElementById(panelId);
      const rect = paintingPanel.getBoundingClientRect();
      const targetWidth = paintingPanel.clientWidth;
      const targetHeight = paintingPanel.clientHeight;

      setTargetPosition(prev => ({
        ...prev,
        xFraction: (e.clientX - rect.left)/targetWidth,
        yFraction: (e.clientY - rect.top)/targetHeight
      }))
    }
  }

  // 逐帧更新过渡位置,逼近目标值
  useEffect(() => {
    const smoothTransition = () => {
      // 平滑系数:值越小,滞后感越强,平滑效果越明显(建议0.1~0.2)
      const smoothFactor = 0.15;

      currentPosition.current.xFraction += (targetPosition.xFraction - currentPosition.current.xFraction) * smoothFactor;
      currentPosition.current.yFraction += (targetPosition.yFraction - currentPosition.current.yFraction) * smoothFactor;
      currentPosition.current.zoomRatio += (targetPosition.zoomRatio - currentPosition.current.zoomRatio) * smoothFactor;

      animationFrameRef.current = requestAnimationFrame(smoothTransition);
    }

    animationFrameRef.current = requestAnimationFrame(smoothTransition);

    // 组件卸载时取消动画帧
    return () => {
      animationFrameRef.current && cancelAnimationFrame(animationFrameRef.current);
    }
  }, [targetPosition])

  // 根据当前过渡位置计算变换值
  const xTransform = 100*((0.5 - currentPosition.current.xFraction) * (1-(1/currentPosition.current.zoomRatio)));
  const yTransform = 100*((0.5 - currentPosition.current.yFraction) * (1-(1/currentPosition.current.zoomRatio)));

  const paintingImgStyle: React.CSSProperties = {
    transform: `scale(${currentPosition.current.zoomRatio}) translateX(${xTransform}%) translateY(${yTransform}%)`,
    willChange: 'transform', // 提示浏览器提前优化动画
  }
  
  const panelOneProps: PaintingPanelProps = {
    paintingName: paintingName,
    isDiff: false,
    paintingImgStyle,
    zoomPainting,
    createFollowMouseOnPainting
  }

  const panelTwoProps: PaintingPanelProps = {
    paintingName: paintingName,
    isDiff: true,
    paintingImgStyle,
    zoomPainting,
    createFollowMouseOnPainting
  }

  return (
    <div id='panel-container'>
      <PaintingPanel {...panelOneProps} />
      <PaintingPanel {...panelTwoProps} />
    </div>
  )
}

export default App

关键细节

  • 用targetPosition维护最终目标状态,仅在用户操作时更新,减少重渲染频率
  • currentPosition通过useRef存储,更新时不会触发组件重渲染,避免React状态更新带来的额外开销
  • 调整smoothFactor可以自定义平滑程度,数值越大跟随速度越快,越小则滞后感越强

解决方案二:优化CSS Transition(针对原卡顿问题修复)

如果想保留CSS过渡的实现方式,需要解决状态更新不连续的问题:

  1. 用useMemo缓存样式,避免不必要的重计算
  2. 使用函数式更新获取最新状态,保证过渡连续性
  3. 明确指定过渡属性和时长

修改后的关键代码片段:

import { useMemo } from 'react'

// ...其他代码不变

// 用useMemo缓存样式,仅依赖变化的参数
const paintingImgStyle: React.CSSProperties = useMemo(() => ({
  transform: `scale(${paintingPosition.zoomRatio}) translateX(${xTransform}%) translateY(${yTransform}%)`,
  transition: 'transform 0.1s ease-out', // 指定过渡属性和时长
  willChange: 'transform',
}), [xTransform, yTransform, paintingPosition.zoomRatio])

// 鼠标事件处理改为函数式更新
function createFollowMouseOnPainting(panelId: string) {
  return (e: MouseEvent) => {
    const paintingPanel: HTMLDivElement = document.getElementById(panelId);
    const rect = paintingPanel.getBoundingClientRect();
    const targetWidth = paintingPanel.clientWidth;
    const targetHeight = paintingPanel.clientHeight;

    const xFraction = (e.clientX - rect.left)/targetWidth;
    const yFraction = (e.clientY - rect.top)/targetHeight;

    setPaintingPosition(prev => ({
      ...prev,
      xFraction,
      yFraction
    }))
  }
}

说明

这种方式实现简单,但平滑效果的可控性不如requestAnimationFrame方案,如果还是存在卡顿,优先选择第一种方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 20:45:36