markdown

はじめに (対象読者・この記事でわかること)

この記事は、フロントエンド開発に携わるエンジニアや、Webページ上でアニメーションを実装したいと考えている方を対象にしています。特に、複数の <div> 要素を同じスピードで順次位置変更した際に、予期せぬ隙間ができてしまう現象に悩んでいる方に最適です。この記事を読み終えると、隙間が発生する根本的な理由と、requestAnimationFrame と CSS の transitiontransform を組み合わせた滑らかな実装パターンが理解でき、実際に自分のプロジェクトにすぐ適用できるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。
- HTML と CSS の基本的な構造・プロパティ
- JavaScript(ES6+)の変数宣言、関数、addEventListener の使い方

複数要素を同時に動かすときに起きやすい問題と基本的な対策

Webページ上で 「同じ速度で順次位置を変える」 という操作は、見た目を統一感のあるアニメーションにするためにしばしば利用されます。しかし、単純に setIntervallefttop プロパティを書き換えるだけだと、要素間に 不均一な隙間 が生じることがあります。主な原因は次の三点です。

  1. ブラウザの再描画タイミングの差
    setInterval は一定の時間間隔でコールバックを呼び出しますが、実際の描画はブラウザのリフレッシュレート(多くは 60Hz)に合わせて行われます。CPU が忙しいと setInterval の呼び出しが遅れ、特定の <div> が他の要素よりも遅れて描画され、結果として隙間ができやすくなります。

  2. レイアウト計算によるオーバーヘッド
    left/top を変更するとレイアウト(reflow)とペイントが走ります。要素数が増えると計算コストが上がり、フレームごとの処理時間が不均一になるため、途中で位置がずれるケースがあります。

  3. 数値の累積誤差
    ピクセル単位で位置を少しずつ増減させる際、浮動小数点演算の丸め誤差が累積し、予定した座標と実際の座標が微妙にずれることがあります。特に parseIntMath.floor で切り捨てた場合、予期せぬギャップが発生しやすいです。

基本的な解決アプローチ

  • requestAnimationFrame の利用
    ブラウザの描画サイクルに最適化されたコールバックでアニメーションを行うことで、タイミングのずれを最小化できます。CPU の過負荷時でも自動的にフレームレートを落とし、スムーズさを保ちます。

  • transform: translateX/Y の活用
    left/top の変更はレイアウトを再計算しますが、transform はコンポジットレイヤー上だけで処理されるため、再描画コストが大幅に下がります。GPU がオフロードできる点も高速化に寄与します。

  • 全要素の位置を同時更新
    位置情報を配列に保持し、1 フレームごとに全要素の座標を一括で更新することで、フレーム間のずれを防ぎます。

実装例:requestAnimationFrametransform で隙間なしアニメーション

以下では、5 つの <div> を横方向に同一速度で左から右へ移動させ、途中で隙間ができないように実装する手順を解説します。コードはモジュール化し、再利用可能な関数としてまとめています。

ステップ1:HTML と基本スタイルの用意

