0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vibe Coding - ハノイの塔アプリ解析(初心者向け)

Posted at

📚 目次

  1. はじめに
  2. プロジェクト概要
  3. ファイル構成
  4. 各ファイルの詳細説明
  5. アプリケーションの動作フロー
  6. 重要な概念の解説
  7. 初心者向けのポイント

はじめに

この記事は、note記事のマガジン:DakotaRed流:Vibe Codingへの道と連動しています。

今回は、独学『バイブコーディングをやってみよう#1』で作成した『ハノイの塔』アプリの解析記事です。

この記事の内容を理解することで、以下のスキルが身につきます:
✅ Electron アプリの基本構造
✅ DOM 操作の基礎
✅ イベント駆動プログラミング
✅ 非同期処理(async/await)
✅ 再帰アルゴリズム
✅ CSS アニメーション
✅ Web Animations API

『ハノイの塔』アプリの概要

このアプリはElectron というフレームワークを使って作られた「ハノイの塔」パズルの視覚化アプリケーションです。

ハノイの塔とは?

3 本の棒と、大きさの異なる複数の円盤を使ったパズルです。すべての円盤を別の棒に移動させることが目標で、以下のルールがあります:

  • 一度に 1 枚の円盤しか動かせない
  • 小さい円盤の上に大きい円盤を置いてはいけない

Electron とは?

Web 技術(HTML、CSS、JavaScript)を使って、デスクトップアプリケーションを作ることができるフレームワークです。

  • メリット: Web サイトを作る技術でデスクトップアプリが作れる
  • 使用例: Visual Studio Code、Obsidian、Slack、Discord など

ファイル構成

polar-planck/
├── package.json         # プロジェクトの設定ファイル
├── main.js              # Electronのメインプロセス(アプリの起動部分)
├── index.html           # アプリの見た目の構造
├── styles.css           # アプリのデザイン(色、配置など)
├── renderer.js          # アプリの動作ロジック(ユーザー操作の処理)
├── README.md            # プロジェクトの説明書
├── node_modules/        # 依存ライブラリ(自動生成)
├── dist/                # ビルド結果(自動生成)
└── release-builds/      # パッケージ化されたアプリ(自動生成)

各ファイルの詳細説明

1. package.json - プロジェクトの設定ファイル

{
  "name": "polar-planck",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "package": "electron-packager . \"Tower of Hanoi\" --platform=win32 --arch=x64 --out=release-builds --overwrite",
    "dist": "electron-builder"
  },
  "devDependencies": {
    "electron": "^39.2.4",
    "electron-builder": "^26.0.12",
    "electron-packager": "^17.1.2"
  }
}

各項目の説明

基本情報:

  • "name": プロジェクトの名前
  • "version": バージョン番号
  • "main": アプリが起動する時に最初に実行されるファイル

scripts(コマンド):

  • "start": npm start と入力すると、Electron アプリが起動します
  • "package": アプリを配布用にパッケージ化します(Windows 用の実行ファイルを作成)
  • "dist": インストーラーを作成します

devDependencies(開発に必要なツール):

  • electron: Electron フレームワーク本体
  • electron-builder: アプリをビルドするツール
  • electron-packager: アプリをパッケージ化するツール

2. main.js - メインプロセス(アプリの起動部分)

const { app, BrowserWindow } = require("electron");
const path = require("path");

行ごとの説明

1-2 行目: 必要なモジュールの読み込み

const { app, BrowserWindow } = require("electron");
  • require('electron'): Electron の機能を使えるようにする
  • app: アプリケーション全体を管理するオブジェクト
  • BrowserWindow: ウィンドウを作成するためのクラス
const path = require("path");
  • path: ファイルパスを扱うための Node.js の標準モジュール(このコードでは実際には使われていません)

4-18 行目: ウィンドウを作成する関数

function createWindow() {
  const win = new BrowserWindow({
    width: 1000,
    height: 800,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
    backgroundColor: "#1e1e1e",
    title: "Tower of Hanoi Solver",
  });

  win.loadFile("index.html");
}

