在 React 中滚动时沿 SVG 路径设置动画元素

Animate element along SVG path on scroll in React

在 vanilla Javascript 中有相对简单的方法来完成此操作(请参阅 了解其中一种方法),但我正在努力让类似的东西在 React 中工作,尤其是使用像 Framer Motion 这样的动画库。

Framer Motion 的 useViewPortScroll returns 一个方便的 scrollYProgress 对象,其“当前”属性 告诉您用户当前向下滚动页面的百分比。

我想用这个 属性 做这样的事情:const pts = path.getPointAtLength(scrollPercentage * pathLength);,然后使用 pts.xpts.y 作为 x 和 y 的属性,比如说,一个圆圈 SVG - 所以每次我向下(或向上)滚动页面时,圆圈的位置将 update/animate 沿着 SVG 路径。

我正在努力让它与 React 更具声明性的风格一起工作,因为我必须对圆和路径元素都使用 refs,这意味着我必须放置上述 pts = path.getPointAtLength... 代码在 useEffect 调用的 内部,两个引用都作为依赖项(否则引用将未定义,在这种情况下,x 和 y 上的 pts.xpts.y 属性我的圈子 SVG 在第一次渲染时将无法访问。

有没有人解决过类似的问题或者可以提供指导?

对于这样一个简单的动画(圆点 旋转 )你可以使用简单的 transform: rotate():

const { useState } = React,
      { render } = ReactDOM,
      rootNode = document.getElementById('root')
      
const ScrollMeter = ({progress=0}) => (
    <svg 
      className="meter"
      style={{transform:`rotate(${360*progress}deg)`}} 
      viewBox="0 0 100 100" 
      xmlns="http://www.w3.org/2000/svg"
    >
      <circle 
        cx="50" 
        cy="50" 
        r="40" 
        fill="none" 
        stroke="gray"
      />
      <circle 
        cx="90"
        cy="50"
        r="10" 
        fill="gray" 
        stroke="white"
      />
    </svg>
) 

const App = () => {
  const [scrollProgress, setScrollProgress] = useState(0),
    onScroll = ({ target: { scrollTop, scrollHeight } }) =>
      setScrollProgress(scrollTop / scrollHeight)
  return (
    <div>
      <ScrollMeter progress={scrollProgress} />
      <div 
        className="scrollArea" 
        onScroll={onScroll}
      >
        { 'there should be some random content here '.repeat(1e3)}
      </div>
    </div>
  )
}

render(<App />, rootNode)
.scrollArea {
  width: 100%;
  height: 200px;
  overflow: auto;
}

.meter {
  width: 100px;
  display: block;
  position: -webkit-sticky; /* Safari */
  position: sticky;
  top: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>

但是,如果您需要更通用的东西(例如,对于任意形状的轨迹),您可以使用与 post 中相同的方法,您指的是:

const { useState, useRef } = React,
      { render } = ReactDOM,
      rootNode = document.getElementById('root')
      
const ScrollMeter = ({progress=0}) => {
  const route = useRef(),
        routeLength = (route.current && route.current.getTotalLength()),
        {x,y} = (route.current ? route.current.getPointAtLength(routeLength*progress) : {x: 90, y:50})
  return (
    <svg className="meter" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <circle 
        ref={route}
        cx="50" 
        cy="50" 
        r="40" 
        fill="none" 
        stroke="gray"
      />
      <circle 
        cx={x}
        cy={y}
        r="10" 
        fill="gray" 
        stroke="white"
      />
    </svg>
  )
}     

const App = () => {
  const [scrollProgress, setScrollProgress] = useState(0),
    onScroll = ({ target: { scrollTop, scrollHeight } }) =>
      setScrollProgress(scrollTop / scrollHeight)
  return (
    <div>
      <ScrollMeter progress={scrollProgress} />
      <div 
        className="scrollArea" 
        onScroll={onScroll}
      >
        { 'there should be some random content here '.repeat(1e3)}
      </div>
    </div>
  )
}

render(<App />, rootNode)
.scrollArea {
  width: 100%;
  height: 200px;
  overflow: auto;
}

.meter {
  width: 100px;
  display: block;
  position: -webkit-sticky; /* Safari */
  position: sticky;
  top: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>