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, () => {