Html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>隙間なしアニメーションデモ</title> <style> body { margin: 0; overflow-x: hidden; } .track { position: relative; height: 120px; background: #f0f0f0; } .box { position: absolute; width: 100px; height: 100px; background: #4caf50; border-radius: 8px; /* 初期位置は左端 */ transform: translateX(0); } </style> </head> <body> <div class="track" id="track"> <!-- JavaScript で生成 --> </div> <script src="animation.js"></script> </body> </html>
  • .track は全体のコンテナで、overflow-x: hidden にすることで要素が画面外に出てもスクロールバーが出ません。
  • .box は動かす対象の <div>transform だけで位置を制御するため、left/top は固定です。

ステップ2:JavaScript で要素とアニメーションロジックを作成

Js
// animation.js const track = document.getElementById('track'); const BOX_COUNT = 5; const BOX_WIDTH = 100; const GAP = 20; // 各ボックス間の理想的な隙間 const SPEED_PX_PER_SEC = 150; // 1 秒あたりの移動距離 // 初期配置情報(左端に等間隔で並べる) const boxes = []; for (let i = 0; i < BOX_COUNT; i++) { const el = document.createElement('div'); el.className = 'box'; track.appendChild(el); boxes.push({ el, // 初期オフセットは i 番目の要素が前の要素の幅+隙間分だけ後ろにある位置 offset: i * (BOX_WIDTH + GAP) }); } // アニメーション用のタイムスタンプ保持 let lastTimestamp = null; function animate(timestamp) { if (!lastTimestamp) lastTimestamp = timestamp; const delta = (timestamp - lastTimestamp) / 1000; // 秒単位に変換 lastTimestamp = timestamp; // すべての box の位置を同時に更新 boxes.forEach(box => { box.offset += SPEED_PX_PER_SEC * delta; // 速度分だけ前進 // 画面右端を超えたら左端に巻き戻す(無限ループ) const totalLength = BOX_COUNT * (BOX_WIDTH + GAP); if (box.offset >= totalLength) { box.offset -= totalLength; } // CSS の transform に適用 box.el.style.transform = `translateX(${box.offset}px)`; }); // 次フレームへ requestAnimationFrame(animate); } // アニメーション開始 requestAnimationFrame(animate);

コード解説

  1. 配置情報の配列化
    boxes 配列に各要素と現在のオフセット位置を保持します。offsettransform: translateX に直接反映され、レイアウト計算を発生させません。

  2. フレームごとの時間差 (delta) を利用
    requestAnimationFrame が渡す timestamp を使い、前回フレームからの経過時間を正確に測ります。フレームレートが変動しても、ピクセル/秒 の速度が一定になるようにしています。

  3. 全要素を同時に更新
    ループ内部で一括更新することで、個別に requestAnimationFrame を呼び出すよりも 同期性が保たれ、隙間ができにくくなります。

  4. ループ処理
    すべての要素が右端に達したら、全体幅 totalLength を引いて左端に戻すことで、シームレスな無限スクロール が実現できます。

ステップ3:ハマりやすいポイントと対策

項目 典型的な問題 解決策
setInterval によるアニメーション フレームごとに時間がずれ、隙間が拡大 requestAnimationFrame に置き換える
left/top の変更 レイアウト再計算が頻繁に走り、CPU が逼迫 transform: translateX/Y に統一
小数点誤差 累積で 1px 以上のずれが出る offsetNumber で保持し、描画時は toFixed(2) などで丸める
画面サイズ変更 画面幅が変わるとループ長がずれる resize イベントで totalLength を再計算

実際にハマった例

問題: BOX_COUNT を 10 に増やしたら、途中で2つのボックスが重なり、隙間が消えた
原因: totalLength の計算が BOX_COUNT の増減に追従していなかった(ハードコーディングしていた)
対策: totalLength を関数 calcTotalLength() でリアルタイムに算出し、resizeDOMContentLoaded の両方で再設定した

このように、全体幅 を動的に管理することが、隙間防止の鍵です。

ステップ4:パフォーマンス測定とチューニング

  1. Chrome DevTools の Performance タブFrames を確認。60fps が維持できているかチェック。
  2. will-change: transform;.box に付与すると、ブラウザが事前にレイヤーを作成し、GPU アクセラレーションが有効になることがあります(ただし過度な使用は逆効果)。
  3. translate3d(0,0,0) でも同様に GPU オフロードを促すことができますが、現代ブラウザでは translate だけでも最適化が自動的に行われます。
Css
.box { will-change: transform; }

まとめ

本記事では、複数の <div> を同一速度で順次 position を変更すると隙間ができる原因を、ブラウザの描画タイミング、レイアウトコスト、数値誤差の三観点から解析しました。その上で、requestAnimationFrametransform を組み合わせた実装を具体例とともに提示し、ハマりやすいポイントと最適化テクニックも紹介しています。

  • 原因: 描画タイミングのずれ、レイアウト再計算、浮動小数点誤差
  • 解決策: requestAnimationFrametransform: translateX/Y、全要素の同時更新
  • ベストプラクティス: GPU オフロード (will-change)、動的幅計算、フレームごとの時間差利用

この記事を読むことで、読者は 滑らかで隙間のないアニメーション を自分のプロジェクトに組み込む自信がつくでしょう。次回は、スクロール連動型のアニメーションGSAP など外部ライブラリとの比較 について掘り下げる予定です。

参考資料