概要
CoffeeScriptに慣れるための習作。
ライフゲームの世界をLifeWorldクラスで構築し、パターンのセットや描画を行わせている。表示に関してはcanvasを利用している。
スマートフォン用はこちら
デモンストレーション
HTML内容
bodyブロックの構成は以下の通り。
- 初期パターン選択
- サイズ選択
- 時間間隔選択
- 開始/停止ボタン
- 表示領域
初期パターン選択
規定パターンの中から、ドロップダウンリストで初期パターンを選択。この要素の選択状態が変化した場合の処理は、スクリプト側でjQueryを用いてコールバックを定義している。
|
<p> <label>初期パターン:</label> <select id="pattern"> <option value="glider">Glider</option> <option value="spaceshipL">Lightweight Spaceship</option> <option value="spaceshipM">Middleweight Spaceship</option> <option value="spaceshipH">Heavyweight Spaceship</option> <option value="octagon">Octagon</option> <option value="galaxy">Galaxy</option> <option value="pulser">Pulser</option> <option value="pentadecathron">Pentadecathron</option> <option value="dieHard">Die Hard</option> <option value="random">Random</option> </select> </p> |
サイズと表示間隔の選択
それぞれラジオボタン群で選択させる。これらの要素の選択状態が変化した場合の処理も、スクリプト側でjQueryを用いてコールバックを定義している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
<p> <label>サイズ:</label> <label><input name="size" type="radio" value="10" />10</label> <label><input checked="checked" name="size" type="radio" value="15" />15</label> <label><input name="size" type="radio" value="20" />20</label> <label><input name="size" type="radio" value="30" />30</label> <label><input name="size" type="radio" value="40" />40</label> </p> <p> <label>表示間隔:</label> <label><input name="interval" type="radio" value="100" />0.1sec</label> <label><input name="interval" type="radio" value="200" />0.2sec</label> <label><input checked="checked" name="interval" type="radio" value="500" />0.5sec</label> <label><input name="interval" type="radio" value="1000" />1.0sec</label> <label><input name="interval" type="radio" value="1500" />1.5sec</label> </p> |
開始/停止ボタン
ボタンが押されるた時の処理はスクリプト側でjQueryを用いてコールバックを定義しており、その時の状態によって停止と起動を繰り返す。
|
<input id="btn_start_stop" type="button" value="Start/Stop" /> |
描画領域
HTMLのcanvasを設置。
|
<canvas id="canvas" width="399" height="399"> |
CoffeeScriptコード
初期パターン
初期パターンを配列にセットしている。たとえば「グライダー」の場合は以下のように定義している。
|
@patternGrider = [ [1, 1, 1] [1, 0, 0] [0, 1, 0] ] |
本体
本体コードは、LifeWorldクラスとグローバル関数の定義からなる。
ブラウザのボタン操作なや状態変化などに対応するコールバックは、jQueryを利用して実装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
# LifeWorldクラス # Lifegameのグリッドを保持し、初期設定や世代交代の操作をし、アニメーションを描画 class LifeWorld # コンストラクタ constructor: (canvas)-> # 指定サイズで、0で埋められたグリッドを生成 create: (row, col)-> # 表示間隔設定 setDisplayInterval: (@interval)-> # 初期状態を設定 setPattern: (patternString)-> # gridの状態を表示 draw: -> # 次の世代の状態を計算 nextGeneration: -> # グリッドと位置を指定して生死を返すメソッド getNewStatusAt: (grid, row, col)-> # タイマ処理に渡すメソッド animate: -> # アニメーションの開始/停止 startAndStop: -> # アニメーションの停止 stop: -> # 以下はグローバル関数 # ブラウザからのイベントによって起動され、LifeWorldクラスをコントロールする # 初期化関数 getNewLifeWorld = -> # 以下は初期動作 # ブラウザから読み込まれたときに一回だけ作動する。 jQuery ($) -> |
初期動作
ブラウザからこのスクリプトが呼び出されたときに一回だけ動作。
LifeWorldの初期化や、コールバックの設定を行う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
jQuery ($) -> lifeWorld = getNewLifeWorld() lifeWorld.draw() $("#pattern").change -> lifeWorld.stop() lifeWorld = getNewLifeWorld() lifeWorld.draw() return $("input[name='size']:radio").change -> lifeWorld.stop() lifeWorld = getNewLifeWorld() lifeWorld.draw() return $("input[name='interval']:radio").change -> lifeWorld.stop() lifeWorld = getNewLifeWorld() lifeWorld.draw() return $("#btn_start_stop").click -> lifeWorld.startAndStop() return |
グローバル関数
getNewLifeWorld()
LifeWorldの新しい状態を設定する際に呼ばれる関数。
ブラウザの部品から直接は呼ばれないため、windows.の要素とはしていないが、グローバルスコープの関数。
|
getNewLifeWorld = -> # 初期パターン、サイズ、表示間隔の取得・設定 initialPattern = $("#pattern").val() gridSize = parseInt($("input[name='size']:checked").val(), 10) interval= parseInt($("input[name='interval']:checked").val(), 10) canvas = $("#canvas")[0] lifeWorld = new LifeWorld(canvas) lifeWorld.create(gridSize, gridSize) lifeWorld.setDisplayInterval(interval) lifeWorld.setPattern(initialPattern) return lifeWorld |
LifeWorldクラス
constructor(canvas)
LifeWorldクラスのコンストラクタ。
canvasを引数にしてインスタンスを生成するが、この段階ではグリッドも生成されていない。他のパラメータについては個別に設定することを意図している。
|
constructor: (canvas)-> @row = 0 @col = 0 @grid = [] @canvas = canvas @isRunning = false @timer = null @interval = 1000 |
create(row, col)
行数と列数を指定してインスタンスのグリッドを構築する。グリッドの全要素が0で埋められている。
|
create: (row, col)-> @row = row @col = col @grid = ((0 for c in [0..@col+1]) for r in [0..@row+1]) return |
setDisplayInterval(interval)
動画フレームの時間間隔を設定。
CoffeeScript特有の、引数を直接パラメータに代入する形式。
|
# 表示間隔設定 setDisplayInterval: (@interval)-> |
setPattern(patternString)
選択されているラジオボタンの文字により、あらかじめ定義された初期パターンを登録。
初期パターンがグリッドの中央付近に来るように調整している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
setPattern: (patternString)-> switch patternString when "glider" then pattern = patternGrider when "spaceshipL" then pattern = patternSpaceshipL when "spaceshipM" then pattern = patternSpaceshipM when "spaceshipH" then pattern = patternSpaceshipH when "octagon" then pattern = patternOctagon when "galaxy" then pattern = patternGalaxy when "pulser" then pattern = patternPulser when "pentadecathron" then pattern = patternPentadecathron when "dieHard" then pattern = patternDieHard when "random" for r in [1..@row] for c in [1..@col] if Math.random() > 0.5 then @grid[r][c] = 1 return @grid[r][c] = 0 for r in [0...@grid.length] for c in [0...@grid[0].length] pivotR = Math.floor((@grid.length - pattern.length) / 2) pivotC = Math.floor((@grid[0].length - pattern[0].length) / 2) for r in [0...pattern.length] for c in [0...pattern[0].length] @grid[pivotR + r][pivotC + c] = pattern[r][c] return |
draw()
グリッドの状態に応じてcanvasのコンテキストに描画。格子線も描いている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
draw: -> w = @canvas.width h = @canvas.height ctx = @canvas.getContext("2d") ctx.fillStyle = "#000000" ctx.fillRect(0, 0, w, h) cellSize = (h + 1) / @row ctx.strokeStyle = "#808080" ctx.beginPath() for r in [0...@row] ctx.moveTo(r*cellSize, 0) ctx.lineTo(r*cellSize, w) for c in [0...@col] ctx.moveTo(0, c*cellSize) ctx.lineTo(h, c*cellSize) ctx.stroke() ctx.fillStyle = "#ffffff" ctx.strokeStyle = "#404040" y = 0 for r in [1..@row] x = 0 for c in [1..@col] if @grid[r][c] == 1 ctx.fillRect(x, y, cellSize, cellSize) ctx.strokeRect(x, y, cellSize, cellSize) x += cellSize y += cellSize return |
nextGeneration()
次のgetNewStatusAt()メソッドを使いながら、次の世代のグリッドの状態を形成。
新しい世代用の一時的なグリッドを生成している。
|
nextGeneration: -> # grid->cloneへの複製 clone = ((x for x in rowArray) for rowArray in @grid) # 各セルの次の世代の生死を設定 for r in [1..@row] for c in [1..@col] clone[r][c] = @getNewStatusAt(@grid, r, c) # clone->gridへの複製 @grid = ((x for x in rowArray) for rowArray in clone) return |
getNewStatusAt(grid, row, col)
現在の状態から、次の世代のグリッドの状態を算出。
自分のマスの周囲8つのマスの存在状態から、次の世代の誕生/存続/死滅を決定。
周囲の生存数 |
現状=存在 |
現状=不在 |
0 |
死滅 |
不在 |
1~2 |
存続 |
不在 |
3 |
存続 |
誕生 |
4以上 |
死滅 |
不在 |
|
getNewStatusAt: (grid, row, col)-> count = 0 count += grid[r][c] for c in [col-1..col+1] for r in [row-1..row+1] count -= grid[row][col] switch count when 0 then status = 0 when 1 then status = 0 when 2 then status = grid[row][col] when 3 then status = 1 else status = 0 return status |
animate()
setInterval()メソッドに登録するための、ワンフレームの画像を描くメソッド。
|
animate: -> @nextGeneration() @draw() return |
startAndStop() / stop()
startAndStop()は、現在動画を実行中かどうかのフラグをチェックし、停止していればsetInterval()を用いて起動、起動中ならclearInterval()停止を行う。
stop()は、状態によらず停止処理。
|
# アニメーションの開始/停止 startAndStop: -> if @isRunning clearInterval(@timer) @isRunning = false else @timer = setInterval(@animate.bind(this), @interval) @isRunning = true return # アニメーションの停止 stop: -> if @isRunning clearInterval(@timer) return |