詳細解説:

  • new BrowserWindow({...}): 新しいウィンドウを作成

    • width: 1000: ウィンドウの幅を 1000 ピクセルに設定
    • height: 800: ウィンドウの高さを 800 ピクセルに設定
    • webPreferences: Web ページの動作設定
      • nodeIntegration: true: HTML ファイル内で Node.js の機能を使えるようにする
      • contextIsolation: false: セキュリティ設定を簡略化(本番環境では推奨されません)⇒ Vibe Codingによる自動生成コードにより推奨していないfalseが設定された模様)
    • backgroundColor: '#1e1e1e': 背景色をダークグレーに設定
    • title: ウィンドウのタイトル
  • win.loadFile('index.html'): index.html ファイルをウィンドウに読み込む

20-28 行目: アプリが準備できたら実行

app.whenReady().then(() => {
  createWindow();

  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

詳細解説:

  • app.whenReady(): Electron の準備が完了したら実行
  • createWindow(): ウィンドウを作成
  • app.on('activate', ...): macOS でアプリアイコンをクリックした時の処理
    • ウィンドウがない場合は新しく作成する

30-34 行目: すべてのウィンドウが閉じられた時の処理

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

詳細解説:

  • window-all-closed: すべてのウィンドウが閉じられた時にあがるイベント
  • process.platform !== 'darwin': macOS 以外の場合
    • macOS ではウィンドウを閉じてもアプリは終了しない習慣があるため
  • app.quit(): アプリケーションを終了

3. index.html - アプリの見た目の構造

HTML は、Web ページの「骨組み」を作る言語です。

主要な部分の解説

1-11 行目: HTML の基本設定

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tower of Hanoi Visualizer</title>
    <link rel="stylesheet" href="styles.css" />
    <link
      href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap"
      rel="stylesheet"
    />
  </head>
</html>

詳細解説:

  • <!DOCTYPE html>: HTML5 であることを宣言
  • <meta charset="UTF-8">: 文字コードを UTF-8 に設定(日本語などを表示できる)
  • <title>: ブラウザのタブに表示されるタイトル
  • <link rel="stylesheet" href="styles.css">: CSS ファイルを読み込む
  • Google フォント「Outfit」を読み込んで、おしゃれなフォントを使用

14-17 行目: ヘッダー部分

<header>
  <h1>Tower of Hanoi</h1>
  <p>Recursive Solution Visualizer</p>
</header>
  • <h1>: 大きな見出し(タイトル)
  • <p>: 段落(サブタイトル)

19-33 行目: コントロール部分(ユーザーの入力エリア部分)

<div class="controls">
  <div class="input-group">
    <label for="diskCount">Number of Disks:</label>
    <input type="number" id="diskCount" min="1" max="8" value="3" />
  </div>
  <div class="input-group">
    <label for="speed">Speed (ms):</label>
    <input
      type="range"
      id="speed"
      min="100"
      max="2000"
      value="500"
      step="100"
    />
    <span id="speedValue">500ms</span>
  </div>
  <div class="button-group">
    <button id="startBtn" class="btn primary">Start Solving</button>
    <button id="resetBtn" class="btn secondary">Reset</button>
  </div>
</div>

詳細解説:

  • 円盤の数を選ぶ入力欄:

    • type="number": 数値入力欄
    • min="1" max="8": 1 から 8 までの数値のみ入力可能
    • value="3": 初期値は 3
    • id="diskCount": JavaScript から操作するための識別子
  • スピード調整スライダー:

    • type="range": スライダー(バー)
    • min="100" max="2000": 100ms から 2000ms まで調整可能
    • step="100": 100ms 刻みで調整
  • ボタン:

    • id="startBtn": 「開始」ボタン
    • id="resetBtn": 「リセット」ボタン

35-56 行目: 視覚化エリア(棒と円盤を表示する部分)

<div class="visualization-area">
  <div class="pegs-container">
    <div class="peg-wrapper" id="peg-A">
      <div class="pole"></div>
      <div class="base"></div>
      <div class="label">Source (A)</div>
      <div class="disks-container"></div>
    </div>
    <!-- peg-B と peg-C も同様 -->
  </div>
</div>

詳細解説:

  • peg-wrapper: 各棒(ペグ)を囲む容器
  • pole: 縦の棒
  • base: 棒の土台
  • label: 棒の名前(A、B、C)
  • disks-container: 円盤を配置する場所(JavaScript で動的に円盤を追加)

58-61 行目: ステータスバー

<div class="status-bar">
  <span id="statusText">Ready</span>
  <span id="moveCounter">Moves: 0</span>
</div>
  • 現在の状態(Ready、Solving、Finished など)と移動回数を表示

64 行目: JavaScript ファイルの読み込み

<script src="renderer.js"></script>
  • renderer.js を読み込んで、アプリの動作ロジックを実行

4. styles.css - デザイン設定

CSS は、Web ページの「見た目」を整える言語です。

主要な部分の解説

1-10 行目: CSS 変数の定義

:root {
  --bg-color: #121212;
  --surface-color: #1e1e1e;
  --primary-color: #bb86fc;
  --secondary-color: #03dac6;
  --text-color: #e0e0e0;
  --accent-color: #cf6679;
  --peg-color: #333;
  --disk-base-hue: 260;
}

詳細解説:

  • :root: CSS 変数を定義する場所
  • --bg-color: 背景色(ダークグレー)
  • --primary-color: メインカラー(紫)
  • --secondary-color: サブカラー(シアン)
  • これらの変数を使うことで、色を一括で変更できます

12-16 行目: すべての要素の基本設定

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
  • *: すべての要素に適用
  • box-sizing: border-box: サイズ計算を簡単にする設定
  • margin: 0; padding: 0;: デフォルトの余白をリセット

18-27 行目: body(全体)の設定

body {
  font-family: "Outfit", sans-serif;
  background-color: var(--bg-color);
  color: var(--text-color);
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

詳細解説:

  • font-family: フォントを「Outfit」に設定
  • background-color: var(--bg-color): 背景色を変数から取得
  • height: 100vh: 画面の高さいっぱいに表示(vh = viewport height)
  • display: flex: フレックスボックスレイアウトを使用
  • justify-content: center: 横方向の中央揃え
  • align-items: center: 縦方向の中央揃え

46-52 行目: タイトルのグラデーション効果

header h1 {
  font-weight: 600;
  font-size: 2.5rem;
  background: linear-gradient(
    45deg,
    var(--primary-color),
    var(--secondary-color)
  );
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

詳細解説:

  • background: linear-gradient(...): グラデーション背景を作成
  • -webkit-background-clip: text: 背景をテキストの形に切り抜く
  • -webkit-text-fill-color: transparent: テキストを透明にして、背景が見えるようにする
  • 結果: 紫からシアンへのグラデーションテキスト

95-108 行目: ボタンのホバー効果

.btn {
  padding: 0.6rem 1.5rem;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: transform 0.2s, opacity 0.2s;
}

.btn:hover {
  transform: translateY(-2px);
  opacity: 0.9;
}

詳細解説:

  • transition: アニメーションの設定(0.2 秒かけて変化)
  • :hover: マウスを乗せた時のスタイル
  • transform: translateY(-2px): 上に 2 ピクセル移動(浮き上がる効果)
  • opacity: 0.9: 少し透明にする

180-187 行目: 円盤のスタイル

.disk {
  height: 20px;
  border-radius: 10px;
  transition: all 0.3s ease-in-out;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
  position: absolute;
  bottom: 0;
}

詳細解説:

  • border-radius: 10px: 角を丸くする
  • transition: all 0.3s ease-in-out: すべての変化を 0.3 秒でアニメーション
  • box-shadow: 影をつけて立体感を出す
  • position: absolute: 絶対位置指定(アニメーションのため)

5. renderer.js - アプリの動作ロジック

JavaScript は、Web ページに「動き」を与える言語です。

主要な部分の解説

1-7 行目: HTML 要素の取得

const startBtn = document.getElementById("startBtn");
const resetBtn = document.getElementById("resetBtn");
const diskCountInput = document.getElementById("diskCount");
const speedInput = document.getElementById("speed");
const speedValue = document.getElementById("speedValue");
const statusText = document.getElementById("statusText");
const moveCounter = document.getElementById("moveCounter");

詳細解説:

  • document.getElementById('id名'): HTML の要素を ID で取得
  • const: 定数(変更されない変数)を宣言
  • これらの変数を使って、JavaScript から HTML 要素を操作できます

例:

startBtn.disabled = true; // ボタンを無効化
statusText.textContent = "Solving..."; // テキストを変更

9-13 行目: 棒(ペグ)の取得

const pegs = {
  A: document.getElementById("peg-A").querySelector(".disks-container"),
  B: document.getElementById("peg-B").querySelector(".disks-container"),
  C: document.getElementById("peg-C").querySelector(".disks-container"),
};

詳細解説:

  • querySelector('.disks-container'): クラス名で要素を検索
  • pegs.A: A 棒の円盤コンテナ
  • オブジェクト形式で管理することで、pegs['A'] のようにアクセスできる

15-19 行目: 変数の初期化

let moves = [];
let isSolving = false;
let abortController = null;
let moveCount = 0;
let animationSpeed = 500;

詳細解説:

  • let: 変更可能な変数を宣言
  • moves: 移動手順を保存する配列
  • isSolving: 現在解決中かどうかのフラグ(true/false)
  • moveCount: 移動回数のカウンター
  • animationSpeed: アニメーションの速度(ミリ秒)

22-25 行目: スピードスライダーのイベントリスナー

speedInput.addEventListener("input", (e) => {
  animationSpeed = parseInt(e.target.value);
  speedValue.textContent = `${animationSpeed}ms`;
});

詳細解説:

  • addEventListener('input', ...): スライダーが動いた時に実行される関数を登録
  • (e) => {...}: アロー関数(新しい関数の書き方)
  • e.target.value: スライダーの現在の値
  • parseInt(...): 文字列を整数に変換
  • `${変数}`: テンプレートリテラル(変数を文字列に埋め込む)

28-56 行目: 円盤の初期化関数

function initDisks(count) {
  // 既存の円盤をクリア
  Object.values(pegs).forEach((peg) => (peg.innerHTML = ""));

  for (let i = count; i >= 1; i--) {
    const disk = document.createElement("div");
    disk.classList.add("disk");
    disk.id = `disk-${i}`;

    // 幅の計算: 最大180px、最小40px
    const width = 40 + (140 / (count - 1 || 1)) * (i - 1);
    disk.style.width = `${width}px`;

    // 色のグラデーション
    const hue = 260 + i * 15;
    disk.style.backgroundColor = `hsl(${hue}, 70%, 60%)`;

    // 初期位置(下から積み上げ)
    disk.style.bottom = `${(count - i) * 22}px`;

    pegs.A.appendChild(disk);
  }

  moveCount = 0;
  updateStatus();
}

詳細解説:

  1. 既存の円盤をクリア:

    Object.values(pegs).forEach((peg) => (peg.innerHTML = ""));
    
    • Object.values(pegs): pegs オブジェクトの値(A、B、C の要素)を配列として取得
    • forEach(...): 配列の各要素に対して処理を実行
    • peg.innerHTML = '': 中身を空にする
  2. 円盤を作成(大きい順に):

    for (let i = count; i >= 1; i--) {
    
    • forループ: count から 1 まで逆順に繰り返す
    • 例: count=3 の場合、i=3, 2, 1 の順に実行
  3. 円盤要素の作成:

    const disk = document.createElement("div");
    disk.classList.add("disk");
    disk.id = `disk-${i}`;
    
    • createElement('div'): 新しい div 要素を作成
    • classList.add('disk'): 'disk'クラスを追加
    • id: 各円盤に一意の ID を設定(例: disk-1, disk-2, disk-3)
  4. 幅の計算:

    const width = 40 + (140 / (count - 1 || 1)) * (i - 1);
    
    • 最小幅: 40px(i=1 の時)
    • 最大幅: 180px(i=count の時)
    • 等間隔で幅が増加するように計算
  5. 色の設定(HSL 色空間):

    const hue = 260 + i * 15;
    disk.style.backgroundColor = `hsl(${hue}, 70%, 60%)`;
    
    • hsl(色相, 彩度, 明度): 色を指定する方法の一つ
    • 色相を変えることで、紫から青へのグラデーションを作成
  6. 位置の設定:

    disk.style.bottom = `${(count - i) * 22}px`;
    
    • 下から積み上げるように配置
    • 各円盤の高さ 20px + 隙間 2px = 22px
  7. A 棒に追加:

    pegs.A.appendChild(disk);
    

63-69 行目: ハノイの塔アルゴリズム(再帰関数)

function hanoi(n, source, target, auxiliary) {
  if (n > 0) {
    hanoi(n - 1, source, auxiliary, target);
    moves.push({ disk: n, from: source, to: target });
    hanoi(n - 1, auxiliary, target, source);
  }
}

詳細解説:

これは再帰関数という、自分自身を呼び出す関数です。

パラメータ:

  • n: 移動する円盤の数
  • source: 移動元の棒
  • target: 移動先の棒
  • auxiliary: 補助の棒

アルゴリズムの考え方:

n 枚の円盤を A から C に移動する場合:

  1. 上の n-1 枚を A から B(補助)に移動
  2. 一番大きい円盤を A から C に移動
  3. n-1 枚を B(補助)から C に移動

例: n=3 の場合

hanoi(3, 'A', 'C', 'B')
  ├─ hanoi(2, 'A', 'B', 'C')  // 上2枚をAからBへ
  │   ├─ hanoi(1, 'A', 'C', 'B')
  │   ├─ 円盤2をA→B
  │   └─ hanoi(1, 'C', 'B', 'A')
  ├─ 円盤3をA→C  // 最大の円盤を移動
  └─ hanoi(2, 'B', 'C', 'A')  // 2枚をBからCへ
      ├─ hanoi(1, 'B', 'A', 'C')
      ├─ 円盤2をB→C
      └─ hanoi(1, 'A', 'C', 'B')

moves 配列に保存:

moves.push({ disk: n, from: source, to: target });
  • 各移動を配列に記録
  • 例: { disk: 1, from: 'A', to: 'C' }

71-89 行目: 移動を再生する関数

async function playMoves() {
  isSolving = true;
  startBtn.disabled = true;
  diskCountInput.disabled = true;
  statusText.textContent = "Solving...";

  for (const move of moves) {
    if (!isSolving) break;

    await animateMove(move);
    moveCount++;
    updateStatus();
  }

  isSolving = false;
  startBtn.disabled = false;
  diskCountInput.disabled = false;
  statusText.textContent = "Finished!";
}

詳細解説:

  1. async 関数:

    async function playMoves() {
    
    • 非同期処理を扱う関数
    • awaitを使って、アニメーションの完了を待つことができる
  2. UI の無効化:

    startBtn.disabled = true;
    diskCountInput.disabled = true;
    
    • 解決中は操作できないようにする
  3. 移動を順番に実行:

    for (const move of moves) {
      await animateMove(move);
      moveCount++;
      updateStatus();
    }
    
    • for...of: 配列の各要素を順番に処理
    • await animateMove(move): アニメーションが完了するまで待つ
    • moveCount++: 移動回数をカウント

99-178 行目: アニメーション関数

async function animateMove(move) {
  return new Promise((resolve) => {
    const disk = document.getElementById(`disk-${move.disk}`);
    const fromPeg = pegs[move.from];
    const toPeg = pegs[move.to];

    // 現在の位置を取得
    const startRect = disk.getBoundingClientRect();

    // 目標位置を計算
    const disksOnTarget = toPeg.children.length;
    const targetBottom = disksOnTarget * 22;

    // 固定位置指定に切り替え
    disk.style.position = "fixed";
    disk.style.left = `${startRect.left}px`;
    disk.style.top = `${startRect.top}px`;
    disk.style.zIndex = 100;

    // アニメーションを定義
    const animation = disk.animate(
      [
        { left: `${initialLeft}px`, top: `${initialTop}px` },
        {
          left: `${initialLeft}px`,
          top: `${toPegRect.top - 40}px`,
          offset: 0.3,
        },
        { left: `${targetX}px`, top: `${toPegRect.top - 40}px`, offset: 0.6 },
        { left: `${targetX}px`, top: `${targetY}px` },
      ],
      {
        duration: animationSpeed,
        easing: "ease-in-out",
      }
    );

    animation.onfinish = () => {
      // 位置をリセットして新しい親に追加
      disk.style.position = "absolute";
      disk.style.bottom = `${targetBottom}px`;
      toPeg.appendChild(disk);
      resolve();
    };
  });
}

詳細解説:

  1. Promise を返す:

    return new Promise(resolve => {
    
    • アニメーションが完了したらresolve()を呼ぶ
    • awaitで待つことができるようになる
  2. 要素の取得:

    const disk = document.getElementById(`disk-${move.disk}`);
    
    • 移動する円盤を取得
  3. 位置情報の取得:

    const startRect = disk.getBoundingClientRect();
    
    • getBoundingClientRect(): 要素の画面上の位置とサイズを取得
  4. 固定位置指定:

    disk.style.position = "fixed";
    
    • 親要素から独立して、画面上の絶対位置で配置
    • これにより、異なる棒の間をスムーズに移動できる
  5. Web Animations API を使用:

    const animation = disk.animate(
      [
        { left: "...", top: "..." }, // 開始位置
        { left: "...", top: "...", offset: 0.3 }, // 持ち上げ(30%地点)
        { left: "...", top: "...", offset: 0.6 }, // 横移動(60%地点)
        { left: "...", top: "..." }, // 下ろす(100%地点)
      ],
      {
        duration: animationSpeed,
        easing: "ease-in-out",
      }
    );
    
    • 4 段階のアニメーション:
      1. 開始位置
      2. 上に持ち上げる
      3. 横に移動
      4. 下に下ろす
    • offset: アニメーションのタイミング(0.0〜1.0)
    • easing: 'ease-in-out': 滑らかな加速・減速
  6. アニメーション完了時の処理:

    animation.onfinish = () => {
      disk.style.position = "absolute";
      disk.style.bottom = `${targetBottom}px`;
      toPeg.appendChild(disk);
      resolve();
    };
    
    • 位置指定を元に戻す
    • 新しい棒に円盤を追加
    • resolve()を呼んで、Promise を完了させる

181-193 行目: 開始ボタンのイベントリスナー

startBtn.addEventListener("click", () => {
  const count = parseInt(diskCountInput.value);
  moves = [];

  // ボードをリセット
  initDisks(count);

  // 移動手順を計算
  hanoi(count, "A", "C", "B");

  // アニメーション開始
  playMoves();
});

詳細解説:

  1. 円盤の数を取得
  2. 移動配列をクリア
  3. 円盤を初期化
  4. ハノイアルゴリズムで移動手順を計算
  5. アニメーションを開始

195-200 行目: リセットボタンのイベントリスナー

resetBtn.addEventListener("click", () => {
  isSolving = false;
  const count = parseInt(diskCountInput.value);
  initDisks(count);
  statusText.textContent = "Ready";
});

詳細解説:

  • 解決中フラグを false に設定(アニメーションを中断)
  • 円盤を初期状態に戻す

203 行目: 初期セットアップ

initDisks(3);
  • アプリ起動時に 3 枚の円盤で初期化

アプリケーションの動作フロー

1. アプリケーション起動時

1. main.js が実行される
   ↓
2. Electronが準備完了(app.whenReady())
   ↓
3. createWindow()でウィンドウを作成
   ↓
4. index.htmlを読み込む
   ↓
5. styles.cssでデザインを適用
   ↓
6. renderer.jsが実行される
   ↓
7. initDisks(3)で3枚の円盤を初期化
   ↓
8. ユーザーの操作を待つ

2. 「Start Solving」ボタンをクリックした時

1. startBtnのクリックイベントが発火
   ↓
2. 円盤の数を取得(diskCountInput.value)
   ↓
3. initDisks(count)で円盤をリセット
   ↓
4. hanoi(count, 'A', 'C', 'B')で移動手順を計算
   ↓
5. playMoves()でアニメーション開始
   ↓
6. moves配列の各移動を順番に実行
   ↓
7. animateMove(move)で円盤を移動
   ↓
8. すべての移動が完了したら「Finished!」と表示

3. アニメーションの詳細フロー

animateMove(move)が呼ばれる
   ↓
1. 円盤要素を取得
   ↓
2. 現在位置を取得(getBoundingClientRect)
   ↓
3. 目標位置を計算
   ↓
4. position: fixedに変更(親要素から独立)
   ↓
5. Web Animations APIでアニメーション実行
   - 持ち上げ(上に移動)
   - 横移動
   - 下ろす(下に移動)
   ↓
6. アニメーション完了時
   - position: absoluteに戻す
   - 新しい棒に円盤を追加
   - Promiseを解決(次の移動へ)

重要な概念の解説

1. Electron の仕組み

Electron は2 つのプロセスで動作します:

メインプロセス(main.js)

  • アプリケーション全体を管理
  • ウィンドウの作成・管理
  • システムとのやり取り
  • 1 つだけ存在

レンダラープロセス(renderer.js)

  • 各ウィンドウで実行
  • HTML の操作
  • ユーザーとのやり取り
  • ウィンドウごとに存在

2. 非同期処理(async/await)

JavaScript は通常、コードを上から順番に実行しますが、アニメーションなどの時間がかかる処理を待つ必要があります。

従来の方法(コールバック):

function step1(callback) {
  setTimeout(() => {
    console.log("Step 1");
    callback();
  }, 1000);
}

function step2(callback) {
  setTimeout(() => {
    console.log("Step 2");
    callback();
  }, 1000);
}

step1(() => {
  step2(() => {
    console.log("Done");
  });
});

→ ネストが深くなって読みにくい(コールバック地獄)

async/await を使った方法:

async function step1() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Step 1");
      resolve();
    }, 1000);
  });
}

async function step2() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Step 2");
      resolve();
    }, 1000);
  });
}

