LoginSignup
1
1

この記事の概要

以下の記事を書きました。

素朴な HTML と JavaScript だけで実現したのですが、実際に使うことを考えると、もう少し再利用性を高めたいです。

というわけで、React コンポーネントとして実装し直し、それも記事にしました。

この記事は、上記の記事を読んでいるという前提で進めます。

完成形

最初に、完成形を載せておきます。

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

type PathPair = {
  beforePath: string;
  afterPath: string;
  fill?: string;
}

type MorphingSVGProps = {
  paths: PathPair[];
  trigger: boolean;
  width?: number;
  height?: number;
}

export function MorphingSVG({
  paths,
  trigger,
  width,
  height,
}: MorphingSVGProps) {
  const pathRefs = useRef<(SVGPathElement | null)[]>([]);

  const lerp = useCallback((start: number, end: number, t: number): number => {
    return start + (end - start) * t;
  }, []);

  const parsePath = useCallback((path: string): number[] => {
    return path.match(/[-+]?[0-9]*\.?[0-9]+/g)?.map(Number) || [];
  }, []);

  const generatePathString = useCallback(
    (commands: { cmd: string; length: number }[], values: number[]): string => {
      let valueIndex = 0;
      return commands.reduce((pathString, command) => {
        const commandValues = values.slice(
          valueIndex,
          valueIndex + command.length
        );
        valueIndex += command.length;
        return pathString + command.cmd + commandValues.join(" ");
      }, "");
    },
    []
  );

  const morphPaths = useCallback(
    (path1: string, path2: string, t: number): string => {
      const commands =
        path1.match(/[a-zA-Z][^a-zA-Z]*/g)?.map((cmd) => {
          const numbers = cmd.slice(1).match(/[-+]?[0-9]*\.?[0-9]+/g) || [];
          return { cmd: cmd[0], length: numbers.length };
        }) || [];

      const values1 = parsePath(path1);
      const values2 = parsePath(path2);

      const morphedValues = values1.map((val, i) => lerp(val, values2[i], t));

      return generatePathString(commands, morphedValues);
    },
    [parsePath, generatePathString, lerp]
  );

  useEffect(() => {
    if (trigger) {
      let t = 0;

      const animate = () => {
        t += 0.02;
        if (t > 1) t = 1;
        paths.forEach((pathPair, index) => {
          const morphedPath = morphPaths(
            pathPair.beforePath,
            pathPair.afterPath,
            t
          );
          if (pathRefs.current[index]) {
            pathRefs.current[index]!.setAttribute("d", morphedPath);
          }
        });
        if (t < 1) {
          requestAnimationFrame(animate);
        }
      };

      requestAnimationFrame(animate);
    }
  }, [trigger, paths, morphPaths]);

  return (
    <svg
      width={width ? width : 100}
      height={height ? height : 100}
      viewBox={`0 0 ${width ? width : 100} ${height ? height : 100}`}
      xmlns="http://www.w3.org/2000/svg"
    >
      {paths.map((pathPair, index) => (
        <path
          key={index}
          ref={(el) => (pathRefs.current[index] = el)}
          d={pathPair.beforePath}
          fill={pathPair.fill ? pathPair.fill : "#000"}
        />
      ))}
    </svg>
  );
}

次のように利用します。

import { useState } from "react";
import { MorphingSVG } from "./components/MorphingSVG";

function App() {
  const [trigger, setTrigger] = useState(false);

  const paths = [
    {
      beforePath: "M50 50L150 50L150 150L50 150Z",
      afterPath: "M115 50L180 115L115 180L50 115Z",
      fill: "#f00",
    },
    {
      beforePath: "M49.6025 13L92.9038 88L49.6025 88L6.30127 88L49.6025 13Z",
      afterPath: "M88 12L88 88L12 88L12 12L88 12Z",
      fill: "#00f",
    },
  ];

  return (
    <div style={{ display: "grid", justifyItems: "center" }}>
      <h1>Self-made SVG Morphing</h1>
      <MorphingSVG
        paths={paths}
        trigger={trigger}
        width={200}
        height={200}
      />
      <button onClick={() => setTrigger(true)}>Morph</button>
    </div>
  );
}

export default App;

前の記事の内容から変わったこと

  • 複数のpathを指定できるようになった
    • それぞれのパスの色も変えられる
  • 変形タイミングが操作しやすくなった
    • triggertrueが渡ったタイミングで変形する
    • 上に書いたようにonClickを起点にするも良し、他のフックと同じタイミングで変形するも良し
  • svg 要素の大きさを変えられるようになった

説明

PathPairの形式でpropsを受け取れるようにすることで、複数のパスが一度に変形できています。
また、個別にfillも設定できるようになっています。

また、propsとしてwidthheightも用意したので、svg 要素そのものの大きさも変えることができます。

これにより、画面内に複数の svg を配置し、それぞれを動かしたいときでも楽に扱えるようになりました

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1