AI先生のロボットキャラクター
第9章 - セクション3

オセロ式ローディングアニメーション

オセロの反転ルールを活用した、LOADINGテキストが順次表示されるローディングアニメーション

導入

男子生徒のアイコン

ローディングアニメーションって、ただの回転するアイコンとかが多くて、あんまり面白くないですよね。もっとクリエイティブなものは作れないんですか?

AI先生のアイコン

もちろん!今回はオセロの反転ルールを使った、ユニークなローディングアニメーションを作ってみよう。石が順番に置かれていき、最終的に「LOADING」という文字が完成するんだ。

女子生徒のアイコン

オセロのルールを使うなんて面白いですね!どんな仕組みで動くんですか?

AI先生のアイコン

オセロ盤を8x8のグリッドで表現して、最初に黒石を1つ置く。その後、横一列に白石を反転させていき、最後に「LOADING…」の文字を段階的に表示するんだ。JavaScriptでアニメーションのタイミングを制御しているよ。

オセロ式ローディングアニメーション

オセロの反転ルールを活用した、LOADINGテキストが順次表示されるローディングアニメーションです。

HTML
<div class="container">
    <div class="board" id="board"></div>
</div>
CSS
body {
    margin: 0;
    padding: 0;
    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    font-family: 'Arial', sans-serif;
}

.container {
    text-align: center;
}

.board {
    display: inline-grid;
    grid-template-columns: repeat(8, 50px);
    grid-template-rows: repeat(8, 50px);
    gap: 2px;
    background: #0f3460;
    padding: 10px;
    border-radius: 8px;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5),
                0 0 60px rgba(22, 160, 133, 0.15);
}

.cell {
    width: 50px;
    height: 50px;
    background: #16a085;
    border-radius: 4px;
    display: flex;
    justify-content: center;
    align-items: center;
}

.stone {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 18px;
    font-weight: bold;
    transition: background-color 0.4s ease, transform 0.4s ease;
}

.stone.black {
    background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
    color: white;
    box-shadow: 
        0 2px 4px rgba(0, 0, 0, 0.3),
        0 4px 8px rgba(0, 0, 0, 0.2),
        inset 0 -2px 4px rgba(0, 0, 0, 0.3),
        inset 0 2px 4px rgba(255, 255, 255, 0.1);
}

.stone.white {
    background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
    box-shadow: 
        0 2px 4px rgba(0, 0, 0, 0.2),
        0 4px 8px rgba(0, 0, 0, 0.15),
        inset 0 -2px 4px rgba(0, 0, 0, 0.1),
        inset 0 2px 4px rgba(255, 255, 255, 0.8);
}

.stone .letter {
    opacity: 0;
    transition: opacity 0.2s ease;
}

.stone.show-letter .letter {
    opacity: 1;
}

@keyframes flip {
    0% { transform: rotateY(0deg); }
    50% { transform: rotateY(90deg); }
    100% { transform: rotateY(0deg); }
}

.stone.flip {
    animation: flip 0.4s ease;
}

@keyframes placeStone {
    0% {
        transform: scale(0);
        opacity: 0;
    }
    50% { transform: scale(1.2); }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

.stone.placing {
    animation: placeStone 0.5s ease;
}

@keyframes stonePulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(0.9); }
}

.stone.stone-pulse {
    animation: stonePulse 0.6s ease;
}
JavaScript
const board = document.getElementById('board');
const BOARD_SIZE = 8;

const HORIZONTAL_POSITIONS = [
    {row: 0, col: 0}, // L
    {row: 0, col: 1}, // O
    {row: 0, col: 2}, // A
    {row: 0, col: 3}, // D
    {row: 0, col: 4}, // I
    {row: 0, col: 5}, // N
    {row: 0, col: 6}, // G
    {row: 0, col: 7}  // ...
];

const letters = ['L', 'O', 'A', 'D', 'I', 'N', 'G', '...'];
let cells = [];

function createBoard() {
    board.innerHTML = '';
    cells = [];
    
    for (let row = 0; row < BOARD_SIZE; row++) {
        for (let col = 0; col < BOARD_SIZE; col++) {
            const cell = document.createElement('div');
            cell.className = 'cell';
            
            const stone = document.createElement('div');
            stone.className = 'stone';
            
            const letter = document.createElement('span');
            letter.className = 'letter';
            stone.appendChild(letter);
            
            if (row === 0 && col === 0) {
                stone.style.display = 'none';
            } else if (row === 0 && col >= 1 && col <= 6) {
                stone.classList.add('white');
            } else {
                stone.classList.add('black');
            }
            
            cell.appendChild(stone);
            board.appendChild(cell);
            cells.push({ cell, stone, letter });
        }
    }
}