async function run() {
  await step1();
  await step2();
  console.log("Done");
}

run();

→ 順番に書けて読みやすい!

3. 再帰関数

再帰関数は、自分自身を呼び出す関数です。

簡単な例: 階乗の計算

function factorial(n) {
  if (n === 0) {
    return 1; // 基底ケース(終了条件)
  }
  return n * factorial(n - 1); // 再帰呼び出し
}

factorial(5);
// 5 * factorial(4)
// 5 * 4 * factorial(3)
// 5 * 4 * 3 * factorial(2)
// 5 * 4 * 3 * 2 * factorial(1)
// 5 * 4 * 3 * 2 * 1 * factorial(0)
// 5 * 4 * 3 * 2 * 1 * 1 = 120

ハノイの塔の再帰:

function hanoi(n, source, target, auxiliary) {
  if (n > 0) {
    // 基底ケース: n=0なら何もしない
    hanoi(n - 1, source, auxiliary, target); // 小さい問題に分割
    moves.push({ disk: n, from: source, to: target });
    hanoi(n - 1, auxiliary, target, source); // 小さい問題に分割
  }
}

再帰の利点:

  • 複雑な問題を小さな問題に分割できる
  • コードが短く、理解しやすい

注意点:

  • 終了条件(基底ケース)が必要
  • 深すぎる再帰はスタックオーバーフローを起こす可能性がある

