【JS応用】promise でのアニメーション付きサークル

Promiseを活用したアニメーション付きサークルの実装と制御

Webフロントエンド開発において、アニメーションは単なる視覚的装飾ではなく、ユーザー体験(UX)を向上させるための重要なフィードバックメカニズムです。特に、SVGを用いたサークルアニメーションは、プログレスバーやローディングスピナーなど、多くのUIコンポーネントで活用されます。

本稿では、JavaScriptのPromiseを活用し、アニメーションの「開始」と「終了」を非同期処理として正確に制御する方法を解説します。単に動かすだけでなく、完了を待機し、次のアクションへ繋げるための堅牢な設計手法を紐解きます。

アニメーションとPromiseの親和性

なぜアニメーションにPromiseが必要なのでしょうか。Web Animations API(WAAPI)やCSS Transitionは、本来イベントベースで動作します。しかし、複雑なUIフローでは「アニメーションAが終わったらBを実行し、その次にCを実行する」といったシーケンシャルな制御が頻発します。

Promiseを利用することで、これらのアニメーションを「非同期タスク」として抽象化できます。これにより、コールバック地獄を回避し、async/awaitを用いた直感的なコード記述が可能になります。サークルアニメーションのように、円周を描く進捗率(stroke-dashoffset)を操作する際、完了をPromiseでラップすることで、状態管理が飛躍的に容易になります。

SVGサークルの構造とアニメーションの仕組み

サークルアニメーションの基本は、SVGの「stroke-dasharray」と「stroke-dashoffset」プロパティにあります。

– stroke-dasharray: 線の点線の間隔を制御します。円周の長さと同じ値を設定することで、円全体を隠すことができます。
– stroke-dashoffset: 線の開始位置をずらします。これを円周の長さから0へと変化させることで、円が描かれるようなアニメーションを実現します。

このプロパティをJavaScriptから制御し、アニメーションの終了を検知してPromiseを解決(resolve)させる設計が、今回の核心です。

Promiseを用いたアニメーション制御の実装

以下に、指定したパーセンテージまでサークルを描画し、完了をPromiseで返すクラスの実装例を示します。


class CircleAnimator {
  constructor(element) {
    this.circle = element;
    this.circumference = this.circle.getTotalLength();
    this.circle.style.strokeDasharray = `${this.circumference} ${this.circumference}`;
    this.circle.style.strokeDashoffset = this.circumference;
  }

  animate(progress, duration = 1000) {
    return new Promise((resolve) => {
      const startOffset = parseFloat(this.circle.style.strokeDashoffset);
      const targetOffset = this.circumference - (this.circumference * progress) / 100;
      const startTime = performance.now();

      const step = (currentTime) => {
        const elapsed = currentTime - startTime;
        const fraction = Math.min(elapsed / duration, 1);
        
        // イージング(線形補間)
        const currentOffset = startOffset + (targetOffset - startOffset) * fraction;
        this.circle.style.strokeDashoffset = currentOffset;

        if (fraction < 1) {
          requestAnimationFrame(step);
        } else {
          resolve(); // アニメーション終了時にPromiseを解決
        }
      };

      requestAnimationFrame(step);
    });
  }
}

// 使用例
const circleElement = document.querySelector('.progress-circle');
const animator = new CircleAnimator(circleElement);

async function runSequence() {
  console.log('アニメーション開始');
  await animator.animate(50, 1000);
  console.log('50%で停止');
  await animator.animate(100, 500);
  console.log('100%まで完了');
}

runSequence();

詳細解説:requestAnimationFrameとPromiseの連携

上記のコードでは、`requestAnimationFrame` を使用してフレームごとの更新を行っています。ポイントは以下の通りです。

1. **getTotalLengthの活用**: SVGの円周長を動的に取得することで、要素のサイズ変更に強いコードになります。
2. **時間ベースの制御**: フレームレートに依存しないよう、`performance.now()` を用いて経過時間を計算しています。これにより、PCのスペックに関わらず一定の速度でアニメーションが進行します。
3. **Promiseのラップ**: `requestAnimationFrame` のループが完了したタイミング(fraction >= 1)で `resolve()` を呼び出しています。これにより、外部から `await animator.animate(...)` と記述するだけで、アニメーションの完了を待機できるようになります。

実務における高度な設計と注意点

実務の現場では、単に動かすだけでなく、以下の点に配慮する必要があります。

1. **中断処理(AbortController)**:
アニメーション中に別の操作が行われた場合、即座にアニメーションを停止・破棄する必要があります。`AbortController` を活用し、Promiseを途中で `reject` するか、クリーンアップ関数を用意しておくことが重要です。

2. **イージング関数の導入**:
線形(linear)な動きは機械的で安っぽく見えます。`easeInOutQuad` や `easeOutCubic` などの数学的なイージング関数を `fraction` に適用することで、人間にとって心地よい動きを実現できます。

3. **アクセシビリティの考慮**:
アニメーションが苦手なユーザーのために、`prefers-reduced-motion` メディアクエリを確認しましょう。CSSでアニメーションを無効化するだけでなく、JS側でもアニメーション時間を0にするなどの対応が必要です。

4. **メモリリークの防止**:
`requestAnimationFrame` はブラウザがタブを非アクティブにすると停止します。しかし、Promiseが解決されないままの状態が残るとメモリリークの原因となります。コンポーネントが破棄される際には、必ずアニメーションループを停止させるロジックを組み込んでください。

Promiseチェーンによる複雑なUIフローの構築

Promiseの真価は、複数のアニメーションを連結する際に発揮されます。例えば、「ローディング(0-50%)→ データ取得 → 完了(50-100%)」というフローを以下のように記述できます。


async function loadData() {
  try {
    await animator.animate(50, 1000);
    const data = await fetchData(); // 非同期API通信
    await animator.animate(100, 500);
    showResult(data);
  } catch (error) {
    handleError(error);
  }
}

このように、非同期処理とUIアニメーションを同一のPromiseチェーン上で管理することで、コードの可読性が劇的に向上します。状態変数を大量に用意して `if` 文で分岐させるような古い手法から脱却し、宣言的かつメンテナンス性の高い設計を目指しましょう。

まとめ

Promiseを活用したサークルアニメーションの実装は、現代的なフロントエンド開発における「状態管理」と「非同期制御」の優れたサンプルです。

- SVGのプロパティをJSで操作し、アニメーションの終了をPromiseでラップする。
- `requestAnimationFrame` で滑らかな描画を実現し、時間管理を行う。
- `async/await` を用いて、複雑なUIフローを同期的な記述で分かりやすく構成する。

これらの技術を組み合わせることで、ユーザーに対して直感的で洗練されたインターフェースを提供できます。単なる「動く円」ではなく、アプリケーションのロジックと密接に連携した、プロフェッショナルなUIコンポーネントを構築してください。アニメーションはユーザーとシステムを繋ぐ対話の手段です。その対話のテンポをPromiseで正確に制御することが、優れたフロントエンドエンジニアへの第一歩となるはずです。

コメント

タイトルとURLをコピーしました