Reactで円形のプログレスバーを実装🎅

この記事は[ファンタアドベントカレンダー2023](https://qiita.com/advent-calendar/2023/funteractive-inc)の10日目です。

今回はReactで矢印がぐるっと回るような円形のプログレスバーの作り方を紹介します。

この記事で作るのはこちら
<img width="500" alt="progress.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54771/b0769142-1331-96b6-7c76-1ae0cd548c60.gif">
## 概要

円形のプログレスバーの作り方は調べると色々出てきますがここではSVGを使って実装していきます。SVGのパーツとしては円を描画している`<circle>`と矢印の先端の[＞]を描画している`<path>`に分けることができます。

バーの進捗は円の破線をコントロールする`strokeDasharray`と`strokeDashoffset`を使って表現しています。

## 環境

- React
- Tailwind

## 円の配置

このコードでは各種定数の用意と、`<circle>`タグで円を描画しています。`progressPercent`に0~100を指定することでその割合で描画できるようにしています。

```tsx
export const Progress = () => {
  // SVGの描画サイズ
  const size = 120;
  // 現在の進捗
  const progressPercent = 80;
  // 円の半径
  const radius = 50;
  // 円周
  const circumference = 2 * Math.PI * radius;
  // 表示割合
  const strokeDashoffset = circumference - (progressPercent / 100) * circumference;
  return (
    <div className="w-full h-full">
      <svg
        viewBox={`0 0 ${size} ${size}`}
        style={{ transform: "rotate(-90deg)" }} // そのままだと3時の方向が起点になってしまうので-90°回転させてます
      >
        <circle
          r={radius}
          cx={size / 2}
          cy={size / 2}
          stroke="#4169e1"
          strokeWidth="5"
          fill="#F6FBF6"
          strokeLinecap="round"
          strokeDasharray={circumference}
          strokeDashoffset={strokeDashoffset}
        />
      </svg>
    </div>
  );
};
```

このコンポーネントを呼び出すことで以下の円が描画できます。背景色やサイズは親要素で設定しています。

```tsx
<div className="h-[512px] w-[512px] bg-[#65CFEA]">
    <Progress />
 </div>
 ```
 <img width="400" alt="circle.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54771/18c71e32-ecad-81ab-fd1c-ce1c749749b8.png">


## 矢印の先端の配置

続いて`<path>`を使って矢印の先端を作っていきます。描画の詳しい記述方法はドキュメントにあります。

https://developer.mozilla.org/ja/docs/Web/SVG/Attribute/d

まず、d属性に描画する図形の座標を記述します。そして`transformOrigin`で回転するときの原点をviewBoxの中心に設定し、`transform`で回転させています。`progressPercent`は0~100の値を取るので3.6を掛けて360°回転できるようにし、svgタグで指定している`rotate(-90deg)`を打ち消すために90を加算しています。

```tsx
<path
  d="M 55,5 61,10 55,15"
  fillOpacity={0}
  strokeWidth={4}
  stroke="#4169e1"
  strokeLinecap="round"
  strokeLinejoin="round"
  style={{
    transform: `rotate(${progressPercent * 3.6 + 90}deg)`,
    transformOrigin: `${size / 2}px ${size / 2}px`,
  }}
/>
```

この`<path>`を`<svg>`内に入れることで矢印がぐるっと回っているような表現ができるようになります。続いてアニメーションを追加していきます。

 <img width="400" alt="arrow.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54771/5d2f3f23-77b3-8fba-5116-8ee9204188b6.png">

## アニメーションの設定

アニメーションは`@keyframes`であらかじめ定義し、`<circle>`と`<path>`から呼び出す流れで行います。

https://developer.mozilla.org/ja/docs/Web/CSS/@keyframes

circleStrokeのfrom(初期値)では`stroke-dashoffset`に円周を渡すことで線の描画を非表示にしています。その後to(アニメーション後)で表示割合である`strokeDashoffset`を渡しています。

rotationObjectも同様にfromに`<path>`の初期位置である`rotate(90deg)`を、toに`rotate(${progressPercent * 3.6 + 90}deg)`を設定しています。

keyframesの設定を終えたら`<circle>`と`<path>`にanimationを指定します。animationは `使いたいkeyframesの名前 | 再生時間 | イージングの指定 | アニメーション後の状態の指定`　を設定しています。

```tsx
return (
  <>
    <style>
      {`@keyframes circleStroke {
        from {
          stroke-dashoffset: ${circumference};
        }
        to {
          stroke-dashoffset: ${strokeDashoffset};
        }
      }
      @keyframes rotationObject {
        from {
          transform: rotate(90deg);
        }
        to {
          transform: rotate(${progressPercent * 3.6 + 90}deg)
        }
      }
      `}
    </style>
    <div className="w-full h-full">
      <svg
        viewBox={`0 0 ${size} ${size}`}
        style={{ transform: "rotate(-90deg)" }}
      >
        <circle
          r={radius}
          cx={size / 2}
          cy={size / 2}
          stroke="#4169e1"
          strokeWidth="5"
          fill="#F6FBF6"
          strokeLinecap="round"
          strokeDasharray={circumference}
          style={{
            animation: "circleStroke 3s ease forwards",
          }}
        />
        <path
          d="M 55,5 61,10 55,15"
          fillOpacity={0}
          strokeWidth={4}
          stroke="#4169e1"
          strokeLinecap="round"
          strokeLinejoin="round"
          style={{
            animation: "rotationObject 3s ease forwards",
            transformOrigin: `${size / 2}px ${size / 2}px`,
          }}
        />
      </svg>
    </div>
  </>
);
```

これで冒頭のプログレスバーのアニメーションが完成しました🎉

このプログレスバーの作りを応用することで任意の画像を一緒に回転させることもできます。

最後におまけとして見た目をクリスマスぽくして終わりにします。

## おまけ：サンタさんを走らせる

`<path>`を消して画像を配置していきます。画像はSVGの外の要素なので`@keyframes`でのrotateの起点を0にしています。また、画像の座標はviewBoxのサイズではなく親要素で指定しているwidthとheightに依存しています。

```tsx
return (
  <>
    <style>
      {`@keyframes circleStroke {
        from {
          stroke-dashoffset: ${circumference};
        }
        to {
          stroke-dashoffset: ${strokeDashoffset};
        }
      }
      @keyframes rotationObject {
        from {
          transform: rotate(0deg);
        }
        to {
          transform: rotate(${progressPercent * 3.6}deg)
        }
      }
      `}
    </style>
    {/* relativeを追加 */}
    <div className="relative w-full h-full">
      <svg
        viewBox={`0 0 ${size} ${size}`}
        style={{ transform: "rotate(-90deg)" }}
      >
        <circle
          r={radius}
          cx={size / 2}
          cy={size / 2}
          stroke="#FAF3BE"
          strokeWidth="5"
          fill="#B23333"
          strokeLinecap="round"
          strokeDasharray={circumference}
          style={{
            animation: "circleStroke 3s ease forwards",
          }}
        />
      </svg>
      {/* 回転する画像 */}
      <div
        className="absolute top-0 left-1/2"
        style={{
          translate: "-50% -50%",
          transformOrigin: `center ${256 + 126 / 2}px`, // 回転する座標の起点
          animation: "rotationObject 3s ease forwards",
        }}
      >
        <Image src="/santa.png" alt="santa" width="256" height="126" />
      </div>
      <div
        className="absolute top-1/2 left-1/2 text-4xl font-copperplate text-white italic whitespace-nowrap"
        style={{
          translate: "-50% -50%",
        }}
      >
        Merry Christmas
      </div>
    </div>
  </>
);
```

そして親要素でも画像の配置などの調整をして完成です🎅

![santa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54771/0b5d1943-cfb6-0a88-ef0f-5eb28ed20d12.gif)

みなさま素敵なクリスマスを！🎄

明日は優しく頼りになるデザイナーの花牟禮さんの記事です！お楽しみに！