4. DOM 操作

DOM(Document Object Model)は、HTML を JavaScript から操作するための仕組みです。

要素の取得:

// IDで取得
const element = document.getElementById("myId");

// クラス名で取得
const elements = document.getElementsByClassName("myClass");

// CSSセレクタで取得
const element = document.querySelector(".myClass");
const elements = document.querySelectorAll(".myClass");

要素の作成と追加:

// 新しい要素を作成
const div = document.createElement("div");

// 属性を設定
div.id = "myDiv";
div.classList.add("myClass");
div.textContent = "Hello";

// 親要素に追加
parentElement.appendChild(div);

スタイルの変更:

element.style.color = "red";
element.style.width = "100px";
element.style.backgroundColor = "blue";

イベントリスナーの登録:

button.addEventListener("click", () => {
  console.log("Button clicked!");
});

5. CSS の Flexbox

Flexbox は、要素を柔軟に配置するためのレイアウト方法です。

.container {
  display: flex;
  justify-content: center; /* 横方向の配置 */
  align-items: center; /* 縦方向の配置 */
  flex-direction: row; /* 並べる方向 */
}

主要なプロパティ:

  • justify-content: 主軸方向の配置

    • flex-start: 先頭
    • center: 中央
    • flex-end: 末尾
    • space-between: 均等配置(両端詰め)
    • space-around: 均等配置(余白あり)
  • align-items: 交差軸方向の配置

    • flex-start: 上揃え
    • center: 中央揃え
    • flex-end: 下揃え
  • flex-direction: 並べる方向

    • row: 横並び
    • column: 縦並び

