diff --git a/src/app.ts b/src/app.ts
new file mode 100644
index 0000000..d34e767
--- /dev/null
+++ b/src/app.ts
@@ -0,0 +1,125 @@
+import * as BABYLON from "babylonjs";
+import { BitGrid, BitWorld } from "@ca-ts/algo/bit";
+import { $autoRandom } from "./bind";
+import { createTemplateCell } from "./lib/cell";
+import { setRLE } from "./lib/setRLE";
+
+const WORLD_SIZE = 32 * 2;
+const stackHeight = 1;
+
+export class App {
+  private prevGrid: BitGrid | null = null;
+  private prevPrevGrid: BitGrid | null = null;
+  private bitWorld = BitWorld.make({ width: WORLD_SIZE, height: WORLD_SIZE });
+  private generation = 0;
+  public historySize = 16;
+  private templateMesh: BABYLON.Mesh;
+  private cellMaterial: BABYLON.StandardMaterial;
+  private cellMeshes: BABYLON.InstancedMesh[][] = [];
+
+  constructor(
+    private engine: BABYLON.Engine,
+    private scene: BABYLON.Scene,
+    private camera: BABYLON.ArcRotateCamera,
+    private pointLight: BABYLON.PointLight
+  ) {
+    this.bitWorld.random();
+    this.initCamera();
+    const { templateCell, cellMaterial } = createTemplateCell(scene);
+    this.templateMesh = templateCell;
+    this.cellMaterial = cellMaterial;
+  }
+
+  private initCamera() {
+    const worldCenterX = this.bitWorld.getWidth() / 2;
+    const worldCenterY = this.bitWorld.getHeight() / 2;
+    // BitWorldの中心を計算
+    this.camera.target = new BABYLON.Vector3(worldCenterX, 0, worldCenterY);
+  }
+
+  setSize(size: number) {
+    this.generation = 0;
+    this.camera.target.y = 0;
+    this.pointLight.position.y = 0;
+    this.clearCell();
+    this.bitWorld = BitWorld.make({ width: size, height: size });
+    this.initCamera();
+    this.random();
+  }
+
+  updateWorld() {
+    this.prevPrevGrid = this.prevGrid;
+    this.prevGrid = this.bitWorld.bitGrid.clone();
+    this.bitWorld.next();
+
+    if (
+      $autoRandom.checked &&
+      this.prevPrevGrid &&
+      this.bitWorld.bitGrid.equal(this.prevPrevGrid)
+    ) {
+      this.clearCell();
+      this.bitWorld.random();
+    }
+
+    const newCells: BABYLON.InstancedMesh[] = [];
+
+    // 新しい世代のセルを表示
+    this.bitWorld.forEach((x, y, alive) => {
+      if (alive === 1) {
+        // セルのインスタンスを作成
+        const instance = this.templateMesh.createInstance(`cell_${x}_${y}`);
+        instance.position = new BABYLON.Vector3(
+          x,
+          this.generation * stackHeight,
+          y
+        );
+
+        // マテリアルや色はテンプレートセルと共有されるので追加設定不要
+        newCells.push(instance);
+        // instance.scaling = new Vector3(0.5, 0.5, 0.5);
+      }
+    });
+
+    this.cellMeshes.push(newCells);
+
+    // 古い世代のセルを削除
+    if (this.cellMeshes.length >= this.historySize) {
+      const old = this.cellMeshes.slice(
+        0,
+        this.cellMeshes.length - this.historySize
+      );
+      old.forEach((a) => {
+        a.forEach((c) => c.dispose());
+      });
+      this.cellMeshes = this.cellMeshes.slice(
+        this.cellMeshes.length - this.historySize
+      );
+    }
+    // カメラを移動
+    this.camera.target.y += stackHeight;
+    // 点光源の位置を移動させる
+    this.pointLight.position.y += stackHeight;
+    this.generation++;
+  }
+
+  clearCell() {
+    this.cellMeshes.forEach((a) => {
+      a.forEach((c) => c.dispose());
+    });
+    this.cellMeshes = [];
+    this.bitWorld.clear();
+  }
+
+  random() {
+    this.clearCell();
+    this.bitWorld.random();
+  }
+
+  setRLE(rle: string) {
+    setRLE(this.bitWorld, rle);
+  }
+
+  setCellColor(colorHex: string) {
+    this.cellMaterial.diffuseColor = BABYLON.Color3.FromHexString(colorHex);
+  }
+}
diff --git a/src/bind.ts b/src/bind.ts
index 4f22ab9..c5a54ef 100644
--- a/src/bind.ts
+++ b/src/bind.ts
@@ -2,33 +2,39 @@ export const $canvas = document.querySelector(
   "#renderCanvas"
 ) as HTMLCanvasElement;
 