async function animate() {
    createBoard();
    await sleep(800);
    
    // 黒石を置く
    const placeIndex = 0;
    const placeStone = cells[placeIndex].stone;
    const placeLetter = cells[placeIndex].letter;
    
    placeStone.style.display = 'flex';
    placeStone.classList.add('black', 'placing', 'show-letter');
    placeLetter.textContent = letters[0];
    
    await sleep(500);
    
    // 横一列を反転
    for (let i = 1; i <= 6; i++) {
        const hPos = HORIZONTAL_POSITIONS[i];
        const hIndex = hPos.row * BOARD_SIZE + hPos.col;
        const hStone = cells[hIndex].stone;
        const hLetter = cells[hIndex].letter;
        
        hLetter.textContent = letters[i];
        hStone.classList.add('flip');
        
        await sleep(200);
        
        hStone.classList.remove('white');
        hStone.classList.add('black', 'show-letter');
        
        await sleep(350);
    }
    
    await sleep(300);
    
    // ...を段階的に表示
    const lastIndex = 7;
    const lastStone = cells[lastIndex].stone;
    const lastLetter = cells[lastIndex].letter;
    
    lastLetter.innerHTML = '<span class="dot" style="opacity: 0;">.</span><span class="dot" style="opacity: 0;">.</span><span class="dot" style="opacity: 0;">.</span>';
    lastStone.classList.add('show-letter');
    
    const dots = lastLetter.querySelectorAll('.dot');
    dots[0].style.opacity = '1';
    await sleep(200);
    dots[1].style.opacity = '1';
    await sleep(200);
    dots[2].style.opacity = '1';
    
    await sleep(1000);
    
    // パルス効果
    for (let row = 0; row < BOARD_SIZE; row++) {
        for (let col = 0; col < BOARD_SIZE; col++) {
            const idx = row * BOARD_SIZE + col;
            cells[idx].stone.classList.add('stone-pulse');
        }
    }
    
    await sleep(600);
    
    for (let row = 1; row < BOARD_SIZE; row++) {
        for (let col = 0; col < BOARD_SIZE; col++) {
            const idx = row * BOARD_SIZE + col;
            cells[idx].stone.classList.remove('stone-pulse');
        }
    }
    
    await sleep(1000);
    animate();
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

createBoard();
animate();

AIへのプロンプト例

以下のようなプロンプトをAIに送信します:

オセロゲームのルールを使ったローディングアニメーションを作成してください。

### 盤面の設定
- 8×8のオセロ盤を作成
- 各マスには緑色の背景
- 盤面全体は濃い青の背景で囲む

### 石のデザイン
- 白石と黒石を円形で表現
- グラデーションを使い立体的な質感を演出
- 各石に影を付けて浮き上がったような効果

### アニメーションの流れ
1. 最初に黒石を左上に配置し「L」の文字を表示
2. 横一列の白石を順番に反転させ、「LOADING」の文字を1文字ずつ表示
3. 最後の石には「...」を段階的に表示
4. 完成後、盤面全体が脈打つようなパルスアニメーション
5. ループして繰り返す

### 反転のアニメーション
- Y軸を中心に回転して裏返る動き
- 回転中は少し透明にして奥行き感を演出
- 滑らかな動きで自然な反転を表現

### タイミング調整
- 石の配置から反転まで適度な間隔
- 文字が読みやすいテンポで順次表示
- 完了後のパルスは短めに設定

このパーツの特徴

  • ゲームルールの応用 オセロの反転ルールを忠実に再現し、文字表示に活用
  • 段階的な演出 石の配置→反転→文字表示の流れで視覚的なストーリーを構築
  • 完了アニメーション LOADING表示完了後、盤面全体が脈打つような効果で次の段階を示唆
  • 立体的な表現 グラデーションと複数の影を使い、石の質感をリアルに表現
  • 視認性の高さ 横一列配置で文字が読みやすく、待機時間のストレスを軽減

コードのポイント

盤面と石の3D表現:

.board {
    display: inline-grid;
    grid-template-columns: repeat(8, 50px);
    grid-template-rows: repeat(8, 50px);
    gap: 2px;
    background: #0f3460;
    padding: 10px;
    border-radius: 8px;
}

.stone.black {
    background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
    box-shadow: 
        0 2px 4px rgba(0, 0, 0, 0.3),
        inset 0 -2px 4px rgba(0, 0, 0, 0.3),
        inset 0 2px 4px rgba(255, 255, 255, 0.1);
}

.stone.white {
    background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
    box-shadow: 
        0 2px 4px rgba(0, 0, 0, 0.2),
        inset 0 -2px 4px rgba(0, 0, 0, 0.1),
        inset 0 2px 4px rgba(255, 255, 255, 0.8);
}
  • グリッドレイアウトで8×8の盤面を構築
  • グラデーションで石の光沢感を表現
  • 内側と外側の影を組み合わせて立体感を演出

オセロ盤の質感は、複数のbox-shadowを重ねることで実現しています。insetによる内側の影が凹凸を作り、外側の影が浮き上がりを表現します。白石と黒石で異なるグラデーション方向と影の強さを設定し、それぞれの素材感を際立たせています。

反転アニメーションの仕組み:

@keyframes flip {
    0% { transform: rotateY(0deg); }
    50% { transform: rotateY(90deg); }
    100% { transform: rotateY(0deg); }
}

.stone.flip {
    animation: flip 0.4s ease;
}
// 横一列を反転
for (let i = 1; i <= 6; i++) {
    const hPos = HORIZONTAL_POSITIONS[i];
    const hIndex = hPos.row * BOARD_SIZE + hPos.col;
    const hStone = cells[hIndex].stone;
    const hLetter = cells[hIndex].letter;
    
    hLetter.textContent = letters[i];
    hStone.classList.add('flip');
    
    await sleep(200);
    
    hStone.classList.remove('white');
    hStone.classList.add('black', 'show-letter');
    
    await sleep(350);
}
  • Y軸回転で石が裏返る動きを実現
  • 反転途中(90度)で色を切り替え
  • async/awaitで順序制御し、タイミングを調整

オセロの反転を視覚的に表現するため、CSS AnimationのrotateY()を使用しています。90度の時点で石の表裏が入れ替わるため、このタイミングでクラスを変更することで白から黒への自然な切り替えを実現しています。JavaScriptのasync/await構文により、各反転が完了するまで次の処理を待機し、リズミカルなアニメーションを作り出しています。

アニメーションシーケンスの制御:

async function animate() {
    createBoard();
    await sleep(800);
    
    // 1. 黒石を配置(L)
    placeStone.classList.add('black', 'placing', 'show-letter');
    await sleep(500);
    
    // 2. 白石を順番に反転(O-G)
    for (let i = 1; i <= 6; i++) {
        // 反転処理
        await sleep(350);
    }
    
    // 3. ...を段階表示
    dots[0].style.opacity = '1';
    await sleep(200);
    
    // 4. パルスアニメーション
    cells.forEach(c => c.stone.classList.add('stone-pulse'));
    await sleep(600);
    
    // 5. ループ
    animate();
}
  • 各ステップをsleep()で区切り、見やすいテンポを作成
  • ...は1文字ずつ透明度を変えて段階表示
  • パルスで完了を視覚的に伝える

アニメーション全体は複数のステップに分かれており、async/awaitと自作のsleep()関数で時間制御を行っています。石の配置、反転、文字表示、パルスという一連の流れを、適切な間隔で実行することで、視聴者が内容を追いやすいリズムを生み出しています。完了時のパルスアニメーションは、「処理が終わった」という明確なフィードバックをユーザーに提供する重要な役割を担っています。

まとめ

女子生徒のアイコン

オセロのルールを使ってローディング画面を作るなんて、思いつきませんでした!

AI先生のアイコン

ゲームのルールや身近な体験を、Web演出に応用するのは面白いよね。ユーザーは無意識にルールを理解しているから、自然と動きを受け入れやすくなるんだ。

男子生徒のアイコン

反転のタイミングと文字表示のタイミングを合わせるのが、けっこう難しそうですね。

AI先生のアイコン

そうだね。async/awaitを使ってアニメーションの順序を制御しているから、タイミング調整も比較的やりやすくなっているよ。sleep関数で待機時間を調整すれば、全体のテンポを簡単に変えられるんだ。

女子生徒のアイコン

最後のパルスアニメーションも、「完成した!」って感じが伝わってきますね。

AI先生のアイコン

完了の瞬間に盤面全体が「ドクン」と反応することで、ユーザーに「次の画面に進みますよ」というサインを送っているんだ。待機中の不安を減らす効果もあるね。

オセロ式ローディングの活用ポイント
  • ルールの明確さ オセロという誰もが知るゲームを使うことで、直感的に理解しやすい演出に
  • タイミング設計 各ステップの待機時間を調整し、見やすくストレスのないテンポを実現
  • 完了の合図 パルスアニメーションで処理完了を明示的に伝え、ユーザーの不安を軽減
  • 応用の幅 オセロ以外のゲームや身近なルールも、同様にローディング演出に転用可能

ゲームの仕組みをUI演出に活用することで、ユニークで記憶に残るローディング体験を作り出せます。

学習チェック

このレッスンを理解できたら「完了」をクリックしてください。
後で見直したい場合は「未完了に戻す」で進捗をリセットできます。

レッスン完了!🎉

お疲れさまでした!