6. Web Animations API

CSS の transition や animation よりも、JavaScript から細かく制御できるアニメーション方法です。

element.animate(
  [
    { transform: "translateX(0px)" }, // 開始状態
    { transform: "translateX(100px)" }, // 終了状態
  ],
  {
    duration: 1000, // 1秒
    easing: "ease-in-out", // イージング
    fill: "forwards", // アニメーション後の状態を保持
  }
);

キーフレームの詳細制御:

element.animate(
  [
    { opacity: 0, offset: 0 }, // 0%地点
    { opacity: 1, offset: 0.5 }, // 50%地点
    { opacity: 0, offset: 1 }, // 100%地点
  ],
  {
    duration: 2000,
  }
);

初心者向けのポイント

1. コードを読む順番

初めてコードを読む時は、以下の順番がおすすめです:

  1. README.md: プロジェクトの概要を理解
  2. package.json: 使用している技術を確認
  3. index.html: 画面の構造を理解
  4. styles.css: デザインを確認
  5. main.js: アプリの起動処理を理解
  6. renderer.js: 動作ロジックを理解

2. デバッグ方法

バイブコーディングでは、問題点の確認のため、AIエージェントがコンソールログを埋め込んだり、開発者ツールでの確認ポイントを指示してくれたりします。基本的にはAIエージェントの指示に従えば大丈夫ですが、開発者ツールの利用に慣れておくことは大切だと思います。

