
1.はじめにw
JavaScriptでゲーム開発途中、Gridをベースとしたアニメーション処理を考えてるうちに、かなりのパターン作成したので、備忘録メモも兼ねて実装方法やロジックを紹介したいと思います。
アニメションの魅力と多様性
初期の頃は、ブログ記事でも紹介していた通り、カード画像をグリッド状に分割して配置し、全部揃ったら合体するアニメーションを作りました。 ただ、1つのパターンだけでは演出として飽きやすい為、複数のパターンを作成するに至っています。
![[JavaScript] 画像を分割して合体アニメにする方法【Canvas+Grid】](https://humanxai.info/images/uploads/javascript-image-splitter.webp)
[JavaScript] 画像を分割して合体アニメにする方法【Canvas+Grid】
Canvasで画像を分割し、CSS GridとJavaScriptで合体アニメーションを実現。非同期処理、setTimeout、dataURLの使い方も詳しく解説。再利用可能なImageModule.js付き。
https://humanxai.info/posts/javascript-image-splitter/目指したこと
UI強化を強化して、操作に違和感をなくした事と、アニメーションのバリエーションを増やし演出に幅を持たせ、 尚且つ、アニメーションパターンによるコード量の増加に伴いの再利用性を強化しました。
結果、コードの見やすさ、バグを生みにくく、拡張性をしやすくできるなど。
2. 実装のポイント
今回の実装では、画像を [X x Y] のグリッドに分割し、各ピースに対して表示タイミングをずらすことで様々な演出を生み出します。
主な工夫は以下の通りです。
モジュール化で拡張性を確保
各アニメーションパターンは個別の関数として定義し、共通のインターフェース(pieces, container, xCount, yCount)を持たせました。これにより、アニメーション関数を追加・切り替えやすくなっています。
共通インターフェースの実装の一部:
const animations = [
{ num: "0", name: "順番", fn: showMergeAnimation },
{ num: "1", name: "ランダム", fn: showMergeAnimationRandom },
...
];
インターフェースの呼び出しは以下のように
- 番号
- 名前
- 未指定(ランダム)
のいずれでも対応できるようにしました。
これにより柔軟に関数を呼び出すことが出来ます。
関数呼び出しの記述例:
await showMergeAnimationMain(pieces, container, 4, 4, "波紋逆"); // 名前で指定
await showMergeAnimationMain(pieces, container, 4, 4, "11"); // 番号で指定
await showMergeAnimationMain(pieces, container, 4, 4); // ランダム表示
14種類のアニメーション演出
現在実装しているアニメーションは全部で14種類(+予備)に及びます。
No. | 演出パターン | 説明 |
---|---|---|
0 | 順番 | 左上から順に表示 |
1 | ランダム | ランダムに表示 |
2 | 左→右 縦列 | 縦1列ずつ左から |
3 | 右→左 縦列 | 縦1列ずつ右から |
4〜7 | 斜め(4方向) | 左上・右上・左下・右下 |
8 | 渦巻き | 時計回り/反時計回り |
10 | 波紋 | 中心から外へ広がる |
11 | 波紋逆 | 外から中心に集まる |
12 | ジグザグ | 行ごとに左右を交互に進む |
13 | ジグザグ逆 | 上記の逆順 |
14 | X字状 | 中央を起点に斜め交差 |
アニメーションの仕組み
各アニメーション関数の基本構造は以下のようになっています:
1. グリッドに従ってcontainerにピースを配置
2. 表示順に応じて遅延時間(delay)を計算
3. setTimeout()で遅れて表示(opacity = 1)
4. awaitで全ピース表示完了まで待機
例えば「波紋」では中心との距離に応じて遅延を計算します:
const dx = piece.x - centerX;
const dy = piece.y - centerY;
const delay = Math.sqrt(dx * dx + dy * dy) * ANIME_DELAY_TIME;
逆波紋にする場合は最大距離からの引き算:
delay = (maxDistance - dist) * ANIME_DELAY_TIME;
数学的な演出制御
幾何学的な配置(距離・角度・方向)を応用して、演出ロジックを構築しています。以下は代表的なテクニック:
- Math.sqrt:距離(ピタゴラス)
- Math.abs:X字状などでの中央からの距離計算
- 交互の方向切り替え:ジグザグ表示
if (row % 2 === 0) {
// 左→右
} else {
// 右→左
}
2.全体構造とモジュール構成
このアニメーション機構は、グリッド状に分割された画像ピースを HTML 要素として再構築し、順番や遅延タイミングを制御することで演出効果を実現しています。
構成を以下の3つの観点から解説します。
pieces, container, xCount/yCount などの役割説明:
変数名 | 内容と役割 |
---|---|
pieces |
分割済み画像ピースの配列。各要素は {x, y, src} の情報を持ち、位置と画像データを管理 |
container |
表示先となるHTML要素(通常は div )。ここにピース画像が img タグとして追加される |
xCount |
水平方向の分割数(例:4だと列4本) |
yCount |
垂直方向の分割数(例:3だと行3段) |
export async function showMergeAnimationMain(pieces, container, xCount, yCount, fIndex = null)
全てのアニメーション関数は、この共通インターフェースで動作します。
アニメーションの一括管理:animations[]
アニメーション関数を1つずつ個別に呼び出すのではなく、以下のように配列にまとめて管理しています:
const animations = [
{ num: "0", name: "順番", fn: showMergeAnimation },
{ num: "1", name: "ランダム", fn: showMergeAnimationRandom },
...
];
これにより、
- ループ処理によるランダム切り替え
- 番号や名前での指定呼び出し
- セレクトボックスやメニューとの連携
などが柔軟に行えるようになります。
コントロール関数:showMergeAnimationMain()
この関数がアニメーション呼び出しの司令塔になります。
✅ 処理の流れ:
1.fIndex に指定された番号 or 名前をもとに animations[] を検索 2.一致する関数があればそれを await 実行 3.指定がない場合(null)はランダムに1つ選択して実行
✅ 実装の例:
export async function showMergeAnimationMain(piece, container, x, y, fIndex = null) {
let anim = animations.find((a) => a.num === fIndex || a.name === fIndex);
if (anim) {
await anim.fn(piece, container, x, y);
} else {
const index = fIndex ?? Math.floor(Math.random() * animations.length);
anim = animations[index];
if (anim) await anim.fn(piece, container, x, y);
}
}
このように 「選択 → 実行」 の流れが一本化されており、今後アニメーションを追加する際も animations[] に1行加えるだけで済みます。
🔧 アニメーション関数の共通構造
各アニメーション関数の内部構造は似ており、以下のような流れです:
- container.innerHTML = “":前のピースをクリア
- container.style.gridTemplateColumns = …:グリッド指定
- ピースをループで並べて img 要素生成
- 遅延時間を計算して setTimeout() で表示タイミングを制御
- 最後に await new Promise(…) でアニメーション完了を待機
この構成のメリット
- アニメーションごとにファイルを分けず関数で集約管理できる
- showMergeAnimationMain() を呼ぶだけで簡単に再生できる
- 将来的に UI(ボタン/ドロップダウン/シーン分岐など)との連携が容易
3.アニメーションの種類と実装ロジック
このセクションでは、アニメーション演出をパターンごとに分類し、それぞれの動きとコードロジックの特徴を解説します。 CSSだけでは実現が難しいような**「ピースごとの細かい遅延制御」**をJavaScriptで実現しています。
各パターンは、基本的に以下の処理構造で共通しています:
for (const piece of sortedPieces) {
const delay = [x, y, index] を元に計算;
setTimeout(() => {
piece表示を実行(img.style.opacity = 1 など)
}, delay);
}
1. 順番表示(左上から順に表示)
動きの特徴: 左上から右下へ、行→列の順に1つずつ表示される最も基本的なアニメーション。
ロジック:
delay = index * ANIME_DELAY_TIME;
ポイント:
- ピース配列の順番通りに遅延をかけて表示
- forEach ループ+ setTimeout() の組み合わせ

2. ランダム表示
動きの特徴: 各ピースがランダムな順でバラバラにフェードイン表示される。
ロジック:
const shuffled = Utils.shuffle([...pieces]);
delay = index * ANIME_DELAY_TIME;
ポイント:
- 配列をランダムシャッフル
- 一定の間隔で順番に出現(完全同時ではない)

3. 縦波(左→右)
動きの特徴: 列ごとに縦方向に波打つように出現。
ロジック:
delay = x * ANIME_DELAY_TIME;
**逆バージョン(右→左)**も x を反転すれば同様。
4. 斜め方向(左上→右下、他3方向)
動きの特徴: 斜め方向(対角線)に沿って中央に向かって or 端から端へ進行。
ロジック:
delay = Math.abs(x - centerX) + Math.abs(y - centerY);
ポイント:
- 中心からの「距離」が同じピースが同時に表示される
- ダイアゴナル方向を direction パラメータで切り替え可能
その他、左下→右上 右下→左上。
5. 渦巻き(スパイラル)
動きの特徴: 中心または外周から時計回り(または反転)に渦を巻くようにピースが表示。
ロジック:
const order = Utils.getSpiralOrder(xCount, yCount, reverse);
delay = orderIndex * ANIME_DELAY_TIME;
export function getSpiralOrder(rows, cols) {
const order = [];
let top = 0,
bottom = rows - 1;
let left = 0,
right = cols - 1;
while (top <= bottom && left <= right) {
for (let i = left; i <= right; i++) order.push([top, i]); // → 右へ
top++;
for (let i = top; i <= bottom; i++) order.push([i, right]); // ↓ 下へ
right--;
if (top <= bottom) {
// ← 左へ
for (let i = right; i >= left; i--) order.push([bottom, i]);
bottom--;
}
if (left <= right) {
// ↑ 上へ
for (let i = bottom; i >= top; i--) order.push([i, left]);
left++;
}
}
return order;
}
ポイント:
- x/y 座標ではなく、事前に定義した順番リストに基づいて表示
- 規則的かつ視覚的に面白い印象を与える
6. 波紋(中心から外に広がる)
動きの特徴: 中央から水の波紋のように広がっていくアニメーション。
ロジック:
delay = Math.abs(x - centerX) + Math.abs(y - centerY);
逆バージョンでは maxDelay - delay に変換。
波紋(中心から外に広がる) ※逆バージョン有

7. ジグザグ(折り返し)
動きの特徴: 横に進んで折り返す蛇行的な表示(例:左→右 → 右→左 …)
ロジック:
const order = Utils.getZigZagOrder(yCount, xCount);
delay = indexInOrder * ANIME_DELAY_TIME;
export function getZigZagOrder(rows, cols) {
const order = [];
for (let y = 0; y < rows; y++) {
if (y % 2 === 0) {
// 左→右
for (let x = 0; x < cols; x++) {
order.push([y, x]);
}
} else {
// 右→左
for (let x = cols - 1; x >= 0; x--) {
order.push([y, x]);
}
}
}
return order;
}
ポイント:
- y 行ごとに x の向きを交互に切り替えることで実現
8. X字表示(交差点)
動きの特徴: 左上→右下、右上→左下の2方向から中心に向かってクロス表示される演出。
ロジック:
delay = Math.max(Math.abs(x - centerX), Math.abs(y - centerY));
ポイント:
- Math.max() を使ってX字の軸上に近いものほど早く表示
- アニメーションのバランスが良く、派手すぎず印象的
数学的なロジックの工夫
数学関数 | 用途 |
---|---|
Math.abs() |
距離(差分)の絶対値により、中心からの広がりを表現 |
Math.max() |
X字や放射状の境界処理に活用 |
findIndex() |
カスタム順序リスト内の位置取得に使用 |
Utils.shuffle() |
配列シャッフルによるランダム性の付与 |
遅延処理の基本
setTimeout(() => {
img.style.opacity = "1";
}, delay);
各ピースに対して delay を個別に指定し、フェードインや拡大縮小などを順に適用することで、見た目にインパクトのある演出が可能になります。
補足:アニメーション管理と拡張性、UI連携の工夫
本プロジェクトでは、アニメーション関数をただ並べるだけでなく、以下のような工夫により拡張性と再利用性を高める構成を採用しています。
アニメーション関数の管理構造
const animations = [
{ num: "1", name: "順番", fn: showMergeAnimation },
{ num: "2", name: "ランダム", fn: showMergeAnimationRandom },
...
];
このように、番号(num)と名前(name)、対応関数(fn)を1つのオブジェクトにまとめて配列化することで:
- UI選択時に番号や名前を指定するだけで切り替え可能
- アニメーション一覧の表示も容易
- 特定のアニメーションをランダム表示にも応用可能
柔軟な呼び出し関数(showMergeAnimationMain)
export async function showMergeAnimationMain(piece, container, x, y, fIndex = null)
- fIndex に “3” や “渦巻反転” のように文字列で呼び出し可能
- 未指定時はランダム選択で多彩な演出を自動化
- ゲームのステージ番号や演出条件に応じて動的に選択できる
UIとの連携設計(例:ドロップダウン選択)
<select id="animationSelect">
<option value="ランダム">ランダム</option>
<option value="波紋">波紋</option>
...
</select>
const value = document.getElementById("animationSelect").value;
await showMergeAnimationMain(pieces, container, x, y, value);
- UI側と name を一致させれば、そのままアニメーションにバインドできる
- 開発後の「UIからアニメを選んで再生」デバッグにも便利
アニメーションの追加も簡単!
animations.push({
num: "14",
name: "落下演出",
fn: (p, c, x, y) => showMergeAnimationDrop(p, c, x, y)
});
- animations[] に1行追加するだけで拡張可能
- 開発途中でも新演出を即座にテスト可能な構造
拡張アイデア:組み合わせや連続再生
- 1つの演出ではなく 複数のアニメーションを順に再生
- 組み合わせを事前に登録しておき、ステージごとの特徴に応じて切り替え
- BGMやSE、吹き出し、キャラ演出との連携にも応用可能
まとめ
このように、アニメーションロジックだけでなく管理構造そのものを柔軟に設計しておくことで:
- スケーラビリティが高まり
- テスト・UI連携・デバッグが楽になり
- ユーザーに多彩な演出体験を届けることができます
単なるビジュアル演出の枠を超えた、「設計としての強さ」が魅力です。
💬 コメント