-export const $autoRandom = document.querySelector(
-  "#auto-random"
-) as HTMLInputElement;
-export const $fullScreen = document.querySelector(
-  "#full-screen"
+export const $configButton = document.querySelector(
+  "#configButton"
 ) as HTMLElement;
-export const $colorInput = document.querySelector("#color") as HTMLInputElement;
 
+// dialog
 export const $settingsDialog = document.querySelector(
   "#settingsDialog"
 ) as HTMLDialogElement;
-export const $historySizeInput = document.querySelector(
-  "#historySize"
-) as HTMLInputElement;
 
 export const $closeSettings = document.querySelector(
   "#closeSettings"
 ) as HTMLElement;
 
-export const $configButton = document.querySelector(
-  "#configButton"
-) as HTMLElement;
+export const $historySizeInput = document.querySelector(
+  "#historySize"
+) as HTMLInputElement;
+export const $colorInput = document.querySelector("#color") as HTMLInputElement;
 
-export const $readRLE = document.querySelector("#readRLE") as HTMLButtonElement;
-export const $rleErrorMessage = document.querySelector(
-  "#rleError"
+export const $autoRandom = document.querySelector(
+  "#auto-random"
+) as HTMLInputElement;
+
+export const $autoRotate = document.querySelector(
+  "#auto-rotate"
+) as HTMLInputElement;
+export const $fullScreen = document.querySelector(
+  "#full-screen"
 ) as HTMLElement;
+
 export const $inputRLE = document.querySelector(
   "#inputRLE"
 ) as HTMLTextAreaElement;
+export const $rleErrorMessage = document.querySelector(
+  "#rleError"
+) as HTMLElement;
+export const $readRLE = document.querySelector("#readRLE") as HTMLButtonElement;
diff --git a/src/camera.ts b/src/lib/camera.ts
similarity index 100%
rename from src/camera.ts
rename to src/lib/camera.ts
diff --git a/src/cell.ts b/src/lib/cell.ts
similarity index 100%
rename from src/cell.ts
rename to src/lib/cell.ts
diff --git a/src/setRLE.ts b/src/lib/setRLE.ts
similarity index 100%
rename from src/setRLE.ts
rename to src/lib/setRLE.ts
diff --git a/src/settings.ts b/src/lib/settings.ts
similarity index 100%
rename from src/settings.ts
rename to src/lib/settings.ts
diff --git a/src/main.ts b/src/main.ts
index 20d02b1..6380543 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,13 +1,11 @@
 import "./style.css";
 import * as BABYLON from "babylonjs";
 import { Engine, Vector3 } from "babylonjs";
-import { BitGrid, BitWorld } from "@ca-ts/algo/bit";
-import { setupArcRotateCamera } from "./camera";
-import { createTemplateCell } from "./cell";
-import { setRLE } from "./setRLE";
-import { setupFullScreenButton } from "./settings";
+import { setupArcRotateCamera } from "./lib/camera";
+import { setupFullScreenButton } from "./lib/settings";
 import {
   $autoRandom,
+  $autoRotate,
   $canvas,
   $closeSettings,
   $colorInput,
@@ -19,9 +17,7 @@ import {
   $rleErrorMessage,
   $settingsDialog,
 } from "./bind";
-
-const WORLD_SIZE = 32 * 2;
-let historySize = 16;
+import { App } from "./app";
 
 const engine = new Engine($canvas, true);
 const scene = new BABYLON.Scene(engine);
@@ -32,12 +28,6 @@ scene.clearColor = new BABYLON.Color4(0, 0, 0, 1);
 // ArcRotateCameraをBitWorldの中心に設定
 const camera = setupArcRotateCamera(scene, $canvas);
 
-let prevGrid: BitGrid | null = null;
-let prevPrevGrid: BitGrid | null = null;
-
-// 基本的なライトを追加
-new BABYLON.HemisphericLight("light1", new Vector3(0, 1, 0), scene);
-
 const pointLight = new BABYLON.PointLight(
   "pointLight",
   new BABYLON.Vector3(-100, 100, 0), // ライトの初期位置
@@ -47,72 +37,16 @@ pointLight.intensity = 0.3; // 光の強さ
 pointLight.diffuse = new BABYLON.Color3(0.8, 1, 1); // 光の色
 pointLight.specular = new BABYLON.Color3(1, 1, 1); // 反射光の色
 
-// BitWorldを作成
-const bitWorld = BitWorld.make({ width: WORLD_SIZE, height: WORLD_SIZE });
-bitWorld.random();
-
-// BitWorldの中心を計算
-const worldCenterX = bitWorld.getWidth() / 2;
-const worldCenterY = bitWorld.getHeight() / 2;
-camera.target = new Vector3(worldCenterX, 0, worldCenterY);
-
-let generation = 0;
-let cellMeshes: BABYLON.InstancedMesh[][] = [];
-const stackHeight = 1;
-
-const { templateCell, cellMaterial } = createTemplateCell(scene);
-
-function updateWorld() {
-  prevPrevGrid = prevGrid;
-  prevGrid = bitWorld.bitGrid.clone();
-  bitWorld.next();
-
-  if (
-    $autoRandom.checked &&
-    prevPrevGrid &&
-    bitWorld.bitGrid.equal(prevPrevGrid)
-  ) {
-    clearCell();
-    bitWorld.random();
-  }
+const app = new App(engine, scene, camera, pointLight);
+
+// 基本的なライトを追加
+new BABYLON.HemisphericLight("light1", new Vector3(0, 1, 0), scene);
 
-  const newCells: BABYLON.InstancedMesh[] = [];
-
-  // 新しい世代のセルを表示
-  bitWorld.forEach((x, y, alive) => {
-    if (alive === 1) {
-      // セルのインスタンスを作成
-      const instance = templateCell.createInstance(`cell_${x}_${y}`);
-      instance.position = new Vector3(x, generation * stackHeight, y);
-
-      // マテリアルや色はテンプレートセルと共有されるので追加設定不要
-      newCells.push(instance);
-      // instance.scaling = new Vector3(0.5, 0.5, 0.5);
-    }
-  });
-
-  cellMeshes.push(newCells);
-
-  // 古い世代のセルを削除
-  if (cellMeshes.length >= historySize) {
-    const old = cellMeshes.slice(0, cellMeshes.length - historySize);
-    old.forEach((a) => {
-      a.forEach((c) => c.dispose());
-    });
-    cellMeshes = cellMeshes.slice(cellMeshes.length - historySize);
-  }
-  // 点光源の位置を移動させる
-  pointLight.position.y += stackHeight;
-  // カメラを移動
-  camera.target.y += stackHeight;
-  generation++;
-}
-const autoRotate = document.querySelector("#auto-rotate") as HTMLInputElement;
 let running = true;
 let i = 0;
 
 engine.runRenderLoop(() => {
-  if (autoRotate.checked) {
+  if ($autoRotate.checked) {
     camera.alpha += scene.deltaTime / 4000;
   }
   scene.render();
@@ -123,7 +57,7 @@ engine.runRenderLoop(() => {
 
   i++;
   if (i % INTERVAL === 0) {
-    updateWorld();
+    app.updateWorld();
   }
 });
 
@@ -131,14 +65,6 @@ window.addEventListener("resize", () => {
   engine.resize();
 });
 
-function clearCell() {
-  cellMeshes.forEach((a) => {
-    a.forEach((c) => c.dispose());
-  });
-  cellMeshes = [];
-  bitWorld.clear();
-}
-
 document.addEventListener("keydown", (e) => {
   if (e.isComposing) {
     return;
@@ -147,20 +73,19 @@ document.addEventListener("keydown", (e) => {
     running = !running;
   }
   if (e.key === "r") {
-    clearCell();
-    bitWorld.random();
+    app.random();
   }
 });
 
 // ダイアログ制御
 $historySizeInput.addEventListener("input", () => {
-  historySize = parseInt($historySizeInput.value, 10);
-  if (Number.isNaN(historySize)) {
+  let historySize = parseInt($historySizeInput.value, 10);
+  if (Number.isNaN(app.historySize)) {
     historySize = 1;
   }
-  historySize = Math.min(Math.max(historySize, 1), 500);
+  app.historySize = Math.min(Math.max(historySize, 1), 500);
 });
-$historySizeInput.value = historySize.toString();
+$historySizeInput.value = app.historySize.toString();
 
 $closeSettings.addEventListener("click", () => {
   $settingsDialog.close();
@@ -186,19 +111,20 @@ $inputRLE.addEventListener("input", () => {
 
 $readRLE.addEventListener("click", () => {
   $autoRandom.checked = false;
-  clearCell();
+  app.clearCell();
   $rleErrorMessage.textContent = null;
   try {
-    setRLE(bitWorld, $inputRLE.value);
+    app.setRLE($inputRLE.value);
   } catch (error) {
     $rleErrorMessage.textContent = "Invalid RLE or oversized";
+    app.clearCell();
     throw error;
   }
   $settingsDialog.close();
 });
 
 $colorInput.addEventListener("input", () => {
-  cellMaterial.diffuseColor = BABYLON.Color3.FromHexString($colorInput.value);
+  app.setCellColor($colorInput.value);
 });
 
 setupFullScreenButton($fullScreen, () => {