コンソールログを使う:

console.log("変数の値:", myVariable);
console.log("ここまで実行されました");

開発者ツールを開く:

  • main.js の 17 行目のコメントを外す:
    win.webContents.openDevTools();
    
  • アプリ起動時に開発者ツールが自動で開く

ブレークポイントを使う:

  • 開発者ツールの「Sources」タブでコードを開く
  • 行番号をクリックしてブレークポイントを設定
  • その行で実行が一時停止し、変数の値を確認できる

3. よくあるエラーと対処法

エラー: Cannot read property 'xxx' of null

  • 原因: 要素が見つからない
  • 対処: ID やクラス名が正しいか確認

エラー: xxx is not a function

  • 原因: 関数が定義されていない、または変数の型が違う
  • 対処: 関数名のスペルミスを確認、変数の型を確認

アニメーションが動かない:

  • 対処: コンソールログでエラーを確認
  • 対処: CSS の transition や animation が競合していないか確認

4. 学習リソース

JavaScript:

Electron:

CSS:

アルゴリズム:


まとめ

このアプリケーションは、以下の技術を組み合わせて作られています:

  • Electron: デスクトップアプリケーションフレームワーク
  • HTML: 画面の構造
  • CSS: デザインとレイアウト
  • JavaScript: 動作ロジック
  • 再帰アルゴリズム: ハノイの塔の解法
  • Web Animations API: スムーズなアニメーション

各ファイルが役割を分担し、協力して動作しています:

main.js        → アプリの起動とウィンドウ管理
index.html     → 画面の構造定義
styles.css     → デザインとレイアウト
renderer.js    → ユーザー操作とアニメーション

最後まで読んで頂き、ありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?