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で正確に制御することが、優れたフロントエンドエンジニアへの第一歩となるはずです。

コメント