📚 目次
はじめに
この記事は、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();
}
詳細解説:
-
既存の円盤をクリア:
Object.values(pegs).forEach((peg) => (peg.innerHTML = ""));-
Object.values(pegs): pegs オブジェクトの値(A、B、C の要素)を配列として取得 -
forEach(...): 配列の各要素に対して処理を実行 -
peg.innerHTML = '': 中身を空にする
-
-
円盤を作成(大きい順に):
for (let i = count; i >= 1; i--) {-
forループ: count から 1 まで逆順に繰り返す - 例: count=3 の場合、i=3, 2, 1 の順に実行
-
-
円盤要素の作成:
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)
-
-
幅の計算:
const width = 40 + (140 / (count - 1 || 1)) * (i - 1);- 最小幅: 40px(i=1 の時)
- 最大幅: 180px(i=count の時)
- 等間隔で幅が増加するように計算
-
色の設定(HSL 色空間):
const hue = 260 + i * 15; disk.style.backgroundColor = `hsl(${hue}, 70%, 60%)`;-
hsl(色相, 彩度, 明度): 色を指定する方法の一つ - 色相を変えることで、紫から青へのグラデーションを作成
-
-
位置の設定:
disk.style.bottom = `${(count - i) * 22}px`;- 下から積み上げるように配置
- 各円盤の高さ 20px + 隙間 2px = 22px
-
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 に移動する場合:
- 上の n-1 枚を A から B(補助)に移動
- 一番大きい円盤を A から C に移動
- 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!";
}
詳細解説:
-
async 関数:
async function playMoves() {- 非同期処理を扱う関数
-
awaitを使って、アニメーションの完了を待つことができる
-
UI の無効化:
startBtn.disabled = true; diskCountInput.disabled = true;- 解決中は操作できないようにする
-
移動を順番に実行:
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();
};
});
}
詳細解説:
-
Promise を返す:
return new Promise(resolve => {- アニメーションが完了したら
resolve()を呼ぶ -
awaitで待つことができるようになる
- アニメーションが完了したら
-
要素の取得:
const disk = document.getElementById(`disk-${move.disk}`);- 移動する円盤を取得
-
位置情報の取得:
const startRect = disk.getBoundingClientRect();-
getBoundingClientRect(): 要素の画面上の位置とサイズを取得
-
-
固定位置指定:
disk.style.position = "fixed";- 親要素から独立して、画面上の絶対位置で配置
- これにより、異なる棒の間をスムーズに移動できる
-
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 段階のアニメーション:
- 開始位置
- 上に持ち上げる
- 横に移動
- 下に下ろす
-
offset: アニメーションのタイミング(0.0〜1.0) -
easing: 'ease-in-out': 滑らかな加速・減速
-
4 段階のアニメーション:
-
アニメーション完了時の処理:
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();
});
詳細解説:
- 円盤の数を取得
- 移動配列をクリア
- 円盤を初期化
- ハノイアルゴリズムで移動手順を計算
- アニメーションを開始
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. コードを読む順番
初めてコードを読む時は、以下の順番がおすすめです:
- README.md: プロジェクトの概要を理解
- package.json: 使用している技術を確認
- index.html: 画面の構造を理解
- styles.css: デザインを確認
- main.js: アプリの起動処理を理解
- 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:
- MDN CSS リファレンス
- Flexbox Froggy(Flexbox を学ぶゲーム)
アルゴリズム:
まとめ
このアプリケーションは、以下の技術を組み合わせて作られています:
- Electron: デスクトップアプリケーションフレームワーク
- HTML: 画面の構造
- CSS: デザインとレイアウト
- JavaScript: 動作ロジック
- 再帰アルゴリズム: ハノイの塔の解法
- Web Animations API: スムーズなアニメーション
各ファイルが役割を分担し、協力して動作しています:
main.js → アプリの起動とウィンドウ管理
index.html → 画面の構造定義
styles.css → デザインとレイアウト
renderer.js → ユーザー操作とアニメーション
最後まで読んで頂き、ありがとうございました。