가수면

svg 스크롤 애니메이션 (선 그리기) 본문

일지

svg 스크롤 애니메이션 (선 그리기)

니비앙 2023. 10. 11. 20:05

들어가기 전 필요한 기본 지식

strokeDasharray - 점선의 길이와 간격 (길이와 간격을 각각 전체 선 길이로 설정할 경우 선이 감춰진다.)

strokeDashoffset - 선이 시작되는 위치 (선의 전체 길이 = 선의 시작점, 0 = 끝점 )

원리는 다음과 같다.

1. svg를 숨김

2. svg가 보여지는 순간을 0, svg가 완전히 다 지나면 1이 되는 비율값을 구한다.

3. strokeDashoffset에 비율값을 적용해 스크롤을 내릴 때마다 마치 선이 그려지는 듯한 효과를 낸다.

구현 과정

1. ref 설정

svg가 그려지는 구역의 ref와 svg 길이의 ref를 저장한다.

interface Props {
  containerRef: MutableRefObject<HTMLDivElement | null>;
}

const Curve = ({ containerRef }: Props) => {
  const pathRef = useRef<SVGPathElement | null>(null);
.
.
.
<svg
      width="1920"
      height="844"
      viewBox="0 0 1920 844"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        ref={pathRef}
        d="M19...."
        stroke="url(#paint0_linear_14_46)"
        strokeWidth="20"
      />
    </svg>
  );
};

export default Curve;

2. svg 전체 선의 길이를 구한다.

  const [length, setLength] = useState<number>(0);

  useEffect(() => {
    if (pathRef.current) {
      const pathLength = pathRef.current.getTotalLength();
      setLength(pathLength);
    }
  }, []);

3. svg 선을 숨긴다.

strokeDasharray의 점선 길이와 점선 간격을 전체 길이로 설정해 감춰준다.

  useEffect(() => {
    if (pathRef.current) {
      const pathLength = pathRef.current.getTotalLength();
      setLength(pathLength);
      pathRef.current.style.strokeDasharray = `${pathLength} ${pathLength}`;
    }
  }, []);

4. 스크롤이 될 때마다 선의 시작점을 새로이 설정하는 로직을 작성한다.

스크롤 될 때마다 선의 시작점이 계산되어 마치 선이 그려지는 듯한 효과를 내게된다.

  useEffect(() => {
    const handleScroll = () => {
      if (pathRef.current) {
        pathRef.current.style.strokeDashoffset = `${계산된 값}`;
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [length]);

5. `${계산된 값}`에 들어갈 로직을 작성한다.

svg가 보여지는 순간을 0, svg가 완전히 다 지나면 1이 되는 비율값을 구해야한다.

콘테이너 전체 높이에서 현재 보여지고 있는 콘테이너가 어느정도 인지에 대한 비율을 구하면 된다.

  const calcDashoffset = () => {
    const scrolledHeight = window.scrollY + window.innerHeight * 0.8; // 보고 있는 높이의 80%가 됐을 때 선이 그려지는 것으로 설정
    if (containerRef.current) {
      const ratio =
        (scrolledHeight - containerRef.current.offsetTop) /
        containerRef.current.offsetHeight;
    }
  };

6. 비율이 계산된 값 구하기

전체 길이값에서 점점 줄어들도록 계산하면 된다.

  const calcDashoffset = () => {
    const scrolledHeight = window.scrollY + window.innerHeight * 0.8;
    if (containerRef.current) {
      const ratio =
        (scrolledHeight - containerRef.current.offsetTop) /
        containerRef.current.offsetHeight;
      const value = length - length * ratio;
      return value < 0 ? 0 : value > length ? length : value;
    }
  };

최종코드

import { useRef, useEffect, useState } from "react";

interface Props {
  containerRef: React.MutableRefObject<HTMLDivElement | null>;
}

const Curve = ({ containerRef }: Props) => {
  const pathRef = useRef<SVGPathElement | null>(null);
  const [length, setLength] = useState<number>(0);

  const calcDashoffset = () => {
    const scrolledHeight = window.scrollY + window.innerHeight * 0.8;
    if (containerRef.current) {
      const ratio =
        (scrolledHeight - containerRef.current.offsetTop) /
        containerRef.current.offsetHeight;
      const value = length - length * ratio;
      return value < 0 ? 0 : value > length ? length : value;
    }
  };

  useEffect(() => {
    if (pathRef.current) {
      const pathLength = pathRef.current.getTotalLength();
      setLength(pathLength);
      pathRef.current.style.strokeDasharray = `${pathLength} ${pathLength}`;
    }
  }, []);

  useEffect(() => {
    const handleScroll = () => {
      if (pathRef.current) {
        pathRef.current.style.strokeDashoffset = `${calcDashoffset()}`;
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [length]);

  return (
    <svg
      width="1920"
      height="844"
      viewBox="0 0 1920 844"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        ref={pathRef}
        d="M1920.14 10C1086.82 52.4154 684.091 354.064 -5.36523 268.5C-5.36523 268.5 -212.239 233.476 -137.865 346C-63.492 458.524 -432.231 328.5 -5.36526 482.5C421.5 636.5 1310.2 389.243 1920.14 835.5"
        stroke="url(#paint0_linear_14_46)"
        strokeWidth="20"
      />
      <defs>
        <linearGradient
          id="paint0_linear_14_46"
          x1="883.068"
          y1="10"
          x2="883.068"
          y2="835.5"
          gradientUnits="userSpaceOnUse"
        >
          <stop stopColor="#70FFD4" />
          <stop offset="0.473958" stopColor="#2EB7E1" />
          <stop offset="1" stopColor="#93FEFE" />
        </linearGradient>
      </defs>
    </svg>
  );
};

export default Curve;
Comments